Search code examples
javascriptoopweb-applicationshtml5-canvasgame-development

Canvas: Two balls having the same color


I'm trying to learn canvas and I'm at a point that I want to spawn balls of different colors.

The problem is when I instantiate a new object called ball2 it seems that ball doesn't retain its original color when it was instat.

How do I keep the first ball have its original color?

To recreate the issue:

  • Have live server installed in VScode
  • Copy paste the code below in the same directory
  • Start a live server with index.html

This is the main entry of the code:

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()
})

Here is the rest of the code:

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()

    cost 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
}

I have tried passing in the context that we get from World.js on to Ball.js and it still was the same... It is as if the state of this.ctx.fillStyle is shared by both ball and ball2.

I may not have a good grasp of OOP but shouldn't this.ctx be of the scope of the object this and that it shouldn't affect another object's property with the same property name?


Solution

  • Canvas elements only have one 2d drawing context.

    "use strict";
     let ctxBall1 = canvas.getContext('2d');
     let ctxBall2 = canvas.getContext('2d');
     console.log( "Multiple calls to getContext('2d') on the same convas return the same ctx object: ",
        ctxBall1 === ctxBall2);
        
    <canvas id=canvas></canvas>

    Hence copying the canvas context to ball objects in the Ball constructor with

    (this.ctx = this.canvas.getContext('2d'))
    

    simply takes a copy of the canvas context without creating a new one - you can leave out "as if" in your description of the behaviour.

    I should expect setting canvas fillStyle with the innerColor recorded in the Ball instance, in the spawn method before filling the path, would fix the problem. Use of outerColor would require similar treatment when coded.

    Note supporting different outer thicknesses would also require resetting ball's stroke width in spawn as well. That is to say any and all context properties that differ between balls must be set in spawn before redrawing them.