Skip to content

Commit f129a92

Browse files
Port to use pixels instead squares in grid; Calculate all steps in between frames (fixes constant speed); Rewrite collision logic to check vertical, horizontal and diagonal collisions (fixes #6, #18, #19, #21, #22)
1 parent d0c70cb commit f129a92

File tree

1 file changed

+234
-84
lines changed

1 file changed

+234
-84
lines changed

index.html

+234-84
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969

7070
<body>
7171
<div id="container">
72-
<canvas id="pongCanvas" width="800" height="800"></canvas>
72+
<canvas id="pongCanvas" width="832" height="832"></canvas>
7373
<div id="score"></div>
7474
<p id="made">
7575
made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on
@@ -101,91 +101,180 @@
101101
const NIGHT_COLOR = colorPalette.NocturnalExpedition;
102102
const NIGHT_BALL_COLOR = colorPalette.MysticMint;
103103

104-
const SQUARE_SIZE = 25;
104+
const SQUARE_SIZE = 26; // integers only - must be factor of canvas size
105+
const BALL_RADIUS = 13; // integers only
106+
107+
const SPEED = 12;
105108

106109
const numSquaresX = canvas.width / SQUARE_SIZE;
107110
const numSquaresY = canvas.height / SQUARE_SIZE;
108111

109-
let squares = [];
112+
let pixels = [];
113+
114+
for (let x = 0; x < canvas.width; x++) {
115+
pixels[x] = [];
116+
}
117+
118+
function calculateDistance(p1x, p1y, p2x, p2y) {
119+
return Math.sqrt(Math.pow(p1x - p2x, 2) + Math.pow(p1y - p2y, 2))
120+
}
121+
122+
function ballIsInSquare(sx, sy, ballColor) {
123+
let xSquare = sx * SQUARE_SIZE;
124+
let ySquare = sy * SQUARE_SIZE;
125+
126+
let xBall, yBall;
127+
if (ballColor === DAY_COLOR) {
128+
xBall = xDay;
129+
yBall = yDay;
130+
}
131+
else {
132+
xBall = xNight;
133+
yBall = yNight;
134+
}
135+
136+
// If this is the square --> this is the exclusion zone.
137+
// and ball has radius 2
138+
// ---bb----------------- ----------------------
139+
// --bbbb---------------- ----------------------
140+
// --bbbb---------------- --------######--------
141+
// ---bb----------------- -------########-------
142+
// ---------ssss--------- -------##ssss##-------
143+
// ---------ssss--------- -------##ssss##-------
144+
// ---------ssss--------- -------##ssss##-------
145+
// ---------ssss--------- -------##ssss##-------
146+
// ---------------------- -------########-------
147+
// ---------------------- --------######--------
148+
// ---------------------- ----------------------
149+
// ---------------------- ----------------------
150+
151+
const squareTopLeftX = xSquare;
152+
const squareTopLeftY = ySquare;
153+
const squareTopRightX = xSquare + SQUARE_SIZE;
154+
const squareTopRightY = ySquare;
155+
const squareBottomLeftX = xSquare;
156+
const squareBottomLeftY = ySquare + SQUARE_SIZE;
157+
const squareBottomRightX = xSquare + SQUARE_SIZE;
158+
const squareBottomRightY = ySquare + SQUARE_SIZE;
159+
160+
if (
161+
xBall >= xSquare - BALL_RADIUS
162+
&& xBall <= xSquare + SQUARE_SIZE + BALL_RADIUS
163+
&& yBall >= ySquare - BALL_RADIUS
164+
&& yBall <= ySquare + SQUARE_SIZE + BALL_RADIUS
165+
) {
166+
// ball is inside the exclusion zone
167+
const distanceToTopLeft = calculateDistance(xBall, yBall, squareTopLeftX, squareTopLeftY);
168+
const distanceToTopRight = calculateDistance(xBall, yBall, squareTopRightX, squareTopRightY);
169+
const distanceToBottomLeft = calculateDistance(xBall, yBall, squareBottomLeftX, squareBottomLeftY);
170+
const distanceToBottomRight = calculateDistance(xBall, yBall, squareBottomRightX, squareBottomRightY);
171+
172+
if (
173+
(xBall < squareTopLeftX && yBall < squareTopLeftY && distanceToTopLeft >= BALL_RADIUS)
174+
|| (xBall > squareTopRightX && yBall < squareTopRightY && distanceToTopRight >= BALL_RADIUS)
175+
|| (xBall < squareBottomLeftX && yBall > squareBottomLeftY && distanceToBottomLeft >= BALL_RADIUS)
176+
|| (xBall > squareBottomRightX && yBall > squareBottomRightY && distanceToBottomRight >= BALL_RADIUS)
177+
) {
178+
// ball is at corners of exclusion zone (which are not part of it)
179+
return false;
180+
}
110181

111-
for (let i = 0; i < numSquaresX; i++) {
112-
squares[i] = [];
113-
for (let j = 0; j < numSquaresY; j++) {
114-
squares[i][j] = i < numSquaresX / 2 ? DAY_COLOR : NIGHT_COLOR;
182+
// bass is inside exclusion zone and not at corners
183+
return true;
184+
}
185+
else {
186+
// ball is outside the exclusion zone
187+
return false;
115188
}
116189
}
117190

118-
let x1 = canvas.width / 4;
119-
let y1 = canvas.height / 2;
120-
let dx1 = 12.5;
121-
let dy1 = -12.5;
191+
function flipColorOfSquareThatPixelIsIn(x, y) {
192+
let currentColor = pixels[x][y];
193+
let newColor = currentColor === DAY_COLOR ? NIGHT_COLOR : DAY_COLOR;
194+
let otherBallColor = currentColor === DAY_COLOR ? DAY_COLOR : NIGHT_COLOR;
195+
196+
let {sx, sy} = getSquareThatPixelIsIn(x, y);
122197

123-
let x2 = (canvas.width / 4) * 3;
124-
let y2 = canvas.height / 2;
125-
let dx2 = -12.5;
126-
let dy2 = 12.5;
198+
// Do not flip color of square if the other ball and the square overlap
199+
if (ballIsInSquare(sx, sy, otherBallColor)) {
200+
return;
201+
}
202+
203+
colorPixelsInSquare(sx, sy, newColor);
204+
}
205+
206+
function getSquareThatPixelIsIn(pixelX, pixelY) {
207+
let sx = Math.floor(pixelX / SQUARE_SIZE);
208+
let sy = Math.floor(pixelY / SQUARE_SIZE);
209+
return {sx, sy}
210+
}
211+
212+
function colorPixelsInSquare(sx, sy, color) {
213+
for (let x = sx * SQUARE_SIZE; x < (sx + 1) * SQUARE_SIZE; x++) {
214+
for (let y = sy * SQUARE_SIZE; y < (sy + 1) * SQUARE_SIZE; y++) {
215+
pixels[x][y] = color;
216+
}
217+
}
218+
}
219+
220+
for (let x = 0; x < numSquaresX; x++) {
221+
for (let y = 0; y < numSquaresY; y++) {
222+
const color = x < numSquaresX / 2 ? DAY_COLOR : NIGHT_COLOR;
223+
colorPixelsInSquare(x, y, color)
224+
}
225+
}
226+
227+
function getRandomSign() {
228+
return Math.sign(Math.random() - 0.5);
229+
}
230+
231+
function getRandomIntInRange(start, end) {
232+
let randomFloatInRange = start + Math.random() * (end - start + 1);
233+
return Math.floor(randomFloatInRange);
234+
}
235+
236+
let xDay = canvas.width / 4 + getRandomIntInRange(-5 * SQUARE_SIZE, 5 * SQUARE_SIZE);
237+
let yDay = getRandomIntInRange(0.5 * SQUARE_SIZE, canvas.height - 0.5 * SQUARE_SIZE);
238+
let dxDay = getRandomSign();
239+
let dyDay = getRandomSign();
240+
241+
let xNight = (canvas.width / 4) * 3;
242+
let yNight = getRandomIntInRange(0.5 * SQUARE_SIZE, canvas.height - 0.5 * SQUARE_SIZE);
243+
let dxNight = getRandomSign();
244+
let dyNight = getRandomSign();
127245

128246
let iteration = 0;
129247

130248
function drawBall(x, y, color) {
131249
ctx.beginPath();
132-
ctx.arc(x, y, SQUARE_SIZE / 2, 0, Math.PI * 2, false);
250+
ctx.arc(x, y, BALL_RADIUS, 0, Math.PI * 2, false);
133251
ctx.fillStyle = color;
134252
ctx.fill();
135253
ctx.closePath();
136254
}
137255

138256
function drawSquares() {
139-
for (let i = 0; i < numSquaresX; i++) {
140-
for (let j = 0; j < numSquaresY; j++) {
141-
ctx.fillStyle = squares[i][j];
257+
for (let i = 0; i < canvas.width; i += SQUARE_SIZE) {
258+
for (let j = 0; j < canvas.height; j += SQUARE_SIZE) {
259+
ctx.fillStyle = pixels[i][j];
142260
ctx.fillRect(
143-
i * SQUARE_SIZE,
144-
j * SQUARE_SIZE,
261+
i,
262+
j,
145263
SQUARE_SIZE,
146264
SQUARE_SIZE
147265
);
148266
}
149267
}
150268
}
151269

152-
function updateSquareAndBounce(x, y, dx, dy, color) {
153-
let updatedDx = dx;
154-
let updatedDy = dy;
155-
156-
// Check multiple points around the ball's circumference
157-
for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) {
158-
let checkX = x + Math.cos(angle) * (SQUARE_SIZE / 2);
159-
let checkY = y + Math.sin(angle) * (SQUARE_SIZE / 2);
160-
161-
let i = Math.floor(checkX / SQUARE_SIZE);
162-
let j = Math.floor(checkY / SQUARE_SIZE);
163-
164-
if (i >= 0 && i < numSquaresX && j >= 0 && j < numSquaresY) {
165-
if (squares[i][j] !== color) {
166-
squares[i][j] = color;
167-
168-
// Determine bounce direction based on the angle
169-
if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) {
170-
updatedDx = -updatedDx;
171-
} else {
172-
updatedDy = -updatedDy;
173-
}
174-
}
175-
}
176-
}
177-
178-
return { dx: updatedDx, dy: updatedDy };
179-
}
180-
181270
function updateScoreElement() {
182271
let dayScore = 0;
183272
let nightScore = 0;
184-
for (let i = 0; i < numSquaresX; i++) {
185-
for (let j = 0; j < numSquaresY; j++) {
186-
if (squares[i][j] === DAY_COLOR) {
273+
for (let i = 0; i < numSquaresX * SQUARE_SIZE; i += SQUARE_SIZE) {
274+
for (let j = 0; j < numSquaresY * SQUARE_SIZE; j += SQUARE_SIZE) {
275+
if (pixels[i][j] === DAY_COLOR) {
187276
dayScore++;
188-
} else if (squares[i][j] === NIGHT_COLOR) {
277+
} else if (pixels[i][j] === NIGHT_COLOR) {
189278
nightScore++;
190279
}
191280
}
@@ -194,46 +283,107 @@
194283
scoreElement.textContent = `day ${dayScore} | night ${nightScore}`;
195284
}
196285

197-
function checkBoundaryCollision(x, y, dx, dy) {
198-
if (x + dx > canvas.width - SQUARE_SIZE / 2 || x + dx < SQUARE_SIZE / 2) {
199-
dx = -dx;
286+
function calculateNextStep(xOld, yOld, dxOld, dyOld, color) {
287+
let dxNew = dxOld;
288+
let dyNew = dyOld;
289+
290+
let horCanvasOut = false;
291+
let verCanvasOut = false;
292+
293+
294+
// possible vertical and horizontal collide points (CP)
295+
const horCpX = xOld + dxOld * BALL_RADIUS;
296+
const horCpY = yOld;
297+
const verCpX = xOld;
298+
const verCpY = yOld + dyOld * BALL_RADIUS;
299+
300+
// possible diagonal collide point
301+
const diaCpX = xOld + Math.ceil((1 / Math.sqrt(2)) * BALL_RADIUS) * dxOld;
302+
const diaCpY = yOld + Math.ceil((1 / Math.sqrt(2)) * BALL_RADIUS) * dyOld;
303+
304+
// possible vertical and horizontal collide points (CP)
305+
const horCpNextX = horCpX + dxOld;
306+
const horCpNextY = horCpY + dyOld;
307+
const verCpNextX = verCpX + dxOld;
308+
const verCpNextY = verCpY + dyOld;
309+
const diaCpNextX = diaCpX + dxOld;
310+
const diaCpNextY = diaCpY + dyOld;
311+
312+
// pixel horizontally out of canvas
313+
if (horCpNextX < 0 || canvas.width <= horCpNextX) {
314+
dxNew = -dxNew;
315+
horCanvasOut = true;
200316
}
201-
if (
202-
y + dy > canvas.height - SQUARE_SIZE / 2 ||
203-
y + dy < SQUARE_SIZE / 2
204-
) {
205-
dy = -dy;
317+
318+
// pixel vertically out of canvas
319+
if (verCpNextY < 0 || canvas.height <= verCpNextY) {
320+
dyNew = -dyNew;
321+
verCanvasOut = true;
206322
}
207323

208-
return { dx: dx, dy: dy };
324+
// horizontal collision
325+
if (!horCanvasOut && pixels[horCpNextX][horCpNextY] === color) {
326+
// change horizontal direction
327+
dxNew = -dxNew;
328+
329+
// flip next square
330+
flipColorOfSquareThatPixelIsIn(horCpNextX, horCpNextY);
331+
}
332+
333+
// vertical collision
334+
if (!verCanvasOut && pixels[verCpNextX][verCpNextY] === color) {
335+
// change vertical direction
336+
dyNew = -dyNew;
337+
338+
// flip next square
339+
flipColorOfSquareThatPixelIsIn(verCpNextX, verCpNextY);
340+
}
341+
342+
// diagonal collision (if ball radius is bigger 2)
343+
if (!horCanvasOut && !verCanvasOut && pixels[diaCpNextX][diaCpNextY] === color) {
344+
// change horizontal and vertical direction
345+
dxNew = -dxNew;
346+
dyNew = -dyNew;
347+
348+
// flip next square
349+
flipColorOfSquareThatPixelIsIn(diaCpNextX, diaCpNextY);
350+
}
351+
352+
let xNew = xOld + dxNew;
353+
let yNew = yOld + dyNew;
354+
355+
return {xNew, yNew, dxNew, dyNew};
356+
}
357+
358+
function calculateNextFrame() {
359+
let step = 0
360+
while (step < SPEED) {
361+
if (iteration % 2 === 0) {
362+
let newDay = calculateNextStep(xDay, yDay, dxDay, dyDay, DAY_BALL_COLOR);
363+
xDay = newDay.xNew;
364+
yDay = newDay.yNew;
365+
dxDay = newDay.dxNew;
366+
dyDay = newDay.dyNew;
367+
}
368+
else {
369+
let newNight = calculateNextStep(xNight, yNight, dxNight, dyNight, NIGHT_BALL_COLOR);
370+
xNight = newNight.xNew;
371+
yNight = newNight.yNew;
372+
dxNight = newNight.dxNew;
373+
dyNight = newNight.dyNew;
374+
}
375+
step++;
376+
}
209377
}
210378

211379
function draw() {
212380
ctx.clearRect(0, 0, canvas.width, canvas.height);
213381
drawSquares();
214382

215-
drawBall(x1, y1, DAY_BALL_COLOR);
216-
let bounce1 = updateSquareAndBounce(x1, y1, dx1, dy1, DAY_COLOR);
217-
dx1 = bounce1.dx;
218-
dy1 = bounce1.dy;
219-
220-
drawBall(x2, y2, NIGHT_BALL_COLOR);
221-
let bounce2 = updateSquareAndBounce(x2, y2, dx2, dy2, NIGHT_COLOR);
222-
dx2 = bounce2.dx;
223-
dy2 = bounce2.dy;
224-
225-
let boundary1 = checkBoundaryCollision(x1, y1, dx1, dy1);
226-
dx1 = boundary1.dx;
227-
dy1 = boundary1.dy;
228-
229-
let boundary2 = checkBoundaryCollision(x2, y2, dx2, dy2);
230-
dx2 = boundary2.dx;
231-
dy2 = boundary2.dy;
383+
drawBall(xDay, yDay, DAY_BALL_COLOR);
384+
drawBall(xNight, yNight, NIGHT_BALL_COLOR);
232385

233-
x1 += dx1;
234-
y1 += dy1;
235-
x2 += dx2;
236-
y2 += dy2;
386+
calculateNextFrame();
237387

238388
iteration++;
239389
if (iteration % 1_000 === 0) console.log("iteration", iteration);

0 commit comments

Comments
 (0)