從0到1,開發一個動畫庫(2)

傳送門:從0到1,開發一個動畫庫(1)javascript

上一節講到了最基礎的內容,爲動畫構建「幀-值」對應的函數關係,完成「由幀到值」的計算過程。這一節將在上節代碼的基礎上談談如何給一個完整的動畫添加各種事件。css

在添加各種事件以前,咱們先對_loop循環函數進行一些改進:html

_loop() {
  const t = Date.now() - this.beginTime,
        d = this.duration,
        func = Tween[this.timingFunction] || Tween['linear'];

  if (this.state === 'end' || t >= d) {
    this._end();
  } else if (this.state === 'stop') {
    this._stop(t);
  } else if (this.state === 'init') {
    this._reset();
  } else {
    this._renderFunction(t, d, func)
    window.requestAnimationFrame(this._loop.bind(this));
  }
}

能夠清晰地看到,咱們在循環中增長了不少類型的判斷,根據state當前不一樣的狀態執行相應的處理函數:咱們新增了_end_stop_reset方法分別去處理結束、暫停和重置這三種狀態,接下來咱們依次講解這些狀態的處理。java

End

咱們在Core類下增長_endendrenderEndState方法,end方法用於主動結束動畫:segmentfault

end() {
  this.state === 'play' ? (this.state = 'end') : this._end();
}

_end() {
  this.state = 'end';
  this._renderEndState();
  this.onEnd && this.onEnd();
}

_renderEndState() {
  const d = this.duration,
        func = Tween[this.timingFunction] || Tween['linear'];
  this._renderFunction(d, d, func);
}

經過執行end方法,咱們能夠主動結束動畫:若是當前目標處於運動狀態,則將其設置爲end,所以下一個_loop函數被執行的時候,程序就被流向了_end處理函數;若爲其餘狀態,意味着循環沒有被打開,咱們就直接調用_end方法,使其直接到終止狀態。app

_end函數的做用有三個:函數

  • 將當前狀態設置爲end(爲什麼要重複設置一次狀態呢?這不是多餘的嗎?其實,假若咱們主動觸發end去結束動畫,這的確是多餘的,但若是是動畫本身進行到了末尾,也就是t >= d的時刻,則必須得在_end中去設置狀態,以確保它處於結束狀態)
  • 經過_renderEndState方法,將目標變成結束狀態
  • 如有回調函數則執行回調

Reset

重置動畫的方式也是大同小異,與上面同樣oop

reset() {
  this.state === 'play' ? (this.state = 'init') : this._reset();
}

_reset() {
  this.state = 'init';
  this._renderInitState();
  this.onReset && this.onReset();
}

_renderInitState() {
  const d = this.duration,
        func = Tween[this.timingFunction] || Tween['linear'];
  this._renderFunction(0, d, func);
}

Stop

讓動畫暫停也是與上述二者同樣,但惟一的區別是,須要給_renderStopState方法傳入當前時間進度:動畫

stop() {
  if (this.state === 'play') {
    this.state = 'stop';
  } else {
    // 使目標暫停,無需像end或reset那樣將目標變成結束/起始狀態,保持當前狀態便可
    this.state = 'stop';
    this.onStop && this.onStop();
  }
}

_stop(t) {
  this.state = 'stop';
  this._renderStopState(t);
  this.onStop && this.onStop();
}

_renderStopState(t) {
  const d = this.duration,
        func = Tween[this.timingFunction] || Tween['linear'];
  this._renderFunction(t, d, func);
}

play

咱們要在動畫開始執行的時候觸發onPlay事件,只需在_play方法內增長一行代碼便可:this

_play() {
      this.state = 'play';
  
      // 新增部分
    this.onPlay && this.onPlay();
  
      this.beginTime = Date.now();
      const loop = this._loop.bind(this);
    window.requestAnimationFrame(loop);
}```

完整代碼以下:

import Tween from './tween';

class Core {
    constructor(opt) {
        this._init(opt);
        this.state = 'init';
    }

    _init(opt) {
    this._initValue(opt.value);
    this.duration = opt.duration || 1000;
    this.timingFunction = opt.timingFunction || 'linear';
    this.renderFunction = opt.render || this._defaultFunc;

    /* Events */
    this.onPlay = opt.onPlay;
    this.onEnd = opt.onEnd;
    this.onStop = opt.onStop;
    this.onReset = opt.onReset;
  }

  _initValue(value) {
      this.value = [];
      value.forEach(item => {
          this.value.push({
              start: parseFloat(item[0]),
              end: parseFloat(item[1]),
          });
      })
  }

  _loop() {
    const t = Date.now() - this.beginTime,
      d = this.duration,
      func = Tween[this.timingFunction] || Tween['linear'];

    if (this.state === 'end' || t >= d) {
      this._end();
    } else if (this.state === 'stop') {
      this._stop(t);
    } else if (this.state === 'init') {
      this._reset();
    } else {
      this._renderFunction(t, d, func)
      window.requestAnimationFrame(this._loop.bind(this));
    }
  }

  _renderFunction(t, d, func) {
      const values = this.value.map(value => func(t, value.start, value.end - value.start, d));
      this.renderFunction.apply(this, values);
  }
  
  _renderEndState() {
    const d = this.duration,
      func = Tween[this.timingFunction] || Tween['linear'];
    this._renderFunction(d, d, func);
  }

  _renderInitState() {
    const d = this.duration,
      func = Tween[this.timingFunction] || Tween['linear'];
    this._renderFunction(0, d, func);
  }

  _renderStopState(t) {
    const d = this.duration,
      func = Tween[this.timingFunction] || Tween['linear'];
    this._renderFunction(t, d, func);
  }

  _stop(t) {
    this.state = 'stop';
    this._renderStopState(t);
    this.onStop && this.onStop();
  }

  _play() {
      this.state = 'play';
    this.onPlay && this.onPlay();
    
      this.beginTime = Date.now();
      const loop = this._loop.bind(this);
    window.requestAnimationFrame(loop);
  }

  _end() {
    this.state = 'end';
    this._renderEndState();
    this.onEnd && this.onEnd.call(this);
  }

  _reset() {
    this.state = 'init';
    this._renderInitState();
    this.onReset && this.onReset();
  }

  play() {
      this._play();
  }

  end() {
    this.state === 'play' ? (this.state = 'end') : this._end();
  }

  reset() {
    this.state === 'play' ? (this.state = 'init') : this._reset();
  }

  stop() {
    if (this.state === 'play') {
      this.state = 'stop';
    } else {
      this.state = 'stop';
      this.onStop && this.onStop();
    }
  }
}

window.Timeline = Core;

相應地,html的代碼也更新以下,添加了各種按鈕,主動觸發目標的各種事件:

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style type="text/css">
        #box {
            width: 100px;
            height: 100px;
            background: green;
        }
    </style>
</head>
<body>
<div id="box"></div>
<button id="start">START</button>
<button id="end">END</button>
<button id="stop">STOP</button>
<button id="reset">RESET</button>
<script type="text/javascript" src="timeline.min.js"></script>
<script type="text/javascript">
    const el = (name) => document.querySelector(name);
    const box = el('#box');
    const timeline = new Timeline({
        duration: 3000,
        value: [[0, 400], [0, 600]],
        render: function(value1, value2) {
            box.style.transform = `translate(${ value1 }px, ${ value2 }px)`;
        },
        timingFunction: 'easeOut',
        onPlay: () => console.log('play'),
        onEnd: () => console.log('end'),
        onReset: () =>  console.log('reset'),
        onStop: () => console.log('stop')
    })

    el('#start').onclick = () => timeline.play();
    el('#end').onclick = () => timeline.end();
    el('#stop').onclick = () => timeline.stop()
    el('#reset').onclick = () => timeline.reset()
</script>
</body>
</html>

看到這裏,咱們第二節的內容就結束啦,下一節,咱們將介紹:

  • 支持自定義路徑動畫
  • 動畫間的鏈式調用

下一節再見啦^_^

相關文章
相關標籤/搜索