Canvas多色小球问题:实例化新小球后原小球颜色丢失
我正在学习Canvas开发,希望生成不同颜色的小球。但实例化名为ball2的新对象后,原本的ball无法保留其初始颜色。我该如何让第一个小球保持原有颜色?
复现步骤:
- 在VSCode中安装live server插件
- 将以下代码复制到同一目录下
- 通过index.html启动live server
代码主入口
index.html
<!DOCTYPE html> <html> <head> <meta charset=utf-8 /> <title>Draw a circle</title> <script type="module" src="./index.js"></script> </head> <body> <canvas id="canvas"></canvas> </body> </html>
index.js
import World from './World.js' function createHiPPICanvas(width, height) { const ratio = window.devicePixelRatio const canvas = document.getElementById('canvas') canvas.width = width * ratio canvas.height = height * ratio canvas.style.width = width + 'px' canvas.style.height = height + 'px' canvas.getContext('2d').scale(ratio, ratio) return canvas } function setupCanvas() { const canvas = createHiPPICanvas(500, 300) const world = new World(canvas) world.loadWorld() } window.addEventListener('load', () => { setupCanvas() })
其余代码
World.js
export default class World { constructor(canvas) { this.canvas = canvas this.ctx = this.canvas.getContext('2d') } loadWorld() { const border = new Border(this.canvas, this.ctx) border.draw() const ball = new Ball({ canvas: this.canvas, innerColor: '#44CCFF', outerThickness: 1, pos: { x: 10, y: 10 }, radius: 10, initVelocity: { xV: 10, yV: 15 }, }) const ball2 = new Ball({ canvas: this.canvas, innerColor: '#E0FF4F', outerThickness: 1, pos: { x: 50, y: 100 }, radius: 10, initVelocity: { xV: 20, yV: 19 }, }) ball.spawn() ball2.spawn() const playWorld = () => { requestAnimationFrame(playWorld, canvas) this.ctx.clearRect(0, 0, this.ctx.canvas.clientWidth, this.ctx.canvas.clientHeight) ball.animate() ball2.animate() } playWorld() } }
Ball.js
import getStopPos from './getStopPos.js' export default class Ball { constructor({ canvas, outerThickness, innerColor, pos, radius, initVelocity }) { ;(this.canvas = canvas), (this.ctx = this.canvas.getContext('2d')) this.pos = pos this.radius = radius this.velocity = initVelocity this.ctx.fillStyle = innerColor this.ctx.lineWidth = outerThickness } spawn() { this.ctx.beginPath() this.ctx.arc(this.pos.x, this.pos.y, this.radius, 0, 2 * Math.PI) this.ctx.fill() this.ctx.strokeStyle = this.outerColor this.ctx.stroke() this.ctx.closePath() } move({ xV = 0, yV = 0 }) { ;(this.pos.x += xV), (this.pos.y += yV), this.spawn() } animate() { const { clientHeight, clientWidth } = this.ctx.canvas const heightStopBottom = getStopPos(clientHeight, this.radius) const heightStopTop = this.radius const widthStopRight = getStopPos(clientWidth, this.radius) const widthtStopLeft = this.radius const friction = 0.04 const gravity = 0.2 const bounce = Math.sqrt(2.3 * gravity) this.pos.y > heightStopBottom ? (this.velocity.yV *= -bounce) : (this.velocity.yV += gravity) if (this.velocity.xV < friction && this.velocity.xV > -friction) { this.velocity.xV = 0 } else { this.pos.x < 0 ? (this.velocity.xV += friction) : (this.velocity.xV -= friction) } if (this.pos.y < heightStopTop) this.velocity.yV *= -bounce if (this.pos.x > widthStopRight) this.velocity.xV *= -bounce if (this.pos.x < widthtStopLeft) this.velocity.xV *= -bounce this.move(this.velocity) } }
getStopPos.js
export default function getStopPos(canvasDimension, radius) { return canvasDimension - 1 - radius }
我尝试从World.js传递上下文到Ball.js,但问题依旧,似乎this.ctx.fillStyle的状态被ball和ball2共享。我对OOP的理解可能不够深入,疑惑为何this.ctx的作用域不能隔离两个对象的同名称属性?
问题根源
Canvas的2D上下文是全局状态机,所有Ball实例调用canvas.getContext('2d')获取的都是同一个上下文对象。你在Ball构造函数里直接修改this.ctx.fillStyle,本质是修改这个全局上下文的状态,后面实例化的ball2会覆盖这个值,导致ball绘制时使用的是ball2设置的颜色。
修改方法
把每个Ball的颜色、线宽作为实例属性保存,在每次绘制前设置上下文的样式,确保每个小球绘制时用自己的样式:
修改后的Ball.js代码:
import getStopPos from './getStopPos.js' export default class Ball { constructor({ canvas, outerThickness, innerColor, 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 = '#000000' // 补充原代码未定义的外框颜色 } spawn() { // 绘制前设置当前小球的样式 this.ctx.fillStyle = this.innerColor this.ctx.lineWidth = this.outerThickness this.ctx.strokeStyle = this.outerColor this.ctx.beginPath() this.ctx.arc(this.pos.x, this.pos.y, this.radius, 0, 2 * Math.PI) this.ctx.fill() this.ctx.stroke() this.ctx.closePath() } move({ xV = 0, yV = 0 }) { this.pos.x += xV this.pos.y += yV this.spawn() } animate() { const { clientHeight, clientWidth } = this.ctx.canvas const heightStopBottom = getStopPos(clientHeight, this.radius) const heightStopTop = this.radius const widthStopRight = getStopPos(clientWidth, this.radius) const widthtStopLeft = this.radius const friction = 0.04 const gravity = 0.2 const bounce = Math.sqrt(2.3 * gravity) this.pos.y > heightStopBottom ? (this.velocity.yV *= -bounce) : (this.velocity.yV += gravity) if (this.velocity.xV < friction && this.velocity.xV > -friction) { this.velocity.xV = 0 } else { this.pos.x < 0 ? (this.velocity.xV += friction) : (this.velocity.xV -= friction) } if (this.pos.y < heightStopTop) this.velocity.yV *= -bounce if (this.pos.x > widthStopRight) this.velocity.xV *= -bounce if (this.pos.x < widthtStopLeft) this.velocity.xV *= -bounce this.move(this.velocity) } }
同时修复World.js里的笔误:将cost playWorld改为const playWorld。
补充解释
每个Ball实例的this.ctx指向的是同一个Canvas上下文对象——Canvas的2D上下文是单例模式,无论调用多少次getContext('2d'),返回的都是同一个对象。所以你修改的是这个共享对象的属性,而非每个Ball实例的独立属性,自然会互相干扰。
正确的做法是把每个实例的样式数据存在自身属性里,每次绘制时临时设置上下文状态,这样每次绘制不同小球时,上下文会被切换到对应的样式,不会互相覆盖。
内容的提问来源于stack exchange,提问作者G R

