1. 程式人生 > 其它 >canvas動畫替代flash動畫方案

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>