#rp-canvas{width:100%;height:100%;display:block}
.rp-caps{position:absolute;left:0;right:0;bottom:4%;display:grid;place-items:center;font-size:clamp(9px,2.4vmin,12px);color:var(--muted)}
.rp-caps span{grid-area:1/1;transition:opacity .3s linear}
const canvas = document.getElementById('rp-canvas');
const ctx = canvas.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const colFg = cs.getPropertyValue('--fg').trim() || '#222';
const colMuted = cs.getPropertyValue('--muted').trim() || '#888';
function resize() {
const r = canvas.getBoundingClientRect();
canvas.width = Math.round(r.width * devicePixelRatio);
canvas.height = Math.round(r.height * devicePixelRatio);
}
addEventListener('resize', resize); resize();
const N_BLOB = 18;
const blobR = [];
for (let i = 0; i < N_BLOB; i++) blobR.push(0.8 + Math.sin(i * 1.7) * 0.08 + Math.cos(i * 0.6) * 0.06);
function blobPoint(a) {
const idx = (a / (Math.PI * 2)) * N_BLOB;
const i0 = Math.floor(idx) % N_BLOB, i1 = (i0 + 1) % N_BLOB;
const f = idx - Math.floor(idx);
const r = blobR[i0] * (1 - f) + blobR[i1] * f;
return [Math.cos(a) * r, Math.sin(a) * r];
}
const messyPts = [];
for (let i = 0; i < 55; i++) {
const a = Math.random() * Math.PI * 2;
const rr = 0.12 + Math.random() * 0.82;
const [bx, by] = blobPoint(a);
messyPts.push([bx * rr, by * rr]);
}
const messyLines = [];
for (let i = 0; i < messyPts.length; i++) {
const dists = messyPts
.map((p, j) => [j, Math.hypot(p[0] - messyPts[i][0], p[1] - messyPts[i][1])])
.filter(([j]) => j !== i)
.sort((a, b) => a[1] - b[1]);
for (let k = 0; k < 3; k++) messyLines.push([i, dists[k][0]]);
}
function ease(x) { return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; }
function draw(t) {
const w = canvas.width, h = canvas.height;
ctx.clearRect(0, 0, w, h);
const cx = w / 2, cy = h / 2;
const scale = Math.min(w, h) * 0.42;
const cycle = 6000;
const local = (t % cycle) / cycle;
let clean;
if (local < 0.4) clean = 0;
else if (local < 0.5) clean = ease((local - 0.4) / 0.1);
else if (local < 0.9) clean = 1;
else clean = 1 - ease((local - 0.9) / 0.1);
ctx.globalAlpha = 0.35;
ctx.strokeStyle = colFg;
ctx.lineWidth = Math.max(1, scale * 0.006);
ctx.beginPath();
for (let i = 0; i <= 64; i++) {
const a = (i / 64) * Math.PI * 2;
const [x, y] = blobPoint(a);
const px = cx + x * scale, py = cy + y * scale;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath(); ctx.stroke();
ctx.globalAlpha = (1 - clean) * 0.85;
ctx.strokeStyle = colMuted;
ctx.lineWidth = Math.max(1, scale * 0.004);
ctx.beginPath();
for (const [i, j] of messyLines) {
ctx.moveTo(cx + messyPts[i][0] * scale, cy + messyPts[i][1] * scale);
ctx.lineTo(cx + messyPts[j][0] * scale, cy + messyPts[j][1] * scale);
}
ctx.stroke();
ctx.globalAlpha = clean;
ctx.strokeStyle = colFg;
ctx.lineWidth = Math.max(1, scale * 0.006);
const RINGS = 4, SPOKES = 14;
for (let ring = 1; ring <= RINGS; ring++) {
const frac = ring / RINGS;
ctx.beginPath();
for (let i = 0; i <= 64; i++) {
const a = (i / 64) * Math.PI * 2;
const [x, y] = blobPoint(a);
const px = cx + x * frac * scale, py = cy + y * frac * scale;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
}
for (let s = 0; s < SPOKES; s++) {
const a = (s / SPOKES) * Math.PI * 2;
const [x, y] = blobPoint(a);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + x * scale, cy + y * scale);
ctx.stroke();
}
ctx.globalAlpha = 1;
document.getElementById('rp-messy').style.opacity = String(1 - clean);
document.getElementById('rp-clean').style.opacity = String(clean);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);