@jokerized/decksmith 0.9.3 → 0.9.5

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 (59) hide show
  1. package/README.md +449 -90
  2. package/dist/animate/LICENSE +21 -0
  3. package/dist/animate/NOTICE +32 -0
  4. package/dist/animate/core.js +530 -0
  5. package/dist/animate/cut-paper.js +206 -0
  6. package/dist/animate/morph.js +131 -0
  7. package/dist/cli.js +13424 -8867
  8. package/dist/ds-animate.js +1 -0
  9. package/dist/index.js +13251 -8697
  10. package/dist/mcp.js +9845 -7577
  11. package/dist/types/bespoke/art.d.ts +172 -6
  12. package/dist/types/bespoke/cache.d.ts +10 -0
  13. package/dist/types/bespoke/cards.d.ts +66 -0
  14. package/dist/types/bespoke/contract.d.ts +0 -8
  15. package/dist/types/bespoke/databuild.d.ts +50 -0
  16. package/dist/types/bespoke/depth.d.ts +127 -0
  17. package/dist/types/bespoke/grammar.d.ts +73 -0
  18. package/dist/types/bespoke/inspect.d.ts +40 -1
  19. package/dist/types/bespoke/pipeline.d.ts +195 -11
  20. package/dist/types/bespoke/probe.d.ts +2 -2
  21. package/dist/types/bespoke/prompt.d.ts +110 -34
  22. package/dist/types/bespoke/references.d.ts +13 -14
  23. package/dist/types/bespoke/repair.d.ts +23 -29
  24. package/dist/types/bespoke/scene.d.ts +55 -10
  25. package/dist/types/bespoke/select.d.ts +28 -21
  26. package/dist/types/bespoke/sheet.d.ts +66 -1
  27. package/dist/types/bespoke/shots.d.ts +225 -22
  28. package/dist/types/build/files.d.ts +7 -0
  29. package/dist/types/build/piece.d.ts +8 -0
  30. package/dist/types/emit/animate-runtime.d.ts +149 -0
  31. package/dist/types/emit/archetypes/claim-figure.d.ts +84 -1
  32. package/dist/types/emit/archetypes/equation-walk.d.ts +2 -2
  33. package/dist/types/emit/archetypes/hero-number.d.ts +21 -0
  34. package/dist/types/emit/archetypes/index.d.ts +2 -2
  35. package/dist/types/emit/archetypes/kinetic.d.ts +2 -0
  36. package/dist/types/emit/archetypes/pipeline.d.ts +14 -2
  37. package/dist/types/emit/archetypes/stage.d.ts +57 -0
  38. package/dist/types/emit/backdrop.d.ts +114 -0
  39. package/dist/types/emit/kit.d.ts +8 -0
  40. package/dist/types/emit/look.d.ts +14 -0
  41. package/dist/types/emit/motion.d.ts +78 -46
  42. package/dist/types/emit/type.d.ts +48 -5
  43. package/dist/types/images/providers.d.ts +6 -0
  44. package/dist/types/plan/codex.d.ts +18 -4
  45. package/dist/types/plan/direct.d.ts +1 -1
  46. package/dist/types/plan/duration.d.ts +1 -1
  47. package/dist/types/plan/prompt.d.ts +1 -1
  48. package/dist/types/plan/refs.d.ts +2 -2
  49. package/dist/types/plan/variety.d.ts +74 -0
  50. package/dist/types/prefs.d.ts +20 -1
  51. package/dist/types/render/captions.d.ts +4 -5
  52. package/dist/types/render/capture.d.ts +7 -0
  53. package/dist/types/types.d.ts +680 -4
  54. package/dist/types/verify/drift.d.ts +5 -0
  55. package/dist/types/verify/fidelity.d.ts +15 -1
  56. package/dist/types/verify/index.d.ts +17 -0
  57. package/dist/types/verify/scenes.d.ts +185 -5
  58. package/package.json +1 -1
  59. package/dist/types/bespoke/callouts.d.ts +0 -76
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cth9191
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ The three .js files in this directory are vendored, unmodified, from animate:
2
+
3
+ https://github.com/cth9191/animate
4
+ commit 7e5eb56feb2dd573f890e1b7b34748af43d58263 ("Animate 0.4.0")
5
+ Copyright (c) 2026 cth9191, MIT License — the full text is LICENSE, beside this file.
6
+
7
+ core.js <- plugins/animate/skills/animate/kit/core.js
8
+ morph.js <- plugins/animate/skills/animate/kit/morph.js
9
+ cut-paper.js <- plugins/animate/skills/animate/styles/cut-paper/kit.js
10
+
11
+ sha256, as fetched and as shipped (test/wiring.test.ts holds these):
12
+
13
+ d791e21cdc1396d20bd7f1507a34693c64be9220a0d996cc4688149554967fc9 core.js
14
+ f5791c4b6481407d4eba0eab69eca35d227481816b384967f6adf86c1ccd1a1e cut-paper.js
15
+ 08d1427f6b2044dedcca4487d92cc6f67a176834b6e9d020aca146ef1813209d morph.js
16
+
17
+ NOT PATCHED. morph.js still writes window.renderFrame and window.anchorAt and
18
+ still falls back to getElementById('c') when renderFrame is handed no canvas.
19
+ DeckSmith always hands it the mounted canvas, and allows ONE piece per deck
20
+ (src/emit/composition.ts refuses a second by name), so those two globals have a
21
+ single writer. Lifting that limit means patching morph.js and testing two pieces
22
+ on one page — record the patch here when it happens.
23
+
24
+ Only the cut-paper style is vendored. Riso and pixel read pixels back from the
25
+ canvas (getImageData), which flips Chrome's 2D raster mode; core.js, morph.js
26
+ and cut-paper.js call it zero times. See
27
+ .planning/2026-10-09-animate-piece-design.md and
28
+ .planning/2026-10-09-animate-piece-spike.md.
29
+
30
+ src/build/piece.ts assembles these, in animate's own build order (head, core,
31
+ style, the author's scenes and bridges, morph), into each piece's script.
32
+ Nothing else of animate is used: no head.html, board, score or tools.
@@ -0,0 +1,530 @@
1
+ // =====================================================================
2
+ // kit/core.js — seeded RNG, math, easing, palette, geometry, the hand-drawn drawing kit
3
+ // (wobble on the boil, ink, paint, hatch, pencil, stipple, grain), cameras, mono text.
4
+ // Needs from the piece head: FPS, W, H. Render state: ctx, F (frame), B (boil = floor(F/2)).
5
+ // =====================================================================
6
+
7
+ // ---------------------------------------------------------------------
8
+ // seeded RNG
9
+ // ---------------------------------------------------------------------
10
+ function mulberry32(a) {
11
+ return function () {
12
+ a |= 0; a = (a + 0x6D2B79F5) | 0;
13
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
14
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
15
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
16
+ };
17
+ }
18
+ function hash(...xs) {
19
+ let h = 2166136261 >>> 0;
20
+ for (const x of xs) {
21
+ const s = String(x);
22
+ for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
23
+ h ^= 124; h = Math.imul(h, 16777619);
24
+ }
25
+ return h >>> 0;
26
+ }
27
+ function RNG(...xs) {
28
+ const f = mulberry32(hash(...xs));
29
+ return {
30
+ f,
31
+ r: (a, b) => a + (b - a) * f(),
32
+ n: (amt) => (f() * 2 - 1) * amt,
33
+ i: (a, b) => a + Math.floor(f() * (b - a + 1)),
34
+ pick: (arr) => arr[Math.floor(f() * arr.length)],
35
+ };
36
+ }
37
+
38
+ // ---------------------------------------------------------------------
39
+ // math
40
+ // ---------------------------------------------------------------------
41
+ const TAU = Math.PI * 2, DEG = Math.PI / 180;
42
+ const clamp = (x, a = 0, b = 1) => (x < a ? a : x > b ? b : x);
43
+ const lerp = (a, b, t) => a + (b - a) * t;
44
+ const seg = (t, a, b) => clamp((t - a) / (b - a));
45
+ // has absolute time t reached cut time c? (cuts snap to the nearest frame, like shot boundaries)
46
+ const past = (t, c) => Math.round(t * FPS + 1e-6) >= Math.round(c * FPS);
47
+ const dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
48
+ const EZ = {
49
+ o2: (t) => 1 - (1 - clamp(t)) ** 2,
50
+ o3: (t) => 1 - (1 - clamp(t)) ** 3,
51
+ i2: (t) => clamp(t) ** 2,
52
+ i3: (t) => clamp(t) ** 3,
53
+ io: (t) => { t = clamp(t); return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2; },
54
+ back: (t) => { t = clamp(t) - 1; return 1 + 2.70158 * t * t * t + 1.70158 * t * t; },
55
+ };
56
+
57
+ // ---------------------------------------------------------------------
58
+ // render state
59
+ // ---------------------------------------------------------------------
60
+ let ctx = null; // current 2d context
61
+ let F = 0; // absolute frame index
62
+ let B = 0; // boil index — reseeds every 2 frames (Math.floor(t*12))
63
+
64
+ const C = {
65
+ ink: '#3a2417', cream: '#efe5cf', paper: '#f4ecd9', paperLine: 'rgba(90,130,190,0.32)',
66
+ orange: '#e58a4e', orangeD: '#c3622e', orangeL: '#f4b27c', terra: '#d97757',
67
+ rose: '#d98b8b', roseD: '#b0616a', roseL: '#efb9b0',
68
+ wood: '#d6a466', woodD: '#a8773f', woodL: '#e9c48f',
69
+ sky: '#9cc4d6', skyD: '#6f9fb8', skyL: '#c9e1ea',
70
+ teal: '#3d9f96', tealL: '#86c9be',
71
+ yellow: '#f0c55a', yellowL: '#f7dd92',
72
+ green: '#5f9c5c', greenL: '#a3cf92',
73
+ red: '#d4382c', redL: '#ea7a6c',
74
+ skin: '#ebb68f', skinD: '#c7815d', skinL: '#f7d4b4',
75
+ lav: 'rgba(104,112,214,0.7)', lavS: 'rgba(104,112,214,0.42)',
76
+ navy: '#0c0d1c', navy2: '#15173a', peri: '#c8d0f0', periD: '#7c86c4', periS: 'rgba(150,164,230,0.20)',
77
+ cyan: '#52d6c4', mag: '#e0368f', glow: '#5de37a', white: '#f4f6ff',
78
+ };
79
+ const RAINBOW = ['#f0d042', '#e050b8', '#46cde0', '#7ad866', '#a47ef0'];
80
+ const MONO = 'Consolas, "Cascadia Mono", Menlo, "DejaVu Sans Mono", "Courier New", monospace'; // Windows first, then macOS / Linux
81
+
82
+ // ---------------------------------------------------------------------
83
+ // geometry
84
+ // ---------------------------------------------------------------------
85
+ function ellipsePts(cx, cy, rx, ry, rot = 0, n = 64, a0 = 0, a1 = TAU) {
86
+ const P = [], c = Math.cos(rot), s = Math.sin(rot), full = Math.abs(a1 - a0 - TAU) < 1e-6;
87
+ for (let i = 0; i < n; i++) {
88
+ const a = a0 + (a1 - a0) * i / (full ? n : n - 1);
89
+ const x = Math.cos(a) * rx, y = Math.sin(a) * ry;
90
+ P.push([cx + x * c - y * s, cy + x * s + y * c]);
91
+ }
92
+ return P;
93
+ }
94
+ function rrectPts(x, y, w, h, r, n = 6) {
95
+ r = Math.min(r, w / 2, h / 2);
96
+ const P = [], cs = [[x + w - r, y + r, -Math.PI / 2], [x + w - r, y + h - r, 0], [x + r, y + h - r, Math.PI / 2], [x + r, y + r, Math.PI]];
97
+ for (const [cx, cy, a0] of cs) for (let i = 0; i <= n; i++) { const a = a0 + (Math.PI / 2) * i / n; P.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]); }
98
+ return P;
99
+ }
100
+ function arcPts(cx, cy, r, a0, a1, n = 18, sy = 1) {
101
+ const P = [];
102
+ for (let i = 0; i <= n; i++) { const a = a0 + (a1 - a0) * i / n; P.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r * sy]); }
103
+ return P;
104
+ }
105
+ function xform(P, tx, ty, rot = 0, sx = 1, sy = sx) {
106
+ const c = Math.cos(rot), s = Math.sin(rot);
107
+ return P.map(([x, y]) => [tx + x * sx * c - y * sy * s, ty + x * sx * s + y * sy * c]);
108
+ }
109
+ function bbox(P, pad = 0) {
110
+ let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
111
+ for (const [x, y] of P) { if (x < x0) x0 = x; if (y < y0) y0 = y; if (x > x1) x1 = x; if (y > y1) y1 = y; }
112
+ return { x0: x0 - pad, y0: y0 - pad, x1: x1 + pad, y1: y1 + pad };
113
+ }
114
+ function resample(P, closed, step) {
115
+ const out = [], n = P.length, m = closed ? n : n - 1;
116
+ for (let i = 0; i < m; i++) {
117
+ const a = P[i], b = P[(i + 1) % n], k = Math.max(1, Math.ceil(dist(a, b) / step));
118
+ for (let j = 0; j < k; j++) out.push([a[0] + (b[0] - a[0]) * j / k, a[1] + (b[1] - a[1]) * j / k]);
119
+ }
120
+ if (!closed) out.push(P[n - 1]);
121
+ return out;
122
+ }
123
+ function pathLen(P) { let L = 0; for (let i = 1; i < P.length; i++) L += dist(P[i - 1], P[i]); return L; }
124
+ function subPath(P, frac) {
125
+ let want = pathLen(P) * clamp(frac);
126
+ const out = [P[0]];
127
+ for (let i = 1; i < P.length; i++) {
128
+ const d = dist(P[i - 1], P[i]);
129
+ if (want >= d) { out.push(P[i]); want -= d; }
130
+ else { const k = want / (d || 1); out.push([lerp(P[i - 1][0], P[i][0], k), lerp(P[i - 1][1], P[i][1], k)]); break; }
131
+ }
132
+ return out;
133
+ }
134
+ function pointAt(P, frac) { const S = subPath(P, frac); return S[S.length - 1]; }
135
+
136
+ // ---------------------------------------------------------------------
137
+ // drawing kit — boil, sketchy lines, hatching, stipple, pencil, grain
138
+ // ---------------------------------------------------------------------
139
+ // smooth hand wobble along the normal; reseeded every boil step
140
+ function wobble(P, closed, amt, key, step = 7) {
141
+ const Q = resample(P, closed, step), n = Q.length;
142
+ const R = RNG('wob', key, B);
143
+ const f1 = R.r(0.012, 0.03), f2 = R.r(0.05, 0.1), p1 = R.r(0, TAU), p2 = R.r(0, TAU);
144
+ const sx = R.n(amt * 0.5), sy = R.n(amt * 0.5);
145
+ const out = new Array(n); let s = 0;
146
+ for (let i = 0; i < n; i++) {
147
+ const a = Q[closed ? (i - 1 + n) % n : Math.max(0, i - 1)], b = Q[closed ? (i + 1) % n : Math.min(n - 1, i + 1)];
148
+ const nx = a[1] - b[1], ny = b[0] - a[0], l = Math.hypot(nx, ny) || 1;
149
+ if (i) s += dist(Q[i - 1], Q[i]);
150
+ const o = amt * (0.75 * Math.sin(s * f1 + p1) + 0.35 * Math.sin(s * f2 + p2));
151
+ out[i] = [Q[i][0] + nx / l * o + sx, Q[i][1] + ny / l * o + sy];
152
+ }
153
+ return out;
154
+ }
155
+ function trace(P, closed) {
156
+ ctx.beginPath();
157
+ const n = P.length; if (n < 2) return;
158
+ if (closed) {
159
+ ctx.moveTo((P[n - 1][0] + P[0][0]) / 2, (P[n - 1][1] + P[0][1]) / 2);
160
+ for (let i = 0; i < n; i++) { const p = P[i], q = P[(i + 1) % n]; ctx.quadraticCurveTo(p[0], p[1], (p[0] + q[0]) / 2, (p[1] + q[1]) / 2); }
161
+ ctx.closePath();
162
+ } else {
163
+ ctx.moveTo(P[0][0], P[0][1]);
164
+ for (let i = 1; i < n - 1; i++) { const p = P[i], q = P[i + 1]; ctx.quadraticCurveTo(p[0], p[1], (p[0] + q[0]) / 2, (p[1] + q[1]) / 2); }
165
+ ctx.lineTo(P[n - 1][0], P[n - 1][1]);
166
+ }
167
+ }
168
+ // sketchy stroke. o: closed, color, w, amt, key, alpha, frac (write-on), double
169
+ function ink(P, o = {}) {
170
+ const closed = !!o.closed, key = o.key ?? 'ink';
171
+ let Q = o.raw ? P : wobble(P, closed, o.amt ?? 1.8, key);
172
+ let cl = closed;
173
+ if (o.frac != null && o.frac < 1) { if (o.frac <= 0) return Q; Q = subPath(closed ? [...Q, Q[0]] : Q, o.frac); cl = false; }
174
+ ctx.strokeStyle = o.color || C.ink; ctx.lineWidth = o.w ?? 3; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
175
+ ctx.globalAlpha = o.alpha ?? 1;
176
+ trace(Q, cl); ctx.stroke();
177
+ if (o.double) {
178
+ let Q2 = wobble(P, closed, (o.amt ?? 1.8) * 1.6 + 0.8, key + '~');
179
+ if (!cl && closed) Q2 = subPath([...Q2, Q2[0]], o.frac ?? 1);
180
+ ctx.lineWidth = (o.w ?? 3) * 0.4; ctx.globalAlpha = (o.alpha ?? 1) * 0.55; trace(Q2, cl); ctx.stroke();
181
+ }
182
+ ctx.globalAlpha = 1;
183
+ return Q;
184
+ }
185
+ // filled shape + textures (clipped) + ink outline
186
+ function paint(P, o = {}) {
187
+ const key = o.key || 'paint';
188
+ const Q = o.raw ? P : wobble(P, true, o.amt ?? 1.6, key);
189
+ ctx.save();
190
+ trace(Q, true);
191
+ if (o.fill) { ctx.globalAlpha = o.fillAl ?? 1; ctx.fillStyle = o.fill; ctx.fill(); ctx.globalAlpha = 1; }
192
+ if (o.tex) { ctx.clip(); const bb = bbox(Q, 6); o.tex.forEach((t, i) => texture(bb, { key: key + ':' + i, ...t })); }
193
+ ctx.restore();
194
+ if (o.line !== false) {
195
+ ctx.strokeStyle = o.line || C.ink; ctx.lineWidth = o.w ?? 3; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
196
+ ctx.globalAlpha = o.lineAl ?? 1; trace(Q, true); ctx.stroke(); ctx.globalAlpha = 1;
197
+ }
198
+ return Q;
199
+ }
200
+ function texture(bb, t) {
201
+ if (t.k === 'hatch') hatch(bb, t);
202
+ else if (t.k === 'cross') { hatch(bb, t); hatch(bb, { ...t, a: (t.a ?? 0.7) + (t.a2 ?? 1.25), key: t.key + 'x' }); }
203
+ else if (t.k === 'pencil') pencil(bb, t);
204
+ else if (t.k === 'stipple') stipple(bb, t);
205
+ else if (t.k === 'fn') t.fn(bb);
206
+ }
207
+ // parallel hatching inside current clip. t: a angle, gap, c, w, al, dash [min,max], skip, dens(x,y)
208
+ function hatch(bb, t) {
209
+ const a = t.a ?? 0.7, gap = t.gap ?? 7, S = RNG('hs', t.key), R = RNG('hj', t.key, B);
210
+ const cx = (bb.x0 + bb.x1) / 2, cy = (bb.y0 + bb.y1) / 2, rad = Math.hypot(bb.x1 - bb.x0, bb.y1 - bb.y0) / 2 + 4;
211
+ const ca = Math.cos(a), sa = Math.sin(a), dens = t.dens, jit = t.jit ?? 0.9, dash = t.dash;
212
+ ctx.beginPath();
213
+ for (let d = -rad; d <= rad; d += gap) {
214
+ const dd = d + S.n(gap * 0.3);
215
+ let u = -rad + (dash ? S.r(0, dash[1]) : 0);
216
+ while (u < rad) {
217
+ const len = dash ? S.r(dash[0], dash[1]) : rad * 2, u2 = Math.min(rad, u + len);
218
+ const mu = (u + u2) / 2, mx = cx + ca * mu - sa * dd, my = cy + sa * mu + ca * dd, p = S.f();
219
+ if (!dens || p < dens(mx, my)) {
220
+ const e1 = R.n(jit), e2 = R.n(jit);
221
+ ctx.moveTo(cx + ca * u - sa * (dd + e1), cy + sa * u + ca * (dd + e1));
222
+ ctx.lineTo(cx + ca * u2 - sa * (dd + e2), cy + sa * u2 + ca * (dd + e2));
223
+ }
224
+ u = u2 + (dash ? S.r(t.skip?.[0] ?? 3, t.skip?.[1] ?? 12) : 0);
225
+ }
226
+ }
227
+ ctx.strokeStyle = t.c || C.ink; ctx.lineWidth = t.w ?? 1.2; ctx.globalAlpha = t.al ?? 0.5; ctx.lineCap = 'round';
228
+ ctx.stroke(); ctx.globalAlpha = 1;
229
+ }
230
+ // colored-pencil diagonal strokes
231
+ function pencil(bb, t) {
232
+ const S = RNG('ps', t.key), R = RNG('pj', t.key, B);
233
+ const area = (bb.x1 - bb.x0) * (bb.y1 - bb.y0);
234
+ const n = Math.min(t.max ?? 6000, t.n ?? Math.floor(area / (t.per ?? 700)));
235
+ const a = t.a ?? -0.95, L = t.len ?? 14, dens = t.dens;
236
+ ctx.beginPath();
237
+ for (let i = 0; i < n; i++) {
238
+ const x = S.r(bb.x0, bb.x1), y = S.r(bb.y0, bb.y1), p = S.f(), aa = a + S.n(0.22), l = L * (0.5 + S.f() * 0.7);
239
+ if (dens && p > dens(x, y)) continue;
240
+ const jx = R.n(1.1), jy = R.n(1.1), dx = Math.cos(aa) * l / 2, dy = Math.sin(aa) * l / 2;
241
+ ctx.moveTo(x - dx + jx, y - dy + jy); ctx.lineTo(x + dx + jx, y + dy + jy);
242
+ }
243
+ ctx.strokeStyle = t.c; ctx.lineWidth = t.w ?? 1.4; ctx.globalAlpha = t.al ?? 0.45; ctx.lineCap = 'round';
244
+ ctx.stroke(); ctx.globalAlpha = 1;
245
+ }
246
+ // square stipple dots
247
+ function stipple(bb, t) {
248
+ const S = RNG('ss', t.key), R = RNG('sj', t.key, B);
249
+ const n = t.n ?? 400, sz = t.s ?? 2.2, dens = t.dens, jit = t.jit ?? 0.8;
250
+ ctx.fillStyle = t.c; ctx.globalAlpha = t.al ?? 1; ctx.beginPath();
251
+ for (let i = 0; i < n; i++) {
252
+ const x = S.r(bb.x0, bb.x1), y = S.r(bb.y0, bb.y1), p = S.f(), s = sz * (0.6 + S.f() * 0.8);
253
+ if (dens && p > dens(x, y)) continue;
254
+ ctx.rect(x + R.n(jit), y + R.n(jit), s, s);
255
+ }
256
+ ctx.fill(); ctx.globalAlpha = 1;
257
+ }
258
+ // screen-space paper grain, reseeded with the boil
259
+ function grain(o = {}) {
260
+ ctx.save(); screen();
261
+ const R = RNG('grain', B), n = o.n ?? 2600;
262
+ ctx.fillStyle = o.dark || 'rgba(70,45,25,0.13)'; ctx.beginPath();
263
+ for (let i = 0; i < n; i++) ctx.rect(R.r(0, W), R.r(0, H), 1.7, 1.7);
264
+ ctx.fill();
265
+ ctx.fillStyle = o.light || 'rgba(255,250,236,0.20)'; ctx.beginPath();
266
+ for (let i = 0; i < n; i++) ctx.rect(R.r(0, W), R.r(0, H), 1.9, 1.9);
267
+ ctx.fill();
268
+ // a few paper fibres
269
+ ctx.strokeStyle = 'rgba(80,55,35,0.07)'; ctx.lineWidth = 1; ctx.beginPath();
270
+ for (let i = 0; i < 90; i++) { const x = R.r(0, W), y = R.r(0, H), a = R.r(0, TAU), l = R.r(6, 22); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); }
271
+ ctx.stroke();
272
+ ctx.restore();
273
+ }
274
+
275
+ // ---------------------------------------------------------------------
276
+ // construction lines, rulers, sparks, bursts, rainbow contours
277
+ // ---------------------------------------------------------------------
278
+ function construct(cx, cy, r, o = {}) {
279
+ const R = RNG('con', o.key ?? 'c', B);
280
+ ctx.save();
281
+ ctx.strokeStyle = o.c || C.lav; ctx.lineWidth = o.w ?? 1.4; ctx.globalAlpha = o.al ?? 1; ctx.lineCap = 'round';
282
+ const a0 = (o.a0 ?? R.r(0, TAU)), sw = o.sweep ?? 1.05;
283
+ ctx.beginPath(); ctx.ellipse(cx + R.n(1.2), cy + R.n(1.2), r, r * (o.sq ?? 1), o.rot ?? 0, a0, a0 + TAU * sw); ctx.stroke();
284
+ if (o.cross) { const k = o.cross; ctx.beginPath(); ctx.moveTo(cx - k, cy); ctx.lineTo(cx + k, cy); ctx.moveTo(cx, cy - k); ctx.lineTo(cx, cy + k); ctx.stroke(); }
285
+ if (o.ticks) {
286
+ ctx.beginPath();
287
+ for (let i = 0; i < o.ticks; i++) { const a = i / o.ticks * TAU + (o.rot ?? 0); ctx.moveTo(cx + Math.cos(a) * (r - 6), cy + Math.sin(a) * (r - 6)); ctx.lineTo(cx + Math.cos(a) * (r + 6), cy + Math.sin(a) * (r + 6)); }
288
+ ctx.stroke();
289
+ }
290
+ ctx.restore();
291
+ }
292
+ function cline(x1, y1, x2, y2, o = {}) {
293
+ const R = RNG('cl', o.key ?? 'l', B);
294
+ ctx.save(); ctx.strokeStyle = o.c || C.lavS; ctx.lineWidth = o.w ?? 1.2; ctx.globalAlpha = o.al ?? 1;
295
+ ctx.beginPath(); ctx.moveTo(x1 + R.n(1), y1 + R.n(1)); ctx.lineTo(x2 + R.n(1), y2 + R.n(1)); ctx.stroke(); ctx.restore();
296
+ }
297
+ function ruler(x1, y1, x2, y2, o = {}) {
298
+ const R = RNG('rul', o.key ?? 'r', B);
299
+ const L = Math.hypot(x2 - x1, y2 - y1) || 1, ux = (x2 - x1) / L, uy = (y2 - y1) / L, nx = -uy, ny = ux;
300
+ const frac = clamp(o.frac ?? 1), step = o.step ?? 16, tk = o.tick ?? 7, side = o.side ?? 1;
301
+ ctx.save(); ctx.strokeStyle = o.c || C.periD; ctx.lineWidth = o.w ?? 1.5; ctx.globalAlpha = o.al ?? 1; ctx.lineCap = 'round';
302
+ const jx = R.n(0.8), jy = R.n(0.8);
303
+ ctx.beginPath(); ctx.moveTo(x1 + jx, y1 + jy); ctx.lineTo(x1 + ux * L * frac + jx, y1 + uy * L * frac + jy);
304
+ const n = Math.floor(L * frac / step);
305
+ for (let i = 0; i <= n; i++) {
306
+ const k = i % 5 === 0 ? tk * 1.9 : tk, px = x1 + ux * i * step + jx, py = y1 + uy * i * step + jy;
307
+ ctx.moveTo(px, py); ctx.lineTo(px + nx * k * side, py + ny * k * side);
308
+ }
309
+ ctx.moveTo(x1 - nx * tk * 1.6, y1 - ny * tk * 1.6); ctx.lineTo(x1 + nx * tk * 2.2, y1 + ny * tk * 2.2);
310
+ if (frac >= 1) { ctx.moveTo(x2 - nx * tk * 1.6, y2 - ny * tk * 1.6); ctx.lineTo(x2 + nx * tk * 2.2, y2 + ny * tk * 2.2); }
311
+ ctx.stroke(); ctx.restore();
312
+ }
313
+ // glowing radial spark (dark world)
314
+ function sparkBurst(cx, cy, r, o = {}) {
315
+ if (r <= 0.5) return;
316
+ const R = RNG('spk', o.key ?? 's', B), n = o.n ?? 18, rays = [];
317
+ for (let i = 0; i < n; i++) { const a = i / n * TAU + R.n(0.14); rays.push([a, r * R.r(0.28, 0.5), r * R.r(0.72, 1.15)]); }
318
+ const path = () => { ctx.beginPath(); for (const [a, r0, r1] of rays) { ctx.moveTo(cx + Math.cos(a) * r0, cy + Math.sin(a) * r0); ctx.lineTo(cx + Math.cos(a) * r1, cy + Math.sin(a) * r1); } };
319
+ ctx.save(); ctx.lineCap = 'round';
320
+ path(); ctx.strokeStyle = o.glow || 'rgba(82,214,196,0.22)'; ctx.lineWidth = (o.w ?? 2.4) * 3.4; ctx.stroke();
321
+ path(); ctx.strokeStyle = o.c || '#8ff5e6'; ctx.lineWidth = o.w ?? 2.4; ctx.stroke();
322
+ if (o.core !== false) {
323
+ const cr = Math.max(2.5, r * (o.coreR ?? 0.15));
324
+ ctx.beginPath(); ctx.arc(cx, cy, cr * 2.2, 0, TAU); ctx.fillStyle = 'rgba(190,215,255,0.16)'; ctx.fill();
325
+ ctx.beginPath(); ctx.arc(cx, cy, cr, 0, TAU); ctx.fillStyle = C.white; ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = C.periD; ctx.stroke();
326
+ }
327
+ if (o.specks) { ctx.fillStyle = o.c || '#8ff5e6'; for (let i = 0; i < o.specks; i++) { const a = R.r(0, TAU), d = r * R.r(0.2, 1.4); ctx.fillRect(cx + Math.cos(a) * d, cy + Math.sin(a) * d, 2, 2); } }
328
+ ctx.restore();
329
+ }
330
+ // radial tick burst (warm world impact)
331
+ function burst(cx, cy, r0, r1, o = {}) {
332
+ const R = RNG('bur', o.key ?? 'b', B), n = o.n ?? 12;
333
+ ctx.save(); ctx.strokeStyle = o.c || C.ink; ctx.lineWidth = o.w ?? 3; ctx.lineCap = 'round'; ctx.globalAlpha = o.al ?? 1; ctx.beginPath();
334
+ for (let i = 0; i < n; i++) {
335
+ const a = i / n * TAU + R.n(0.15) + (o.rot ?? 0), a1 = r0 * R.r(0.85, 1.1), b1 = r1 * R.r(0.75, 1.15);
336
+ ctx.moveTo(cx + Math.cos(a) * a1, cy + Math.sin(a) * a1); ctx.lineTo(cx + Math.cos(a) * b1, cy + Math.sin(a) * b1);
337
+ }
338
+ ctx.stroke(); ctx.restore();
339
+ }
340
+ function star4(x, y, s, c) {
341
+ ctx.save(); ctx.fillStyle = c; ctx.beginPath();
342
+ ctx.moveTo(x, y - s); ctx.quadraticCurveTo(x, y, x + s, y); ctx.quadraticCurveTo(x, y, x, y + s);
343
+ ctx.quadraticCurveTo(x, y, x - s, y); ctx.quadraticCurveTo(x, y, x, y - s); ctx.fill(); ctx.restore();
344
+ }
345
+ // rainbow contour strokes around a moving part
346
+ function rainbow(P, o = {}) {
347
+ const bb = bbox(P), cx = o.cx ?? (bb.x0 + bb.x1) / 2, cy = o.cy ?? (bb.y0 + bb.y1) / 2, n = o.n ?? 3;
348
+ for (let i = 0; i < n; i++) {
349
+ const S = RNG('rb', o.key, i, B), sc = 1 + (o.spread ?? 0.07) * (i + 1) + S.n(0.02);
350
+ const Q = P.map(([x, y]) => [cx + (x - cx) * sc + S.n(o.drift ?? 3), cy + (y - cy) * sc + S.n(o.drift ?? 3)]);
351
+ ink(Q, { closed: o.closed ?? true, color: RAINBOW[(i + (o.off ?? 0)) % RAINBOW.length], w: o.w ?? 1.6, amt: o.amt ?? 3.5, key: o.key + 'rb' + i, alpha: o.al ?? 0.85 });
352
+ }
353
+ }
354
+ // hex cell grid inside current clip
355
+ function hexGrid(bb, size, o = {}) {
356
+ const h = size * Math.sqrt(3), R = RNG('hex', o.key ?? 'h', B);
357
+ ctx.save(); ctx.strokeStyle = o.c || C.periS; ctx.lineWidth = o.w ?? 1.2; ctx.globalAlpha = o.al ?? 1; ctx.beginPath();
358
+ let row = 0;
359
+ for (let y = bb.y0 - h; y < bb.y1 + h; y += h * 0.5, row++) {
360
+ for (let x = bb.x0 - size * 3 + (row % 2) * size * 1.5; x < bb.x1 + size * 3; x += size * 3) {
361
+ const jx = R.n(0.6), jy = R.n(0.6);
362
+ for (let k = 0; k < 6; k++) {
363
+ const a = k / 6 * TAU, b = (k + 1) / 6 * TAU;
364
+ if (k === 0) ctx.moveTo(x + Math.cos(a) * size + jx, y + Math.sin(a) * size + jy);
365
+ ctx.lineTo(x + Math.cos(b) * size + jx, y + Math.sin(b) * size + jy);
366
+ }
367
+ }
368
+ }
369
+ ctx.stroke(); ctx.restore();
370
+ }
371
+
372
+ // ---------------------------------------------------------------------
373
+ // iris, camera, text
374
+ // ---------------------------------------------------------------------
375
+ // ragged hatched rim; dir=+1 strokes reach outward, -1 inward
376
+ function irisRim(cx, cy, r, dir, o = {}) {
377
+ const R = RNG('irisR', o.key ?? 'i', B), n = Math.min(1600, Math.floor(r * TAU / 2.0));
378
+ ctx.strokeStyle = o.c || '#0b0c17'; ctx.lineWidth = o.w ?? 1.6; ctx.lineCap = 'round'; ctx.beginPath();
379
+ for (let i = 0; i < n; i++) {
380
+ const a = R.r(0, TAU), L = R.r(3, 11) + (R.f() < 0.28 ? R.r(10, 34) : 0), sl = R.n(0.4);
381
+ const r0 = r - dir * R.r(2, 6), r1 = r + dir * L, ax = Math.cos(a), ay = Math.sin(a);
382
+ ctx.moveTo(cx + ax * r0, cy + ay * r0); ctx.lineTo(cx + ax * r1 - ay * L * sl, cy + ay * r1 + ax * L * sl);
383
+ }
384
+ ctx.stroke();
385
+ }
386
+ // iris into a black hole (screen space)
387
+ function irisHole(cx, cy, r, o = {}) {
388
+ if (r <= 0.5) return;
389
+ ctx.save(); screen();
390
+ const col = o.c || '#0b0c17';
391
+ const P = wobble(ellipsePts(cx, cy, r, r, 0, 140), true, Math.min(4, 0.6 + r * 0.02), 'ih' + (o.key ?? ''), 9);
392
+ trace(P, true); ctx.fillStyle = col; ctx.fill();
393
+ irisRim(cx, cy, r, +1, { c: col, key: o.key });
394
+ if (o.inner) { ctx.save(); trace(P, true); ctx.clip(); o.inner(r); ctx.restore(); }
395
+ ctx.restore();
396
+ }
397
+ // world point (cx,cy) lands at screen centre
398
+ function camera(cx, cy, zoom = 1, rot = 0, sx = 0, sy = 0) {
399
+ const c = Math.cos(rot) * zoom, s = Math.sin(rot) * zoom;
400
+ ctx.setTransform(c, s, -s, c, W / 2 + sx - (c * cx - s * cy), H / 2 + sy - (s * cx + c * cy));
401
+ }
402
+ function screen() { ctx.setTransform(1, 0, 0, 1, 0, 0); }
403
+ function toScreen(x, y) { const p = ctx.getTransform().transformPoint(new DOMPoint(x, y)); return [p.x, p.y]; }
404
+ function fillAll(color) { ctx.save(); screen(); ctx.fillStyle = color; ctx.fillRect(0, 0, W, H); ctx.restore(); }
405
+
406
+ // monospace text with per-glyph hand jitter
407
+ function monoText(str, x, y, size, color, o = {}) {
408
+ ctx.save();
409
+ ctx.font = `${o.weight ?? 700} ${size}px ${MONO}`; ctx.fillStyle = color; ctx.textBaseline = 'alphabetic';
410
+ const cw = ctx.measureText('M').width, R = RNG('txt', o.key ?? str, B), jit = o.jit ?? 0.9;
411
+ const n = Math.min(o.count ?? str.length, str.length);
412
+ ctx.globalAlpha = o.al ?? 1;
413
+ for (let i = 0; i < n; i++) {
414
+ const ch = str[i]; if (ch === ' ') continue;
415
+ ctx.save(); ctx.translate(x + i * cw + R.n(jit), y + R.n(jit)); ctx.rotate(R.n(0.045)); ctx.fillText(ch, 0, 0); ctx.restore();
416
+ }
417
+ ctx.restore();
418
+ return cw;
419
+ }
420
+ function monoW(size) { ctx.save(); ctx.font = `700 ${size}px ${MONO}`; const w = ctx.measureText('M').width; ctx.restore(); return w; }
421
+
422
+ // single-stroke hand lettering (x-height 0.5, ascender 1.0, baseline 0)
423
+ const GLYPHS = {
424
+ ' ': { w: 0.26, s: [] },
425
+ c: { w: 0.5, s: [arcPts(0.26, -0.25, 0.25, -35 * DEG, -322 * DEG, 22)] },
426
+ o: { w: 0.52, s: [arcPts(0.26, -0.25, 0.25, -95 * DEG, -460 * DEG, 30)] },
427
+ a: { w: 0.54, s: [[...arcPts(0.25, -0.25, 0.235, -28 * DEG, -335 * DEG, 24)], [[0.49, -0.5], [0.49, -0.05], [0.55, 0.0]]] },
428
+ d: { w: 0.56, s: [[...arcPts(0.25, -0.25, 0.235, -28 * DEG, -335 * DEG, 24)], [[0.49, -1.0], [0.49, -0.05], [0.56, 0.0]]] },
429
+ l: { w: 0.24, s: [[[0.08, -1.0], [0.08, -0.1], [0.12, -0.01], [0.2, 0.0]]] },
430
+ u: { w: 0.54, s: [[[0.03, -0.5], [0.03, -0.22], ...arcPts(0.255, -0.22, 0.225, 180 * DEG, 0, 14), [0.48, -0.5]], [[0.48, -0.5], [0.48, -0.04], [0.54, 0.0]]] },
431
+ e: { w: 0.52, s: [[[0.03, -0.26], [0.5, -0.26], ...arcPts(0.265, -0.25, 0.245, -4 * DEG, -318 * DEG, 26)]] },
432
+ };
433
+ function handwrite(str, x, y, size, o = {}) {
434
+ const strokes = []; let pen = 0;
435
+ for (const ch of str) {
436
+ const g = GLYPHS[ch] || GLYPHS[' '];
437
+ for (const s of g.s) strokes.push(s.map(([u, v]) => [x + (pen + u) * size, y + v * size]));
438
+ pen += g.w + 0.1;
439
+ }
440
+ const lens = strokes.map(pathLen), total = lens.reduce((a, b) => a + b, 0);
441
+ let budget = clamp(o.frac ?? 1) * total;
442
+ for (let i = 0; i < strokes.length && budget > 0; i++) {
443
+ const f = Math.min(1, budget / lens[i]); budget -= lens[i];
444
+ if (o.glow) ink(strokes[i], { color: o.glow, w: (o.w ?? size * 0.06) * 3, amt: o.amt ?? 1.0, key: 'hw' + i + str, frac: f });
445
+ ink(strokes[i], { color: o.c, w: o.w ?? size * 0.06, amt: o.amt ?? 1.0, key: 'hw' + i + str, frac: f });
446
+ }
447
+ return pen * size;
448
+ }
449
+
450
+ // =====================================================================
451
+ // ---- small props and helpers
452
+ function drawPencil(tx, ty, ang, len, k = 1, key = 'pencil') {
453
+ ctx.save(); ctx.translate(tx, ty); ctx.rotate(ang); ctx.scale(k, k);
454
+ paint([[46, -16], [len, -16], [len, 16], [46, 16]], {
455
+ fill: C.yellow, key: key + 'b', w: 3,
456
+ tex: [{ k: 'hatch', a: 0.05, gap: 5, c: '#a8741d', al: 0.55, w: 1.1, dash: [20, 90], dens: (x, y) => (y > 5 ? 1 : 0.08) }, { k: 'pencil', c: '#fff1b8', al: 0.5, a: 0.05, len: 30, per: 120, dens: (x, y) => (y < -4 ? 1 : 0.1) }],
457
+ });
458
+ ink([[50, -5], [len, -5]], { w: 1.6, key: key + 'f1', alpha: 0.7 });
459
+ ink([[50, 6], [len, 6]], { w: 1.6, key: key + 'f2', alpha: 0.7 });
460
+ paint([[1, 0], [46, -15], [46, 15]], { fill: '#efcf9f', key: key + 'c', w: 2.6, tex: [{ k: 'hatch', a: 1.2, gap: 4, al: 0.35, dens: (x, y) => (y > 2 ? 1 : 0.1) }] });
461
+ paint([[0, 0], [15, -5], [15, 5]], { fill: '#3b3438', key: key + 't', w: 2 });
462
+ paint([[len, -17], [len + 26, -17], [len + 26, 17], [len, 17]], { fill: '#bcb7ab', key: key + 'fe', w: 2.6, tex: [{ k: 'hatch', a: Math.PI / 2, gap: 5, al: 0.5 }] });
463
+ paint(rrectPts(len + 24, -16, 34, 32, 10), { fill: '#e58f8c', key: key + 'e', w: 2.6, tex: [{ k: 'pencil', c: C.roseD, al: 0.5, per: 60 }] });
464
+ ctx.restore();
465
+ }
466
+ // smoothstep of x between a and b (NOT manim's `smooth` rate function — the math style calls that smoothM)
467
+ const smooth = (a, b, x) => { const u = clamp((x - a) / (b - a)); return u * u * (3 - 2 * u); };
468
+ const mod = (a, n) => ((a % n) + n) % n;
469
+ function asterisk(x, y, r0, r1, n, rot, color, w) {
470
+ ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = w; ctx.lineCap = 'round'; ctx.beginPath();
471
+ for (let i = 0; i < n; i++) { const a = i / n * TAU + rot; ctx.moveTo(x + Math.cos(a) * r0, y + Math.sin(a) * r0); ctx.lineTo(x + Math.cos(a) * r1, y + Math.sin(a) * r1); }
472
+ ctx.stroke(); ctx.restore();
473
+ }
474
+
475
+ // ---------------------------------------------------------------------
476
+ // time (set by the renderer): TT = absolute time on the motion step (on 2s unless STYLE.ones);
477
+ // E0 = the current era's start; DEFER = overlay queue (captions drawn after the camera); SPARK_AT = the hero's screen spot
478
+ // ev(t, d) = 0..1 progress of an event at t over d seconds; popS(t) = back-eased pop-in scale
479
+ // ---------------------------------------------------------------------
480
+ let TT = 0, E0 = 0, DEFER = null, SPARK_AT = null;
481
+ const ev = (t, d = 0.25) => clamp((TT - t) / d);
482
+ const popS = (t) => (TT < t ? 0 : EZ.back(clamp((TT - t) / 0.25)));
483
+
484
+ // ---------------------------------------------------------------------
485
+ // springs: closed-form, so any frame can be drawn on its own (deterministic, no state)
486
+ // springEase(t, k, d) 0 -> 1 over time t >= 0 (stiffness k, damping d): accelerates, overshoots a hair, settles
487
+ // springMove(t0, a, b, p) a value moving from a to b, starting at time t0 (p = a SPRING preset)
488
+ // springTrack(keys, p) a value with many targets: keys = [[t, value], ...] sorted by t. One spring per change,
489
+ // added up, so a target change mid-move stays continuous (cursors, widths, positions)
490
+ // ---------------------------------------------------------------------
491
+ const SPRING = {
492
+ snappy: { k: 320, d: 30 }, // UI: buttons, toggles, leading edges
493
+ smooth: { k: 170, d: 26 }, // cards, containers, the camera
494
+ heavy: { k: 90, d: 19 }, // big type, logos, large objects
495
+ playful: { k: 260, d: 14 }, // mascots, stickers: visible overshoot
496
+ };
497
+ function springEase(t, k = 170, d = 26) {
498
+ if (t <= 0) return 0;
499
+ const w0 = Math.sqrt(k), z = d / (2 * w0);
500
+ if (z < 1) { const wd = w0 * Math.sqrt(1 - z * z); return 1 - Math.exp(-z * w0 * t) * (Math.cos(wd * t) + (z * w0 / wd) * Math.sin(wd * t)); }
501
+ return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
502
+ }
503
+ const springMove = (t0, a, b, p = SPRING.smooth) => a + (b - a) * springEase(TT - t0, p.k, p.d);
504
+ function springTrack(keys, p = SPRING.smooth) {
505
+ let v = keys[0][1];
506
+ for (let i = 1; i < keys.length; i++) v += (keys[i][1] - keys[i - 1][1]) * springEase(TT - keys[i][0], p.k, p.d);
507
+ return v;
508
+ }
509
+
510
+ // ---------------------------------------------------------------------
511
+ // layout for several formats (9:16, 1:1, 16:9, 4:5 from one piece): place things by fractions of the frame,
512
+ // size them by UNIT (1 at 1080 on the short side). LX(0.5), LY(0.62), 120 * UNIT. PORTRAIT / WIDE pick layouts.
513
+ // ---------------------------------------------------------------------
514
+ const LX = (f) => f * W, LY = (f) => f * H, UNIT = Math.min(W, H) / 1080, PORTRAIT = H > W * 1.2, WIDE = W > H * 1.2;
515
+
516
+ // ---------------------------------------------------------------------
517
+ // the user's own assets (screenshots, logos): files in <piece>/assets/ that tools/build.mjs embeds in the page.
518
+ // asset('logo.png') -> the Image (or null); drawAsset(name, x, y, w, h, { fit: 'contain'|'cover', r, alpha })
519
+ // Real product UI only: never redraw a product screen from imagination when a screenshot exists.
520
+ // ---------------------------------------------------------------------
521
+ const asset = (name) => (typeof ASSETS !== 'undefined' && ASSETS[name] && ASSETS[name].complete ? ASSETS[name] : null);
522
+ function drawAsset(name, x, y, w, h, o = {}) {
523
+ const img = asset(name); if (!img) return false;
524
+ const iw = img.naturalWidth || img.width, ih = img.naturalHeight || img.height;
525
+ const s = (o.fit === 'cover' ? Math.max : Math.min)(w / iw, h / ih), dw = iw * s, dh = ih * s;
526
+ ctx.save(); ctx.globalAlpha *= o.alpha ?? 1;
527
+ if (o.r || o.fit === 'cover') { ctx.beginPath(); ctx.roundRect(x, y, w, h, o.r || 0); ctx.clip(); }
528
+ ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
529
+ ctx.restore(); return true;
530
+ }