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; } let lastTime = document.timeline.currentTime; let interCount = 1; function renderFrame(t, canvas, ctx, fps) { if (t > lastTime) { fps.textContent = Math.floor(1_000 * interCount / (t - lastTime)); lastTime = t; interCount = 1; } else { interCount++; } ctx.clearRect(0, 0, canvas.width, canvas.height); renderPoints(ctx, points); points.sort((a, b) => a.y > b.y); const polyPoints = findPoly3(points); ctx.lineWidth = 0.005; let last = polyPoints[0]; for (let i = 1; i < polyPoints.length; i++) { const p = polyPoints[i]; ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.strokeStyle = last.color; ctx.lineTo(p.x, p.y); ctx.stroke(); last = p; } } function update(points) { if (bouncePoints(points)) { // console.debug("bounced"); } movePoints(points); } export default async function () { const canvas = document.querySelector('section.watch canvas'); const ctx = canvas.getContext('2d'); console.debug('canvas:', canvas, 'ctx', ctx); ctx.scale(canvas.width, canvas.height); const fps = document.querySelector('section.watch .fps'); const goButton = document.querySelector('section.watch button'); let paused = true; goButton.onclick = e => { paused = !paused; if (paused) { e.target.textContent = 'go'; } else { e.target.textContent = 'pause'; self.requestAnimationFrame(render); } }; const benchButton = document.querySelector('section.bench button'); benchButton.onclick = e => { console.debug('bench clicked'); const iters = Number(document.querySelector('section.bench input').value); const start = self.performance.now(); for (let i = 0; i < iters; i++) { update(points); points.sort((a, b) => a.y > b.y); findPoly3(points); } const end = self.performance.now(); const delta = end - start; const iters_per_ms = iters / delta; const results = document.querySelector('section.bench .results'); results.textContent = `${iters} iters in ${delta.toFixed(2)} ms (${iters_per_ms.toFixed(2)} iters per ms)`; } let interCount = 1; function render(t) { update(points); renderFrame(t, canvas, ctx, fps); if (!paused) { self.requestAnimationFrame(render); } } renderFrame(document.timeline.currentTime, canvas, ctx, fps); }