Skip to content

Commit 9519916

Browse files
Rewrite 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 9519916

File tree

1 file changed

+235
-87
lines changed

1 file changed

+235
-87
lines changed

index.html

+235-87
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="1600" height="1600"></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,139 +101,287 @@
101101
const NIGHT_COLOR = colorPalette.NocturnalExpedition;
102102
const NIGHT_BALL_COLOR = colorPalette.MysticMint;
103103

104-
const SQUARE_SIZE = 25;
104+
const SQUARE_SIZE = 50; // even integers only - must be factor of canvas size
105+
const BALL_RADIUS = SQUARE_SIZE / 2;
106+
107+
const SPEED = 25;
105108

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

109-
let squares = [];
112+
let pixels = [];
113+
114+
let xDay = (Math.floor(numSquaresX / 4) + 0.5) * SQUARE_SIZE;
115+
let yDay = (getRandomIntInRange(0, numSquaresY - 1) + 0.5) * SQUARE_SIZE;
116+
let dxDay = getRandomSign();
117+
let dyDay = getRandomSign();
118+
119+
let xNight = (Math.ceil(3 * numSquaresX / 4) + 0.5) * SQUARE_SIZE;
120+
let yNight = (getRandomIntInRange(0, numSquaresY - 1) + 0.5) * SQUARE_SIZE;
121+
let dxNight = getRandomSign();
122+
let dyNight = getRandomSign();
123+
124+
let dayScore = Math.floor(numSquaresX / 2) * numSquaresY;
125+
let nightScore = Math.ceil(numSquaresX / 2) * numSquaresY
126+
127+
let iteration = 0;
128+
129+
for (let x = 0; x < canvas.width; x++) {
130+
pixels[x] = [];
131+
}
132+
133+
function calculateDistance(p1x, p1y, p2x, p2y) {
134+
return Math.sqrt(Math.pow(p1x - p2x, 2) + Math.pow(p1y - p2y, 2))
135+
}
136+
137+
function ballIsInSquare(sx, sy, ballColor) {
138+
let xSquare = sx * SQUARE_SIZE;
139+
let ySquare = sy * SQUARE_SIZE;
110140

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;
141+
let xBall, yBall;
142+
if (ballColor === DAY_COLOR) {
143+
xBall = xDay;
144+
yBall = yDay;
145+
}
146+
else {
147+
xBall = xNight;
148+
yBall = yNight;
149+
}
150+
151+
// If this is the square --> this is the exclusion zone.
152+
// and ball has radius 2
153+
// ---bb----------------- ----------------------
154+
// --bbbb---------------- ----------------------
155+
// --bbbb---------------- --------######--------
156+
// ---bb----------------- -------########-------
157+
// ---------ssss--------- -------##ssss##-------
158+
// ---------ssss--------- -------##ssss##-------
159+
// ---------ssss--------- -------##ssss##-------
160+
// ---------ssss--------- -------##ssss##-------
161+
// ---------------------- -------########-------
162+
// ---------------------- --------######--------
163+
// ---------------------- ----------------------
164+
// ---------------------- ----------------------
165+
166+
const squareTopLeftX = xSquare;
167+
const squareTopLeftY = ySquare;
168+
const squareTopRightX = xSquare + SQUARE_SIZE;
169+
const squareTopRightY = ySquare;
170+
const squareBottomLeftX = xSquare;
171+
const squareBottomLeftY = ySquare + SQUARE_SIZE;
172+
const squareBottomRightX = xSquare + SQUARE_SIZE;
173+
const squareBottomRightY = ySquare + SQUARE_SIZE;
174+
175+
if (
176+
xBall >= xSquare - BALL_RADIUS
177+
&& xBall <= xSquare + SQUARE_SIZE + BALL_RADIUS
178+
&& yBall >= ySquare - BALL_RADIUS
179+
&& yBall <= ySquare + SQUARE_SIZE + BALL_RADIUS
180+
) {
181+
// ball is inside the exclusion zone
182+
const distanceToTopLeft = calculateDistance(xBall, yBall, squareTopLeftX, squareTopLeftY);
183+
const distanceToTopRight = calculateDistance(xBall, yBall, squareTopRightX, squareTopRightY);
184+
const distanceToBottomLeft = calculateDistance(xBall, yBall, squareBottomLeftX, squareBottomLeftY);
185+
const distanceToBottomRight = calculateDistance(xBall, yBall, squareBottomRightX, squareBottomRightY);
186+
187+
if (
188+
(xBall < squareTopLeftX && yBall < squareTopLeftY && distanceToTopLeft >= BALL_RADIUS)
189+
|| (xBall > squareTopRightX && yBall < squareTopRightY && distanceToTopRight >= BALL_RADIUS)
190+
|| (xBall < squareBottomLeftX && yBall > squareBottomLeftY && distanceToBottomLeft >= BALL_RADIUS)
191+
|| (xBall > squareBottomRightX && yBall > squareBottomRightY && distanceToBottomRight >= BALL_RADIUS)
192+
) {
193+
// ball is at corners of exclusion zone (which are not part of it)
194+
return false;
195+
}
196+
197+
// bass is inside exclusion zone and not at corners
198+
return true;
199+
}
200+
else {
201+
// ball is outside the exclusion zone
202+
return false;
115203
}
116204
}
117205

118-
let x1 = canvas.width / 4;
119-
let y1 = canvas.height / 2;
120-
let dx1 = 12.5;
121-
let dy1 = -12.5;
206+
function flipColorOfSquareThatPixelIsIn(x, y) {
207+
let currentColor = pixels[x][y];
208+
const isPointForNight = currentColor === DAY_COLOR;
122209

123-
let x2 = (canvas.width / 4) * 3;
124-
let y2 = canvas.height / 2;
125-
let dx2 = -12.5;
126-
let dy2 = 12.5;
210+
let newColor = isPointForNight ? NIGHT_COLOR : DAY_COLOR;
211+
let otherBallColor = isPointForNight ? DAY_COLOR : NIGHT_COLOR;
127212

128-
let iteration = 0;
213+
let {sx, sy} = getSquareThatPixelIsIn(x, y);
214+
215+
// Do not flip color of square if the other ball and the square overlap
216+
if (ballIsInSquare(sx, sy, otherBallColor)) {
217+
return;
218+
}
219+
220+
if (isPointForNight) {
221+
dayScore--;
222+
nightScore++;
223+
}
224+
else {
225+
dayScore++;
226+
nightScore--;
227+
}
228+
229+
colorPixelsInSquare(sx, sy, newColor);
230+
}
231+
232+
function getSquareThatPixelIsIn(pixelX, pixelY) {
233+
let sx = Math.floor(pixelX / SQUARE_SIZE);
234+
let sy = Math.floor(pixelY / SQUARE_SIZE);
235+
return {sx, sy}
236+
}
237+
238+
function colorPixelsInSquare(sx, sy, color) {
239+
for (let x = sx * SQUARE_SIZE; x < (sx + 1) * SQUARE_SIZE; x++) {
240+
for (let y = sy * SQUARE_SIZE; y < (sy + 1) * SQUARE_SIZE; y++) {
241+
pixels[x][y] = color;
242+
}
243+
}
244+
}
245+
246+
for (let x = 0; x < numSquaresX; x++) {
247+
for (let y = 0; y < numSquaresY; y++) {
248+
const color = x < Math.floor(numSquaresX / 2) ? DAY_COLOR : NIGHT_COLOR;
249+
colorPixelsInSquare(x, y, color)
250+
}
251+
}
252+
253+
function getRandomSign() {
254+
return Math.sign(Math.random() - 0.5);
255+
}
256+
257+
function getRandomIntInRange(start, end) {
258+
let randomFloatInRange = start + Math.random() * (end - start + 1);
259+
return Math.floor(randomFloatInRange);
260+
}
129261

130262
function drawBall(x, y, color) {
131263
ctx.beginPath();
132-
ctx.arc(x, y, SQUARE_SIZE / 2, 0, Math.PI * 2, false);
264+
ctx.arc(x, y, BALL_RADIUS, 0, Math.PI * 2, false);
133265
ctx.fillStyle = color;
134266
ctx.fill();
135267
ctx.closePath();
136268
}
137269

138270
function drawSquares() {
139-
for (let i = 0; i < numSquaresX; i++) {
140-
for (let j = 0; j < numSquaresY; j++) {
141-
ctx.fillStyle = squares[i][j];
271+
for (let i = 0; i < canvas.width; i += SQUARE_SIZE) {
272+
for (let j = 0; j < canvas.height; j += SQUARE_SIZE) {
273+
ctx.fillStyle = pixels[i][j];
142274
ctx.fillRect(
143-
i * SQUARE_SIZE,
144-
j * SQUARE_SIZE,
275+
i,
276+
j,
145277
SQUARE_SIZE,
146278
SQUARE_SIZE
147279
);
148280
}
149281
}
150282
}
151283

152-
function updateSquareAndBounce(x, y, dx, dy, color) {
153-
let updatedDx = dx;
154-
let updatedDy = dy;
284+
function updateScoreElement() {
285+
scoreElement.textContent = `day ${dayScore} | night ${nightScore}`;
286+
}
155287

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);
288+
function calculateNextStep(xOld, yOld, dxOld, dyOld, color) {
289+
let dxNew = dxOld;
290+
let dyNew = dyOld;
160291

161-
let i = Math.floor(checkX / SQUARE_SIZE);
162-
let j = Math.floor(checkY / SQUARE_SIZE);
292+
let horCanvasOut = false;
293+
let verCanvasOut = false;
163294

164-
if (i >= 0 && i < numSquaresX && j >= 0 && j < numSquaresY) {
165-
if (squares[i][j] !== color) {
166-
squares[i][j] = color;
295+
// possible vertical and horizontal collide points (CP)
296+
const horCpX = dxOld === 1 ? xOld + BALL_RADIUS : xOld - BALL_RADIUS - 1;
297+
const horCpY = yOld;
298+
const verCpX = xOld;
299+
const verCpY = dyOld === 1 ? yOld + BALL_RADIUS : yOld - BALL_RADIUS - 1;
167300

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-
}
301+
// possible diagonal collide point
302+
const diaCpX = xOld + Math.ceil((1 / Math.sqrt(2)) * BALL_RADIUS) * dxOld;
303+
const diaCpY = yOld + Math.ceil((1 / Math.sqrt(2)) * BALL_RADIUS) * dyOld;
304+
305+
// pixel horizontally out of canvas
306+
if (horCpX === -1 || horCpX === canvas.width) {
307+
dxNew = -dxNew;
308+
horCanvasOut = true;
176309
}
177310

178-
return { dx: updatedDx, dy: updatedDy };
179-
}
311+
// pixel vertically out of canvas
312+
if (verCpY === -1 || verCpY === canvas.height) {
313+
dyNew = -dyNew;
314+
verCanvasOut = true;
315+
}
180316

181-
function updateScoreElement() {
182-
let dayScore = 0;
183-
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) {
187-
dayScore++;
188-
} else if (squares[i][j] === NIGHT_COLOR) {
189-
nightScore++;
190-
}
191-
}
317+
// horizontal collision
318+
if (!horCanvasOut && pixels[horCpX][horCpY] === color) {
319+
// change horizontal direction
320+
dxNew = -dxNew;
321+
322+
// flip next square
323+
flipColorOfSquareThatPixelIsIn(horCpX, horCpY);
192324
}
193325

194-
scoreElement.textContent = `day ${dayScore} | night ${nightScore}`;
195-
}
326+
// vertical collision
327+
if (!verCanvasOut && pixels[verCpX][verCpY] === color) {
328+
// change vertical direction
329+
dyNew = -dyNew;
196330

197-
function checkBoundaryCollision(x, y, dx, dy) {
198-
if (x + dx > canvas.width - SQUARE_SIZE / 2 || x + dx < SQUARE_SIZE / 2) {
199-
dx = -dx;
331+
// flip next square
332+
flipColorOfSquareThatPixelIsIn(verCpX, verCpY);
200333
}
201-
if (
202-
y + dy > canvas.height - SQUARE_SIZE / 2 ||
203-
y + dy < SQUARE_SIZE / 2
204-
) {
205-
dy = -dy;
334+
335+
// diagonal collision (if ball radius is bigger 2)
336+
if (!horCanvasOut && !verCanvasOut && pixels[diaCpX][diaCpY] === color) {
337+
// change horizontal and vertical direction
338+
dxNew = -dxNew;
339+
dyNew = -dyNew;
340+
341+
// flip next square
342+
flipColorOfSquareThatPixelIsIn(diaCpX, diaCpY);
206343
}
207344

208-
return { dx: dx, dy: dy };
345+
let xNew = xOld + dxNew;
346+
let yNew = yOld + dyNew;
347+
348+
return {xNew, yNew, dxNew, dyNew};
209349
}
210350

211-
function draw() {
212-
ctx.clearRect(0, 0, canvas.width, canvas.height);
213-
drawSquares();
351+
function calculateNextStepDay() {
352+
let newDay = calculateNextStep(xDay, yDay, dxDay, dyDay, DAY_BALL_COLOR);
353+
xDay = newDay.xNew;
354+
yDay = newDay.yNew;
355+
dxDay = newDay.dxNew;
356+
dyDay = newDay.dyNew;
357+
}
358+
359+
function calculateNextStepNight() {
360+
let newNight = calculateNextStep(xNight, yNight, dxNight, dyNight, NIGHT_BALL_COLOR);
361+
xNight = newNight.xNew;
362+
yNight = newNight.yNew;
363+
dxNight = newNight.dxNew;
364+
dyNight = newNight.dyNew;
365+
}
214366

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;
367+
function calculateNextFrame() {
368+
let step = 0
369+
while (step < SPEED) {
370+
calculateNextStepDay();
371+
calculateNextStepNight();
219372

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;
373+
step++;
374+
}
375+
}
224376

225-
let boundary1 = checkBoundaryCollision(x1, y1, dx1, dy1);
226-
dx1 = boundary1.dx;
227-
dy1 = boundary1.dy;
377+
function draw() {
378+
ctx.clearRect(0, 0, canvas.width, canvas.height);
379+
drawSquares();
228380

229-
let boundary2 = checkBoundaryCollision(x2, y2, dx2, dy2);
230-
dx2 = boundary2.dx;
231-
dy2 = boundary2.dy;
381+
drawBall(xDay, yDay, DAY_BALL_COLOR);
382+
drawBall(xNight, yNight, NIGHT_BALL_COLOR);
232383

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

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

0 commit comments

Comments
 (0)