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

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

实现方案

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

箭头拐弯效果

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

你能不能做个GIF图?

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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

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

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

offset-path

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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

浏览器支持度

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

1
offset-path: path('<path-url>')

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

1
offset-distance: <length> | <percentage>

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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

1
offset-rotate: [ auto | reverse ] || <angle>             

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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型曲线运动;虽然想法很美好啦,不过现实也十分的现实,最后的效果如下:

offset-path效果

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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就可以:

1
2
3
4
5
6
7
8
<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属性,这个属性可以用来控制实现描边的点的图案样式;它的语法很简单,就是传入一个数列:

1
stroke-dasharray="none | <dasharray>"

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

1
2
3
4
5
6
7
8
<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

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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

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

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

1
2
3
4
5
6
7
8
<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>

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

stroke-dasharray

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

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

animateMotion

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

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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画布的固定大小:

1
<svg width="200" height="200"></svg>

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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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

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

1
<svg viewBox="min-x, min-y, width, height">

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

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

1
2
3
4
5
6
7
8
<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属性

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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

1
2
3
4
5
6
7
<svg
width="200"
height="200"
viewBox="0 0 60 60"
>
<!-- 其他元素 -->
</svg>

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

带viewBox属性

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

1
2
3
4
5
<svg
viewBox="0 0 730 561"
>
<!-- 其他元素 -->
</svg>

  点击查看本文的实现效果

总结

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

参考

svg viewBox与自适应


谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。