本文深入解析如何利用JavaScript实现时间轴动画,解决CSS3 @keyframes在多元素协同动画中的局限性。通过构建声明式的动画对象和基于requestAnimationFrame的执行引擎,实现多个DOM元素在统一时间轴上的精确同步控制,代码不足百行,支持自定义运动曲线与可视化配置。
CSS动画
在前端开发实践中,简单的视觉动效通常借助CSS3的@keyframes规则来实现,例如:
.div1 {
width: 100px;
height: 100px;
background: red;
animation: changeColor 2s;
}
@keyframes changeColor {
0% {background: red;}
50% {background: yellow;}
100% {background: blue;}
}
若设定动画时长为2秒,则该动画表现为:某元素背景色在2秒内由红色渐变为黄色,最终转为蓝色。
假设存在如下需求场景:
一个总时长为4秒的动画中,div1需在0至2秒内完成背景色从红色到黄色再到蓝色的变化;同时,div2需在2至4秒内将宽度从100px扩展至200px。
.div1 {
width: 100px;
height: 100px;
background: red;
animation: changeColor 2s forwards;
}
@keyframes changeColor {
0% {background: red;}
50% {background: yellow;}
100% {background: blue;}
}
.div2 {
width: 100px;
height: 100px;
background: pink;
animation: changeWidth 4s forwards;
}
@keyframes changeWidth {
0% {width: 100px}
50% {width: 100px}
100% {width: 200px}
}

上述方案看似可行,但若需求复杂化,例如:div1在第1秒变色,div2在2至5秒内变形,div3在3至7秒内同时变色与变形,div4在4至10秒内执行旋转等,所有元素共同构成最终动画效果。
此时,开发者需计算整个动画的总时长,并推算每个div运动时间占总时长的百分比区间,进而设定复杂的keyframe动画。
至此,keyframes的复杂性逐渐显现。我们日常频繁使用keyframes,但其本质究竟是什么?
基于人类视觉停留原理,连续展示的图片序列形成动画视觉效果,翻书动画或显示器逐帧刷新均利用此原理。在动画制作中,除像素级和2D动画仍逐帧绘制外,多数动画类型已将帧转换为时间进行计算。
keyframes作为高度定制化的产物,仅在命名上保留了“帧”的概念,它对单个元素的动画编辑简便直观。然而,如示例二所示,它并未针对多元素组合动画进行专门设计。代码层面,div1与div2缺乏逻辑关联(尽管运行时它们在统一计时器中执行),二者没有统一的时间轴,也无父级容器包裹,实现此类效果需开发者手动维护。
什么是时间轴动画?
对于具备视频剪辑经验的人员而言,基于时间轴的多轨视频编辑器是高效便捷的核心工具,它能整合多个动画或特效片段,即时间轴动画。基于此理念,我们可以开发一个简易的JS版本,以应对多元素、长周期的动画需求。

如图所示,整个视频由多段小动画交错拼接而成,它们共用一个时间轴,从而构成完整的视频内容。
类比至前端开发,我们可采用类似方法,以时间轴为基准,整合多个独立的动画片段。
具体实现思路如下:
首先创建一个对象作为参数,称为“动画对象”,用于描述整个动画过程。接着创建一个执行动画的函数,称为“动画函数”,该函数接收“动画对象”及一个回调函数(动画结束时触发)作为参数。随后进入代码实现环节:
动画对象
首先观察以下代码结构:
var anim = [
{ // 动画对象,可以多个
dom: document.getElementById('a1'), // dom对象
anim: [ // 动画数组,里面可为多个对象
{
style: 'height',
// 要改变的样式名称如width,height
from: '10rem', // 样式的起始值要和to保持一致
to: '20rem', // 样式的目标值要和from保持一致
start: 0, // 开始时间 毫秒
end: 100, // 结束时间 毫秒
},
{
style: 'width',
from: '10rem',
to: '20rem',
start: 100,
end: 200,
},
{
style: 'transform',
from: 'rotate(20deg)',
to: 'rotate(40deg)',
start: 0,
end: 2000,
}
]
},
{
dom: document.getElementById('a2'),
anim: [
{
style: 'background',
from: 'rgba(0.0, 0.0, 0.0, 1)',
to: 'rgba(255, 0, 100, 0.5)',
start: 0,
end: 3000,
}
]
},
];
timeLineAnim(anim, function () {
console.log('animation finished');
});
anim对象清晰描述了每个DOM元素包含的动画数量及具体执行方式。随后,anim对象作为参数传入timeLineAnim函数,在实际执行过程中,它将按以下步骤操作:
div1在0.1秒内高度从10rem变为20rem,随后宽度变化,同时在前2秒内持续旋转。
div2在0至3秒内,颜色由rgba(0, 0, 0, 1)渐变为rgba(255, 0, 100, 0.5)。
3秒后,动画结束,控制台输出“animation finished”。
其视觉效果如下图所示:

动画函数
实现该动画函数的基本思路如下:1. 提取参数anim中的from和to数值,便于后续计算。2. 启动requestAnimationFrame,根据时间间隔和动画参数anim计算当前帧的CSS样式,并赋值给对应的DOM元素。3. 在最后一个动画结束后,取消requestAnimationFrame,并执行回调函数。
进入编码阶段:基于上述思路,在具体实现中,通过timeCount判断动画是否结束,并使用cssText进行样式赋值。
function timeLineAnim(arr, cb) {
/*
initData: 对数据进行初始化,将from、to中的信息转为数字,计算出动画的个数
setCss: 逐帧执行,根据传入的单个动画对象和每帧的时间间隔,计算出当前帧的样式,并修改元素的style值
*/
function initData(obj) {
var reg = /[\d\.]+/g;
var numArrFrom = obj.from.match(reg);
var numArrTo = obj.to.match(reg);
if (!numArrFrom || !numArrTo || numArrFrom.length == 0 || numArrTo.length == 0 || numArrFrom.length != numArrTo.length) {
console.warn('数据输入错误');
}
for (var i = 0; i < numArrFrom.length; i++) {
numArrFrom[i] = parseFloat(numArrFrom[i]);
numArrTo[i] = parseFloat(numArrTo[i]);
}
var strArr = obj.from.split(reg);
if (numArrFrom.length <= 0) { return; };
animCount++;
return {
strArr: strArr,
numArrFrom: numArrFrom,
numArrTo: numArrTo,
totalLength: strArr.length + numArrFrom.length,
isEnd: false,
}
}
function setCss(obj, timeCount) {
var styleStr = '';
var tempCount = animCount;
// 将对象的多个动画,根据时间计算出样式,并合并
for (var i = 0; i < obj.anim.length; i++) {
var target = obj.anim[i];
if (timeCount > target.start && !target.initData.isEnd) {
// 根据起止时间 计算出当前动画进度
var percent = (timeCount - target.start) / (target.end - target.start);
if (percent >= 1) {
percent = 1;
target.initData.isEnd = true;
tempCount--;
}
styleStr += getValue(percent);
}
}
obj.dom.style.cssText += ';' + styleStr;
animCount = tempCount;
// 这里进行单个动画每帧样式的生成,这里是匀速运动
function getValue(percent) {
var numFrom = target.initData.numArrFrom;
var numTo = target.initData.numArrTo;
var arr = [];
for (var i = 0; i < numFrom.length; i++) {
arr.push(numFrom[i] + (numTo[i] - numFrom[i]) * percent);
}
var numIndex = 0;
var strIndex = 0;
var turnForNum = false;
var str = target.style + ':';
for (var i = 0; i < target.initData.totalLength; i++) {
if (turnForNum) {
str += arr[numIndex];
numIndex++;
turnForNum = false;
}
else {
str += target.initData.strArr[strIndex];
strIndex++;
turnForNum = true;
}
}
return str + ';';
}
}
// 主体逻辑: 初始化数据、帧动画、动画结束执行回调
var animCount = 0;
for (var i = 0; i < arr.length; i++) {
for (var j = 0; j < arr[i].anim.length; j++) {
arr[i].anim[j].initData = initData(arr[i].anim[j]);
}
}
this.timer && cancelAnimationFrame(this.timer);
var _this = this;
(function animate(timeCount) {
if (animCount > 0) {
for (var i = 0; i < arr.length; i++) {
setCss(arr[i], timeCount);
}
_this.timer = requestAnimationFrame(animate);
}
else {
cancelAnimationFrame(_this.timer);
cb && cb();
}
})();
}
开发者可直接复制运行。至此,一个基于时间轴的简易动画函数即告完成,不足100行的代码即可实现声明式的时间轴动画编辑。此外,在getValue阶段,可根据需求搭配不同的运动曲线函数,以丰富动画功能。
由于参数采用类JSON格式,若需进一步优化,完全可以开发一个UI界面,用于可视化生成动画参数,从而方便非程序员群体使用。
思考
文章开头提及,市面上部分动画(如2D像素类、2D动画)的制作仍基于帧进行。
无论是keyframes还是本文提供的timeLineAnim函数,均以时间为设计基础。
那么:若改为基于帧进行计算和制作,结果如何?各有何优缺点?欢迎在评论区讨论。
总结
至此,关于如何利用JS实现时间轴动画效果的介绍已结束。更多相关JS时间轴动画内容,请搜索本站以往文章或继续浏览后续相关文章,希望大家今后多多支持本站!