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

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

現在市面上關於動畫的開源庫多得數不勝數,有關於CSS、js甚至是canvas渲染的,百花齊放,效果炫酷。但你是否曾想過,本身親手去實現(封裝)一個簡單的動畫庫?css

本文將從零開始,講授如何搭建一個簡單的動畫庫,它將具有如下幾個特徵:html

  • 從實際動畫中抽象出來,根據給定的動畫速度曲線,完成「由幀到值」的計算過程,而實際渲染則交給開發者決定,更具拓展性
  • 支持基本的事件監聽,如onPlayonStoponReset onEnd,及相應的回調函數
  • 支持手動式觸發動畫的各類狀態,如playstopresetend
  • 支持自定義路徑動畫
  • 支持多組動畫的鏈式觸發

完整的項目在這裏:https://github.com/JS-Hao/tim...,歡迎各類吐槽和指正^_^java

OK,話很少說,如今正式開始。webpack

做爲開篇,本節將介紹的是最基本、最核心的步驟——構建「幀-值」對應的函數關係,完成「由幀到值」的計算過程。git

目錄結構

首先介紹下咱們的項目目錄結構:github

/timeline
    /index..js
    /core.js
    /tween.js

/timeline是本項目的根目錄,各文件的做用分別以下:web

  • index.js 項目入口文件
  • core.js 動畫核心文件
  • easing.js 存放基本緩動函數

引入緩動函數

所謂動畫,簡單來講,就是在一段時間內不斷改變目標某些狀態的結果。這些狀態值在運動過程當中,隨着時間不斷髮生變化,狀態值與時間存在一一對應的關係,這就是所謂的「幀-值」對應關係,常說的動畫緩動函數也是相同的道理。canvas

有了這種函數關係,給定任意一個時間點,咱們都能計算出對應的狀態值。OK,那如何在動畫中引入緩動函數呢?不說廢話,直接上代碼:segmentfault

首先咱們在core.js中建立了一個Core類:

class Core {
  constructor(opt) {
    // 初始化,並將實例當前狀態設置爲'init'
    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;

    // 將來會用到的事件函數
    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]),
        });
      });
  }
}

咱們在構造函數中對實例調用_init函數,對其初始化:將傳入的參數保存在實例屬性中。

當你看到_initValue的時候可能不大明白:外界傳入的value究竟是啥?其實value是一個數組,它的每個元素都保存着獨立動畫的起始與結束兩種狀態。這樣說好像有點亂,舉個栗子好了:假設咱們要建立一個動畫,讓頁面上的div同時往右、左分別平移300px、500px,此外還同時把本身放大1.5倍。在這個看似複雜的動畫過程當中,其實能夠拆解成三個獨立的動畫,每一動畫都有本身的起始與終止值:

  • 對於往右平移,就是把css屬性的translateX的0px變成了300px
  • 同理,往下平移,就是把tranlateY的0px變成500px
  • 放大1.5倍,也就是把`scale從1變成1.5

所以傳入的value應該長成這樣:[[0, 300], [0, 500], [1, 1.5]] 。咱們將數組的每個元素依次保存在實例的value屬性中。

此外,renderFunction是由外界提供的渲染函數,即opt.render,它的做用是:

動畫運動的每一幀,都會調用一次該函數,並把計算好的當前狀態值以參數形式傳入,有了當前狀態值,咱們就能夠自由地選擇渲染動畫的方式啦。

接下來咱們給Core類添加一個循環函數:

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

  if (t >= d) {
    this.state = 'end';
    this._renderFunction(d, d, func);
  } 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);
}

_loop的做用是:假若當前時間進度t還未到終點,則根據當前時間進度計算出目標如今的狀態值,並以參數的形式傳給即將調用的渲染函數,即renderFunction,並繼續循環。若是大於duration,則將目標的運動終止值傳給renderFunction,運動結束,將狀態設爲end

代碼中的Tween是從tween.js文件引入的緩動函數,tween.js的代碼以下(網上搜搜基本都差很少= =):

/*
 * t: current time(當前時間);
 * b: beginning value(初始值);
 * c: change in value(變化量);
 * d: duration(持續時間)。
 * Get effect on 'http://easings.net/zh-cn'
 */

const Tween = {
    linear: function (t, b, c, d) {
        return c * t / d + b;
    },
    // Quad
    easeIn: function (t, b, c, d) {
        return c * (t /= d) * t + b;
    },
    easeOut: function (t, b, c, d) {
        return -c * (t /= d) * (t - 2) + b;
    },
    easeInOut: function (t, b, c, d) {
        if ((t /= d / 2) < 1) return c / 2 * t * t + b;
        return -c / 2 * ((--t) * (t - 2) - 1) + b;
    },
    // Cubic
    easeInCubic: function (t, b, c, d) {
        return c * (t /= d) * t * t + b;
    },
    easeOutCubic: function (t, b, c, d) {
        return c * ((t = t / d - 1) * t * t + 1) + b;
    },
    easeInOutCubic: function (t, b, c, d) {
        if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
        return c / 2 * ((t -= 2) * t * t + 2) + b;
    },
    // Quart
    easeInQuart: function (t, b, c, d) {
        return c * (t /= d) * t * t * t + b;
    },
    easeOutQuart: function (t, b, c, d) {
        return -c * ((t = t / d - 1) * t * t * t - 1) + b;
    },
    easeInOutQuart: function (t, b, c, d) {
        if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
        return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
    },
    // Quint
    easeInQuint: function (t, b, c, d) {
        return c * (t /= d) * t * t * t * t + b;
    },
    easeOutQuint: function (t, b, c, d) {
        return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
    },
    easeInOutQuint: function (t, b, c, d) {
        if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
        return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
    },
    // Sine
    easeInSine: function (t, b, c, d) {
        return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
    },
    easeOutSine: function (t, b, c, d) {
        return c * Math.sin(t / d * (Math.PI / 2)) + b;
    },
    easeInOutSine: function (t, b, c, d) {
        return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
    },
    // Expo
    easeInExpo: function (t, b, c, d) {
        return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
    },
    easeOutExpo: function (t, b, c, d) {
        return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
    },
    easeInOutExpo: function (t, b, c, d) {
        if (t == 0) return b;
        if (t == d) return b + c;
        if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
        return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
    },
    // Circ
    easeInCirc: function (t, b, c, d) {
        return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
    },
    easeOutCirc: function (t, b, c, d) {
        return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
    },
    easeInOutCirc: function (t, b, c, d) {
        if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
        return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
    },
    // Elastic
    easeInElastic: function (t, b, c, d, a, p) {
        let s;
        if (t == 0) return b;
        if ((t /= d) == 1) return b + c;
        if (typeof p == "undefined") p = d * .3;
        if (!a || a < Math.abs(c)) {
            s = p / 4;
            a = c;
        } else {
            s = p / (2 * Math.PI) * Math.asin(c / a);
        }
        return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
    },
    easeOutElastic: function (t, b, c, d, a, p) {
        let s;
        if (t == 0) return b;
        if ((t /= d) == 1) return b + c;
        if (typeof p == "undefined") p = d * .3;
        if (!a || a < Math.abs(c)) {
            a = c;
            s = p / 4;
        } else {
            s = p / (2 * Math.PI) * Math.asin(c / a);
        }
        return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
    },
    easeInOutElastic: function (t, b, c, d, a, p) {
        let s;
        if (t == 0) return b;
        if ((t /= d / 2) == 2) return b + c;
        if (typeof p == "undefined") p = d * (.3 * 1.5);
        if (!a || a < Math.abs(c)) {
            a = c;
            s = p / 4;
        } else {
            s = p / (2 * Math.PI) * Math.asin(c / a);
        }
        if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
        return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;
    },
    // Back
    easeInBack: function (t, b, c, d, s) {
        if (typeof s == "undefined") s = 1.70158;
        return c * (t /= d) * t * ((s + 1) * t - s) + b;
    },
    easeOutBack: function (t, b, c, d, s) {
        if (typeof s == "undefined") s = 1.70158;
        return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
    },
    easeInOutBack: function (t, b, c, d, s) {
        if (typeof s == "undefined") s = 1.70158;
        if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
        return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
    },
    // Bounce
    easeInBounce: function (t, b, c, d) {
        return c - Tween.easeOutBounce(d - t, 0, c, d) + b;
    },
    easeOutBounce: function (t, b, c, d) {
        if ((t /= d) < (1 / 2.75)) {
            return c * (7.5625 * t * t) + b;
        } else if (t < (2 / 2.75)) {
            return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
        } else if (t < (2.5 / 2.75)) {
            return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
        } else {
            return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
        }
    },
    easeInOutBounce: function (t, b, c, d) {
        if (t < d / 2) {
            return Tween.easeInBounce(t * 2, 0, c, d) * .5 + b;
        } else {
            return Tween.easeOutBounce(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
        }
    }
};

export default Tween;

最後,給Core類增長play方法:

_play() {
  this.state = 'play';
  this.beginTime = Date.now();
  // 執行動畫循環
  const loop = this._loop.bind(this);
  window.requestAnimationFrame(loop);
}

play() {
  this._play();
}

core.js的完整代碼以下:

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 (t >= d) {
        this.state = 'end';
        this._renderFunction(d, d, func);
    } 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);
  }

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

  play() {
      this._play();
  }
}

window.Timeline = Core;

在html中引入它後就能夠愉快地調用啦^ _ ^

PS:該項目是用webpack打包並以timeline.min.js做爲輸出文件,因爲暫時沒用到index.js文件,所以暫時以core.js做爲打包入口啦~

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <style type="text/css">
        #box {
            width: 100px;
            height: 100px;
            background: green;
        }
    </style>
</head>
<body>
<div id="box"></div>
<script type="text/javascript" src="timeline.js"></script>
<script type="text/javascript">
    const box = document.querySelector('#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',
    })
    timeline.play();
</script>
</body>
</html>

看到這裏,本文就差很少結束了,下節將介紹如何在項目中加入各種事件監聽及觸發方式。

本系列文章將會繼續不按期更新,歡迎各位大大指正^_^

相關文章
相關標籤/搜索