当前位置:首页 > 文章列表 > 文章 > 前端 > requestAnimationFrame掌控复杂动画流程

requestAnimationFrame掌控复杂动画流程

2025-08-31 14:36:43 0浏览 收藏

学习知识要善于思考,思考,再思考!今天golang学习网小编就给大家带来《requestAnimationFrame管理复杂动画序列》,以下内容主要包含等知识点,如果你正在学习或准备学习文章,就都不要错过本文啦~让我们一起来看看吧,能帮助到你就更好了!

使用 requestAnimationFrame 实现复杂动画序列管理

本文深入探讨了如何利用 requestAnimationFrame API 有效管理和编排复杂的动画序列。针对直接调用 requestAnimationFrame 导致动画同时执行的问题,文章提出了一种通用的插值动画序列管理方案。通过详细解析核心代码结构、参数、内部逻辑及示例,展示了如何实现平滑的过渡、自定义缓动函数以及复杂的动画组合,为开发者提供了构建高性能、可控动画的专业指南。

1. requestAnimationFrame 与动画序列挑战

在 Web 开发中,requestAnimationFrame 是实现流畅动画的首选 API。它会通知浏览器在下一次重绘之前执行指定的回调函数,从而确保动画与浏览器帧率同步,避免丢帧,并减少 CPU/GPU 负载。

然而,当需要按顺序执行多个动画时,直接简单地链式调用 requestAnimationFrame 往往会导致意想不到的结果——动画同时运行而非顺序执行。考虑以下一个简单的淡出(fadeOut)和淡入(fadeIn)效果的实现:

let alpha = 1; // 全局透明度变量
const delta = 0.02; // 透明度变化步长
let ctx; // Canvas 2D 上下文

function fadeOut(content) {
    console.log('fade out');
    alpha -= delta;
    ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除画布

    ctx.globalAlpha = alpha; // 设置全局透明度
    content(); // 绘制内容
    if (alpha > 0) {
        requestAnimationFrame(fadeOut.bind(this, content));
    } else {
        alpha = 1; // 重置透明度,为下一个动画准备
        ctx.globalAlpha = alpha;
    }
}

function fadeIn(content) {
    console.log('fade in');
    alpha += delta;
    ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除画布

    ctx.globalAlpha = alpha; // 设置全局透明度
    content(); // 绘制内容

    if (alpha < 1) {
        requestAnimationFrame(fadeIn.bind(this, content));
    } else {
        alpha = 1; // 重置透明度
        ctx.globalAlpha = alpha;
    }
}

// 假设 drawMap 是一个绘制内容的函数
// ctx = document.getElementById('canvas').getContext('2d');
// requestAnimationFrame(fadeOut.bind(this, drawMap.bind(this, MAP1)));
// requestAnimationFrame(fadeIn.bind(this, drawMap.bind(this, MAP1))); // 这样调用会导致同时运行

上述代码中的 fadeOut 和 fadeIn 函数各自通过 requestAnimationFrame 递归调用,可以独立实现淡出或淡入效果。但如果像注释中那样,紧接着调用 requestAnimationFrame(fadeOut(...)) 和 requestAnimationFrame(fadeIn(...)),它们将几乎同时被安排到下一个动画帧执行。这是因为 requestAnimationFrame 仅仅是请求在 下一个可用帧 执行回调,而不是等待当前动画完成。因此,我们需要一个更精细的机制来管理动画的顺序和状态。

2. 通用动画序列管理方案

为了解决上述问题,我们可以构建一个通用的动画序列管理器,它能够接收一系列动画步骤,并按序执行它们,同时支持自定义持续时间、缓动函数和插值范围。

以下是一个名为 animateInterpolationSequence 的高级函数,它能够管理任意复杂的动画序列:

function animateInterpolationSequence (callback, ...sequence) {
    if (sequence.length === 0) {
        return null;
    }

    // 为了更高的精度,将时间戳乘以100,避免浮点误差
    let animationTimeStart = Math.floor(performance.now() * 100);
    let timeStart = animationTimeStart; // 当前序列项的起始时间
    let duration = 0; // 当前序列项的持续时间
    let easing; // 当前序列项的缓动函数
    let valueStart; // 当前插值范围的起始值
    let valueEnd = sequence[0].start; // 当前插值范围的结束值,初始化为第一个序列项的起始值
    let nextId = 0; // 下一个要处理的序列项索引
    // 判断最后一个序列项的 end 属性是否为数字,决定是否循环
    let looped = (typeof sequence[sequence.length - 1].end !== 'number');
    let alive = true; // 动画是否仍在运行的标志
    let rafRequestId = null; // requestAnimationFrame 的 ID,用于取消动画

    // requestAnimationFrame 的回调函数
    function update (time) {
        // 如果是第一次调用,time 使用 animationTimeStart;否则使用传入的时间戳
        time = (rafRequestId === null)
            ? animationTimeStart
            : Math.floor(time * 100);

        // 循环处理已完成的序列项
        while (time - timeStart >= duration) {
            if (sequence.length > nextId) {
                // 处理下一个序列项
                let currentItem = sequence[nextId++];
                let action =
                    (sequence.length > nextId) // 如果后面还有序列项,则继续
                        ? 'continue':
                    (looped) // 如果设置了循环,则回到第一个序列项
                        ? 'looping'
                        : 'finishing'; // 否则,动画即将结束

                if (action === 'looping') {
                    nextId = 0; // 重置到第一个序列项
                }
                timeStart += duration; // 更新当前序列项的起始时间
                duration = Math.floor(currentItem.duration * 100); // 更新持续时间
                easing = (typeof currentItem.easing === 'function') ? currentItem.easing : null; // 获取缓动函数
                valueStart = valueEnd; // 当前插值起始值是上一个插值的结束值
                // 根据 action 确定下一个插值结束值
                valueEnd = (action === 'finishing') ? currentItem.end : sequence[nextId].start;
            } else {
                // 所有序列项都已处理完毕,动画结束
                safeCall(() => callback((time - animationTimeStart) / 100, valueEnd, true));
                return; // 终止动画循环
            }
        }

        // 插值计算
        let x = (time - timeStart) / duration; // 归一化的时间进度 (0 到 1)
        if (easing) {
            x = safeCall(() => easing(x), x); // 应用缓动函数
        }
        let value = valueStart + (valueEnd - valueStart) * x; // 线性插值

        // 继续动画
        safeCall(() => callback((time - animationTimeStart) / 100, value, false));
        if (alive) {
            rafRequestId = window.requestAnimationFrame(update); // 请求下一帧
        }
    }

    // 异常捕获辅助函数,避免动画因错误中断
    function safeCall (callback, defaultResult) {
        try {
            return callback();
        } catch (e) {
            window.setTimeout(() => { throw e; }); // 异步抛出错误,不阻塞主线程
            return defaultResult;
        }
    }

    update(); // 立即启动动画
    // 返回一个停止动画的函数
    return function stopAnimation () {
        window.cancelAnimationFrame(rafRequestId);
        alive = false;
    };
}

2.1 animateInterpolationSequence 函数解析

这个函数是整个动画管理的核心。它接收两个主要参数:

  • callback: 这是一个在每一帧动画更新时被调用的函数。它接收三个参数:
    • elapsedTime: 动画从开始到当前的总耗时(秒)。
    • interpolatedValue: 当前帧计算出的插值。
    • isFinished: 一个布尔值,指示动画序列是否已全部完成。
  • ...sequence: 这是一个可变参数,表示动画序列的定义。每个序列项都是一个对象,通常包含:
    • start: 当前动画段的起始值(用于插值)。
    • duration: 当前动画段的持续时间(毫秒)。
    • easing (可选): 一个缓动函数,用于调整插值进度。

内部工作机制:

  1. 时间管理与精度:

    • performance.now() 提供高精度时间戳。为了避免浮点误差,所有时间值都被乘以 100 转换为整数进行内部计算,最后再除以 100 转换回秒或毫秒。
    • animationTimeStart: 整个动画序列开始的绝对时间。
    • timeStart: 当前正在执行的序列项的起始时间。
    • duration: 当前序列项的持续时间。
  2. 序列项迭代 (while 循环):

    • update 函数的核心是一个 while (time - timeStart >= duration) 循环。这个循环非常关键,它确保即使在浏览器卡顿导致帧率下降时,动画也能“追赶”上预定的进度。如果一帧跳过了多个序列项的持续时间,它会迅速迭代并处理完所有已完成的序列项,确保动画状态的正确性。
    • nextId: 跟踪当前正在处理的序列项的索引。
    • action: 判断当前序列项完成后是继续下一个、循环还是结束。
  3. 插值计算:

    • x = (time - timeStart) / duration: 计算当前序列项的归一化时间进度,范围从 0 到 1。
    • easing: 如果定义了缓动函数,x 会通过缓动函数进行变换,从而实现非线性的动画效果(如加速、减速)。
    • value = valueStart + (valueEnd - valueStart) * x: 根据归一化的进度 x 进行线性插值,得到当前帧的动画值。
  4. 回调与递归:

    • callback 函数在每一帧被调用,将计算出的插值 value 传递给外部逻辑,例如更新 Canvas 元素的绘制。
    • window.requestAnimationFrame(update) 递归调用自身,实现动画循环。
  5. 异常处理 (safeCall):

    • safeCall 函数包裹了 callback 的调用,它捕获回调函数中可能发生的错误,并使用 setTimeout 异步抛出,从而避免动画主循环被中断。
  6. 动画停止:

    • animateInterpolationSequence 返回一个 stopAnimation 函数,外部可以通过调用它来取消正在进行的动画。它通过 cancelAnimationFrame 停止 requestAnimationFrame 循环,并设置 alive 标志为 false,确保 update 函数不再请求下一帧。

3. 缓动函数(Easing Functions)

缓动函数允许动画在不同阶段以不同的速度进行,使动画看起来更自然、更有动感。它们通常接收一个 0 到 1 之间的进度值 x,并返回一个经过变换的 0 到 1 之间的值。

例如,一个五次方的缓出函数 easeOutQuint:

function easeOutQuint (x) {
    return 1 - Math.pow(1 - x, 5);
}

4. 示例:Canvas 星形动画

为了演示 animateInterpolationSequence 的用法,我们创建一个在 Canvas 上绘制星形的函数 renderStar,并将其作为回调函数传递给动画序列管理器。

// 获取 Canvas 元素和 2D 上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

function renderStar (alpha, rotation, corners, density) {
    ctx.save(); // 保存当前 Canvas 状态

    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制棋盘格背景(可选,用于视觉效果)
    ctx.fillStyle = 'rgba(0, 0, 0, .2)';
    let gridSize = 20;
    for (let y = 0; y * gridSize < canvas.height; y++) {
        for (let x = 0; x * gridSize < canvas.width; x++) {
            if ((y + x + 1) & 1) {
                ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
            }
        }
    }

    // 星形几何计算
    let centerX = canvas.width / 2;
    let centerY = canvas.height / 2;
    let radius = Math.min(centerX, centerY) * 0.9; // 星形半径
    function getCornerCoords (corner) {
        let angle = rotation + (Math.PI * 2 * corner / corners);
        return [
            centerX + Math.cos(angle) * radius,
            centerY + Math.sin(angle) * radius
        ];
    }

    // 构建星形路径
    ctx.beginPath();
    ctx.moveTo(...getCornerCoords(0));
    for (let i = density; i !== 0; i = (i + density) % corners) {
        ctx.lineTo(...getCornerCoords(i));
    }
    ctx.closePath();

    // 绘制星形
    ctx.shadowColor = 'rgba(0, 0, 0, .5)';
    ctx.shadowOffsetX = 6;
    ctx.shadowOffsetY = 4;
    ctx.shadowBlur = 5;
    ctx.fillStyle = `rgba(255, 220, 100, ${alpha})`; // 根据传入的 alpha 值设置填充颜色
    ctx.fill();
    ctx.restore(); // 恢复之前保存的 Canvas 状态
}

在 renderStar 函数中,alpha 参数将由 animateInterpolationSequence 计算并传递,实现星形的透明度变化。rotation 参数通过 Date.now() / 1000 实时计算,使星形持续旋转。

5. 动画序列组合与演示

现在,我们可以定义一个复杂的动画序列,并将其传递给 animateInterpolationSequence。

// 示例动画序列定义
animateInterpolationSequence(
    // 每一帧的回调函数:更新星形绘制
    (time, value, finished) => {
        // value 是插值后的 alpha 值
        // Date.now() / 1000 用于使星形持续旋转
        renderStar(value, Date.now() / 1000, 5, 2);
    },

    // 序列项定义:
    { start: 1, duration: 2000 }, // 0 到 2 秒:保持不透明 (alpha = 1)

    // 2 到 3 秒:线性淡出 + 淡入 (alpha: 1 -> 0 -> 1)
    { start: 1, duration: 500  },
    { start: 0, duration: 500  },
    { start: 1, duration: 500  }, // 3 到 4 秒:再次线性淡出 + 淡入
    { start: 0, duration: 500  },

    { start: 1, duration: 2000 }, // 4 到 6 秒:保持不透明

    // 6 到 7 秒:使用自定义缓动函数 easeOutQuint 进行淡出 + 淡入
    { start: 1, duration: 500,    easing: easeOutQuint },
    { start: 0, duration: 500,    easing: easeOutQuint },
    { start: 1, duration: 500,    easing: easeOutQuint }, // 7 到 8 秒:再次使用缓动函数
    { start: 0, duration: 500,    easing: easeOutQuint },

    { start: 1, duration: 2000 }, // 8 到 10 秒:保持不透明
    { start: 1, duration: 0    }, // 瞬间切换到下一个状态 (持续时间为 0)

    // 10 到 11 秒:闪烁效果 (使用立即切换和短暂等待)
    ...((delay, times) => {
        let items = [
            { start: .75, duration: delay }, // 等待一段时间 (alpha = 0.75)
            { start: .75, duration: 0     }, // 瞬间切换到 0.25
            { start: .25, duration: delay }, // 等待一段时间 (alpha = 0.25)
            { start: .25, duration: 0     }  // 瞬间切换到 0.75
        ];
        while (--times) { // 重复闪烁多次
            items.push(items[0], items[1], items[2], items[3]);
        }
        return items;
    })(50, 20) // 每次闪烁延迟 50ms,重复 20 次
);

对应的 HTML 结构:

<canvas id="canvas" width="400" height="180"></canvas>

这段代码定义了一个复杂的动画序列:

  • 初始 2 秒保持不透明。
  • 接着是两次线性淡出淡入的循环。
  • 再保持 2 秒不透明。
  • 然后是两次使用 easeOutQuint 缓动函数的淡出淡入循环。
  • 最后是 2 秒不透明,然后紧接着一个复杂的闪烁效果,通过设置 duration: 0 实现瞬间切换,并结合短暂的 delay 来控制闪烁频率。

6. 注意事项与总结

  • 性能优化: requestAnimationFrame 是实现高性能动画的关键。它确保浏览器在最佳时机进行重绘,避免不必要的计算。
  • 状态管理: 动画序列管理器的核心在于对动画状态(当前序列项、时间进度、插值范围)的精确管理。
  • 时间精度: 使用 performance.now() 获取高精度时间戳,并进行适当的单位转换(如乘以 100)以减少浮点误差,可以确保动画的平滑性。
  • 缓动函数: 灵活运用缓动函数能极大地提升动画的视觉效果和用户体验。
  • 可取消性: 提供 stopAnimation 函数是良好的实践,允许在需要时优雅地停止动画。
  • 错误处理: safeCall 模式可以防止回调函数中的错误中断整个动画循环。
  • 通用性: 这种通用动画序列管理方案不仅适用于透明度变化,还可以应用于任何需要数值插值的动画,例如位置、大小、颜色等属性的变化。

通过 animateInterpolationSequence 这样的通用解决方案,开发者可以轻松地编排复杂的动画序列,实现从简单的淡入淡出到复杂的场景切换,极大地提高了动画开发的可控性和效率。

今天关于《requestAnimationFrame掌控复杂动画流程》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!

如何下载WindowsMovieMaker步骤详解如何下载WindowsMovieMaker步骤详解
上一篇
如何下载WindowsMovieMaker步骤详解
Doctrine查询排除重叠时间区间
下一篇
Doctrine查询排除重叠时间区间
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    511次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    499次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 千音漫语:智能声音创作助手,AI配音、音视频翻译一站搞定!
    千音漫语
    千音漫语,北京熠声科技倾力打造的智能声音创作助手,提供AI配音、音视频翻译、语音识别、声音克隆等强大功能,助力有声书制作、视频创作、教育培训等领域,官网:https://qianyin123.com
    643次使用
  • MiniWork:智能高效AI工具平台,一站式工作学习效率解决方案
    MiniWork
    MiniWork是一款智能高效的AI工具平台,专为提升工作与学习效率而设计。整合文本处理、图像生成、营销策划及运营管理等多元AI工具,提供精准智能解决方案,让复杂工作简单高效。
    599次使用
  • NoCode (nocode.cn):零代码构建应用、网站、管理系统,降低开发门槛
    NoCode
    NoCode (nocode.cn)是领先的无代码开发平台,通过拖放、AI对话等简单操作,助您快速创建各类应用、网站与管理系统。无需编程知识,轻松实现个人生活、商业经营、企业管理多场景需求,大幅降低开发门槛,高效低成本。
    629次使用
  • 达医智影:阿里巴巴达摩院医疗AI影像早筛平台,CT一扫多筛癌症急慢病
    达医智影
    达医智影,阿里巴巴达摩院医疗AI创新力作。全球率先利用平扫CT实现“一扫多筛”,仅一次CT扫描即可高效识别多种癌症、急症及慢病,为疾病早期发现提供智能、精准的AI影像早筛解决方案。
    648次使用
  • 智慧芽Eureka:更懂技术创新的AI Agent平台,助力研发效率飞跃
    智慧芽Eureka
    智慧芽Eureka,专为技术创新打造的AI Agent平台。深度理解专利、研发、生物医药、材料、科创等复杂场景,通过专家级AI Agent精准执行任务,智能化工作流解放70%生产力,让您专注核心创新。
    623次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码