animation.vue 7.74 KB
<!--
 * @Date: 2025-03-28 09:23:04
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-03-28 14:41:28
 * @FilePath: /mlaj/src/views/animation.vue
 * @Description: 贝塞尔曲线动画路径组件
 *
 * 该组件实现了一个基于SVG的贝塞尔曲线动画效果:
 * 1. 在画布上展示多个连接点
 * 2. 点击"下一步"按钮可以逐步显示连接点之间的贝塞尔曲线路径
 * 3. 路径带有动画效果和箭头指示方向
 * 4. 已激活的路径段会以绿色高亮显示
-->
<template>
  <div class="animation-container">
    <div style="position: fixed; top: 0; right: 0;">
      <button class="next-button" @click="nextStep" :disabled="activeNodeIndex >= points.length - 1">下一步</button>
    </div>
    <svg width="100%" height="100%" viewBox="0 0 1000 1200" class="animation-svg">
      <!-- 定义箭头标记 -->
      <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="12"
          :class="['node', { 'node-active': activeNodeIndex >= index - 1 }]"
        />
      </template>
    </svg>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 定义节点坐标数组,每个节点包含x和y坐标
// 这些点将用于生成贝塞尔曲线的路径
const points = ref([
  { x: 700, y: 50 },
  { x: 300, y: 300 },
  { x: 700, y: 600 },
  { x: 300, y: 800 },
  { x: 700, y: 1000 }
]);

// 当前激活的节点索引,初始值为-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 = -30; // 添加垂直偏移量参数

  // 计算单位向量
  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;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #f5f5f5;
  padding: 1.25rem;
  box-sizing: border-box;
}

.animation-svg {
  max-width: 100%;
  max-height: 100vh;
  margin: 0 auto;
}

@media screen and (max-width: 768px) {
  .animation-container {
    padding: 0.9375rem;
  }

  .animation-svg {
    height: 80vh;
  }

  .next-button {
    padding: 0.5rem 1rem;
    font-size: 1rem;
  }
}

.node {
  fill: white;
  stroke: #ccc;
  stroke-width: 0.125rem;
  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: -1.25rem;
  }
}

.next-button {
  position: absolute;
  top: 1.25rem;
  right: 1.25rem;
  padding: 0.5rem 1rem;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
  transition: all 0.3s ease;
}

.next-button:hover {
  background-color: #45a049;
}

.next-button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}
</style>