Skip to content

Commit 3ec5ccc

Browse files
committed
添加动画特效:1.节点被选中时的波纹动画 2.指针相交时的黄色高亮效果
1 parent 22975c8 commit 3ec5ccc

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as d3 from 'd3';
2+
import { ListNode } from '../../../../types';
3+
import {
4+
addNodeSelectedEffect,
5+
addPointersIntersectionEffect
6+
} from '../../../../utils/d3utils/pointers';
7+
8+
/**
9+
* 为可视化添加动画效果
10+
*/
11+
export const applyAnimationEffects = (
12+
svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
13+
currentNodeA: ListNode | null,
14+
currentNodeB: ListNode | null,
15+
nodePositions: Map<ListNode, {x: number, y: number}>,
16+
nodeRadius: number,
17+
scale: number = 1
18+
) => {
19+
if (!currentNodeA && !currentNodeB) return;
20+
21+
// 应用当前选中节点的动画效果
22+
if (currentNodeA) {
23+
const position = nodePositions.get(currentNodeA);
24+
if (position) {
25+
// 为指针A选中的节点添加蓝色波纹动画
26+
addNodeSelectedEffect(
27+
svg,
28+
position.x,
29+
position.y,
30+
nodeRadius,
31+
'#3498db', // 蓝色
32+
scale
33+
);
34+
}
35+
}
36+
37+
if (currentNodeB) {
38+
const position = nodePositions.get(currentNodeB);
39+
if (position) {
40+
// 如果不是同一个节点,为指针B添加紫色波纹动画
41+
if (currentNodeB !== currentNodeA) {
42+
addNodeSelectedEffect(
43+
svg,
44+
position.x,
45+
position.y,
46+
nodeRadius,
47+
'#9b59b6', // 紫色
48+
scale
49+
);
50+
}
51+
}
52+
}
53+
54+
// 如果两个指针指向同一个节点,添加相交动画特效
55+
if (currentNodeA && currentNodeB && currentNodeA === currentNodeB) {
56+
const position = nodePositions.get(currentNodeA); // 两个是同一个节点,用A或B都可以
57+
if (position) {
58+
// 添加黄色相交节点特效
59+
addPointersIntersectionEffect(
60+
svg,
61+
position.x,
62+
position.y,
63+
nodeRadius,
64+
scale
65+
);
66+
}
67+
}
68+
};
69+
70+
/**
71+
* 记录节点位置,用于动画效果
72+
*/
73+
export const collectNodePositions = (
74+
nodeInfo: any,
75+
leftOffset: number,
76+
nodeSpacing: number,
77+
topRowY: number,
78+
bottomRowY: number
79+
): Map<ListNode, {x: number, y: number}> => {
80+
const positions = new Map<ListNode, {x: number, y: number}>();
81+
82+
// 保存链表A中节点的位置
83+
nodeInfo.preIntersectionNodesA.forEach((node: ListNode, index: number) => {
84+
positions.set(node, {
85+
x: leftOffset + index * nodeSpacing,
86+
y: topRowY
87+
});
88+
});
89+
90+
// 保存链表B中节点的位置
91+
nodeInfo.preIntersectionNodesB.forEach((node: ListNode, index: number) => {
92+
positions.set(node, {
93+
x: leftOffset + index * nodeSpacing,
94+
y: bottomRowY
95+
});
96+
});
97+
98+
// 保存相交部分节点的位置
99+
const intersectionY = (topRowY + bottomRowY) / 2;
100+
nodeInfo.intersectionNodes.forEach((node: ListNode, index: number) => {
101+
positions.set(node, {
102+
x: nodeInfo.startX + index * nodeSpacing,
103+
y: intersectionY
104+
});
105+
});
106+
107+
return positions;
108+
};
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as d3 from 'd3';
2+
3+
/**
4+
* 添加节点被选中时的波纹动画效果 - 蓝色波纹动画
5+
* 类似于第一张图中的效果,蓝色节点被选中时产生多层波纹
6+
*/
7+
export const addNodeSelectedEffect = (
8+
svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
9+
x: number,
10+
y: number,
11+
nodeRadius: number,
12+
color: string = '#3498db', // 默认蓝色
13+
scale: number = 1
14+
) => {
15+
const group = svg.append('g')
16+
.attr('class', 'node-selected-effect')
17+
.attr('transform', `translate(${x}, ${y})`);
18+
19+
// 添加多层波纹效果
20+
for (let i = 0; i < 3; i++) {
21+
const delayFactor = i * 700; // 错开动画开始时间
22+
23+
// 添加波纹圆圈
24+
const circle = group.append('circle')
25+
.attr('r', nodeRadius)
26+
.attr('fill', 'none')
27+
.attr('stroke', color)
28+
.attr('stroke-width', 2.5 * scale)
29+
.attr('stroke-opacity', 0.8)
30+
.attr('stroke-dasharray', '3,3') // 虚线效果
31+
.style('transform-origin', 'center')
32+
.style('transform-box', 'fill-box');
33+
34+
// 第一个动画:扩散
35+
const animateRipple = () => {
36+
circle
37+
.attr('stroke-opacity', 0.8)
38+
.attr('r', nodeRadius)
39+
.transition()
40+
.duration(2000)
41+
.attr('r', nodeRadius * 3)
42+
.attr('stroke-opacity', 0)
43+
.on('end', animateRipple); // 循环动画
44+
};
45+
46+
// 延迟启动动画
47+
setTimeout(animateRipple, delayFactor);
48+
}
49+
50+
// 添加内部发光效果
51+
const innerGlow = group.append('circle')
52+
.attr('r', nodeRadius * 1.1)
53+
.attr('fill', 'none')
54+
.attr('stroke', color)
55+
.attr('stroke-width', 3 * scale)
56+
.attr('filter', 'url(#glow-filter)');
57+
58+
// 脉冲动画
59+
const pulseInnerGlow = () => {
60+
innerGlow
61+
.transition()
62+
.duration(1000)
63+
.attr('stroke-opacity', 1)
64+
.attr('r', nodeRadius * 1.2)
65+
.transition()
66+
.duration(1000)
67+
.attr('stroke-opacity', 0.5)
68+
.attr('r', nodeRadius * 1.1)
69+
.on('end', pulseInnerGlow); // 循环动画
70+
};
71+
72+
pulseInnerGlow();
73+
74+
return group;
75+
};
76+
77+
/**
78+
* 添加指针节点相交时的特效 - 黄色/橙色的相交高亮效果
79+
* 类似于第二张图中的效果,当两个指针相交时产生的特殊黄色高亮
80+
*/
81+
export const addPointersIntersectionEffect = (
82+
svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
83+
x: number,
84+
y: number,
85+
nodeRadius: number,
86+
scale: number = 1
87+
) => {
88+
const group = svg.append('g')
89+
.attr('class', 'pointers-intersection-effect')
90+
.attr('transform', `translate(${x}, ${y})`);
91+
92+
// 添加外部光环 - 黄色渐变
93+
const outerHalo = group.append('circle')
94+
.attr('r', nodeRadius * 1.6)
95+
.attr('fill', 'none')
96+
.attr('stroke', '#f39c12')
97+
.attr('stroke-width', 3 * scale)
98+
.attr('stroke-opacity', 0.3)
99+
.attr('filter', 'url(#glow-filter)');
100+
101+
// 添加第二层光环
102+
const middleHalo = group.append('circle')
103+
.attr('r', nodeRadius * 1.3)
104+
.attr('fill', 'none')
105+
.attr('stroke', '#f1c40f')
106+
.attr('stroke-width', 3 * scale)
107+
.attr('stroke-opacity', 0.5);
108+
109+
// 添加内部光环
110+
const innerHalo = group.append('circle')
111+
.attr('r', nodeRadius * 1.1)
112+
.attr('fill', 'none')
113+
.attr('stroke', '#f39c12')
114+
.attr('stroke-width', 4 * scale)
115+
.attr('stroke-opacity', 0.7);
116+
117+
// 脉冲动画 - 外部光环
118+
const pulseOuterHalo = () => {
119+
outerHalo
120+
.transition()
121+
.duration(1500)
122+
.attr('stroke-opacity', 0.5)
123+
.attr('r', nodeRadius * 1.8)
124+
.transition()
125+
.duration(1500)
126+
.attr('stroke-opacity', 0.3)
127+
.attr('r', nodeRadius * 1.6)
128+
.on('end', pulseOuterHalo);
129+
};
130+
131+
// 脉冲动画 - 中间光环
132+
const pulseMiddleHalo = () => {
133+
middleHalo
134+
.transition()
135+
.duration(1200)
136+
.attr('stroke-opacity', 0.7)
137+
.attr('r', nodeRadius * 1.4)
138+
.transition()
139+
.duration(1200)
140+
.attr('stroke-opacity', 0.5)
141+
.attr('r', nodeRadius * 1.3)
142+
.on('end', pulseMiddleHalo);
143+
};
144+
145+
// 脉冲动画 - 内部光环
146+
const pulseInnerHalo = () => {
147+
innerHalo
148+
.transition()
149+
.duration(900)
150+
.attr('stroke-opacity', 0.9)
151+
.attr('r', nodeRadius * 1.15)
152+
.transition()
153+
.duration(900)
154+
.attr('stroke-opacity', 0.7)
155+
.attr('r', nodeRadius * 1.1)
156+
.on('end', pulseInnerHalo);
157+
};
158+
159+
// 启动所有动画
160+
pulseOuterHalo();
161+
setTimeout(() => pulseMiddleHalo(), 300); // 错开动画开始时间
162+
setTimeout(() => pulseInnerHalo(), 600); // 错开动画开始时间
163+
164+
return group;
165+
};

0 commit comments

Comments
 (0)