前端星-11:前端动画

285 阅读3分钟

主讲导师:吴亮(月影)

动画的基本原理?

  1. 定时器改变对象的属性
  2. 根据新的属性重新渲染动画
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.贝塞尔轨迹:

包括两个顶点(p0和p3),两个控制点(p1和p2)

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:时间轴为贝塞尔轨迹

  1. B(px) 作为输入, B(py) 作为输出
  2. 通过牛顿迭代,从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',
});