月影)"/>
360前端星计划—前端动画可以这么玩(月影)
JS动画原理与实现
- 1. 动画的基本原理
- 2. 动画的种类
- 2.1 简单动画第一个版本:
- 2.2 第二个版本:
- 2.3 通用化
- 2.3 通用化2
- 2.3 通用化3
- 2.4 封装Timing
- 3. 实例
- 3.1 匀速运动
- 3.2 自由落体运动
- 3.3 摩擦力(匀减速)
- 3.4 平抛(x轴匀速和y轴匀加速)
- 3.5 旋转+平抛
- 3.5 贝塞尔轨迹
- 3.6 贝塞尔作easing
- 3.7 bezier-easing 轨迹
- 3.8 椭圆轨迹
- 3.9 椭圆周期运动
- 3.10 连续运动
- 3.11 线性插值(lerp)
- 3.12 弹跳的小球
- 3.13 弹跳的小球2
- 3.13 滚动
- 3.14 平稳变速
- 3.15 甩球
- 3.16 逐帧动画
- 4. Web Animation API
- 5. 总结
1. 动画的基本原理
- 定时器改变对象的属性
- 根据新的属性重新渲染动画
function update(context) {// 更新属性
}
const ticker = new Ticker();
ticker.tick(update, context);
2. 动画的种类
- JavaScript 动画
- 操作DOM
- Canvas - CSS 动画
- transition
- animation - SVG 动画
- SMIL
JavaScript 动画优缺点:
- 优点:灵活度、可控性、性能
- 缺点:易用性
2.1 简单动画第一个版本:
let rotation = 0;
requestAnimationFrame(function update() {block.style.transform = `rotate(${rotation++}deg)`; //增量,很难精确知道旋转周期requestAnimationFrame(update);
});
2.2 第二个版本:
let rotation = 0;
let startTime = null;
const T = 2000; //2s周期
requestAnimationFrame(function update() {if(!startTime) startTime = Date.now();const p = (Date.now() - startTime)/T;block.style.transform = `rotate(${360 * p}deg)`; requestAnimationFrame(update);
});
2.3 通用化
function update({target}, count) {target.style.transform = `rotate(${count++}deg)`;
}class Ticker {tick(update, context) {let count = 0;requestAnimationFrame(function next() {if(update(context, ++count) !== false) {requestAnimationFrame(next);}});}
}const ticker = new Ticker();
ticker.tick(update, {target: block});
2.3 通用化2
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});
2.3 通用化3
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();
}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);}});}
}
2.4 封装Timing
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);}});}}
3. 实例
3.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}
);
3.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.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),
});
3.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)`;
}
3.5 旋转+平抛
function update({target}, {timing}) {target.style.transform = `translate(${200 * timing.op}px, ${200 * timing.p}px)rotate(${720 * timing.op}deg)`;
}
3.5 贝塞尔轨迹
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),
});
3.6 贝塞尔作easing
- B(px) 作为输入, B(py) 作为输出
- 通过牛顿迭代,从B(px)求p,从p求B(py)
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),
});
3.7 bezier-easing 轨迹
function update({target}, {timing}) {target.style.transform =`translate(${100 * timing.p}px, ${100 * timing.op}px)`;
}const ticker = new Ticker();
ticker.tick(update, {target: block}, {duration: 2000,easing: BezierEasing(0.5, -1.5, 0.5, 2.5),
});
3.8 椭圆轨迹
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);}
}
3.9 椭圆周期运动
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});
3.10 连续运动
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);}}); });}
}
function left({target}, {timing}) {target.style.left = `${100 + 200 * timing.p}px`;
}
function down({target}, {timing}) {target.style.top = `${100 + 200 * timing.p}px`;
}
function right({target}, {timing}) {target.style.left = `${300 - 200 * timing.p}px`;
}
function up({target}, {timing}) {target.style.top = `${300 - 200 * timing.p}px`;
}(async function() {const ticker = new Ticker();await ticker.tick(left, {target: block},{duration: 2000});await ticker.tick(down, {target: block},{duration: 2000});await ticker.tick(right, {target: block},{duration: 2000});await ticker.tick(up, {target: block},{duration: 2000});
})();
3.11 线性插值(lerp)
f§ = from + (to - from) * p
f§ = to * p + from * (1 - p)
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);}
}
function setValue(target, value) {for(let key in value) {target.style[key] = `${value[key]}px`;}
}const left = lerp(setValue, {left: 100}, {left: 300});
const down = lerp(setValue, {top: 100}, {top: 300});
const right = lerp(setValue, {left: 300}, {left: 100});
const up = lerp(setValue, {top: 300}, {top: 100});(async function() {const ticker = new Ticker();await ticker.tick(left, {target: block},{duration: 2000});await ticker.tick(down, {target: block},{duration: 2000});await ticker.tick(right, {target: block},{duration: 2000});await ticker.tick(up, {target: block},{duration: 2000});
})();
3.12 弹跳的小球
const down = lerp(setValue, {top: 100}, {top: 300});
const up = lerp(setValue, {top: 300}, {top: 100});(async function() {const ticker = new Ticker();// noprotectwhile(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)});}
})();
3.13 弹跳的小球2
(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)});
}
})();
3.13 滚动
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});const ticker = new Ticker();ticker.tick(roll, {target: block},{duration: 2000, easing: p => p});
3.14 平稳变速
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)});
}());
3.15 甩球
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});
}());
3.16 逐帧动画
<style type="text/css">
.sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(.png);
}.bird0 {width:86px; height:60px; background-position: -178px -2px}
.bird1 {width:86px; height:60px; background-position: -90px -2px}
.bird2 {width:86px; height:60px; background-position: -2px -2px}#bird{position: absolute;left: 100px;top: 100px;zoom: 0.5;}
</style>
<div id="bird" class="sprite bird1"></div>
<script type="text/javascript">
var i = 0;
setInterval(function(){bird.className = "sprite " + 'bird' + ((i++) % 3);
}, 1000/10);
</script>
4. 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',
});
使用web annimation API实现的动画:
function animate(target, keyframes, options) {const anim = target.animate(keyframes, options);return new Promise((resolve) => {anim.onfinish = function() {resolve(anim);}});
}(async function() {await animate(ball1, [{top: '10px'},{top: '150px'},], {duration: 2000,easing: 'ease-in-out',fill: 'forwards',});await animate(ball2, [{top: '200px'},{top: '350px'},], {duration: 2000,easing: 'ease-in-out',fill: 'forwards',});await animate(ball3, [{top: '400px'},{top: '550px'},], {duration: 2000,easing: 'ease-in-out',fill: 'forwards',});
}());
5. 总结
通过时间进行动画
更多推荐
360前端星计划—前端动画可以这么玩(月影)
发布评论