0xray 4.0.12 → 4.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/.xray/features.schema.json +1 -1
  4. package/AGENTS-consumer.md +4 -2
  5. package/AGENTS.md +8 -1
  6. package/README.md +1 -1
  7. package/SKILLS.md +1 -1
  8. package/dist/AGENTS.md +8 -1
  9. package/dist/CHANGELOG.md +25 -1
  10. package/dist/README.md +1 -1
  11. package/dist/SKILLS.md +1 -1
  12. package/dist/integrations/cursor/README.md +61 -0
  13. package/dist/integrations/cursor/hooks/after-file-edit.js +38 -0
  14. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +170 -0
  15. package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +298 -0
  16. package/dist/integrations/cursor/hooks/hooks.json +20 -0
  17. package/dist/integrations/cursor/hooks/pre-compact.js +88 -0
  18. package/dist/integrations/cursor/hooks/pre-tool-use.js +105 -0
  19. package/dist/integrations/hooks/station-hook-runtime.cjs +89 -1
  20. package/dist/integrations/hooks/station-hook-runtime.mjs +2 -0
  21. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  22. package/dist/nucleus/suit-temperament.d.ts +2 -2
  23. package/dist/nucleus/suit-temperament.js +3 -1
  24. package/llms.txt +6 -4
  25. package/package.json +4 -2
  26. package/scripts/foundry/CHANGELOG.md +37 -1
  27. package/scripts/foundry/README.md +13 -1
  28. package/scripts/foundry/assert-packed-dist-cli.mjs +97 -0
  29. package/scripts/foundry/blip-render.cjs +1237 -0
  30. package/scripts/foundry/blip-rippel.cjs +1963 -0
  31. package/scripts/foundry/blip.mjs +109 -0
  32. package/scripts/foundry/cli.mjs +8 -4
  33. package/scripts/foundry/inspect.mjs +117 -7
  34. package/scripts/foundry/mint-suit.cjs +215 -14
  35. package/scripts/foundry/package.json +7 -1
  36. package/scripts/foundry/plant/agents/blip-inspect.yml +5 -0
  37. package/scripts/foundry/plant/agents/blip.yml +5 -0
  38. package/scripts/foundry/plant/agents/inspect.yml +1 -1
  39. package/scripts/foundry/plant/agents/mill.yml +1 -1
  40. package/scripts/foundry/plant/agents/sound-inspect.yml +5 -0
  41. package/scripts/foundry/plant/agents/sound.yml +5 -0
  42. package/scripts/foundry/plant/motions/MOTION-REGISTRY.md +35 -0
  43. package/scripts/foundry/plant/motions/registry.json +81 -0
  44. package/scripts/foundry/plant/skills/blip/SKILL.md +39 -0
  45. package/scripts/foundry/plant/skills/blip-inspect/SKILL.md +32 -0
  46. package/scripts/foundry/plant/skills/inspect/SKILL.md +2 -2
  47. package/scripts/foundry/plant/skills/sound/SKILL.md +29 -0
  48. package/scripts/foundry/plant/skills/sound-inspect/SKILL.md +28 -0
  49. package/scripts/foundry/release-gate.mjs +18 -10
  50. package/scripts/foundry/release.mjs +3 -0
  51. package/scripts/foundry/sound-bed.cjs +454 -0
  52. package/scripts/foundry/sound-rippel.cjs +1434 -0
  53. package/scripts/foundry/sound.mjs +93 -0
  54. package/scripts/node/assert-packed-dist-cli.mjs +4 -0
  55. package/scripts/node/consumer-install-smoke.mjs +20 -1
  56. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  57. package/xray/features.json +1 -1
  58. package/xray/features.schema.json +1 -1
@@ -0,0 +1,1963 @@
1
+ /**
2
+ * Factory-blip Rippel canvas — Rippel v2 headless port of SimplifiedVisualConverter.
3
+ *
4
+ * SSOT htafolla/rippel-synapse-flow@e5014cd46fbe5f132391333d8296f4416896dbee
5
+ * animationIcons.ts — Animation + ANIMATION_TO_VISUALIZATION
6
+ * types/index.ts — ChecksumResponse.visualConfig { circles[], canvas, tlmCommand }
7
+ * SimplifiedVisualConverter.tsx — routes Animation → viz id, canvas fallback 1280×720
8
+ * MiniAnimationViewer / FiveDimensionalVisualizer — viz backends for those ids
9
+ *
10
+ * This file is the converter spine, not a drawbox/geq label. Brief+seed → checksum
11
+ * visualConfig (CircleConfig[]) → viz backend. Power Plant palette is the Blip theme.
12
+ * v2 look variants (seed + --look): focus | cage.
13
+ * focus — lost sharp-dogfood: solid cyan disc + gold pupil + orbiting stampFocusDisc satellites.
14
+ * Field + mesh verts (solid mass, not wire) carry the mint id under the disc.
15
+ * cage — sparse Wu hairline mesh + field accents (stars, grid, gradient, blinkers).
16
+ * v2 motion: genre tempo + CircleConfig.frequency LFOs (same mill the audio bed uses).
17
+ * 4.44s is a Short: hook → turn → tag on the same motion grid as the bed.
18
+ * Phrase weights (hookEase/turnEase/tagEase) crossfade; binaries stay section flags.
19
+ * Uniqueness is look + field + mesh fingerprint — not more wire in the middle.
20
+ * Wireframe ffmpeg geometry lives in blip-render.cjs and is flag-only.
21
+ */
22
+
23
+ const soundRippel = require("./sound-rippel.cjs");
24
+
25
+ const ENGINE = "rippel-headless";
26
+ const LOOK = "rippel-v2";
27
+ const MOTION_WIDTH = 1280;
28
+ const MOTION_HEIGHT = 720;
29
+ const FPS = 30;
30
+ const TLM = "checksum";
31
+
32
+ const SSOT = {
33
+ repo: "htafolla/rippel-synapse-flow",
34
+ commit: "e5014cd46fbe5f132391333d8296f4416896dbee",
35
+ access: "headless-port",
36
+ paths: [
37
+ "animationIcons.ts",
38
+ "types/index.ts",
39
+ "SimplifiedVisualConverter.tsx",
40
+ "MiniAnimationViewer",
41
+ "FiveDimensionalVisualizer",
42
+ ],
43
+ note: "Rippel v2 — VisualConfig.circles + look variants (focus disc-satellites | cage Wu+field). Soft tints, no photosensitive strobe. Wireframe is flag-only.",
44
+ };
45
+
46
+ const GRID_KINDS = ["floor", "meridian", "ticks", "none"];
47
+ const GRAD_KINDS = ["horizon", "corner", "veil"];
48
+
49
+ const MESH_FAMILIES = [
50
+ "tetra",
51
+ "octa",
52
+ "cube",
53
+ "prism",
54
+ "star",
55
+ "cage",
56
+ "spire",
57
+ "icosa",
58
+ "helix",
59
+ "torus",
60
+ "lattice",
61
+ "flower",
62
+ ];
63
+ const MESH_GAITS = ["tumble", "shear", "pulse", "orbit", "snap"];
64
+ const CORE_STYLES = ["disc", "eclipse", "pulse"];
65
+ const LOOK_KINDS = ["focus", "cage"];
66
+
67
+ /** animationIcons.ts — names are imports into the plant registry. */
68
+ const ANIMATION_TO_VISUALIZATION = {
69
+ orb: "canvas",
70
+ swirl: "3d-sacred",
71
+ snap: "neural",
72
+ waves: "waveform",
73
+ spark: "particles",
74
+ };
75
+
76
+ const THEME = {
77
+ void: [8, 9, 11],
78
+ ink: [245, 247, 250],
79
+ cyan: [61, 224, 232],
80
+ gold: [245, 197, 24],
81
+ blue: [74, 127, 212],
82
+ };
83
+
84
+ const THEME_CYCLE = [THEME.cyan, THEME.gold, THEME.blue, THEME.ink];
85
+ const LIGHT_CYCLE = [THEME.cyan, THEME.gold, THEME.blue];
86
+
87
+ /** getGenreConfig.ts scale tables — same SSOT the sound mill already ported. */
88
+ const SCALES = {
89
+ ambient: [261.63, 311.13, 349.23, 392.0, 466.16],
90
+ techno: [65.41, 87.31, 130.81, 174.61, 196.0],
91
+ phonk: [32.7, 43.65, 55.0, 65.41, 82.41],
92
+ jazz: [130.81, 164.81, 196.0, 220.0, 261.63],
93
+ };
94
+
95
+ const NOTE_NAMES = ["C", "D#", "F", "G", "A#"];
96
+
97
+ const visualCache = new Map();
98
+
99
+ function clamp(n, lo, hi) {
100
+ return n < lo ? lo : n > hi ? hi : n;
101
+ }
102
+
103
+ function seedU32(seedHex, offset) {
104
+ const hex = String(seedHex || "").replace(/^0x/, "");
105
+ const n = Number.parseInt(hex.slice(offset, offset + 8), 16);
106
+ return Number.isFinite(n) ? n >>> 0 : 1;
107
+ }
108
+
109
+ function mulberry32(seed) {
110
+ let t = seed >>> 0;
111
+ return function next() {
112
+ t += 0x6d2b79f5;
113
+ let x = t;
114
+ x = Math.imul(x ^ (x >>> 15), x | 1);
115
+ x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
116
+ return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
117
+ };
118
+ }
119
+
120
+ function rgbHex(rgb) {
121
+ return `#${rgb.map((c) => clamp(c, 0, 255).toString(16).padStart(2, "0")).join("")}`;
122
+ }
123
+
124
+ function parseHex(hex) {
125
+ const h = String(hex || "").replace(/^#/, "");
126
+ return [
127
+ Number.parseInt(h.slice(0, 2), 16) || 0,
128
+ Number.parseInt(h.slice(2, 4), 16) || 0,
129
+ Number.parseInt(h.slice(4, 6), 16) || 0,
130
+ ];
131
+ }
132
+
133
+ function norm3(x, y, z) {
134
+ const len = Math.hypot(x, y, z) || 1;
135
+ return [x / len, y / len, z / len];
136
+ }
137
+
138
+ function kNearestEdges(verts, k) {
139
+ const edges = [];
140
+ const seen = new Set();
141
+ for (let i = 0; i < verts.length; i++) {
142
+ const dist = [];
143
+ for (let j = 0; j < verts.length; j++) {
144
+ if (j === i) continue;
145
+ const dx = verts[i][0] - verts[j][0];
146
+ const dy = verts[i][1] - verts[j][1];
147
+ const dz = verts[i][2] - verts[j][2];
148
+ dist.push({ j, d: dx * dx + dy * dy + dz * dz });
149
+ }
150
+ dist.sort((p, q) => p.d - q.d);
151
+ for (let n = 0; n < k && n < dist.length; n++) {
152
+ const a = i < dist[n].j ? i : dist[n].j;
153
+ const b = i < dist[n].j ? dist[n].j : i;
154
+ const key = `${a}-${b}`;
155
+ if (!seen.has(key)) {
156
+ seen.add(key);
157
+ edges.push([a, b]);
158
+ }
159
+ }
160
+ }
161
+ return edges;
162
+ }
163
+
164
+ function trianglesFromEdges(verts, edges) {
165
+ const adj = verts.map(() => []);
166
+ for (let i = 0; i < edges.length; i++) {
167
+ const a = edges[i][0];
168
+ const b = edges[i][1];
169
+ adj[a].push(b);
170
+ adj[b].push(a);
171
+ }
172
+ const faces = [];
173
+ const seen = new Set();
174
+ for (let a = 0; a < verts.length; a++) {
175
+ for (let bi = 0; bi < adj[a].length; bi++) {
176
+ const b = adj[a][bi];
177
+ if (b <= a) continue;
178
+ for (let ci = 0; ci < adj[a].length; ci++) {
179
+ const c = adj[a][ci];
180
+ if (c <= b) continue;
181
+ if (!adj[b].includes(c)) continue;
182
+ const key = `${a}-${b}-${c}`;
183
+ if (seen.has(key)) continue;
184
+ seen.add(key);
185
+ faces.push([a, b, c]);
186
+ }
187
+ }
188
+ }
189
+ return faces;
190
+ }
191
+
192
+ function platonic(family) {
193
+ if (family === "tetra") {
194
+ return {
195
+ verts: [
196
+ [1, 1, 1],
197
+ [1, -1, -1],
198
+ [-1, 1, -1],
199
+ [-1, -1, 1],
200
+ ].map((v) => norm3(v[0], v[1], v[2])),
201
+ edges: [
202
+ [0, 1],
203
+ [0, 2],
204
+ [0, 3],
205
+ [1, 2],
206
+ [1, 3],
207
+ [2, 3],
208
+ ],
209
+ faces: [
210
+ [0, 1, 2],
211
+ [0, 1, 3],
212
+ [0, 2, 3],
213
+ [1, 2, 3],
214
+ ],
215
+ };
216
+ }
217
+ if (family === "octa") {
218
+ return {
219
+ verts: [
220
+ [1, 0, 0],
221
+ [-1, 0, 0],
222
+ [0, 1, 0],
223
+ [0, -1, 0],
224
+ [0, 0, 1],
225
+ [0, 0, -1],
226
+ ],
227
+ edges: [
228
+ [0, 2],
229
+ [0, 3],
230
+ [0, 4],
231
+ [0, 5],
232
+ [1, 2],
233
+ [1, 3],
234
+ [1, 4],
235
+ [1, 5],
236
+ [2, 4],
237
+ [2, 5],
238
+ [3, 4],
239
+ [3, 5],
240
+ ],
241
+ faces: [
242
+ [0, 2, 4],
243
+ [0, 4, 3],
244
+ [0, 3, 5],
245
+ [0, 5, 2],
246
+ [1, 2, 5],
247
+ [1, 5, 3],
248
+ [1, 3, 4],
249
+ [1, 4, 2],
250
+ ],
251
+ };
252
+ }
253
+ if (family === "cube") {
254
+ const verts = [];
255
+ for (const x of [-1, 1]) for (const y of [-1, 1]) for (const z of [-1, 1]) verts.push(norm3(x, y, z));
256
+ const edges = [];
257
+ for (let i = 0; i < 8; i++) {
258
+ for (let j = i + 1; j < 8; j++) {
259
+ let d = 0;
260
+ for (let k = 0; k < 3; k++) if (verts[i][k] !== verts[j][k]) d += 1;
261
+ if (d === 1) edges.push([i, j]);
262
+ }
263
+ }
264
+ return { verts, edges, faces: trianglesFromEdges(verts, edges) };
265
+ }
266
+ if (family === "prism") {
267
+ const verts = [];
268
+ for (let i = 0; i < 3; i++) {
269
+ const a = (i * Math.PI * 2) / 3;
270
+ verts.push(norm3(Math.cos(a), Math.sin(a), 0.7));
271
+ verts.push(norm3(Math.cos(a), Math.sin(a), -0.7));
272
+ }
273
+ return {
274
+ verts,
275
+ edges: [
276
+ [0, 2],
277
+ [2, 4],
278
+ [4, 0],
279
+ [1, 3],
280
+ [3, 5],
281
+ [5, 1],
282
+ [0, 1],
283
+ [2, 3],
284
+ [4, 5],
285
+ ],
286
+ faces: [
287
+ [0, 2, 4],
288
+ [1, 3, 5],
289
+ [0, 1, 2],
290
+ [2, 1, 3],
291
+ [2, 3, 4],
292
+ [4, 3, 5],
293
+ [4, 5, 0],
294
+ [0, 5, 1],
295
+ ],
296
+ };
297
+ }
298
+ if (family === "star") {
299
+ const a = platonic("tetra");
300
+ const verts = a.verts.concat(a.verts.map((v) => [-v[0], -v[1], -v[2]]));
301
+ const edges = a.edges.concat(a.edges.map((e) => [e[0] + 4, e[1] + 4]));
302
+ const faces = (a.faces || []).concat((a.faces || []).map((f) => [f[0] + 4, f[1] + 4, f[2] + 4]));
303
+ return { verts, edges, faces };
304
+ }
305
+ if (family === "spire") {
306
+ const verts = [[0, 0, 1], [0, 0, -0.35]];
307
+ const edges = [[0, 1]];
308
+ for (let i = 0; i < 5; i++) {
309
+ const a = (i * Math.PI * 2) / 5;
310
+ verts.push(norm3(Math.cos(a) * 0.95, Math.sin(a) * 0.95, -0.2));
311
+ edges.push([0, i + 2], [1, i + 2], [i + 2, 2 + ((i + 1) % 5)]);
312
+ }
313
+ return { verts, edges };
314
+ }
315
+ if (family === "icosa") {
316
+ const phi = (1 + Math.sqrt(5)) / 2;
317
+ const raw = [
318
+ [0, 1, phi],
319
+ [0, 1, -phi],
320
+ [0, -1, phi],
321
+ [0, -1, -phi],
322
+ [1, phi, 0],
323
+ [1, -phi, 0],
324
+ [-1, phi, 0],
325
+ [-1, -phi, 0],
326
+ [phi, 0, 1],
327
+ [phi, 0, -1],
328
+ [-phi, 0, 1],
329
+ [-phi, 0, -1],
330
+ ].map((v) => norm3(v[0], v[1], v[2]));
331
+ const edges = kNearestEdges(raw, 5);
332
+ return { verts: raw, edges, faces: trianglesFromEdges(raw, edges) };
333
+ }
334
+ if (family === "helix") {
335
+ const verts = [];
336
+ const edges = [];
337
+ const n = 10;
338
+ for (let i = 0; i < n; i++) {
339
+ const a = i * 0.82;
340
+ const y = (i / (n - 1)) * 2 - 1;
341
+ verts.push(norm3(Math.cos(a) * 0.92, y, Math.sin(a) * 0.92));
342
+ if (i > 0) edges.push([i - 1, i]);
343
+ if (i > 1) edges.push([i - 2, i]);
344
+ }
345
+ edges.push([0, n - 1], [0, n - 2]);
346
+ return { verts, edges, faces: trianglesFromEdges(verts, edges) };
347
+ }
348
+ if (family === "torus") {
349
+ const verts = [];
350
+ const rings = 7;
351
+ const tube = 4;
352
+ const R = 0.78;
353
+ const r = 0.34;
354
+ for (let i = 0; i < rings; i++) {
355
+ const u = (i * Math.PI * 2) / rings;
356
+ for (let j = 0; j < tube; j++) {
357
+ const v = (j * Math.PI * 2) / tube;
358
+ verts.push(
359
+ norm3(
360
+ (R + r * Math.cos(v)) * Math.cos(u),
361
+ r * Math.sin(v),
362
+ (R + r * Math.cos(v)) * Math.sin(u),
363
+ ),
364
+ );
365
+ }
366
+ }
367
+ const edges = [];
368
+ for (let i = 0; i < rings; i++) {
369
+ for (let j = 0; j < tube; j++) {
370
+ const a = i * tube + j;
371
+ edges.push([a, i * tube + ((j + 1) % tube)]);
372
+ edges.push([a, ((i + 1) % rings) * tube + j]);
373
+ }
374
+ }
375
+ return { verts, edges, faces: trianglesFromEdges(verts, edges).slice(0, 36) };
376
+ }
377
+ if (family === "lattice") {
378
+ const verts = [];
379
+ const idx = new Map();
380
+ const vals = [-0.72, 0, 0.72];
381
+ for (let i = 0; i < vals.length; i++) {
382
+ for (let j = 0; j < vals.length; j++) {
383
+ for (let k = 0; k < vals.length; k++) {
384
+ idx.set(`${i},${j},${k}`, verts.length);
385
+ verts.push(norm3(vals[i], vals[j], vals[k]));
386
+ }
387
+ }
388
+ }
389
+ const edges = [];
390
+ for (let i = 0; i < vals.length; i++) {
391
+ for (let j = 0; j < vals.length; j++) {
392
+ for (let k = 0; k < vals.length; k++) {
393
+ const a = idx.get(`${i},${j},${k}`);
394
+ if (i + 1 < vals.length) edges.push([a, idx.get(`${i + 1},${j},${k}`)]);
395
+ if (j + 1 < vals.length) edges.push([a, idx.get(`${i},${j + 1},${k}`)]);
396
+ if (k + 1 < vals.length) edges.push([a, idx.get(`${i},${j},${k + 1}`)]);
397
+ }
398
+ }
399
+ }
400
+ return { verts, edges, faces: trianglesFromEdges(verts, edges).slice(0, 36) };
401
+ }
402
+ if (family === "flower") {
403
+ const verts = [[0, 0, 0.15]];
404
+ const edges = [];
405
+ for (let ring = 0; ring < 2; ring++) {
406
+ const z = ring === 0 ? 0.2 : -0.28;
407
+ const rad = ring === 0 ? 0.62 : 1;
408
+ for (let i = 0; i < 7; i++) {
409
+ const a = (i * Math.PI * 2) / 7 + ring * 0.22;
410
+ verts.push(norm3(Math.cos(a) * rad, Math.sin(a) * rad, z));
411
+ const vi = verts.length - 1;
412
+ edges.push([0, vi]);
413
+ if (i > 0) edges.push([vi - 1, vi]);
414
+ if (i === 6) edges.push([vi, 1 + ring * 7]);
415
+ }
416
+ }
417
+ for (let i = 0; i < 7; i++) edges.push([1 + i, 8 + i]);
418
+ return { verts, edges, faces: trianglesFromEdges(verts, edges) };
419
+ }
420
+ const verts = [];
421
+ const n = 8;
422
+ const phi = Math.PI * (3 - Math.sqrt(5));
423
+ for (let i = 0; i < n; i++) {
424
+ const y = 1 - (i / (n - 1)) * 2;
425
+ const r = Math.sqrt(Math.max(0, 1 - y * y));
426
+ const a = phi * i;
427
+ verts.push(norm3(Math.cos(a) * r, y, Math.sin(a) * r));
428
+ }
429
+ const edges = [];
430
+ for (let i = 0; i < n; i++) {
431
+ const dist = [];
432
+ for (let j = 0; j < n; j++) {
433
+ if (j === i) continue;
434
+ const dx = verts[i][0] - verts[j][0];
435
+ const dy = verts[i][1] - verts[j][1];
436
+ const dz = verts[i][2] - verts[j][2];
437
+ dist.push({ j, d: dx * dx + dy * dy + dz * dz });
438
+ }
439
+ dist.sort((p, q) => p.d - q.d);
440
+ for (let k = 0; k < 3; k++) {
441
+ const j = dist[k].j;
442
+ const a = i < j ? i : j;
443
+ const b = i < j ? j : i;
444
+ if (!edges.some((e) => e[0] === a && e[1] === b)) edges.push([a, b]);
445
+ }
446
+ }
447
+ return { verts, edges, faces: trianglesFromEdges(verts, edges) };
448
+ }
449
+
450
+ function thinConnectedEdges(edges, vertCount, keep, rng) {
451
+ if (edges.length <= keep) return edges.map((e) => [e[0], e[1]]);
452
+ const adj = Array.from({ length: vertCount }, () => []);
453
+ for (let i = 0; i < edges.length; i++) {
454
+ adj[edges[i][0]].push(edges[i][1]);
455
+ adj[edges[i][1]].push(edges[i][0]);
456
+ }
457
+ const start = (rng() * vertCount) | 0;
458
+ const seen = new Set([start]);
459
+ const kept = [];
460
+ const q = [start];
461
+ while (q.length && kept.length < keep) {
462
+ const v = q.shift();
463
+ const nbrs = adj[v];
464
+ for (let i = 0; i < nbrs.length && kept.length < keep; i++) {
465
+ const w = nbrs[i];
466
+ if (seen.has(w)) continue;
467
+ seen.add(w);
468
+ q.push(w);
469
+ kept.push(v < w ? [v, w] : [w, v]);
470
+ }
471
+ }
472
+ for (let i = 0; i < edges.length && kept.length < keep; i++) {
473
+ const a = edges[i][0];
474
+ const b = edges[i][1];
475
+ if (!kept.some((e) => e[0] === a && e[1] === b)) kept.push([a, b]);
476
+ }
477
+ return kept;
478
+ }
479
+
480
+ function buildMesh(seedHex, brief) {
481
+ const text = String(brief || "factory-blip");
482
+ let mix = 0;
483
+ for (let i = 0; i < text.length; i++) mix = (Math.imul(mix, 33) + text.charCodeAt(i)) >>> 0;
484
+ const rng = mulberry32(seedU32(seedHex, 0) ^ seedU32(seedHex, 8) ^ seedU32(seedHex, 16) ^ mix);
485
+ const family = MESH_FAMILIES[(rng() * MESH_FAMILIES.length) | 0];
486
+ const gait = MESH_GAITS[(rng() * MESH_GAITS.length) | 0];
487
+ const base = platonic(family);
488
+ const jitter = 0.05 + rng() * 0.16;
489
+ const verts = base.verts.map((v) =>
490
+ norm3(v[0] + (rng() - 0.5) * jitter, v[1] + (rng() - 0.5) * jitter, v[2] + (rng() - 0.5) * jitter),
491
+ );
492
+ let edges = base.edges.map((e) => [e[0], e[1]]);
493
+ const extra = (rng() * 3) | 0;
494
+ for (let i = 0; i < extra; i++) {
495
+ const a = (rng() * verts.length) | 0;
496
+ const b = (rng() * verts.length) | 0;
497
+ if (a === b) continue;
498
+ const lo = a < b ? a : b;
499
+ const hi = a < b ? b : a;
500
+ if (!edges.some((e) => e[0] === lo && e[1] === hi)) edges.push([lo, hi]);
501
+ }
502
+ if (edges.length > 16) {
503
+ edges = thinConnectedEdges(edges, verts.length, 10 + ((rng() * 6) | 0), rng);
504
+ }
505
+ let faces = (base.faces && base.faces.length ? base.faces : trianglesFromEdges(verts, edges)).map((f) => [
506
+ f[0],
507
+ f[1],
508
+ f[2],
509
+ ]);
510
+ if (faces.length > 16) faces = faces.slice(0, 16);
511
+ const accentIndex = (rng() * THEME_CYCLE.length) | 0;
512
+ const coreStyle = CORE_STYLES[(rng() * CORE_STYLES.length) | 0];
513
+ return {
514
+ family,
515
+ gait,
516
+ coreStyle,
517
+ id: `${family}-${gait}-${verts.length}v${edges.length}e${faces.length}f-${((rng() * 0xfffffff) | 0).toString(16)}`,
518
+ verts,
519
+ edges,
520
+ faces,
521
+ twist: rng() * Math.PI * 2,
522
+ spin: 1.7 + rng() * 2.4,
523
+ scale: 0.58 + rng() * 0.2,
524
+ warp: 0.06 + rng() * 0.14,
525
+ shells: 1,
526
+ cuts: 3 + ((rng() * 3) | 0),
527
+ dual: false,
528
+ ghost: false,
529
+ fill: false,
530
+ accentIndex,
531
+ accent: THEME_CYCLE[accentIndex],
532
+ };
533
+ }
534
+
535
+ function buildField(seedHex, brief) {
536
+ const text = String(brief || "factory-blip");
537
+ let mix = 1;
538
+ for (let i = 0; i < text.length; i++) mix = (Math.imul(mix, 31) + text.charCodeAt(i)) >>> 0;
539
+ const rng = mulberry32(seedU32(seedHex, 4) ^ seedU32(seedHex, 12) ^ mix ^ 0x51ed);
540
+ const stars = [];
541
+ const want = 16 + ((rng() * 14) | 0);
542
+ for (let i = 0; i < want * 3 && stars.length < want; i++) {
543
+ const x = rng();
544
+ const y = rng();
545
+ if (Math.hypot(x - 0.5, (y - 0.5) * 0.72) < 0.2) continue;
546
+ stars.push({
547
+ x,
548
+ y,
549
+ r: 1.4 + rng() * 1.8,
550
+ color: THEME_CYCLE[(rng() * THEME_CYCLE.length) | 0],
551
+ phase: rng() * Math.PI * 2,
552
+ sparkle: rng() > 0.55,
553
+ });
554
+ }
555
+ const blinkers = [];
556
+ const blinkWant = 3 + ((rng() * 5) | 0);
557
+ for (let i = 0; i < blinkWant; i++) {
558
+ blinkers.push({
559
+ x: rng() < 0.5 ? 0.06 + rng() * 0.16 : 0.78 + rng() * 0.16,
560
+ y: rng() < 0.5 ? 0.07 + rng() * 0.18 : 0.72 + rng() * 0.2,
561
+ color: THEME_CYCLE[(rng() * 3) | 0],
562
+ onAnd: rng() > 0.42,
563
+ });
564
+ }
565
+ return {
566
+ id: `field-${stars.length}s${blinkers.length}b-${((rng() * 0xfffff) | 0).toString(16)}`,
567
+ stars,
568
+ blinkers,
569
+ grid: GRID_KINDS[(rng() * GRID_KINDS.length) | 0],
570
+ gradient: GRAD_KINDS[(rng() * GRAD_KINDS.length) | 0],
571
+ gridColor: THEME_CYCLE[(rng() * 3) | 0],
572
+ gradColor: THEME_CYCLE[(rng() * 3) | 0],
573
+ gradStrength: 0.22 + rng() * 0.16,
574
+ };
575
+ }
576
+
577
+ function rotate3(v, yaw, pitch, roll) {
578
+ const cy = Math.cos(yaw);
579
+ const sy = Math.sin(yaw);
580
+ const x1 = v[0] * cy + v[2] * sy;
581
+ const z1 = -v[0] * sy + v[2] * cy;
582
+ const cp = Math.cos(pitch);
583
+ const sp = Math.sin(pitch);
584
+ const y2 = v[1] * cp - z1 * sp;
585
+ const z2 = v[1] * sp + z1 * cp;
586
+ const cr = Math.cos(roll);
587
+ const sr = Math.sin(roll);
588
+ return [x1 * cr - y2 * sr, x1 * sr + y2 * cr, z2];
589
+ }
590
+
591
+ function projectMesh(mesh, width, height, t, checksum, scaleMul) {
592
+ const gait = mesh.gait || "tumble";
593
+ let yaw = t * mesh.spin + mesh.twist;
594
+ let pitch = Math.sin(t * mesh.spin * 0.42 + mesh.twist) * 0.52;
595
+ let roll = t * 0.38 + mesh.twist * 0.25;
596
+ const beat = beatPhase(checksum, t);
597
+ const kick = kickAccent(beat);
598
+ const and = andAccent(beat);
599
+ const phrase = phraseOf(checksum, t);
600
+ let breathe =
601
+ (scaleMul || 1) *
602
+ mesh.scale *
603
+ (1 + 0.07 * kick + 0.045 * and) *
604
+ phraseMix(phrase, 0.92, 1.06 + 0.08 * phrase.turnHit, 0.88);
605
+ yaw += phrase.turnHit * 0.32;
606
+ let ox = 0;
607
+ let oy = 0;
608
+ if (gait === "snap") {
609
+ const q = Math.floor(beat);
610
+ yaw = q * 1.15 + mesh.twist;
611
+ pitch = Math.sin(q * 1.7 + mesh.twist) * 0.46;
612
+ roll = q * 0.55 + mesh.twist * 0.3;
613
+ } else if (gait === "pulse") {
614
+ breathe *= 1 + mesh.warp * (kick + and * 0.65);
615
+ } else if (gait === "orbit") {
616
+ ox = Math.cos(beat * Math.PI * 2 + mesh.twist) * mesh.warp * 0.42;
617
+ oy = Math.sin(beat * Math.PI + mesh.twist) * mesh.warp * 0.26;
618
+ } else if (gait === "tumble") {
619
+ yaw = beat * mesh.spin + mesh.twist;
620
+ pitch = beat * mesh.spin * 0.38 + Math.sin(mesh.twist) * 0.2;
621
+ roll = beat * mesh.spin * 0.22 + mesh.twist;
622
+ }
623
+ const minSide = Math.min(width, height);
624
+ const cx = (width - 1) * 0.5 + ox * minSide;
625
+ const cy = (height - 1) * 0.5 + oy * minSide;
626
+ const shear = gait === "shear";
627
+ return mesh.verts.map((v) => {
628
+ let r = rotate3(v, yaw, pitch, roll);
629
+ if (shear) {
630
+ r = [
631
+ r[0] + r[1] * Math.sin(beat * Math.PI * 2 + mesh.twist) * mesh.warp * 1.55,
632
+ r[1] + r[2] * Math.cos(beat * Math.PI * 1.4) * mesh.warp,
633
+ r[2],
634
+ ];
635
+ }
636
+ const z = r[2] + 2.35;
637
+ const p = (breathe * minSide) / z;
638
+ return { x: cx + r[0] * p, y: cy + r[1] * p * 0.84, z: r[2] };
639
+ });
640
+ }
641
+
642
+ /**
643
+ * Checksum TLM → VisualConfig. Mirrors types/index.ts ChecksumResponse.visualConfig.
644
+ * Sequence notes + genre frequencies become CircleConfig[] (note, frequency, radius, color).
645
+ */
646
+ function resolveLookKind(opts) {
647
+ const raw = opts && opts.lookKind;
648
+ if (raw) {
649
+ if (!LOOK_KINDS.includes(raw)) {
650
+ const err = new Error(`unknown look "${raw}" (want ${LOOK_KINDS.join("|")})`);
651
+ err.code = "BLIP_LOOK";
652
+ throw err;
653
+ }
654
+ return raw;
655
+ }
656
+ return LOOK_KINDS[seedU32(opts && opts.seedHex, 0) % LOOK_KINDS.length];
657
+ }
658
+
659
+ function buildVisualConfig({ brief, seedHex, genre, width, height, lookKind }) {
660
+ const g = soundRippel.resolveGenre(genre || "ambient");
661
+ const scale = SCALES[g.id] || SCALES.ambient;
662
+ const rng = mulberry32(seedU32(seedHex, 0) ^ seedU32(seedHex, 8));
663
+ const text = String(brief || "factory-blip");
664
+ const count = 6 + (seedU32(seedHex, 16) % 4);
665
+ const circles = [];
666
+ const notes = [];
667
+ const durations = [];
668
+ const velocities = [];
669
+ const frequencies = [];
670
+ for (let i = 0; i < count; i++) {
671
+ const ch = text.charCodeAt(i % text.length) || 32;
672
+ const idx = (ch + Math.floor(rng() * scale.length) + i) % scale.length;
673
+ const frequency = scale[idx];
674
+ const velocity = 0.45 + rng() * 0.5;
675
+ const note = `${NOTE_NAMES[idx % NOTE_NAMES.length]}${3 + (i % 3)}`;
676
+ const radius = 28 + (220 / Math.max(frequency, 40)) * 36 + velocity * 18;
677
+ const color = rgbHex(THEME_CYCLE[i % THEME_CYCLE.length]);
678
+ circles.push({ note, frequency, radius, color });
679
+ notes.push(note);
680
+ durations.push("8n");
681
+ velocities.push(velocity);
682
+ frequencies.push(frequency);
683
+ }
684
+ return {
685
+ tlmCommand: TLM,
686
+ sequence: { notes, durations, velocities },
687
+ genreConfig: {
688
+ tempo: soundRippel.tempoFromSeed(g.id, seedHex),
689
+ phase0: 0,
690
+ instrument: g.voices[0],
691
+ frequencies,
692
+ scale: g.id,
693
+ },
694
+ visualConfig: {
695
+ circles,
696
+ canvas: { width: width || MOTION_WIDTH, height: height || MOTION_HEIGHT, fps: FPS },
697
+ tlmCommand: TLM,
698
+ },
699
+ gematria: {
700
+ promptValue: seedU32(seedHex, 0),
701
+ checksumValue: seedU32(seedHex, 8),
702
+ parity: (seedU32(seedHex, 0) & 1) === 0,
703
+ },
704
+ lookKind: resolveLookKind({ seedHex, lookKind }),
705
+ mesh: buildMesh(seedHex, text),
706
+ field: buildField(seedHex, text),
707
+ };
708
+ }
709
+
710
+ function cachedChecksum(opts) {
711
+ const lookKind = resolveLookKind(opts);
712
+ const key = `${opts.seedHex || ""}::${opts.brief || ""}::${opts.genre || "ambient"}::${opts.width || MOTION_WIDTH}x${opts.height || MOTION_HEIGHT}::${lookKind}`;
713
+ let hit = visualCache.get(key);
714
+ if (!hit) {
715
+ hit = buildVisualConfig({ ...opts, lookKind });
716
+ visualCache.set(key, hit);
717
+ }
718
+ return hit;
719
+ }
720
+
721
+ function fillVoid(buf) {
722
+ const v = THEME.void;
723
+ for (let i = 0; i < buf.length; i += 3) {
724
+ buf[i] = v[0];
725
+ buf[i + 1] = v[1];
726
+ buf[i + 2] = v[2];
727
+ }
728
+ }
729
+
730
+ function mixPixel(buf, width, x, y, color, alpha) {
731
+ if (alpha <= 0) return;
732
+ const xi = x | 0;
733
+ const yi = y | 0;
734
+ if (xi < 0 || yi < 0 || xi >= width) return;
735
+ const height = (buf.length / 3 / width) | 0;
736
+ if (yi >= height) return;
737
+ const i = (yi * width + xi) * 3;
738
+ const a = alpha > 1 ? 1 : alpha;
739
+ buf[i] = (buf[i] + (color[0] - buf[i]) * a + 0.5) | 0;
740
+ buf[i + 1] = (buf[i + 1] + (color[1] - buf[i + 1]) * a + 0.5) | 0;
741
+ buf[i + 2] = (buf[i + 2] + (color[2] - buf[i + 2]) * a + 0.5) | 0;
742
+ }
743
+
744
+ /**
745
+ * Soft falloff over the FULL radius. Confirmed CoS #67 look:
746
+ * hardness ~1.15–2.6 → every stamp is a glow, not a disc. Kept as a
747
+ * primitive; Rippel v2 painters use stampFocusDisc / paintSharpLine.
748
+ */
749
+ function stampDisc(buf, width, height, cx, cy, radius, color, hardness) {
750
+ if (radius <= 0) return;
751
+ const hard = hardness > 0 ? hardness : 2;
752
+ const x0 = Math.max(0, Math.floor(cx - radius));
753
+ const x1 = Math.min(width - 1, Math.ceil(cx + radius));
754
+ const y0 = Math.max(0, Math.floor(cy - radius));
755
+ const y1 = Math.min(height - 1, Math.ceil(cy + radius));
756
+ const r2 = radius * radius;
757
+ for (let y = y0; y <= y1; y++) {
758
+ const dy = y - cy;
759
+ const dy2 = dy * dy;
760
+ for (let x = x0; x <= x1; x++) {
761
+ const dx = x - cx;
762
+ const d2 = dx * dx + dy2;
763
+ if (d2 > r2) continue;
764
+ const u = 1 - Math.sqrt(d2) / radius;
765
+ const a = u <= 0 ? 0 : u >= 1 ? 1 : Math.pow(u, hard);
766
+ mixPixel(buf, width, x, y, color, a);
767
+ }
768
+ }
769
+ }
770
+
771
+ /**
772
+ * In-focus disc: opaque body + thin crisp rim + short glow tail.
773
+ * Not pow(u, ~1.2) across the whole radius (that is the Phase 1 bokeh).
774
+ */
775
+ function stampFocusDisc(buf, width, height, cx, cy, radius, color, opts) {
776
+ if (radius <= 0) return;
777
+ const rimW = opts && opts.rim != null ? opts.rim : 2;
778
+ const glowW = opts && opts.glow != null ? opts.glow : 5;
779
+ const glowA = opts && opts.glowAlpha != null ? opts.glowAlpha : 0.3;
780
+ const rimColor = (opts && opts.rimColor) || THEME.ink;
781
+ const outer = radius + glowW;
782
+ const x0 = Math.max(0, Math.floor(cx - outer));
783
+ const x1 = Math.min(width - 1, Math.ceil(cx + outer));
784
+ const y0 = Math.max(0, Math.floor(cy - outer));
785
+ const y1 = Math.min(height - 1, Math.ceil(cy + outer));
786
+ const bodyR = Math.max(0.5, radius - rimW);
787
+ for (let y = y0; y <= y1; y++) {
788
+ const dy = y - cy;
789
+ const dy2 = dy * dy;
790
+ for (let x = x0; x <= x1; x++) {
791
+ const dx = x - cx;
792
+ const d = Math.sqrt(dx * dx + dy2);
793
+ if (d <= bodyR) {
794
+ mixPixel(buf, width, x, y, color, 1);
795
+ continue;
796
+ }
797
+ if (d <= radius) {
798
+ const u = (radius - d) / Math.max(rimW, 1e-6);
799
+ mixPixel(buf, width, x, y, rimColor, 1);
800
+ mixPixel(buf, width, x, y, color, clamp(u, 0, 1));
801
+ continue;
802
+ }
803
+ if (d <= outer) {
804
+ const u = 1 - (d - radius) / glowW;
805
+ const a = glowA * Math.pow(clamp(u, 0, 1), 3.6);
806
+ mixPixel(buf, width, x, y, color, a);
807
+ }
808
+ }
809
+ }
810
+ }
811
+
812
+ function stampSegment(buf, width, height, x0, y0, x1, y1, radius, color, hardness) {
813
+ const dx = x1 - x0;
814
+ const dy = y1 - y0;
815
+ const len = Math.hypot(dx, dy);
816
+ const steps = Math.max(2, Math.ceil(len / Math.max(1, radius * 0.55)));
817
+ for (let i = 0; i <= steps; i++) {
818
+ const t = i / steps;
819
+ stampDisc(buf, width, height, x0 + dx * t, y0 + dy * t, radius, color, hardness);
820
+ }
821
+ }
822
+
823
+ function stampFocusSegment(buf, width, height, x0, y0, x1, y1, radius, color, opts) {
824
+ const dx = x1 - x0;
825
+ const dy = y1 - y0;
826
+ const len = Math.hypot(dx, dy);
827
+ const steps = Math.max(2, Math.ceil(len / Math.max(1.2, radius * 0.75)));
828
+ const focus = opts || { rim: 1, glow: 2, glowAlpha: 0.18, rimColor: THEME.ink };
829
+ for (let i = 0; i <= steps; i++) {
830
+ const u = i / steps;
831
+ stampFocusDisc(buf, width, height, x0 + dx * u, y0 + dy * u, radius, color, focus);
832
+ }
833
+ }
834
+
835
+ function paintSharpRibbon(buf, width, height, yAtX, color, half, rimColor) {
836
+ const rim = rimColor || THEME.ink;
837
+ const thick = Math.max(1, half);
838
+ for (let x = 0; x < width; x++) {
839
+ const y = yAtX(x);
840
+ for (let d = -thick; d <= thick; d++) {
841
+ mixPixel(buf, width, x, y + d, color, 1);
842
+ }
843
+ mixPixel(buf, width, x, y - thick - 1, rim, 1);
844
+ mixPixel(buf, width, x, y + thick + 1, rim, 1);
845
+ }
846
+ }
847
+
848
+ function visualizationFor(renderer) {
849
+ return ANIMATION_TO_VISUALIZATION[renderer] || null;
850
+ }
851
+
852
+ function circlePulse(circle, t) {
853
+ const hz = Math.max(20, circle.frequency);
854
+ const lfo = Math.sin(2 * Math.PI * (hz / 220) * t);
855
+ const overtone = Math.sin(2 * Math.PI * (hz / 110) * t + 0.7);
856
+ return circle.radius * (0.7 + 0.22 * (0.5 + 0.5 * lfo) + 0.08 * overtone);
857
+ }
858
+
859
+ function beatPhase(checksum, t) {
860
+ const bpm = (checksum.genreConfig && checksum.genreConfig.tempo) || 90;
861
+ const phase0 = (checksum.genreConfig && checksum.genreConfig.phase0) || 0;
862
+ return (t * bpm) / 60 + phase0;
863
+ }
864
+
865
+ /** Soft kick swell — scale only, never a full-field color gate (photosensitive). */
866
+ function kickAccent(beat) {
867
+ const frac = beat - Math.floor(beat);
868
+ if (frac < 0.12) return 0.55 * (1 - frac / 0.12);
869
+ if (frac < 0.28) return 0.18 * (1 - (frac - 0.12) / 0.16);
870
+ return 0;
871
+ }
872
+
873
+ /** Off-beat (the AND) — same grid as the bed hat/cowbell. Scale only. */
874
+ function andAccent(beat) {
875
+ const frac = beat - Math.floor(beat);
876
+ const d = Math.abs(frac - 0.5);
877
+ if (d < 0.1) return 0.45 * (1 - d / 0.1);
878
+ return 0;
879
+ }
880
+
881
+ function phraseOf(checksum, t) {
882
+ const tempo = (checksum && checksum.genreConfig && checksum.genreConfig.tempo) || 90;
883
+ const seconds =
884
+ (checksum &&
885
+ checksum.visualConfig &&
886
+ checksum.visualConfig.canvas &&
887
+ checksum.visualConfig.canvas.duration) ||
888
+ 4.44;
889
+ return soundRippel.shortformPhrase(t, seconds, { beatSec: 60 / tempo });
890
+ }
891
+
892
+ function phraseWeight(phrase, key) {
893
+ const ease = phrase && phrase[`${key}Ease`];
894
+ if (typeof ease === "number") return ease;
895
+ return (phrase && phrase[key]) || 0;
896
+ }
897
+
898
+ function phraseMix(phrase, hookV, turnV, tagV) {
899
+ return (
900
+ hookV * phraseWeight(phrase, "hook") +
901
+ turnV * phraseWeight(phrase, "turn") +
902
+ tagV * phraseWeight(phrase, "tag")
903
+ );
904
+ }
905
+
906
+ function mixRgb(a, b, amount) {
907
+ const u = clamp(amount, 0, 1);
908
+ return [
909
+ (a[0] + (b[0] - a[0]) * u + 0.5) | 0,
910
+ (a[1] + (b[1] - a[1]) * u + 0.5) | 0,
911
+ (a[2] + (b[2] - a[2]) * u + 0.5) | 0,
912
+ ];
913
+ }
914
+
915
+ /** Slow frequency wander (~0.3–0.8 Hz). Not a hard gold flash. */
916
+ function freqTint(circle, t) {
917
+ const hz = Math.max(20, circle.frequency);
918
+ return 0.22 + 0.28 * (0.5 + 0.5 * Math.sin(2 * Math.PI * (hz / 1100) * t));
919
+ }
920
+
921
+ function paintSharpLine(buf, width, height, x0, y0, x1, y1, color, half, rimColor) {
922
+ const rim = rimColor || THEME.ink;
923
+ const thick = Math.max(1, half);
924
+ const dx = x1 - x0;
925
+ const dy = y1 - y0;
926
+ const len = Math.hypot(dx, dy);
927
+ if (len < 0.5) {
928
+ stampFocusDisc(buf, width, height, x0, y0, thick + 1, color, {
929
+ rim: 1,
930
+ glow: 2,
931
+ glowAlpha: 0.16,
932
+ rimColor: rim,
933
+ });
934
+ return;
935
+ }
936
+ const steps = Math.ceil(len);
937
+ const nx = -dy / len;
938
+ const ny = dx / len;
939
+ for (let i = 0; i <= steps; i++) {
940
+ const u = i / steps;
941
+ const x = x0 + dx * u;
942
+ const y = y0 + dy * u;
943
+ for (let d = -thick; d <= thick; d++) {
944
+ mixPixel(buf, width, x + nx * d, y + ny * d, color, 1);
945
+ }
946
+ mixPixel(buf, width, x + nx * (thick + 1), y + ny * (thick + 1), rim, 1);
947
+ mixPixel(buf, width, x - nx * (thick + 1), y - ny * (thick + 1), rim, 1);
948
+ }
949
+ }
950
+
951
+ function fillTri(buf, width, height, a, b, c, color, alpha) {
952
+ if (alpha <= 0) return;
953
+ const minX = Math.max(0, Math.floor(Math.min(a.x, b.x, c.x)));
954
+ const maxX = Math.min(width - 1, Math.ceil(Math.max(a.x, b.x, c.x)));
955
+ const minY = Math.max(0, Math.floor(Math.min(a.y, b.y, c.y)));
956
+ const maxY = Math.min(height - 1, Math.ceil(Math.max(a.y, b.y, c.y)));
957
+ if (maxX - minX > 420 || maxY - minY > 320) return;
958
+ const area = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
959
+ if (Math.abs(area) < 8) return;
960
+ const a0 = alpha * (area > 0 ? 1 : 0.38);
961
+ for (let y = minY; y <= maxY; y++) {
962
+ for (let x = minX; x <= maxX; x++) {
963
+ const w0 = (b.x - x) * (c.y - y) - (c.x - x) * (b.y - y);
964
+ const w1 = (c.x - x) * (a.y - y) - (a.x - x) * (c.y - y);
965
+ const w2 = (a.x - x) * (b.y - y) - (b.x - x) * (a.y - y);
966
+ if ((w0 >= 0 && w1 >= 0 && w2 >= 0) || (w0 <= 0 && w1 <= 0 && w2 <= 0)) {
967
+ mixPixel(buf, width, x, y, color, a0);
968
+ }
969
+ }
970
+ }
971
+ }
972
+
973
+ function meshAccent(mesh, t) {
974
+ const cuts = mesh.cuts || 3;
975
+ const phase = ((t / 4.44) * cuts + (mesh.accentIndex || 0)) % LIGHT_CYCLE.length;
976
+ const i = phase | 0;
977
+ return mixRgb(LIGHT_CYCLE[i], LIGHT_CYCLE[(i + 1) % LIGHT_CYCLE.length], phase - i);
978
+ }
979
+
980
+ function iridesce(u, t, beat, accent) {
981
+ const cycle = [THEME.cyan, accent || THEME.gold, THEME.gold, THEME.blue];
982
+ const phase = ((u * 0.94 + beat * 0.06 + t * 0.035) % 1 + 1) % 1;
983
+ const p = phase * (cycle.length - 1);
984
+ const i = p | 0;
985
+ const f = p - i;
986
+ return mixRgb(cycle[i], cycle[i + 1] || cycle[cycle.length - 1], f);
987
+ }
988
+
989
+ function livingShade(mesh, t, u, beat, bias) {
990
+ const c = iridesce(u, t, beat, meshAccent(mesh, t));
991
+ return bias ? mixRgb(c, bias, 0.08) : c;
992
+ }
993
+
994
+ /** Hairline Wu stroke: 1px core, coverage AA as the only glow. Gradient via shader(u). */
995
+ function paintGlowLine(buf, width, height, x0, y0, x1, y1, color, opts) {
996
+ const shader = opts && opts.shader;
997
+ const glowA = opts && opts.glowAlpha != null ? opts.glowAlpha : 0.08;
998
+ let ax = x0;
999
+ let ay = y0;
1000
+ let bx = x1;
1001
+ let by = y1;
1002
+ const steep = Math.abs(by - ay) > Math.abs(bx - ax);
1003
+ if (steep) {
1004
+ const sx = ax;
1005
+ ax = ay;
1006
+ ay = sx;
1007
+ const ex = bx;
1008
+ bx = by;
1009
+ by = ex;
1010
+ }
1011
+ if (ax > bx) {
1012
+ const sx = ax;
1013
+ ax = bx;
1014
+ bx = sx;
1015
+ const sy = ay;
1016
+ ay = by;
1017
+ by = sy;
1018
+ }
1019
+ const wdx = bx - ax;
1020
+ const wdy = by - ay;
1021
+ const grad = wdx === 0 ? 0 : wdy / wdx;
1022
+ function plot(px, py, a, u) {
1023
+ if (a <= 0.03) return;
1024
+ const c = shader ? shader(u) : color;
1025
+ if (steep) mixPixel(buf, width, py, px, c, a);
1026
+ else mixPixel(buf, width, px, py, c, a);
1027
+ }
1028
+ let y = ay;
1029
+ const x0i = Math.round(ax);
1030
+ const x1i = Math.round(bx);
1031
+ const span = x1i - x0i || 1;
1032
+ for (let x = x0i; x <= x1i; x++) {
1033
+ const u = (x - x0i) / span;
1034
+ const yi = Math.floor(y);
1035
+ const f = y - yi;
1036
+ plot(x, yi, 1, u);
1037
+ plot(x, yi + 1, f * 0.35 + glowA, u);
1038
+ y += grad;
1039
+ }
1040
+ }
1041
+
1042
+ function paintMeshFaces(buf, width, height, mesh, pts, color, alpha) {
1043
+ if (!mesh.faces || !mesh.faces.length || alpha <= 0) return;
1044
+ const ranked = mesh.faces
1045
+ .map((f) => {
1046
+ const a = pts[f[0]];
1047
+ const b = pts[f[1]];
1048
+ const c = pts[f[2]];
1049
+ return { a, b, c, z: (a.z + b.z + c.z) / 3 };
1050
+ })
1051
+ .sort((p, q) => p.z - q.z);
1052
+ for (let i = 0; i < ranked.length; i++) {
1053
+ fillTri(buf, width, height, ranked[i].a, ranked[i].b, ranked[i].c, color, alpha);
1054
+ }
1055
+ }
1056
+
1057
+ function paintMesh(buf, width, height, mesh, pts, color, half, opts) {
1058
+ const t = (opts && opts.t) || 0;
1059
+ const checksum = (opts && opts.checksum) || { genreConfig: { tempo: 90 } };
1060
+ const beat = beatPhase(checksum, t);
1061
+ const kick = kickAccent(beat);
1062
+ const and = andAccent(beat);
1063
+ const fillA = opts && opts.fill != null ? opts.fill : 0;
1064
+ if (fillA > 0) paintMeshFaces(buf, width, height, mesh, pts, color, fillA);
1065
+ const ranked = mesh.edges
1066
+ .map((e, i) => ({ e, i, z: (pts[e[0]].z + pts[e[1]].z) * 0.5 }))
1067
+ .sort((a, b) => a.z - b.z);
1068
+ for (let i = 0; i < ranked.length; i++) {
1069
+ const [a, b] = ranked[i].e;
1070
+ const bias = color || null;
1071
+ paintGlowLine(buf, width, height, pts[a].x, pts[a].y, pts[b].x, pts[b].y, THEME.cyan, {
1072
+ glowAlpha: 0.07 + 0.03 * kick,
1073
+ shader: (u) => livingShade(mesh, t, u, beat, bias),
1074
+ });
1075
+ }
1076
+ if (ranked.length) {
1077
+ const hot = ranked[0];
1078
+ const [a, b] = hot.e;
1079
+ const life = (beat * 0.7 + mesh.twist) % 1;
1080
+ mixPixel(
1081
+ buf,
1082
+ width,
1083
+ pts[a].x + (pts[b].x - pts[a].x) * life,
1084
+ pts[a].y + (pts[b].y - pts[a].y) * life,
1085
+ livingShade(mesh, t, life, beat, THEME.gold),
1086
+ 0.85 + 0.15 * and,
1087
+ );
1088
+ }
1089
+ }
1090
+
1091
+ function paintChecksumMesh(buf, width, height, t, checksum, opts) {
1092
+ const mesh = checksum.mesh;
1093
+ if (!mesh) return null;
1094
+ const scale = (opts && opts.scale) || 1;
1095
+ const half = (opts && opts.half) || 1;
1096
+ const pts = projectMesh(mesh, width, height, t, checksum, scale);
1097
+ paintMesh(buf, width, height, mesh, pts, opts && opts.color, half, {
1098
+ fill: 0,
1099
+ t,
1100
+ checksum,
1101
+ });
1102
+ return { mesh, pts, dual: null };
1103
+ }
1104
+
1105
+ function paintMeshOverlay(buf, width, height, opts) {
1106
+ const checksum = cachedChecksum({
1107
+ brief: opts.brief,
1108
+ seedHex: opts.seedHex,
1109
+ genre: opts.genre,
1110
+ width,
1111
+ height,
1112
+ lookKind: opts.lookKind,
1113
+ });
1114
+ paintField(buf, width, height, opts.t || 0, checksum);
1115
+ return paintChecksumMesh(buf, width, height, opts.t || 0, checksum, {
1116
+ scale: 0.72,
1117
+ half: 1,
1118
+ });
1119
+ }
1120
+
1121
+ function paintGradient(buf, width, height, field, t) {
1122
+ const aCol = field.gradColor || THEME.blue;
1123
+ const bCol = mixRgb(THEME.gold, THEME.cyan, 0.45 + 0.2 * Math.sin((t || 0) * 0.7));
1124
+ const s = field.gradStrength || 0.2;
1125
+ if (field.gradient === "horizon") {
1126
+ const y0 = (height * 0.58) | 0;
1127
+ for (let y = y0; y < height; y++) {
1128
+ const u = (y - y0) / Math.max(1, height - y0);
1129
+ const c = mixRgb(aCol, bCol, u);
1130
+ const a = s * (0.35 + 0.65 * u);
1131
+ for (let x = 0; x < width; x += 1) mixPixel(buf, width, x, y, c, a);
1132
+ }
1133
+ return;
1134
+ }
1135
+ if (field.gradient === "corner") {
1136
+ const cx = field.stars[0] ? field.stars[0].x * width : width * 0.12;
1137
+ const cy = field.stars[0] ? field.stars[0].y * height : height * 0.18;
1138
+ const reach = Math.min(width, height) * 0.72;
1139
+ const x0 = Math.max(0, (cx - reach) | 0);
1140
+ const x1 = Math.min(width - 1, (cx + reach) | 0);
1141
+ const y0 = Math.max(0, (cy - reach) | 0);
1142
+ const y1 = Math.min(height - 1, (cy + reach) | 0);
1143
+ for (let y = y0; y <= y1; y++) {
1144
+ for (let x = x0; x <= x1; x++) {
1145
+ const d = Math.hypot(x - cx, y - cy) / reach;
1146
+ if (d >= 1) continue;
1147
+ const fade = (1 - d) * (1 - d);
1148
+ mixPixel(buf, width, x, y, mixRgb(aCol, bCol, fade), s * fade);
1149
+ }
1150
+ }
1151
+ return;
1152
+ }
1153
+ const y1 = (height * 0.28) | 0;
1154
+ for (let y = 0; y < y1; y++) {
1155
+ const u = 1 - y / y1;
1156
+ const c = mixRgb(THEME.cyan, aCol, 0.35 + 0.4 * u);
1157
+ for (let x = 0; x < width; x += 1) mixPixel(buf, width, x, y, c, s * u);
1158
+ }
1159
+ }
1160
+
1161
+ function paintGrid(buf, width, height, field, t, checksum) {
1162
+ if (!field.grid || field.grid === "none") return;
1163
+ const beat = beatPhase(checksum || { genreConfig: { tempo: 90 } }, t || 0);
1164
+ const accent = field.gridColor || THEME.cyan;
1165
+ const line = (x0, y0, x1, y1) =>
1166
+ paintGlowLine(buf, width, height, x0, y0, x1, y1, accent, {
1167
+ glowAlpha: 0.07,
1168
+ shader: (u) => iridesce(u, t || 0, beat, accent),
1169
+ });
1170
+ const cx = (width - 1) * 0.5;
1171
+ if (field.grid === "ticks") {
1172
+ for (let i = 0; i < 9; i++) {
1173
+ const x = ((i + 1) / 10) * width;
1174
+ line(x, 8, x, 28);
1175
+ line(x, height - 28, x, height - 8);
1176
+ }
1177
+ for (let i = 0; i < 5; i++) {
1178
+ const y = ((i + 1) / 6) * height;
1179
+ line(8, y, 28, y);
1180
+ line(width - 28, y, width - 8, y);
1181
+ }
1182
+ return;
1183
+ }
1184
+ if (field.grid === "meridian") {
1185
+ line(28, 28, width - 28, 28);
1186
+ line(28, height - 28, width - 28, height - 28);
1187
+ line(28, 28, 28, height - 28);
1188
+ line(width - 28, 28, width - 28, height - 28);
1189
+ const inset = width * 0.17;
1190
+ line(inset, 40, inset * 0.82, height - 40);
1191
+ line(width - inset, 40, width - inset * 0.82, height - 40);
1192
+ return;
1193
+ }
1194
+ const vanishY = height * 0.64;
1195
+ const floorY = height * 0.76;
1196
+ line(0, floorY, width, floorY);
1197
+ for (let i = -4; i <= 4; i++) {
1198
+ if (i === 0) continue;
1199
+ line(cx + i * width * 0.05, vanishY, cx + i * width * 0.26, height - 6);
1200
+ }
1201
+ for (let k = 1; k <= 3; k++) {
1202
+ const y = floorY + (height - 8 - floorY) * (k / 4);
1203
+ const span = width * (0.24 + k * 0.14);
1204
+ line(cx - span, y, cx + span, y);
1205
+ }
1206
+ }
1207
+
1208
+ function paintField(buf, width, height, t, checksum) {
1209
+ const field = checksum.field;
1210
+ if (!field) return;
1211
+ const beat = beatPhase(checksum, t);
1212
+ const kick = kickAccent(beat);
1213
+ const and = andAccent(beat);
1214
+ paintGradient(buf, width, height, field, t);
1215
+ paintGrid(buf, width, height, field, t, checksum);
1216
+ const stars = field.stars || [];
1217
+ for (let i = 0; i < stars.length; i++) {
1218
+ const star = stars[i];
1219
+ const twinkle = 0.42 + 0.5 * (0.5 + 0.5 * Math.sin(beat * Math.PI * 2 + star.phase));
1220
+ const sx = star.x * width;
1221
+ const sy = star.y * height;
1222
+ const color = iridesce(star.phase, t, beat, star.color);
1223
+ mixPixel(buf, width, sx, sy, color, twinkle);
1224
+ mixPixel(buf, width, sx + 1, sy, color, twinkle * 0.35);
1225
+ mixPixel(buf, width, sx, sy + 1, color, twinkle * 0.35);
1226
+ }
1227
+ const blinkers = field.blinkers || [];
1228
+ for (let i = 0; i < blinkers.length; i++) {
1229
+ const b = blinkers[i];
1230
+ const on = b.onAnd ? and : kick;
1231
+ if (on < 0.08) continue;
1232
+ stampFocusDisc(buf, width, height, b.x * width, b.y * height, 1.8 + on * 1.4, b.color, {
1233
+ rim: 1,
1234
+ glow: 2,
1235
+ glowAlpha: 0.16,
1236
+ rimColor: THEME.ink,
1237
+ });
1238
+ }
1239
+ }
1240
+
1241
+ function startFrame(buf, width, height, t, checksum) {
1242
+ fillVoid(buf);
1243
+ paintField(buf, width, height, t, checksum);
1244
+ }
1245
+
1246
+ function layoutRings(circles, width, height, t, checksum) {
1247
+ const cx = (width - 1) * 0.5;
1248
+ const cy = (height - 1) * 0.5;
1249
+ const minSide = Math.min(width, height);
1250
+ const beat = beatPhase(checksum, t);
1251
+ const scramble = mulberry32(
1252
+ ((checksum.gematria && checksum.gematria.promptValue) || 1) ^
1253
+ ((checksum.gematria && checksum.gematria.checksumValue) || 1) ^
1254
+ 0x9e3779b9,
1255
+ );
1256
+ return circles.map((circle, i) => {
1257
+ const inner = i % 2 === 0;
1258
+ const phase = scramble() * Math.PI * 2;
1259
+ const ecc = 0.62 + scramble() * 0.22;
1260
+ const drift = 0.85 + scramble() * 0.7;
1261
+ const spin = inner ? -t * 3.6 * drift - beat * 0.22 : t * 2.3 * drift + beat * 0.14;
1262
+ const ang = (i / circles.length) * Math.PI * 2 + spin + phase;
1263
+ const wobble = Math.sin(beat * Math.PI * 2 + phase) * minSide * 0.007;
1264
+ const orbit = minSide * (inner ? 0.18 + scramble() * 0.05 : 0.29 + scramble() * 0.05) + wobble + (circle.frequency / 2000) * minSide * 0.04;
1265
+ return {
1266
+ circle,
1267
+ inner,
1268
+ x: cx + Math.cos(ang) * orbit,
1269
+ y: cy + Math.sin(ang) * orbit * ecc,
1270
+ r: circlePulse(circle, t + phase * 0.2),
1271
+ color: parseHex(circle.color),
1272
+ };
1273
+ });
1274
+ }
1275
+
1276
+ function layoutRing(circles, width, height, t, checksum) {
1277
+ return layoutRings(circles, width, height, t, checksum || { genreConfig: { tempo: 90 } });
1278
+ }
1279
+
1280
+ /** Lost sharp-dogfood ring: two orbital bands, stampFocusDisc satellites, beat-locked spin. */
1281
+ function layoutFocusRing(circles, width, height, t, checksum) {
1282
+ const cx = (width - 1) * 0.5;
1283
+ const cy = (height - 1) * 0.5;
1284
+ const minSide = Math.min(width, height);
1285
+ const beat = beatPhase(checksum, t);
1286
+ const phrase = phraseOf(checksum, t);
1287
+ const spin = t * phraseMix(phrase, 0.22, 0.52, 0.16) + beat * Math.PI * 0.12;
1288
+ const orbitMul = phraseMix(phrase, 0.8, 1.08 + phrase.turnHit * 0.1, 0.64);
1289
+ const rMul = phraseMix(phrase, 0.42, 0.42, 0.3);
1290
+ return circles.map((circle, i) => {
1291
+ const ang = (i / circles.length) * Math.PI * 2 + spin;
1292
+ const orbit = minSide * (0.16 + (i % 3) * 0.07) * orbitMul;
1293
+ return {
1294
+ circle,
1295
+ x: cx + Math.cos(ang) * orbit,
1296
+ y: cy + Math.sin(ang) * orbit * 0.72,
1297
+ r: circlePulse(circle, t + i * 0.11) * (rMul / 0.42),
1298
+ color: parseHex(circle.color),
1299
+ };
1300
+ });
1301
+ }
1302
+
1303
+ function paintFocusSatellites(buf, width, height, t, checksum) {
1304
+ if (!checksum || checksum.lookKind !== "focus") return;
1305
+ const circles = checksum.visualConfig && checksum.visualConfig.circles;
1306
+ if (!circles || !circles.length) return;
1307
+ for (const placed of layoutFocusRing(circles, width, height, t, checksum)) {
1308
+ stampFocusDisc(buf, width, height, placed.x, placed.y, placed.r * 0.42, placed.color, {
1309
+ rim: 1.6,
1310
+ glow: 4,
1311
+ glowAlpha: 0.24,
1312
+ rimColor: THEME.ink,
1313
+ });
1314
+ }
1315
+ }
1316
+
1317
+ /** Field under focus: gradient + stars + blinkers. No grid through the disc. */
1318
+ function paintFocusField(buf, width, height, t, checksum) {
1319
+ const field = checksum && checksum.field;
1320
+ if (!field) return;
1321
+ paintGradient(buf, width, height, field, t);
1322
+ const beat = beatPhase(checksum, t);
1323
+ const kick = kickAccent(beat);
1324
+ const and = andAccent(beat);
1325
+ const stars = field.stars || [];
1326
+ for (let i = 0; i < stars.length; i++) {
1327
+ const star = stars[i];
1328
+ const twinkle = 0.42 + 0.5 * (0.5 + 0.5 * Math.sin(beat * Math.PI * 2 + star.phase));
1329
+ const sx = star.x * width;
1330
+ const sy = star.y * height;
1331
+ const color = iridesce(star.phase, t, beat, star.color);
1332
+ mixPixel(buf, width, sx, sy, color, twinkle);
1333
+ mixPixel(buf, width, sx + 1, sy, color, twinkle * 0.35);
1334
+ mixPixel(buf, width, sx, sy + 1, color, twinkle * 0.35);
1335
+ }
1336
+ const blinkers = field.blinkers || [];
1337
+ for (let i = 0; i < blinkers.length; i++) {
1338
+ const b = blinkers[i];
1339
+ const on = b.onAnd ? and : kick;
1340
+ if (on < 0.08) continue;
1341
+ stampFocusDisc(buf, width, height, b.x * width, b.y * height, 1.8 + on * 1.4, b.color, {
1342
+ rim: 1,
1343
+ glow: 2,
1344
+ glowAlpha: 0.16,
1345
+ rimColor: THEME.ink,
1346
+ });
1347
+ }
1348
+ }
1349
+
1350
+ /** Mesh fingerprint as solid mass — verts only, off the equator so orbFocusWidth stays honest. */
1351
+ function paintFocusMeshMass(buf, width, height, t, checksum) {
1352
+ const mesh = checksum && checksum.mesh;
1353
+ if (!mesh || !mesh.verts || !mesh.verts.length) return;
1354
+ const pts = projectMesh(mesh, width, height, t, checksum, 0.78);
1355
+ const cx = (width - 1) * 0.5;
1356
+ const cy = (height - 1) * 0.5;
1357
+ const keep = Math.min(width, height) * 0.16;
1358
+ for (let i = 0; i < pts.length; i++) {
1359
+ const p = pts[i];
1360
+ if (Math.abs(p.y - cy) < keep * 0.38 && Math.abs(p.x - cx) < keep * 1.15) continue;
1361
+ const color = THEME_CYCLE[((mesh.accentIndex || 0) + i) % THEME_CYCLE.length];
1362
+ stampFocusDisc(buf, width, height, p.x, p.y, 3.6 + (i % 3) * 0.8, color, {
1363
+ rim: 1.1,
1364
+ glow: 2.6,
1365
+ glowAlpha: 0.18,
1366
+ rimColor: THEME.ink,
1367
+ });
1368
+ }
1369
+ }
1370
+
1371
+ /** orb focus — cyan disc + gold pupil + satellites. Field + mesh mass carry the mint id. */
1372
+ function paintFocusOrb(buf, width, height, t, checksum) {
1373
+ fillVoid(buf);
1374
+ paintFocusField(buf, width, height, t, checksum);
1375
+ paintFocusMeshMass(buf, width, height, t, checksum);
1376
+ const cx = (width - 1) * 0.5;
1377
+ const cy = (height - 1) * 0.5;
1378
+ const minSide = Math.min(width, height);
1379
+ const phrase = phraseOf(checksum, t);
1380
+ const mesh = checksum && checksum.mesh;
1381
+ const style = (mesh && mesh.coreStyle) || "disc";
1382
+ const pulse = style === "pulse" ? 0.012 : 0;
1383
+ const core =
1384
+ minSide *
1385
+ (0.11 +
1386
+ 0.018 * Math.sin(t * 1.7) +
1387
+ 0.018 * phrase.turnHit +
1388
+ 0.01 * phraseWeight(phrase, "tag") +
1389
+ pulse);
1390
+ stampFocusDisc(buf, width, height, cx, cy, core * 1.08, THEME.cyan, {
1391
+ rim: 2.2,
1392
+ glow: 7,
1393
+ glowAlpha: 0.28,
1394
+ rimColor: THEME.ink,
1395
+ });
1396
+ stampFocusDisc(buf, width, height, cx, cy, core * 0.4, THEME.gold, {
1397
+ rim: 1.6,
1398
+ glow: 3,
1399
+ glowAlpha: 0.22,
1400
+ rimColor: THEME.gold,
1401
+ });
1402
+ paintFocusSatellites(buf, width, height, t, checksum);
1403
+ }
1404
+
1405
+ /** orb → canvas / Orb Glow v2. Seed look: focus (disc satellites) or cage (Wu + field). */
1406
+ function paintCanvas(buf, width, height, t, checksum) {
1407
+ if (checksum.lookKind === "focus") {
1408
+ return paintFocusOrb(buf, width, height, t, checksum);
1409
+ }
1410
+ startFrame(buf, width, height, t, checksum);
1411
+ paintChecksumMesh(buf, width, height, t, checksum, {
1412
+ scale: 0.95,
1413
+ half: 1,
1414
+ noFill: true,
1415
+ });
1416
+ const cx = (width - 1) * 0.5;
1417
+ const cy = (height - 1) * 0.5;
1418
+ const minSide = Math.min(width, height);
1419
+ const beat = beatPhase(checksum, t);
1420
+ const nucleus = paintOrbNucleus(buf, width, height, cx, cy, minSide, beat, checksum.mesh, checksum);
1421
+ const tickR = nucleus.radius;
1422
+ const tickA = beat * Math.PI * 2;
1423
+ stampFocusDisc(
1424
+ buf,
1425
+ width,
1426
+ height,
1427
+ cx + Math.cos(tickA) * tickR,
1428
+ cy + Math.sin(tickA) * tickR,
1429
+ 5,
1430
+ THEME.gold,
1431
+ { rim: 1.2, glow: 3, glowAlpha: 0.2, rimColor: THEME.ink },
1432
+ );
1433
+ const ghostA = tickA - 0.55;
1434
+ stampFocusDisc(
1435
+ buf,
1436
+ width,
1437
+ height,
1438
+ cx + Math.cos(ghostA) * tickR,
1439
+ cy + Math.sin(ghostA) * tickR,
1440
+ 3,
1441
+ THEME.ink,
1442
+ { rim: 1, glow: 2, glowAlpha: 0.12, rimColor: THEME.ink },
1443
+ );
1444
+ }
1445
+
1446
+ /** Orb-only nucleus. Other viz do not wear this bullseye. Color cuts + size on the grid; seed picks disc/eclipse/pulse. */
1447
+ function paintOrbNucleus(buf, width, height, cx, cy, minSide, beat, mesh, checksum) {
1448
+ const kick = kickAccent(beat);
1449
+ const and = andAccent(beat);
1450
+ const style = (mesh && mesh.coreStyle) || "disc";
1451
+ const swell = style === "pulse" ? 0.55 + 0.55 * kick + 0.35 * and : 0.5 + 0.35 * kick + 0.22 * and;
1452
+ const core = minSide * (style === "pulse" ? 0.092 + 0.028 * swell : 0.1 + 0.014 * swell);
1453
+ const t = checksum && checksum.genreConfig ? (beat * 60) / (checksum.genreConfig.tempo || 90) : beat;
1454
+ const outer = mixRgb(THEME.cyan, (mesh && meshAccent(mesh, t)) || THEME.gold, 0.28 + 0.45 * and);
1455
+ const inner = mixRgb(THEME.gold, THEME.cyan, 0.18 + 0.62 * and);
1456
+ stampFocusDisc(buf, width, height, cx, cy, core * 1.08, outer, {
1457
+ rim: 2.2,
1458
+ glow: 5,
1459
+ glowAlpha: 0.18,
1460
+ rimColor: THEME.ink,
1461
+ });
1462
+ const ix = style === "eclipse" ? cx + Math.cos(beat * Math.PI * 2) * core * 0.22 : cx;
1463
+ const iy = style === "eclipse" ? cy + Math.sin(beat * Math.PI) * core * 0.12 : cy;
1464
+ stampFocusDisc(buf, width, height, ix, iy, core * (style === "eclipse" ? 0.48 : 0.4), inner, {
1465
+ rim: 1.6,
1466
+ glow: 5,
1467
+ glowAlpha: 0.28,
1468
+ rimColor: THEME.ink,
1469
+ });
1470
+ return { radius: core * 1.08, style };
1471
+ }
1472
+
1473
+ /** Solid cyan/gold disc from center, then short rim. Mesh edges past the disc are not the body. */
1474
+ function orbFocusWidth(buf, width, height) {
1475
+ const cx = (width - 1) * 0.5;
1476
+ const cy = ((buf.length / 3 / width) | 0) * 0.5;
1477
+ const y = cy | 0;
1478
+ function sample(x) {
1479
+ const i = (y * width + (x | 0)) * 3;
1480
+ return [buf[i], buf[i + 1], buf[i + 2]];
1481
+ }
1482
+ function luma(x) {
1483
+ const [r, g, b] = sample(x);
1484
+ return (r * 0.3 + g * 0.59 + b * 0.11) / 255;
1485
+ }
1486
+ function isBody(x) {
1487
+ const [r, g, b] = sample(x);
1488
+ const cyan = g > 160 && b > 160 && r < 160;
1489
+ const gold = r > 180 && g > 140 && b < 110;
1490
+ const ink = r > 200 && g > 200 && b > 200;
1491
+ return cyan || gold || ink;
1492
+ }
1493
+ const peak = luma(cx);
1494
+ let hi = cx;
1495
+ for (let x = cx; x < width; x++) {
1496
+ if (isBody(x)) hi = x;
1497
+ else break;
1498
+ }
1499
+ let lo = hi;
1500
+ for (let x = hi; x < width; x++) {
1501
+ if (luma(x) <= 0.12) {
1502
+ lo = x;
1503
+ break;
1504
+ }
1505
+ if (!isBody(x)) {
1506
+ lo = x;
1507
+ break;
1508
+ }
1509
+ lo = x;
1510
+ }
1511
+ return { peak, inner: hi - cx, drop: lo - hi };
1512
+ }
1513
+
1514
+ /** swirl → 3d-sacred v2. Seed mesh is the sacred body — lines, not vertex beads. */
1515
+ function paintSacred(buf, width, height, t, checksum) {
1516
+ startFrame(buf, width, height, t, checksum);
1517
+ paintChecksumMesh(buf, width, height, t, checksum, {
1518
+ scale: 0.92,
1519
+ half: 1,
1520
+ });
1521
+ paintFocusSatellites(buf, width, height, t, checksum);
1522
+ }
1523
+
1524
+ /** snap → neural v2. Dual-ring lattice, hub, skip-links, frequency + beat pulses. */
1525
+ function paintNeural(buf, width, height, t, checksum) {
1526
+ startFrame(buf, width, height, t, checksum);
1527
+ const cx = (width - 1) * 0.5;
1528
+ const cy = (height - 1) * 0.5;
1529
+ const beat = beatPhase(checksum, t);
1530
+ const kick = kickAccent(beat);
1531
+ const worn = paintChecksumMesh(buf, width, height, t, checksum, {
1532
+ scale: 0.9,
1533
+ half: 1,
1534
+ });
1535
+ const pts = (worn && worn.pts) || [];
1536
+ const mesh = checksum.mesh;
1537
+ const node = { rim: 1.4, glow: 3, glowAlpha: 0.18, rimColor: THEME.ink };
1538
+ if (mesh && pts.length) {
1539
+ paintFocusSatellites(buf, width, height, t, checksum);
1540
+ return;
1541
+ }
1542
+ const placed = layoutRings(checksum.visualConfig.circles, width, height, t * 1.55, checksum);
1543
+ for (let i = 0; i < placed.length; i++) {
1544
+ const wander = Math.sin(t * 3.4 + i * 2.1) * 16;
1545
+ const a = {
1546
+ ...placed[i],
1547
+ x: placed[i].x + wander,
1548
+ y: placed[i].y + Math.cos(t * 2.7 + i) * 10,
1549
+ };
1550
+ const rawB = placed[(i + 1) % placed.length];
1551
+ const rawSkip = placed[(i + 2) % placed.length];
1552
+ const b = { x: rawB.x - wander * 0.3, y: rawB.y };
1553
+ const skip = { x: rawSkip.x, y: rawSkip.y + wander * 0.2 };
1554
+ paintSharpLine(buf, width, height, a.x, a.y, b.x, b.y, THEME.blue, 1);
1555
+ paintSharpLine(buf, width, height, a.x, a.y, skip.x, skip.y, THEME.blue, 1);
1556
+ paintSharpLine(buf, width, height, cx, cy, a.x, a.y, THEME.blue, 1);
1557
+ const speed = 1.8 + a.circle.frequency / 140;
1558
+ const travel = (t * speed + i * 0.17) % 1;
1559
+ const inbound = (t * speed * 1.35 + beat * 0.08 + i * 0.41) % 1;
1560
+ const third = (t * speed * 0.55 + i * 0.63) % 1;
1561
+ stampFocusDisc(
1562
+ buf,
1563
+ width,
1564
+ height,
1565
+ a.x + (b.x - a.x) * travel,
1566
+ a.y + (b.y - a.y) * travel,
1567
+ 5,
1568
+ THEME.gold,
1569
+ node,
1570
+ );
1571
+ stampFocusDisc(
1572
+ buf,
1573
+ width,
1574
+ height,
1575
+ a.x + (cx - a.x) * inbound,
1576
+ a.y + (cy - a.y) * inbound,
1577
+ 4,
1578
+ THEME.cyan,
1579
+ node,
1580
+ );
1581
+ stampFocusDisc(
1582
+ buf,
1583
+ width,
1584
+ height,
1585
+ a.x + (skip.x - a.x) * third,
1586
+ a.y + (skip.y - a.y) * third,
1587
+ 3,
1588
+ THEME.ink,
1589
+ node,
1590
+ );
1591
+ }
1592
+ for (const seat of placed) {
1593
+ stampFocusDisc(
1594
+ buf,
1595
+ width,
1596
+ height,
1597
+ seat.x,
1598
+ seat.y,
1599
+ 7 + kick * 1.5,
1600
+ mixRgb(seat.color, THEME.gold, freqTint(seat.circle, t)),
1601
+ node,
1602
+ );
1603
+ }
1604
+ paintFocusSatellites(buf, width, height, t, checksum);
1605
+ }
1606
+
1607
+ /** waves → waveform v2. Harmonic ribbons + beat envelope + traveling gold needle. */
1608
+ function paintWaveform(buf, width, height, t, checksum) {
1609
+ startFrame(buf, width, height, t, checksum);
1610
+ paintChecksumMesh(buf, width, height, t, checksum, { scale: 0.72, half: 1 });
1611
+ const mid = (height - 1) * 0.5;
1612
+ const beat = beatPhase(checksum, t);
1613
+ const kick = kickAccent(beat);
1614
+ const env = 0.88 + 0.12 * Math.max(0, Math.sin(beat * Math.PI * 2));
1615
+ const circles = checksum.visualConfig.circles;
1616
+ paintSharpRibbon(buf, width, height, () => mid, THEME.ink, 1, THEME.void);
1617
+ circles.forEach((circle, i) => {
1618
+ const color = iridesce(i / Math.max(1, circles.length), t, beat, parseHex(circle.color));
1619
+ const amp0 = height * (0.05 + (circle.radius / 420) * 0.11) * env;
1620
+ const f0 = 0.0065 + circle.frequency / 22000;
1621
+ const shift = t * (circle.frequency / 1.85);
1622
+ const gem = (checksum.gematria && checksum.gematria.checksumValue) || 1;
1623
+ const phase = i * 0.85 + ((gem >>> ((i * 5) % 24)) & 255) / 70;
1624
+ const jitter = 0.08 + ((gem >>> ((i * 3) % 16)) & 7) / 90;
1625
+ paintSharpRibbon(
1626
+ buf,
1627
+ width,
1628
+ height,
1629
+ (x) =>
1630
+ mid +
1631
+ Math.sin((x + shift) * f0 + phase) * amp0 +
1632
+ Math.sin((x + shift) * f0 * 2 + phase * 1.3) * amp0 * 0.28 +
1633
+ Math.sin((x + shift) * f0 * 5.3 + phase * 2.1) * amp0 * jitter,
1634
+ i === 0 ? THEME.cyan : color,
1635
+ i === 0 ? 3 : 2,
1636
+ );
1637
+ paintSharpRibbon(
1638
+ buf,
1639
+ width,
1640
+ height,
1641
+ (x) => mid + Math.sin((x - shift * 0.7) * f0 * 3 + phase) * amp0 * 0.18,
1642
+ i % 2 === 0 ? THEME.gold : THEME.blue,
1643
+ 1,
1644
+ );
1645
+ });
1646
+ const tickX = ((beat % 1) * width) | 0;
1647
+ const tickH = height * 0.22 * (0.45 + kick);
1648
+ for (let y = mid - tickH; y <= mid + tickH; y++) {
1649
+ mixPixel(buf, width, tickX, y, THEME.gold, 1);
1650
+ mixPixel(buf, width, tickX - 1, y, THEME.ink, 1);
1651
+ mixPixel(buf, width, tickX + 1, y, THEME.ink, 1);
1652
+ }
1653
+ paintFocusSatellites(buf, width, height, t, checksum);
1654
+ }
1655
+
1656
+ /** spark → particles v2. Seed mesh is the spark — lines, not bead rain. */
1657
+ function paintParticles(buf, width, height, t, checksum) {
1658
+ startFrame(buf, width, height, t, checksum);
1659
+ const cx = (width - 1) * 0.5;
1660
+ const cy = (height - 1) * 0.5;
1661
+ const minSide = Math.min(width, height);
1662
+ const beat = beatPhase(checksum, t);
1663
+ const kick = kickAccent(beat);
1664
+ const worn = paintChecksumMesh(buf, width, height, t, checksum, {
1665
+ scale: 0.85,
1666
+ half: 1,
1667
+ });
1668
+ const mote = { rim: 1, glow: 2, glowAlpha: 0.16, rimColor: THEME.ink };
1669
+ const mesh = checksum.mesh;
1670
+ const pts = worn && worn.pts;
1671
+ const circles = checksum.visualConfig.circles;
1672
+ if (mesh && pts) {
1673
+ paintFocusSatellites(buf, width, height, t, checksum);
1674
+ return;
1675
+ }
1676
+ const placed = layoutRings(circles, width, height, t * 1.15, checksum);
1677
+ for (let i = 0; i < placed.length; i++) {
1678
+ const src = placed[i];
1679
+ const tint = freqTint(src.circle, t);
1680
+ const orbitR = 22 + (i % 3) * 11 + Math.sin(t * 1.7 + i) * 5;
1681
+ const segs = 11;
1682
+ for (let s = 0; s < segs; s++) {
1683
+ if ((s + i * 3) % 4 === 0) continue;
1684
+ const a0 = (s / segs) * Math.PI * 2 + t * 3.4 + i * 0.7;
1685
+ const a1 = a0 + (Math.PI * 2 * 0.32) / segs;
1686
+ paintSharpLine(
1687
+ buf,
1688
+ width,
1689
+ height,
1690
+ src.x + Math.cos(a0) * orbitR,
1691
+ src.y + Math.sin(a0) * orbitR * 0.72,
1692
+ src.x + Math.cos(a1) * orbitR,
1693
+ src.y + Math.sin(a1) * orbitR * 0.72,
1694
+ THEME.blue,
1695
+ 1,
1696
+ );
1697
+ }
1698
+ const moteCount = 18;
1699
+ for (let k = 0; k < moteCount; k++) {
1700
+ const life = (t * (1.25 + src.circle.frequency / 420) + k * 0.11 + i * 0.07) % 1;
1701
+ const orbital = k % 3 === 0;
1702
+ const dart = Math.sin(t * 5.1 + k * 1.3 + i) * 0.35;
1703
+ const ang = orbital ? t * (3.2 + (k % 5) * 0.28) + i + k + dart : t * 4.2 + i * 1.3 + k * 0.62 + dart;
1704
+ const dist = orbital
1705
+ ? 18 + (k % 4) * 10 + Math.sin(beat * Math.PI * 2 + k) * 4
1706
+ : life * minSide * 0.38 * (0.7 + kick * 0.12);
1707
+ const x = src.x + Math.cos(ang) * dist;
1708
+ const y = src.y + Math.sin(ang) * dist * (orbital ? 0.72 : 1);
1709
+ const size = Math.max(2.2, (1 - life) * (3.2 + (k % 3)) + kick * 0.6);
1710
+ const color = mixRgb(src.color, THEME.gold, k % 2 === 0 ? tint : tint * 0.4);
1711
+ stampFocusDisc(buf, width, height, x, y, size, color, mote);
1712
+ if (life > 0.12 && !orbital) {
1713
+ const prev = life - 0.1;
1714
+ const pd = prev * minSide * 0.36;
1715
+ stampFocusDisc(buf, width, height, src.x + Math.cos(ang) * pd, src.y + Math.sin(ang) * pd, size * 0.55, color, {
1716
+ rim: 1,
1717
+ glow: 1,
1718
+ glowAlpha: 0.1,
1719
+ rimColor: THEME.ink,
1720
+ });
1721
+ }
1722
+ }
1723
+ stampFocusDisc(buf, width, height, src.x, src.y, 6 + kick, mixRgb(THEME.ink, THEME.gold, tint), mote);
1724
+ }
1725
+ paintFocusSatellites(buf, width, height, t, checksum);
1726
+ }
1727
+
1728
+ function paintVisualization(visualization, buf, width, height, t, checksum) {
1729
+ if (visualization === "canvas") return paintCanvas(buf, width, height, t, checksum);
1730
+ if (visualization === "3d-sacred") return paintSacred(buf, width, height, t, checksum);
1731
+ if (visualization === "neural") return paintNeural(buf, width, height, t, checksum);
1732
+ if (visualization === "waveform") return paintWaveform(buf, width, height, t, checksum);
1733
+ if (visualization === "particles") return paintParticles(buf, width, height, t, checksum);
1734
+ const err = new Error(`no Rippel visualization for "${visualization}"`);
1735
+ err.code = "BLIP_RIPPEL_VIZ";
1736
+ throw err;
1737
+ }
1738
+
1739
+ function paintRippelFrame(opts) {
1740
+ const renderer = String(opts.renderer || "");
1741
+ const visualization = visualizationFor(renderer);
1742
+ if (!visualization) {
1743
+ const err = new Error(`no Rippel headless path for "${renderer}"`);
1744
+ err.code = "BLIP_RIPPEL_MISS";
1745
+ throw err;
1746
+ }
1747
+ const width = opts.width || MOTION_WIDTH;
1748
+ const height = opts.height || MOTION_HEIGHT;
1749
+ if (width < 1280 || height < 720) {
1750
+ const err = new Error(`Rippel motions must be ≥720p (got ${width}×${height})`);
1751
+ err.code = "BLIP_RIPPEL_RES";
1752
+ throw err;
1753
+ }
1754
+ const buf = opts.buffer || Buffer.alloc(width * height * 3);
1755
+ if (buf.length < width * height * 3) {
1756
+ const err = new Error("Rippel frame buffer too small");
1757
+ err.code = "BLIP_RIPPEL_BUF";
1758
+ throw err;
1759
+ }
1760
+ const checksum = cachedChecksum({
1761
+ brief: opts.brief,
1762
+ seedHex: opts.seedHex,
1763
+ genre: opts.genre,
1764
+ width,
1765
+ height,
1766
+ lookKind: opts.lookKind,
1767
+ });
1768
+ paintVisualization(visualization, buf, width, height, opts.t || 0, checksum);
1769
+ return {
1770
+ buffer: buf,
1771
+ width,
1772
+ height,
1773
+ visualization,
1774
+ engine: ENGINE,
1775
+ look: LOOK,
1776
+ lookKind: checksum.lookKind,
1777
+ visualConfig: checksum.visualConfig,
1778
+ tlmCommand: checksum.tlmCommand,
1779
+ tempo: checksum.genreConfig && checksum.genreConfig.tempo,
1780
+ mesh: fingerprintMesh(checksum.mesh),
1781
+ field: fingerprintField(checksum.field),
1782
+ };
1783
+ }
1784
+
1785
+ function framesDiffer(a, b) {
1786
+ if (!a || !b || a.length !== b.length) return true;
1787
+ const step = a.length > 12_000 ? 3 : 1;
1788
+ for (let i = 0; i < a.length; i += step) {
1789
+ if (a[i] !== b[i]) return true;
1790
+ }
1791
+ return false;
1792
+ }
1793
+
1794
+ function edgeSharpness(buf, width) {
1795
+ const height = (buf.length / 3 / width) | 0;
1796
+ let edges = 0;
1797
+ let lit = 0;
1798
+ function luma(x, y) {
1799
+ const i = (y * width + x) * 3;
1800
+ return buf[i] * 0.3 + buf[i + 1] * 0.59 + buf[i + 2] * 0.11;
1801
+ }
1802
+ for (let y = 1; y < height - 1; y += 2) {
1803
+ for (let x = 1; x < width - 1; x += 2) {
1804
+ const v = luma(x, y);
1805
+ if (v < 18) continue;
1806
+ lit += 1;
1807
+ if (Math.abs(v - luma(x + 1, y)) > 36 || Math.abs(v - luma(x, y + 1)) > 36) edges += 1;
1808
+ }
1809
+ }
1810
+ return { edges, lit, ratio: lit ? edges / lit : 0 };
1811
+ }
1812
+
1813
+ function goldPixelCount(buf) {
1814
+ let n = 0;
1815
+ for (let i = 0; i < buf.length; i += 3) {
1816
+ if (buf[i] > 200 && buf[i + 1] > 150 && buf[i + 1] < 230 && buf[i + 2] < 70) n += 1;
1817
+ }
1818
+ return n;
1819
+ }
1820
+
1821
+ function sampleMotionFrames(renderer, seedHex, durationSec, brief) {
1822
+ const a = paintRippelFrame({ renderer, t: 0, seedHex, brief, durationSec });
1823
+ const b = paintRippelFrame({
1824
+ renderer,
1825
+ t: (durationSec || 4.44) * 0.5,
1826
+ seedHex,
1827
+ brief,
1828
+ durationSec,
1829
+ });
1830
+ const c = paintRippelFrame({
1831
+ renderer,
1832
+ t: (durationSec || 4.44) * 0.25,
1833
+ seedHex,
1834
+ brief,
1835
+ durationSec,
1836
+ });
1837
+ const d = paintRippelFrame({
1838
+ renderer,
1839
+ t: (durationSec || 4.44) * 0.75,
1840
+ seedHex,
1841
+ brief,
1842
+ durationSec,
1843
+ });
1844
+ return {
1845
+ visualization: a.visualization,
1846
+ look: a.look,
1847
+ width: a.width,
1848
+ height: a.height,
1849
+ differ: framesDiffer(a.buffer, b.buffer),
1850
+ living:
1851
+ framesDiffer(a.buffer, b.buffer) &&
1852
+ framesDiffer(a.buffer, c.buffer) &&
1853
+ framesDiffer(b.buffer, d.buffer) &&
1854
+ framesDiffer(c.buffer, d.buffer),
1855
+ sharpness: edgeSharpness(a.buffer, a.width),
1856
+ circleCount: a.visualConfig.circles.length,
1857
+ tempo: a.tempo,
1858
+ mesh: a.mesh,
1859
+ field: a.field,
1860
+ lookKind: a.lookKind,
1861
+ fill: frameFill(a.buffer),
1862
+ };
1863
+ }
1864
+
1865
+ function fingerprintField(field) {
1866
+ if (!field) return null;
1867
+ return {
1868
+ id: field.id,
1869
+ grid: field.grid,
1870
+ gradient: field.gradient,
1871
+ starCount: (field.stars && field.stars.length) || 0,
1872
+ blinkerCount: (field.blinkers && field.blinkers.length) || 0,
1873
+ };
1874
+ }
1875
+
1876
+ function fingerprintMesh(mesh) {
1877
+ if (!mesh) return null;
1878
+ return {
1879
+ id: mesh.id,
1880
+ family: mesh.family,
1881
+ gait: mesh.gait,
1882
+ vertexCount: mesh.verts.length,
1883
+ edgeCount: mesh.edges.length,
1884
+ faceCount: (mesh.faces && mesh.faces.length) || 0,
1885
+ shells: mesh.shells || 1,
1886
+ dual: Boolean(mesh.dual),
1887
+ fill: Boolean(mesh.fill),
1888
+ ghost: Boolean(mesh.ghost),
1889
+ coreStyle: mesh.coreStyle || "disc",
1890
+ };
1891
+ }
1892
+
1893
+ function frameFill(buf) {
1894
+ const v = THEME.void;
1895
+ let lit = 0;
1896
+ const pixels = buf.length / 3;
1897
+ for (let i = 0; i < buf.length; i += 3) {
1898
+ if (Math.abs(buf[i] - v[0]) > 2 || Math.abs(buf[i + 1] - v[1]) > 2 || Math.abs(buf[i + 2] - v[2]) > 2) {
1899
+ lit += 1;
1900
+ }
1901
+ }
1902
+ return lit / pixels;
1903
+ }
1904
+
1905
+ function summarizeVisual(checksum) {
1906
+ if (!checksum || !checksum.visualConfig) return null;
1907
+ return {
1908
+ tlmCommand: checksum.tlmCommand,
1909
+ canvas: checksum.visualConfig.canvas,
1910
+ circleCount: checksum.visualConfig.circles.length,
1911
+ notes: checksum.visualConfig.circles.map((c) => c.note),
1912
+ frequencies: checksum.visualConfig.circles.map((c) => c.frequency),
1913
+ lookKind: checksum.lookKind || null,
1914
+ mesh: fingerprintMesh(checksum.mesh),
1915
+ field: fingerprintField(checksum.field),
1916
+ };
1917
+ }
1918
+
1919
+ module.exports = {
1920
+ ENGINE,
1921
+ LOOK,
1922
+ MOTION_WIDTH,
1923
+ MOTION_HEIGHT,
1924
+ FPS,
1925
+ SSOT,
1926
+ ANIMATION_TO_VISUALIZATION,
1927
+ THEME,
1928
+ SCALES,
1929
+ visualizationFor,
1930
+ buildVisualConfig,
1931
+ cachedChecksum,
1932
+ paintRippelFrame,
1933
+ framesDiffer,
1934
+ sampleMotionFrames,
1935
+ summarizeVisual,
1936
+ stampDisc,
1937
+ stampFocusDisc,
1938
+ orbFocusWidth,
1939
+ edgeSharpness,
1940
+ goldPixelCount,
1941
+ kickAccent,
1942
+ andAccent,
1943
+ beatPhase,
1944
+ phraseOf,
1945
+ phraseMix,
1946
+ phraseWeight,
1947
+ motionGrid: soundRippel.motionGrid,
1948
+ fillVoid,
1949
+ buildMesh,
1950
+ buildField,
1951
+ paintMeshOverlay,
1952
+ paintField,
1953
+ fingerprintMesh,
1954
+ fingerprintField,
1955
+ frameFill,
1956
+ MESH_FAMILIES,
1957
+ MESH_GAITS,
1958
+ CORE_STYLES,
1959
+ LOOK_KINDS,
1960
+ GRID_KINDS,
1961
+ GRAD_KINDS,
1962
+ resolveLookKind,
1963
+ };