
在 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 仅仅是请求在 下一个可用帧 执行回调,而不是等待当前动画完成。因此,我们需要一个更精细的机制来管理动画的顺序和状态。
为了解决上述问题,我们可以构建一个通用的动画序列管理器,它能够接收一系列动画步骤,并按序执行它们,同时支持自定义持续时间、缓动函数和插值范围。
以下是一个名为 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;
};
}这个函数是整个动画管理的核心。它接收两个主要参数:
内部工作机制:
时间管理与精度:
序列项迭代 (while 循环):
插值计算:
回调与递归:
异常处理 (safeCall):
动画停止:
缓动函数允许动画在不同阶段以不同的速度进行,使动画看起来更自然、更有动感。它们通常接收一个 0 到 1 之间的进度值 x,并返回一个经过变换的 0 到 1 之间的值。
例如,一个五次方的缓出函数 easeOutQuint:
function easeOutQuint (x) {
return 1 - Math.pow(1 - x, 5);
}为了演示 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 实时计算,使星形持续旋转。
现在,我们可以定义一个复杂的动画序列,并将其传递给 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>
这段代码定义了一个复杂的动画序列:
通过 animateInterpolationSequence 这样的通用解决方案,开发者可以轻松地编排复杂的动画序列,实现从简单的淡入淡出到复杂的场景切换,极大地提高了动画开发的可控性和效率。
以上就是使用 requestAnimationFrame 实现复杂动画序列管理的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号