
Canvas 的 2D 上下文是共享的,所有 Ball 实例共用同一个 ctx 对象,因此直接在构造函数中设置 ctx.fillStyle 会相互覆盖;正确做法是在每次绘制(如 spawn 或 animate)前动态设置当前球所需的样式属性。
canvas 的 2d 上下文是共享的,所有 ball 实例共用同一个 `ctx` 对象,因此直接在构造函数中设置 `ctx.fillstyle` 会相互覆盖;正确做法是在每次绘制(如 `spawn` 或 `animate`)前动态设置当前球所需的样式属性。
在 Canvas 开发中,一个常见误区是认为每个 Ball 实例拥有独立的绘图上下文(ctx)。实际上,对同一 ——它不是副本,而是单例。这意味着 ball.ctx === ball2.ctx 恒为 true,所有样式状态(如 fillStyle、strokeStyle、lineWidth)都是全局共享的。
因此,当你在 Ball 构造函数中执行:
this.ctx.fillStyle = innerColor;
这并非“绑定”该颜色到当前球实例,而只是临时修改了共享上下文的填充色。随后创建 ball2 时再次赋值,就会覆盖 ball 的颜色设置。最终调用 ball.spawn() 时,ctx.fillStyle 已被 ball2 的颜色覆盖,导致两个球显示相同颜色。
✅ 正确解法:将样式设置移至绘制逻辑内部(如 spawn()),确保每次绘制前都显式配置对应球的专属样式。
以下是修正后的 Ball.js 关键部分(已修复颜色、描边、线宽问题):
export default class Ball {
constructor({ canvas, outerThickness = 1, innerColor = '#000', pos, radius, initVelocity }) {
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d'); // 仍可保留,但仅作引用
this.pos = pos;
this.radius = radius;
this.velocity = initVelocity;
// ✅ 仅保存球的个性化属性,不操作 ctx
this.innerColor = innerColor;
this.outerThickness = outerThickness;
this.outerColor = '#000'; // 可扩展为构造参数
}
spawn() {
const { ctx } = this;
// ✅ 每次绘制前重置专属样式
ctx.fillStyle = this.innerColor;
ctx.strokeStyle = this.outerColor;
ctx.lineWidth = this.outerThickness;
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, this.radius, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke(); // 注意:stroke() 前必须有 strokeStyle 和 lineWidth
ctx.closePath();
}
animate() {
// ...(原有物理逻辑保持不变)
// ✅ 动画中调用 spawn 时,样式仍由当前实例控制
this.spawn();
}
}
⚠️ 注意事项:
- 不要在构造函数中设置 ctx 样式:fillStyle、strokeStyle、lineWidth、globalAlpha 等均属上下文状态,需按需动态设置;
- stroke() 前务必设置 strokeStyle 和 lineWidth:原代码中 this.ctx.strokeStyle = this.outerColor 写成了 this.outerColor(未定义),且未赋值 outerColor,已补全;
- 避免重复 clearRect 覆盖问题:World.loadWorld() 中 clearRect 应在 requestAnimationFrame 循环内调用(当前代码中它位于 playWorld 内,位置正确),确保每帧清屏后再逐个绘制球体;
- 性能提示:频繁设置样式开销极小,远小于错误复用导致的视觉 bug,无需缓存或优化。
总结:Canvas 的 2DRenderingContext 是状态机而非实例化对象。面向对象设计中,“球的颜色”属于球的数据属性(this.innerColor),而非上下文的状态属性。始终遵循「绘制即配置」原则——在 spawn()、draw() 等渲染方法入口处,完整、明确地声明当前绘制所需的所有上下文状态。











