前端动画

339 阅读2分钟

360前端星day4

动画的基本原理

  • 定时器改变对象的属性
  • 根据新的属性重新渲染动画

动画的种类

  1. JavaScript动画
    • 操作DOM
    • Canvas
  2. CSS动画
    • transition
    • animation
  3. 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);
});

通用化:封装Ticker对象,使用增量方式


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:使用time的方式

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});

通用化3:使用canvas绘制的方式,传入一个canvas的context

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);
            }
        });
    }
}

Timing:计时方式 使用easing变换,20s之内旋转5周


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);
            }
        });
    }
}

function update({target}, {timing}) {
    target.style.transform = `translate(${200 * timing.p}px, 0)`;
}

const ticker = new Ticker();
ticker.tick(update, {target: block}, {duration: 2000});

各种动画的实现

匀速运动

2s内向右匀速运动200px

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设置平方

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});

摩擦力 匀减速

easing是p*(2-p)

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)});

平抛

x轴和y轴同时translate 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)`;
}

旋转+平抛

加入rotate

function update({target}, {timing}) {
    target.style.transform = `
        translate(${200 * timing.op}px, ${200 * timing.p}px)
        rotate(${720 * timing.op}deg)
    `;
}

贝塞尔曲线

  • 平滑曲线(二阶、三阶的贝塞尔)
  • 三阶:两个顶点和两个控制点
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),
});

贝塞尔easing

控制时间,比如控制easing,通过牛顿迭代反过来求p

  • B(px)输入,B(py)输出
  • 从B(px)求p,从p求B(py)
  • 可以用现成的库

贝塞尔轨迹

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),
});

周期运动


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);
  }
}

连续运动

让tick方法返回promise

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);
                }
            });      
        });
    }
}

逐帧动画

每隔一定时间切换背景位置

<style type="text/css">
.sprite {
    display:inline-block; 
    overflow:hidden; 
    background-repeat: no-repeat;
    background-image:url(https://p.ssl.qhimg.com/t01f265b6b6479fffc4.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>

Web Animation API

用js实现css的关键帧动画

例子:依次的小球运动,封装promise

总结

  • 实现方式:增量和时间
  • 为了更准确:时间 Timing类
  • 把动画封装成promise,在动画结束的时候resolve