@@ -54,7 +54,7 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
54
54
svg . selectAll ( '*' ) . remove ( ) ; // 清空画布
55
55
56
56
// 设置预留边距
57
- const margin = { top : 30 , right : 30 , bottom : 30 , left : 30 } ;
57
+ const margin = { top : 80 , right : 30 , bottom : 30 , left : 30 } ;
58
58
const innerWidth = dimensions . width - margin . left - margin . right ;
59
59
const innerHeight = dimensions . height - margin . top - margin . bottom ;
60
60
@@ -66,7 +66,9 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
66
66
const nodeGroup = g . append ( 'g' ) . attr ( 'class' , 'nodes' ) ;
67
67
const linkGroup = g . append ( 'g' ) . attr ( 'class' , 'links' ) ;
68
68
const textGroup = g . append ( 'g' ) . attr ( 'class' , 'texts' ) ;
69
+ const labelGroup = g . append ( 'g' ) . attr ( 'class' , 'labels' ) ; // 新增:节点标签图层
69
70
const formulaGroup = g . append ( 'g' ) . attr ( 'class' , 'formula' ) ;
71
+ const stepDescGroup = svg . append ( 'g' ) . attr ( 'class' , 'step-description' ) ; // 新增:步骤说明图层
70
72
71
73
// 计算节点的缩放比例
72
74
const maxX = Math . max ( ...state . staircase . nodes . map ( n => n . x ) ) || 100 ;
@@ -78,20 +80,75 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
78
80
const mapX = ( x : number ) => x * scaleX ;
79
81
const mapY = ( y : number ) => y * scaleY ;
80
82
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' )
83
115
. data ( state . staircase . links )
84
116
. 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' )
90
133
. 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' ) ;
92
149
93
150
// 绘制节点
94
- const nodeColor = getColorByAlgorithm ( state . currentAlgorithm ) ;
151
+ const nodeColor = getNodeColor ; // 修改为根据节点状态返回颜色的函数
95
152
const nodeRadius = Math . min ( 20 , Math . max ( 15 , Math . min ( innerWidth , innerHeight ) / 30 ) ) ;
96
153
97
154
nodeGroup . selectAll ( 'circle' )
@@ -101,9 +158,22 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
101
158
. attr ( 'cx' , ( d : NodeData ) => mapX ( d . x ) )
102
159
. attr ( 'cy' , ( d : NodeData ) => mapY ( d . y ) )
103
160
. 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
+ } )
105
168
. 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
+ } ) ;
107
177
108
178
// 添加节点值文本
109
179
textGroup . selectAll ( 'text' )
@@ -117,35 +187,129 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
117
187
. attr ( 'font-weight' , 'bold' )
118
188
. text ( ( d : NodeData ) => d . value ) ;
119
189
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
+ // 渲染公式 - 增强显示
121
240
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
+
122
252
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 ' )
126
256
. 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' )
128
259
. 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 ( '状态转移方程' ) ;
129
269
}
130
270
271
+ // 为动画添加CSS动画效果
272
+ svg . selectAll ( '.node-highlight' )
273
+ . style ( 'animation' , 'pulse 1.5s infinite' ) ;
274
+
131
275
// 渲染矩阵(仅当使用矩阵算法时)
132
276
if ( state . currentAlgorithm === 'matrix' && state . matrix . length > 0 ) {
133
277
renderMatrix ( g , state . matrix , innerWidth , innerHeight ) ;
134
278
}
135
279
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 ] ) ;
137
281
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
+ // 未计算的节点使用较暗的颜色
140
303
switch ( algorithm ) {
141
304
case 'dp' :
142
- return '#4CAF50' ; // 绿色
305
+ return '#4CAF50' ; // 绿色 - 未计算的DP节点
143
306
case 'matrix' :
144
- return '#2196F3' ; // 蓝色
307
+ return '#2196F3' ; // 蓝色 - 未计算的矩阵节点
145
308
case 'formula' :
146
- return '#9C27B0' ; // 紫色
309
+ return '#9C27B0' ; // 紫色 - 未计算的公式节点
147
310
default :
148
311
return '#4CAF50' ;
312
+ }
149
313
}
150
314
} ;
151
315
@@ -158,26 +322,49 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
158
322
) => {
159
323
const matrixGroup = g . append ( 'g' )
160
324
. attr ( 'class' , 'matrix' )
161
- . attr ( 'transform' , `translate(${ width - 100 } , 50)` ) ;
325
+ . attr ( 'transform' , `translate(${ width - 120 } , 50)` ) ;
162
326
163
327
const cellSize = Math . min ( 30 , Math . max ( 20 , width / 20 ) ) ;
164
328
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
+
165
338
// 绘制矩阵单元格
166
339
matrix . forEach ( ( row , i ) => {
167
340
row . forEach ( ( value , j ) => {
341
+ // 绘制单元格背景
168
342
matrixGroup . append ( 'rect' )
169
343
. attr ( 'x' , j * cellSize )
170
344
. attr ( 'y' , i * cellSize )
171
345
. attr ( 'width' , cellSize )
172
346
. 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' ) // 当前更新的单元格高亮
174
350
. attr ( 'stroke' , '#333' ) ;
175
351
352
+ // 绘制单元格文本
176
353
matrixGroup . append ( 'text' )
177
354
. attr ( 'x' , j * cellSize + cellSize / 2 )
178
355
. attr ( 'y' , i * cellSize + cellSize / 2 + 5 )
179
356
. attr ( 'text-anchor' , 'middle' )
180
357
. 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
+ }
181
368
} ) ;
182
369
} ) ;
183
370
@@ -194,6 +381,19 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
194
381
} ;
195
382
196
383
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 >
197
397
< svg
198
398
ref = { svgRef }
199
399
width = { dimensions . width }
@@ -207,6 +407,7 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({ state, width, height
207
407
preserveAspectRatio = "xMidYMid meet"
208
408
viewBox = { `0 0 ${ dimensions . width } ${ dimensions . height } ` }
209
409
/>
410
+ </ >
210
411
) ;
211
412
} ;
212
413
0 commit comments