Public
Edited
Jan 20, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const polygons = [];
const segments = [];
function randomColor() {
return `hsl(${Math.random() * 360},60%,70%)`;
}
const loops = [];
for (let i = 0; i < n; i++) {
const segs = rosetteSegments(n, d, i, i + 1);

const poly = stitchLoop(segs);
loops.push(poly);
if (poly.length == 0) break;
poly.push(poly[0]);
if (i > 0) {
if (i % step == 0) {
polygons.push(poly.concat([...loops[i - step]].reverse()));
for (let s of segs) segments.push(s);
}
} else {
for (let s of segs) segments.push(s);
polygons.push(poly);
}
}
return display(
{ polygons, segments },
{ lineColor: "black", polygonColor: randomColor }
);
}
Insert cell
import { vec2 } from "@esperanc/vec2-utils"
Insert cell
function hash([x, y]) {
return ~~((x * 100 + y) * 100);
}
Insert cell
function samePoint(a, b) {
return Math.hypot(a[0] - b[0], a[1] - b[1]) < 0.001;
}
Insert cell
function rosetteSegments(n, d, min = 0, max = 100) {
const delta = (Math.PI * 2) / n;
const polar = (ang) => [Math.cos(ang), Math.sin(ang)];
const origins = [];
const rays = [];

for (let i = 0; i < n; i++) {
const a = polar(delta * i);
origins[i] = a;
const b = polar(delta * (i + d));
const c = polar(delta * (i - d));
rays[i] = [vec2.sub([], b, a), vec2.sub([], c, a)];
}

let t = [];
for (let i = 0; i < n; i++) {
let p = origins[i];
t.push([]);
for (let k of [0, 1]) {
let v = rays[i][k];
t[i][k] = [1];
for (let j = 0; j < n; j++) {
if (j == i) continue;
let P = origins[j];
for (let V of rays[j]) {
let den = v[0] * V[1] - v[1] * V[0];
if (Math.abs(den) <= 0.0001) continue;
let u = (V[1] * (P[0] - p[0]) + p[1] * V[0] - P[1] * V[0]) / den;
if (u > Number.EPSILON && u < 1 - Number.EPSILON) t[i][k].push(u);
}
}
}
}

let edges = [];
for (let i = 0; i < n; i++) {
let p = origins[i];
for (let k of [0, 1]) {
let v = rays[i][k];
t[i][k].sort((a, b) => a - b);
let prev = p;
let count = 0;
for (let u of t[i][k]) {
let q = vec2.scaleAndAdd([], p, v, u);
if (!samePoint(prev, q)) {
if (count >= max) break;
if (count >= min) edges.push([prev, q]);
count++;
}
prev = q;
}
}
}

return edges;
}
Insert cell
function display(elements, options = {}) {
const {
width = 800,
height = 600,
background = "#BBB",
lineColor = "black",
polygonColor = (i) => "#00000030",
scale = 250
} = options;
const ctx = DOM.context2d(width, height);
ctx.fillStyle = background;
ctx.fillRect(0, 0, width, height);
const T = ([x, y]) => [x * scale + width / 2, y * scale + height / 2];
if (elements.polygons) {
elements.polygons.forEach((poly, i) => {
ctx.beginPath();
ctx.strokeStyle = lineColor;
ctx.fillStyle = polygonColor(i);
for (let p of poly) ctx.lineTo(...T(p));
ctx.closePath();
ctx.fill();
});
}
if (elements.segments) {
ctx.beginPath();
ctx.strokeStyle = lineColor;
for (let [a, b] of elements.segments) {
ctx.moveTo(...T(a));
ctx.lineTo(...T(b));
}
ctx.stroke();
}
return ctx.canvas;
}
Insert cell
function stitchLoop(segments) {
const points = segments.flat();
const indexAngles = points
.map((p, i) => [Math.atan2(p[1], p[0]), i])
.sort((a, b) => a[0] - b[0]);
return indexAngles.map(([p, i]) => points[i]);
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more