CSS技巧:逐帧动画抖动解决方案
笔者所在的前端团队主要从事移动端的H5页面开发,而团队使用的适配方案是: viewport units + rem。具体可以参见凹凸实验室的文章 – 利用视口单位实现适配布局 。
笔者目前(2017.08.12)接触到的移动端适配方案中,「利用视口单位实现适配布局」是最好的方案。不过使用 rem 作为单位会遇到以下两个难点:
- 微观尺寸(20px左右)定位不准
- 逐帧动画容易有抖动
第一个难点的通常出现在 icon 绘制过程,可以使用图片或者 svg-icon 解决这个问题,笔者强烈建议使用 svg-icon,具体理由可以参见:「拥抱Web设计新趋势:SVG Sprites实践应用」。
第二个难点笔者举个例子来分析抖动的原因和寻找解决方案。
一个抖动的例子
做一个8帧的逐帧动画,每帧的尺寸为:360×540。
JavaScript
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | .steps_anim {   position: absolute;   width: 9rem;   height: 13.5rem;   background: url(//misc.aotu.io/leeenx/sprite/m.png) 0 0 no-repeat;   background-size: 45rem 13.5rem;   top: 50%;   left: 50%;   margin: -5.625rem 0 0 -5.625rem;   animation: step 1.2s steps(5) infinite; } @keyframes step {   100% {     background-position: -45rem;   } } | 
观察在主流(手机)分辨率下的播放情况:
| iPhone 6 (375×667) | iPhone 6+ (414×736) | iPhone 5 (320×568) | Android (360×640) | 
|---|---|---|---|
|  |  |  |  | 
四种分辨率下,可以看到除了 ip6 其它的三种分辨率都发生了抖动。(ip6 不抖动的原因是适配方案是基本于 ip6 的分辨率订制的。)
分析抖动
图像由终端(屏幕)显示,而终端则是一个个光点(物理像素)组成的矩阵,换句话说图片也一组光点矩阵。为了方便描述,笔者假设终端上的一个光点代表css中的1px。
以下是一张 9px * 3px 的sprite:

每帧的尺寸为 3px * 3px,逐帧的取位过程如下:
把 sprite 的 background-size 的宽度取一半,那么终端会怎么处理?
9 / 2 = 4.5
终端的光点都是以自然数的形式出现的,这里需要做取整处理。取整一般是三种方式:round/ceil/floor。假设是 round ,那么 background-size: 5px,sprite 会是以下三种的一个:
| 情况一 | 情况二 | 情况三 | 
|---|---|---|
|  |  |  | 
理论上,5 / 3 = 1.666...。但实际上光点取整后,三个帧的宽度都不可能等于 1.666...,而是有一个帧的宽度降级为 1px(亏),另外两个宽度升级为 2px(盈),笔者把这个现象称作「盈亏互补」。
再看一下盈亏互补后,逐帧的取位过程:
| 情况一 | 情况二 | 情况三 | 
|---|---|---|
|  |  |  | 
可以看到由于盈亏互补导致了三个帧的宽度不一致,亏的那一帧在动画中的表示就是抖动。
笔者总结抖动的原因是:sprite在尺寸缩放后,帧与帧之间的盈亏互补现象导致动画抖动
附注:1px 由几个光点表示是由以终端的 dpr 决定
解决方案
「盈亏互补」也可以说是「盈亏不一致」,如果尺寸在缩放后「盈亏一致」那么抖动现象可以解决。
解决构想一
笔者根据「盈亏一致」设计了「解决构想一」:

根据上图,其实很容易就联想到一个简单的方案:不用雪碧图(即一帧对应一张图片)。
这个方案确实是可以解决抖问题,不过笔者并不推荐使用它,因为它有两个负面的东西:
- KB变大与请求数增多
- 多余的 animation 代码
这个方案很简单,这里就不赘述了。
解决构想二
把逐帧取位与图像缩放拆分成两个独立的过程,就是笔者的「解决构想二」:
实现「构想二」,笔者首先想到的是使用 transform: scale(),于是整理了一个实现方案A:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | .steps_anim {   position: absolute;   width: 360px;   height: 540px;   background: url(//misc.aotu.io/leeenx/sprite/m.png) 0 0 no-repeat;   background-size: 1800px 540px;   top: 50%;   left: 50%;   transform-origin: left top;   margin: -5.625rem 0 0 -5.625rem;   transform: scale(.5);   animation: step 1.2s steps(5) infinite; } @keyframes step {   100% {     background-position: -1800px;   } } /* 写断点 */ @media screen and (width: 320px) {  .steps_anim {  transform: scale(0.4266666667);  } } @media screen and (width: 360px) {  .steps_anim {  transform: scale(0.48);  } } @media screen and (width: 414px) {  .steps_anim {  transform: scale(0.552);  } } | 
这个实现方案A存在明显的缺陷:scale 的值需要写很多断点代码。于是笔者结全一段 js 代码来改善这个实现方案B:
css:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | .steps_anim {   position: absolute;   width: 360px;   height: 540px;   background: url("//misc.aotu.io/leeenx/sprite/m.png") 0 0 no-repeat;   background-size: 1800 540px;   top: 50%;   left: 50%;   transform-origin: left top;   margin: -5.625rem 0 0 -5.625rem;   animation: step 1.2s steps(5) infinite; } @keyframes step {   100% {     background-position: -1800px;   } } | 
javascript:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | // 以下代码放到<head></head>中// <![CDATA[ document.write(" .steps_anim {scale(.5); } "); function doResize() {   scaleStyleSheet.innerHTML = ".steps_anim {-webkit-transform: scale(" + (document.documentElement.clientWidth / 750) + ")}"; } window.onresize = doResize; doResize(); // ]]> | 
通过改善后的方案 CSS 的断点没了,感觉是不错了,不过笔者觉得这个方案不是个纯粹的构建方案。
我们知道<img> 是可以根据指定的尺寸自适应缩放尺寸的,如果逐帧动画也能与 <img> 自适应缩放,那就可以从纯构建角度实现「构想二」。
SVG刚好可以解决难题!!!SVG 的表现与 <img>类似同时可以做动画。以下是笔者的实现方案C。
html:
JavaScript
| 1 2 3 | <svg viewBox="0, 0, 360, 540" class="steps_anim">   <image xlink:href="//misc.aotu.io/leeenx/sprite/m.png" width="1800" height="540" /> </svg> | 
css:
JavaScript
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | .steps_anim {   position: absolute;   width: 9rem;   height: 13.5rem;   top: 50%;   left: 50%;   margin: -5.625rem 0 0 -5.625rem;   image {    animation: step 1.2s steps(5) infinite;   } } @keyframes step {   100% {     transform: translate3d(-1800px, 0, 0);   } } | 
方案C的改良
实现方案C很好地解决了方案A和方案B的缺陷,不过方案C也有它的问题:不利于自动化工具去处理图片。
自动化工具一般是怎么处理图片的?
自动化工具一般是扫描 CSS 文件找出所有的 url(...) 语句,然后再处理这些语句指向的图片文件。
如果 可以改用 CSS 的 background-image 就可以解决这个问题,不过 SVG 不支持 CSS 的 background-image。但是,SVG有一个扩展标签:foreignObject,它允许向 插入 html 代码。在使用它前,先看一下它的兼容情况:

iOS 与 Android 4.3 一片草绿兼容情况算是良好,笔者实机测试腾讯 X5 内核的浏览器兼容仍旧良好。以下是改良后的方案。
html:
JavaScript
| 1 2 3 4 5 | <svg viewBox="0, 0, 360, 540" class="steps_anim">   <foreignObject class="html" width="360" height="540">     <div class="img"></div>   </foreignObject> </svg> | 
css:
JavaScript
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | .steps_anim {   position: absolute;   width: 9rem;   height: 13.5rem;   top: 50%;   left: 50%;   margin: -5.625rem 0 0 -5.625rem; } .html {  width: 360px;  height: 540px; } .img {  width: 1800px;  height: 540px;  background: url(//misc.aotu.io/leeenx/sprite/m.png) 0 0 no-repeat;   background-size: 1800px 540px;  animation: step 1.2s steps(5) infinite; } @keyframes step {   100% {     background-position: -1800px 0;   } } | 
改良后的方案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
CSS技巧:逐帧动画抖动解决方案的更多相关文章
- 逐帧动画抖动、适配布局、SVG Sprites
		笔者所在的前端团队主要从事移动端的H5页面开发,而团队使用的适配方案是: viewport units + rem.具体可以参见凹凸实验室的文章 – 利用视口单位实现适配布局 . 笔者目前(2017. ... 
- 利用css3-animation来制作逐帧动画
		前言 趁着还没有元旦之前先码一篇文章,不然到时候估计又被各种虐了,所以趁现在还有力气先来一篇.今天来聊聊css3中的动画属性animation,对这个属性懵懂是在很早的时候有前辈用这个 animati ... 
- css3 实现逐帧动画
		css3 实现逐帧动画 实现逐帧动画需要使用到的是Animation动画,该CSS3的Animation有八个属性:分别是如下:1: animation-name2: animation-durati ... 
- 过渡与动画 - 逐帧动画&steps调速函数
		写在前面 上一篇中我们熟悉五种内置的缓动曲线和(三次)贝塞尔曲线,并且基于此完成了缓动效果. 但是如果我们想要实现逐帧动画,基于贝塞尔曲线的调速函数就显得有些无能为力了,因为我们并不需要帧与帧之间的过 ... 
- 使用node.js开发一个生成逐帧动画小工具
		在实际工作中我们已经下下来不下于一万个npm包了,像我们熟悉的 vue-cli,react-native-cli 等,只需要输入简单的命令 vue init webpack project,即可快速帮 ... 
- Android动画效果之Frame Animation(逐帧动画)
		前言: 上一篇介绍了Android的Tween Animation(补间动画) Android动画效果之Tween Animation(补间动画),今天来总结下Android的另外一种动画Frame ... 
- Android 逐帧动画
		原理: 逐帧动画是最简单的一种动画.原理就是把几张图片连续显示出来,以达到动画的效果.就相当于下面这种手绘翻页动画啦~ 实现: 1.需要建立一个animation-list来设置静态图片资源.持续时间 ... 
- 逐帧动画(Frame-by-frame Animations)
		1.这一类动画可以创建一个Drawable序列,这些Drawable可以按照指定的时间间歇一个一个的显示. xml定义方法 <animation-list xmlns:android=" ... 
- Esfog_UnityShader教程_逐帧动画
		有段日子没出这个系列的新文章了,今天就拿一个比较常见也比较基础的利用改变Shader来改变不断调整UV实现播放逐帧动画的小功能.很久没写了就当练练手了.在新版本的Unity中早就已经集成了Sprite ... 
随机推荐
- iOS: hide UITextField Cursor
			Simply subclass UITextField and override caretRectForPosition (继承 UITextField 并且重写 [UITextField care ... 
- 114自定义UITableViewCell(扩展知识:为UITableViewCell添加动画效果)
			关键操作: 效果如下: ViewController.h #import <UIKit/UIKit.h> @interface ViewController : UITableViewCo ... 
- SharePoint 2013 处理Promoted Links类型的List下的Tiles View的默认Webpart展示方式
			问题: 为了去掉Photo Gallery的水平滚动效果,更改为根据内容进行自适应宽度多行显示效果 Promoter link --> photo gallery Not horizontal ... 
- VCL 中的 Windows API 函数(4): AdjustWindowRectEx
			AdjustWindowRectEx 用在了 Forms.DBCtrls 单元. AdjustWindowRectEx 可以根据窗口样式获取的边缘尺寸. 测试: var R: TRect; beg ... 
- Java集合之LinkedList源码解析
			LinkedList简介 LinkedList基于双向链表,即FIFO(先进先出)和FILO(先进后出)都是支持的,这样它可以作为堆栈,队列使用 继承AbstractSequentialList,该类 ... 
- Bat注释符号
			打开命令显示:echo on关闭命令显示:echo off, @ echo off, (加@表示连echo off都不显示,不然会显示出echo off的命令) rem : 注释, 表示不执行rem ... 
- 关于error:Cannot assign to 'self' outside of a method in the init family
			有时候我们重写父类的init方法时不注意将init后面的第一个字母写成了小写,在这个方法里面又调用父类的初始化方法(self = [super init];)时会报错,错误信息如下:error:Can ... 
- Git学习笔记(三)
			Git提交相关内容 在Git提交时,会保存一个提交对象,该对象包括一个指向暂存区内容快照的指针,包括本次提交作者等相关附属信息,包括零个或多个指向该提交对象的父对象指针:首次提交时是没有祖先,普通提交 ... 
- 8 -- 深入使用Spring -- 3...2 ResouceLoader 接口和 ResourceLoaderAware 接口
			8.3.2 ResouceLoader 接口和 ResourceLoaderAware 接口 Spring 提供如下两个标志性接口: ⊙ ResourceLoader : 该接口实现类的实例可以获得一 ... 
- laravel 5.3升级5.4
			1)修改 composer 配置文件 composer.json 1.如果你用了 laravel-admin,larvel-admin 版本改 1.4.x-dev 2.laravel 版本改 5.4. ... 
