const NUM_POINTS = 5; // thanks vihart const TAU = 2 * Math.PI; // no more than 1% of world size per tick. const MAX_SPEED = 0.01; // size of rendered points in proportion to canvas. const POINT_RADIUS = 0.01; class Point { x = Math.random(); y = Math.random(); #heading = Math.random() * TAU; speed = Math.random() * MAX_SPEED; color = randColor(); #speedX; get speedX() { if (this.#speedX === undefined) { this.#speedX = this.speed * Math.cos(this.heading); } return this.#speedX; } #speedY; get speedY() { if (this.#speedY === undefined) { this.#speedY = this.speed * Math.sin(this.heading); } return this.#speedY; } get heading() { return this.#heading; } set heading(val) { this.#heading = val; this.#speedX = this.#speedY = undefined; } } const points = [...Array(NUM_POINTS)].map(_ => new Point()); function randColor() { const [r, g, b] = [...Array(3)].map(_ => Math.random() * 255); return `rgb(${r} ${g} ${b})` } function renderPoints(ctx, points) { points.forEach(p => { ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, POINT_RADIUS, 0, TAU); ctx.fill(); ctx.lineWidth = 0.005; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x + p.speedX * 3, p.y + p.speedY * 3); ctx.stroke(); }); } function movePoints(points) { points.forEach(p => { p.x += p.speedX; p.y += p.speedY; }); } function bouncePoints(points) { let didBounce = false; points.forEach(p => { const x = p.x + p.speedX; const y = p.y + p.speedY; if (x < 0) { p.heading = Math.PI - p.heading; didBounce = true; } else if (x >= 1) { p.heading = Math.PI - p.heading; didBounce = true; } else if (y < 0) { p.heading = -1 * p.heading; didBounce = true; } else if (y >= 1) { p.heading = -1 * p.heading; didBounce = true; } }); return didBounce; } async function loaded() { const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); console.debug('canvas:', canvas, 'ctx', ctx, 'points:', points); ctx.scale(canvas.width, canvas.height); const fps = document.querySelector('#fps'); const goButton = document.querySelector('button'); let paused = true; goButton.onclick = e => { paused = !paused; if (paused) { e.target.textContent = 'go'; } else { e.target.textContent = 'pause'; self.requestAnimationFrame(render); } }; let lastTime = document.timeline.currentTime; function render(t) { if (t > lastTime) { fps.textContent = Math.floor(1_000 / (t - lastTime)); lastTime = t; } ctx.clearRect(0, 0, canvas.width, canvas.height); renderPoints(ctx, points); if (bouncePoints(points)) { //goButton.onclick({ target: goButton }); } movePoints(points); if (!paused) { self.requestAnimationFrame(render); } } //goButton.onclick({ target: goButton }); render(document.timeline.currentTime); } document.addEventListener('DOMContentLoaded', loaded);