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:
index.html
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()
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?
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.