我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细

大家好!我是猫小白,本期给大家带来一个简单的实战demo,希望大家食用得开心,有收获。
==首先声明:此demo存粹为了学习其中的动画知识,请不要用于真实的场景,否则被JC叔叔抓起包吃包住就不应该了瑟!==
此项目基于==成都天府健康通==,其它地区的场所码不知道是否有类似的动画,如果没有也没关系,可以学习下是如何实现的。
为啥子要做这个呢?那是因为有一天我出去买菜,回来保安叔叔恩是要我扫一下场所码,作为一个5星好公民,肯定是非常配合的掏出手机开始扫 。叮~,屏幕上出现一个不挺放大缩小的箭头。我就想,要不用代码来实现一下这个动画吧。
于是就有了这篇文章,我一共是实现了4种方法:
  1. 第一种,css,零js代码(最简单)
  2. 第二种,svg,零js代码 (较简单)
  3. 第三种,js+dom (较复杂)
  4. 第四种,js+canvas (较复杂)
全文源码地址:点击跳转到github,可以拉下来亲操练操练,顺便给一个start!
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

接下来,我们一个一个来分析如何实现的。
准备工作 首先我们假装出去买个菜(未解封的同胞除外例如上海的朋友),回来的时候去保安那里故作镇定,连贯的掏出手机打开天府健康通(成都的哈)扫一下,然后给保安亮一下,不退出界面,淡定回家。
回到家后,拿出手机,打开刚刚的界面,截一个图:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

加入html中,作为div容器的背景图片,然后我们创建一个白色的框把中间的圆圈和勾遮住,因为我们要自己实现其中的动画效果。
实现效果如下:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

遮板的css代码:
/**遮板**/ .shutter { position: absolute; display: block; width: 120px; height: 120px; background-color: #fff; top: 206px; left: 150px; }

核心html代码结构如下,:

只是列举关键的代码,完整代码请到github仓库查看。
准备工作完成,开始正片!
第一种,css,零js代码 使用css3animation动画,可以很容易实现这样的效果,不用一行js代码。单关键是要看明白我们目标动画是由哪些部分组成的,动画是怎么变化的。所以先再次看看这个gif,注意其中动画部分:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

不难发现:
  • 圆圈是由小到大,再由大到小
  • 内圈放大到一定程度时,外圈(扩散部分)出现,并且半径不停增大,当内圈放到最大时,扩散圈消失
  • 内部的勾也会一起放大缩小,频率和内圈相同
    根据上面的规律,我们先实现一个内圈(绿色圈部分)
内部圈css代码:
/**内部圆圈**/ .circle { position: absolute; z-index: 9; width: var(--circle-size); height: var(--circle-size); top: 218px; left: 165px; background-color: var(--theme-green); /* background-color: red; */ border-radius: 45px; animation: circleMove 0.7s alternate infinite cubic-bezier(1, 1, 1, 1); }

--circle-size、--theme-green是我们在开头定义的变量,方便后续使用。
:root { --theme-green: #4ba776; /**主题绿色**/ --circle-size: 90px; /**圆圈大小 宽高一样**/ }

内部圈画好了,还差一个白色的"√",为了简单我直接使用了"√"这个字符串,加入伪类元素的content中。你们也可以用图片或者svg。
"√"的css代码如下:
/**内部的勾,使用`.circle`的`before`伪类**/ .circle::before { z-index: 2; //z-index要比外圈(阔散圈)大,否则要被背景覆盖 position: absolute; display: block; content: "?"; font-size: 63px; color: #fff; left: 18px; top: -1px; }

让我们预览下现在的效果:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

哟也~看起来是那么回事了。
现在我们按照内圈相同的颜色和大小,画一个外部圈(扩散圈)。
参考上面的"√"的实现,我们用after伪类来实现:
/**动画时外部渐变圈*/ .circle::after { z-index: 1; //z-index要比内部的勾小,否则要覆盖掉勾 position: absolute; display: block; content: ""; width: calc(var(--circle-size)); height: calc(var(--circle-size)); box-sizing: border-box; border-radius: 50%; background-color: #4ba776; transform: scale(1.2); /**外圈动画 一轮时间是内圈的2倍 内部圈放大0.7s+缩小0.7s=1.4sinfinite无限重复**/ /* animation: outCircleMove 1.4s infinite cubic-bezier(1, 1, 1, 1); */ }

内圈加入放大缩小动画,在.circle中计入animation
/**内部圆圈**/ .circle { position: absolute; z-index: 9; width: var(--circle-size); height: var(--circle-size); top: 218px; left: 165px; background-color: var(--theme-green); /* background-color: red; */ border-radius: 45px; /*加入放大缩小动画alternate:轮流播放,放大再放小infinite:无限播放ease-in-out:变化函数*/ animation: circleMove 0.7s alternate infinite ease-in-out; } /**定义放大缩小动画,最大放大到1.2倍**/ @keyframes circleMove { 0% { transform: scale(1); } 100% { transform: scale(1.2); } }

解读:时间设置为0.7s,我们定义了circleMove 动画,意思是在0秒的时候圆圈保持1倍大小,在0.1~0.7s圆圈逐渐变为1.2倍大小。变化的快慢是由animation-timing-function参数决定的,文中设置的ease-in-out是进入和退出较慢,中间速度较快的一种变化曲线。
我们可以打开开发者工具,点击animation属性的曲线图标,查看和选择想要的动画曲线
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片
【我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细】好内圈动画已经完成,我们来看下效果:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

perfect!现在我们加入外圈动画,让我们回顾一下:外圈动画是要在内圈放大到一定时间外圈才出现,我们暂定1/5,也就是20%的时候显示并且开始放大(放大对应属性:scale);放大后外圈会越来越便透明,直到内圈放到最大,此时完全透明,感觉像是扩散的效果(透明对应属性:opacity)。
知道这些后,我们设置外圈动画如下:
/*动画时外部渐变圈/
.circle::after { z-index: 1; position: absolute; display: block; content: ""; width: calc(var(--circle-size)); height: calc(var(--circle-size)); box-sizing: border-box; border-radius: 50%; background-color: #4ba776; /**外圈动画 一轮时间是内圈的2倍 内部圈放大0.7s+缩小0.7s=1.4sinfinite无限重复**/ animation: outCircleMove 1.4s infinite ease-in-out; } /**定义扩散圈的放大缩小动画**/ @keyframes outCircleMove { /**0%~50% 0s~ 0.7s前的时间 此时内圈放大,扩散圈等待时机出现**/ 0% { opacity: 0; } /**10% 此时内圈已经放大一段时间, 扩散全显示opacity设为1,倍率为1 **/ 10% { opacity: 1; transform: scale(1); } /**50%到100%是动画0.7s~1.4s的时间 此时内圈缩小,扩散圈变为1.3倍,但是透明度设为0**/ 50%, 100% { transform: scale(1.3); opacity: 0; } }

至此就大功告成了,看下效果:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

第二种,svg,零js代码 (较简单) 上面了利用css3的动画属性,操作起来非常简单。现在介绍一种用svg动画的方式实现相同的效果。我们知道svg也是有比较丰富的动画特性支持。
我们简单列举一下svg实现动画的几种方法:
1.set
set元素是SVG动画元素中最简单的元素。在经过特定时间间隔后,它只是将属性设置为特定值。因此,形状不会连续进行动画处理,而只是更改属性值一次。
2.animate
元素通常放置到一个SVG图像元素里面,用来定义这个图像元素的某个属性的动画变化过程。
3.animateMotion
元素也是放置一个图像元素里面,它可以引用一个事先定义好的动画路径,让图像元素按路径定义的方式运动。
4.animateTransform
动画上一个目标元素变换属性,从而使动画控制平移,缩放,旋转或倾斜。
简单了解过后,我们今天主要用animate来实现动画。
首先,我们还是拿出上面的基础布局,祖传代码(背景+遮罩)

基本框架的样式和上一节css动画的样式相同,就不重复粘贴了。
然后我们画一个圆和勾,也就是内部的部分;svg中画圆是用,于是我们有这样的代码:

效果如下:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

继续添加中间的白色"√"。为了简介,我们用文字标签:
添加后的代码如下:
?

添加文字后效果如下:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

好了,内圈添加好了,我们再添加一个外圈(扩散圈),还是用,大小和位置都和内圈一模一样,这里就不粘贴代码了。
现在来添加动画,根据上一章节的css3动画,我们需要不挺的放大缩小内圈的大小。所以我们通过动画改编圆的半径就ok了。上面介绍了标签可以动态改变某一属,这里的属性就是半径R:
...其它代码 ...其它代码

实现效果:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

完美!看起来就是那么回事,后面的步骤其实都是依葫芦画瓢。
添加内部"√"的动画,用改变font-size
?

添加后效果我们来看下:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

最后,我们添加扩散效果。扩散效果其实分两步,第一步是放大半径,另一步是从不透明到透明的过程。所以我们用改变半径的同时,改变它的透明度。
...其它代码 ...其它代码

最终效果如下:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

第三种,js+dom (较复杂) 前面都是通过css或者svg自带特性完成的动画效果,下面这种是通过js动态改变圆圈div大小实现的,较为复杂,理解起来需要一定的js基础,在做项目优先选择css3,我们一起来看下是如何实现的。
首先,我们还是拿出上面的基础布局,添加内外圈的div和布局。
关键css部分:
/**外部圆圈**/ .circle, .outCircle { z-index: 1; position: absolute; width: var(--circle-size); height: var(--circle-size); top: 218px; left: 165px; background-color: var(--theme-green); /* background-color: red; */ border-radius: 45px; } /**内部的勾**/ .circle::before { position: absolute; display: block; content: "?"; font-size: 63px; color: #fff; left: 18px; top: -1px; }

html部分:

至此,元素已经画好了,就差动画部分了,简单分析一波:
内圈需要逐渐放大再逐渐放小,我们用一个变量size_inner来代表放大缩小的倍数,STEP_INNER代表内圈递增或者递减的步长,最终设置到style上的transform属性。
外圈也需要有一个放大过程,且放大的速度比内圈快,我们用size_outer来代表放大倍数,STEP_OUT代表外圈递增步长。
外圈需要一个透明度逐渐小的值opacity_out ,透明度变化的步长用OPA_STEP代替,最终设置style上的opacity属性。
动画是持续不断的,所以我们需要不停的执行,先暂时采用setInterval函数;
放大到一定倍数然后再缩小,最大倍数我们用MAX_SCALE常量代表,如果递增到最大然后STEP_INNER设置为负数,意味着开始缩小。
最终的源代码如下:

因篇幅有限,不逐一解释各行代码的含义,有不理解的可以私聊我。
最终效果:
我用四种方式实现某场所码箭头动画(css3+canvas+svg+js)讲解超详细
文章图片

众所周知,因为浏览器的eventloop机制,setInterval往往并不能按照设置的时间执行,在稍微复杂一些的页面中会出现卡顿。固可采用requestAnimationFrameapi代替。
代码如下:

第四种,js+canvas (较复杂) 经过上面章节的洗礼,canvas版本和js+dom版本非常相似,唯一的不同就是绘画圆或者文字的api不同。
我们分析下要点:
  1. 需要在画布上用arcapi绘制2个圆,一个内圈一个外圈,并且不停改变圆圈的大小或透明度。
  2. 需要用fillText绘制一个?字样,并且改变字体大小。
  3. 需要不停执行绘制函数,这里使用流畅的requestAnimationFrameapi。
  4. 每次绘制前,需清空画布clearRect
    其实原理和第三种差不多,都需要用变量记录内部圈变化的大小,到最大值后方向递减。外部圈是需要等到内圈放大到一定倍数(1.1)倍,才显示出来,并且放大速度要比内圈快,随着外圈变大,透明度逐渐减小到0。
我想第四种和第三种其实都有这样的逻辑,为此我花了一点事件把这个动作进行了封装,采用了一种订阅发布模式来实现。
为了大家浏览我这篇文章的同时,除了能学习到动画知识,也能学习到一些其它的比较重要的js知识。大家应该了解或者听过js的各种模式,什么工厂模式、单列模式、观察者模式、发布订阅模式等等,都是学习到了理论知识。今天我就用这个例子介绍下发布订阅模式是如何在实践中使用的。
刚我们分析了canvas实现的方式和js+dom的方式,这两种方式都需要用变量来记录一个值(这里是放大倍数),而且每次执行渲染函数时都要递增一个常量值(成为步长),到达某个最大值(比如1.3倍大小)后开始反向递减(步长乘以-1),然后递减到最初值(1倍大小)。同时还有一个细节,当内圈递增到一定倍数(比如1.1倍)要让外圈开始加入动画,当内圈递增到最大时,外圈要消失(透明度设置为0)
基于这个分析,我们得出封装的类需要设计成这样:
/*** * 放大缩小数字变化类 * 根据放大缩小初始值和步长等递增 * 提供到达某一数值时的回调 * @params {Number} from 数字初始值 * @params {Number} to 数字最大值 * @params {Number} step 数字步长 * @params {Array} dingNums 达到dingNums的区间触发回调 只再放大轮回触发 */ export default class MoveCrycle { constructor({ from = 0, to = 100, step = 1, dingNums = [] }) { this.from = from; this.to = to; this.step = step; this.dingNums = dingNums; this.currValue = https://www.it610.com/article/this.from; this._turnNum = 0; //放大缩小轮次 一次放大一次缩小为一轮 this._event = { onchange: [], //每一次变化触发的回调 onlarge: [], //每次到最大值触发的回调 onsmall: [], //每次到最小值触发的回调 onding: [], //每次到dingNums区间值时触发的回调 }; this._init(); } _init() { //注册放大事件 //记录轮次 每次放大+1 this.on("onlarge", function turn() { this._turnNum++; }); } //开始滚动数字 jump() { if (this.step > 0) { //放大过程 if (this.currValue <= this.to) { //递增过程中 this.currValue += this.step; //判断是否在dingNums区间 if ( this.currValue >= this.dingNums[0] && this.currValue <= this.dingNums[1] ) { this._emit("onding", this.currValue, this._turnNum); } } else { this._emit("onsmall", this.currValue); this.currValue = https://www.it610.com/article/this.to; this.step *= -1; } } else { //缩小过程 if (this.currValue> this.from) { this.currValue += this.step; } else { //缩到初始值了 重新放大 this.step *= -1; this.currValue = https://www.it610.com/article/this.from; this._emit("onlarge", this.currValue); } } this._emit("onchange", this.currValue, this.step > 0, this._turnNum); } //触发事件函数 _emit(type, ...args) { //触发每次变动函数 this._event[type].forEach((f) => { try { f.apply({}, args); } catch (e) { console.error(e); } }); }//注册回调 on(type, func) { if ( this._event[type] && !this._event[type].filter((f) => f === func).length ) { this._event[type].push(func); } } //注销回调 off(type, func) { if (this._event[type]) { this._event[type] = this._event[type].filter((f) => f !== func); } } }

一看比较长,别急慢慢分析:
  1. constructor部分就是接收入参,没有什么好说的。需要注意的是this._init(),初始化时自己也注册一个onlarge事件,来记录每次轮回的次数_turnNum
  2. jump是核心代码,主要管理每一次被执行时的递增或递减状态,同时通过this.emit()函数触发响应的回调事件。放大过程中判断是否到达dingNums的区间,如果在其中则不停触发onding事件,当到达最大值时触发onsmall事件代表即将变小,同时this.step *= -1步长取反方向值。到达最小值时触发onlarge事件,代表即将增大,同时this.step *= -1步长取反方向值。jump函数并没有这个类中进行递归调用,是因为把这个调用权交给使用者会更加灵活,后面会介绍如何调用。
  3. _emit函数是触发事件的控制中心,根据传入的type类型找到事件对象中对应的函数数组,遍历执行。用try{} catch(e){}是为了避免在某个回调函数中出错而影响了其它回调函数正常执行。
  4. onoff顾名思义,就是注册和注销回调的入口。on函数把传入的事件类型和函数加入对应的数组中,如果重复添加同一个函数是无效的。off函数接收事件类型和函数,然后数组中过滤掉。这里留给你想像一下,如果用匿名函数注册的事件能取消掉吗?
好了,MoveCrycle类封装好了,回到我们的主题,看下如何在这个箭头动画中使用。
HTML部分

script部分
引入MoveCrycle类,初始化
import MoveCrycle from "./js/MoveCrycle.js"; let cavas = document.getElementById("canvas"); var ctx = cavas.getContext("2d"); //定义的一些常量和变量下面会用到 const R_SMALL = 45; //内圈初始大小 ...省略部分代码//看着里!!!!初始化封装的类 let move = new MoveCrycle({ from: R_SMALL, //初始值 to: R_SMALL * 1.35, //最大1.35倍 step: 0.3, //步长 dingNums: [R_SMALL * 1.1, R_SMALL * 1.35], //外圈开始出现的区间内圈的1.1倍到1.35倍 }); ...

注册onchange事件,监听每一次变化,然后渲染画布
//注册数值变化事件,每次变化获取值渲染 move.on("onchange", (value, step) => { //绘制内圆 clear(); //清空画布 //绘制内圈 drawInnerCircle(value); //绘制外圈 drawOutCircle(); //文字放在最后,不然会被上面的圆属性fillStyle覆盖 drawText(value); });

注册onding事件,控制外圈的变大,透明度减小
//再设定的区间触发事件,控制外圈的变大,透明度减小 move.on("onding", (value) => { r_out += R_OUT_STEP; //外圈变大 opa -= OPA_STEP; //外圈变大的同时透明度减小 });

注册onlarge事件,控制外圈
//动画圈开始变大控制外圈 move.on("onlarge", (value) => { opa = 1; //开始变大,透明度为1 r_out = 45; //外部开始变大时半径回复到初始值 });

注册onsmall事件,控制外圈透明度为0
move.on("onsmall", (value) => { //控制外圈光环-隐藏 opa = 0; });

绘制内圈、外圈、文字函数
//绘制内圆 function drawInnerCircle(val) { ctx.beginPath(); ctx.arc(SIZE / 2, SIZE / 2, val, 0, 2 * Math.PI); ctx.fillStyle = `rgba(${COLOR}, 1)`; ctx.fill(); } //绘制外圈圆 变大的同时透明度下降 function drawOutCircle() { ctx.beginPath(); ctx.arc(SIZE / 2, SIZE / 2, r_out, 0, 2 * Math.PI); ctx.fillStyle = `rgba(${COLOR}, ${opa})`; ctx.fill(); } //放大字体 大小随内圈一起变化 function drawText(val) { //font放大倍数和内圈一样 ctx.font = (val / R_SMALL) * 60 + "px Arial"; ctx.textBaseline = "center"; //设置字体底线居中 ctx.textAlign = "center"; //设置字体对齐方式居中 ctx.fillStyle = `rgba(255,255,255,1)`; ctx.fillText("?", 70, 90); }

清除画布
//清除画布 function clear() { ctx.clearRect(0, 0, SIZE, SIZE); }

循环递归函数
//循环递归函数 (function start() { move.jump(); //开始执行 requestAnimationFrame(() => { start(); }); })

script部分完整代码:

以上。
完整代码可以到我的github仓库获取!
各位大佬,不要忘了给我点赞+评论+收藏

    推荐阅读