美文网首页
第六届360前端星计划_JS 动画原理与实现

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

作者: lhang_91ad | 来源:发表于2020-04-11 21:38 被阅读0次

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

    逐帧动画

    <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(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 动画原理与实现

          本文链接:https://www.haomeiwen.com/subject/mgbymhtx.html