@@ -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