动画的基本原理:什么是动画、动画的历史、计算机动画原理
前端动画的分类:CSS 动画、SVG 动画、JS 动画、如何选择
前端动画如何实现(主要是 JS):JS 动画的函数封装、简单动画、复杂动画
相关实践:动画资源、工作实践、动画的优化
# 前端动画实现 - 笔记
# 动画的基本原理
# 什么是动画
动画是通过快速连续排列彼此差异极小的连续图像来制造运动错觉和变化错觉的过程。
- 快速、连续排列、彼此差异极小、制造错觉
# 动画的历史
如今前端动画技术已经普及
-
常见的前端动画技术
Sprite 动画、CSS 动画、JS 动画、SVG 动画和 WebGL 动画 -
按应用分类
UI 动画、基于 Web 的游戏动画和动画数据可视化
最早的技术是 GIF,然后是 Flash,如今是 HTML/CSS/JS
# 计算机动画原理
计算机图形学:
计算机视觉的基础,涵盖点、线、面、体、场的数学构造方法。
- 几何和图形数据的输入、存储和压缩。
- 描述纹理、曲线、光影等算法。
- 物体图形的数据输出 (图形接口、动画技术),硬件和图形的交互技术。
- 图形开发软件的相关技术标准。
计算机动画:
计算机图形学的分支,主要包含 2D、3D 动画。
无论动画多么简单,始终需要定义两个基本状态,即开始状态和结束状态。没有它们,我们将无法定义插值状态,从而填补了两者之间的空白。
帧:连续变换的多张画面,其中的每一幅画面都是一帧。
帧率:用于度量一定时间段内的帧数,通常的测量单位是 FPS (frame per second) 。
帧率与人眼:一般每秒 10-12 帧人会认为画面是连贯的,这个现象称为视觉暂留。对于一些电脑动画和游戏来说低于 30FPS 会感受到明显卡顿,目前主流的屏幕、显卡输出为 60FPS,效果会明显更流畅。
空白的补全方式有以下两种
补间动画:
传统动画,主画师绘制关键帧,交给清稿部门,清稿部门的补间动画师补充关键帧进行交付。(类比到这里,补间动画师由浏览器来担任,如 keyframe
, transition
)
逐帧动画 (Frame By Frame) :
从词语来说意味着全片每一帧逐帧都是纯手绘。(如 CSS 的 steps
实现精灵动画)
# 前端动画分类
# CSS 动画
CSS animation 是常见的 CSS 动画实现方式:
CSS animation
属性是 animation-name
, animation-
duration
, animation-timing-function
, animation-delay
, animation-iteration-count
, animation-direction
, animation-fill-mode
和 animation-play-state
属性的一个简写属性形式。
CSS 补间动画使用 Transition API
和 Keyframe
实现
CSS 逐帧动画使用 Animation API
中的 steps
实现
优点:简单、高效声明式的不依赖于主线程,采用硬件加速 (GPU) 简单的控制 keyframe animation 播放和暂停。
缺点:不能动态修改或定义动画内容不同的动画无法实现同步多个动画彼此无法堆叠。
适用场景:简单的 h5 活动 / 宣传页。
推荐库:animation.css、shake.css 等。
# SVG 动画
svg 是基于 XML 的矢量图形描述语言,它可以与 CSS 和 S 较好的配合,实现 svg 动画通常有三种方式:SMIL、JS、CSS
我们经常使用 animation, transform, transition 来实现 svg 动画,它比 JS 更加简单方便。
优点:通过矢量元素实现动画,不同的屏幕下均可获得较好的清晰度。可以实现一些特殊的效果,如:描字,形变,墨水扩散等。
缺点:使用方式较为复杂,过多使用可能会带来性能问题。
# SMIL
SMIL: Synchronized Multimedia Integration Language (同步多媒体集成语言)
可以使用 svg 标签进行动画的描述,但是兼容性不是很理想
# JS、CSS
使用 JS 来操作 SVG 动画自不必多说,目前也有很多现成的类库。例如老牌的 Snap.svg 以及 anime.js ,都能让我们快速制作 SVG 动画。当然,除了这些类库,HTML 本身也有原生的 Web Animation 实现。使用 Web Animation 也能让我们方便快捷地制作动画。
文字形变(基于 CSS 中的 filter 属性):https://codepen.io/jiangxiang/pen/MWmdjeY
Path 实现写字动画:https://codepen.io/jiangxiang/pen/rNmgjqX
# JS 笔画的原理
stroke-dashoffset、stroke-dasharray 配合使用实现笔画效果。
属性 stroke-dasharray 可控制用来描边的点划线的图案范式。
它是一个数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度。如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。因此,5,3,2 等同于 5,3,2,5,3,2。
stroke-dashoffset 属性指定了 dash 模式到路径开始的距离。
参考:https://codepen.io/jiangxiang/pen/LYzvvxd
path 路径–d 属性 (路径描述) <path d="...." />
* 大写字母跟随的是绝对坐标 x,y,小写为相对坐标 dx,dy
-
M/m 绘制起始点。
-
L/I 绘制一条线段。
-
C/c 为绘制贝塞尔曲线。
-
Z/z 将当前点与起始点用直线连接。
计算 path 的长度: path.getTotalLength();
计算 path 上某个点的坐标: path.getPointAtLength(lengthNumber);
例子:https://codepen.io/jiangxiang/pen/eYWagxq
# JS 动画
JS 可以实现复杂的动画,也可以操作 canvas 动画 API 上进行绘制。
# 如何做选择
CSS 优点:
-
浏览器会对 CSS3 动画做一些优化,使得 CSS3 动画性能上稍有优势(新建一个图层来跑动画)。
-
CSS3 动画的代码相对简单。
CSS 缺点:
- 动画控制上不够灵活。
- 兼容性不佳。
- 部分动画无法实现(视差效果、滚动动画)。
JS 优点:
-
使用灵活,同样在定义一个动画的 keyframe 序列时,可以根据不同的条件调节若干参数(JS 动画函数)改变动画方式。(CSS 会有非常多的代码冗余)
-
对比与 CSS 的 keyframe 粒度更粗,css 本身的时间函数是有限的,这块 JS 都可做弥补。
-
CSS 很难做到两个以上的状态转化(要么使用关键帧,要么需要多个动画延时触发,再想到要对动画循环播放或暂停倒序等,复杂度极高)。
JS 缺点:
- 使用到 JS 运行时,调优方面不如 CSS 简单,CSS 调优方式固定。
- 对于性能和兼容性较差的浏览器,CSS 可以做到优雅降级,而 JS 需要额外代码兼容。
结论:
- 当 UI 元素采用较小的独立状态时,使用 CSS。
- 在需要对动画进行大量控制时,使用 JavaScript。
- 在特定的场景下可以使用 SVG,可以使用 CSS 或 JS 去操作 SVG 变化。
# 前端动画实现
# JS 动画函数封装
先来一个基础的 animate
函数:
/** | |
* 入参说明: | |
* draw 动画绘制函数(例如:() => { ctx.draw ()... }) | |
* easing 缓动函数(数学)(例如:(x) => y) | |
* duration 动画持续时间(例如:2000) | |
* @returns 一个可以表示动画是否完成的 Promise 对象,同时,由于动画可以是连续的,所以 Promise.then 就能让动画按顺序被调用 | |
*/ | |
function animate ({easing, draw, duration}) { | |
// 动画开始的时间戳 | |
// Q:为什么使用 performance.now () 而非 Date.now ()? | |
// A:因为 performance.now () 会以恒定速度自增,精确到微秒级别,不易被篡改。 | |
let start = performance.now(); | |
return new Promise(resolve => { | |
requestAnimationFrame(function animate(time) { | |
// (time - start) 算出距离动画开始,时间已经过去了多少,然后根据过去了多少时间 ÷ 规定的动画持续时间,算出目前动画进度(百分比) | |
// 注意:这是不算上缓动函数修正的百分比(原始百分比) | |
// 例如:动画开始时间为 start = 1666,现在的时间为 time = 2666,想让动画持续的时间为 duration = 2000,那么 timeFraction 就是 0.5,即 50% | |
let timeFraction = (time - start) / duration; | |
// 如果 timeFraction > 1,即原始百分比已经大于 100%,即动画照理来说应该是已经结束了的,那么就将原始百分比设为 100%,即 timeFraction = 1 | |
if (timeFraction > 1) timeFraction = 1; | |
//progress 是动画的实际进程(通过缓动函数计算后的真实百分比),这个值应该也是要小于 100% 的,你可以把 easing 函数理解为一个纯数学函数,接受 [0, 1]-> 输出 [0, 1],建立真实时间到动画百分比的映射关系 | |
let progress = easing(timeFraction); | |
// 给 draw 函数传入 progress(动画目前应该到达的进度),那么 draw 函数就可以根据这个进度指示,来绘制相应的图像(可以类比 CSS animation 中的 keyframe 百分比) | |
draw(progress); | |
// 如上所述,当 timeFrction(原始百分比)< 1 时,说明动画还为完成,则继续调用 rAF,否则说明动画已结束,将此 Promise 解决 (resolve) 掉 | |
if (timeFraction < 1) { | |
requestAnimationFrame(animate); | |
} else { | |
resolve(); | |
} | |
}); | |
}); | |
} |
注意: easing
函数也不一定只能返回 [0, 1] 的数值,根据实际使用情况可以与 draw
函数协调
# JS 执行动画的核心思想
基本公式:
简单理解:r 是距离,v 是速度,t 是时间
比例尺 / 距离系数:通过比例尺将实际的大小、长度等比例缩放 / 增加到屏幕上显示的大小、长度
# 简单动画
# 匀速运动
const ball = document.querySelector( '.ball'); | |
const draw = (progress) => { | |
ball.style.transform = `translate(${progress * 100}px, 0)`; | |
// 这里的 * 100 实际上就是一个比例尺,将 [0, 1] 映射到 [0, 100] px | |
} | |
// 沿着 x 轴匀速运动 | |
animate({ | |
duration: 1000, | |
easing(timeFraction) { | |
// 这就是一个匀速运动函数,相当于 (x) => x | |
return timeFraction; | |
}, | |
draw | |
}); |
# 重力效果
从这个动画开始,就需要考虑数学公式了,即:怎么把 套用到 animate
这个模板里面
const draw = (progress) => { | |
ball.style.transform = `translate(0, ${(progress - 1) * 500}px)`; | |
// 这里的 500 就是比例尺 | |
} | |
// 沿着 x 轴匀速运动 | |
animate({ | |
duration: 1000, | |
easing(timeFraction) { | |
// 这个函数通过 t^2,模拟了重力的最显著特点 | |
return timeFraction ** 2; | |
// 也可以模拟的真实一点:0.5 * 9.8 * (timeFraction ** 2),当然,这样的话 draw 函数内部就也要做相应的调整了 | |
}, | |
draw | |
}); |
# 摩擦力(匀速减速运动)
同样的,根据摩擦力数学公式写出缓动函数:
// 初始高度 500px | |
const draw = (progress) => { | |
ball.style.transform = `translate(${500 * progress.x}px),${500 * (progress.y - 1)}px)`; | |
}; | |
// 匀减速运动 | |
animate({ | |
duration: 1000, | |
easing(t) { | |
// v0 = 2,a = 2 | |
return 2 * t - (t ** 2); | |
}, | |
draw | |
}); |
# 平抛运动(x 轴匀速,y 轴加速)
const draw = (progress) => { | |
ball.style.transform = `translate(translate(${500 * progress.x}px), ${500 * (progress - 1)}px)`; | |
}; | |
// 有两个方向,沿着 x 轴匀速运动,沿着 y 轴加速运动 | |
animate({ | |
duration: 1000, | |
easing(t) { | |
return { | |
x: t, | |
y: t ** 2 | |
}; | |
}, | |
draw | |
}); |
# 旋转 + 平抛
其实就是在平抛的基础上加一个旋转效果而已
const draw = (progress) => { | |
ball.style.transform = `translate(o,${500 * (progress - 1)}px rotate(${2000 * progress.rorate}deg))`; | |
}; | |
// 有两个方向,沿着 x 轴匀速运动,沿着 y 轴加速运动 | |
animate({ | |
duration: 1000, | |
easing(t) { | |
return { | |
x: t, | |
y: t ** 2, | |
rotate: t // 匀速旋转 | |
}; | |
}, | |
draw | |
}); |
# 拉弓效果
拉弓效果的本质就是:x 轴匀速运动;y 轴为初始速度为负的匀加速
知道这两点后,就不难通过数学表达式写出缓动函数
const draw = (progress) => { | |
ball.style.transform = `translate(translate(${500 * progress.x}px), ${500 * (progress - 1)}px)`; | |
}; | |
// 有两个方向,沿着 x 轴匀速运动,沿着 y 轴加速运动 | |
animate({ | |
duration: 1000, | |
easing(t) { | |
return { | |
x: t, | |
y: -2 * t - (t ** 2) // v0 = 2,a = 2 | |
}; | |
}, | |
draw | |
}); |
# 贝塞尔曲线
贝塞尔曲线的详细描述和公式见 Wikipedia,这里给出三次贝塞尔曲线的数学表达式:
由于 P0和 P3的位置是确定的((0,0) 和 (1,1)),所以实际上只需要给出 P1和 P2的坐标即可:
const bezierPath = (x1, y1, x2, y2, t) => { | |
const x = 3 * t * ((1 - t) ** 2) * x1 + 3 * (t ** 2) * (1 - t) * x2 + (t ** 3) * 1; | |
const y = 3 * t * ((1 - t) ** 2) * y1 + 3 * (t ** 2) * (1 - t) * y2 + (t ** 3) * 1; | |
return [x, y]; | |
} |
# 复杂动画
# 弹跳小球
实质上就是到达终点后的反弹和衰减,是重力效果的延伸
async function autoDamping() { | |
let damping = 0.7, // 衰减系数 | |
duration = 1000, | |
height = 300; | |
// 当衰减到一定边界值时停止动画 | |
while(height > 1) { | |
const down = (progress) => { | |
ball.style.transform = `translate(0, ${height * (progress - 1)}px)`; | |
}; | |
await animate({ | |
duration: duration, | |
easing(t) { | |
return t ** 2; | |
}, | |
draw: down, | |
}); | |
height *= damping ** 2; // ** 2 可以使动画效果更加柔和 | |
duration *= damping; | |
const up = (progress) => { | |
ball.style.transform = `translate(0, ${-height * progress}px)`; | |
} | |
await animate({ | |
duration: duration, | |
easing(t) { | |
return 2 * t - (t ** 2); | |
}, | |
draw: down, | |
}); | |
} | |
} |
# 椭圆运动
也是套用公式:
const draw = (progress) => { | |
const x = 150 * Math.cos(Math.PI * 2 * progress); | |
const y = 100 * Math.sin(Math.PI * 2 * progress); | |
ball.style.transform = `translate(${x}px, ${y}px)`; | |
} | |
animate({ | |
duration: 2000, | |
easing(t) { | |
return 2 * t - (t ** 2); | |
}, | |
draw, | |
}); |
# 相关实践
# 动画资源
动画代码示例:
- codepen.com
- codesandbox.com
设计网站:
- dribbble.com
动画制作工具(一般都是 UE、UI 同学使用):
- 2D:Animate CC、After Effects
- 3D:Cinema 4D、Blender、Autodesk Maya
svg :
-
Snap.svg - 现代 SVG 图形的 JavaScript 库。
-
Svg.js - 用于操作和动画 SVG 的轻量级库。
js :
- GSAP - JavaScript 动画库。
- TweenJs - 一个简单但功能强大的 JavaScript 补间 / 动画库。CreateJS 库套件的一部分。
- Velocity - 加速的 JavaScript 动画。
css :
- Animate.css - CSS 动画的跨浏览器库。像一件简单的事情一样容易使用。
canvas :
- EaselJs - EaselJS 是一个用于在 HTML5 中构建高性能交互式 2D 内容的库
- Fabric.js - 支持动画的 JavaScript 画布库。
- Paper.js - 矢量图形脚本的瑞士军刀 - Scriptographer 使用 HTML5Canvas 移植到 JavaScript 和浏览器。
- Pixijs - 使用最快、最灵活的 2D WebGL 渲染器创建精美的数字内容。
# 工作实践
-
需要完全前端自己开发
使用已经封装好的动画库,从开发成本和体验角度出发进行取舍。
-
设计不是很有空
清晰度,图片格式可以指定,动画尽量给出示意或者相似案例参考。索要精灵资源、资源等需要帮忙压缩。
-
设计资源充足
要求设计导出 lottie 格式文件。
(Lottie 是可应用于 Android, iOS, Web 和 Windows 的库,通过 Bodymovin 解析 AE 动画,并导出可在移动端和 web 端渲染动画的 json 文件)
# 动画的优化
# 用户体验
-
《The Guide To CSS Animation: Principles and Examples》
-
动画的 12 项基本法则 - Wikipedia
# 性能
在实际的应用里,最为简单的一个注意点就是,触发动画的开始不要用 display: none
属性值,因为它会引起 Layout、Paint 环节,通过切换类名就已经是一种很好的办法。
translate
属性值来替换 top/left/right/bottom
的切换, scale
属性值替换 width/height
, opacity
属性替换 display/visibility
等等。
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 repaint,所以能大大提高网页的性能。
CSS 中的以下几个属性能触发硬件加速∶
-
transform
-
opacity
-
filter
-
Will-change
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速。
# 其他
算法优化:
- 线性函数代替真实计算
- 几何模型优化
- 碰撞检测优化
内存 / 缓存优化
离屏绘制
# 参考资料
- 字节青训营课程
- Wikipedia