Skip to content

Svg 相关

Svg 描边动画

请点击启动按钮

代码实现

点击展开
vue
<template>
  <div class="svgStroke">
    <div
      :class="['container', { active: isTrue }]"
      :style="{ '--svgStroke-color': isDark ? '#d7ecb1' : '#666' }"
    >
      <svg width="200" height="200" viewBox="0 0 300 300" ref="svgStroke">
        <ellipse ry="70" rx="70" cy="147.5" cx="137" class="p" />
        <ellipse ry="42" rx="42" cy="49" cx="48.5" class="p" />
        <ellipse ry="35" rx="35" cy="148" cx="252.5" class="p" />
        <ellipse ry="15" rx="15" cy="89" cx="278" class="p" />
        <ellipse ry="32" rx="32" cy="260.5" cx="131" class="p" />
        <ellipse ry="18" rx="18" cy="118" cx="45" class="p" />
        <path
          d="m14.17988,126c-9.96402,57.47692 15.13462,113.88462 74.82012,131"
          class="p"
        />
        <path d="m103,35c59.02702,-19 116.24324,10 136,53" class="p" />
        <path d="m173,262c36.81579,-7 55.31579,-20 71,-61" class="p" />
        <path
          d="m93,178c0.47945,10.86931 11.0274,23.41081 35,21.32056"
          class="p"
        />
      </svg>
    </div>

    <el-button @click="isTrue = true">启动动画</el-button>
    <el-button @click="isTrue = false">隐藏</el-button>
  </div>
</template>

<script setup lang="ts">
import { useData } from "vitepress";
import { ref, onMounted } from "vue";
const { isDark } = useData();
const isTrue = ref(false);
const svgStroke = ref<SVGSVGElement>();
onMounted(() => {
  if (svgStroke.value) {
    const paths = Array.from(
      svgStroke.value?.children || []
    ) as SVGPathElement[];
    for (const path of paths) {
      const totalLength = Math.ceil(path.getTotalLength());
      path.style.setProperty("--svgStroke-l", `${totalLength}`);
    }
  }
});
</script>

<style lang="scss" scoped>
.svgStroke {
  .container {
    --svgStroke-color: #d7ecb1;
    .p {
      --svgStroke-l: 0;
      fill: none;
      stroke: var(--svgStroke-color);
      stroke-width: 4;
      stroke-dasharray: var(--svgStroke-l);
      stroke-dashoffset: var(--svgStroke-l);
      stroke-linecap: round;
    }
    &.active {
      .p {
        animation: draw 0.8s linear forwards;
      }
    }
  }
}
@keyframes draw {
  to {
    stroke-dashoffset: 0;
  }
}
</style>

视频讲解

Svg 路径动画

svg 路径创建

  • path 属性 路径的 dom 节点

例子直线

html
<svg view="0 0 50 30" class="svg">
  <path d="M0,25 L50,5"></path>
</svg>

M[x,y] 移动 到 x,y 点
L[x,y] 直线 到 x,y 点

二次贝塞尔曲线

html
<svg view="0 0 50 30" class="svg">
  <path d="M2,2 Q50,0 40,20"></path>
</svg>

Q[xq,yq x,y] 二次贝塞尔曲线 到 x,y 点 xq,yq 是控制点

三次贝塞尔曲线

html
<svg view="0 0 50 30" class="svg">
  <path d="M2,15 C10,0 30,30 40,15"></path>
</svg>

C[xq1,yq1 xq2,yq2 x,y] 三次贝塞尔曲线 到 x,y 点 xq1,yq1 xq2,yq2是两个控制点

椭圆弧

html
<svg view="0 0 50 30" class="svg">
  <path d="M2,15 A30,60 10,0,0 50,0"></path>
</svg>

A[r1,r1 r2,r2 x-axis-rotation,large-arc-flag,sweep-flag x,y] 椭圆弧 到 x,y 点

  • r1,r1 r2,r2 是椭圆的两个半径 半径相等就是正圆
  • x-axis-rotation 旋转角度 用来矫正椭圆弧线(正圆怎么转都是一个弧度)旋转 取值 0-360
  • large-arc-flag 是标记绘制大弧(1)还是小弧(0)部分
  • sweep-flag 是标记向顺时针(1)还是逆时针(0)方向绘制

来个恋爱过程

见面kisspa pa pa
光效流动方向PF 光效的长度10光效及线的粗细4光点数量25光点运动时间15

代码实现

点击展开
vue
<template>
  <div class="svgLine">
    <div class="content">
      <svg class="lineSvg" viewbox="0 0 660 200" preserveAspectRatio="none">
        <linearGradient id="PF" x1="0" y1="50%" x2="100%" y2="50%">
          <stop offset="0%" stop-color="rgba(250,113,133,0)" />
          <stop offset="100%" stop-color="rgba(250,113,133,1)" />
        </linearGradient>
        <linearGradient id="NF" x1="0" y1="50%" x2="100%" y2="50%">
          <stop offset="0%" stop-color="rgba(52,211,153,0)" />
          <stop offset="100%" stop-color="rgba(52,211,153,1)" />
        </linearGradient>
        <path :d="isPF(type) ? PFPath : NFPath" id="path" fill="none" :stroke-width="rectHeight"
          :class="['line-path']" />
        <rect v-if="show" :width="rectWidth" :height="rectHeight" :rx="rectHeight / 2" :y="-rectHeight / 2"
          :x="-rectWidth / 2" v-for="num in pointNumber" :key="num" :fill="`url(#${type})`">
          <animateMotion rotate="auto" :dur="pointDur" repeatCount="indefinite" :begin="lineMoveStartTime(num)">
            <mpath href="#path" />
          </animateMotion>
        </rect>
      </svg>
    </div>
    <div class="btn">
      <span>光效流动方向</span>
      <span>{{ type }}</span>
      <el-button @click="changeDirection">改变方向</el-button>
      <span>&nbsp;</span>
      <span>光效的长度</span>
      <span>{{ rectWidth }}</span>
      <el-button @click="changeRectWidth(true)">++</el-button>
      <el-button @click="changeRectWidth(false)">--</el-button>
      <span>光效及线的粗细</span>
      <span>{{ rectHeight }}</span>
      <el-button @click="changeRectHeight(true)">++</el-button>
      <el-button @click="changeRectHeight(false)">--</el-button>
      <span>光点数量</span>
      <span>{{ pointNumber }}</span>
      <el-button @click="changePointNumber(true)">++</el-button>
      <el-button @click="changePointNumber(false)">--</el-button>
      <span>光点运动时间</span>
      <span>{{ pointDur }}</span>
      <el-button @click="changePointDur(false)">++</el-button>
      <el-button @click="changePointDur(true)">--</el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick } from "vue";
// 控制光点变化时候的重新渲染
const show = ref(true);
// 光点数量
const pointNumber = ref(25);
// 光点运动时间
const pointDur = ref(15);
const PFPath = `M20,150 L70,150
  A15,15 0,0,1 85,160 L90,170 L110,60 L130,180 L160,10 L170,135 A15,15 0,0,0 185,150
  L300,150
  Q230,80 250,40 A44,44 0,0,1 330,50 A44,44 0,0,1 410,40 Q430,80 360,150
  L470,150
  Q480,100 470,60 A28,28 0,0,1 490,25 A10,12 0,0,1 510 25 A28,28 0,0,1 530,60 Q520,100 530,150
  L600,150`;
// 上面路径的返转
const NFPath = `M600,150 L530,150
  Q520,100 530,60 A28,28 0,0,0 510 25 A10,12 0,0,0 490,25 A28,28 0,0,0 470,60 Q480,100 470,150
  L360,150
  Q430,80 410,40  A44,44 0,0,0 330,50  A44,44 0,0,0 250,40 Q230,80 300,150
  L185,150
  A15,15 0,0,1 170,135 L160,10 L130,180 L110,60 L90,170 L85,160 A15,15 0,0,0 70,150
  L20,150`;
// 光点的方向
const type = ref("PF");
// 判断是否是正方向
const isPF = (type: string) => {
  return type === "PF";
};
// 光点运动开始时间
const lineMoveStartTime = (num: number) => {
  return ((-1 * pointDur.value) / pointNumber.value) * num;
};
// 光点的长度
const rectWidth = ref(10);
// 光点的粗细
const rectHeight = ref(4);
// 方向改变方法
const changeDirection = () => {
  if (type.value === "PF") {
    type.value = "NF";
  } else {
    type.value = "PF";
  }
};
// 光点长度改变方法
const changeRectWidth = (flag: boolean) => {
  let midNumber = rectWidth.value;
  midNumber += flag ? 1 : -1;
  rectWidth.value = Math.min(Math.max(midNumber, 1), 20);
};
// 光点粗细改变方法
const changeRectHeight = (flag: boolean) => {
  let midNumber = rectHeight.value;
  midNumber += flag ? 1 : -1;
  rectHeight.value = Math.min(Math.max(midNumber, 1), 10);
};
// 光点数量改变方法
const changePointNumber = (flag: boolean) => {
  show.value = false;
  nextTick(() => {
    let midNumber = pointNumber.value;
    midNumber += flag ? 2 : -2;
    pointNumber.value = Math.min(Math.max(midNumber, 1), 50);
    show.value = true;
  });
};
// 光点运动时间改变方法
const changePointDur = (flag: boolean) => {
  show.value = false;
  nextTick(() => {
    let midNumber = pointDur.value;
    midNumber += flag ? 2 : -2;
    pointDur.value = Math.min(Math.max(midNumber, 1), 30);
    show.value = true;
  });
};
</script>

<style lang="scss" scoped>
.svgLine {
  width: 100%;

  .content {
    overflow-x: auto;
  }

  .lineSvg {
    width: 660px;
    height: 200px;

  }

  .line-path {
    fill: none;
    stroke-linejoin: round;
    stroke-linecap: round;
    stroke: rgba(160, 160, 160, 0.2);
  }

  .btn {
    width: 100%;
    display: grid;
    grid-template-columns: repeat(4, 1fr);

    @media (max-width: 768px) {
      grid-template-columns: repeat(2, 1fr);
    }

    grid-gap: 10px;

    .el-button+.el-button {
      margin-left: 0;
    }
  }
}
</style>