This repository has no description
11 kB
406 lines
1const P = [
2 { r: 0.22, hue: 350, phase: 0.0 },
3 { r: 0.37, hue: 25, phase: 1.4 },
4 { r: 0.53, hue: 140, phase: 2.8 },
5 { r: 0.7, hue: 205, phase: 4.1 },
6 { r: 0.88, hue: 280, phase: 5.4 },
7];
8
9const H = [320, 50, 170, 230, 95, 285];
10
11const G = 0.15;
12const m = 0.0016;
13const mn = 0.002;
14const mx = 0.08;
15const M = 0.03;
16const e = 0.006;
17const S = 0.92;
18const s = 1.1;
19const h = 1 / 120;
20const X = 0.1;
21const b = 0.08;
22const k = 0.6;
23const z = 0.25;
24const d = 0.8;
25const fd = 0.92;
26const l = 0.12;
27const F = 0.05;
28const E = 0.12;
29const Q = 1.8;
30const R = 1.7;
31const T = 1.4;
32const K = 2.0;
33const L = 0.16;
34const V = 1.5;
35const B = 12;
36const W = [-2, -1, 0, 1, 2].flatMap((ox) => [-2, -1, 0, 1, 2].map((oy) => [ox, oy]));
37
38const cn = 28;
39const cw = 14;
40const bc = "aspect-square rounded-full max-w-full max-h-full size-[4px] bg-gray-200 dark:bg-gray-700";
41const wq = "(min-width: 768px)";
42const dq = "(prefers-color-scheme: dark)";
43const rq = "(prefers-reduced-motion: reduce)";
44const q = 64;
45const hz = 60;
46
47const c = (f) => Math.max(0, Math.min(1, f));
48const r = (a, b) => Array.from({ length: b - a + 1 }, (f, i) => a + i);
49const C = (x, y, f) => {
50 const m = Math.hypot(x, y);
51 return m > f ? { x: (x / m) * f, y: (y / m) * f } : { x, y };
52};
53const v = (r) => Math.sqrt((G * r * r) / Math.pow(r * r + e, 1.5));
54
55const p = (b, tx, ty, gm) => {
56 const dx = tx - b.x;
57 const dy = ty - b.y;
58 const r2 = dx * dx + dy * dy + e;
59 const inv = gm / (r2 * Math.sqrt(r2));
60 return [dx * inv, dy * inv];
61};
62
63const U = (b, all, k) =>
64 all.reduce(
65 (acc, o, j) => {
66 if (j === k) return acc;
67 const f = p(b, o.x, o.y, o.mass);
68 return [acc[0] + f[0], acc[1] + f[1]];
69 },
70 [0, 0],
71 );
72
73const a = (b, i, all, n, u) => {
74 const s = p(b, n.x, n.y, G);
75 const m = u.mass > 0 ? p(b, u.x, u.y, u.mass) : [0, 0];
76 const f = U(b, all, i);
77 return [s[0] + m[0] + f[0], s[1] + m[1] + f[1]];
78};
79
80const A = (n, all) => {
81 const g = U(n, all, -1);
82 return [b * g[0] - k * n.x - z * n.vx, b * g[1] - k * n.y - z * n.vy];
83};
84
85const I = (sim, u, dt) => {
86 const n = sim.sun;
87 const o = sim.bodies.map((b, i) => {
88 const f = a(b, i, sim.bodies, n, u);
89 const vx = b.vx + f[0] * dt;
90 const vy = b.vy + f[1] * dt;
91 return { ...b, vx, vy, x: b.x + vx * dt, y: b.y + vy * dt };
92 });
93 const j = A(n, sim.bodies);
94 const vx = n.vx + j[0] * dt;
95 const vy = n.vy + j[1] * dt;
96 return { bodies: o, sun: { x: n.x + vx * dt, y: n.y + vy * dt, vx, vy } };
97};
98
99const O = (sim, u, t) => (t >= h ? O(I(sim, u, h), u, t - h) : { sim, rem: t });
100
101const y = (b, n) => {
102 const f = Math.hypot(b.x - n.x, b.y - n.y);
103 if (f >= E && Math.hypot(b.x, b.y) <= Q) return { keep: b, ate: 0 };
104 return { keep: null, ate: f < E ? 1 : 0 };
105};
106
107const D = () =>
108 P.map((p) => {
109 const f = v(p.r);
110 return {
111 x: p.r * Math.cos(p.phase),
112 y: p.r * Math.sin(p.phase),
113 vx: -Math.sin(p.phase) * f,
114 vy: Math.cos(p.phase) * f,
115 hue: p.hue,
116 mass: m,
117 size: 1,
118 };
119 });
120
121const w = (arr) => (arr.length <= B ? arr : arr.slice(arr.length - B));
122
123const o = (N, j) => {
124 const f = Array.from(N.children, (c) => c.firstElementChild).filter(Boolean);
125 f.forEach((el) => {
126 el.className = bc;
127 el.style.transition = "none";
128 el.style.transformOrigin = "center";
129 el.style.willChange = "background-color, transform";
130 });
131 return { cells: f, cols: j, rows: Math.max(1, Math.ceil(f.length / j)) };
132};
133
134const x = (el) => {
135 el.style.backgroundColor = "";
136 el.style.transform = "";
137 el.style.border = "";
138};
139
140const g = (N) => {
141 const ac = new AbortController();
142 const op = { signal: ac.signal };
143 const mq = matchMedia(wq);
144 const dm = matchMedia(dq);
145 const rd = matchMedia(rq).matches;
146 const cl = () => (mq.matches ? cw : cn);
147
148 N.style.userSelect = "none";
149 N.style.WebkitUserSelect = "none";
150 N.style.cursor = "crosshair";
151
152 let v = o(N, cl());
153 let dk = dm.matches;
154 let bs = dk ? "rgb(55 65 81)" : "rgb(229 231 235)";
155 let dl = dk ? 60 : 45;
156
157 const al = (i) => ({ energy: new Float32Array(i), hue: new Float32Array(i) });
158 let f = al(v.cells.length);
159 let G = al(v.cells.length);
160 let dr = new Float32Array(v.cells.length).fill(-1);
161 let dh = new Float32Array(v.cells.length);
162
163 let sim = { bodies: D(), sun: { x: 0, y: 0, vx: 0, vy: 0 } };
164 let u = { x: 0, y: 0, over: false, mass: 0 };
165 let pd = null;
166 let vel = { x: 0, y: 0 };
167 let lm = 0;
168 let cu = 0;
169 let hp = 0;
170 let fl = 0;
171 let acc = 0;
172 let lt = 0;
173 let raf = 0;
174 let vs = true;
175
176 const rl = () => {
177 v = o(N, cl());
178 v.cells.forEach(x);
179 f = al(v.cells.length);
180 G = al(v.cells.length);
181 dr = new Float32Array(v.cells.length).fill(-1);
182 dh = new Float32Array(v.cells.length);
183 };
184
185 const os = () => {
186 dk = dm.matches;
187 bs = dk ? "rgb(55 65 81)" : "rgb(229 231 235)";
188 dl = dk ? 60 : 45;
189 dr.fill(-1);
190 };
191
192 mq.addEventListener("change", rl, op);
193 dm.addEventListener("change", os, op);
194
195 const ts = (a, e) => {
196 const cx = (v.cols - 1) / 2;
197 const cy = (v.rows - 1) / 2;
198 if (cx === 0 || cy === 0) return { x: 0, y: 0 };
199 const rc = N.getBoundingClientRect();
200 const p = ((a - rc.left) / rc.width) * v.cols - 0.5;
201 const q = ((e - rc.top) / rc.height) * v.rows - 0.5;
202 return { x: (p - cx) / (S * cx), y: (q - cy) / (S * cy) };
203 };
204
205 const mv = (E) => {
206 const tt = E.timeStamp / 1000;
207 const s = ts(E.clientX, E.clientY);
208 const dtm = tt - lm;
209 if (dtm > 0 && dtm < 0.1) {
210 vel = {
211 x: vel.x * 0.5 + ((s.x - u.x) / dtm) * 0.5,
212 y: vel.y * 0.5 + ((s.y - u.y) / dtm) * 0.5,
213 };
214 }
215 lm = tt;
216 u = { ...u, x: s.x, y: s.y, over: true };
217 };
218
219 const dn = (E) => {
220 if (E.pointerType === "touch") return;
221 E.preventDefault();
222 N.setPointerCapture(E.pointerId);
223 mv(E);
224 vel = { x: 0, y: 0 };
225 pd = { start: E.timeStamp / 1000, hue: H[hp % H.length] };
226 hp += 1;
227 };
228
229 const up = (E) => {
230 if (!pd) return;
231 if (N.hasPointerCapture(E.pointerId)) N.releasePointerCapture(E.pointerId);
232 const tt = E.timeStamp / 1000;
233 const c2 = c((tt - pd.start) / T);
234 const fs = tt - lm < 0.09;
235 const lc = C(fs ? vel.x * L : 0, fs ? vel.y * L : 0, V);
236 sim = {
237 ...sim,
238 bodies: w([
239 ...sim.bodies,
240 {
241 x: u.x,
242 y: u.y,
243 vx: lc.x,
244 vy: lc.y,
245 hue: pd.hue,
246 mass: mn + c2 * (mx - mn),
247 size: 0.8 + c2 * 2.2,
248 },
249 ]),
250 };
251 cu = tt + K;
252 pd = null;
253 };
254
255 N.addEventListener("pointermove", mv, { passive: true, signal: ac.signal });
256 N.addEventListener("pointerdown", dn, op);
257 N.addEventListener("pointerup", up, op);
258 N.addEventListener(
259 "pointerleave",
260 () => {
261 if (!pd) u = { ...u, over: false };
262 },
263 op,
264 );
265
266 const add = (buf, x, y, hue, e) => {
267 if (x < 0 || x >= v.cols || y < 0 || y >= v.rows) return;
268 const i = y * v.cols + x;
269 if (i >= 0 && i < buf.energy.length && e > buf.energy[i]) {
270 buf.energy[i] = e;
271 buf.hue[i] = hue;
272 }
273 };
274
275 const bl = (buf, fx, fy, hue, e, rad) => {
276 const bx = Math.round(fx);
277 const by = Math.round(fy);
278 const sp = r(-Math.ceil(rad), Math.ceil(rad));
279 sp.forEach((ox) =>
280 sp.forEach((oy) => {
281 const px = bx + ox;
282 const py = by + oy;
283 const dt = Math.hypot(px - fx, py - fy);
284 if (dt > rad) return;
285 add(buf, px, py, hue, e * (1 - dt / rad));
286 }),
287 );
288 };
289
290 const fm = (ms) => {
291 const t = ms / 1000;
292 const re = lt ? Math.min(X, (ms - lt) / 1000) : 0;
293 lt = ms;
294 const k = re * hz;
295 const dK = Math.pow(d, k);
296 const fK = Math.pow(fd, k);
297 const uK = 1 - Math.pow(1 - l, k);
298
299 const tg = u.over && t >= cu ? M : 0;
300 u = { ...u, mass: u.mass + (tg - u.mass) * uK };
301
302 acc += re * s;
303 const ot = O(sim, u, acc);
304 acc = ot.rem;
305 const rs = ot.sim.bodies.map((b) => y(b, ot.sim.sun));
306 sim = { bodies: rs.map((j) => j.keep).filter(Boolean), sun: ot.sim.sun };
307 const ea = rs.reduce((a, j) => a + j.ate, 0);
308 if (ea > 0) fl = Math.min(1.6, fl + ea * 0.9);
309 fl *= fK;
310
311 const cx = (v.cols - 1) / 2;
312 const cy = (v.rows - 1) / 2;
313 const n = sim.sun;
314
315 f.energy.forEach((j, i) => {
316 G.energy[i] = j * dK;
317 G.hue[i] = f.hue[i];
318 });
319
320 const br = 0.88 + 0.12 * Math.sin(t) + fl;
321 const Fx = cx + n.x * S * cx;
322 const Fy = cy + n.y * S * cy;
323 const bX = Math.round(Fx);
324 const bY = Math.round(Fy);
325 W.forEach(([ox, oy]) => {
326 const px = bX + ox;
327 const py = bY + oy;
328 const dt = Math.hypot(px - Fx, py - Fy);
329 if (dt > R) return;
330 add(G, px, py, 45, br * (0.45 + 0.55 * (1 - dt / R)));
331 });
332
333 sim.bodies.forEach((b) => {
334 const dt = Math.hypot(b.x - n.x, b.y - n.y);
335 const ht = c((0.45 - dt) / 0.45);
336 bl(G, cx + b.x * S * cx, cy + b.y * S * cy, b.hue, 0.85 + 0.15 * ht, 0.4 + b.size * 0.5);
337 });
338
339 if (pd) {
340 const c2 = c((t - pd.start) / T);
341 const sz = 0.8 + c2 * 2.2;
342 bl(G, cx + u.x * S * cx, cy + u.y * S * cy, pd.hue, 0.55 + 0.45 * c2, 0.4 + sz * 0.5);
343 } else if (u.over) {
344 add(G, Math.round(cx + u.x * S * cx), Math.round(cy + u.y * S * cy), 200, 0.5);
345 }
346
347 v.cells.forEach((el, i) => {
348 const e = G.energy[i];
349 if (e >= F) {
350 const Q = Math.round(Math.min(1, e) * q);
351 const hue = G.hue[i];
352 if (Q !== dr[i] || hue !== dh[i]) {
353 const qe = Q / q;
354 const mi = Math.min(100, qe * 170).toFixed(1);
355 el.style.backgroundColor = `color-mix(in srgb, hsl(${hue} 95% ${dl}%) ${mi}%, ${bs})`;
356 el.style.transform = `scale(${(1 + qe * 0.8).toFixed(3)})`;
357 el.style.border = "0";
358 dr[i] = Q;
359 dh[i] = hue;
360 }
361 } else if (dr[i] !== -1) {
362 x(el);
363 dr[i] = -1;
364 }
365 });
366
367 const tp = f;
368 f = G;
369 G = tp;
370
371 if (!rd && vs) raf = requestAnimationFrame(fm);
372 };
373
374 const io = new IntersectionObserver((en) => {
375 const vi = en.some((E) => E.isIntersecting);
376 if (vi === vs) return;
377 vs = vi;
378 if (vs) {
379 lt = 0;
380 raf = requestAnimationFrame(fm);
381 } else {
382 cancelAnimationFrame(raf);
383 }
384 });
385 io.observe(N);
386
387 raf = requestAnimationFrame(fm);
388
389 return () => {
390 cancelAnimationFrame(raf);
391 io.disconnect();
392 ac.abort();
393 };
394};
395
396let Y = null;
397let Z = null;
398const J = () => {
399 const t = document.querySelector("[data-punchcard]");
400 if (t === Z) return;
401 if (Y) Y();
402 Z = t;
403 Y = t ? g(t) : null;
404};
405J();
406document.addEventListener("htmx:load", J);