资讯专栏INFORMATION COLUMN

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

canopus4u / 2322人阅读

摘要:传送门从到,开发一个动画库如今市面上关于动画的开源库多得数不胜数,有关于甚至是渲染的,百花齐放,效果炫酷。当你看到的时候可能不大明白外界传入的到底是啥其实是一个数组,它的每一个元素都保存着独立动画的起始与结束两种状态。

传送门:从0到1,开发一个动画库(2)

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

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

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

支持基本的事件监听,如onPlayonStoponReset onEnd,及相应的回调函数

支持手动式触发动画的各种状态,如playstopresetend

支持自定义路径动画

支持多组动画的链式触发

完整的项目在这里:https://github.com/JS-Hao/tim...,欢迎各种吐槽和指正^_^

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

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

目录结构

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

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

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

index.js 项目入口文件

core.js 动画核心文件

easing.js 存放基本缓动函数

引入缓动函数

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

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

首先我们在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作为打包入口啦~




    
    


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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92654.html

相关文章

  • 01开发一个画库(2)

    摘要:传送门从到,开发一个动画库上一节讲到了最基础的内容,为动画构建帧值对应的函数关系,完成由帧到值的计算过程。这一节将在上节代码的基础上谈谈如何给一个完整的动画添加各类事件。 传送门:从0到1,开发一个动画库(1) 上一节讲到了最基础的内容,为动画构建帧-值对应的函数关系,完成由帧到值的计算过程。这一节将在上节代码的基础上谈谈如何给一个完整的动画添加各类事件。 在添加各类事件之前,我们先对...

    adam1q84 评论0 收藏0
  • 01开发一个画库(1)

    摘要:传送门从到,开发一个动画库如今市面上关于动画的开源库多得数不胜数,有关于甚至是渲染的,百花齐放,效果炫酷。当你看到的时候可能不大明白外界传入的到底是啥其实是一个数组,它的每一个元素都保存着独立动画的起始与结束两种状态。 传送门:从0到1,开发一个动画库(2) 如今市面上关于动画的开源库多得数不胜数,有关于CSS、js甚至是canvas渲染的,百花齐放,效果炫酷。但你是否曾想过,自己亲手...

    jerry 评论0 收藏0
  • [译]2018年值得关注的10大JavaScript画库

    摘要:幸运的是,供应似乎与需求相匹配,并且有多种选择。让我们来看看年值得关注的十大动画库。八年了,仍然是一个强大的动画工具。接下来在这个令人惊叹的动画库列表上的就是了。,通常被称为动画平台,我们忽略它在列表中的排名,它是列表中最受欢迎的库之一。 原文链接原译文链接 现代网站的客户端依靠高质量的动画,这就使得JavaScript动画库的需求不断增加。幸运的是,供应似乎与需求相匹配,并且有多种选...

    Me_Kun 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<