const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT = cs.getPropertyValue('--accent').trim() || '#5b5bf7';
const FG = cs.getPropertyValue('--fg').trim() || '#f1f0ec';
let w, h;
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
w = innerWidth; h = innerHeight;
c.width = w * dpr; c.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();
function circumcircle(a, b, cpt) {
const d = 2 * (a[0] * (b[1] - cpt[1]) + b[0] * (cpt[1] - a[1]) + cpt[0] * (a[1] - b[1]));
if (Math.abs(d) < 1e-6) return null;
const A2 = a[0] * a[0] + a[1] * a[1], B2 = b[0] * b[0] + b[1] * b[1], C2 = cpt[0] * cpt[0] + cpt[1] * cpt[1];
const ux = (A2 * (b[1] - cpt[1]) + B2 * (cpt[1] - a[1]) + C2 * (a[1] - b[1])) / d;
const uy = (A2 * (cpt[0] - b[0]) + B2 * (a[0] - cpt[0]) + C2 * (b[0] - a[0])) / d;
return { x: ux, y: uy, r: Math.hypot(ux - a[0], uy - a[1]) };
}
function triangulate(points) {
const M = 6000;
const pts = points.concat([[-M, -M], [M * 2, -M], [-M, M * 2]]);
const s0 = points.length, s1 = points.length + 1, s2 = points.length + 2;
let tris = [[s0, s1, s2]];
for (let i = 0; i < points.length; i++) {
const p = points[i], bad = [];
for (const t of tris) { const cc = circumcircle(pts[t[0]], pts[t[1]], pts[t[2]]); if (cc && Math.hypot(p[0] - cc.x, p[1] - cc.y) < cc.r) bad.push(t); }
const edgeCount = new Map();
for (const t of bad) for (const e of [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]]) {
const key = [...e].sort((a, b) => a - b).join(',');
edgeCount.set(key, (edgeCount.get(key) || 0) + 1);
}
tris = tris.filter((t) => !bad.includes(t));
for (const t of bad) for (const e of [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]]) {
const key = [...e].sort((a, b) => a - b).join(',');
if (edgeCount.get(key) === 1) tris.push([e[0], e[1], i]);
}
}
return { pts, tris: tris.filter((t) => t.every((idx) => idx < points.length)) };
}
const N = 26;
const nodes = Array.from({ length: N }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10 }));
function draw() {
ctx.clearRect(0, 0, w, h);
const points = nodes.map((n) => [n.x, n.y]);
const { pts, tris } = triangulate(points);
ctx.strokeStyle = ACCENT; ctx.globalAlpha = 0.5; ctx.lineWidth = 1;
for (const t of tris) {
ctx.beginPath();
ctx.moveTo(pts[t[0]][0], pts[t[0]][1]); ctx.lineTo(pts[t[1]][0], pts[t[1]][1]); ctx.lineTo(pts[t[2]][0], pts[t[2]][1]);
ctx.closePath(); ctx.stroke();
}
ctx.globalAlpha = 1; ctx.fillStyle = FG;
for (const n of nodes) { ctx.beginPath(); ctx.arc(n.x, n.y, 2.6, 0, 7); ctx.fill(); }
}
function step(dt) {
for (const n of nodes) {
n.x += n.vx * dt; n.y += n.vy * dt;
if (n.x < 10 || n.x > w - 10) n.vx *= -1;
if (n.y < 10 || n.y > h - 10) n.vy *= -1;
}
}
draw();
(function loop() { step(0.03); draw(); requestAnimationFrame(loop); })();