canvas動畫替代flash動畫方案
阿新 • • 發佈:2021-06-10
最近工作不是很忙,所以借這個時間整理一下之前遇到的問題,記錄一下相關的解決方案,這個問題是這樣的,之前有一個專案是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>