前端进阶:利用JS实现多元素时间轴动画

作者:交互手记时间:2026-10-25 13:51:21浏览:249

本文深入解析如何利用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}            
}

CSS动画

上述方案看似可行,但若需求复杂化,例如:div1在第1秒变色,div2在2至5秒内变形,div3在3至7秒内同时变色与变形,div4在4至10秒内执行旋转等,所有元素共同构成最终动画效果。

此时,开发者需计算整个动画的总时长,并推算每个div运动时间占总时长的百分比区间,进而设定复杂的keyframe动画。

至此,keyframes的复杂性逐渐显现。我们日常频繁使用keyframes,但其本质究竟是什么?

基于人类视觉停留原理,连续展示的图片序列形成动画视觉效果,翻书动画或显示器逐帧刷新均利用此原理。在动画制作中,除像素级和2D动画仍逐帧绘制外,多数动画类型已将帧转换为时间进行计算。

keyframes作为高度定制化的产物,仅在命名上保留了“帧”的概念,它对单个元素的动画编辑简便直观。然而,如示例二所示,它并未针对多元素组合动画进行专门设计。代码层面,div1与div2缺乏逻辑关联(尽管运行时它们在统一计时器中执行),二者没有统一的时间轴,也无父级容器包裹,实现此类效果需开发者手动维护。

什么是时间轴动画?

对于具备视频剪辑经验的人员而言,基于时间轴的多轨视频编辑器是高效便捷的核心工具,它能整合多个动画或特效片段,即时间轴动画。基于此理念,我们可以开发一个简易的JS版本,以应对多元素、长周期的动画需求。

CSS动画

如图所示,整个视频由多段小动画交错拼接而成,它们共用一个时间轴,从而构成完整的视频内容。

类比至前端开发,我们可采用类似方法,以时间轴为基准,整合多个独立的动画片段。

具体实现思路如下:

首先创建一个对象作为参数,称为“动画对象”,用于描述整个动画过程。接着创建一个执行动画的函数,称为“动画函数”,该函数接收“动画对象”及一个回调函数(动画结束时触发)作为参数。随后进入代码实现环节:

动画对象

首先观察以下代码结构:

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”。

其视觉效果如下图所示:

CSS动画

动画函数

实现该动画函数的基本思路如下: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时间轴动画内容,请搜索本站以往文章或继续浏览后续相关文章,希望大家今后多多支持本站!

作者最新文章