aboutsummaryrefslogtreecommitdiffstats
path: root/site/main.mjs
diff options
context:
space:
mode:
authorbrian cully <bjc@spork.org>2025-12-26 12:04:14 -0500
committerbrian cully <bjc@spork.org>2025-12-26 12:04:14 -0500
commitfa3067a16f8ba06ee756eb2b2293be4cb728681d (patch)
tree41b5ab1e76e12be764d4f6481febe49862bf8ea1 /site/main.mjs
downloadpolyring-fa3067a16f8ba06ee756eb2b2293be4cb728681d.tar.gz
polyring-fa3067a16f8ba06ee756eb2b2293be4cb728681d.zip
add bouncing particles
Diffstat (limited to 'site/main.mjs')
-rw-r--r--site/main.mjs140
1 files changed, 140 insertions, 0 deletions
diff --git a/site/main.mjs b/site/main.mjs
new file mode 100644
index 0000000..0c81b41
--- /dev/null
+++ b/site/main.mjs
@@ -0,0 +1,140 @@
+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);