likes
comments
collection
share

从炫酷的波浪动画学习anime.js设计原理

作者站长头像
站长
· 阅读数 20

anime.js

Anime.js 是一个Star数44k的轻量级JavaScript动画库,提供了简单而灵活的API,支持创建所有类型的动画,包括关键帧动画、CSS 动画和 SVG 动画以及Javascript对象。 Anime.js具有清晰简洁的语法,可以轻松快速地创建出复杂动画。支持的动画功能包括缓动、时间轴、回调等,使其成为一个多功能的动画工具,可以为从简单的页面转换到复杂的UI元素的所有内容制作动画。

anime({
  targets: 'div',
  translateX: 250,
  rotate: '1turn',
  backgroundColor: '#FFF',
  duration: 800
});

Anime.js的一个关键特性是文件很小,非常适合在移动应用程序和其他性能至关重要的环境中使用。非常多的社区开发人员为其开发和改进做出了贡献, 因此也到秩序维护和更新。

学习优秀的开源库是了解底层原理的重要手段,anime.js的star数接近45k,也证明了它的优秀。相比其他动画库,anime.js提供stagger、timeline、control模块为动画执行提供了高灵活性、扩展性。

动画拆解

从炫酷的波浪动画学习anime.js设计原理 打开anime.js官网,首页即可看到上图的波浪效果,如何从0到实现一个类似的动画效果?除了赞叹炫酷之外,只剩一脸懵逼,不知如何下手。一个波浪动画的实现,大致可分为以下几个步骤:

  • 初始化元素和样式: 元素由32 * 16的矩阵组成,因此需要将这些元素按顺序排列并设置初始样式。
  • 动画参数设置: 设置动画执行效果、执行周期等属性。
  • 动画执行顺序:动画由某个随机位置(如位置[15, 8])向外蔓延,由近到远延迟执行。
  • 多关键帧实现: 一个复杂动画一般由多个关键帧组合构成。

接下来就按这几个步骤依次介绍anime.js的底层实现。

初始化元素和样式

.dots-wrapper节点作为元素排列的容器,水平、垂直居中显示在屏幕上。.dot指每个元素的样式,由于容器大小为32 * 16, 一共包含32*16个元素按[32, 16]矩阵排列。

    :root {
      font-size: 20px;
    }

    body {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: #000;
    }

    .stagger-visualizer {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-items: center;
      position: relative;
      width: 32rem;
      height: 16rem;
    }

    .dots-wrapper {
      position: absolute;
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
    }

    .stagger-visualizer .dot {
      position: relative;
      width: calc(1rem - 8px);
      height: calc(1rem - 8px);
      margin: 4px;
      border-radius: 50%;
      background-color: currentColor;
    }

设置矩阵大小为[32, 16],那么元素总数numberOfElements即为32*16,直接创建numberOfElements个dom元素并设置样式为dot

var staggerVisualizerEl = document.querySelector('.stagger-visualizer');
var dotsWrapperEl = staggerVisualizerEl.querySelector('.dots-wrapper');
var dotsFragment = document.createDocumentFragment();
var grid = [32, 16];
var cellSize = 1;
var numberOfElements = grid[0] * grid[1];
var animation;
var paused = true;

for (var i = 0; i < numberOfElements; i++) {
  var dotEl = document.createElement('div');
  dotEl.classList.add('dot');
  dotsFragment.appendChild(dotEl);
}

dotsWrapperEl.appendChild(dotsFragment);

从炫酷的波浪动画学习anime.js设计原理

参数设置

有了元素素材,接下来考虑让元素动起来。在实例化动画时,通过配置基础参数,确定动画的执行动作和顺序。几个关键参数:

  • keyFrames:类似CSS的关键帧,区别在于CSS关键帧按百分比分布,如0%、20%、...、100%分多个阶段,而这里的keyFrames定义为数组类型,后面单独介绍。
  • delay: 当动画的目标元素包含多个时,常常需要元素与元素间有延迟执行,如排列的32 * 16个元素,当从中间触发动画时,中间元素先执行、四周元素后执行。delay可以定义为函数类型,支持动态设置元素样式。
  • easing: 执行动画执行效果,包含淡入easIn、淡出easOut、淡入淡出三次贝塞尔easeInOutCubic。
  • complete: 假如当前动画结束后,需要自动执行下一次动画,complete指定上一次动画执行完成之后需要触发的回调。
function play() {
  ...
  animation = anime({
    targets: '.stagger-visualizer .dot',
    keyframes: [
      {
        zIndex: function(el, i, total) {
          return Math.round(anime.stagger([numberOfElements, 0], {grid: grid, from: index})(el, i, total));
        },
        translateX: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'x'}),
        translateY: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'y'}),
        duration: 200
      }, {
        translateX: anime.stagger('.075rem', {grid: grid, from: index, axis: 'x'}),
        translateY: anime.stagger('.075rem', {grid: grid, from: index, axis: 'y'}),
        scale: anime.stagger([2, .2], {grid: grid, from: index}),
        backgroundColor: staggeredGridColors,
        duration: 450
      }, {
        translateX: 0,
        translateY: 0,
        scale: 1,
        duration: 500,
      }
    ],
    delay: anime.stagger(60, {grid: grid, from: index}),
    easing: 'easeInOutQuad',
    complete: play
  })
  ...
}

play();

以上代码初始化了动画执行需要的参数

  • targets: 指定动画要素,也就是所有class包含.dot的元素;
  • complete: 设置当动画执行完成后的回调,这里当上一个动画执行完成后重复执行play函数,只是每次指定的初始位置index不同,设置为随机索引index = anime.random(0, numberOfElements-1)
  • delay: 指定元素与元素之间的延迟间隔,类型可以为number或函数。函数可以为(el, i) => i * 150,表示元素间动画延迟间隔为150ms。 函数还可设置为更高级的anime.stagger(150),stagger后面再详细介绍;
  • easing: 指定缓冲效果,这里设置为三次贝塞尔easeInOutQuad效果;
  • keyframes:数组类型,每一项设置元素属性值,如设置translateX、translateY、scale属性,指定周期duration等。属性值也可通过stagger动态调整,这里能看出stagger是一个非常重要的函数;

这几个参数的底层逻辑了解清楚后,anime.js的设计原理掌握也就八九不离十。

事件机制

anime({
    ...
    complete: play
 })

类似于React的生命周期机制,anime在动画执行环节提供了begin、change、complete多种生命周期事件,并在process进度更新过程触发相应事件,在初始化通过传入以下事件就能在外部实现监听和扩展。

// 设置动画hook事件、循环次数、方向等参数
const defaultInstanceSettings = {
  update: null, // 每一帧动画都会触发update
  begin: null, // 动画开始时触发,只执行一次
  loopBegin: null, // 当启动循环loop执行时,每一次循环开始都会触发loopBegin
  changeBegin: null, 
  change: null,
  changeComplete: null,
  loopComplete: null, // 一次循环结束
  complete: null, // 动画结束
  loop: 1, // 动画循环次数,如果为true表示无限循环
  direction: 'normal', // 动画执行方向,包含normal、revserse、alternate
  autoplay: true, // 是否自动播放
  timelineOffset: 0 // 
}

事件周期

在了解上面的各项事件之前,先介绍duration、delay、endDelay三个和时间相关的参数。 从炫酷的波浪动画学习anime.js设计原理 动画开始的时刻等同于上一个动画结束的时刻,如果没有上一个动画则开始时刻为0.

    tween.start = previousTween ? previousTween.end : 0;

动画结束的时刻需要加上delay、duration、enddelay三个时间段。

    tween.end = tween.start + tween.delay + tween.duration + tween.endDelay;

所以一个动画实际的运行周期为tween.end - tween.start,而动画真正的执行时间仅在duration阶段。

anime.js提供了update、begin、loopBegin、changeBegin、change、changeComplete、loopComplete、complete事件,这些事件可以分为两类,一类是按帧触发(如update、change),另一类按时间片刻触发。

  • 按帧触发:update、change的区别在于,update在start到end期间每一帧触发,而change仅在动画执行duration期间按帧触发。
  • 按片刻触发:执行的顺序begin->loopBegin->changeBegin->changeComplete->loopComplete->complete。

从炫酷的波浪动画学习anime.js设计原理 在整个动画执行期间,begin、complete只会执行一次,当设置了loop(如loop: 3)则loopBegin到loopComplete之间的事件会执行多次。

事件执行

anime动画实体提供有tick函数,通过requestAnimationFrame按帧执行。其中speed默认为1,lastTime默认为0, 因此setInstanceProgress传入的参数为已执行的时间差。

  instance.tick = function(t) {
    now = t;
    if (!startTime) startTime = now;
    setInstanceProgress((now + (lastTime - startTime)) * anime.speed);
  }

setInstanceProgress函数包含了动画执行过程的所有事件,函数整体分时间计算、启动事件、change事件、结束事件四部分。

  • 时间计算: insDuration动画执行周期、insDelay动画执行延期周期,insTime为已执行时间。adjustTime函数会判断动画的reversed是否反转属性,revsersed为true时动画反转同时进度也会从100%到0%。
      function setInstanceProgress(engineTime) {
        // 动画执行周期
        const insDuration = instance.duration;
        // 动画延迟执行时间
        const insDelay = instance.delay;
        
        const insEndDelay = insDuration - instance.endDelay;
        // 已执行时间
        const insTime = adjustTime(engineTime);
        instance.progress = minMax((insTime / insDuration) * 100, 0, 100);
        instance.reversePlayback = insTime < instance.currentTime;
        ...
      }
  • 启动事件: instance.began标记动画是否启动,如果第一次启动则触发begin事件。instance.loopBegan标记一次循环开始,同时触发loopBegin事件。接下来调用setAnimationsProgress函数设置两种边界的进度值,如果执行时间小于insDelay则设置进度为0,如果执行时间已经大于insEndDelay则设置进度为最大值。
  function setInstanceProgress(engineTime) {
    ...
    // 触发begin hook
    if (!instance.began && instance.currentTime > 0) {
      instance.began = true;
      setCallback('begin');
    }
    // 触发loopBegin,循环启动
    if (!instance.loopBegan && instance.currentTime > 0) {
      instance.loopBegan = true;
      setCallback('loopBegin');
    }
    // 如果执行时间小于insDelay,设置为0
    if (insTime <= insDelay && instance.currentTime !== 0) {
      setAnimationsProgress(0);
    }
    if ((insTime >= insEndDelay && instance.currentTime !== insDuration) || !insDuration) {
      setAnimationsProgress(insDuration);
    }
    ...
  }
  • Change事件: 当执行时间处于insDelay和insEndDelay之间时,第一次会触发changeBegin,但每一帧都会触发change事件。否则,表示本轮动画执行结束并触发changeComplete事件。
  function setInstanceProgress(engineTime) {
    ...
    // 当前时间大于delay并且小于endDelay,表示处于可执行时间
    if (insTime > insDelay && insTime < insEndDelay) {
      // 第一帧执行,触发changeBegin
      if (!instance.changeBegan) {
        instance.changeBegan = true;
        instance.changeCompleted = false;
        setCallback('changeBegin');
      }
      // 每一帧执行都会触发change hook
      setCallback('change');
      setAnimationsProgress(insTime);
    } else {
      // 如果changeBegan已触发,并且执行时间已超出,则触发changeComplete hook
      if (instance.changeBegan) {
        instance.changeCompleted = true;
        instance.changeBegan = false;
        setCallback('changeComplete');
      }
    }
    ...
  }
  • 事件结束: 通过事件周期可知不管动画处于哪个环节,每一帧渲染都会触发update事件。当engineTime >= insDuration,表明本轮动画执行结束,接下来就该执行结束相关的事件,先重新统计动画迭代次数,然后使用remaining判断是否还剩余迭代次数。
  • remaining为false,表明迭代次数执行完毕,loopComplete、complete事件被触发。
  • ramaining为true,表明迭代次数还未执行完,那么仅触发loopComplete事件。
  function setInstanceProgress(engineTime) {
    ...
    if (instance.began) setCallback('update');
    // 如果engineTime时间大于执行周期,表示本次循环执行结束
    if (engineTime >= insDuration) {
      lastTime = 0;
      // 假如loop为数字,每执行一次需要更新剩余迭代次数
      countIteration();
      if (!instance.remaining) {
        instance.paused = true;
        if (!instance.completed) {
          instance.completed = true;
        // 如果循环执行完毕,触发loopComplete和complete两个hook
          setCallback('loopComplete');
          setCallback('complete');
          if (!instance.passThrough && 'Promise' in window) {
            // 当动画执行结束时,执行promise的resolve,外部可通过instance.finished.then添加后置函数
            resolve();
            promise = makePromise(instance);
          }
        }
      } else {
        // 如果循环次数还未结束,则仅触发loopComplete
        startTime = now;
        setCallback('loopComplete');
        instance.loopBegan = false;
        // 如果direction为交替执行,则需要更新reversed状态记录当前执行的方向
        if (instance.direction === 'alternate') {
          toggleInstanceDirection();
        }
      }
    }
    ...
  }

到此,anime.js涉及的所有事件全部介绍完毕。

delay

在初始化动画时,可设置延迟执行参数delay,除了为数字类型(如1000),anime.js还支持stagger类型。stagger字面意思交错、错开,结合下面的代码看,targets为组成波浪矩阵的所有元素,这些元素按什么顺序执行?

  animation = anime({
    targets: '.stagger-visualizer .dot',
    ...
    delay: anime.stagger(60, {grid: grid, from: index}),
    easing: 'easeInOutQuad',
    complete: play
  }, 30)

delay设置为anime.stagger(60, {grid: grid, from: index}), 第一个参数指定元素与元素之间延迟间隔为60ms,第二个参数设定元素的执行顺序按网格[32, 16]中的index位置开始执行,动画效果将会是从矩阵中间向四周蔓延。

从炫酷的波浪动画学习anime.js设计原理

从炫酷的波浪动画学习anime.js设计原理

anime.stagger也可以为线性执行,如anime.stagger(60),所有元素将会按索引0开始线性执行。Value还可以为数组类型,例如rotate: anime.stagger([-360, 360]),第3个元素的旋转角度为-360 + (360 - (-360)) / 5 * 2

从炫酷的波浪动画学习anime.js设计原理

可以说stagger是anime.js动画的灵魂,也是支持动画批量执行的核心能力。但stagger的实现并不复杂。

stagger API的结构如下,包含val、params两个参数,以delay: anime.stagger(60, {grid: grid, from: index})为例,val=60表示每个元素间隔60ms执行;第二个参数指定元素排列形式,例如以grid[32,16]排列,第一个元素从index位置开始。

function stagger(val, params = {}) {
  ...计算初始值

  // 返回函数,函数体内遍历所有的元素并计算新值
  return (el, i, t) => {
    ...
    return newVal + unit;
  }
}

stagger函数整体可分为初始化、返回计算函数两部分。

首先是初始化, easing计算缓动效果,如easeInOutQuad淡入淡出。from选项指定什么位置的元素先执行,from可为first、center、last类型。stagger第一个参数可为数组类型,例如rotate: anime.stagger([-360, 360]),角度变化为-360-360=-720,第i个元素的角度变化-360 + ((360 - (-360)) / t) * i, t表示元素个数。

function stagger(val, params = {}) {
  const direction = params.direction || 'normal';
  const easing = params.easing ? parseEasings(params.easing) : null;
  const grid = params.grid;
  const axis = params.axis;
  // 从第几个元素开始
  let fromIndex = params.from || 0;
  // from可以为字符串,包含开始、中间、结束为止
  const fromFirst = fromIndex === 'first';
  const fromCenter = fromIndex === 'center';
  const fromLast = fromIndex === 'last';
  // val值是否为数组类型
  const isRange = is.arr(val);
  const val1 = isRange ? parseFloat(val[0]) : parseFloat(val);
  const val2 = isRange ? parseFloat(val[1]) : 0;
  // 从val获取单位
  const unit = getUnit(isRange ? val[1] : val) || 0;
  // 第一个元素动画从start开始
  const start = params.start || 0 + (isRange ? val1 : 0);
  let values = [];
  let maxValue = 0;
  ...
}

有些动画属性带有单位,如translateX: anime.stagger('.075rem', {grid: grid, from: index, axis: 'x'}), 使用getUnit可获取属性单位,支持的单位如下代码所示。start指定动画属性从start位置开始执行。

    function getUnit(val) {
      const split = /[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(val);
      if (split) return split[1];
    }

其次是返回动画函数,fromIndex计算起始位置,如fromCenter为true则起始位置从中间(t - 1)/2开始。stagger使用闭包,将计算结果缓存到values中,下次执行时如果values有值则直接返回结果即可。

假设grid不为空,需要计算元素在网格grid中X、Y两个方向的位移量,fromX、fromY计算元素在X、Y两个方向的起始索引。

function stagger(val, params = {}) {
    ...
    // t表示元素数量total
  return (el, i, t) => {
    // 计算fromIndex从什么位置开始,从头、从中间、从尾部
    if (fromFirst) fromIndex = 0;
    if (fromCenter) fromIndex = (t - 1) / 2;
    if (fromLast) fromIndex = t - 1;
    if (!values.length) {
      // 遍历所有元素
      for (let index = 0; index < t; index++) {
        if (!grid) { // 如果grid为空则按队列顺序添加
          values.push(Math.abs(fromIndex - index));
        } else {
          // 如果非fromCenter,fromX从fromIndex*grid[0]位置开始;否则,从中间位置开始
          const fromX = !fromCenter ? fromIndex%grid[0] : (grid[0]-1)/2;
          // 如果非fromCenter,fromY从fromIndex/grid[0]行开始;否则,从中间行位置开始
          const fromY = !fromCenter ? Math.floor(fromIndex/grid[0]) : (grid[1]-1)/2;
          const toX = index%grid[0]; // 目标X方向位置
          const toY = Math.floor(index/grid[0]); // 目标Y方向位置
          const distanceX = fromX - toX; // X方向移动长度
          const distanceY = fromY - toY; // Y方向移动长度
          let value = Math.sqrt(distanceX * distanceX + distanceY * distanceY); // 移动距离
          if (axis === 'x') value = -distanceX;
          if (axis === 'y') value = -distanceY;
          values.push(value); // 将结果添加到values中
        }
        maxValue = Math.max(...values); // 移动的最长距离
      }
      if (easing) values = values.map(val => easing(val / maxValue) * maxValue); // 添加缓动效果
      if (direction === 'reverse') values = values.map(val => axis ? (val < 0) ? val * -1 : -val : Math.abs(maxValue - val));
    }
    const spacing = isRange ? (val2 - val1) / maxValue : val1; // 单位步长
    return start + (spacing * (Math.round(values[i] * 100) / 100)) + unit; // 当前元素i的移动长度
  }
}

所有元素的起始位置都为fromX、fromY,然后通过toX、toY设置位移量,如下图网格grid[10,9],中心位置fromX=4、fromY=4,第1、5、6行元素的目标位置[toX、toY]分别为[1, 5]、[5,8]、[6, 3]。而移动距离为from、to之间的笛卡尔坐标距离。

从炫酷的波浪动画学习anime.js设计原理

spacing计算单位步长,假如设置初始val为[-360,360],那么spacing=-720/maxValue,而mavValue表示中心点到到元素的最大距离,这里为到元素[9,8]的距离。最终元素i的属性移动量为start + (spacing * (Math.round(values[i] * 100) / 100)) + unit

stagger支持对元素集合批量设置变化量,支持网格grid或者队列排列模式,位移方向也支持按axisX、axisY单独设置, 也就是说一个元素默认可以沿着两个方向同时位移,也可以单独沿着X或Y方向位移。

keyFrames

参数设置、事件机制、stagger为动画的基础,而keyFrames关键帧才是动画执行最核心的模块。

animation = anime({
    ...
    keyframes: [
      {
        zIndex: function(el, i, total) {
          return Math.round(anime.stagger([numberOfElements, 0], {grid: grid, from: index})(el, i, total));
        },
        translateX: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'x'}),
        translateY: anime.stagger('-.001rem', {grid: grid, from: index, axis: 'y'}),
        duration: 200
      }, {
        translateX: anime.stagger('.075rem', {grid: grid, from: index, axis: 'x'}),
        translateY: anime.stagger('.075rem', {grid: grid, from: index, axis: 'y'}),
        scale: anime.stagger([2, .2], {grid: grid, from: index}),
        backgroundColor: staggeredGridColors,
        duration: 450
      }, {
        translateX: 0,
        translateY: 0,
        scale: 1,
        duration: 500,
      }
    ],
  }, 30)

上述代码的keyFrames共包含三项, 第一项执行200ms,第二项执行450毫秒,第三项执行500ms。可以看下每一项的动画效果:

  • 帧二:假如起始index从0开始,则由近到远元素逐渐变小,并且颜色由浅变深。

从炫酷的波浪动画学习anime.js设计原理

  • 帧三:从第0个元素开始,由近到远依次恢复元素的大小和位置。

从炫酷的波浪动画学习anime.js设计原理

元素一共有32*16=512个,以上的keyFrames为输入时格式,在最终执行动画之前会经过一系列转换: 原始输入->属性扁平化->获取元素->动画绑定->动画执行,以上的逻辑包含在createNewInstance函数中。

function createNewInstance(params) {
  const instanceSettings = replaceObjectProps(defaultInstanceSettings, params);
  const tweenSettings = replaceObjectProps(defaultTweenSettings, params);
  const properties = getProperties(tweenSettings, params);
  const animatables = getAnimatables(params.targets);
  const animations = getAnimations(animatables, properties);
  const timings = getInstanceTimings(animations, tweenSettings);
  const id = instanceID;
  instanceID++;
  return mergeObjects(instanceSettings, {
    id: id,
    children: [],
    animatables: animatables,
    animations: animations,
    duration: timings.duration,
    delay: timings.delay,
    endDelay: timings.endDelay
  });
}
  • 原始输入: 即keyFrames参数
  • 属性扁平化: replaceObjectProps(defaultTweenSettings, params)初始化缓动动画参数,包含delay、duration、easing、endDelay属性。

从炫酷的波浪动画学习anime.js设计原理

getProperties(tweenSettings, params)方法内部会调用flattenKeyframes将输入的keyFrames参数扁平化处理,处理后的格式如下,将原keyFrames中设置的zIndex、translateX等5个属性提取为数组形式,每一项包含{name, tweens},tweens为关键帧动画参数。

从炫酷的波浪动画学习anime.js设计原理

  • 获取元素: 获取需要执行动画的所有元素, 调用getAnimatables(params.targets)方法将dom元素格式化为{ id, target, i, total, transforms},id为元素索引,target为DOM实体,i为索引,total为元素总数,transfroms保存元素的原始transforms属性并提取为数组格式。执行结果如下图,一共包含512个元素。

从炫酷的波浪动画学习anime.js设计原理

  • 动画绑定:属性扁平化、获取元素两个阶段之后,需要将元素和扁平化的动画结合起来,这个过程在getAnimations(animatables, properties)方法中执行,参数animatables为元素列表,properties为扁平化后的属性。getAnimations方法比较简单,为512个元素animatables分别创建属性动画,512个元素、5个属性一共生成2560个动画animation。
function getAnimations(animatables, properties) {
  return filterArray(flattenArray(animatables.map(animatable => {
    return properties.map(prop => {
      return createAnimation(animatable, prop);
    });
  })), a => !is.und(a));
}

每一项animation包含的属性有animatable、delay、duration、endDelay、property、tweens,animatable为DOM元素,tweens为缓动动画列表。

从炫酷的波浪动画学习anime.js设计原理

  • 动画执行:动画执行也是anime.js最核心、最复杂的一部分,在下一节中详细介绍。

动画执行

动画实体instance包含play方法,用于启动动画执行,每一个实体会push到activeInstances队列中,然后在engine方法中执行。

  instance.play = function() {
    if (!instance.paused) return;
    if (instance.completed) instance.reset();
    instance.paused = false;
    activeInstances.push(instance);
    resetTime();
    engine();
  }

engine方法调用requestAnimationFrame(step),在每一帧执行step函数,而step函数会遍历上述提到的activeInstances列表,如果动画实体已暂停paused为true,则从列表中移除。否则,调用activeInstance.tick执行动画。

    const engine = (() => {
      let raf;

      function play() {
        if (!raf && (!isDocumentHidden() || !anime.suspendWhenDocumentHidden) && activeInstances.length > 0) {
          raf = requestAnimationFrame(step);
        }
      }
      function step(t) {
        // memo on algorithm issue:
        // dangerous iteration over mutable `activeInstances`
        // (that collection may be updated from within callbacks of `tick`-ed animation instances)
        let activeInstancesLength = activeInstances.length;
        let i = 0;
        while (i < activeInstancesLength) {
          const activeInstance = activeInstances[i];
          if (!activeInstance.paused) {
            activeInstance.tick(t);
            i++;
          } else {
            activeInstances.splice(i, 1);
            activeInstancesLength--;
          }
        }
        raf = i > 0 ? requestAnimationFrame(step) : undefined;
      }
      ...

tick会在每一帧执行,其内部会调用前面已经介绍过的setInstanceProgress更新动画进度的方法。

  instance.tick = function(t) {
    now = t;
    if (!startTime) startTime = now;
    setInstanceProgress((now + (lastTime - startTime)) * anime.speed);
  }

setInstanceProgress在触发begin、change、complete等各个事件的同时调用setAnimationsProgress执行具体的动画效果。

在介绍setAnimationsProgress方法之前,先回顾下每一个DOM元素生成的动画对象,如下图所示,包含animatable、delay、duration、endDelay、property属性,另外还包含缓动动画列表tweens。

从炫酷的波浪动画学习anime.js设计原理 tweens列表中每一项都包含start、delay、duration、end、endDelay,时间顺序如下图, 而属性值的变化区间[from, to]。

从炫酷的波浪动画学习anime.js设计原理

了解清楚动画属性,接下来查看setAnimationProgress方法内部逻辑, 方法首先遍历animations动画列表,一个元素的缓动动画tweens虽然为列表,但命中当前时刻的关键帧肯定只有一项,通过t => insTime < t.end找到目前时间的第一项缓动动画。对于一个动画,已执行的时间段赋值到eased(范围[0, 1]),一个关键帧属性变化范围从from.numbers变至to.numbers,那么当前最新的属性值即为value = fromNumber + (eased * (toNumber - fromNumber))。最后的strings列表遍历,目的是为变化值加上对应的单位,如0.5rem

  function setAnimationsProgress(insTime) {
    let i = 0;
    const animations = instance.animations;
    const animationsLength = animations.length;
    // 遍历所有的动画实体
    while (i < animationsLength) {
      const anim = animations[i];
      // DOM元素
      const animatable = anim.animatable;
      // 每个元素对应的缓动动画列表
      const tweens = anim.tweens;
      const tweenLength = tweens.length - 1;
      let tween = tweens[tweenLength];
      // 由于关键帧keyframes列表中同时只会执行一个,因此使用insTime<t.end来查询需要执行的关键帧
      if (tweenLength) tween = filterArray(tweens, t => (insTime < t.end))[0] || tween;
      // 已执行时间段,范围[0, 1]
      const elapsed = minMax(insTime - tween.start - tween.delay, 0, tween.duration) / tween.duration;
      // 缓动动画后的已执行时间段
      const eased = isNaN(elapsed) ? 1 : tween.easing(elapsed);
      const strings = tween.to.strings;
      const numbers = [];
      const toNumbersLength = tween.to.numbers.length;
      let progress;
      for (let n = 0; n < toNumbersLength; n++) {
        let value;
        const toNumber = tween.to.numbers[n];
        const fromNumber = tween.from.numbers[n] || 0;
        value = fromNumber + (eased * (toNumber - fromNumber));
        numbers.push(value);
      }
      // Manual Array.reduce for better performances
      const stringsLength = strings.length;
      progress = strings[0];
      for (let s = 0; s < stringsLength; s++) {
          const a = strings[s];
          const b = strings[s + 1];
          const n = numbers[s];
          if (!isNaN(n)) {
            if (!b) {
              progress += n + ' ';
            } else {
              progress += n + b;
            }
          }
      }
      setProgressValue[anim.type](animatable.target, anim.property, progress, animatable.transforms);
      anim.currentValue = progress;
      i++;
    }
  }

要将计算后的最新属性值progress绑定到DOM要素的style上,还需调用setProgressValue方法来执行,例如setProgressValue['transform'](dom, 'translateX', '0.5rem', transforms),transforms是什么?假如dom当前的style.transform = 'scale(2)',那么这里的transforms格式为: { list: ['scale(2)'] },同时会将新专递的translateX附加到list,那么新的transforms为{ list: ['scale(2)', 'translateX('0.5rem')'] }, 最终将完成的transorms转换字符串并赋值给dom.style.transform。

    const setProgressValue = {
      transform: (t, p, v, transforms, manual) => {
        transforms.list.set(p, v);
        if (p === transforms.last || manual) {
          let str = '';
          transforms.list.forEach((value, prop) => { str += `${prop}(${value}) `; });
          t.style.transform = str;
        }
      }
    }

执行动画的调用链路总结为play->step->tick->setAnimationsProgress->setProgressValue

总结

由于提供了stagger特性,anime.js可支持矩阵元素的动画效果。在执行周期,可通过delay、duration、endDelay等属性设置元素与元素之间的动画间隔。类似于CSS的关键帧,anime.js也提供了keyFrames关键帧列表,支持元素按关键帧列表序列依次执行动画。

除了以上的特性,anime.js还提供有timeline、controls、svg等特性。

  • timeline: 可设置多个动画按时间轴顺序依次执行
// Create a timeline with default parameters
var tl = anime.timeline({
  easing: 'easeOutExpo',
  duration: 750
});

// Add children
tl
.add({
  targets: '.basic-timeline-demo .el.square',
  translateX: 250,
})
.add({
  targets: '.basic-timeline-demo .el.circle',
  translateX: 250,
})
.add({
  targets: '.basic-timeline-demo .el.triangle',
  translateX: 250,
});

  • controls: 支持动画控制,如暂停、重置、反向等操作。

从炫酷的波浪动画学习anime.js设计原理

  • SVG: 获取svg元素的path路径,沿着path执行动画
var path = anime.path('.motion-path-demo path');

anime({
  targets: '.motion-path-demo .el',
  translateX: path('x'),
  translateY: path('y'),
  rotate: path('angle'),
  easing: 'linear',
  duration: 2000,
  loop: true
});

写在最后,如果大家有疑问可直接留言,一起探讨!感兴趣的可以点一波关注。

转载自:https://juejin.cn/post/7239376445657661496
评论
请登录