第六届360前端星计划_JS 动画原理与实现

JS 动画原理与实现

主讲人

  • 吴亮
  • 360前端技术专家
  • 奇舞团

动画

动画的基本原理:

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

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

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

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

自由落体运动

代码:

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

摩擦力:

代码:

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

平抛

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

旋转+平抛

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

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

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

椭圆轨迹

周期运动

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

椭圆周期运动

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

连续运动

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

线性插值(lerp)

f(p) = from + (to - from) * p

f(p) = 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});
})();

弹跳的小球

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

弹跳的小球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)});
  }
})();

滚动

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

平稳变速

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

甩球

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

逐帧动画


Web Animation API(Working Draft)

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

代码:

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

你可能感兴趣的:(第六届360前端星计划_JS 动画原理与实现)