CSS技巧:逐幀動畫抖動解決方案

發表於2017-08-16

筆者所在的前端團隊主要從事移動端的H5頁面開發,而團隊使用的適配方案是: viewport units + rem。具體可以參見凹凸實驗室的文章 – 利用視口單位實現適配佈局

筆者目前(2017.08.12)接觸到的移動端適配方案中,「利用視口單位實現適配佈局」是最好的方案。不過使用 rem 作為單位會遇到以下兩個難點:

  • 微觀尺寸(20px左右)定位不準
  • 逐幀動畫容易有抖動

第一個難點的通常出現在 icon 繪製過程,可以使用圖片或者 svg-icon 解決這個問題,筆者強烈建議使用 svg-icon,具體理由可以參見:「擁抱Web設計新趨勢:SVG Sprites實踐應用」。

第二個難點筆者舉個例子來分析抖動的原因和尋找解決方案。

一個抖動的例子

做一個8幀的逐幀動畫,每幀的尺寸為:360×540。

觀察在主流(手機)解析度下的播放情況:

iPhone 6
(375×667)
iPhone 6+
(414×736)
iPhone 5
(320×568)
Android
(360×640)
20170815-ip6 20170815-ip6+ 20170815-ip5-3 20170815-android

四種解析度下,可以看到除了 ip6 其它的三種解析度都發生了抖動。ip6 不抖動的原因是適配方案是基本於 ip6 的解析度訂製的。)

分析抖動

影像由終端(螢幕)顯示,而終端則是一個個光點(物理畫素)組成的矩陣,換句話說圖片也一組光點矩陣。為了方便描述,筆者假設終端上的一個光點代表css中的1px。

以下是一張 9px * 3px 的sprite:

20170814-1

每幀的尺寸為 3px * 3px,逐幀的取位過程如下:
20170814-2

把 sprite 的 background-size 的寬度取一半,那麼終端會怎麼處理?
9 / 2 = 4.5
終端的光點都是以自然數的形式出現的,這裡需要做取整處理。取整一般是三種方式:round/ceil/floor。假設是 round ,那麼 background-size: 5px,sprite 會是以下三種的一個:

情況一 情況二 情況三
20170814-3 20170814-4 20170814-5

理論上,5 / 3 = 1.666...。但實際上光點取整後,三個幀的寬度都不可能等於 1.666...,而是有一個幀的寬度降級為 1px(虧),另外兩個寬度升級為 2px(盈),筆者把這個現象稱作「盈虧互補」。

再看一下盈虧互補後,逐幀的取位過程:

情況一 情況二 情況三
20170814-3 20170814-4 20170814-5

可以看到由於盈虧互補導致了三個幀的寬度不一致,虧的那一幀在動畫中的表示就是抖動

筆者總結抖動的原因是:sprite在尺寸縮放後,幀與幀之間的盈虧互補現象導致動畫抖動

附註:1px 由幾個光點表示是由以終端的 dpr 決定

解決方案

「盈虧互補」也可以說是「盈虧不一致」,如果尺寸在縮放後「盈虧一致」那麼抖動現象可以解決。

解決構想一

筆者根據「盈虧一致」設計了「解決構想一」:

20170814-6

根據上圖,其實很容易就聯想到一個簡單的方案:不用雪碧圖(即一幀對應一張圖片)
這個方案確實是可以解決抖問題,不過筆者並不推薦使用它,因為它有兩個負面的東西:

  • KB變大與請求數增多
  • 多餘的 animation 程式碼

這個方案很簡單,這裡就不贅述了。

解決構想二

把逐幀取位與影像縮放拆分成兩個獨立的過程,就是筆者的「解決構想二」:
20170814-7

實現「構想二」,筆者首先想到的是使用 transform: scale(),於是整理了一個實現方案A:

這個實現方案A存在明顯的缺陷:scale 的值需要寫很多斷點程式碼。於是筆者結全一段 js 程式碼來改善這個實現方案B:

css:

javascript:

通過改善後的方案 CSS 的斷點沒了,感覺是不錯了,不過筆者覺得這個方案不是個純粹的構建方案。

我們知道<img> 是可以根據指定的尺寸自適應縮放尺寸的,如果逐幀動畫也能與 <img> 自適應縮放,那就可以從純構建角度實現「構想二」。

SVG剛好可以解決難題!!!SVG 的表現與 <img>類似同時可以做動畫。以下是筆者的實現方案C。

html:

css:

方案C的改良

實現方案C很好地解決了方案A和方案B的缺陷,不過方案C也有它的問題:不利於自動化工具去處理圖片

自動化工具一般是怎麼處理圖片的?
自動化工具一般是掃描 CSS 檔案找出所有的 url(...) 語句,然後再處理這些語句指向的圖片檔案。

如果 可以改用 CSS 的 background-image 就可以解決這個問題,不過 SVG 不支援 CSS 的 background-image。但是,SVG有一個擴充套件標籤:foreignObject,它允許向 插入 html 程式碼。在使用它前,先看一下它的相容情況:

caniuse

iOS 與 Android 4.3 一片草綠相容情況算是良好,筆者實機測試騰訊 X5 核心的瀏覽器相容仍舊良好。以下是改良後的方案。

html:

css:

改良後的方案DEMO: http://jdc.jd.com/fd/promote/leeenx/201708/svg-sprite.html

總結

感謝閱讀完本文章的讀者。本文是筆者的個人觀點,希望能幫助到有相關問題的朋友,如果本文有不妥之處請不吝賜教。


參考資料:

https://stackoverflow.com/questions/9946604/insert-html-code-inside-svg-text-element
https://www.w3.org/TR/SVG/extend.html
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject

相關文章