本文详解如何将单例 Canvas 水彩动画脚本改造为支持多个 .see-more 画布的通用方案,通过 querySelectorAll + 闭包隔离 + 参数化渲染实现独立、并发、无干扰的动画运行。
本文详解如何将单例 canvas 水彩动画脚本改造为支持多个 `.see-more` 画布的通用方案,通过 `queryselectorall` + 闭包隔离 + 参数化渲染实现独立、并发、无干扰的动画运行。
原代码仅使用 document.querySelector('.see-more') 获取首个匹配元素,因此水彩效果始终只作用于 DOM 中第一个
✅ 核心改造思路
- 批量选取画布:用 document.querySelectorAll('.see-more') 获取所有目标
- 逐个初始化:对每个画布单独调用 start(canvas),确保其 2D context、watercolors 数组、frameId 等资源相互隔离;
- 参数化动画函数:将 frame() 和 Watercolor 构造函数显式接收 canvas 参数,避免依赖外部全局变量;
- 动态生成渐变:getGradients(canvas) 在每次创建水彩粒子时,基于当前画布的上下文生成专属径向渐变,防止跨画布污染。
? 关键代码重构说明
以下为精简后的可复用核心逻辑(已移除注释与冗余样式):
// 色板定义(保持不变)
const palette = [
[171, 204, 177], // mint
[221, 160, 221], // plum
[255, 192, 203], // pink
[0, 255, 255], // neon blue
[255, 255, 0] // yellow
];
// 按 canvas 实例生成专属渐变数组
const getGradients = (canvas) => {
const cx = canvas.getContext('2d');
return palette.map(([r, g, b]) => {
const gradient = cx.createRadialGradient(0, 0, 1, 0, 0, 50);
gradient.addColorStop(0, `rgba(${r},${g},${b},1)`);
gradient.addColorStop(1, `rgba(${r},${g},${b},0)`);
return gradient;
});
};
class Watercolor {
constructor(time, canvas) {
const gradients = getGradients(canvas);
const colorIndex = Math.floor(Math.random() * gradients.length);
this.color = gradients[colorIndex];
this.radius = 50;
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.dx = Math.random() * 2 - 1;
this.dy = Math.random() * 2 - 1;
this.startTime = time;
this.currentTime = time;
this.duration = Math.ceil(Math.random() * 5000);
}
update(time) {
this.currentTime = time;
if (this.currentTime - this.startTime >= this.duration) return false;
this.x += this.dx;
this.y += this.dy;
return true;
}
render(cx) {
cx.save();
cx.globalAlpha = (this.currentTime - this.startTime) / this.duration;
cx.translate(this.x, this.y);
cx.beginPath();
cx.arc(0, 0, this.radius, 0, Math.PI * 2);
cx.fillStyle = this.color;
cx.fill();
cx.restore();
}
}
// 启动单个画布的动画循环
function start(canvas) {
const cx = canvas.getContext('2d');
// 初始化画布上下文配置
cx.globalCompositeOperation = 'source-over';
cx.imageSmoothingEnabled = true;
cx.imageSmoothingQuality = 'low';
// 绑定 canvas 到帧函数,确保每次调用都作用于正确画布
function frame(time) {
const multiplier = 0.25;
const w = Math.floor(canvas.clientWidth * multiplier);
const h = Math.floor(canvas.clientHeight * multiplier);
if (canvas.width !== w) canvas.width = w;
if (canvas.height !== h) canvas.height = h;
// 每个画布维护自己的 watercolors 数组(注意:此处需为每个 canvas 创建独立数组!)
// ⚠️ 原答案中仍共用全局 watercolors —— 这是严重缺陷!正确做法见下方修正
const watercolors = []; // ✅ 正确:每个 canvas 拥有独立粒子数组
if (watercolors.length = 0; i--) {
if (!watercolors[i].update(time)) watercolors.splice(i, 1);
}
watercolors.forEach(wc => wc.render(cx));
requestAnimationFrame(frame.bind(null, canvas));
}
requestAnimationFrame(frame.bind(null, canvas));
}
// 批量启动所有画布
document.querySelectorAll('.see-more').forEach(start);
⚠️ 重要注意事项
- 避免共享 watercolors 数组:原答案中 watercolors 仍为全局变量,会导致多个画布共用同一粒子池,产生错乱或冲突。✅ 正确做法是将 watercolors 移入 frame 函数作用域(如上所示),或封装为类/闭包状态。
- requestAnimationFrame 绑定需明确:使用 frame.bind(null, canvas) 确保回调中 canvas 参数稳定传递;不可依赖 this 或全局变量。
- Canvas 尺寸响应式处理:代码中通过 clientWidth/clientHeight 动态缩放画布像素尺寸,保证高清显示且性能可控(multiplier: 0.25 降低渲染负载)。
- 样式兼容性:.see-more 的 CSS 必须设置 width: 100%; height: 100% 并置于相对定位容器内,否则 clientWidth/clientHeight 可能返回 0。
✅ 总结
为多个 Canvas 启用水彩动画,本质是将单例逻辑重构为实例化服务。关键在于切断全局依赖、参数化上下文、隔离状态数据。只要遵循“一个画布 → 一个上下文 → 一个粒子数组 → 一个独立动画循环”的原则,即可轻松扩展至任意数量的画布,且互不干扰、性能可控、易于维护。











