主讲导师:吴亮(月影)
动画的基本原理?
- 定时器改变对象的属性
- 根据新的属性重新渲染动画
function update(context) {
// 更新属性
}
const ticker = new Ticker();
ticker.tick(update, context);
动画的种类?
- JavaScript 动画
- 操作DOM
- Canvas
- CSS 动画
- transition
- animation
- SVG 动画
- SMIL
js动画的优缺点?
优点:
- 灵活度
- 可控性
- 性能
缺点:
- 易用性差
举个小栗子?
简单的翻转动画第一版:
let rotation = 0;
requestAnimationFrame(function update() {
block.style.transform = `rotate(${rotation++}deg)`;
requestAnimationFrame(update);
});
优点:增量控制方法简单易用
缺点:不能精确控制翻转的速度(时间)
第二版改进:
let rotation = 0;
let startTime = null;
const T = 2000;
requestAnimationFrame(function update() {
if(!startTime) startTime = Date.now();
const p = (Date.now() - startTime)/T;
block.style.transform = `rotate(${360 * p}deg)`;
requestAnimationFrame(update);
});
优点:用时间进度来控制翻转的速度
缺点:不具备通用性
第三版改进:
function update({target}, {time}) {
target.style.transform = `rotate(${360 * time / 2000}deg)`;
}
class Ticker {
tick(update, context) {
let count = 0;
let startTime = Date.now();
requestAnimationFrame(function next() {
count++;
const time = Date.now() - startTime;
if(update(context, {count, time}) !== false) {
requestAnimationFrame(next);
}
});
}
}
const ticker = new Ticker();
ticker.tick(update, {target: block}); //block为需要操作的dom元素
优点:将动画逻辑与动画行为独立开来,自定义一个类,通过实例化传入动画行为,提高代码可复用性。 update方法也可以改为渲染canvas动画:
function update({context}, {time}) {
context.clearRect(0, 0, 512, 512);
context.save();
context.translate(100, 100);
context.rotate(time * 0.005);
context.fillStyle = '#00f';
context.fillRect(-50, -50, 100, 100);
context.restore();
}
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const ticker = new Ticker();
ticker.tick(update, {context});
第四版改进:增加周期控制和停止控制
function update({context}, {timing}) {
context.clearRect(0, 0, 512, 512);
context.save();
context.translate(100, 100);
context.rotate(Math.PI * 10 * timing.p); //20秒内旋转5周后停止
context.fillStyle = '#00f';
context.fillRect(-50, -50, 100, 100);
context.restore();
}
class Timing {
constructor({duration, easing} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
}
get time() {
return Date.now() - this.startTime;
}
get p() {
return this.easing(Math.min(this.time / this.duration, 1.0));
}
}
class Ticker {
tick(update, context, timing) {
let count = 0;
timing = new Timing(timing);
requestAnimationFrame(function next() {
count++;
if(update(context, {count, timing}) !== false) {
requestAnimationFrame(next);
}
});
}
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const ticker = new Ticker();
ticker.tick(update, {context},{duration:20000});
各种动画效果实现及相关原理?
1.匀速运动动画实现:
function update({target}, {timing}) {
target.style.transform = `translate(${200 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update,
{target: block},
{duration: 2000}
);
2.匀加速(自由落体):
function update({target}, {timing}) {
target.style.transform = `translate(0, ${200 * timing.p}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p ** 2,
});
3.匀减速(摩擦力):
function update({target}, {timing}) {
target.style.transform = `translate(${200 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p * (2 - p),
});
4.平抛:(x轴匀速运动,y轴匀加速运动)
class Timing {
constructor({duration, easing} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
}
get time() {
return Date.now() - this.startTime;
}
get op() {
return Math.min(this.time / this.duration, 1.0);
}
get p() {
return this.easing(this.op);
}
}
function update({target}, {timing}) {
target.style.transform =
`translate(${200 * timing.op}px, ${200 * timing.p}px)`;
}
5.旋转+平抛:
function update({target}, {timing}) {
target.style.transform = `
translate(${200 * timing.op}px, ${200 * timing.p}px)
rotate(${720 * timing.op}deg)
`;
}
6.贝塞尔轨迹:
function bezierPath(x1, y1, x2, y2, p) {
const x = 3 * x1 * p * (1 - p) ** 2 + 3 * x2 * p ** 2 * (1 - p) + p ** 3;
const y = 3 * y1 * p * (1 - p) ** 2 + 3 * y2 * p ** 2 * (1 - p) + p ** 3;
return [x, y];
}
function update({target}, {timing}) {
const [px, py] = bezierPath(0.2, 0.6, 0.8, 0.2, timing.p);
target.style.transform = `translate(${100 * px}px, ${100 * py}px)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: p => p * (2 - p),
});
7.实现bezier-easing:时间轴为贝塞尔轨迹
- B(px) 作为输入, B(py) 作为输出
- 通过牛顿迭代,从B(px)求p,从p求B(py)
//可用BezierEasing第三方库实现
function update({target}, {timing}) {
target.style.transform = `translate(${100 * timing.p}px, 0)`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block}, {
duration: 2000,
easing: BezierEasing(0.5, -1.5, 0.5, 2.5),
});
8.椭圆轨迹:
9.周期运动:
class Timing {
constructor({duration, easing, iterations = 1} = {}) {
this.startTime = Date.now();
this.duration = duration;
this.easing = easing || function(p){return p};
this.iterations = iterations;
}
get time() {
return Date.now() - this.startTime;
}
get finished() {
return this.time / this.duration >= 1.0 * this.iterations;
}
get op() {
let op = Math.min(this.time / this.duration, 1.0 * this.iterations);
if(op < 1.0) return op;
op -= Math.floor(op);
return op > 0 ? op : 1.0;
}
get p() {
return this.easing(this.op);
}
}
10.椭圆周期运动:
function update({target}, {timing}) {
const x = 150 * Math.cos(Math.PI * 2 * timing.p);
const y = 100 * Math.sin(Math.PI * 2 * timing.p);
target.style.transform = `
translate(${x}px, ${y}px)
`;
}
const ticker = new Ticker();
ticker.tick(update, {target: block},
{duration: 2000, iterations: 10});
11.连续运动
class Ticker {
tick(update, context, timing) {
let count = 0;
timing = new Timing(timing);
return new Promise((resolve) => {
requestAnimationFrame(function next() {
count++;
if(update(context, {count, timing}) !== false && !timing.finished) {
requestAnimationFrame(next);
} else {
resolve(timing);
}
});
});
}
}
12.线性插值:
function lerp(setter, from, to) {
return function({target}, {timing}) {
const p = timing.p;
const value = {};
for(let key in to) {
value[key] = to[key] * p + from[key] * (1 - p);
}
setter(target, value);
}
}
13.弹跳的小球:
const down = lerp(setValue, {top: 100}, {top: 300});
const up = lerp(setValue, {top: 300}, {top: 100});
(async function() {
const ticker = new Ticker();
// noprotect
while(1) {
await ticker.tick(down, {target: block},
{duration: 2000, easing: p => p * p});
await ticker.tick(up, {target: block},
{duration: 2000, easing: p => p * (2 - p)});
}
})();
(async function() {
const ticker = new Ticker();
let damping = 0.7,
duration = 2000,
height = 300;
// noprotect
while(height >= 1) {
let down = lerp(setValue, {top: 400 - height}, {top: 400});
await ticker.tick(down, {target: block},
{duration, easing: p => p * p});
height *= damping ** 2;
duration *= damping;
let up = lerp(setValue, {top: 400}, {top: 400 - height});
await ticker.tick(up, {target: block},
{duration, easing: p => p * (2 - p)});
}
})();
14.滚动:
const roll = lerp((target, {left, rotate}) => {
target.style.left = `${left}px`;
target.style.transform = `rotate(${rotate}deg)`;
},
{left: 100, rotate: 0},
{left: 414, rotate: 720}); //周长=2*pi*半径
const ticker = new Ticker();
ticker.tick(roll, {target: block},
{duration: 2000, easing: p=>p});
15.平稳变速:
function forward(target, {y}) {
target.style.top = `${y}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(
lerp(forward, {y: 100}, {y: 200}),
{target: block},
{duration: 2000, easing: p => p * p}); //匀加速
await ticker.tick(
lerp(forward, {y: 200}, {y: 300}),
{target: block},
{duration: 1000, easing: p => p}); //匀速
await ticker.tick(
lerp(forward, {y: 300}, {y: 350}),
{target: block},
{duration: 1000, easing: p => p * (2 - p)}); //匀减速
}());
16.甩球:
function circle({target}, {timing}) {
const p = timing.p;
const rad = Math.PI * 2 * p;
const x = 200 + 100 * Math.cos(rad);
const y = 200 + 100 * Math.sin(rad);
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
function shoot({target}, {timing}) {
const p = timing.p;
const rad = Math.PI * 0.2;
const startX = 200 + 100 * Math.cos(rad);
const startY = 200 + 100 * Math.sin(rad);
const vX = -100 * Math.PI * 2 * Math.sin(rad);
const vY = 100 * Math.PI * 2 * Math.cos(rad);
const x = startX + vX * p;
const y = startY + vY * p;
target.style.left = `${x}px`;
target.style.top = `${y}px`;
}
(async function() {
const ticker = new Ticker();
await ticker.tick(circle, {target: block},
{duration: 2000, easing: p => p, iterations: 2.1});
await ticker.tick(shoot, {target: block},
{duration: 2000});
}());
17.关键帧动画Web Animation API
使用:element.animate(keyframes, options);
实现:
target.animate([
{backgroundColor: '#00f', width: '100px', height: '100px', borderRadius: '0'},
{backgroundColor: '#0a0', width: '200px', height: '100px', borderRadius: '0'},
{backgroundColor: '#f0f', width: '200px', height: '200px', borderRadius: '100px'},
], {
duration: 5000,
fill: 'forwards',
});