CSS3:逐帧动画


前面我们花了很大的篇幅去讲解「补间动画」的开发,除了因为它最常见,还因为其中间的许多实现思路(如「动效的审查与分解」、「基于 AE 动效稿还原动画」)同样适用于本小节将要介绍的另一种常见的基础动画形式 —— 「逐帧动画」。

什么是逐帧动画

逐帧动画的英文名字是 Frame-By-Frame Animation,其在维基百科中有如下定义:

定格动画,又名逐帧动画,是一种动画技术,其原理即将每帧不同的图像连续播放,从而产生动画效果。



简而言之,逐帧动画有两个要素:
  • 相关联的不同图像,即动画帧
  • 连续播放


逐帧动画最经典的例子,莫过于手翻书了。动画帧绘制在书本的不同页上,通过手动翻页来实现连续播放:



逐帧动画的前端实现方案

在细聊 CSS3 逐帧动画之前,我们先了解下前端实现逐帧动画的几种方案。

1\. 直接使用 GIF

GIF 可以有多个动画帧,连续播放是其自身属性,是否循环也是由其本身决定的。
GIF 往往用来实现小细节动画,成本较低、使用方便、兼容性好,但同时也存在画质低、交互不灵活等问题。

2\. 使用 JavaScript 控制动画播放

将动画帧合并成雪碧图,放到 `background-image` 中,通过 JavaScript 改变 `background-position` 的值来实现动画的播放。
使用 JavaScript 实现逐帧动画,兼容性佳,且交互灵活。

3\. 使用 Canvas 及相关库

将动画帧绘制到 Canvas 上,通过不断地重绘即可实现逐帧动画。[CreateJS]、[Pixi.js] 等库都提供了成熟的方案。
使用 Canvas 可以利用硬件加速,功能强大,操作灵活,有丰富的类库,但学习成本较高,且老式浏览器不兼容 Canvas。

4\. 使用 CSS3 阶梯函数 `steps(number_of_steps, direction)`

CSS3 使用 `animation-timing-function` 的阶梯函数 `steps(number_of_steps, direction)` 来实现逐帧动画。

在实际工作过程中,开发「逐帧动画」最为常见的两种方式是第 3 和 4 种,CSS3 Animation 兼容性良好,相对于 JavaScript,CSS3 逐帧动画使用简单,且开发效率更高;而 Canvas 因为其性能优势,帧与帧之间切换的衔接度更高,适合实现帧数或尺寸(宽高)较大的逐帧动画。



案例实战 1 - 利用 CSS3 实现逐帧动画

与使用 JavaScript 实现相同,通过 CSS3 实现逐帧动画时,也是将动画帧放到 `background-image` 中。
逐帧动画往往有多个不同的动画帧,可以直接通过更改 `background-image` 的值实现帧的切换,但多个图片文件会带来多个 HTTP 请求,且不利于文件的管理。
比较妥当的做法是,将所有的动画帧合并成一张雪碧图(sprite),通过改变 `background-position` 的值来实现动画帧切换。因此,逐帧动画也被称为“精灵动画(sprite animation)”。
下面以京东到家的触屏页面[《年货送到家》]中的一个场景为例,为大家讲解如何利用 CSS3 来实现逐帧动画。

3.gif


步骤 1 - 将动画帧合并为雪碧图

动画帧的合并方法有很多,可以使用图片处理软件、在线雪碧图工具、自动化脚本等。这里将介绍 Photoshop 中的操作。

1. 准备好需要合并的动画帧,这里使用的动画帧尺寸为 200 x 206

4.png


2. 打开 Photoshop - 文件 - 自动 - 联系表 II,选取所有动画帧,设置文档尺寸,注意红框部分取消勾选

5.png


3. 背景图层不可见,导出雪碧图并命名为 p8.png

6.png


7.png


此时,我们可以得到如下的雪碧图 p8.png:

8.png


步骤 2 - 元素定位并设置背景

元素的尺寸需与动画帧的尺寸相同/等比例,将雪碧图放到元素的 `background-image` 中:
```
.page_key {
position: absolute;
left: 20px;
top: 20px;
width: 200px;
height: 206px;
background-image: url("../img/p8.png");
}

```

步骤 3 - 使用 `steps` 实现动画播放

通过查看 W3C 文档,可知 `steps(number_of_steps, direction)` 指定了一个阶梯函数,包含两个参数:
  • 第一个参数指定了函数中的间隔数量(必须是正整数)
  • 第二个参数可选,指定在每个间隔的起点或是终点发生阶跃变化,接受 `start` 和 `end` 两个值,默认为 `end`


可以通过下图更深入地理解 `steps` 函数:

9.png


我们可以通过两种写法来实现例子中的逐帧动画,下面是第一种写法。
```
/* 写法一 */
.page_key {
animation: p8 steps(1,end) 1.5s infinite;
}
@keyframes p8 {
0% {background-position: 0 0;}
33.33333% {background-position: 0 -206px;}
66.66667% {background-position: 0 -412px;}
100% {background-position: 0 -618px;}
}

```

这里可能有人疑惑,`steps` 的第一个参数为什么是 1?
前文中提到,`steps` 是 `animation-timing-function` 的一个属性值,在 [W3C] 中有如下说明:

For a keyframed animation, the ‘animation-timing-function’ applies between keyframes, not over the entire animation.



也就是说,`animation-timing-function` 应用于两个关键帧(状态)之间,而非整个动画。在上面的 `keyframes` 中,我们已经把每个关键帧都写出来了,所以两个关键帧之间的间隔是 1。
既然 `steps` 第一个参数是指函数的间隔数量,那么我们就可以把 `keyframes` 的计算直接交给 `steps` 来完成。
```
/* 写法二 */
.page_key{
animation: p8 steps(3,end) 1.5s infinite;
}
@keyframes p8 {
100% {background-position: 0 -618px;}
}

```

上述两种写法最终的动画效果是相同的,最终效果:
3.gif


CSS3 逐帧动画的一些技巧

CSS3 实现逐帧动画虽然简单,但也不乏技巧。

1\. `step-start` 与 `step-end`

除了 `steps` 函数,`animation-timing-function` 还有两个与逐帧动画相关的属性值 `step-start` 与 `step-end`。
  • `step-start` 等同于 `steps(1, start)`:动画执行时以开始端点为开始。
  • `step-end` 等同于 `steps(1, end)`:动画执行时以结尾端点为开始。


2\. 使用 Sass 完成动画帧的计算
```
/* 写法一 */
@mixin frame($frameNum, $frameHeight) {
@for $i from 0 through $frameNum {
#{100/$frameNum*$i}% {background-position: 0 #{-$i*$frameHeight}px;}
}
}
/* 写法二 */
@mixin frame($frameNum, $frameHeight) {
100% {background-position: 0 #{-$frameNum*$frameHeight}px;}
}

@keyframes p8 {
@include frame($frameNum: 3, $frameHeight: 206)
}

```

3\. 移动端使用 rem 配合 scale 适配,防止动画抖动

我们知道,`rem` 的计算会存在误差,因此使用雪碧图时我们并不推荐用 `rem`。
如果是逐帧动画的话,由于计算的误差,可能会出现动画抖动的情况。
为了解决这个问题,可以参考以下的适配思路:
  • 非逐帧动画部分,使用 `rem` 做单位
  • 逐帧动画部分,使用 `px` 做单位,再结合 JavaScript 对动画部分使用 `scale` 进行缩放


案例实战 2 - 利用 Canvas 做一个会动的京东 JOY

CSS3 实现的逐帧动画,如果它的帧数较多或尺寸较大时,移动端可能会存在渲染性能问题,此时建议改用 Canvas 实现。 相对于 CSS3 来说,Canvas 具有更高的学习成本,所以实际项目中推荐使用业务成熟的 Canvas 动画库,如 [CreateJS]、[Pixi.js] 等。

下面我们以 [「京东 JOY 福星会场 一一 八仙乐游记」] 中的其中一个动画为例,为大家讲解如何利用 Canvas 实现逐帧动画。


步骤 1 - 准备动画帧所需的雪碧图

在前面的案例中我们介绍了通过 Photoshop 手动合成雪碧图的方式,这里我们使用 [TexturePacker] 工具(付费软件,帧动画开发利器,值得购买)来更快地完成同样的事情。

11.png


[TexturePacker]导出雪碧图的步骤如下:
  1. 将逐帧动画所有帧拖到 TexturePacker
  2. 选择逐帧动画的渲染载体(标记1)
  3. 根据需要设置其余配置,如:当雪碧图超过 1 张时,请选择 `Multipack` 选项(标记2)
  4. 点击 Publish sprite sheet(标记4)即可导出雪碧图和相应渲染载体的数据(若有)


预览逐帧动画的步骤:

1. 选择所有帧
选择左侧 Sprites 面板其中一帧,然后按 Cmd/Ctrl + A 全选所有帧;或者拖拽选择所有帧。
2. 点击 Anim preview(标记3)
如果我们选了渲染载体为 **EaselJS / CreateJS**, 其导出的 JSON 文件如下:
```
{
"images": [
"hxr-0.png",
"hxr-1.png",
"hxr-2.png",
"hxr-3.png",
"hxr-4.png",
"hxr-5.png"
],

"framerate": 20,
"frames": [
[1, 1, 519, 535, 0, -109, -499],
[522, 1, 514, 538, 0, -108, -499],
[1, 538, 514, 538, 0, -109, -499],
...
],

"animations": {
"花想容": { "frames": [45, 34, 36, 39, 41, 42, 43, 27, 44, 15, 30, 16, 17, 18, 21, 23, 24, 9, 12, 14, 19, 1, 2, 4, 5, 7, 10, 11, 13, 20, 31, 22, 25, 35, 28, 29, 32, 37, 38, 40, 33, 26, 8, 6, 0, 3] }
},

"texturepacker": [
"SmartUpdateHash: $TexturePacker:SmartUpdate:d81882f9ddc9b1a6b4cc21c262ac0125:4ebba912052ed522502e73edaa8a5333:c8130f68479de7028295f1ccf1a4ea15$",
"Created with TexturePacker (https://www.codeandweb.com/texturepacker) for EaselJS"
]
}

```

[TexturePacker] 除了生成雪碧图,其导出的 JSON 文件其实还包含了逐帧动画所需的运行数据,只需结合渲染载体的相应 API 即可快速实现逐帧动画。

步骤 2 - 实现动画

以 CreateJS 为例。
准备 HTML:
```
<canvas id="hxr-canvas" width="526" height="536"></canvas>

```

准备 CreateJS 代码:
```
var hxrCanvas = document.getElementById("hxr-canvas")
var hxrStage = new createjs.Stage(hxrCanvas)

// 将上 JSON 数据进行修改:如 "images" 和 "animations" 字段
var spriteSheet = new createjs.SpriteSheet({
"images": [
preload.getResult('hxr0'),
preload.getResult('hxr1'),
preload.getResult('hxr2'),
preload.getResult('hxr3'),
preload.getResult('hxr4'),
preload.getResult('hxr5'),
],

"framerate": 20,
"frames": [
[1, 1, 519, 535, 0, -109, -499],
[522, 1, 514, 538, 0, -108, -499],
[1, 538, 514, 538, 0, -109, -499],
...
],

"animations": {
"play": { "frames": [45, 34, 36, 39, 41, 42, 43, 27, 44, 15, 30, 16, 17, 18, 21, 23, 24, 9, 12, 14, 19, 1, 2, 4, 5, 7, 10, 11, 13, 20, 31, 22, 25, 35, 28, 29, 32, 37, 38, 40, 33, 26, 8, 6, 0, 3] }
},
})

hxrSprite = new createjs.Sprite(spriteSheet)
hxrSprite.x = 0
hxrSprite.y = 0
hxrStage.addChild(hxrSprite)
hxrSprite.gotoAndPlay("play") // 播放指定的动作 "play"

```

这样即可完成一个逐帧动画

小结

本文结合案例为大家讲解了两种最为常见的逐帧动画实现方案:「基于 CSS3 Animation 实现逐帧动画」和「基于 Canvas 实现逐帧动画」。考虑到 Canvas 的方案需要引入第三方的脚本库(以 CreateJS 为例,引入的脚本库体积大小在 190 多 KB,GZIP 后仍然有 50 多 KB),在实际工作过程中,应尽量使用 CSS3 来实现逐帧动画,当 CSS3 实现的逐帧动画出现性能体验问题(或预估到会有此类问题)时,再考虑使用 Canvas 的实现方案。
 

0 个评论

要回复文章请先登录注册