一般來說,在H5開發中,使用canvas往往只是為了展示一些簡單的圖表或者簡單短小的動畫,很少考慮到有閃爍的問題。
最近,在手機QQ魔法表情的項目中,就遇到了奇葩的閃爍問題。
這里說的閃爍,是指動畫剛開始播放,突然出現瞬間空白(大概1幀到2幀的時間)。
閃爍分析
這個魔法表情,實際是html5版本的動畫,使用Fanvas(即將騰訊開源),從swf轉化為canvas 2d動畫。
在iOS體系下,無論哪個機型還是哪個系統版本,都沒有出現問題。
但是,在部分Android機器上則出現了很奇葩的閃爍,包括小米note,小米4,三星,魅族。奇怪的是,小米同體系的紅米note則完全正常。
翻閱H5 api的資料,我們知道requestAnimationFrame在Android 4.4后才支持,而動畫的機制是,如果該接口不可用,則采用setInterval取代。
那么貌似有點眉目了,紅米note也是4.4系統,而iOS全系都ok,也許問題就在這。
重溫一下FPS和瀏覽器重繪的知識。瀏覽器保持一個幀頻(一般60fps)刷新畫面,這就包括頁面中的canvas。而動畫的繪制過程,包括幾個步驟:
1、擦除整個canvas;
2、計算所有元件/圖元的位置顏色;
3、逐個逐個,繪制所有元件到canvas上。
這個過程,由不精准的setInterval驅動,這個時鍾無法跟瀏覽器重繪的頻率同步。
那么,就可能出現這樣的時序情況:
1、擦除整個canvas;
2、瀏覽器到達重繪時間點,此時canvas為空白,瀏覽器繪制空白的canvas;
3、50ms后,這一幀動畫所有元件繪制完成(可能會因為動畫復雜, 而消耗長時間,超過16ms)
關鍵點就在這里了。
好招不怕舊
雙緩沖,只要對圖形圖象處理編程有稍稍一些了解,都應該聽過這個術語,即使不知道這玩意是什么。這個技術非常非常古老,也非常非常簡單,但效果卻非常非常好。
來看看百度百科的說明,可能沒有wikipedia專業,但我覺得足夠解釋問題了。
閃爍是圖形編程的一個常見問題。需要多重復雜繪制操作的圖形操作會導致呈現的圖像閃爍或具有其他不可接受的外觀。雙緩沖的使用解決這些問題。雙緩沖使用內存緩沖區來解決由多重繪制操作造成的閃爍問題。當啟用雙緩沖時,所有繪制操作首先呈現到內存緩沖區,而不是屏幕上的繪圖圖面。所有繪制操作完成后,內存緩沖區直接復制到與其關聯的繪圖圖面。因為在屏幕上只執行一個圖形操作,所以消除了由復雜繪制操作造成的圖像閃爍。
回到我們的動畫中,發現異曲同工,閃爍、掉幀的問題根源就是因為部分機型下沒有自動實現cnavas的雙緩沖(一般這些都是底層實現的),而canvas每一幀動畫過程又比較漫長,擦除上一幀動畫后,要過幾十毫秒才能繪制完成下一幀,而這段時間內就會出現明顯的空白。
解決辦法就是:
創建一個臨時canvas,先把下一幀動畫繪制到臨時canvas上。在每次真正繪制的時候,擦除正式canvas后,馬上drawImage把臨時canvas的內容copy過去,而這個copy過程是非常非常高效的,所以基本可以杜絕閃爍。
具體代碼
p.update = function() { if (!this.cacheCanvas) { this.cacheCanvas = document.createElement("canvas"); this.cacheCanvas.width = this.canvas.width; this.cacheCanvas.height = this.canvas.height; } updateMovieClip(); //圖形變換 var ctx = this.cacheCanvas.getContext("2d"); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.save(); ctx.clearRect(0, 0, this.canvas.width + 1, this.canvas.height + 1); //部分Android機器很奇葩,如果局部刷新會出現空白的情況 drawMovieclip(ctx); //繪制 ctx.restore(); //雙緩沖,先畫到臨時canvas,再轉到正式canvas ctx = this.canvas.getContext("2d"); ctx.clearRect(0, 0, this.canvas.width + 1, this.canvas.height + 1); ctx.drawImage(this.cacheCanvas, 0, 0, this.canvas.width, this.canvas.height); };