
本文详解如何将原本仅作用于单个 .see-more Canvas 的水彩动画,扩展至页面中所有匹配元素,核心在于用 querySelectorAll 替代 querySelector,并为每个 Canvas 实例化独立的渲染上下文与动画逻辑。
本文详解如何将原本仅作用于单个 `.see-more` canvas 的水彩动画,扩展至页面中所有匹配元素,核心在于用 `queryselectorall` 替代 `queryselector`,并为每个 canvas 实例化独立的渲染上下文与动画逻辑。
JavaScript 动画(如本例中的水彩扩散效果)默认只对首个匹配元素生效,是因为 document.querySelector('.see-more') 仅返回第一个 DOM 节点。若页面中存在多个 <canvas class="see-more">,必须显式遍历所有实例,并为每个 Canvas 绑定独立的绘图上下文(2D context)、状态管理(如 watercolors 数组)和动画帧循环——否则所有动画会共享同一上下文与状态,导致行为错乱或仅首项响应。
✅ 正确实现:为每个 Canvas 创建隔离作用域
关键改造点如下:
用户要生成可打印的中文字帖/练习纸、导出多页 A4 PDF 报告,或把 SVG 设计稿零误差还原到 Canvas 时使用。本技能是「Canvas 内容工厂闭环」的总控,编排:网格渲染引擎(13 种教育网格+拼音标注) → 多页 PDF 导出(A4 合成) → SVG 精准复刻(坐标误差<0.001px)。触发词:生成字帖、练习纸、导出 PDF、SVG 转 Canvas、印刷级还原、A4 报告、米字格田字格。
-
批量获取 Canvas 元素
使用 document.querySelectorAll('.see-more') 获取 NodeList,再通过 forEach 遍历初始化:
const canvases = document.querySelectorAll('.see-more');
if (canvases.length > 0) {
canvases.forEach(canvas => {
const cx = canvas.getContext('2d');
// 初始化该 canvas 的专属上下文配置
cx.globalCompositeOperation = 'source-over';
cx.imageSmoothingEnabled = true;
cx.imageSmoothingQuality = 'low';
start(canvas); // 启动专属动画循环
});
}-
重构动画函数,支持 Canvas 参数透传
原 frame() 函数需接收 canvas 参数,并使用 bind() 或箭头函数确保每次调用作用于正确实例:
function start(canvas) {
window.requestAnimationFrame(time => frame(canvas, time));
}
function frame(canvas, time) {
const cx = canvas.getContext('2d');
// 动态适配 canvas 尺寸(注意:此处应为每个 canvas 独立计算)
const multiplier = 0.25;
const expectedWidth = Math.floor(canvas.clientWidth * multiplier);
const expectedHeight = Math.floor(canvas.clientHeight * multiplier);
if (canvas.width !== expectedWidth) canvas.width = expectedWidth;
if (canvas.height !== expectedHeight) canvas.height = expectedHeight;
// ✅ 关键:watercolor 实例需绑定到当前 canvas
if (watercolors.length < 2) {
watercolors.push(new Watercolor(time, canvas));
}
// 更新 & 渲染逻辑(保持在当前 canvas 上下文中执行)
for (let i = watercolors.length - 1; i >= 0; i--) {
if (!watercolors[i].update(time)) {
watercolors.splice(i, 1);
}
}
watercolors.forEach(wc => wc.render(cx));
// 递归调用自身,持续动画
window.requestAnimationFrame(time => frame(canvas, time));
}-
确保 Watercolor 类支持多 Canvas 上下文
构造函数需接收 canvas 参数,并在 render() 中使用其专属 context:
class Watercolor {
constructor(time, canvas) {
const cx = canvas.getContext('2d'); // ✅ 每次取当前 canvas 的 context
const colorIndex = Math.floor(Math.random() * palette.length);
const gradient = cx.createRadialGradient(0, 0, 1, 0, 0, 50);
const [r, g, b] = palette[colorIndex];
gradient.addColorStop(0, `rgba(${r},${g},${b},1)`);
gradient.addColorStop(1, `rgba(${r},${g},${b},0)`);
this.color = gradient;
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 由外部传入,确保作用于正确 canvas
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();
}
}⚠️ 注意事项与最佳实践
- 避免全局状态污染:原代码中 watercolors 数组与 frameId 为全局变量,会导致多个 Canvas 共享同一粒子池。理想方案是为每个 Canvas 维护独立的 watercolors 数组(例如封装成对象或 Map),但本例采用简化策略——通过 canvas 参数区分上下文,实际运行中仍需注意粒子数量控制。
- 性能优化建议:若 Canvas 数量较多(>5),建议使用 requestIdleCallback 或限制每帧最大绘制粒子数,防止主线程阻塞。
- CSS 样式兼容性:确保 .see-more 在 DOM 中已渲染完成(可置于 DOMContentLoaded 或 window.load 后执行),且 clientWidth/clientHeight 能正确读取尺寸。
- Canvas 初始化时机:动态插入的 Canvas 需手动调用初始化逻辑;服务端渲染(SSR)场景下,务必在客户端 hydrate 后执行。
通过以上改造,水彩动画即可在页面中所有 .see-more Canvas 上独立、同步运行,真正实现“一处代码,多处生效”的复用目标。

















