canvas动画替代flash动画方案
最近工作不是很忙,所以借这个时间整理一下之前遇到的问题,记录一下相关的解决方案,这个问题是这样的,之前有一个项目是Electron开发的桌面客户端,里面有一个循环播放动画的功能,原先是由flash来实现的,由于flash不再受到支持,所以考虑用其他方案来替代。
动画本身不太复杂,我有考虑过使用css来实现,但是综合考虑之后,还是决定采用canvas来实现。
动画效果是这样的:将一张类似地图的背景图进行放大和移动,最终定位显示图上的一个目标点,然后在目标点处显示一个标记,标记上下跳动进行提示,然后标记消失,背景图缩小到原始大小再循环整个过程。
背景图的放大和移动是分多段进行的,即先放大移动到某个区域,然后再放大移动,每一段放大移动的位置和倍数都是定义在xml文件中,最后显示的标记位置也是定义在这个xml文件中。每次更换动画效果都是直接更新xml文件。
具体的xml文件如下:
1 <map> 2 <!-- scale是缩放的倍数,duration是这一阶段动画的时常,x,y是图片左上角在画布上的坐标 --> 3 <step scale="0.14" duration="1" x="-30" y="227"/> 4 <step scale="0.5" duration="1" x="-1903" y="-4"/> 5 <step scale="1" duration="5" x="-4489" y="-113"> 6 <!-- x,y是标记点在画布上的坐标, alpha是标记点的透明度--> 7 <point x="595" y="297.5" alpha="1"/> 8 </step> 9 </map>
动画绘制的思路是这样的:
- 读取到xml文件后,将配置转换为一个数组,数组每一项为一个step对象,如果step中由子元素则放在step对象的children属性中;
- 在页面上创建2个canvas元素,2个canvas重叠,下面的canvas绘制背景图片,上面的canvas绘制标记点,不设置背景的话,是不会出现遮盖的问题的;
- 循环绘制的思路,使用一个调度函数来控制动画的循环,使用2个绘制函数分别绘制背景和标记点
- 调度函数记录一个绘制次数count,当前绘制的step的index为count%数组的length,每绘制一次都会将count加1,由于标记点总在动画的最后一个阶段出现,所以如果当前的index小于下一个index,执行背景图绘制函数,否则执行标记点绘制函数以及还原背景图片状态。这里有一个需要注意的地方,即每次背景绘制都是根据当前step变为下一step,3个step其实只执行的2次函数。
- 背景图片绘制函数draw的逻辑是,接收3个参数,分别是当前step属性(s1),下一step属性(s2),以及duration每一帧动画的时常即step.duration / 60。动画的绘制频率为每秒60次,先计算出每次变化的x,y,scale,(s1.x - s2.x)/ 60,其他2个属性计算方法也一样。然后设置一个定时器,每step.duration / 60执行一次,每次绘制清空canvas然后将图片的x,y,scale,加上每次变动的值进行绘制,跳出定时器的条件为当前图片的属性等于目标属性,由于计算精度的问题,当两个值相差小于0.01时,认为已经绘制完毕,退出定时器,并且count++,再次执行调度函数
- 绘制标记点的逻辑也类似,但是又有点不一样,标记点需要先往上移动10像素,再往下移动10像素,然后再重复一次,总共4次,所以定时器就是每 5 / 4 秒执行60次,每移动10像素更改移动方向,并且总次数减1,退出定时器的条件为标记点往下移动结束,总次数为0。
示例代码如下:
1 <template> 2 <div class="viewer-container"> 3 <canvas 4 :width="width" 5 :height="height" 6 id="point-canvas" 7 ></canvas> 8 <canvas 9 :width="width" 10 :height="height" 11 id="canvas" 12 ></canvas> 13 </div> 14 </template> 15 16 <script> 17 export default { 18 props: { 19 width: { 20 type: Number, 21 default: 1080, 22 }, 23 height: { 24 type: Number, 25 default: 1125, 26 }, 27 frequency: { 28 type: Number, 29 default: 60, 30 }, 31 }, 32 data () { 33 return { 34 config: {}, 35 ctx: null, 36 pointCanvas: null, 37 img: null, 38 count: 0, 39 drawInterval: null, 40 paintControlInterval: null, 41 redPointTimeout: null, 42 } 43 }, 44 45 mounted () { 46 this.ctx = document.getElementById('canvas').getContext('2d') 47 this.pointCanvas = document.getElementById('point-canvas').getContext('2d') 48 }, 49 methods: { 50 setMapData (args) { 51 console.log('渲染进程收到的数据:', args) 52 53 // 清除所有定时器 54 this.clearAll() 55 // 将属性值由字符串转为数字 56 args.step.forEach((item) => { 57 item.attributes.duration = parseInt(item.attributes.duration) * 1000 58 item.attributes.scale = parseFloat(item.attributes.scale) 59 item.attributes.x = parseFloat(item.attributes.x) 60 item.attributes.y = parseFloat(item.attributes.y) 61 }) 62 this.config = args 63 this.img = new Image() 64 this.img.onload = () => { 65 if (args.hasOwnProperty('step')) { 66 this.paint() 67 } 68 } 69 this.img.src = this.config.imgPath 70 }, 71 clearAll () { 72 console.log(this.drawInterval, this.paintControlInterval, this.redPointTimeout) 73 if (this.drawInterval) clearInterval(this.drawInterval) 74 if (this.paintControlInterval) clearInterval(this.paintControlInterval) 75 if (this.redPointTimeout) clearTimeout(this.redPointTimeout) 76 this.ctx.clearRect(0, 0, 1080, 1125) 77 this.pointCanvas.clearRect(0, 0, 1080, 1125) 78 }, 79 paint () { 80 // 81 let length = this.config.step.length 82 let stepArr = this.config.step 83 84 let index = this.count % length 85 let attr = Object.assign({}, stepArr[index].attributes) 86 87 let nextIndex = (this.count + 1) % length 88 let nextAttr = Object.assign({}, stepArr[nextIndex].attributes) 89 90 // 如果当前的index小于下一个index,正常绘画 91 if (index < nextIndex) { 92 let duration = attr.duration / this.frequency 93 this.draw(attr, nextAttr, duration) 94 } else { 95 // 否则画出小红点,然后再等最后一个step的duration时间绘画为初始状态 96 97 this.pointControl(stepArr[index], attr.duration) 98 99 if (this.redPointTimeout) { 100 clearTimeout(this.redPointTimeout) 101 } 102 this.redPointTimeout = setTimeout(() => { 103 this.draw(attr, nextAttr, 1000 / this.frequency) 104 }, attr.duration - 1000) 105 } 106 }, 107 draw (attr, nextAttr, duration) { 108 let scaleStep = (nextAttr.scale - attr.scale) / this.frequency 109 let xStep = (nextAttr.x - attr.x) / this.frequency 110 let yStep = (nextAttr.y - attr.y) / this.frequency 111 112 // 设置定时器,将当前step的duration除以frequency,frequency就是每次循环的间隔 113 // 每次循环判断当前的scale与下一次的scale相差是否小于0.01 114 // 如果不是,则各属性增加,绘制画面 115 // 如果是,则认为当前step已经变形为下一step的形状,count增加,下一step变为当前step,清除定时器,重新开始绘画 116 if(this.drawInterval) { 117 clearInterval(this.drawInterval) 118 } 119 this.drawInterval = setInterval(() => { 120 if (Math.abs(attr.scale - nextAttr.scale) > 0.01) { 121 attr.scale = attr.scale + scaleStep 122 attr.x += xStep 123 attr.y += yStep 124 this.ctx.clearRect(0, 0, 1080, 1125) 125 this.ctx.drawImage(this.img, attr.x, attr.y, this.img.width * attr.scale, this.img.height * attr.scale) 126 } else { 127 this.count++ 128 clearInterval(this.drawInterval) 129 this.paint() 130 } 131 }, duration) 132 }, 133 134 pointControl (attr, duration) { 135 136 let point = Object.assign({}, attr.elements[0].attributes) 137 point.x = parseFloat(point.x) 138 point.y = parseFloat(point.y) 139 let img = new Image() 140 // 总循环次数 141 let count = 4 142 // 每次循环的总时间 143 let singleDuration = duration / count 144 // 每次定时器间隔时间 145 let intervalDuration = parseInt(singleDuration / this.frequency) 146 // y移动的总距离 147 let distance = 10 148 // y每次移动的距离 149 let singleDistance = distance / intervalDuration 150 img.onload = () => { 151 if(this.paintControlInterval) { 152 clearInterval(this.paintControlInterval) 153 } 154 this.paintControlInterval = setInterval(() => { 155 this.pointCanvas.clearRect(0, 0, 1080, 1125) 156 this.pointCanvas.drawImage(img, point.x - img.width / 2, point.y - img.height) 157 if (distance <= 0) { 158 point.y += singleDistance 159 } else { 160 point.y -= singleDistance 161 } 162 distance -= singleDistance 163 164 if (distance <= -10) { 165 if (count == 0) { 166 this.pointCanvas.clearRect(0, 0, 1080, 1125) 167 clearInterval(this.paintControlInterval) 168 } 169 distance = 10 170 count-- 171 } 172 }, intervalDuration) 173 } 174 img.src = this.config.point 175 }, 176 }, 177 } 178 </script> 179 180 <style> 181 .viewer-container { 182 position: relative; 183 } 184 #point-canvas { 185 position: absolute; 186 left: 0; 187 top: 0; 188 } 189 </style>