从 0 到 1,开发一个动画库(1)

2018 年 1 月 29 日 前端大全

(点击上方公众号,可快速关注)

作者:jshao

https://segmentfault.com/a/1190000012923589


如今市面上关于动画的开源库多得数不胜数,有关于CSS、js甚至是canvas渲染的,百花齐放,效果炫酷。但你是否曾想过,自己亲手去实现(封装)一个简单的动画库?

本文将从零开始,讲授如何搭建一个简单的动画库,它将具备以下几个特征:

  • 从实际动画中抽象出来,根据给定的动画速度曲线,完成“由帧到值”的计算过程,而实际渲染则交给开发者决定,更具拓展性

  • 支持基本的事件监听,如 onPlay、 onStop、 onReset、 onEnd,及相应的回调函数

  • 支持手动式触发动画的各种状态,如 play、 stop、 reset、 end

  • 支持自定义路径动画

  • 支持多组动画的链式触发

完整的项目在这里:点赞行为高尚!,欢迎各种吐槽和指正^_^

OK,话不多说,现在正式开始。

作为开篇,本节将介绍的是最基本、最核心的步骤——构建“帧-值”对应的函数关系,完成“由帧到值”的计算过程。

目录结构

首先介绍下我们的项目目录结构:

  
    
    
    
  1. /timeline

  2.  /index..js

  3.  /core.js

  4.  /tween.js

/timeline 是本项目的根目录,各文件的作用分别如下:

  • index.js 项目入口文件

  • core.js 动画核心文件

  • easing.js 存放基本缓动函数

引入缓动函数

所谓动画,简单来说,就是在一段时间内不断改变目标某些状态的结果。这些状态值在运动过程中,随着时间不断发生变化,状态值与时间存在一一对应的关系,这就是所谓的“帧-值”对应关系,常说的动画缓动函数也是相同的道理。

有了这种函数关系,给定任意一个时间点,我们都能计算出对应的状态值。OK,那如何在动画中引入缓动函数呢?不说废话,直接上代码。

首先我们在core.js中创建了一个 Core类:

  
    
    
    
  1. class Core {

  2.  constructor(opt) {

  3.    // 初始化,并将实例当前状态设置为'init'

  4.    this._init(opt);

  5.    this.state = 'init';

  6.  }

  7.  _init(opt) {

  8.    this._initValue(opt.value);

  9.    // 保存动画总时长、缓动函数以及渲染函数

  10.    this.duration = opt.duration || 1000;

  11.    this.timingFunction = opt.timingFunction || 'linear';

  12.    this.renderFunction = opt.render || this._defaultFunc;

  13.    // 未来会用到的事件函数

  14.    this.onPlay = opt.onPlay;

  15.    this.onEnd = opt.onEnd;

  16.    this.onStop = opt.onStop;

  17.    this.onReset = opt.onReset;

  18.  }

  19.  _initValue(value) {

  20.    // 初始化运动值

  21.      this.value = [];

  22.      value.forEach(item => {

  23.        this.value.push({

  24.          start: parseFloat(item[0]),

  25.          end: parseFloat(item[1]),

  26.        });

  27.      });

  28.  }

  29. }

我们在构造函数中对实例调用 _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类添加一个循环函数:

  
    
    
    
  1. _loop() {

  2.  const t = Date.now() - this.beginTime,

  3.        d = this.duration,

  4.        func = Tween[this.timingFunction] || Tween['linear'];

  5.  if (t >= d) {

  6.    this.state = 'end';

  7.    this._renderFunction(d, d, func);

  8.  } else {

  9.    this._renderFunction(t, d, func);

  10.    window.requestAnimationFrame(this._loop.bind(this));

  11.  }

  12. }

  13. _renderFunction(t, d, func) {

  14.  const values = this.value.map(value => func(t, value.start, value.end - value.start, d));

  15.  this.renderFunction.apply(this, values);

  16. }

_loop的作用是:倘若当前时间进度 t还未到终点,则根据当前时间进度计算出目标现在的状态值,并以参数的形式传给即将调用的渲染函数,即 renderFunction,并继续循环。如果大于 duration,则将目标的运动终止值传给 renderFunction,运动结束,将状态设为 end

代码中的 Tween是从tween.js文件引入的缓动函数,tween.js的代码如下(网上搜搜基本都差不多= =):

  
    
    
    
  1. /*

  2. * t: current time(当前时间);

  3. * b: beginning value(初始值);

  4. * c: change in value(变化量);

  5. * d: duration(持续时间)。

  6. * Get effect on 'http://easings.net/zh-cn'

  7. */

  8. const Tween = {

  9.    linear: function (t, b, c, d) {

  10.        return c * t / d + b;

  11.    },

  12.    // Quad

  13.    easeIn: function (t, b, c, d) {

  14.        return c * (t /= d) * t + b;

  15.    },

  16.    easeOut: function (t, b, c, d) {

  17.        return -c * (t /= d) * (t - 2) + b;

  18.    },

  19.    easeInOut: function (t, b, c, d) {

  20.        if ((t /= d / 2) < 1) return c / 2 * t * t + b;

  21.        return -c / 2 * ((--t) * (t - 2) - 1) + b;

  22.    },

  23.    // Cubic

  24.    easeInCubic: function (t, b, c, d) {

  25.        return c * (t /= d) * t * t + b;

  26.    },

  27.    easeOutCubic: function (t, b, c, d) {

  28.        return c * ((t = t / d - 1) * t * t + 1) + b;

  29.    },

  30.    easeInOutCubic: function (t, b, c, d) {

  31.        if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;

  32.        return c / 2 * ((t -= 2) * t * t + 2) + b;

  33.    },

  34.    // Quart

  35.    easeInQuart: function (t, b, c, d) {

  36.        return c * (t /= d) * t * t * t + b;

  37.    },

  38.    easeOutQuart: function (t, b, c, d) {

  39.        return -c * ((t = t / d - 1) * t * t * t - 1) + b;

  40.    },

  41.    easeInOutQuart: function (t, b, c, d) {

  42.        if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;

  43.        return -c / 2 * ((t -= 2) * t * t * t - 2) + b;

  44.    },

  45.    // Quint

  46.    easeInQuint: function (t, b, c, d) {

  47.        return c * (t /= d) * t * t * t * t + b;

  48.    },

  49.    easeOutQuint: function (t, b, c, d) {

  50.        return c * ((t = t / d - 1) * t * t * t * t + 1) + b;

  51.    },

  52.    easeInOutQuint: function (t, b, c, d) {

  53.        if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;

  54.        return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;

  55.    },

  56.    // Sine

  57.    easeInSine: function (t, b, c, d) {

  58.        return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;

  59.    },

  60.    easeOutSine: function (t, b, c, d) {

  61.        return c * Math.sin(t / d * (Math.PI / 2)) + b;

  62.    },

  63.    easeInOutSine: function (t, b, c, d) {

  64.        return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;

  65.    },

  66.    // Expo

  67.    easeInExpo: function (t, b, c, d) {

  68.        return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;

  69.    },

  70.    easeOutExpo: function (t, b, c, d) {

  71.        return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;

  72.    },

  73.    easeInOutExpo: function (t, b, c, d) {

  74.        if (t == 0) return b;

  75.        if (t == d) return b + c;

  76.        if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;

  77.        return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;

  78.    },

  79.    // Circ

  80.    easeInCirc: function (t, b, c, d) {

  81.        return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;

  82.    },

  83.    easeOutCirc: function (t, b, c, d) {

  84.        return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;

  85.    },

  86.    easeInOutCirc: function (t, b, c, d) {

  87.        if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;

  88.        return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;

  89.    },

  90.    // Elastic

  91.    easeInElastic: function (t, b, c, d, a, p) {

  92.        let s;

  93.        if (t == 0) return b;

  94.        if ((t /= d) == 1) return b + c;

  95.        if (typeof p == "undefined") p = d * .3;

  96.        if (!a || a < Math.abs(c)) {

  97.            s = p / 4;

  98.            a = c;

  99.        } else {

  100.            s = p / (2 * Math.PI) * Math.asin(c / a);

  101.        }

  102.        return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;

  103.    },

  104.    easeOutElastic: function (t, b, c, d, a, p) {

  105.        let s;

  106.        if (t == 0) return b;

  107.        if ((t /= d) == 1) return b + c;

  108.        if (typeof p == "undefined") p = d * .3;

  109.        if (!a || a < Math.abs(c)) {

  110.            a = c;

  111.            s = p / 4;

  112.        } else {

  113.            s = p / (2 * Math.PI) * Math.asin(c / a);

  114.        }

  115.        return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);

  116.    },

  117.    easeInOutElastic: function (t, b, c, d, a, p) {

  118.        let s;

  119.        if (t == 0) return b;

  120.        if ((t /= d / 2) == 2) return b + c;

  121.        if (typeof p == "undefined") p = d * (.3 * 1.5);

  122.        if (!a || a < Math.abs(c)) {

  123.            a = c;

  124.            s = p / 4;

  125.        } else {

  126.            s = p / (2 * Math.PI) * Math.asin(c / a);

  127.        }

  128.        if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;

  129.        return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;

  130.    },

  131.    // Back

  132.    easeInBack: function (t, b, c, d, s) {

  133.        if (typeof s == "undefined") s = 1.70158;

  134.        return c * (t /= d) * t * ((s + 1) * t - s) + b;

  135.    },

  136.    easeOutBack: function (t, b, c, d, s) {

  137.        if (typeof s == "undefined") s = 1.70158;

  138.        return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;

  139.    },

  140.    easeInOutBack: function (t, b, c, d, s) {

  141.        if (typeof s == "undefined") s = 1.70158;

  142.        if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;

  143.        return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;

  144.    },

  145.    // Bounce

  146.    easeInBounce: function (t, b, c, d) {

  147.        return c - Tween.easeOutBounce(d - t, 0, c, d) + b;

  148.    },

  149.    easeOutBounce: function (t, b, c, d) {

  150.        if ((t /= d) < (1 / 2.75)) {

  151.            return c * (7.5625 * t * t) + b;

  152.        } else if (t < (2 / 2.75)) {

  153.            return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;

  154.        } else if (t < (2.5 / 2.75)) {

  155.            return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;

  156.        } else {

  157.            return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;

  158.        }

  159.    },

  160.    easeInOutBounce: function (t, b, c, d) {

  161.        if (t < d / 2) {

  162.            return Tween.easeInBounce(t * 2, 0, c, d) * .5 + b;

  163.        } else {

  164.            return Tween.easeOutBounce(t * 2 - d, 0, c, d) * .5 + c * .5 + b;

  165.        }

  166.    }

  167. };

  168. export default Tween;

最后,给 Core类增加 play方法:

  
    
    
    
  1. _play() {

  2.  this.state = 'play';

  3.  this.beginTime = Date.now();

  4.  // 执行动画循环

  5.  const loop = this._loop.bind(this);

  6.  window.requestAnimationFrame(loop);

  7. }

  8. play() {

  9.  this._play();

  10. }

core.js的完整代码如下:

  
    
    
    
  1. import Tween from './tween';

  2. class Core {

  3.    constructor(opt) {

  4.        this._init(opt);

  5.        this.state = 'init';

  6.    }

  7.    _init(opt) {

  8.    this._initValue(opt.value);

  9.    this.duration = opt.duration || 1000;

  10.    this.timingFunction = opt.timingFunction || 'linear';

  11.    this.renderFunction = opt.render || this._defaultFunc;

  12.    /* Events */

  13.    this.onPlay = opt.onPlay;

  14.    this.onEnd = opt.onEnd;

  15.    this.onStop = opt.onStop;

  16.    this.onReset = opt.onReset;

  17.  }

  18.  _initValue(value) {

  19.      this.value = [];

  20.      value.forEach(item => {

  21.          this.value.push({

  22.              start: parseFloat(item[0]),

  23.              end: parseFloat(item[1]),

  24.          });

  25.      })

  26.  }

  27.  _loop() {

  28.      const t = Date.now() - this.beginTime,

  29.          d = this.duration,

  30.          func = Tween[this.timingFunction] || Tween['linear'];

  31.    if (t >= d) {

  32.        this.state = 'end';

  33.        this._renderFunction(d, d, func);

  34.    } else {

  35.        this._renderFunction(t, d, func);

  36.        window.requestAnimationFrame(this._loop.bind(this));

  37.    }

  38.  }

  39.  _renderFunction(t, d, func) {

  40.      const values = this.value.map(value => func(t, value.start, value.end - value.start, d));

  41.      this.renderFunction.apply(this, values);

  42.  }

  43.  _play() {

  44.      this.state = 'play';

  45.      this.beginTime = Date.now();

  46.      const loop = this._loop.bind(this);

  47.    window.requestAnimationFrame(loop);

  48.  }

  49.  play() {

  50.      this._play();

  51.  }

  52. }

  53. window.Timeline = Core;

在html中引入它后就可以愉快地调用啦^ _ ^

PS:该项目是用webpack打包并以timeline.min.js作为输出文件,由于暂时没用到index.js文件,因此暂时以core.js作为打包入口啦~

  
    
    
    
  1. <!DOCTYPE html>

  2. <html>

  3. <head>

  4.    <title></title>

  5.    <style type="text/css">

  6.        #box {

  7.            width: 100px;

  8.            height: 100px;

  9.            background: green;

  10.        }

  11.    </style>

  12. </head>

  13. <body>

  14. <div id="box"></div>

  15. <script type="text/javascript" src="timeline.js"></script>

  16. <script type="text/javascript">

  17.    const box = document.querySelector('#box');

  18.    const timeline = new Timeline({

  19.        duration: 3000,

  20.        value: [[0, 400], [0, 600]],

  21.        render: function(value1, value2) {

  22.            box.style.transform = `translate(${ value1 }px, ${ value2 }px)`;

  23.        },

  24.        timingFunction: 'easeOut',

  25.    })

  26.    timeline.play();

  27. </script>

  28. </body>

  29. </html>

看到这里,本文就差不多结束了,下节将介绍如何在项目中加入各类事件监听及触发方式。

本系列文章将会继续不定期更新,欢迎各位大大指正^_^


觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

登录查看更多
1

相关内容

【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
60+阅读 · 2020年6月26日
FPGA加速系统开发工具设计:综述与实践
专知会员服务
68+阅读 · 2020年6月24日
专知会员服务
81+阅读 · 2020年6月20日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
164+阅读 · 2020年5月14日
【综述】自动驾驶领域中的强化学习,附18页论文下载
专知会员服务
175+阅读 · 2020年2月8日
知识图谱更新技术研究及其应用,复旦大学硕士论文
专知会员服务
105+阅读 · 2019年11月4日
用 Python 开发 Excel 宏脚本的神器
私募工场
26+阅读 · 2019年9月8日
开发、调试计算机视觉代码有哪些技巧?
AI研习社
3+阅读 · 2018年7月9日
实战 | 用Python做图像处理(三)
七月在线实验室
15+阅读 · 2018年5月29日
教你用Python来玩跳一跳
七月在线实验室
6+阅读 · 2018年1月2日
【强烈推荐】浅谈将Pytorch模型从CPU转换成GPU
机器学习研究会
7+阅读 · 2017年12月24日
十五条有用的Golang编程经验
CSDN大数据
5+阅读 · 2017年8月7日
iOS高级调试&逆向技术
CocoaChina
3+阅读 · 2017年7月30日
Real-time Scalable Dense Surfel Mapping
Arxiv
5+阅读 · 2019年9月10日
Arxiv
5+阅读 · 2018年5月1日
Arxiv
3+阅读 · 2012年11月20日
VIP会员
相关VIP内容
相关资讯
用 Python 开发 Excel 宏脚本的神器
私募工场
26+阅读 · 2019年9月8日
开发、调试计算机视觉代码有哪些技巧?
AI研习社
3+阅读 · 2018年7月9日
实战 | 用Python做图像处理(三)
七月在线实验室
15+阅读 · 2018年5月29日
教你用Python来玩跳一跳
七月在线实验室
6+阅读 · 2018年1月2日
【强烈推荐】浅谈将Pytorch模型从CPU转换成GPU
机器学习研究会
7+阅读 · 2017年12月24日
十五条有用的Golang编程经验
CSDN大数据
5+阅读 · 2017年8月7日
iOS高级调试&逆向技术
CocoaChina
3+阅读 · 2017年7月30日
Top
微信扫码咨询专知VIP会员