从炫酷的波浪动画学习anime.js设计原理
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官网,首页即可看到上图的波浪效果,如何从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);
参数设置
有了元素素材,接下来考虑让元素动起来。在实例化动画时,通过配置基础参数,确定动画的执行动作和顺序。几个关键参数:
- 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三个和时间相关的参数。
动画开始的时刻等同于上一个动画结束的时刻,如果没有上一个动画则开始时刻为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。
在整个动画执行期间,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.stagger也可以为线性执行,如anime.stagger(60)
,所有元素将会按索引0开始线性执行。Value还可以为数组类型,例如rotate: anime.stagger([-360, 360])
,第3个元素的旋转角度为-360 + (360 - (-360)) / 5 * 2
。
可以说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之间的笛卡尔坐标距离。
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开始,则由近到远元素逐渐变小,并且颜色由浅变深。
- 帧三:从第0个元素开始,由近到远依次恢复元素的大小和位置。
元素一共有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属性。
getProperties(tweenSettings, params)
方法内部会调用flattenKeyframes
将输入的keyFrames参数扁平化处理,处理后的格式如下,将原keyFrames中设置的zIndex、translateX等5个属性提取为数组形式,每一项包含{name, tweens},tweens为关键帧动画参数。
- 获取元素: 获取需要执行动画的所有元素, 调用
getAnimatables(params.targets)
方法将dom元素格式化为{ id, target, i, total, transforms}
,id为元素索引,target为DOM实体,i为索引,total为元素总数,transfroms保存元素的原始transforms属性并提取为数组格式。执行结果如下图,一共包含512个元素。
- 动画绑定:属性扁平化、获取元素两个阶段之后,需要将元素和扁平化的动画结合起来,这个过程在
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最核心、最复杂的一部分,在下一节中详细介绍。
动画执行
动画实体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。
tweens列表中每一项都包含start、delay、duration、end、endDelay,时间顺序如下图, 而属性值的变化区间[from, to]。
了解清楚动画属性,接下来查看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: 支持动画控制,如暂停、重置、反向等操作。
- 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