@litfamily/litopencode 1.0.29 → 1.0.31

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 (154) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README-Ko-KR.md +7 -7
  3. package/README.md +7 -7
  4. package/dist/activation-primary-prompts.js +15 -6
  5. package/dist/activation-prompt-utils.d.ts +2 -2
  6. package/dist/activation-prompt-utils.js +9 -3
  7. package/dist/activation-reentry.d.ts +29 -0
  8. package/dist/activation-reentry.js +71 -0
  9. package/dist/activation-routing.d.ts +1 -1
  10. package/dist/activation-routing.js +40 -7
  11. package/dist/activation-workflow-prompts.d.ts +2 -0
  12. package/dist/activation-workflow-prompts.js +32 -7
  13. package/dist/activation.d.ts +6 -1
  14. package/dist/activation.js +34 -7
  15. package/dist/cli/args.js +14 -0
  16. package/dist/cli/auto-update.js +2 -4
  17. package/dist/cli/command-plan.d.ts +40 -0
  18. package/dist/cli/command-plan.js +62 -0
  19. package/dist/cli/doctor.d.ts +4 -1
  20. package/dist/cli/doctor.js +65 -4
  21. package/dist/cli/host-detect.d.ts +58 -0
  22. package/dist/cli/host-detect.js +199 -0
  23. package/dist/cli/install-report.js +3 -1
  24. package/dist/cli/install.d.ts +9 -1
  25. package/dist/cli/install.js +134 -31
  26. package/dist/cli/managed-skill-assets.d.ts +1 -1
  27. package/dist/cli/managed-skill-assets.js +5 -3
  28. package/dist/cli/office-dependencies.d.ts +2 -0
  29. package/dist/cli/office-dependencies.js +7 -3
  30. package/dist/cli/scientific-visualization-dependencies.js +1 -1
  31. package/dist/cli/tui-config.d.ts +27 -2
  32. package/dist/cli/tui-config.js +77 -2
  33. package/dist/cli/types.d.ts +17 -0
  34. package/dist/cli.js +9 -6
  35. package/dist/commands.d.ts +10 -1
  36. package/dist/commands.js +17 -3
  37. package/dist/features.d.ts +27 -1
  38. package/dist/features.js +40 -8
  39. package/dist/host/server.d.ts +7 -0
  40. package/dist/host/server.js +12 -0
  41. package/dist/host/tui.d.ts +7 -0
  42. package/dist/host/tui.js +12 -0
  43. package/dist/server.js +9 -0
  44. package/dist/skill-renames.d.ts +1 -1
  45. package/dist/skills.d.ts +9 -2
  46. package/dist/skills.js +22 -8
  47. package/dist/tui/mission-source-v2.d.ts +51 -0
  48. package/dist/tui/mission-source-v2.js +52 -0
  49. package/dist/v2/agents.d.ts +105 -0
  50. package/dist/v2/agents.js +185 -0
  51. package/dist/v2/commands.d.ts +44 -0
  52. package/dist/v2/commands.js +65 -0
  53. package/dist/v2/context.d.ts +67 -0
  54. package/dist/v2/context.js +17 -0
  55. package/dist/v2/events.d.ts +55 -0
  56. package/dist/v2/events.js +193 -0
  57. package/dist/v2/hooks.d.ts +39 -0
  58. package/dist/v2/hooks.js +61 -0
  59. package/dist/v2/server.d.ts +2 -0
  60. package/dist/v2/server.js +68 -0
  61. package/dist/v2/session-hooks.d.ts +64 -0
  62. package/dist/v2/session-hooks.js +230 -0
  63. package/dist/v2/tools.d.ts +26 -0
  64. package/dist/v2/tools.js +49 -0
  65. package/dist/v2/tui.d.ts +63 -0
  66. package/dist/v2/tui.js +259 -0
  67. package/docs/assets/cover-motion-still.webp +0 -0
  68. package/docs/assets/cover-motion.webp +0 -0
  69. package/docs/assets/cover.webp +0 -0
  70. package/docs/assets/readme/badge-version.svg +1 -1
  71. package/docs/assets/readme/litfamily-machines.png +0 -0
  72. package/docs/migration.md +4 -3
  73. package/docs/reference-Ko-KR.md +109 -7
  74. package/docs/reference.md +122 -9
  75. package/package.json +10 -5
  76. package/skills/deep-interview/SKILL.md +1 -1
  77. package/skills/lit-brainstorm/SKILL.md +299 -0
  78. package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
  79. package/skills/lit-brainstorm/references/lenses.md +106 -0
  80. package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
  81. package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
  82. package/skills/lit-docx/SKILL.md +1 -1
  83. package/skills/lit-docx/run.py +42 -4
  84. package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
  85. package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
  86. package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
  87. package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
  88. package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
  89. package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
  90. package/skills/lit-humanizer/scripts/detect.mjs +1 -1
  91. package/skills/lit-pptx/SKILL.md +1 -1
  92. package/skills/lit-pptx/command.mjs +94 -0
  93. package/skills/lit-pptx/run.mjs +16 -6
  94. package/skills/lit-pptx/scripts/compile-deck.js +2 -2
  95. package/skills/lit-typographic-motion/SKILL.md +5 -2
  96. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
  97. package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
  98. package/skills/lit-typographic-motion/engine/command.mjs +94 -0
  99. package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
  100. package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
  101. package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
  102. package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
  103. package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
  104. package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
  105. package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
  106. package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
  107. package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
  108. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
  109. package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  110. package/skills/lit-typographic-motion/engine/look.mjs +113 -10
  111. package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  112. package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
  113. package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
  114. package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
  115. package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
  116. package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  117. package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
  118. package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
  119. package/skills/lit-typographic-motion/fonts/pins.json +3 -1
  120. package/skills/lit-typographic-motion/gate.mjs +34 -1
  121. package/skills/lit-typographic-motion/probe.mjs +8 -3
  122. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
  123. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
  124. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
  125. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
  126. package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
  127. package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  128. package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
  129. package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
  130. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
  131. package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
  132. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
  133. package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
  134. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
  135. package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  136. package/skills/lit-typographic-motion/references/cover.md +423 -0
  137. package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
  138. package/skills/lit-typographic-motion/references/runtime.md +17 -2
  139. package/skills/lit-typographic-motion/references/stage.md +3 -0
  140. package/skills/lit-typographic-motion/references/treatment.md +4 -0
  141. package/skills/lit-typographic-motion/render.mjs +30 -6
  142. package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
  143. package/skills/lit-typographic-motion/runtime.mjs +12 -13
  144. package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
  145. package/skills/managed-skill-manifest.json +176 -32
  146. package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
  147. package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  148. package/skills/review-work/SKILL.md +27 -18
  149. package/skills/skill-rename-aliases.json +2 -1
  150. package/tools/check-pack-payload.mjs +4 -1
  151. package/tools/payload-substance-allowlist.json +1 -1
  152. package/tools/payload-substance-parity.json +6 -4
  153. package/tools/qa-cover-recipes.mjs +167 -0
  154. package/tools/version-manifests.json +6 -0
@@ -0,0 +1,232 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Cover recipe "painted-light" (the default): a finished painting relit by a travelling window of
6
+ light. Worked example for one painting, a 1536x1024 line-up of five robots in panels under a
7
+ painted title. Everything that belongs to that painting sits in PAINTING below, in its own pixel
8
+ coordinates: measure your own base art and replace that block, keep the rest. The painting is
9
+ scaled into the cover, never cropped; at 2:1 a 3:2 painting is pillarboxed and the side wings
10
+ stay quiet. Any panel can be the hero: set PAINTING.hero, give that machine its own glow
11
+ anchors in PAINTING.glows, and point the base art's checkBox in assets.json at the same panel.
12
+ Every layer is drawn on one canvas from t alone. -->
13
+ <link rel="stylesheet" href="/lit/fonts.css">
14
+ <script src="/lit/loop-kit.js"></script>
15
+ <style>
16
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #05080d; }
17
+ canvas { display: block; width: 1200px; height: 600px; }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <canvas id="cover" width="2400" height="1200"></canvas>
22
+ <script>
23
+ const PAINTING = {
24
+ src: "assets/base.png", width: 1536, height: 1024,
25
+ panels: [[13, 303], [320, 607], [623, 906], [921, 1208], [1224, 1526]], // x spans, left to right
26
+ panelTop: 148, panelBottom: 945,
27
+ hero: 0, // index into panels: the machine that stays lit, carries the beat glows and the comet
28
+ title: { box: [455, 32, 635, 88], lift: 1, maskAbove: 60 }, // painted letters; lift > 1 raises dark letters
29
+ // Glow anchors per panel index, measured on that machine: two eyes and one core. Only the hero's
30
+ // entry glows. A hero with no entry stops the page; [] runs it with no beat glows.
31
+ glows: { 0: [{ x: 88, y: 378, rx: 20, ry: 6, beat: "eye" }, { x: 122, y: 374, rx: 16, ry: 7, beat: "eye" }, { x: 85, y: 686, rx: 32, ry: 32, beat: "core" }] },
32
+ accent: { x: 902, y: 987, rx: 60, ry: 22 } // the accent word of the painted motto
33
+ };
34
+
35
+ const { TAU, clamp, fract, mix, ease, swell, cubicPulse, cdist, rng, makeNoise, loopNoise } = LitLoop;
36
+ const W = 1200, H = 600, SCALE = H / PAINTING.height, LEFT = (W - PAINTING.width * SCALE) / 2;
37
+ const ctx = document.getElementById("cover").getContext("2d");
38
+ const toPainting = () => ctx.setTransform(2 * SCALE, 0, 0, 2 * SCALE, 2 * LEFT, 0);
39
+ const toPage = () => ctx.setTransform(2, 0, 0, 2, 0, 0);
40
+ const layer = (w, h) => { const c = document.createElement("canvas"); c.width = w; c.height = h; return c; };
41
+ const base = layer(PAINTING.width, PAINTING.height);
42
+ const titleMask = layer(PAINTING.width, PAINTING.height);
43
+
44
+ if (!PAINTING.panels[PAINTING.hero]) throw new Error(`PAINTING.hero ${PAINTING.hero} is not a panel index (0 to ${PAINTING.panels.length - 1})`);
45
+ const heroGlows = PAINTING.glows[PAINTING.hero];
46
+ if (!Array.isArray(heroGlows)) throw new Error(`PAINTING.glows has no entry for the hero panel ${PAINTING.hero}: measure that machine's eyes and core, or set glows[${PAINTING.hero}] = [] to run with no beat glows`);
47
+ const [hx0, hx1] = PAINTING.panels[PAINTING.hero];
48
+
49
+ // MACRO: a window of work light walks the dimmed panels once per loop. Its edges are short ramps
50
+ // (24 painting px): a moving edge changes each pixel by many levels at once, which the lossy encoder
51
+ // keeps; a wide soft edge changes each pixel by a level or two per frame, which it drops. The pool
52
+ // spans the first to the last dimmed panel and is clipped around the hero, so a hero in the middle
53
+ // stays lit while the window still walks every panel as one light.
54
+ const dimmed = PAINTING.panels.filter((_, i) => i !== PAINTING.hero);
55
+ const POOL = { x0: dimmed[0][0], x1: dimmed.at(-1)[1], half: 230, edge: 24, dim: 0.68, open: 0.04 };
56
+ const windowAt = (t) => mix(POOL.x0 - 330, POOL.x1 + 330, fract(t + 0.18));
57
+ const openness = (t, x) => 1 - ease.smooth(clamp((Math.abs(x - windowAt(t)) - POOL.half) / POOL.edge));
58
+ function drawPool(t) {
59
+ const centre = windowAt(t), span = POOL.x1 - POOL.x0, at = (x) => clamp((x - POOL.x0) / span);
60
+ const shade = ctx.createLinearGradient(POOL.x0, 0, POOL.x1, 0);
61
+ const stops = [[POOL.x0, POOL.dim], [centre - POOL.half - POOL.edge, POOL.dim], [centre - POOL.half, POOL.open], [centre + POOL.half, POOL.open], [centre + POOL.half + POOL.edge, POOL.dim], [POOL.x1, POOL.dim]];
62
+ for (const [x, a] of stops) shade.addColorStop(at(x), `rgba(2, 5, 10, ${a})`);
63
+ const height = PAINTING.panelBottom - PAINTING.panelTop;
64
+ ctx.save();
65
+ ctx.beginPath();
66
+ if (hx0 > POOL.x0) ctx.rect(POOL.x0, PAINTING.panelTop, hx0 - POOL.x0, height);
67
+ if (hx1 < POOL.x1) ctx.rect(hx1, PAINTING.panelTop, POOL.x1 - hx1, height);
68
+ ctx.clip();
69
+ ctx.fillStyle = shade;
70
+ ctx.fillRect(POOL.x0, PAINTING.panelTop, span, height);
71
+ ctx.restore();
72
+ }
73
+
74
+ // MID: one scan band per panel, two passes per loop, offset by the panel's position. A band is a
75
+ // 3 px leading line over two flat afterglow steps (0.10, 0.05): flat steps change pixels only at
76
+ // their edges, and each step clears the encoder's threshold. A third, fainter step drifts.
77
+ const BAND = { height: 64, steps: [0.10, 0.05] };
78
+ function drawBands(t) {
79
+ ctx.globalCompositeOperation = "screen";
80
+ PAINTING.panels.forEach(([x0, x1], i) => {
81
+ const gain = i === PAINTING.hero ? 0.8 : 0.35 + 0.65 * openness(t, (x0 + x1) / 2);
82
+ const y = mix(PAINTING.panelTop - BAND.height, PAINTING.panelBottom, fract(2 * t - 0.11 * i));
83
+ ctx.save();
84
+ ctx.beginPath();
85
+ ctx.rect(x0 + 2, PAINTING.panelTop, x1 - x0 - 4, PAINTING.panelBottom - PAINTING.panelTop);
86
+ ctx.clip();
87
+ const slab = BAND.height / BAND.steps.length;
88
+ BAND.steps.forEach((a, j) => { ctx.fillStyle = `rgba(255, 178, 122, ${a * gain})`; ctx.fillRect(x0, y + BAND.height - (j + 1) * slab, x1 - x0, slab); });
89
+ ctx.fillStyle = `rgba(255, 201, 160, ${0.32 * gain})`;
90
+ ctx.fillRect(x0, y + BAND.height - 3, x1 - x0, 3);
91
+ ctx.restore();
92
+ });
93
+ ctx.globalCompositeOperation = "source-over";
94
+ }
95
+
96
+ // SLOW: a comet laps the hero panel's chamfered border once per loop, linear and perpetual.
97
+ const C = 10, corners = [[hx0, PAINTING.panelTop + C], [hx0 + C, PAINTING.panelTop], [hx1 - C, PAINTING.panelTop], [hx1, PAINTING.panelTop + C], [hx1, PAINTING.panelBottom - C], [hx1 - C, PAINTING.panelBottom], [hx0 + C, PAINTING.panelBottom], [hx0, PAINTING.panelBottom - C]];
98
+ const legs = corners.map((p, i) => { const q = corners[(i + 1) % corners.length]; return { p, q, len: Math.hypot(q[0] - p[0], q[1] - p[1]) }; });
99
+ const LAP = legs.reduce((sum, leg) => sum + leg.len, 0);
100
+ function pointOnBorder(s) {
101
+ let d = ((s % LAP) + LAP) % LAP;
102
+ for (const leg of legs) { if (d <= leg.len) return [mix(leg.p[0], leg.q[0], d / leg.len), mix(leg.p[1], leg.q[1], d / leg.len)]; d -= leg.len; }
103
+ return corners[0];
104
+ }
105
+ function drawComet(t) {
106
+ const head = fract(t) * LAP, tail = 0.16 * LAP;
107
+ ctx.globalCompositeOperation = "screen";
108
+ ctx.lineCap = "round";
109
+ for (let s = 0; s < 6; s++) {
110
+ const length = tail * (1 - s / 7);
111
+ ctx.strokeStyle = `rgba(255, 122, 70, ${0.55 * (1 - s / 6)})`;
112
+ ctx.lineWidth = 9 - s;
113
+ ctx.beginPath();
114
+ for (let k = 0; k <= 24; k++) { const [x, y] = pointOnBorder(head - length + (length * k) / 24); k ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }
115
+ ctx.stroke();
116
+ }
117
+ const [tx, ty] = pointOnBorder(head);
118
+ const tip = ctx.createRadialGradient(tx, ty, 0, tx, ty, 22);
119
+ tip.addColorStop(0, "rgba(255, 241, 224, 1)"); tip.addColorStop(0.4, "rgba(255, 178, 122, 0.5)"); tip.addColorStop(1, "rgba(255, 122, 70, 0)");
120
+ ctx.fillStyle = tip;
121
+ ctx.fillRect(tx - 22, ty - 22, 44, 44);
122
+ ctx.globalCompositeOperation = "source-over";
123
+ }
124
+
125
+ // BEAT: the eyes breathe twice per loop with one blink, the core once, the accent word flares at
126
+ // t = 0 and t = 0.5 with a symmetric pulse (an exponential tail would be a slow fade, and the pulse
127
+ // at t = 0 puts the loop's keyframe reset under motion).
128
+ function glow(x, y, rx, ry, alpha) {
129
+ const g = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
130
+ g.addColorStop(0, `rgba(255, 240, 216, ${0.85 * alpha})`); g.addColorStop(0.3, `rgba(255, 99, 55, ${0.5 * alpha})`); g.addColorStop(1, "rgba(255, 99, 55, 0)");
131
+ ctx.save();
132
+ ctx.translate(x, y); ctx.scale(rx, ry);
133
+ ctx.fillStyle = g;
134
+ ctx.fillRect(-1, -1, 2, 2);
135
+ ctx.restore();
136
+ }
137
+ function drawBeats(t) {
138
+ ctx.globalCompositeOperation = "screen";
139
+ const blink = 1 - cubicPulse(0.62, 0.018, t);
140
+ heroGlows.forEach((g, k) => {
141
+ const level = g.beat === "eye" ? (0.55 + 0.45 * swell(fract(2 * t - 0.06 * k))) * blink : 0.45 + 0.4 * swell(fract(t - 0.35));
142
+ glow(g.x, g.y, g.rx + 30, g.ry + 30, level);
143
+ });
144
+ const flare = Math.max(cubicPulse(0, 0.03, cdist(t, 0)), cubicPulse(0, 0.03, cdist(t, 0.5)));
145
+ glow(PAINTING.accent.x, PAINTING.accent.y, PAINTING.accent.rx, PAINTING.accent.ry, 0.18 + 0.5 * flare);
146
+ ctx.globalCompositeOperation = "source-over";
147
+ }
148
+
149
+ // MID: one fast skewed glint across the title, in overlay: the dark plate stays dark and only the
150
+ // letters brighten. Screen would brighten a visible box; colour-dodge turns coloured letters red.
151
+ function drawGlint(t) {
152
+ const p = LitLoop.window(t, 0.08, 0.11);
153
+ if (p < 0) return;
154
+ const [tx, ty, tw, th] = PAINTING.title.box;
155
+ const x = mix(tx - 40, tx + tw + 40, ease.smooth(p));
156
+ ctx.save();
157
+ ctx.globalCompositeOperation = "overlay";
158
+ ctx.filter = "blur(6px)";
159
+ ctx.globalAlpha = 0.9 * Math.sin(Math.PI * p);
160
+ ctx.fillStyle = "#ffffff";
161
+ ctx.beginPath();
162
+ ctx.moveTo(x, ty - 8); ctx.lineTo(x + 60, ty - 8); ctx.lineTo(x + 25, ty + th + 8); ctx.lineTo(x - 35, ty + th + 8);
163
+ ctx.fill();
164
+ ctx.restore();
165
+ }
166
+
167
+ // SLOW: motes rise one or two screens per loop, few, mostly over the plain wings (a mote over the
168
+ // painting makes the encoder re-send the textured blocks around it).
169
+ const draw = rng(11), noise = makeNoise(5);
170
+ const motes = Array.from({ length: 50 }, (_, i) => ({ x: draw() * W, y: draw() * H, laps: draw() < 0.3 ? 2 : 1, r: 0.6 + draw() * 1.4, a: 0.25 + draw() * 0.45, i }));
171
+ function drawWingsAndMotes(t) {
172
+ for (const [x, w, a] of [[LEFT, -LEFT, 0.08], [W - LEFT, LEFT, 0.03]]) {
173
+ const spill = ctx.createLinearGradient(x, 0, x + w, 0);
174
+ spill.addColorStop(0, `rgba(255, 120, 70, ${a})`); spill.addColorStop(1, "rgba(255, 120, 70, 0)");
175
+ ctx.fillStyle = spill;
176
+ ctx.fillRect(Math.min(x, x + w), 0, Math.abs(w), H);
177
+ }
178
+ for (const m of motes) {
179
+ const y = H + 20 - fract(m.y / H + m.laps * t) * (H + 40);
180
+ const x = m.x + 14 * loopNoise(noise, t, 1.7 * m.i, 1.1);
181
+ const lit = x >= LEFT && x <= W - LEFT ? 0.3 + 0.7 * openness(t, (x - LEFT) / SCALE) : 0.35;
182
+ ctx.fillStyle = `rgba(255, 214, 170, ${m.a * lit})`;
183
+ ctx.beginPath(); ctx.arc(x, y, m.r, 0, TAU); ctx.fill();
184
+ }
185
+ }
186
+
187
+ window.renderFrame = (t, { mask = false } = {}) => {
188
+ toPage();
189
+ ctx.fillStyle = mask ? "#000000" : "#05080d";
190
+ ctx.fillRect(0, 0, W, H);
191
+ toPainting();
192
+ if (mask) { ctx.drawImage(titleMask, 0, 0); return; }
193
+ ctx.drawImage(base, 0, 0);
194
+ drawPool(t);
195
+ drawBands(t);
196
+ drawComet(t);
197
+ drawBeats(t);
198
+ drawGlint(t);
199
+ toPage();
200
+ drawWingsAndMotes(t);
201
+ };
202
+
203
+ // The painting is decoded and drawn once. Dark painted title letters are lifted in place by a
204
+ // luminance-keyed gain (only pixels above the plate's level move, so the plate stays dark), and the
205
+ // title mask is the same letters, white on black, for the contrast and size gates.
206
+ const art = new Image();
207
+ art.src = PAINTING.src;
208
+ art.decode().then(() => {
209
+ const b = base.getContext("2d", { willReadFrequently: true });
210
+ b.drawImage(art, 0, 0, PAINTING.width, PAINTING.height);
211
+ const [tx, ty, tw, th] = PAINTING.title.box;
212
+ const letters = b.getImageData(tx, ty, tw, th);
213
+ const mask = new ImageData(tw, th);
214
+ for (let p = 0; p < tw * th; p++) {
215
+ const o = 4 * p, Y = 0.2126 * letters.data[o] + 0.7152 * letters.data[o + 1] + 0.0722 * letters.data[o + 2];
216
+ const k = clamp((Y - 8) / 22), gain = 1 + (PAINTING.title.lift - 1) * k * k * (3 - 2 * k);
217
+ for (let c = 0; c < 3; c++) letters.data[o + c] = Math.min(255, Math.round(letters.data[o + c] * gain));
218
+ const on = Y > PAINTING.title.maskAbove ? 255 : 0;
219
+ mask.data[o] = mask.data[o + 1] = mask.data[o + 2] = on;
220
+ mask.data[o + 3] = 255;
221
+ }
222
+ b.putImageData(letters, tx, ty);
223
+ const m = titleMask.getContext("2d");
224
+ m.fillStyle = "#000000";
225
+ m.fillRect(0, 0, PAINTING.width, PAINTING.height);
226
+ m.putImageData(mask, tx, ty);
227
+ window.renderFrame(0);
228
+ window.ready = true;
229
+ });
230
+ </script>
231
+ </body>
232
+ </html>
@@ -0,0 +1,62 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "note": "The decoded gate row this recipe reached when it shipped; tools/qa-cover-recipes.mjs renders it again and fails when median d or the moving share move more than 15 %, seamLocal leaves the cover TARGET, the file passes 2 MiB or the render exits non-zero. Measured 2026-10-20 on headless Chrome's software rung with CPU raster and libwebp 1.6.0.",
4
+ "row": {
5
+ "medianD": 0.004935,
6
+ "movingFrac": 1.0,
7
+ "longestStillSec": 0.0,
8
+ "spike": 1.237,
9
+ "seamLocal": 1.152,
10
+ "bytes": 1706392
11
+ },
12
+ "settings": {
13
+ "size": [
14
+ 1000,
15
+ 500
16
+ ],
17
+ "fps": 16,
18
+ "frames": 128,
19
+ "encode": {
20
+ "q": 58,
21
+ "edgeQ": 80,
22
+ "edgeFrames": 15,
23
+ "method": 4
24
+ }
25
+ },
26
+ "treatment": {
27
+ "mode": "cover",
28
+ "request": "Make a looping README cover for LitOpenCode out of its own name.",
29
+ "idea": "A tide of weight rolls through drifting rows of the name while the bold name in the middle never thins.",
30
+ "recipe": "weight-tide",
31
+ "recipeReason": "The name is the only material, and a variable weight axis can move it without moving it.",
32
+ "brandWord": "LITOPENCODE",
33
+ "aspect": "2:1",
34
+ "loopSec": 8,
35
+ "fps": 16,
36
+ "layers": {
37
+ "slow": "Rows drift one word length per loop, alternate rows opposite ways.",
38
+ "mid": "One weight swell rolls diagonally across every letter.",
39
+ "beat": "A cursor steps under each hero letter and carries back home."
40
+ },
41
+ "system": {
42
+ "rule": "One weight field, gain of a swell.",
43
+ "copies": "About eight hundred letters plus the hero word.",
44
+ "offset": "Each letter's phase comes from its x and y on screen."
45
+ },
46
+ "title": {
47
+ "box": [
48
+ 0.17,
49
+ 0.38,
50
+ 0.66,
51
+ 0.24
52
+ ],
53
+ "kind": "live"
54
+ },
55
+ "encode": {
56
+ "q": 58,
57
+ "edgeQ": 80,
58
+ "edgeFrames": 15
59
+ },
60
+ "ambition": "The wave reads by itself from one rule, and the drift keeps every letter moving through the encoder."
61
+ }
62
+ }
@@ -0,0 +1,81 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Cover recipe "weight-tide": type as material. Rows of the brand word in JetBrains Mono (a fixed
6
+ advance, so a weight change never reflows a line) drift one word length per loop, alternate rows
7
+ opposite ways, at low contrast. One weight field washes over every letter, its phase taken from
8
+ the letter's place on screen, so the wave appears by itself. The hero word on its knockout band
9
+ takes the same field over a narrower range and never thins; a cursor steps under its letters.
10
+ Change WORD and the colours; keep the rule. -->
11
+ <link rel="stylesheet" href="/lit/fonts.css">
12
+ <script src="/lit/loop-kit.js"></script>
13
+ <style>
14
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #0e1319; }
15
+ #field { position: absolute; inset: 0; font: 100 30px/60px "JetBrains Mono", monospace; color: #1f2833; white-space: pre; }
16
+ .row { position: absolute; left: 0; height: 60px; }
17
+ #band { position: absolute; left: 0; right: 0; top: 228px; height: 144px; background: #0e1319; }
18
+ #hero { position: absolute; left: 0; right: 0; top: 228px; height: 144px; font: 330 112px/144px "JetBrains Mono", monospace; color: #f2f5f8; text-align: center; white-space: pre; }
19
+ #cursor { position: absolute; top: 352px; width: 67px; height: 8px; background: #ff7a3d; }
20
+ body.mask { background: #000; }
21
+ body.mask #field, body.mask #cursor { visibility: hidden; }
22
+ body.mask #band { background: #000; }
23
+ body.mask #hero { color: #fff; }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div id="field"></div>
28
+ <div id="band"></div>
29
+ <div id="hero"></div>
30
+ <div id="cursor"></div>
31
+ <script>
32
+ const WORD = "LITOPENCODE";
33
+ const { fract, mix, ease, swell } = LitLoop;
34
+ const W = 1200, H = 600, ADVANCE = 18, ROWS = 10;
35
+ const UNIT = (WORD.length + 1) * ADVANCE; // one word and its space: the drift per loop
36
+ const copies = Math.ceil((W + UNIT) / UNIT) + 1;
37
+
38
+ // Every letter is its own span, built once; a frame only sets weights and row offsets.
39
+ const field = document.getElementById("field");
40
+ const rows = Array.from({ length: ROWS }, (_, r) => {
41
+ const row = document.createElement("div");
42
+ row.className = "row";
43
+ row.style.top = `${r * 60}px`;
44
+ const letters = [];
45
+ for (let c = 0; c < copies; c++) for (const ch of `${WORD} `) {
46
+ const span = document.createElement("span");
47
+ span.textContent = ch;
48
+ row.appendChild(span);
49
+ letters.push({ span, x: letters.length * ADVANCE, space: ch === " " });
50
+ }
51
+ field.appendChild(row);
52
+ return { row, letters, y: r * 60 + 30, dir: r % 2 ? -1 : 1 };
53
+ });
54
+ const hero = document.getElementById("hero");
55
+ const heroLetters = [...WORD].map((ch) => { const span = document.createElement("span"); span.textContent = ch; hero.appendChild(span); return span; });
56
+ const HERO_ADV = 0.6 * 112, heroLeft = (W - HERO_ADV * WORD.length) / 2;
57
+ const cursor = document.getElementById("cursor");
58
+
59
+ // The field: a swell that dwells at both ends, sharpened by gain, phase from screen position.
60
+ const tide = (t, x, y) => ease.gain(swell(fract(t - (0.85 * x) / W - (0.35 * y) / H)), 2.4);
61
+
62
+ window.renderFrame = (t, { mask = false } = {}) => {
63
+ document.body.className = mask ? "mask" : "";
64
+ for (const { row, letters, y, dir } of rows) {
65
+ const shift = dir > 0 ? -UNIT + fract(t) * UNIT : -fract(t) * UNIT;
66
+ row.style.transform = `translateX(${shift.toFixed(3)}px)`;
67
+ for (const letter of letters) if (!letter.space) letter.span.style.fontWeight = String(Math.round(mix(100, 800, tide(t, letter.x + shift, y))));
68
+ }
69
+ heroLetters.forEach((span, i) => { span.style.fontWeight = String(Math.round(mix(330, 800, tide(t, heroLeft + (i + 0.5) * HERO_ADV, 300)))); });
70
+ // Beat: the cursor dwells under each letter, steps on with outBack, and in the last slot carries
71
+ // back home, so t = 1 lands where t = 0 starts.
72
+ const slot = Math.floor(t * WORD.length), travel = (t * WORD.length - slot - 0.7) / 0.3;
73
+ const from = slot, to = (slot + 1) % WORD.length;
74
+ const at = travel <= 0 ? from : mix(from, to, ease.outBack(Math.min(1, travel)));
75
+ cursor.style.transform = `translateX(${(heroLeft + at * HERO_ADV + 2.5).toFixed(3)}px)`;
76
+ };
77
+
78
+ Promise.all([100, 330, 800].map((w) => document.fonts.load(`${w} 30px "JetBrains Mono"`))).then(() => { window.renderFrame(0); window.ready = true; });
79
+ </script>
80
+ </body>
81
+ </html>