最近做的好几个营销项目的页面都有比较多的动画,把 CSS3, DOM, Canvas 这几种方式都给实践了一下。期间也考虑着是不是能够产出一套方案来解决动画开发维护繁琐的问题,然后也算是落地了一些想法,这里记录一下。

痛点

常见的动画开发方式包括 CSS3, DOM, Canvas, WebGL 这几种,根据不同的动画形式选择不同的实现方法。对于变换简单、数量少的动画需求,CSS3 是最简单的实现方式。简单的动画简单写就行了,这里讨论动画数量多,时间轴要求严格的场景。在这种场景下,我们会考虑通过操作 DOM 或者 Canvas 甚至 WebGL 去实现。不管是哪种实现方法,大概都会经历这样子的开发步骤:

  1. 切图和重构:通常需要把设计稿里面数十个动画元素的图片切出来,然后逐个测量它们的位置,并定位到页面或者画布中
  2. 动效的编写:根据设计师提供的 Demo 和关键帧标注,用代码实现每个元素的动效。通常会是位移、旋转、渐变或者精灵图变换
  3. 时间轴的控制:编写好每个动画元素的动效之后,需要把它们都组织起来,安排动画的顺序和切入的时间点,包括用户的交互行为对动画的影响
  4. 代码的维护:部署并体验之后,需求方或者设计师会跟你说这里需求变动了或者效果不理想需要返工,然后就再来一次

试过一次就会发现以上每个步骤很揪心。切图和重构有大量重复操作在设计稿上面折腾;动效的编写需要通过动画框架根据不同动效元素创建不同的实例,不断调用接口但仅仅传入不同的参数;时间轴的控制需要编写大量的胶水代码把之前所有的动画元素组织起来;最后由于代码繁多造成维护的成本提高,返工消耗大量时间。

在发现每次动画开发都会有这么多重复且单一的工作量之后,肯定就会想法设法地通过工具的支撑或者新的开发模式把开发的成本减下来啦,然后就有了下面的尝试。

Canvas 与 Preact

由于目前项目中大量使用 Preact,因此在也想着是不是也可以用类似的方式给 Canvas 动画框架的各个类做一层 Preact 的组件封装。在使用的过程中通过组件的嵌套对动画元素进行组合,减少胶水代码;通过配置数据的形式驱动动画,使得动画框架的接口对于开发者透明,开发动画变成了编写配置的过程,后续代码的维护性还是蛮高的。这里贴下简单的示例代码:

// 配置数据
const material = {
shadow: {
opts: {
width: 640,
height: 1136,
opacity: 1,
x: 0,
y: 0,
color: '#000',
},
animation: [{
opacity: 0.4,
delay: 1000,
time: 1250,
timeFunction: 'linear',
}],
},
// ...
}
// 组件
export default class App {
render() {
return (
<div id="page_index">
<FrameScene>
<FrameContainer>
<FrameSprite {...material.video} />
<FrameQuadangle {...material.shadow} />
<FrameText {...materia.text} />
<FrameImage {...material.logo} />
<FrameImage {...material.slogan} />
</FrameContainer>
</FrameScene>
</div>
)
}
}

在 Canvas 动画框架中,上面的 Scene, Container, Image, Text 应该都是比较常见的类了。在封装成 Preact 的组件之后,可以用组件标签声明一个元素,并添加到场景或者容器中,然后通过 props 提供配置数据,就完成了重构和动效编写这一块的工作。配置数据提供了 opts 和 animation 两个属性。opts 是提供给动画框架初始化对应实例的配置,animation 以数组的形式声明了元素会有怎样的一连串关键帧。元素组件会在 animation 属性更新的时候启动动画。而时间轴的控制,只需要在正确的时间点切换正确的数据配置就可以了。

这里很自然地把一个动画元素分为两部分:属性和动效。属性描述了这个元素的样式,包括大小颜色背景坐标等等;动效描述了元素的变换过程的关键帧,包括渐变位移缩放等等。同时,动效经常会配置有变换时长,延时,循环次数,缓动函数。

Velovity 与 Preact

由于 Canvas 动画框架在实现自适应方面还没有探索出比较友好的方式,所以对于一些比较简单但需要比较好控制动画的场景,通过封装 Velocity 去实现之前类似的效果。简单的示例如下:

// 配置文件
const config = {
'.nav': {
keyframes: [{
opacity: 0,
}, {
opacity: 1,
duration: 500,
delay: 600,
}],
},
'.kv': {
keyframes: [
opacity: 0,
translateY: 0,
}, {
opacity: 1,
duration: 800,
delay: 600,
}],
},
// ...
}
// 组件
export default class App {
render() {
const getProps = selector => config[selector]
return (
<div class="wrap">
<div class="group">
<div class="container">
<VelocityComponent class="list" {...getProps('.list')} />
</div>
<VelocityComponent class="nav" {...getProps('.nav')} />
<VelocityComponent class="kv" {...getProps('.kv')} />
</div>
</div>
)
}
}

到这会发现很有意思的东西,通过对框架的二次封装,可以隐藏动画具体实现的方法,也就是说开发者可以不关心动画底层是怎么实现的, Canvas 框架还是 Velocity 都没关系;开发动画的时候关注元素属性动效,也就是配置文件。以配置文件的形式管理动画会使得开发和维护的成本降低,至于时间轴的控制其实就是配置数据的控制了。

Ani

在几个项目后,把之前写的部分代码整理下,放到了 Ani 这个仓库下面,目前包括 velocity-component 和 ani-loader 两个模块。velocity-component 就是上面那货啦,然后 ani-loader 是了更加舒服和直观写代码而搞出来的东西。

在上面的代码可以发现,配置文件以 JSON 的形式维护每一个动画元素的属性和动效,看起来还算比较直观,但是当一个配置文件里面有数十个动画元素的配置,其实就有点乱糟糟了。然后便考虑着是不是有更清爽的方式来写配置文件,最终还是回到了 CSS3 的 animation 和 keyframes。

.foo
width 10px
height 10px
opacity 1
animation fade-in 1s 10ms ease-in 2
@keyframes fade-in
from
opacity 0
to
opacity 1

CSS3 的 animation 的写法十分简洁并且应该都很熟悉了,包括动效类型、时长、延时、循环次数、缓动函数这些配置。样式属性与动效分离,动效允许复用。ani-loader 基于 stylus 和 postcss 实现。能够把 stylus 的语法转换为组件支持的 JSON 配置。为了更舒服地写动画,规则上做了一些调整,比如省略了 @keyframes 直接用关键帧名称定义,animation 声明的动画逐个执行而不是 CSS3 那样一起执行。效果如下:

// raw
.foo
width 10px
height 10px
opacity 1
animation fade-in 1s,
fade-out 2s 10ms ease-in 2
@fade-in
opacity 1
@fade-out
opacity 0
// output
{
'.foo': {
width: '10px',
height: '10px',
opacity: 1,
animation: 'fade-in 1s, fade-out 2s 10ms ease-in 2',
keyframes: [{
__aniName: 'fade-in',
opacity: 1,
duration: 1000
}, {
__aniName: 'fade-out',
opacity: 0,
duration: 2000,
delay: 10,
easing: 'ease-in',
loop: 2
}]
}
}

然后会发现大部分时间写出来的动效不具备复用性,每次纠结一个动效的命名太艰难了,于是又增加了类似 mixin 这样的写法:

// raw
.foo
width 10px
animation to(opacity, 1, translateY, 10px) 2s
// output
{
'.foo': {
width: '10px',
animation: 'anonymous 2s',
keyframes: [{
__aniName: 'anonymous',
opacity: 1,
translateY: '10px',
duration: 2000
}]
}
}

其他

ani-loader 并没有用到生产环境中,Ani 这套东西的思路和代码也都是试水性质。可以改进的地方,一个是不同需求场景的打磨,一个是上层封装必须脱离 Preact 来搞,一个是动画框架的抽象和封装,一个是 Adobe 工具的打通,用于导出动画元素的属性和动效配置(这才是我最大的期盼啊)。之前捣鼓过的 psd-clipper 虽然还不够完善,但在项目中尝试过确实明显减低返工成本,至于 AE 方面目前还没有找到好的方法去解决。

然后到这里 Ani 也就 deprecated 了,因为最近发现集团内已经有了这方面比较完善的解决方案,献上膝盖,期待后续使用带来的生产力的提升。