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>

动画绘制的思路是这样的:

  1. 读取到xml文件后,将配置转换为一个数组,数组每一项为一个step对象,如果step中由子元素则放在step对象的children属性中;
  2. 在页面上创建2个canvas元素,2个canvas重叠,下面的canvas绘制背景图片,上面的canvas绘制标记点,不设置背景的话,是不会出现遮盖的问题的;
  3. 循环绘制的思路,使用一个调度函数来控制动画的循环,使用2个绘制函数分别绘制背景和标记点
  4. 调度函数记录一个绘制次数count,当前绘制的step的index为count%数组的length,每绘制一次都会将count加1,由于标记点总在动画的最后一个阶段出现,所以如果当前的index小于下一个index,执行背景图绘制函数,否则执行标记点绘制函数以及还原背景图片状态。这里有一个需要注意的地方,即每次背景绘制都是根据当前step变为下一step,3个step其实只执行的2次函数。
  5. 背景图片绘制函数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++,再次执行调度函数
  6. 绘制标记点的逻辑也类似,但是又有点不一样,标记点需要先往上移动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>

 

posted @ 2021-06-10 15:45  KlllB  阅读(484)  评论(0编辑  收藏  举报