前端动画,是前端工程师“拉开差距”非常明显的一项能力。

很多人会:

  • transition
  • transform
  • 会一点 animation

但真正高级前端,会:

  • 理解浏览器渲染机制
  • 知道为什么动画会卡
  • 知道什么时候该用 CSS、JS、Canvas、WebGL
  • 能做复杂交互动效
  • 能做高性能动画架构
  • 能实现产品级动画系统

你现在的目标,不应该是“记 API”。

而是:建立一整套“动画思维体系”。


一、先建立动画世界观

你先记住一句:动画,本质是“让某个值随时间变化”。


二、浏览器如何产生动画

你必须理解浏览器渲染流程。

生成 DOM 树   // 解析 HTML 标签,生成文档对象模型(DOM)
 ↓
生成 CSSOM 树    // 解析 CSS 样式,生成 CSS 对象模型(CSSOM)
 ↓
合成渲染树 Render Tree   // 将 DOM 和 CSSOM 合并,dom节点和对应的样式合并
 ↓
Layout(重描/回流)   // 计算每个元素在屏幕上的确切位置和大小
 ↓
Paint(重绘)   // 将渲染树的每个节点转换为屏幕上的实际像素
 ↓
Composite(合成)// 将不同的图层合成在一起,最终显示在屏幕上

1. layout(重描/回流)

最贵

例如:宽高(width,height)位置信息(top,left)自身布局(margin,padding,font-size)等

发生改变时,会影响外部布局或者内部布局,会触发:重新计算布局

2. paint(重绘)

中等。

例如:背景(background)、阴影(box-shadow)、颜色(color)、圆角(border-radius)

这些属性发生改变,需要重新绘制。

3. composite(合成层)

最便宜。

例如:transform、opacity

只需要 GPU 合成。

所以:

黄金法则

尽量动画:

  • transform
  • opacity

少动:

  • width
  • height
  • top
  • left

这是动画性能核心。


为什么transform,opacity不会引起重描和重绘?

1、元素使用了 transform 或 opacity(且该元素被提升为独立图层)

2、元素的几何尺寸没变,只是变换矩阵变了,所以跳过重排。图层里的像素内容没变(只是整体移动或变透明),所以跳过重绘。


三、Transition(控制如何变化)

transition:

  • transition-property,// all/background/opacity,transform
  • transition-duration,// 动画持续时长 2s
  • transition-timing-function,//linear/ease/ease-in/ease-out/ease-in-out
  • transition-delay

示例:

  transition:
    transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1),
    box-shadow 0.35s ease;
1. timing-function

控制速度曲线。


linear

匀速。斜率为1的直线


ease

默认。

慢 -> 快 -> 慢。

起步稍快一点,中间加速明显,结尾减速更柔和;非对称 S 形(默认)


ease-in

越来越快。


ease-out

越来越慢。


ease-in-out

两头慢。

前半段加速、后半段减速,形状镜像;对称 S 形

四、真正理解贝塞尔曲线

CSS 动画核心:cubic-bezier(x1, y1, x2, y2, x3, y3)

含义:

  • 起点固定:(0, 0)
  • 终点固定:(1, 1)
  • (x1, y1):第一个控制点
  • (x2, y2):第二个控制点 // 二次贝塞尔
  • (x3, y3):第三个控制点 // 三次贝塞尔

控制点不落在曲线上,而是把曲线「拉」向自己

看斜率,斜率与变化速率成正比

控制点直觉

P1 的 y1 小

开头慢(ease-in)

P1 的 y1 大

开头快

P2 的 y2 大

结尾还快一阵再刹住(ease-out)

P2 的 y2 小

结尾提前减速

P1、P2 对称

接近 ease-in-out

等价写法示例

linear: cubic-bezier(0, 0, 1, 1);

ease: cubic-bezier(0.25, 0.1, 0.25, 1);

ease-in: cubic-bezier(0.42, 0, 1, 1);

ease-out: cubic-bezier(0, 0, 0.58, 1);

ease-in-out: cubic-bezier(0.42, 0, 0.58, 1);
合法范围(timing-function 时)
参数范围说明

x1, x2

必须在 0 ~ 1

时间不能倒流、不能跳过

y1, y2

可以 小于 0 或大于 1

允许「冲过头再回来」(回弹)

例如:

transition: all .5s cubic-bezier(0.4, 0, 0.2, 1);

本质:

控制速度变化。


五、 transform(触发transition,及自身如何变化),
1. translate 移动

translate(tx, ty)。如果ty未指定,则假定其值为零。

2. scale 缩放

scale(sx, sy)。如果sy未指定,则假定等于sx

3. rotate 旋转

将元素绕其原点(由transform-origin属性指定)旋转指定角度。可以写成rotate(a)

4、transform-origin: 水平方向 垂直方向;
  • x:0%,50%,100%
  • y:0%,50%,100%

可以想象元素是一个矩形:

top left                top center                top right

┌─────────────────────────┐

│                                                                │

│                                                                │

left center                                         right center

│                                                                │

│                                                                │

└─────────────────────────┘

bottom left         bottom center         bottom right

六、 keyframes 定义关键帧
@keyframes move1 {
  0% {
    transform: translateX(0);
  }

  100% {
    transform: translateX(300px);
  }
}

@keyframes move2 {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(300px);
  }
}
七、animation(使用关键帧) 核心参数
  • @keyframes:使用哪个动画帧
  • animation-name:使用的动画帧的名称
  • animation-duration:完成动画时长
  • animation-timing-function:动画变化速率
  • animation-delay:动画延迟多久开始
  • animation-iteration-count:动画循环次数
示例:
  animation:
    typing 3s steps(12, end) forwards,
    cursorBlink 0.7s step-end infinite;

八、真正重要:requestAnimationFrame
  1. requestAnimationFrame 会让浏览器在下一次页面重绘前执行你传入的函数。
  2. 浏览器通常一秒刷新 60 次,也就是:1000ms / 60 ≈ 16.6ms
  3. 所以大约每隔 16.6ms 绘制一帧。
  4. 如果你每一帧都更新一次元素位置,就能形成流畅动画。
基本用法
let animationId;

function animate(time) {
  console.log("time",time);
  animationId = requestAnimationFrame(animate);
}

animationId = requestAnimationFrame(animate);

// 停止
cancelAnimationFrame(animationId);

// 不需要时间参数的时候
animate()
  • 这个 time 表示当前动画帧的时间,可以用来计算动画进度。

注意:requestAnimationFrame 只执行一次。如果想连续动画,需要在回调里再次调用它。

更多推荐