如何让Canvas滚动驱动的序列帧图像动画更流畅?
优化滚动驱动序列帧动画,实现类似ClearMotion网站的流畅效果
Hey there! I’ve worked on similar scroll-driven sequence animations before, so let’s walk through how to tweak your code to get that buttery-smooth feel like the ClearMotion site. Your existing code has the right core idea, but we can fix a few performance bottlenecks and add some robustness.
1. 修复图片预加载逻辑(避免部分加载导致卡顿)
你当前的循环只是创建Image对象,但没有跟踪图片是否完全加载完成。如果动画在所有帧加载完毕前启动,很容易出现卡顿。我们来添加一个带完成追踪的预加载流程:
const totalImages = 200; const images = []; let loadedCount = 0; function preloadImages() { for (let i = 1; i <= totalImages; i++) { const img = new Image(); // 更简洁的3位序号格式化方式 const slug = String(i).padStart(3, '0'); img.src = `https://s3.amazonaws.com/clearmotion/hero/high-min/frame${slug}-low.jpg`; img.onload = () => { loadedCount++; // 只有当所有帧都加载完成后,才初始化动画 if (loadedCount === totalImages) { initScrollAnimation(); } }; // 添加错误处理,捕获加载失败的帧 img.onerror = (e) => console.error(`加载第${i}帧失败:`, e); images.push(img); } }
2. 节流滚动事件(避免主线程过载)
滚动事件的触发频率极高(每秒可达数百次),如果每一次事件都执行绘制逻辑,会阻塞主线程导致动画卡顿。用节流函数限制更新频率(目标是60fps,所以设置16ms间隔):
function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } }
3. 用requestAnimationFrame同步绘制
把节流和requestAnimationFrame结合,确保Canvas绘制和浏览器的刷新周期对齐,消除因更新时机错位导致的视觉卡顿:
function initScrollAnimation() { const c = document.getElementById("background"); const ctx = c.getContext("2d"); // 一次性设置Canvas尺寸匹配帧的大小 c.width = images[0].width; c.height = images[0].height; const updateFrame = () => { // 计算滚动进度 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const maxScroll = document.body.scrollHeight - window.innerHeight; // 计算帧索引并做边界检查(避免数组越界错误) const frameIndex = maxScroll === 0 ? 0 : Math.max(0, Math.min(totalImages - 1, Math.floor((scrollTop / maxScroll) * (totalImages - 1)))); // 绘制当前帧 const currentImg = images[frameIndex]; ctx.clearRect(0, 0, c.width, c.height); ctx.drawImage(currentImg, 0, 0); }; // 结合节流与requestAnimationFrame,兼顾性能与流畅度 const throttledScrollHandler = throttle(() => { requestAnimationFrame(updateFrame); }, 16); // 绑定滚动事件处理器 window.addEventListener('scroll', throttledScrollHandler); // 立即绘制第一帧 updateFrame(); }
4. 额外优化:针对大数量帧的内存优化
如果200帧的初始加载或内存占用过高,可以实现帧懒加载:只加载当前帧前后的若干帧。不过如果追求全程无卡顿,预加载所有帧仍是最优选择——记得确保图片已经过压缩(比如用WebP格式),减少文件大小。
完整优化代码
结合DOM就绪检查的最终版本:
// 等待DOM完全加载后再启动 document.addEventListener('DOMContentLoaded', () => { const totalImages = 200; const images = []; let loadedCount = 0; function preloadImages() { for (let i = 1; i <= totalImages; i++) { const img = new Image(); const slug = String(i).padStart(3, '0'); img.src = `https://s3.amazonaws.com/clearmotion/hero/high-min/frame${slug}-low.jpg`; img.onload = () => { loadedCount++; if (loadedCount === totalImages) { initScrollAnimation(); } }; img.onerror = (e) => console.error(`加载第${i}帧失败:`, e); images.push(img); } } function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } function initScrollAnimation() { const c = document.getElementById("background"); const ctx = c.getContext("2d"); c.width = images[0].width; c.height = images[0].height; const updateFrame = () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const maxScroll = document.body.scrollHeight - window.innerHeight; const frameIndex = maxScroll === 0 ? 0 : Math.max(0, Math.min(totalImages - 1, Math.floor((scrollTop / maxScroll) * (totalImages - 1)))); const currentImg = images[frameIndex]; ctx.clearRect(0, 0, c.width, c.height); ctx.drawImage(currentImg, 0, 0); }; const throttledScrollHandler = throttle(() => { requestAnimationFrame(updateFrame); }, 16); window.addEventListener('scroll', throttledScrollHandler); updateFrame(); } preloadImages(); });
内容的提问来源于stack exchange,提问作者Deryckxie




