aboutsummaryrefslogtreecommitdiffstats
path: root/site/pure.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'site/pure.mjs')
-rw-r--r--site/pure.mjs262
1 files changed, 262 insertions, 0 deletions
diff --git a/site/pure.mjs b/site/pure.mjs
new file mode 100644
index 0000000..43f7213
--- /dev/null
+++ b/site/pure.mjs
@@ -0,0 +1,262 @@
+const NUM_POINTS = 40;
+
+// 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();
+
+ constructor(x = Math.random(), y = Math.random()) {
+ this.x = x;
+ this.y = y;
+ }
+
+ equalPos(other) {
+ return this.x === other.x && this.y === other.y;
+ }
+
+ dot(other) {
+ return this.x * other.x + this.y * other.y;
+ }
+
+ get mag() {
+ return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
+ }
+
+ #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;
+ }
+
+ toString() {
+ return `(${this.x.toFixed(2)}, ${this.y.toFixed(2)})`
+ }
+}
+
+class Line {
+ constructor(p1, p2) {
+ this.p1 = p1;
+ this.p2 = p2;
+ }
+
+ get vec() {
+ return new Point(this.p2.x - this.p1.x, this.p2.y - this.p1.y)
+ }
+
+ angleBetween(other) {
+ // console.debug(' angleBetween', this.toString(), other.toString());
+ const v1 = this.vec;
+ const v2 = other.vec;
+ // console.debug(` dot of ${v1} * ${v2}`, v1.dot(v2));
+ // console.debug(` mag of ${v1} * ${v2}`, v1.mag * v2.mag);
+ const theta = Math.acos(v1.dot(v2) / (v1.mag * v2.mag));
+ // console.debug(` theta is`, 360 * theta / TAU);
+ return theta;
+ }
+ angleBetween2(other) {
+ console.debug('angleBetween',
+ `(${this.p1.x}, ${this.p1.y})-(${this.p2.x}, ${this.p2.y})`,
+ `(${other.p1.x}, ${other.p1.y})-(${other.p2.x}, ${other.p2.y})`,
+ );
+ return Math.atan(Math.abs((this.slope - other.slope) / (1 + this.slope * other.slope)))
+ }
+
+ get slope() {
+ return (this.p2.y - this.p1.y) / (this.p2.x - this.p1.x);
+ }
+
+ toString() {
+ return `${this.p1}-${this.p2}`;
+ }
+}
+
+// assume points is sorted min first.
+function findPoly3(points) {
+ // console.debug('findPoly()', ...points.map(p => { return {x: p.x, y: p.y} }));
+
+ let lastLine = new Line(new Point(0, 0), new Point(1, 0));
+ // console.debug('horiz slope', lastLine.slope);
+
+ // bail out counter. shouldn't be necessary, but who knows.
+ let xxx = 100;
+ let res = [points[0]];
+ do {
+ // last result is always a point on the edge.
+ // const p1 = res[res.length-1];
+ // console.debug(`-- checking lines from ${p1}`);
+ let last = res[res.length-1];
+ let minTheta = TAU;
+ let minP = points[0];
+ for (let i = 0; i < points.length; i++) {
+ if (!last.equalPos(points[i])) {
+ let l2 = new Line(last, points[i]);
+ let theta = lastLine.angleBetween(l2);
+ // console.debug('+++ l2 slope', l2.slope, theta, 360 * theta / TAU);
+ if (theta < minTheta) {
+ // console.debug(' -- min angle')
+ minTheta = theta;
+ minP = points[i];
+ }
+ }
+ }
+ lastLine = new Line(res[res.length-1], minP);
+ res.push(minP);
+ // console.debug('term', xxx, res[0].x, res[0].y, res[res.length-1].x, res[res.length-1].y)
+ } while (xxx-- > 0 && !res[0].equalPos(res[res.length-1]));
+ if (xxx === 0) {
+ alert('couldn\'t find your polycule');
+ }
+
+ // console.debug('findPoly res', res);
+ return res;
+}
+
+const points = [...Array(NUM_POINTS)].map(_ => new Point());
+// const points = [
+// new Point(0.5, 0.1),
+// new Point(0.75, 0.2),
+// new Point(0.9, 0.9),
+// new Point(0.2, 0.5),
+// new Point(0.6, 0.4), // should be inside
+// ];
+
+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.strokeStyle = p.color;
+ 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);
+ 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;
+ let interCount = 0;
+ function render(t) {
+ if (t > lastTime) {
+ fps.textContent = Math.floor(1_000 * interCount / (t - lastTime));
+ lastTime = t;
+ interCount = 0;
+ } else {
+ interCount++;
+ }
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ renderPoints(ctx, points);
+ if (bouncePoints(points)) {
+ //goButton.onclick({ target: goButton });
+ }
+ movePoints(points);
+
+ points.sort((a, b) => a.y > b.y);
+ const polyPoints = findPoly3(points);
+
+ ctx.lineWidth = 0.005;
+ ctx.beginPath();
+ ctx.moveTo(polyPoints[0].x, polyPoints[0].y);
+ polyPoints.forEach(p => ctx.lineTo(p.x, p.y));
+ ctx.strokeStyle = 'blue';
+ ctx.stroke();
+
+ if (!paused) {
+ self.requestAnimationFrame(render);
+ }
+ }
+
+ //goButton.onclick({ target: goButton });
+ render(document.timeline.currentTime);
+}
+
+document.addEventListener('DOMContentLoaded', loaded);