Description
本文是早期译版,未经校审。仅供参考。
Frame-by-frame animations
逐帧动画
Prerequisites
- Basic CSS animations
- the {#secret-xxx}
背景知识
- 基本的 CSS 动画
- “{$secret$}” 攻略(第 {$page$} 页)
The problem
难题
Quite often, we need an animation that is difficult or impossible to achieve by transitioning CSS properties on elements. For example, a cartoon moving or a complex progress indicator. In this case, image-based frame-by-frame animations are perfect, but surprisingly challenging to accomplish on the Web in a flexible manner.
在很多时候,我们需要一个动画,但这个动画又很难(或不可能)只通过某些 CSS 属性的过渡来实现。比如说,一段卡通影片,或是一个复杂的进度指示框。在这种场景下,基于图片的逐帧动画才是完美选择,但是,想在网页中以一种灵活的方式来实现这种动画,可谓是一项惊人的挑战啊。
At this point, you might be wondering, “Can’t we just use animated GIFs?” The answer is yes, for many cases, animated GIFs are perfect. However, they have a few shortcomings that might be a deal-breaker for certain use cases:
看到这里,你可能会产生这样一种疑问:“难道我们用 GIF 动画不行吗?” 对大多数情况来说,答案是 “行”,GIF 动画可以完美胜任。但是,GIF 动画也有一些短板,在某些场景下可能会让整体效果大打折扣:
- They are limited to a 256 color palette, shared across all frames.
- GIF 图片的所能使用的颜色数量被限制在 256 色。
- They cannot have alpha transparency, which can be a big problem when we don’t know what will be underneath our animated GIF. For example, this is very common with progress indicators (see Figure XX.XX).
- GIF 不具备 Alpha 透明的特性。当我们不确定 GIF 动画的下层是什么东西时,这往往就是一个大问题。比如对于加载提示来说,半透明效果是十分常见的(参见 图 8.13)。
- There is no way to modify certain aspects from within CSS, such as duration, repetitions, pausing, and so on. Once the GIF is generated, everything is baked into the file and can only be changed by using an image editor and generating another file. This is great for portability, but not for experimentation.
- 我们无法在 CSS 层面修改动画的某些参数,比如动画的持续时间、循环次数、是否暂停等等。一旦 GIF 动画生成之后,上述所有参数就固定在文件内部了;如果想作修改,就只能动用图像处理软件去重新生成一个 GIF 文件。从内聚性的角度来看,这种特性确实不错,但对调试过程来说就相当不便。
图 8.13
A semi-transparent progress indicator (on dabblet.com); this is impossible to achieve with animated GIFs
半透明的加载提示(来自 dabblet.com 网站);这个效果用 GIF 动画是无法实现的。
Back in 2004, there was an effort by Mozilla to address the first two issues by allowing frame-by-frame animation in PNG files, akin to the way we can have both static and animated GIF files. It was called APNG and was designed to be backward compatible with non-supporting PNG viewers, by encoding the first frame in the same way as traditional PNG files, so old viewers would at least display that. Promising as it was, APNG never got enough traction and to this day, has very limited browser and image editor support.
在 2004 年的时候,Mozilla 发起了一个建议:在 PNG 格式中增加对逐帧动画的支持,就像 GIF 格式同时支持静态图像和动画一样。这种格式被称作 “APNG”,它在设计时就考虑到了如何向后兼容——它会把动画的第一帧画面以传统 PNG 文件的方式进行编码,因此,对于那些不支持 APNG 特性的旧版看图软件来说,至少可以把第一帧正常显示出来。尽管它看起来很有前途,但 APNG 并没有获得足够的推广,时至今日,只有极少数的浏览器和图像处理软件支持这种格式。
{原书注释!}
For more information about APNG, see wikipedia.org/wiki/APNG.
关于 APNG 的更多信息,请参阅 wikipedia.org/wiki/APNG。
Developers have even used JavaScript to achieve flexible frame-by-frame animations in the browser, by using an image sprite and animating its background-position
with JS. You can even find small libraries to facilitate this! Is there a straightforward way to achieve this with only nice, readable CSS code?
后来,网页开发者们甚至会动用 JavaScript 来在浏览器中实现灵活的逐帧动画,比如用一张拼合图片(image sprite)作为背景,然后用 JS 去动态地控制它的 background-position
。你甚至还可以找到一些专门为此设计的小型类库!我们有没有可能只借助清爽易读的 CSS 代码就把这个需求给搞定呢?
The solution
解决方案
Let’s assume we have all frames of our animation in a PNG sprite like the one shown in Figure XX.XX.
假设我们已经把动画中的所有帧全部拼合到一张 PNG 图片中了,如 图 8.14 所示。
图 8.14
Our spinner’s eight frames (dimensions: 800×100)
旋转菊花所需要的八帧画面已经合并到一起了(图片尺寸为 800×100)。
We also have an element that will hold the loader (don’t forget to include some text, for accessibility!), to which we have already applied the dimensions of a single frame:
然后我们用一个元素来容纳这个加载提示(别忘了在里面写一些文字,来确保可访问性!),并把它的宽高设置为单帧的尺寸:
<div class="loader">Loading...</div>
.loader {
width: 100px; height: 100px;
background: url(img/loader.png) 0 0;
/* Hide text */
/* 把文本隐藏起来 */
text-indent: 200%;
white-space: nowrap;
overflow: hidden;
}
图 8.15
The first frame of our loader shows, but there is no animation yet
加载提示的第一帧显示出来了,但还没有动画效果。
Currently, the result looks like Figure XX.XX: the first frame is displayed, but there is no animation. However, if we play with different background-position
values, we will notice that -100px 0
gives us the second frame, -200px 0
gives us the third frame, and so on. Our first thought could be to apply an animation like this:
目前它的效果就是 图 8.15 中的这个样子了:第一帧显示出来了,但还没有动画效果。但是,如果我们尝试对它应用各种不同的 background-position
值,就会发现 -100px 0
会让它显示出第二帧,-200px 0
会得到第三帧,以此类推。于是我们的第一反应就是用下面的方法来让它动起来:
@keyframes loader {
to { background-position: -800px 0; }
}
.loader {
width: 100px; height: 100px;
background: url(img/loader.png) 0 0;
animation: loader 1s infinite linear;
/* Hide text */
/* 把文本隐藏起来 */
text-indent: 200%;
white-space: nowrap;
overflow: hidden;
}
However, as you can see in the following stills (taken every 167ms), this doesn’t really work:
但是,在下面这几幅静态截图(动画每经历 167ms 时的情形)中,你会发现这样其实是行不通的:
图 8.16
Our initial attempt for a frame-by-frame animation failed, as we did not need interpolation between keyframes
我们为了实现逐帧动画所做出的首次尝试失败了,因为我们并不需要帧与帧之间的过渡状态。
It might seem like we’re going nowhere, but we are actually very close to the solution. The secret here is to use the steps()
timing function, instead of a Bézier-based one.
看起来似乎我们走错了路,但其实我们离真正的答案已经相当接近了。这里的秘诀在于,采用 steps()
这个调速函数,而不是基于贝塞尔曲线的调速函数。
“The what timing function?!” you might ask. As we saw in the previous chapter, all Bézier-based timing functions interpolate between keyframes to give us smooth transitions. This is great; usually, smooth transitions are exactly the reason we are using CSS transitions or animations. However, in this case, this smoothness is destroying our sprite animation.
“采用哪个调速函数?!” 你可能会这样问。就像我们在前一节所看到的,所有基于贝塞尔曲线的调速函数都会在关键帧之间进行插值运算,从而产生平滑的过渡效果。这个特性很棒,因为在通常情况下,平滑的过渡确实是我们使用 CSS 过渡和动画的原因。但在眼前的这个场景下,这种平滑特性恰恰毁掉了我们想实现的逐帧动画效果。
Very unlike Bézier timing functions, steps()
divides the whole animation in frames by the number of steps you specify and abruptly switches between them with no interpolation. Usually this kind of abruptness is undesirable, so steps()
is not talked about much. As far as CSS timing functions go, Bézier-based ones are the popular kids that get invited to all the parties and steps()
is the ugly duckling that nobody wants to have lunch with, sadly. However, in this case, it’s exactly what we need. Once we convert our animation to the following, our loader suddenly starts working the way we wanted it to:
跟贝塞尔曲线调速函数迥然不同的是,steps()
会根据你指定的步进数量,把整个动画切分为多帧,而且整个动画会在帧与帧之间硬切,而不会做任何插值处理。通常这种硬切效果是我们极力避免的,因此我们很少听到关于 steps()
的讨论。在 CSS 调速函数的世界里,贝塞尔曲线家的孩子就像是处处受众人追捧的校花校草,而 steps()
则是旁人避之唯恐不及的丑小鸭。不过,在我们这个案例中,后者却是我们通向成功的关键。一旦我们把整个动画的代码修改为下面的形式,这个加载提示就瞬间变成我们想要的样子了:
animation: loader 1s infinite steps(8);
图 8.17
A comparison of
steps(8)
,linear
and the default timing function,ease
对比
steps(8)
、linear
以及默认的ease
这三种调整函数的差异。
Keep in mind that steps()
also accepts an optional second parameter, start
or end
(default) that specifies when the switch happens on every interval (see Figure XX.XX for the default behavior of end
), but that is rarely needed. If we only need a single step, there are also shortcuts: step-start
and step-end
, which are equivalent to steps(1, start)
and steps(1, end)
, respectively.
别忘了 steps()
还接受可选的第二个参数,其值可以是 start
或 end
(默认值)。这个参数用于指定动画在每个循环周期的什么位置发生帧的切换动作(关于默认值 end
的行为,参见 图 8.17),但实际上这个参数用得并不多。如果我们只需要一个单步切换效果,还可以使用 step-start
和 step-end
这样的简写属性,它们分别等同于 steps(1, start)
和 steps(1, end)
。
{致敬}
Hat tip to Simurai for coming up with this useful technique in Sprite sheet animation with steps().
向 Simurai 脱帽致敬,感谢他在《用
steps()
实现拼合图片的动画效果》这篇文章中提出了这个实用的技巧。
Related specs
相关规范