2024-01-29 16:35:02 -05:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
2024-01-30 15:35:49 -05:00
|
|
|
<head>
|
|
|
|
<title>Pong wars | Koen van Gilst</title>
|
|
|
|
<link rel="canonical" href="https://pong-wars.koenvangilst.nl/" />
|
|
|
|
<style>
|
|
|
|
html {
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
body {
|
|
|
|
height: 100%;
|
|
|
|
margin: 0;
|
|
|
|
padding: 0;
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
background: linear-gradient(to bottom, #172b36 0%, #d9e8e3 100%);
|
|
|
|
}
|
|
|
|
|
|
|
|
#container {
|
|
|
|
display: flex;
|
|
|
|
width: 100%;
|
2024-01-30 19:04:39 -05:00
|
|
|
max-width: 1000px;
|
2024-01-30 15:35:49 -05:00
|
|
|
align-items: center;
|
|
|
|
flex-direction: column;
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
#pongCanvas {
|
|
|
|
display: block;
|
|
|
|
border-radius: 4px;
|
|
|
|
overflow: hidden;
|
|
|
|
width: 100%;
|
|
|
|
margin-top: auto;
|
|
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
|
|
|
|
}
|
|
|
|
|
|
|
|
#score {
|
|
|
|
font-family: monospace;
|
|
|
|
margin-top: 30px;
|
|
|
|
font-size: 20px;
|
|
|
|
padding-left: 20px;
|
|
|
|
color: #172b36;
|
|
|
|
}
|
|
|
|
|
|
|
|
#made {
|
|
|
|
font-family: monospace;
|
|
|
|
margin-top: auto;
|
|
|
|
margin-bottom: 20px;
|
|
|
|
font-size: 12px;
|
|
|
|
padding-left: 20px;
|
|
|
|
}
|
|
|
|
|
2024-02-15 18:48:21 -05:00
|
|
|
#instr {
|
|
|
|
font-family: monospace;
|
|
|
|
font-size: 12px;
|
|
|
|
padding-left: 20px;
|
|
|
|
}
|
|
|
|
|
2024-01-30 15:35:49 -05:00
|
|
|
#made a {
|
|
|
|
color: #172b36;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
|
|
|
|
<body>
|
|
|
|
<div id="container">
|
2024-01-30 19:04:39 -05:00
|
|
|
<canvas id="pongCanvas" width="1200" height="800"></canvas>
|
2024-01-30 15:35:49 -05:00
|
|
|
<div id="score"></div>
|
2024-02-15 18:48:21 -05:00
|
|
|
<p id="instr">
|
|
|
|
balls can be controlled with number keys
|
|
|
|
</p>
|
2024-01-30 15:35:49 -05:00
|
|
|
<p id="made">
|
|
|
|
made by <a href="https://koenvangilst.nl">Koen van Gilst</a> | source on
|
|
|
|
<a href="https://github.com/vnglst/pong-wars">github</a>
|
2024-02-15 18:48:21 -05:00
|
|
|
<br>
|
|
|
|
patches from dogeystamp | patched source on
|
|
|
|
<a href="https://github.com/dogeystamp/garbage-monorepo/tree/main/pongwars">github</a>
|
2024-01-30 15:35:49 -05:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</body>
|
|
|
|
|
|
|
|
<script>
|
2024-02-17 19:42:58 -05:00
|
|
|
// Based on: https://github.com/vnglst/pong-wars
|
|
|
|
// Idea for Pong wars: https://twitter.com/nicolasdnl/status/1749715070928433161
|
|
|
|
|
|
|
|
// This code is patched: see https://github.com/dogeystamp/garbage-monorepo/tree/main/pongwars
|
|
|
|
// Main features added are controlling balls and colors being able to die
|
|
|
|
|
|
|
|
const canvas = document.getElementById("pongCanvas");
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
const scoreElement = document.getElementById("score");
|
|
|
|
|
|
|
|
var suddenDeathStart = null;
|
|
|
|
var suddenDeathCoeff = 0;
|
|
|
|
|
|
|
|
const teams = [
|
|
|
|
{
|
|
|
|
name: "red",
|
|
|
|
color: "indianred",
|
|
|
|
backgroundColor: "darkred",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "blue",
|
|
|
|
color: "blue",
|
|
|
|
backgroundColor: "darkblue",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "green",
|
|
|
|
color: "green",
|
|
|
|
backgroundColor: "darkgreen",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "orange",
|
|
|
|
color: "coral",
|
|
|
|
backgroundColor: "chocolate",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "white",
|
|
|
|
color: "white",
|
|
|
|
backgroundColor: "gainsboro",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "black",
|
|
|
|
color: "#333333",
|
|
|
|
backgroundColor: "black",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ourple",
|
|
|
|
color: "violet",
|
|
|
|
backgroundColor: "purple",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "gray",
|
|
|
|
color: "gray",
|
|
|
|
backgroundColor: "#333333",
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
var state = {}
|
|
|
|
|
|
|
|
const squareSize = 16;
|
|
|
|
const numSquaresX = canvas.width / squareSize;
|
|
|
|
const numSquaresY = canvas.height / squareSize;
|
|
|
|
let squares = [];
|
2024-01-29 16:35:02 -05:00
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
// matrix of "timestamps" where each square was claimed
|
|
|
|
let squareTaken = [];
|
|
|
|
var squareTime = 0;
|
|
|
|
|
2024-01-29 16:35:02 -05:00
|
|
|
document.addEventListener("keydown", (event) => {
|
|
|
|
const key = parseInt(event.key)
|
|
|
|
if (isNaN(key)) return
|
|
|
|
state[key].boostEnabled = true;
|
|
|
|
})
|
|
|
|
document.addEventListener("keyup", (event) => {
|
|
|
|
const key = parseInt(event.key)
|
|
|
|
if (isNaN(key)) return
|
|
|
|
state[key].boostEnabled = false;
|
|
|
|
})
|
|
|
|
|
2024-02-17 19:42:58 -05:00
|
|
|
function initialState() {
|
2024-02-20 19:09:17 -05:00
|
|
|
state = teams.map((team) => ({
|
|
|
|
elim: false,
|
|
|
|
boostEnabled: false,
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
dx: 0,
|
|
|
|
dy: 0,
|
|
|
|
score: 0,
|
2024-02-23 20:38:03 -05:00
|
|
|
// how many consecutive contested tiles it has hit
|
|
|
|
conflict: 0,
|
2024-02-20 19:09:17 -05:00
|
|
|
}))
|
|
|
|
|
2024-02-17 19:42:58 -05:00
|
|
|
// base noise pattern (failsafe in case the grid doesn't cover some part)
|
2024-01-30 15:35:49 -05:00
|
|
|
for (let i = 0; i < numSquaresX; i++) {
|
|
|
|
squares[i] = [];
|
2024-02-23 20:38:03 -05:00
|
|
|
squareTaken[i] = [];
|
2024-01-30 15:35:49 -05:00
|
|
|
for (let j = 0; j < numSquaresY; j++) {
|
2024-02-15 19:21:32 -05:00
|
|
|
const t = randInt(0, teams.length-1);
|
2024-01-30 15:35:49 -05:00
|
|
|
squares[i][j] = t;
|
2024-02-23 20:38:03 -05:00
|
|
|
squareTaken[i][j] = squareTime;
|
2024-01-30 15:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
2024-01-29 16:35:02 -05:00
|
|
|
|
2024-02-17 19:42:58 -05:00
|
|
|
const enableGrid = true;
|
|
|
|
if (enableGrid) {
|
|
|
|
const gridW = 4;
|
|
|
|
const gridH = 2;
|
|
|
|
for (let i = 0; i < gridW; i++) {
|
|
|
|
for (let j = 0; j < gridH; j++) {
|
|
|
|
const gridIdx = j * gridW + i;
|
|
|
|
const t = (gridIdx < teams.length) ? gridIdx : null;
|
|
|
|
|
|
|
|
for (let x = Math.floor(i/gridW*numSquaresX); x < Math.floor((i+1)/gridW*numSquaresX); x++) {
|
|
|
|
for (let y = Math.floor(j/gridH*numSquaresY); y < Math.floor((j+1)/gridH*numSquaresY); y++) {
|
|
|
|
if (t !== null) {
|
|
|
|
squares[x][y] = t;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (t !== null) {
|
2024-02-20 19:09:17 -05:00
|
|
|
state[t].x = Math.floor((i+0.5)/gridW*canvas.width);
|
|
|
|
state[t].y = Math.floor((j+0.5)/gridH*canvas.height);
|
2024-02-17 19:42:58 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < teams.length; i++) {
|
2024-02-17 19:48:16 -05:00
|
|
|
const angle = randomNum(0, 2 * Math.PI);
|
2024-02-20 19:09:17 -05:00
|
|
|
state[i].dx = 8 * Math.cos(angle);
|
|
|
|
state[i].dy = 8 * Math.sin(angle);
|
2024-02-17 19:42:58 -05:00
|
|
|
}
|
2024-01-30 15:35:49 -05:00
|
|
|
}
|
2024-02-17 19:42:58 -05:00
|
|
|
}
|
|
|
|
initialState();
|
|
|
|
|
|
|
|
function randomNum(min, max) {
|
|
|
|
return Math.random() * (max - min) + min;
|
|
|
|
}
|
|
|
|
|
2024-01-29 16:35:02 -05:00
|
|
|
function randInt(min, max) {
|
|
|
|
return Math.floor(randomNum(min, max+1))
|
|
|
|
}
|
|
|
|
|
|
|
|
function elapsedSec() {
|
2024-02-17 18:14:07 -05:00
|
|
|
return ((new Date()) - suddenDeathStart)/1000
|
2024-01-29 16:35:02 -05:00
|
|
|
}
|
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
updateScoreElement();
|
2024-01-30 15:35:49 -05:00
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
function drawBall(x, y, color) {
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.arc(x, y, squareSize / 2, 0, Math.PI * 2, false);
|
|
|
|
ctx.fillStyle = color;
|
|
|
|
ctx.fill();
|
|
|
|
ctx.closePath();
|
|
|
|
}
|
2024-01-30 15:35:49 -05:00
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
function drawSquares() {
|
|
|
|
for (let i = 0; i < numSquaresX; i++) {
|
|
|
|
for (let j = 0; j < numSquaresY; j++) {
|
|
|
|
ctx.fillStyle = teams[squares[i][j]].backgroundColor;
|
|
|
|
ctx.fillRect(i * squareSize, j * squareSize, squareSize, squareSize);
|
2024-01-30 15:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
2024-02-23 20:38:03 -05:00
|
|
|
}
|
2024-01-29 16:35:02 -05:00
|
|
|
|
|
|
|
function clamp(min, max, num) {
|
|
|
|
return Math.min(max, Math.max(min, num))
|
|
|
|
}
|
|
|
|
|
|
|
|
function mix(x, y, v) {
|
|
|
|
return v*x + (1-v)*y;
|
|
|
|
}
|
|
|
|
|
2024-02-17 18:14:07 -05:00
|
|
|
var nTeams = teams.length;
|
2024-02-29 21:37:31 -05:00
|
|
|
// NTeams but with ease in to avoid instant deaths
|
|
|
|
var smoothNTeams = nTeams;
|
2024-02-17 18:14:07 -05:00
|
|
|
|
2024-01-30 15:35:49 -05:00
|
|
|
function updateSquareAndBounce(x, y, dx, dy, color) {
|
|
|
|
if (state[color].elim) return
|
|
|
|
if (Math.max(Math.abs(dx), Math.abs(dy)) < 0.02) state[color].elim = true
|
|
|
|
|
|
|
|
if (Math.min(x, y) < 0 || isNaN(x) || isNaN(y)) {
|
2024-01-30 19:04:39 -05:00
|
|
|
console.warn(`warped ${teams[color].name}`)
|
2024-02-20 19:09:17 -05:00
|
|
|
state[color].x = canvas.width * 0.1;
|
|
|
|
state[color].y = canvas.height * 0.1;
|
2024-01-30 15:35:49 -05:00
|
|
|
dx = 4;
|
|
|
|
dy = 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (x > canvas.width || y > canvas.height) {
|
2024-01-30 19:04:39 -05:00
|
|
|
console.warn(`warped ${teams[color].name}`)
|
2024-02-20 19:09:17 -05:00
|
|
|
state[color].x = canvas.width * 0.9;
|
|
|
|
state[color].y = canvas.height * 0.9;
|
2024-01-30 15:35:49 -05:00
|
|
|
dx = -4;
|
|
|
|
dy = -4;
|
|
|
|
}
|
|
|
|
|
2024-02-17 14:51:42 -05:00
|
|
|
// death coefficient (if not enough territory, slow down)
|
2024-02-29 21:37:31 -05:00
|
|
|
var coeff = Math.min((state[color].score/(numSquaresX*numSquaresY/smoothNTeams/mix(1.2, 4, suddenDeathCoeff))), 1.00);
|
|
|
|
|
|
|
|
// winners don't slow down
|
|
|
|
if (nTeams === 1) coeff = 1;
|
2024-02-17 14:51:42 -05:00
|
|
|
|
|
|
|
// death coefficient when no collision
|
|
|
|
const vacuumCoeff = coeff**(0.01);
|
|
|
|
|
|
|
|
let updatedDx = dx * vacuumCoeff;
|
|
|
|
let updatedDy = dy * vacuumCoeff;
|
2024-01-30 15:35:49 -05:00
|
|
|
|
|
|
|
// 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) * (squareSize / 2);
|
|
|
|
let checkY = y + Math.sin(angle) * (squareSize / 2);
|
|
|
|
|
|
|
|
let i = Math.floor(checkX / squareSize);
|
|
|
|
let j = Math.floor(checkY / squareSize);
|
|
|
|
|
|
|
|
if (i >= 0 && i < numSquaresX && j >= 0 && j < numSquaresY) {
|
|
|
|
if (squares[i][j] !== color) {
|
2024-01-30 17:02:41 -05:00
|
|
|
const foreign = squares[i][j];
|
2024-02-23 20:38:03 -05:00
|
|
|
if (!state[foreign].elim) {
|
|
|
|
// in the event that the ball gets entangled with another on a contested square really long,
|
|
|
|
// have them duel and one of them will win (the one with more territory is more likely to win)
|
|
|
|
if (squareTime == squareTaken[i][j]) {
|
|
|
|
state[color].conflict++;
|
|
|
|
if (state[color].conflict >= 100 && randInt(0, 1) == 1) {
|
|
|
|
const ratio = state[color].score / (state[foreign].score + state[color].score);
|
|
|
|
console.log(`${teams[color].name} and ${teams[foreign].name} start a duel (territory ratio ${ratio.toFixed(2)})`);
|
|
|
|
const val = Math.random();
|
|
|
|
if (val == ratio) {
|
|
|
|
console.log(`nobody wins the duel`);
|
|
|
|
} else if (val < ratio) {
|
|
|
|
console.log(`${teams[color].name} vanquishes ${teams[foreign].name}`);
|
|
|
|
state[foreign].elim = true;
|
|
|
|
} else if (val > ratio) {
|
|
|
|
console.log(`${teams[foreign].name} vanquishes ${teams[color].name}`);
|
|
|
|
state[color].elim = true;
|
|
|
|
}
|
|
|
|
state[color].conflict = 0;
|
|
|
|
state[foreign].conflict = 0;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
state[color].conflict = 0;
|
|
|
|
}
|
|
|
|
}
|
2024-01-30 17:02:41 -05:00
|
|
|
if (state[foreign].elim) {
|
2024-01-30 19:08:16 -05:00
|
|
|
console.log(`${teams[color].name} claims the remaining territory of ${teams[foreign].name}`)
|
2024-01-30 17:02:41 -05:00
|
|
|
for (let ai = 0; ai < numSquaresX; ai++) {
|
|
|
|
for (let aj = 0; aj < numSquaresY; aj++) {
|
|
|
|
if (squares[ai][aj] == foreign) squares[ai][aj] = color;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
squareTaken[i][j] = squareTime;
|
2024-01-30 15:35:49 -05:00
|
|
|
squares[i][j] = color;
|
|
|
|
|
|
|
|
// Determine bounce direction based on the angle
|
|
|
|
if (Math.abs(Math.cos(angle)) > Math.abs(Math.sin(angle))) {
|
|
|
|
updatedDx = -updatedDx;
|
|
|
|
} else {
|
|
|
|
updatedDy = -updatedDy;
|
|
|
|
}
|
|
|
|
|
|
|
|
updatedDx += randomNum(-0.15, mix(0.153, 0.15, suddenDeathCoeff));
|
|
|
|
updatedDy += randomNum(-0.15, 0.15);
|
|
|
|
updatedDx *= coeff;
|
|
|
|
updatedDy *= coeff;
|
2024-02-15 18:48:21 -05:00
|
|
|
const speedLim = mix(30, 13, suddenDeathCoeff)
|
2024-01-30 15:35:49 -05:00
|
|
|
const norm = (updatedDx**2 + updatedDy**2)**(1/2)
|
2024-01-30 19:04:39 -05:00
|
|
|
const scalar = Math.min(speedLim/norm, 1)
|
2024-01-30 15:35:49 -05:00
|
|
|
updatedDx *= scalar;
|
|
|
|
updatedDy *= scalar;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const theta = (Math.PI/180)*10;
|
|
|
|
const ct = Math.cos(theta);
|
|
|
|
const st = Math.sin(theta);
|
|
|
|
if (state[color].boostEnabled) {
|
|
|
|
const rotDx = (ct * updatedDx - st * updatedDy);
|
|
|
|
const rotDy = (st * updatedDx + ct * updatedDy);
|
|
|
|
updatedDx = rotDx;
|
|
|
|
updatedDy = rotDy;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { dx: updatedDx, dy: updatedDy };
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateScoreElement() {
|
|
|
|
if (!squares) {
|
|
|
|
return;
|
|
|
|
}
|
2024-02-20 19:09:17 -05:00
|
|
|
state.forEach((t) => (t.score = 0));
|
2024-01-30 15:35:49 -05:00
|
|
|
for (let i = 0; i < numSquaresX; i++) {
|
|
|
|
for (let j = 0; j < numSquaresY; j++) {
|
2024-02-20 19:09:17 -05:00
|
|
|
state[squares[i][j]].score++;
|
2024-01-30 15:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let i = 0; i < teams.length; i++) {
|
2024-02-20 19:09:17 -05:00
|
|
|
if (state[i].score <= 4) {
|
2024-01-30 15:35:49 -05:00
|
|
|
state[i].elim = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-17 18:14:07 -05:00
|
|
|
if (nTeams > 1) {
|
|
|
|
scoreElement.textContent = teams
|
2024-02-20 19:09:17 -05:00
|
|
|
.map((t, idx) => `(${idx}) ${t.name} ${state[idx].score}`)
|
2024-02-17 18:14:07 -05:00
|
|
|
.filter((t, idx) => !state[idx].elim)
|
|
|
|
.join(" | ") + (suddenDeathCoeff > 0.1 ? " | sudden death!" : "");
|
|
|
|
} else {
|
|
|
|
scoreElement.textContent = teams
|
|
|
|
.filter((t, idx) => !state[idx].elim)
|
|
|
|
.map((t) => `${t.name} 👑 wins`)
|
|
|
|
}
|
2024-01-30 15:35:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function draw() {
|
2024-02-29 21:37:31 -05:00
|
|
|
nTeams = state.map(t => !t.elim).filter(Boolean).length;
|
|
|
|
|
|
|
|
// practically inertia (0-1)
|
|
|
|
const smoothCoeff = 0.999;
|
|
|
|
|
|
|
|
smoothNTeams = smoothNTeams * (smoothCoeff) + nTeams * (1 - smoothCoeff);
|
|
|
|
if (isNaN(smoothNTeams)) smoothNTeams = nTeams;
|
|
|
|
|
2024-01-30 15:35:49 -05:00
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
drawSquares();
|
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
squareTime++;
|
|
|
|
squareTime %= 100000;
|
|
|
|
|
2024-01-30 15:35:49 -05:00
|
|
|
for (let i = 0; i < teams.length; i++) {
|
2024-02-23 20:38:03 -05:00
|
|
|
if (state[i].elim) continue
|
2024-02-20 19:09:17 -05:00
|
|
|
const t = state[i];
|
|
|
|
drawBall(t.x, t.y, teams[i].color);
|
2024-01-30 15:35:49 -05:00
|
|
|
let bounce = updateSquareAndBounce(t.x, t.y, t.dx, t.dy, i);
|
|
|
|
t.dx = bounce.dx;
|
|
|
|
t.dy = bounce.dy;
|
|
|
|
|
|
|
|
if (
|
|
|
|
t.x + t.dx > canvas.width - squareSize / 2 ||
|
|
|
|
t.x + t.dx < squareSize / 2
|
|
|
|
) {
|
|
|
|
t.dx = -t.dx;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
t.y + t.dy > canvas.height - squareSize / 2 ||
|
|
|
|
t.y + t.dy < squareSize / 2
|
|
|
|
) {
|
|
|
|
t.dy = -t.dy;
|
|
|
|
}
|
|
|
|
|
|
|
|
t.x += t.dx;
|
|
|
|
t.y += t.dy;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateScoreElement();
|
2024-02-17 18:14:07 -05:00
|
|
|
if (suddenDeathStart !== null) {
|
|
|
|
suddenDeathCoeff = Math.min((elapsedSec()/60/5)**15, 1);
|
|
|
|
} else if (nTeams <= 3) {
|
|
|
|
suddenDeathStart = new Date();
|
|
|
|
}
|
2024-01-30 15:35:49 -05:00
|
|
|
requestAnimationFrame(draw);
|
|
|
|
}
|
|
|
|
|
2024-02-23 20:38:03 -05:00
|
|
|
requestAnimationFrame(draw);
|
2024-01-30 15:35:49 -05:00
|
|
|
</script>
|
2024-01-29 16:35:02 -05:00
|
|
|
</html>
|