Skip to content

Commit 751e9ce

Browse files
committed
feat: 优化CanvasComponent组件,增强可视化效果
1 parent 10a8e38 commit 751e9ce

File tree

1 file changed

+226
-25
lines changed

1 file changed

+226
-25
lines changed

src/components/CanvasComponent.tsx

Lines changed: 226 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
5454
svg.selectAll('*').remove(); // 清空画布
5555

5656
// 设置预留边距
57-
const margin = { top: 30, right: 30, bottom: 30, left: 30 };
57+
const margin = { top: 80, right: 30, bottom: 30, left: 30 };
5858
const innerWidth = dimensions.width - margin.left - margin.right;
5959
const innerHeight = dimensions.height - margin.top - margin.bottom;
6060

@@ -66,7 +66,9 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
6666
const nodeGroup = g.append('g').attr('class', 'nodes');
6767
const linkGroup = g.append('g').attr('class', 'links');
6868
const textGroup = g.append('g').attr('class', 'texts');
69+
const labelGroup = g.append('g').attr('class', 'labels'); // 新增:节点标签图层
6970
const formulaGroup = g.append('g').attr('class', 'formula');
71+
const stepDescGroup = svg.append('g').attr('class', 'step-description'); // 新增:步骤说明图层
7072

7173
// 计算节点的缩放比例
7274
const maxX = Math.max(...state.staircase.nodes.map(n => n.x)) || 100;
@@ -78,20 +80,75 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
7880
const mapX = (x: number) => x * scaleX;
7981
const mapY = (y: number) => y * scaleY;
8082

81-
// 绘制连接线
82-
linkGroup.selectAll('line')
83+
// 添加步骤描述背景和文本 - 新增内容
84+
const stepDesc = state.timeline[state.currentStep]?.description || "";
85+
const stepBackground = stepDescGroup.append('rect')
86+
.attr('x', 0)
87+
.attr('y', 10)
88+
.attr('width', dimensions.width)
89+
.attr('height', 50)
90+
.attr('fill', '#f0f8ff')
91+
.attr('rx', 5)
92+
.attr('ry', 5)
93+
.attr('stroke', '#2196F3')
94+
.attr('stroke-width', 1)
95+
.attr('opacity', 0.9);
96+
97+
// 添加步骤计数器 - 新增内容
98+
stepDescGroup.append('text')
99+
.attr('x', 20)
100+
.attr('y', 35)
101+
.attr('font-weight', 'bold')
102+
.attr('fill', '#2196F3')
103+
.text(`步骤 ${state.currentStep + 1}/${state.totalSteps}`);
104+
105+
// 添加步骤描述文本 - 新增内容
106+
stepDescGroup.append('text')
107+
.attr('x', 120)
108+
.attr('y', 35)
109+
.attr('fill', '#333')
110+
.attr('font-size', '14px')
111+
.text(stepDesc);
112+
113+
// 绘制连接线 - 修改为箭头
114+
linkGroup.selectAll('path')
83115
.data(state.staircase.links)
84116
.enter()
85-
.append('line')
86-
.attr('x1', (d: LinkData) => mapX(state.staircase.nodes[d.source].x))
87-
.attr('y1', (d: LinkData) => mapY(state.staircase.nodes[d.source].y))
88-
.attr('x2', (d: LinkData) => mapX(state.staircase.nodes[d.target].x))
89-
.attr('y2', (d: LinkData) => mapY(state.staircase.nodes[d.target].y))
117+
.append('path')
118+
.attr('d', (d: LinkData) => {
119+
const sourceX = mapX(state.staircase.nodes[d.source].x);
120+
const sourceY = mapY(state.staircase.nodes[d.source].y);
121+
const targetX = mapX(state.staircase.nodes[d.target].x);
122+
const targetY = mapY(state.staircase.nodes[d.target].y);
123+
124+
// 计算箭头方向和弯曲度
125+
const dx = targetX - sourceX;
126+
const dy = targetY - sourceY;
127+
const dr = Math.sqrt(dx * dx + dy * dy) * 1.5; // 弯曲程度
128+
129+
// 使用弧线路径
130+
return `M${sourceX},${sourceY}A${dr},${dr} 0 0,1 ${targetX},${targetY}`;
131+
})
132+
.attr('fill', 'none')
90133
.attr('stroke', '#666')
91-
.attr('stroke-width', 2);
134+
.attr('stroke-width', 2)
135+
.attr('marker-end', 'url(#arrowhead)'); // 使用箭头标记
136+
137+
// 添加箭头标记定义 - 新增内容
138+
svg.append('defs').append('marker')
139+
.attr('id', 'arrowhead')
140+
.attr('viewBox', '0 -5 10 10')
141+
.attr('refX', 8)
142+
.attr('refY', 0)
143+
.attr('orient', 'auto')
144+
.attr('markerWidth', 6)
145+
.attr('markerHeight', 6)
146+
.append('path')
147+
.attr('d', 'M0,-5L10,0L0,5')
148+
.attr('fill', '#666');
92149

93150
// 绘制节点
94-
const nodeColor = getColorByAlgorithm(state.currentAlgorithm);
151+
const nodeColor = getNodeColor; // 修改为根据节点状态返回颜色的函数
95152
const nodeRadius = Math.min(20, Math.max(15, Math.min(innerWidth, innerHeight) / 30));
96153

97154
nodeGroup.selectAll('circle')
@@ -101,9 +158,22 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
101158
.attr('cx', (d: NodeData) => mapX(d.x))
102159
.attr('cy', (d: NodeData) => mapY(d.y))
103160
.attr('r', nodeRadius)
104-
.attr('fill', nodeColor)
161+
.attr('fill', (d: NodeData, i: number) => {
162+
// 当前步骤相关节点高亮
163+
if (state.timeline[state.currentStep]?.visualChanges.nodeUpdates.some(update => update.index === i)) {
164+
return '#FF5722'; // 高亮颜色 - 橙色
165+
}
166+
return nodeColor(state.currentAlgorithm, i, state.currentStep);
167+
})
105168
.attr('stroke', '#333')
106-
.attr('stroke-width', 2);
169+
.attr('stroke-width', 2)
170+
.attr('class', (d: NodeData, i: number) => {
171+
// 为当前更新的节点添加类名,便于添加动画效果
172+
if (state.timeline[state.currentStep]?.visualChanges.nodeUpdates.some(update => update.index === i)) {
173+
return 'node-highlight';
174+
}
175+
return '';
176+
});
107177

108178
// 添加节点值文本
109179
textGroup.selectAll('text')
@@ -117,35 +187,129 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
117187
.attr('font-weight', 'bold')
118188
.text((d: NodeData) => d.value);
119189

120-
// 渲染公式
190+
// 添加节点标签 - 新增内容
191+
labelGroup.selectAll('text')
192+
.data(state.staircase.nodes)
193+
.enter()
194+
.append('text')
195+
.attr('x', (d: NodeData) => mapX(d.x))
196+
.attr('y', (d: NodeData) => mapY(d.y) - nodeRadius - 5)
197+
.attr('text-anchor', 'middle')
198+
.attr('fill', '#333')
199+
.attr('font-size', '12px')
200+
.text((d: NodeData, i: number) => `f(${i})`);
201+
202+
// 添加当前计算节点的说明气泡 - 新增内容
203+
state.timeline[state.currentStep]?.visualChanges.nodeUpdates.forEach(update => {
204+
if (update.index >= 0 && update.index < state.staircase.nodes.length) {
205+
const node = state.staircase.nodes[update.index];
206+
const bubbleWidth = 120;
207+
const bubbleHeight = 30;
208+
209+
// 绘制说明气泡背景
210+
g.append('rect')
211+
.attr('x', mapX(node.x) - bubbleWidth/2)
212+
.attr('y', mapY(node.y) - nodeRadius - bubbleHeight - 15)
213+
.attr('width', bubbleWidth)
214+
.attr('height', bubbleHeight)
215+
.attr('rx', 10)
216+
.attr('ry', 10)
217+
.attr('fill', '#FFECB3')
218+
.attr('stroke', '#FFC107')
219+
.attr('stroke-width', 1)
220+
.attr('opacity', 0.9);
221+
222+
// 绘制连接线
223+
g.append('path')
224+
.attr('d', `M${mapX(node.x)},${mapY(node.y) - nodeRadius - 15} L${mapX(node.x)},${mapY(node.y) - nodeRadius - 5}`)
225+
.attr('stroke', '#FFC107')
226+
.attr('stroke-width', 1);
227+
228+
// 添加说明文字
229+
g.append('text')
230+
.attr('x', mapX(node.x))
231+
.attr('y', mapY(node.y) - nodeRadius - 30)
232+
.attr('text-anchor', 'middle')
233+
.attr('fill', '#333')
234+
.attr('font-size', '11px')
235+
.text(`计算第 ${update.index} 阶方法数`);
236+
}
237+
});
238+
239+
// 渲染公式 - 增强显示
121240
if (state.formula) {
241+
const formulaBackground = formulaGroup.append('rect')
242+
.attr('x', innerWidth - 220)
243+
.attr('y', -20)
244+
.attr('width', 200)
245+
.attr('height', 40)
246+
.attr('rx', 5)
247+
.attr('ry', 5)
248+
.attr('fill', '#E3F2FD')
249+
.attr('stroke', '#64B5F6')
250+
.attr('stroke-width', 1);
251+
122252
formulaGroup.append('text')
123-
.attr('x', innerWidth - 20)
124-
.attr('y', 20)
125-
.attr('text-anchor', 'end')
253+
.attr('x', innerWidth - 120)
254+
.attr('y', 10)
255+
.attr('text-anchor', 'middle')
126256
.attr('font-family', 'serif')
127-
.attr('font-size', `${Math.min(14, Math.max(10, innerWidth / 40))}px`)
257+
.attr('font-size', `${Math.min(16, Math.max(12, innerWidth / 40))}px`)
258+
.attr('font-weight', 'bold')
128259
.text(state.formula);
260+
261+
// 添加公式说明
262+
formulaGroup.append('text')
263+
.attr('x', innerWidth - 120)
264+
.attr('y', 30)
265+
.attr('text-anchor', 'middle')
266+
.attr('font-size', '11px')
267+
.attr('fill', '#666')
268+
.text('状态转移方程');
129269
}
130270

271+
// 为动画添加CSS动画效果
272+
svg.selectAll('.node-highlight')
273+
.style('animation', 'pulse 1.5s infinite');
274+
131275
// 渲染矩阵(仅当使用矩阵算法时)
132276
if (state.currentAlgorithm === 'matrix' && state.matrix.length > 0) {
133277
renderMatrix(g, state.matrix, innerWidth, innerHeight);
134278
}
135279

136-
}, [state.staircase, state.currentAlgorithm, state.formula, state.matrix, dimensions]);
280+
}, [state.staircase, state.currentAlgorithm, state.formula, state.matrix, dimensions, state.currentStep, state.timeline, state.totalSteps]);
137281

138-
// 根据算法类型获取颜色
139-
const getColorByAlgorithm = (algorithm: AnimationState['currentAlgorithm']): string => {
282+
// 根据算法类型和节点索引获取颜色
283+
const getNodeColor = (algorithm: AnimationState['currentAlgorithm'], nodeIndex: number, currentStep: number): string => {
284+
// 检查节点是否已经计算过
285+
const isCalculated = currentStep > 0 &&
286+
Array.from({length: currentStep}).some((_, stepIdx) =>
287+
state.timeline[stepIdx]?.visualChanges.nodeUpdates.some(update => update.index === nodeIndex)
288+
);
289+
290+
if (isCalculated) {
291+
switch (algorithm) {
292+
case 'dp':
293+
return '#81C784'; // 浅绿色 - 已计算的DP节点
294+
case 'matrix':
295+
return '#64B5F6'; // 浅蓝色 - 已计算的矩阵节点
296+
case 'formula':
297+
return '#BA68C8'; // 浅紫色 - 已计算的公式节点
298+
default:
299+
return '#81C784';
300+
}
301+
} else {
302+
// 未计算的节点使用较暗的颜色
140303
switch (algorithm) {
141304
case 'dp':
142-
return '#4CAF50'; // 绿色
305+
return '#4CAF50'; // 绿色 - 未计算的DP节点
143306
case 'matrix':
144-
return '#2196F3'; // 蓝色
307+
return '#2196F3'; // 蓝色 - 未计算的矩阵节点
145308
case 'formula':
146-
return '#9C27B0'; // 紫色
309+
return '#9C27B0'; // 紫色 - 未计算的公式节点
147310
default:
148311
return '#4CAF50';
312+
}
149313
}
150314
};
151315

@@ -158,26 +322,49 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
158322
) => {
159323
const matrixGroup = g.append('g')
160324
.attr('class', 'matrix')
161-
.attr('transform', `translate(${width - 100}, 50)`);
325+
.attr('transform', `translate(${width - 120}, 50)`);
162326

163327
const cellSize = Math.min(30, Math.max(20, width / 20));
164328

329+
// 添加矩阵标题
330+
matrixGroup.append('text')
331+
.attr('x', cellSize * matrix[0].length / 2)
332+
.attr('y', -10)
333+
.attr('text-anchor', 'middle')
334+
.attr('font-weight', 'bold')
335+
.attr('font-size', '12px')
336+
.text('状态矩阵');
337+
165338
// 绘制矩阵单元格
166339
matrix.forEach((row, i) => {
167340
row.forEach((value, j) => {
341+
// 绘制单元格背景
168342
matrixGroup.append('rect')
169343
.attr('x', j * cellSize)
170344
.attr('y', i * cellSize)
171345
.attr('width', cellSize)
172346
.attr('height', cellSize)
173-
.attr('fill', '#f5f5f5')
347+
.attr('fill', (state.timeline[state.currentStep]?.visualChanges.matrixUpdates.some(
348+
update => update.row === i && update.col === j
349+
)) ? '#FFCC80' : '#f5f5f5') // 当前更新的单元格高亮
174350
.attr('stroke', '#333');
175351

352+
// 绘制单元格文本
176353
matrixGroup.append('text')
177354
.attr('x', j * cellSize + cellSize / 2)
178355
.attr('y', i * cellSize + cellSize / 2 + 5)
179356
.attr('text-anchor', 'middle')
180357
.text(value);
358+
359+
// 添加单元格标签
360+
if (i === 0) {
361+
matrixGroup.append('text')
362+
.attr('x', j * cellSize + cellSize / 2)
363+
.attr('y', -5)
364+
.attr('text-anchor', 'middle')
365+
.attr('font-size', '10px')
366+
.text(`f(${j})`);
367+
}
181368
});
182369
});
183370

@@ -194,6 +381,19 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
194381
};
195382

196383
return (
384+
<>
385+
<style>
386+
{`
387+
@keyframes pulse {
388+
0% { opacity: 1; }
389+
50% { opacity: 0.6; }
390+
100% { opacity: 1; }
391+
}
392+
.node-highlight {
393+
filter: drop-shadow(0 0 5px #FF5722);
394+
}
395+
`}
396+
</style>
197397
<svg
198398
ref={svgRef}
199399
width={dimensions.width}
@@ -207,6 +407,7 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
207407
preserveAspectRatio="xMidYMid meet"
208408
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
209409
/>
410+
</>
210411
);
211412
};
212413

0 commit comments

Comments
 (0)