You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 02:59:57