记一次CSS3和SVG实现箭头拐弯动画

这不最近我司的设计师又给笔者整活了,在数据大屏页面的中间位置,做了一个效果图,不过需要做一个箭头沿着路径实现拐弯的动画效果;这可咋整呢,本文就结合CSS3的特性和svg,看一下实现的思路。

实现方案

我们先来看一下最终的实现效果:

面对这种复杂的(奇奇怪怪)动画需求,作为老前端人了,上来肯定就是质问设计师:

你能不能做个GIF图?

直接贴个GIF图不就完事了,不过,设计给出了自己理由,最后出图的文件太大以及GIF图容易模糊,另一个原因其实是我们的设计做动画不专业;因此,在得到肯定不能的答复之后,设计给出的方案是:使用蒙版动画

所谓的蒙版动画,也就是;类似遮罩层动画,将一个遮罩层首先覆盖在想要动画的物体上方,比如我们这里的箭头;然后随着时间的推移,逐渐的抽离遮罩层,实现动画的目的。

不过这种的动画效果用在这里感觉有点low,不是很合适;因此我们还是来一点点实现吧。

offset-path

就在不知怎么做的时候,好在上网查资料,一眼看到了CSS新的属性offset-path给了我一丝希望;传统的CSS3动画只能实现平移、缩放、拉伸等规则的路径动画。

而offset-path这个属性就比较强大了,可以让元素沿着指定的路径实现不规则路径,可以是任何的形状;我们看下其浏览器的支持程度:

它的语法很强大,支持画circle(圆)、ellipse(椭圆)或者polygon(折线)等多种图形的函数,不过我们这里就使用自定义的path函数即可,传入一个path路径:

CSS 复制代码
offset-path: path('<path-url>')

对路径语法不熟悉的小伙伴可以学习一下MDN路径的这篇文章;此外它还有两个重要的属性,一个是offset-distance,表示元素移动的距离,可以是px单位,也可以是百分比单位:

CSS 复制代码
offset-distance: <length> | <percentage>

我们可以控制offset-distance属性来实现元素的动画效果,一般动画时设置从0%到100%。

另一个属性就是offset-rotate,定义了元素运动时的角度和方向,可以是某个具体的角度,也可以是auto,也可以组合起来一起使用,auto表示让元素运动时自己根据轨迹调整角度即可:

CSS 复制代码
offset-rotate: [ auto | reverse ] || <angle>             

因此有了上面的属性,我只要画一个div,给它一个线性过渡的背景CSS,然后让它沿着指定路径移动不就行了,说干就干。

html 复制代码
<div class="page">
  <div class="box"></div>
</div>
<style lang="scss" scoped>
.box {
    width: 200px;
    height: 20px;
    background: linear-gradient(to left, #15db30, rgba(#15db30, 0.2));
    offset-path: path("M 10 80 C 80 10, 130 10, 190 80 S 300 150, 360 80");
    animation: move 4000ms infinite linear;
}
@keyframes move {
    0% {
        offset-distance: 0%;
    }
    100% {
        offset-distance: 100%;
    }
}
</style>

上面的path路径看着很复杂,其实就是一个简单的S型曲线的运动,这里我们让div沿着S型曲线运动;虽然想法很美好啦,不过现实也十分的现实,最后的效果如下:

因为我们的div是长条的,并不是流体,所以我们看到的效果就是一长条沿着固定的轨道晃晃悠悠的移动,这肯定不行了,不能用div了。

那怎么办呢,笔者又思考了很久,突然一个想法又冒出来了,想起了微积分的思路;同理,既然一个div太长了,那我把它切割成多个不就行了,只要我们的刀把div切得够细,再拼接起来,用户就看不出来是拼接的,说干就干。

html 复制代码
<template>
  <div class="page">
    <div :class="['box', `box${item}`]" v-for="item in 20" :key="item"></div>
  </div>
</template>
<style lang="scss" scoped>
.box {
    width: 20px;
    height: 20px;
    position: absolute;
    left: 0;
    top: 0;
}

@for $i from 1 through 20 {
    .box#{$i} {
        background: linear-gradient(
            to left,
            rgba(#15db30, 1 - ($i - 1) * 0.05),
            rgba(#15db30, 1 - $i * 0.05)
        );
        offset-path: path("M 10 80 C 80 10, 130 10, 190 80 S 300 150, 360 80");
        offset-rotate: auto;
        animation: move 10000ms infinite linear;
        animation-delay: 500ms * $i;
    }
}
</style>

这里代码看着很复杂,其实很简单,首先我们循环了20个div,然后用scss的@for语法给20个div设置样式,这里我们给每个div一个背景颜色的从右到左线性过渡,逐渐透明,最后使用animation-delay设置一个延迟时间,效果如下:

最后的效果就像一条贪吃蛇一样,从头连到尾;不过这样的实现方式可能会有问题,如果delay的间隔太久,div之间就会有空隙,如果间隔太小,就会导致div之间有颜色重叠的部分,导致颜色排布不是很均匀。

不过我们可以通过将div设置成宽只有几个px的长方形条状,同时把div切割的数量拉大,切成100或者200的div,这样就会好很多。

SVG动画

箭头拐弯的效果我们已经实现的差不多了,只要再给它加上合适的路径即可;下面那么我们再来考虑一下,如何把箭头动画结合到背景中去;在切图时,我们肯定是需要将背景中的建筑元素切到单独的一张图片,但是箭头的轨道如果我们自己用代码实现会非常棘手。

因此笔者的想法是将轨道单独让设计切图出来,导出成svg,我们就可以参考svg中的代码,能够知道轨道实现的逻辑了;因此我们将整体放到SVG中实现起来会比较方便一点。

stroke-dasharray

首先我们看一下轨道的实现方式,轨道环的实现很简单,直接使用path,设置stroke颜色和一个opacity就可以:

html 复制代码
<path
    d="M152.444292,118.156631 L492.312457,209.128474 C501.980784,211.716376 509.532541,219.268132 512.120443,228.936459 L603.092286,568.804625 C607.090738,583.742725 598.2224,599.093834 583.2843,603.092286 C578.541228,604.361855 573.547697,604.361855 568.804625,603.092286 L228.936459,512.120443 C219.268132,509.532541 211.716376,501.980784 209.128474,492.312457 L118.156631,152.444292 C114.158179,137.506192 123.026516,122.155083 137.964617,118.156631 C142.707689,116.887062 147.70122,116.887062 152.444292,118.156631 Z"
    transform="rotate(-45) translate(-315.5, 114)"
    fill="none"
    stroke="#2A73E1"
    stroke-width="8"
    opacity="0.2"
></path>

我们重点来看下轨道中间的点,这里就不得不说到svg的stroke-dasharray属性,这个属性可以用来控制实现描边的点的图案样式;它的语法很简单,就是传入一个数列:

css 复制代码
stroke-dasharray="none | <dasharray>"

这个数列中的数用逗号和空格间隔开,比如"5, 3, 2"这种形式,那么每个数字代表什么意思呢?我们先从简单的一个数字和两个数字开始:

html 复制代码
<path
    d="M 0,0 L 300, 0"
    stroke-dasharray="10"
></path>
<path
    d="M 0,0 L 300, 0"
    stroke-dasharray="20 10"
></path>

stroke-dasharray中的每个数字依次来表示短划线和缺口的长度,当一个数字10的时候,表示短划线和缺口的长度都是10,因此我们就会看到它的距离是比较均匀的;而两个数字的时候,第一个数字表示短划线,第二个数字表示缺口,因此我们看到短划线较长,而缺口较短。

理解了上面数字的含义,我们再扩展到三个数字和四个数字来看一下;如果是奇数数字的话会比较特殊,根据MDN文档上的解释:

如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。因此,5,3,2 等同于 5,3,2,5,3,2。

由于奇数个数字在循环的时候会有一个位置衔接不上,因此这个属性定义的时候就将奇数个自动扩展到了偶数个,我们看下具体代码理解一下:

html 复制代码
<path
    d="M 0,0 L 300, 0"
    stroke-dasharray="60, 30, 40"
></path>
<path
    d="M 0,0 L 300, 0"
    stroke-dasharray="60, 30, 40, 50"
></path>

我们在每个短划线和缺口处用数字标记一下:

我们发现,3个数字的时候,在第一次排列之后,第二次排列的时候,60所在的位置自动变成了缺口位,而不是短划线,这样自动进行了一次顺序扩展,这就是这个属性将奇数个自动扩展到了偶数个的效果;而4个数字则是照常循环排列。

理解了stroke-dasharray属性,我们的轨道也可以在svg下来最终完成了。

animateMotion

下面的轨道有了,我们就需要将切好的箭头放到svg中,在svg中,我们使用rect来代替div;那么,如何让rect动起来呢?

svg也有自己的动画元素,这里使用animateMotion元素,它的作用就是让一个元素如何沿着运动路径进行移动。它的用法也很简单,在元素下层嵌入animateMotion元素,最重要的属性就是path,相当于CSS3中的offset-path属性:

svg 复制代码
<rect
    width="3"
    height="6"
    :fill="`url(#orangeGradient${item})`"
    v-for="item in 20"
    :key="item"
    transform="translate(-3, -3)"
>
    <animateMotion
        path="M583.2843,603.092286 C598.2224,599.093834 607.090738,583.742725 603.092286,568.804625 L568.5,440"
        :begin="10 * item + 'ms'"
        dur="3s"
        repeatCount="indefinite"
        rotate="auto"
    ></animateMotion>
</rect>

rotate属性也相当于CSS3中的offset-rotate,因此我们理解了上面CSS中的offset-*等一系列属性,animateMotion也就很好理解了。

viewBox自适应

通过width/height属性,我们可以设置svg画布的固定大小:

html 复制代码
<svg width="200" height="200"></svg>

但是,在实际的场景中,我们经常需要让画布自适应外部div的宽高,以实现画布呈现大小适应页面的缩放;这里就要用到svg另一个属性viewBox了,我们看下mdn上对这个属性的介绍:

viewBox属性允许指定一个给定的一组图形伸展以适应特定的容器元素。

它的属性值是一个包含四个参数的列表:min-x,min-y,width,height,四个值可以用空格或者逗号分隔开;

html 复制代码
<svg viewBox="min-x, min-y, width, height">

viewBox顾名思义就是视图盒子,把它理解成截图工具呈现的效果就行;简单理解,min-x和min-y就是截图的右上角的x和y坐标,width和height就是截图区域的宽度。

我们看下它的具体效果,首先,不带viewBox的情况下展示svg下的内容:

html 复制代码
<svg
    width="200"
    height="200"
    style="border: 1px solid red"
>
    <rect x="0" y="0" width="50" height="40" />
    <circle cx="10" cy="20" r="4" fill="white" />
</svg>

元素正常大小显示,这时候我们给它加一个viewBox:

html 复制代码
<svg
    width="200"
    height="200"
    viewBox="0 0 60 60"
>
    <!-- 其他元素 -->
</svg>

我们会发现两个元素放大显示了,这个也很好理解,我们在200200的画布上截出一个6060的区域,然后就会等比例放大呈现出来。

因此回到我们的箭头svg,为了实现自适应的效果,我们去掉svg的宽高,加上viewBox等于我们的画布宽高即可:

html 复制代码
<svg
    viewBox="0 0 730 561"
>
    <!-- 其他元素 -->
</svg>

点击查看本文的实现效果

总结

我们从CSS3的offset-*属性入手,了解了如何让一个元素实现非规则路径下的动画效果;然后我们为了方便,将动画效果迁移到了svg中去实现,对svg中的关键属性stroke-dasharray进行了详细的学习;最后为了实现自适应效果,使用了viewBox属性。

参考

svg viewBox与自适应

如果觉得写得还不错,敬请关注我的掘金主页。更多文章请访问谢小飞的博客

相关推荐
Jiaberrr6 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele3 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀3 小时前
CSS——属性值计算
前端·css
DOKE4 小时前
VSCode终端:提升命令行使用体验
前端
xgq4 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试