hookehuyr

feat(动画): 添加贝塞尔曲线动画路径组件

新增了一个基于SVG的贝塞尔曲线动画路径组件,支持逐步展示连接点之间的路径,并带有动画效果和箭头指示方向。同时,更新了路由配置以支持新页面,并添加了less依赖以支持样式预处理。
This diff is collapsed. Click to expand it.
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
35 "@vueuse/core": "^13.0.0", 35 "@vueuse/core": "^13.0.0",
36 "autoprefixer": "^10.4.19", 36 "autoprefixer": "^10.4.19",
37 "axios": "^1.8.4", 37 "axios": "^1.8.4",
38 + "less": "^4.2.2",
38 "postcss": "^8.4.35", 39 "postcss": "^8.4.35",
39 "qs": "^6.14.0", 40 "qs": "^6.14.0",
40 "tailwindcss": "^3.4.1", 41 "tailwindcss": "^3.4.1",
......
1 /* 1 /*
2 * @Date: 2025-03-20 20:36:36 2 * @Date: 2025-03-20 20:36:36
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-25 15:17:18 4 + * @LastEditTime: 2025-03-28 09:39:27
5 * @FilePath: /mlaj/src/router/routes.js 5 * @FilePath: /mlaj/src/router/routes.js
6 * @Description: 路由地址映射配置 6 * @Description: 路由地址映射配置
7 */ 7 */
...@@ -175,6 +175,12 @@ export const routes = [ ...@@ -175,6 +175,12 @@ export const routes = [
175 meta: { title: 'test' }, 175 meta: { title: 'test' },
176 }, 176 },
177 { 177 {
178 + path: '/animation',
179 + name: 'animation',
180 + component: () => import('../views/animation.vue'),
181 + meta: { title: 'animation' },
182 + },
183 + {
178 path: '/upload_video', 184 path: '/upload_video',
179 name: 'upload_video', 185 name: 'upload_video',
180 component: () => import('../views/upload_video.vue'), 186 component: () => import('../views/upload_video.vue'),
......
1 +<!--
2 + * @Date: 2025-03-28 09:23:04
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-28 13:26:37
5 + * @FilePath: /mlaj/src/views/animation.vue
6 + * @Description: 贝塞尔曲线动画路径组件
7 + *
8 + * 该组件实现了一个基于SVG的贝塞尔曲线动画效果:
9 + * 1. 在画布上展示多个连接点
10 + * 2. 点击"下一步"按钮可以逐步显示连接点之间的贝塞尔曲线路径
11 + * 3. 路径带有动画效果和箭头指示方向
12 + * 4. 已激活的路径段会以绿色高亮显示
13 + * 5. 支持通过点击节点来激活路径
14 +-->
15 +<template>
16 + <div class="animation-container">
17 + <button class="next-button" @click="nextStep" :disabled="activeNodeIndex >= points.length - 1">下一步</button>
18 + <svg width="1000" height="1000" viewBox="0 0 1000 1000">
19 + <!-- 定义箭头标记 -->
20 + <defs>
21 + <marker
22 + id="arrow-inactive"
23 + viewBox="0 0 10 10"
24 + refX="5"
25 + refY="5"
26 + markerWidth="6"
27 + markerHeight="6"
28 + orient="auto-start-reverse"
29 + >
30 + <path d="M 0 0 L 10 5 L 0 10 z" fill="#ccc" />
31 + </marker>
32 + <marker
33 + id="arrow-active"
34 + viewBox="0 0 10 10"
35 + refX="5"
36 + refY="5"
37 + markerWidth="6"
38 + markerHeight="6"
39 + orient="auto-start-reverse"
40 + >
41 + <path d="M 0 0 L 10 5 L 0 10 z" fill="#4CAF50" />
42 + </marker>
43 + </defs>
44 +
45 + <!-- 贝塞尔曲线路径 -->
46 + <template v-for="(_, index) in points.slice(0, -1)" :key="index">
47 + <path
48 + v-show="activeNodeIndex === -1 || index > activeNodeIndex"
49 + :d="calculatePathSegment(points[index], points[index + 1])"
50 + fill="none"
51 + :stroke="pathColor"
52 + stroke-width="2"
53 + stroke-dasharray="5,5"
54 + class="path-animation"
55 + marker-end="url(#arrow-inactive)"
56 + />
57 + </template>
58 +
59 + <!-- 高亮的路径段 -->
60 + <path
61 + v-for="(segment, index) in activePathSegments"
62 + :key="index"
63 + :d="segment"
64 + fill="none"
65 + stroke="#4CAF50"
66 + stroke-width="2"
67 + stroke-dasharray="10"
68 + marker-end="url(#arrow-active)"
69 + class="active-path-animation"
70 + />
71 +
72 + <!-- 节点圆点 -->
73 + <template v-for="(point, index) in points" :key="index">
74 + <circle
75 + :cx="point.x"
76 + :cy="point.y"
77 + r="6"
78 + :class="['node', { 'node-active': activeNodeIndex >= index - 1 }]"
79 + @click="activateNode(index)"
80 + />
81 + </template>
82 + </svg>
83 + </div>
84 +</template>
85 +
86 +<script setup>
87 +import { ref, computed } from 'vue';
88 +
89 +// 定义节点坐标数组,每个节点包含x和y坐标
90 +// 这些点将用于生成贝塞尔曲线的路径
91 +const points = ref([
92 + { x: 250, y: 50 },
93 + { x: 10, y: 250 },
94 + { x: 400, y: 500 },
95 + { x: 50, y: 700 },
96 + { x: 400, y: 950 }
97 +]);
98 +
99 +// 当前激活的节点索引,初始值为-1表示没有节点被激活
100 +const activeNodeIndex = ref(-1);
101 +
102 +// 存储已激活的路径段数组,每个元素是一个SVG路径字符串
103 +// 用于显示高亮的贝塞尔曲线路径
104 +const activePathSegments = ref([]);
105 +
106 +// 计算完整的贝塞尔曲线路径
107 +// 使用三次贝塞尔曲线(Cubic Bezier)创建平滑的曲线效果
108 +// 控制点的位置通过当前点和下一个点的中点来计算
109 +const pathData = computed(() => {
110 + const path = [];
111 + points.value.forEach((point, index) => {
112 + if (index === 0) {
113 + path.push(`M ${point.x} ${point.y}`);
114 + } else {
115 + const prevPoint = points.value[index - 1];
116 + const segment = calculatePathSegment(prevPoint, point);
117 + path.push(segment.substring(segment.indexOf('C')));
118 + }
119 + });
120 + return path.join(' ');
121 +});
122 +
123 +// 计算两点之间的贝塞尔曲线路径段
124 +// @param startPoint - 起始点坐标 {x, y}
125 +// @param endPoint - 结束点坐标 {x, y}
126 +// @returns 返回SVG路径字符串
127 +const calculatePathSegment = (startPoint, endPoint) => {
128 + // 计算路径的方向向量
129 + const dx = endPoint.x - startPoint.x;
130 + const dy = endPoint.y - startPoint.y;
131 + const distance = Math.sqrt(dx * dx + dy * dy);
132 +
133 + // 设置节点间的偏移距离(可以根据需要调整)
134 + const offset = 0;
135 + const verticalOffset = -20; // 添加垂直偏移量参数
136 +
137 + // 计算单位向量
138 + const unitX = dx / distance;
139 + const unitY = dy / distance;
140 +
141 + // 计算偏移后的起点和终点,根据路径方向调整垂直偏移
142 + const adjustedStart = {
143 + x: startPoint.x + unitX * offset,
144 + y: startPoint.y + (dy > 0 ? -verticalOffset : verticalOffset)
145 + };
146 + const adjustedEnd = {
147 + x: endPoint.x - unitX * offset,
148 + y: endPoint.y + (dy > 0 ? verticalOffset : -verticalOffset)
149 + };
150 +
151 + // 计算控制点
152 + const cpx1 = adjustedStart.x;
153 + const cpy1 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5;
154 + const cpx2 = adjustedEnd.x;
155 + const cpy2 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5;
156 +
157 + return `M ${adjustedStart.x} ${adjustedStart.y} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${adjustedEnd.x} ${adjustedEnd.y}`;
158 +};
159 +
160 +// 未激活路径的颜色
161 +// 使用浅灰色(#ccc)表示未激活状态
162 +const pathColor = computed(() => {
163 + return '#ccc';
164 +});
165 +
166 +
167 +// 处理节点点击事件,激活指定节点并更新路径
168 +// @param index - 被点击节点的索引
169 +// 点击节点时会激活该节点及其之前的所有节点和路径
170 +const activateNode = (index) => {
171 + activeNodeIndex.value = index;
172 + activePathSegments.value = [];
173 +
174 + // 重新计算所有激活的路径段
175 + for (let i = 0; i < index; i++) {
176 + const startPoint = points.value[i];
177 + const endPoint = points.value[i + 1];
178 + if (endPoint) {
179 + const segment = calculatePathSegment(startPoint, endPoint);
180 + activePathSegments.value.push(segment);
181 + }
182 + }
183 +};
184 +
185 +// 处理下一步按钮点击事件
186 +// 激活下一个节点并创建新的高亮路径段
187 +// 当到达最后一个节点时,按钮将被禁用
188 +const nextStep = () => {
189 + if (activeNodeIndex.value < points.value.length - 1) {
190 + activeNodeIndex.value++;
191 + const startPoint = points.value[activeNodeIndex.value];
192 + const endPoint = points.value[activeNodeIndex.value + 1];
193 + // 只有当存在下一个节点时才添加新的路径段
194 + if (endPoint) {
195 + const newSegment = calculatePathSegment(startPoint, endPoint);
196 + // 保留之前的路径段,添加新的路径段
197 + activePathSegments.value.push(newSegment);
198 + }
199 + // 如果是最后一个节点,直接将其设置为激活状态
200 + if (activeNodeIndex.value === points.value.length - 2) {
201 + activeNodeIndex.value = points.value.length - 1;
202 + }
203 + }
204 +};
205 +</script>
206 +
207 +<style scoped>
208 +.animation-container {
209 + display: flex;
210 + justify-content: center;
211 + align-items: center;
212 + min-height: 100vh;
213 + background: #f5f5f5;
214 +}
215 +
216 +.node {
217 + fill: white;
218 + stroke: #ccc;
219 + stroke-width: 2;
220 + cursor: pointer;
221 + transition: all 0.3s ease;
222 +}
223 +
224 +.node-active {
225 + fill: white;
226 + stroke: #4CAF50;
227 +}
228 +
229 +.path-animation {
230 + transition: all 0.5s ease;
231 +}
232 +
233 +.active-path-animation {
234 + animation: dash 1.5s linear infinite;
235 +}
236 +
237 +@keyframes dash {
238 + to {
239 + stroke-dashoffset: -20;
240 + }
241 +}
242 +
243 +.next-button {
244 + position: absolute;
245 + top: 20px;
246 + right: 20px;
247 + padding: 8px 16px;
248 + background-color: #4CAF50;
249 + color: white;
250 + border: none;
251 + border-radius: 4px;
252 + cursor: pointer;
253 + transition: all 0.3s ease;
254 +}
255 +
256 +.next-button:hover {
257 + background-color: #45a049;
258 +}
259 +
260 +.next-button:disabled {
261 + background-color: #cccccc;
262 + cursor: not-allowed;
263 +}
264 +</style>