hookehuyr

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

新增了一个基于SVG的贝塞尔曲线动画路径组件,支持逐步展示连接点之间的路径,并带有动画效果和箭头指示方向。同时,更新了路由配置以支持新页面,并添加了less依赖以支持样式预处理。
This diff is collapsed. Click to expand it.
......@@ -35,6 +35,7 @@
"@vueuse/core": "^13.0.0",
"autoprefixer": "^10.4.19",
"axios": "^1.8.4",
"less": "^4.2.2",
"postcss": "^8.4.35",
"qs": "^6.14.0",
"tailwindcss": "^3.4.1",
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-25 15:17:18
* @LastEditTime: 2025-03-28 09:39:27
* @FilePath: /mlaj/src/router/routes.js
* @Description: 路由地址映射配置
*/
......@@ -175,6 +175,12 @@ export const routes = [
meta: { title: 'test' },
},
{
path: '/animation',
name: 'animation',
component: () => import('../views/animation.vue'),
meta: { title: 'animation' },
},
{
path: '/upload_video',
name: 'upload_video',
component: () => import('../views/upload_video.vue'),
......
<!--
* @Date: 2025-03-28 09:23:04
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-28 13:26:37
* @FilePath: /mlaj/src/views/animation.vue
* @Description: 贝塞尔曲线动画路径组件
*
* 该组件实现了一个基于SVG的贝塞尔曲线动画效果:
* 1. 在画布上展示多个连接点
* 2. 点击"下一步"按钮可以逐步显示连接点之间的贝塞尔曲线路径
* 3. 路径带有动画效果和箭头指示方向
* 4. 已激活的路径段会以绿色高亮显示
* 5. 支持通过点击节点来激活路径
-->
<template>
<div class="animation-container">
<button class="next-button" @click="nextStep" :disabled="activeNodeIndex >= points.length - 1">下一步</button>
<svg width="1000" height="1000" viewBox="0 0 1000 1000">
<!-- 定义箭头标记 -->
<defs>
<marker
id="arrow-inactive"
viewBox="0 0 10 10"
refX="5"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ccc" />
</marker>
<marker
id="arrow-active"
viewBox="0 0 10 10"
refX="5"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4CAF50" />
</marker>
</defs>
<!-- 贝塞尔曲线路径 -->
<template v-for="(_, index) in points.slice(0, -1)" :key="index">
<path
v-show="activeNodeIndex === -1 || index > activeNodeIndex"
:d="calculatePathSegment(points[index], points[index + 1])"
fill="none"
:stroke="pathColor"
stroke-width="2"
stroke-dasharray="5,5"
class="path-animation"
marker-end="url(#arrow-inactive)"
/>
</template>
<!-- 高亮的路径段 -->
<path
v-for="(segment, index) in activePathSegments"
:key="index"
:d="segment"
fill="none"
stroke="#4CAF50"
stroke-width="2"
stroke-dasharray="10"
marker-end="url(#arrow-active)"
class="active-path-animation"
/>
<!-- 节点圆点 -->
<template v-for="(point, index) in points" :key="index">
<circle
:cx="point.x"
:cy="point.y"
r="6"
:class="['node', { 'node-active': activeNodeIndex >= index - 1 }]"
@click="activateNode(index)"
/>
</template>
</svg>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// 定义节点坐标数组,每个节点包含x和y坐标
// 这些点将用于生成贝塞尔曲线的路径
const points = ref([
{ x: 250, y: 50 },
{ x: 10, y: 250 },
{ x: 400, y: 500 },
{ x: 50, y: 700 },
{ x: 400, y: 950 }
]);
// 当前激活的节点索引,初始值为-1表示没有节点被激活
const activeNodeIndex = ref(-1);
// 存储已激活的路径段数组,每个元素是一个SVG路径字符串
// 用于显示高亮的贝塞尔曲线路径
const activePathSegments = ref([]);
// 计算完整的贝塞尔曲线路径
// 使用三次贝塞尔曲线(Cubic Bezier)创建平滑的曲线效果
// 控制点的位置通过当前点和下一个点的中点来计算
const pathData = computed(() => {
const path = [];
points.value.forEach((point, index) => {
if (index === 0) {
path.push(`M ${point.x} ${point.y}`);
} else {
const prevPoint = points.value[index - 1];
const segment = calculatePathSegment(prevPoint, point);
path.push(segment.substring(segment.indexOf('C')));
}
});
return path.join(' ');
});
// 计算两点之间的贝塞尔曲线路径段
// @param startPoint - 起始点坐标 {x, y}
// @param endPoint - 结束点坐标 {x, y}
// @returns 返回SVG路径字符串
const calculatePathSegment = (startPoint, endPoint) => {
// 计算路径的方向向量
const dx = endPoint.x - startPoint.x;
const dy = endPoint.y - startPoint.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 设置节点间的偏移距离(可以根据需要调整)
const offset = 0;
const verticalOffset = -20; // 添加垂直偏移量参数
// 计算单位向量
const unitX = dx / distance;
const unitY = dy / distance;
// 计算偏移后的起点和终点,根据路径方向调整垂直偏移
const adjustedStart = {
x: startPoint.x + unitX * offset,
y: startPoint.y + (dy > 0 ? -verticalOffset : verticalOffset)
};
const adjustedEnd = {
x: endPoint.x - unitX * offset,
y: endPoint.y + (dy > 0 ? verticalOffset : -verticalOffset)
};
// 计算控制点
const cpx1 = adjustedStart.x;
const cpy1 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5;
const cpx2 = adjustedEnd.x;
const cpy2 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5;
return `M ${adjustedStart.x} ${adjustedStart.y} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${adjustedEnd.x} ${adjustedEnd.y}`;
};
// 未激活路径的颜色
// 使用浅灰色(#ccc)表示未激活状态
const pathColor = computed(() => {
return '#ccc';
});
// 处理节点点击事件,激活指定节点并更新路径
// @param index - 被点击节点的索引
// 点击节点时会激活该节点及其之前的所有节点和路径
const activateNode = (index) => {
activeNodeIndex.value = index;
activePathSegments.value = [];
// 重新计算所有激活的路径段
for (let i = 0; i < index; i++) {
const startPoint = points.value[i];
const endPoint = points.value[i + 1];
if (endPoint) {
const segment = calculatePathSegment(startPoint, endPoint);
activePathSegments.value.push(segment);
}
}
};
// 处理下一步按钮点击事件
// 激活下一个节点并创建新的高亮路径段
// 当到达最后一个节点时,按钮将被禁用
const nextStep = () => {
if (activeNodeIndex.value < points.value.length - 1) {
activeNodeIndex.value++;
const startPoint = points.value[activeNodeIndex.value];
const endPoint = points.value[activeNodeIndex.value + 1];
// 只有当存在下一个节点时才添加新的路径段
if (endPoint) {
const newSegment = calculatePathSegment(startPoint, endPoint);
// 保留之前的路径段,添加新的路径段
activePathSegments.value.push(newSegment);
}
// 如果是最后一个节点,直接将其设置为激活状态
if (activeNodeIndex.value === points.value.length - 2) {
activeNodeIndex.value = points.value.length - 1;
}
}
};
</script>
<style scoped>
.animation-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f5f5;
}
.node {
fill: white;
stroke: #ccc;
stroke-width: 2;
cursor: pointer;
transition: all 0.3s ease;
}
.node-active {
fill: white;
stroke: #4CAF50;
}
.path-animation {
transition: all 0.5s ease;
}
.active-path-animation {
animation: dash 1.5s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: -20;
}
}
.next-button {
position: absolute;
top: 20px;
right: 20px;
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.next-button:hover {
background-color: #45a049;
}
.next-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>