讲师中心 微信公众号
AI工具推荐 视频效率加速

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

冬敏酱_9782

冬敏酱_9782

发布时间:2025-08-04 14:44:01

|

365人浏览过

|

来源于php中文网

原创

使用 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. 回调与递归:

    page-agent 浏览器控制(CDP)
    page-agent 浏览器控制(CDP)

    使用 page-agent CLI(CDP)控制浏览器;CLI只能通过 GitHub Release 的 .tgz 安装,npm 官方包暂未发布。

    下载
    • 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 这样的通用解决方案,开发者可以轻松地编排复杂的动画序列,实现从简单的淡入淡出到复杂的场景切换,极大地提高了动画开发的可控性和效率。

热门AI工具

更多
超级简历WonderCV

一款AI办公效率工具,主要用于免费求职简历模版下载制作,应届生职场人必备简历制作神器,适合需要提升相关任务效率的用户。

讯飞绘文

讯飞绘文是一款由科大讯飞推出的一站式 AIGC 内容运营平台。

创客贴AI

创客贴AI是一款AI图像与设计工具,AI辅助的智能在线设计工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

讯飞智作

讯飞智作是一款AI视频创作工具,AI文本配音工具,数字人课程、营销视频制作。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

相关专题

更多
while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

354

2023.09.25

html5动画制作有哪些制作方法
html5动画制作有哪些制作方法

html5动画制作方法有使用CSS3动画、使用JavaScript动画库、使用HTML5 Canvas等。想了解更多html5动画制作方法相关内容,可以阅读本专题下面的文章。

4711

2023.10.23

canva可画使用技巧汇总
canva可画使用技巧汇总

本专题汇集了PHP中文网精心整理的Canva可画新手使用技巧大全,专为零基础用户打造。无论你是想制作精美海报、PPT还是社交媒体配图,这里都有从注册登录、模板搜索到AI辅助设计、导出分享的全流程保姆级教程。通过深入浅出的图文指南,帮助你快速跨越设计门槛,轻松掌握Canva可画的实用操作,让每个人都能成为设计达人!

284

2026.05.19

PHP 高并发与性能优化
PHP 高并发与性能优化

本专题聚焦 PHP 在高并发场景下的性能优化与系统调优,内容涵盖 Nginx 与 PHP-FPM 优化、Opcode 缓存、Redis/Memcached 应用、异步任务队列、数据库优化、代码性能分析与瓶颈排查。通过实战案例(如高并发接口优化、缓存系统设计、秒杀活动实现),帮助学习者掌握 构建高性能PHP后端系统的核心能力。

19873

2025.10.16

PHP 数据库操作与性能优化
PHP 数据库操作与性能优化

本专题聚焦于PHP在数据库开发中的核心应用,详细讲解PDO与MySQLi的使用方法、预处理语句、事务控制与安全防注入策略。同时深入分析SQL查询优化、索引设计、慢查询排查等性能提升手段。通过实战案例帮助开发者构建高效、安全、可扩展的PHP数据库应用系统。

453

2025.11.13

JavaScript 性能优化与前端调优
JavaScript 性能优化与前端调优

本专题系统讲解 JavaScript 性能优化的核心技术,涵盖页面加载优化、异步编程、内存管理、事件代理、代码分割、懒加载、浏览器缓存机制等。通过多个实际项目示例,帮助开发者掌握 如何通过前端调优提升网站性能,减少加载时间,提高用户体验与页面响应速度。

348

2025.12.30

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

363

2026.03.06

宝塔面板Nginx性能优化与高并发配置实践
宝塔面板Nginx性能优化与高并发配置实践

本专题围绕宝塔环境下的 Nginx 优化展开,深入讲解缓存策略、连接数调优、Gzip 压缩、静态资源加速及反向代理配置。帮助用户提升网站访问速度与并发处理能力。

1116

2026.03.25

Postman身份认证怎么配置
Postman身份认证怎么配置

本专题从实际API接口调试场景出发,讲解Postman身份认证的完整配置流程,包括常见认证协议介绍、授权参数设置、访问令牌填写、OAuth 2.0授权及认证失败排查,帮助用户正确完成接口授权与访问验证。

0

2026.11.06

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn