Skip to content

Commit

Permalink
Rewrite to use pixels instead squares in grid; Calculate all steps in…
Browse files Browse the repository at this point in the history
… between frames (fixes constant speed); Rewrite collision logic to check vertical, horizontal and diagonal collisions (fixes #6, #18, #19, #21, #22)
  • Loading branch information
schlagmichdoch committed Feb 9, 2024
1 parent d0c70cb commit 9519916
Showing 1 changed file with 235 additions and 87 deletions.
322 changes: 235 additions & 87 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

<body>
<div id="container">
<canvas id="pongCanvas" width="800" height="800"></canvas>
<canvas id="pongCanvas" width="1600" height="1600"></canvas>
<div id="score"></div>
<p id="made">
made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on
Expand Down Expand Up @@ -101,139 +101,287 @@
const NIGHT_COLOR = colorPalette.NocturnalExpedition;
const NIGHT_BALL_COLOR = colorPalette.MysticMint;

const SQUARE_SIZE = 25;
const SQUARE_SIZE = 50; // even integers only - must be factor of canvas size
const BALL_RADIUS = SQUARE_SIZE / 2;

const SPEED = 25;

const numSquaresX = canvas.width / SQUARE_SIZE;
const numSquaresY = canvas.height / SQUARE_SIZE;

let squares = [];
let pixels = [];

let xDay = (Math.floor(numSquaresX / 4) + 0.5) * SQUARE_SIZE;
let yDay = (getRandomIntInRange(0, numSquaresY - 1) + 0.5) * SQUARE_SIZE;
let dxDay = getRandomSign();
let dyDay = getRandomSign();

let xNight = (Math.ceil(3 * numSquaresX / 4) + 0.5) * SQUARE_SIZE;
let yNight = (getRandomIntInRange(0, numSquaresY - 1) + 0.5) * SQUARE_SIZE;
let dxNight = getRandomSign();
let dyNight = getRandomSign();

let dayScore = Math.floor(numSquaresX / 2) * numSquaresY;
let nightScore = Math.ceil(numSquaresX / 2) * numSquaresY

let iteration = 0;

for (let x = 0; x < canvas.width; x++) {
pixels[x] = [];
}

function calculateDistance(p1x, p1y, p2x, p2y) {
return Math.sqrt(Math.pow(p1x - p2x, 2) + Math.pow(p1y - p2y, 2))
}

function ballIsInSquare(sx, sy, ballColor) {
let xSquare = sx * SQUARE_SIZE;
let ySquare = sy * SQUARE_SIZE;

for (let i = 0; i < numSquaresX; i++) {
squares[i] = [];
for (let j = 0; j < numSquaresY; j++) {
squares[i][j] = i < numSquaresX / 2 ? DAY_COLOR : NIGHT_COLOR;
let xBall, yBall;
if (ballColor === DAY_COLOR) {
xBall = xDay;
yBall = yDay;
}
else {
xBall = xNight;
yBall = yNight;
}

// If this is the square --> this is the exclusion zone.
// and ball has radius 2
// ---bb----------------- ----------------------
// --bbbb---------------- ----------------------
// --bbbb---------------- --------######--------
// ---bb----------------- -------########-------
// ---------ssss--------- -------##ssss##-------
// ---------ssss--------- -------##ssss##-------
// ---------ssss--------- -------##ssss##-------
// ---------ssss--------- -------##ssss##-------
// ---------------------- -------########-------
// ---------------------- --------######--------
// ---------------------- ----------------------
// ---------------------- ----------------------

const squareTopLeftX = xSquare;
const squareTopLeftY = ySquare;
const squareTopRightX = xSquare + SQUARE_SIZE;
const squareTopRightY = ySquare;
const squareBottomLeftX = xSquare;
const squareBottomLeftY = ySquare + SQUARE_SIZE;
const squareBottomRightX = xSquare + SQUARE_SIZE;
const squareBottomRightY = ySquare + SQUARE_SIZE;

if (
xBall >= xSquare - BALL_RADIUS
&& xBall <= xSquare + SQUARE_SIZE + BALL_RADIUS
&& yBall >= ySquare - BALL_RADIUS
&& yBall <= ySquare + SQUARE_SIZE + BALL_RADIUS
) {
// ball is inside the exclusion zone
const distanceToTopLeft = calculateDistance(xBall, yBall, squareTopLeftX, squareTopLeftY);
const distanceToTopRight = calculateDistance(xBall, yBall, squareTopRightX, squareTopRightY);
const distanceToBottomLeft = calculateDistance(xBall, yBall, squareBottomLeftX, squareBottomLeftY);
const distanceToBottomRight = calculateDistance(xBall, yBall, squareBottomRightX, squareBottomRightY);

if (
(xBall < squareTopLeftX && yBall < squareTopLeftY && distanceToTopLeft >= BALL_RADIUS)
|| (xBall > squareTopRightX && yBall < squareTopRightY && distanceToTopRight >= BALL_RADIUS)
|| (xBall < squareBottomLeftX && yBall > squareBottomLeftY && distanceToBottomLeft >= BALL_RADIUS)
|| (xBall > squareBottomRightX && yBall > squareBottomRightY && distanceToBottomRight >= BALL_RADIUS)
) {
// ball is at corners of exclusion zone (which are not part of it)
return false;
}

// bass is inside exclusion zone and not at corners
return true;
}
else {
// ball is outside the exclusion zone
return false;
}
}

let x1 = canvas.width / 4;
let y1 = canvas.height / 2;
let dx1 = 12.5;
let dy1 = -12.5;
function flipColorOfSquareThatPixelIsIn(x, y) {
let currentColor = pixels[x][y];
const isPointForNight = currentColor === DAY_COLOR;

let x2 = (canvas.width / 4) * 3;
let y2 = canvas.height / 2;
let dx2 = -12.5;
let dy2 = 12.5;
let newColor = isPointForNight ? NIGHT_COLOR : DAY_COLOR;
let otherBallColor = isPointForNight ? DAY_COLOR : NIGHT_COLOR;

let iteration = 0;
let {sx, sy} = getSquareThatPixelIsIn(x, y);

// Do not flip color of square if the other ball and the square overlap
if (ballIsInSquare(sx, sy, otherBallColor)) {
return;
}

if (isPointForNight) {
dayScore--;
nightScore++;
}
else {
dayScore++;
nightScore--;
}

colorPixelsInSquare(sx, sy, newColor);
}

function getSquareThatPixelIsIn(pixelX, pixelY) {
let sx = Math.floor(pixelX / SQUARE_SIZE);
let sy = Math.floor(pixelY / SQUARE_SIZE);
return {sx, sy}
}

function colorPixelsInSquare(sx, sy, color) {
for (let x = sx * SQUARE_SIZE; x < (sx + 1) * SQUARE_SIZE; x++) {
for (let y = sy * SQUARE_SIZE; y < (sy + 1) * SQUARE_SIZE; y++) {
pixels[x][y] = color;
}
}
}

for (let x = 0; x < numSquaresX; x++) {
for (let y = 0; y < numSquaresY; y++) {
const color = x < Math.floor(numSquaresX / 2) ? DAY_COLOR : NIGHT_COLOR;
colorPixelsInSquare(x, y, color)
}
}

function getRandomSign() {
return Math.sign(Math.random() - 0.5);
}

function getRandomIntInRange(start, end) {
let randomFloatInRange = start + Math.random() * (end - start + 1);
return Math.floor(randomFloatInRange);
}

function drawBall(x, y, color) {
ctx.beginPath();
ctx.arc(x, y, SQUARE_SIZE / 2, 0, Math.PI * 2, false);
ctx.arc(x, y, BALL_RADIUS, 0, Math.PI * 2, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}

function drawSquares() {
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
ctx.fillStyle = squares[i][j];
for (let i = 0; i < canvas.width; i += SQUARE_SIZE) {
for (let j = 0; j < canvas.height; j += SQUARE_SIZE) {
ctx.fillStyle = pixels[i][j];
ctx.fillRect(
i * SQUARE_SIZE,
j * SQUARE_SIZE,
i,
j,
SQUARE_SIZE,
SQUARE_SIZE
);
}
}
}

function updateSquareAndBounce(x, y, dx, dy, color) {
let updatedDx = dx;
let updatedDy = dy;
function updateScoreElement() {
scoreElement.textContent = `day ${dayScore} | night ${nightScore}`;
}

// Check multiple points around the ball's circumference
for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) {
let checkX = x + Math.cos(angle) * (SQUARE_SIZE / 2);
let checkY = y + Math.sin(angle) * (SQUARE_SIZE / 2);
function calculateNextStep(xOld, yOld, dxOld, dyOld, color) {
let dxNew = dxOld;
let dyNew = dyOld;

let i = Math.floor(checkX / SQUARE_SIZE);
let j = Math.floor(checkY / SQUARE_SIZE);
let horCanvasOut = false;
let verCanvasOut = false;

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

// Determine bounce direction based on the angle
if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) {
updatedDx = -updatedDx;
} else {
updatedDy = -updatedDy;
}
}
}
// possible diagonal collide point
const diaCpX = xOld + Math.ceil((1 / Math.sqrt(2)) * BALL_RADIUS) * dxOld;
const diaCpY = yOld + Math.ceil((1 / Math.sqrt(2)) * BALL_RADIUS) * dyOld;

// pixel horizontally out of canvas
if (horCpX === -1 || horCpX === canvas.width) {
dxNew = -dxNew;
horCanvasOut = true;
}

return { dx: updatedDx, dy: updatedDy };
}
// pixel vertically out of canvas
if (verCpY === -1 || verCpY === canvas.height) {
dyNew = -dyNew;
verCanvasOut = true;
}

function updateScoreElement() {
let dayScore = 0;
let nightScore = 0;
for (let i = 0; i < numSquaresX; i++) {
for (let j = 0; j < numSquaresY; j++) {
if (squares[i][j] === DAY_COLOR) {
dayScore++;
} else if (squares[i][j] === NIGHT_COLOR) {
nightScore++;
}
}
// horizontal collision
if (!horCanvasOut && pixels[horCpX][horCpY] === color) {
// change horizontal direction
dxNew = -dxNew;

// flip next square
flipColorOfSquareThatPixelIsIn(horCpX, horCpY);
}

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

function checkBoundaryCollision(x, y, dx, dy) {
if (x + dx > canvas.width - SQUARE_SIZE / 2 || x + dx < SQUARE_SIZE / 2) {
dx = -dx;
// flip next square
flipColorOfSquareThatPixelIsIn(verCpX, verCpY);
}
if (
y + dy > canvas.height - SQUARE_SIZE / 2 ||
y + dy < SQUARE_SIZE / 2
) {
dy = -dy;

// diagonal collision (if ball radius is bigger 2)
if (!horCanvasOut && !verCanvasOut && pixels[diaCpX][diaCpY] === color) {
// change horizontal and vertical direction
dxNew = -dxNew;
dyNew = -dyNew;

// flip next square
flipColorOfSquareThatPixelIsIn(diaCpX, diaCpY);
}

return { dx: dx, dy: dy };
let xNew = xOld + dxNew;
let yNew = yOld + dyNew;

return {xNew, yNew, dxNew, dyNew};
}

function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquares();
function calculateNextStepDay() {
let newDay = calculateNextStep(xDay, yDay, dxDay, dyDay, DAY_BALL_COLOR);
xDay = newDay.xNew;
yDay = newDay.yNew;
dxDay = newDay.dxNew;
dyDay = newDay.dyNew;
}

function calculateNextStepNight() {
let newNight = calculateNextStep(xNight, yNight, dxNight, dyNight, NIGHT_BALL_COLOR);
xNight = newNight.xNew;
yNight = newNight.yNew;
dxNight = newNight.dxNew;
dyNight = newNight.dyNew;
}

drawBall(x1, y1, DAY_BALL_COLOR);
let bounce1 = updateSquareAndBounce(x1, y1, dx1, dy1, DAY_COLOR);
dx1 = bounce1.dx;
dy1 = bounce1.dy;
function calculateNextFrame() {
let step = 0
while (step < SPEED) {
calculateNextStepDay();
calculateNextStepNight();

drawBall(x2, y2, NIGHT_BALL_COLOR);
let bounce2 = updateSquareAndBounce(x2, y2, dx2, dy2, NIGHT_COLOR);
dx2 = bounce2.dx;
dy2 = bounce2.dy;
step++;
}
}

let boundary1 = checkBoundaryCollision(x1, y1, dx1, dy1);
dx1 = boundary1.dx;
dy1 = boundary1.dy;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSquares();

let boundary2 = checkBoundaryCollision(x2, y2, dx2, dy2);
dx2 = boundary2.dx;
dy2 = boundary2.dy;
drawBall(xDay, yDay, DAY_BALL_COLOR);
drawBall(xNight, yNight, NIGHT_BALL_COLOR);

x1 += dx1;
y1 += dy1;
x2 += dx2;
y2 += dy2;
calculateNextFrame();

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

0 comments on commit 9519916

Please sign in to comment.