什么😮,微信小程序中要实现这样的动画只能这样做 🤔

1,922 阅读7分钟

前言

通常我们开发微信小程序,需要产品经理需要我们前端来写一些动画(小声bb,用 svg 图片不就好了🙃)效果如下

动画-1719559280395-11.gif

可能你会觉得这很简单不是,使用 <svg> 去绘制不就行了,很遗憾,微信小程序到现在还不支持 svg

没办法,我们只能采取折中的办法,使用 canvas 绘制线条,css 绘制动画

实现

因为是在微信小程序中实现这样的路径动画,为了快速构建,我们使用uniapp

并且方便在 vscode 中开发我们可以使用 unibest 这个快速开发模板,来加快我们的构建

创建 path-animation 项目

pnpm create unibest my-project
cd path-animation
pnpm i
pnpm dev:mp-weixin

image-20240627173319393.png

使用微信小程序开发者工具进入目录中,打开 mp-weixin 文件,去掉云服务,设置项目名为 path-animation

image-20240627174241448.png

好了,我们看下效果

image-20240627180524596.png

修改下首页文件 index.vue

image-20240627180652248.png

image-20240627180744557.png

ok 😃,我们先来使用 canvas 来画一条直线

<route lang="json5" type="page">
{
  layout: 'default',
  style: {
    navigationBarTitleText: '',
  },
}
</route>

<template>
  <div
    class="w-300px h-300px flex justify-between items-center flex-col m-auto mb-30px position-relative mt-80rpx"
  >
    <canvas
      id="my-canvas"
      canvas-id="my-canvas"
      class="position-absolute w-300px h-300px z-0"
    ></canvas>
  </div>
</template>

<script lang="ts" setup>
const context = uni.createCanvasContext('my-canvas')
context.setStrokeStyle('#FBD3A7') // 设置 填充的颜色
context.moveTo(50, 150) // 设置 绘制起始坐标
context.lineTo(250, 150) // 设置 绘制结束坐标
context.stroke() // stroke() 方法会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径
context.draw() // 绘制
</script>

<style lang="scss" scoped>
//
</style>

微信小程序的 canvas 一定要设置 canvas-id不然会出问题,我们就设置和 id 一样就好了

使用 uni.createCanvasContext(元素的id) 来获取访问的 <canvas> 对象

  • setStrokeStyle 设置 线的颜色
  • moveTo 设置 绘制起始坐标
  • lineTo 设置 绘制结束坐标
  • stroke stroke 方法会实际地绘制出通过 moveTo()lineTo() 方法定义的路径
  • draw 根据设置绘制图像

看一下效果,不错 😄,线条已经出来了

image-20240627183014453.png

如果直接使用 canvas 来绘制运动动画,需要计算距离,如果有弧度还得计算贝塞尔曲线很是麻烦

转换个思路 🤔,我们可以使用 css 动画来实现运动

<route lang="json5" type="page">
{
  layout: 'default',
  style: {
    navigationBarTitleText: '',
  },
}
</route>

<template>
  <div
    class="w-300px h-300px flex justify-between items-center flex-col m-auto mb-30px position-relative mt-80rpx"
  >
    <canvas
      id="my-canvas"
      canvas-id="my-canvas"
      class="position-absolute w-300px h-300px z-0"
    ></canvas>
    <div class="triangle"></div>
  </div>
</template>

<script lang="ts" setup>
const context = uni.createCanvasContext('my-canvas')
context.setStrokeStyle('#FBD3A7') // 设置 填充的颜色
context.moveTo(50, 150) // 设置 绘制起始坐标
context.lineTo(250, 150) // 设置 绘制结束坐标
context.stroke() // stroke() 方法会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径
context.draw() // 绘制
</script>

<style lang="scss" scoped>
.triangle {
  position: absolute;
  width: 8px;
  height: 8px;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  background: #fbd3a7;
  animation: move 3000ms infinite linear;
  offset-path: path('M -100 150 L 100 150');
}

@keyframes move {
  0% {
    offset-distance: 0%;
  }
  100% {
    offset-distance: 100%;
  }
}
</style>

动画.gif

image-20240628102937031.png

image-20240628104020844.png

使用 clip-pathpolygon 主要用于绘制多边形,当然 clip-path 还有很多其他有趣的属性可以看看

polygon 的使用也很简单,polygon(x1 y1, x2 y2, x3 y3, x4 y4, xn yn)

每两个点就是绘制的多边形的一个点,polygon(0 0, 100% 50%, 0 100%) 绘制三个点就是一个三角形,background 填充背景色

offset-path 是这个运动动画的核心,用于定于一个运动路径

  • M 定义路径起始点
  • L 定义路径结束点

最后使用 animation 定义运动动画

当然这是直线运动,那曲线运动呢,该怎么实现呢?🤔

<route lang="json5" type="page">
{
  layout: 'default',
  style: {
    navigationBarTitleText: '',
  },
}
</route>

<template>
  <div
    class="w-300px h-300px flex justify-between items-center flex-col m-auto mb-30px position-relative mt-80rpx"
  >
    <canvas
      id="my-canvas"
      canvas-id="my-canvas"
      class="position-absolute w-300px h-300px z-0"
    ></canvas>
    <div class="triangle"></div>
  </div>
</template>

<script lang="ts" setup>
const context = uni.createCanvasContext('my-canvas')
context.setStrokeStyle('#FBD3A7') // 设置 填充的颜色
context.moveTo(50, 150) // 设置 绘制起始坐标
context.lineTo(140, 150) // 设置 绘制结束坐标
context.quadraticCurveTo(150, 150, 150, 160)
context.lineTo(150, 200)
context.stroke() // stroke() 方法会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径
context.draw() // 绘制
</script>

<style lang="scss" scoped>
.triangle {
  position: absolute;
  width: 8px;
  height: 8px;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  background: #fbd3a7;
  animation: move 3000ms infinite linear;
  offset-path: path('M -100 150 L -10 150 Q 4 150 4 160 L 4 200');
}

@keyframes move {
  0% {
    offset-distance: 0%;
  }
  100% {
    offset-distance: 100%;
  }
}
</style>

动画-1719545640565-4.gif

image-20240628113745532.png

如果有多个lineTo,除了最后一个是结束坐标,其他都是绘制的中途点

quadraticCurveTo(控制点,结束点),使用这个绘制二次贝赛尔曲线,起始点就是上一步绘制的中途点,也就是 lineTo(140,150)这个点

image-20240628114134195.png

image-20240628114345249.png

css 动画部分也就修改了下路径而已,其中 Q 表示绘制贝赛尔曲线,其中 4 150 表示控制点,4 160 表示结束点,与 canvasquadraticCurveTo 类似

想必你在看了第一个动画的时候就会想,既然都是 canvas 路径 与 offset-path 路径,都是路径那会不会是一样的参数 🤨

我们来对比下,canvas 路径 与 offset-path 路径

image-20240628142215701.png

canvasoffset-path 各自的 5个点分别对应图上的 1~5

但是我们看实际坐标

image-20240628142557352.png

这样按顺序一个个对比下来,我们发现只有 x 坐标不同,y 坐标都是一样的

image-20240628142747140.png

为什么会这样呢?🤔

其实啊,是因为 canvasoffset-path 的初始坐标系不同

image-20240628143651305.png

所以,坐标才会变成

  • 50 -> -100

  • 140 -> -10

  • 150 -> 4

  • 150 -> 4

  • 150 -> 4

前两个换算还算正确,最后三个为什么不是 0 呢?

是因为啊,我们需要加上 元素自身宽度的一半,8 - 4 = 4,这样三角形才会居中在线上

这样太麻烦了,有没有什么办法不用换算直接就能用的方法

因为位移元素元素是 position: absolute ,那么是不是可以使用 left、top 来定义元素初始位置呢?

.triangle {
  position: absolute;
  top: 0;
  left: 0;
  width: 8px;
  height: 8px;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  background: #fbd3a7;
  animation: move 3000ms infinite linear;
  offset-path: path('M 50 150 L 140 150 Q 150 150 150 160 L 150 200');
}

动画-1719559280395-11.gif

完整案例代码

<route lang="json5" type="page">
{
  layout: 'default',
  style: {
    navigationBarTitleText: '',
  },
}
</route>

<template>
  <div
    class="w-300px h-300px flex justify-between items-center flex-col m-auto mb-30px position-relative mt-80rpx b-1px b-solid b-red"
  >
    <canvas
      id="my-canvas"
      canvas-id="my-canvas"
      class="position-absolute w-300px h-300px z-0"
    ></canvas>
    <div class="triangle"></div>
  </div>
</template>

<script lang="ts" setup>
const context = uni.createCanvasContext('my-canvas')
context.setStrokeStyle('#FBD3A7') // 设置 填充的颜色
context.moveTo(50, 150) // 设置 绘制起始坐标
context.lineTo(140, 150) // 设置 中途点
context.quadraticCurveTo(150, 150, 150, 160) // 绘制二次贝赛尔曲线
context.lineTo(150, 200) // 绘制结束坐标
context.stroke() // stroke() 方法会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径
context.draw() // 绘制
</script>

<style lang="scss" scoped>
.triangle {
  position: absolute;
  top: 0;
  left: 0;
  width: 8px;
  height: 8px;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  background: #fbd3a7;
  animation: move 3000ms infinite linear;
  offset-path: path('M 50 150 L 140 150 Q 150 150 150 160 L 150 200');
}

@keyframes move {
  0% {
    offset-distance: 0%;
  }
  100% {
    offset-distance: 100%;
  }
}
</style>

案例代码在路径动画

小结

因为微信小程序不支持 svg,所以我们采用 canvas + css 来代替

使用 canvas 这几个属性进行绘制

  • setStrokeStyle 设置 线的颜色
  • moveTo 设置 绘制起始坐标
  • lineTo 设置 绘制结束坐标
  • stroke stroke 方法会实际地绘制出通过 moveTo()lineTo() 方法定义的路径
  • draw 根据设置绘制图像

运动元素,采用 position: absolute top: 0 left: 0 进行定位,确定 offset-path位移的初始点为 (0,0)

这样就可以和 canvas 的路径一样了

最后我们使用 animation 定义一下动画就可以了