动画的基本原理:什么是动画、动画的历史、计算机动画原理 前端动画的分类:CSS 动画、SVG 动画、JS 动画、如何选择 前端动画如何实现(主要是 JS):JS 动画的函数封装、简单动画、复杂动画 相关实践:动画资源、工作实践、动画的优化
动画是通过快速连续排列彼此差异极小的连续图像来制造运动错觉和变化错觉的过程。
如今前端动画技术已经普及
最早的技术是 GIF,然后是 Flash,如今是 HTML/CSS/JS
计算机图形学:
计算机视觉的基础,涵盖点、线、面、体、场的数学构造方法。
计算机动画:
计算机图形学的分支,主要包含 2D、3D 动画。
无论动画多么简单,始终需要定义两个基本状态,即开始状态和结束状态。没有它们,我们将无法定义插值状态,从而填补了两者之间的空白。
帧:连续变换的多张画面,其中的每一幅画面都是一帧。
帧率:用于度量一定时间段内的帧数,通常的测量单位是 FPS (frame per second) 。
帧率与人眼:一般每秒 10-12 帧人会认为画面是连贯的,这个现象称为视觉暂留。对于一些电脑动画和游戏来说低于 30FPS 会感受到明显卡顿,目前主流的屏幕、显卡输出为 60FPS,效果会明显更流畅。
空白的补全方式有以下两种
补间动画:
传统动画,主画师绘制关键帧,交给清稿部门,清稿部门的补间动画师补充关键帧进行交付。(类比到这里,补间动画师由浏览器来担任,如 keyframe
, transition
)
逐帧动画 (Frame By Frame) :
从词语来说意味着全片每一帧逐帧都是纯手绘。(如 CSS 的 steps
实现精灵动画)
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 是基于 XML 的矢量图形描述语言,它可以与 CSS 和 S 较好的配合,实现 svg 动画通常有三种方式:SMIL、JS、CSS
我们经常使用 animation, transform, transition 来实现 svg 动画,它比 JS 更加简单方便。
优点:通过矢量元素实现动画,不同的屏幕下均可获得较好的清晰度。可以实现一些特殊的效果,如:描字,形变,墨水扩散等。
缺点:使用方式较为复杂,过多使用可能会带来性能问题。
SMIL: Synchronized Multimedia Integration Language (同步多媒体集成语言)
可以使用 svg 标签进行动画的描述,但是兼容性不是很理想
使用 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
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
计算 path 的长度: path.getTotalLength();
计算 path 上某个点的坐标: path.getPointAtLength(lengthNumber);
例子:https://codepen.io/jiangxiang/pen/eYWagxq
JS 可以实现复杂的动画,也可以操作 canvas 动画 API 上进行绘制。
CSS 优点:
CSS 缺点:
JS 优点:
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
函数协调
基本公式:
△r=△v△t\triangle r = \triangle v \triangle t △r=△v△t
简单理解: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
});
从这个动画开始,就需要考虑数学公式了,即:怎么把 v=g⋅t,即x=12gt2v = g·t,即x = \frac{1}{2} gt^{2}v=g⋅t,即x=21gt2 套用到 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
});
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(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
});
贝塞尔曲线的详细描述和公式见 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,
});
动画代码示例:
设计网站:
动画制作工具(一般都是 UE、UI 同学使用):
svg :
js :
css :
canvas :
在实际的应用里,最为简单的一个注意点就是,触发动画的开始不要用 display: none
属性值,因为它会引起 Layout、Paint 环节,通过切换类名就已经是一种很好的办法。
translate
属性值来替换 top/left/right/bottom
的切换, scale
属性值替换 width/height
, opacity
属性替换 display/visibility
等等。
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 repaint,所以能大大提高网页的性能。
CSS 中的以下几个属性能触发硬件加速∶
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速。
算法优化:
内存 / 缓存优化
离屏绘制