|
69 | 69 |
|
70 | 70 | <body>
|
71 | 71 | <div id="container">
|
72 |
| - <canvas id="pongCanvas" width="800" height="800"></canvas> |
| 72 | + <canvas id="pongCanvas" width="832" height="832"></canvas> |
73 | 73 | <div id="score"></div>
|
74 | 74 | <p id="made">
|
75 | 75 | made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on
|
|
101 | 101 | const NIGHT_COLOR = colorPalette.NocturnalExpedition;
|
102 | 102 | const NIGHT_BALL_COLOR = colorPalette.MysticMint;
|
103 | 103 |
|
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; |
105 | 108 |
|
106 | 109 | const numSquaresX = canvas.width / SQUARE_SIZE;
|
107 | 110 | const numSquaresY = canvas.height / SQUARE_SIZE;
|
108 | 111 |
|
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 | + } |
110 | 181 |
|
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; |
115 | 188 | }
|
116 | 189 | }
|
117 | 190 |
|
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); |
122 | 197 |
|
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(); |
127 | 245 |
|
128 | 246 | let iteration = 0;
|
129 | 247 |
|
130 | 248 | function drawBall(x, y, color) {
|
131 | 249 | 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); |
133 | 251 | ctx.fillStyle = color;
|
134 | 252 | ctx.fill();
|
135 | 253 | ctx.closePath();
|
136 | 254 | }
|
137 | 255 |
|
138 | 256 | 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]; |
142 | 260 | ctx.fillRect(
|
143 |
| - i * SQUARE_SIZE, |
144 |
| - j * SQUARE_SIZE, |
| 261 | + i, |
| 262 | + j, |
145 | 263 | SQUARE_SIZE,
|
146 | 264 | SQUARE_SIZE
|
147 | 265 | );
|
148 | 266 | }
|
149 | 267 | }
|
150 | 268 | }
|
151 | 269 |
|
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 |
| - |
181 | 270 | function updateScoreElement() {
|
182 | 271 | let dayScore = 0;
|
183 | 272 | 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) { |
187 | 276 | dayScore++;
|
188 |
| - } else if (squares[i][j] === NIGHT_COLOR) { |
| 277 | + } else if (pixels[i][j] === NIGHT_COLOR) { |
189 | 278 | nightScore++;
|
190 | 279 | }
|
191 | 280 | }
|
|
194 | 283 | scoreElement.textContent = `day ${dayScore} | night ${nightScore}`;
|
195 | 284 | }
|
196 | 285 |
|
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; |
200 | 316 | }
|
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; |
206 | 322 | }
|
207 | 323 |
|
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 | + } |
209 | 377 | }
|
210 | 378 |
|
211 | 379 | function draw() {
|
212 | 380 | ctx.clearRect(0, 0, canvas.width, canvas.height);
|
213 | 381 | drawSquares();
|
214 | 382 |
|
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); |
232 | 385 |
|
233 |
| - x1 += dx1; |
234 |
| - y1 += dy1; |
235 |
| - x2 += dx2; |
236 |
| - y2 += dy2; |
| 386 | + calculateNextFrame(); |
237 | 387 |
|
238 | 388 | iteration++;
|
239 | 389 | if (iteration % 1_000 === 0) console.log("iteration", iteration);
|
|
0 commit comments