@litfamily/litmuse 0.1.0 → 0.1.2

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 (176) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +115 -26
  3. package/README_ko-KR.md +112 -23
  4. package/bin/litmuse.mjs +4 -0
  5. package/docs/assets/card/after.webp +0 -0
  6. package/docs/assets/card/idle.webp +0 -0
  7. package/docs/assets/card/working.webp +0 -0
  8. package/docs/assets/handoff/latest.webp +0 -0
  9. package/docs/assets/handoff/saved.webp +0 -0
  10. package/docs/assets/readme/demo.webp +0 -0
  11. package/lib/stage-card.mjs +180 -5
  12. package/package.json +7 -1
  13. package/plugin/.muse-plugin/plugin.json +26 -11
  14. package/plugin/commands/lit-status.md +2 -1
  15. package/plugin/commands/lit.md +1 -1
  16. package/plugin/hooks/lib/digest.mjs +1 -1
  17. package/plugin/hooks/lib/next-step.mjs +68 -0
  18. package/plugin/hooks/lib/routes.mjs +59 -0
  19. package/plugin/hooks/lib/workflow-stack.mjs +318 -0
  20. package/plugin/hooks/session-start.mjs +29 -2
  21. package/plugin/hooks/user-prompt-submit.mjs +138 -7
  22. package/plugin/lib/capped-fetch.mjs +25 -0
  23. package/plugin/lib/pinned-fetch.mjs +79 -0
  24. package/plugin/lib/plan-task-rows.mjs +32 -0
  25. package/plugin/lib/render_pages.py +29 -1
  26. package/plugin/lib/rewind/collect.mjs +679 -0
  27. package/plugin/lib/rewind/gauge.mjs +72 -0
  28. package/plugin/lib/rewind/next-step.mjs +91 -0
  29. package/plugin/lib/rewind/redact.mjs +83 -0
  30. package/plugin/lib/rewind/transcripts.mjs +227 -0
  31. package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
  32. package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
  33. package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
  34. package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  35. package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  36. package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  37. package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  38. package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  39. package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  40. package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  41. package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  42. package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
  43. package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  44. package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
  45. package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
  46. package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
  47. package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
  48. package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
  49. package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  50. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
  51. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
  52. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
  53. package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
  54. package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
  55. package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  56. package/plugin/skills/lit-adhd/SKILL.md +188 -0
  57. package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
  58. package/plugin/skills/lit-adhd/references/status-taxonomy.md +43 -0
  59. package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
  60. package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
  61. package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
  62. package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
  63. package/plugin/skills/lit-docx/SKILL.md +1 -1
  64. package/plugin/skills/lit-hijack/SKILL.md +589 -0
  65. package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
  66. package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
  67. package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
  68. package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
  69. package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
  70. package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
  71. package/plugin/skills/lit-pptx/SKILL.md +20 -9
  72. package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  73. package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  74. package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  75. package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
  76. package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
  77. package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  78. package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  79. package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  80. package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  81. package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  82. package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
  83. package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
  84. package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
  85. package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
  86. package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
  87. package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
  88. package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
  89. package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  90. package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
  91. package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  92. package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
  93. package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  94. package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  95. package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
  96. package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  97. package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  98. package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  99. package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  100. package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  101. package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  102. package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  103. package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  104. package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  105. package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  106. package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  107. package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  108. package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  109. package/plugin/skills/lit-recap/SKILL.md +3 -0
  110. package/plugin/skills/lit-schematic/SKILL.md +241 -0
  111. package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
  112. package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
  113. package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
  114. package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
  115. package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
  116. package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
  117. package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
  118. package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
  119. package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
  120. package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
  121. package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
  122. package/plugin/skills/lit-schematic/references/verification.md +24 -0
  123. package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
  124. package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
  125. package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
  126. package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
  127. package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
  128. package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
  129. package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
  130. package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
  131. package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
  132. package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
  133. package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
  134. package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
  135. package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
  136. package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
  137. package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
  138. package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
  139. package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
  140. package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
  141. package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
  142. package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
  143. package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
  144. package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
  145. package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
  146. package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
  147. package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
  148. package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
  149. package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
  150. package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
  151. package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
  152. package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
  153. package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
  154. package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
  155. package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
  156. package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
  157. package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  158. package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  159. package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  160. package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  161. package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
  162. package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
  163. package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
  164. package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
  165. package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
  166. package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
  167. package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
  168. package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
  169. package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
  170. package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
  171. package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
  172. package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
  173. package/plugin/skills/readme-studio/SKILL.md +1 -1
  174. package/plugin/skills/readme-studio/templates/hyperframes-cover/package-lock.json +3 -3
  175. package/plugin/skills/readme-studio/templates/remotion-cover/package-lock.json +3 -3
  176. package/plugin/skills/start-work/SKILL.md +6 -0
@@ -0,0 +1,69 @@
1
+ {
2
+ "schema": "litmuse.motion-cover-recipe/v1",
3
+ "note": "The decoded gate row this recipe reached when it shipped (scripts/qa-cover-recipes.mjs re-renders it and fails when median d or moving leave +-15 %, seamLocal leaves TARGET or bytes pass 2 MiB). Measured 2026-10-25 on headless Chrome's GPU rung (ANGLE Metal, Apple M5 Pro) with three.js r170 and libwebp; 20 fps keeps every frame on the 50 ms gate grid.",
4
+ "row": {
5
+ "medianD": 0.002485,
6
+ "movingFrac": 1,
7
+ "longestStillSec": 0.05,
8
+ "spike": 3.684,
9
+ "seamLocal": 1.199,
10
+ "bytes": 1060856
11
+ },
12
+ "settings": {
13
+ "size": [
14
+ 1000,
15
+ 500
16
+ ],
17
+ "fps": 20,
18
+ "frames": 160,
19
+ "encode": {
20
+ "q": 94,
21
+ "edgeQ": 97,
22
+ "edgeFrames": 15,
23
+ "method": 4
24
+ }
25
+ },
26
+ "treatment": {
27
+ "mode": "cover",
28
+ "request": "A README motion cover for LitMuse with a product object that opens.",
29
+ "idea": "A case turns once and opens on its beat to show the lit core inside, beside a still name.",
30
+ "recipe": "hero-3d",
31
+ "recipeReason": "One object acting once per loop says what the project holds better than a type pattern.",
32
+ "hero": "A rounded case with a hinged lid and a lit core.",
33
+ "beat": "The lid opens, the core rises and settles, and the lid closes.",
34
+ "brandWord": "LITMUSE",
35
+ "motto": {
36
+ "text": "KEEP THE WORK LIT.",
37
+ "source": "project",
38
+ "from": "README.md"
39
+ },
40
+ "aspect": "2:1",
41
+ "loopSec": 8,
42
+ "fps": 20,
43
+ "layers": {
44
+ "slow": "The case turns exactly once per loop.",
45
+ "mid": "The camera breathes twice per loop.",
46
+ "beat": "The lid opens on the core and a ring runs out across the floor."
47
+ },
48
+ "system": {
49
+ "rule": "One hinge rule and one turn rule on one object.",
50
+ "copies": "One case, one core, one ring.",
51
+ "offset": "The ring trails the lid by a tenth of the loop."
52
+ },
53
+ "title": {
54
+ "box": [
55
+ 0.05,
56
+ 0.36,
57
+ 0.43,
58
+ 0.14
59
+ ],
60
+ "kind": "live"
61
+ },
62
+ "encode": {
63
+ "q": 94,
64
+ "edgeQ": 97,
65
+ "edgeFrames": 15
66
+ },
67
+ "ambition": "The object reads as a product in a studio and its beat lands once per loop."
68
+ }
69
+ }
@@ -0,0 +1,139 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Recipe D, hero 3D: one product object, lit in a small studio, acting its beat once per loop. A
6
+ worked example: a rounded case turns exactly once per loop (slow layer), the camera breathes twice
7
+ (mid layer), and once per loop the lid opens, a lit core rises and settles back and the lid closes,
8
+ while a ring runs out across the floor (the beat). The word sits still on the left. Drawn with the
9
+ pinned three.js build on the GPU rung (software when the GPU is missing). Replace the word, the
10
+ motto and the HERO block with the project's own object. -->
11
+ <link rel="stylesheet" href="/lit/fonts.css">
12
+ <script src="/lit/loop-kit.js"></script>
13
+ <script type="importmap">{ "imports": { "three": "/lit/three/three.module.js", "three/addons/": "/lit/three/addons/" } }</script>
14
+ <style>
15
+ html, body { margin: 0; width: 1200px; height: 600px; overflow: hidden; background: #0b0f16; }
16
+ canvas { position: absolute; left: 0; top: 0; width: 1200px; height: 600px; display: block; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <canvas id="world"></canvas>
21
+ <canvas id="type" width="2400" height="1200"></canvas>
22
+ <script type="module">
23
+ import * as THREE from "three";
24
+ import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
25
+ import { RoundedBoxGeometry } from "three/addons/geometries/RoundedBoxGeometry.js";
26
+
27
+ const { TAU, clamp, fract, ease } = LitLoop;
28
+ const span = (a, b, t) => clamp((t - a) / (b - a));
29
+ const C = { bg: "#0b0f16", ivory: "#f2efdf", dim: "#8b8f8f", orange: "#ff6337", shell: "#d9d4c4", inner: "#1a2230" };
30
+ const W = 1200, H = 600;
31
+ const AIM = { x: 860, y: 318 };
32
+
33
+ // ---- the world: renderer, studio light, floor ----
34
+ const glCanvas = document.getElementById("world");
35
+ const renderer = new THREE.WebGLRenderer({ canvas: glCanvas, antialias: true, alpha: true, preserveDrawingBuffer: true });
36
+ renderer.setPixelRatio(window.devicePixelRatio);
37
+ renderer.setSize(W, H, false);
38
+ renderer.setClearColor(0x000000, 0);
39
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
40
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
41
+ renderer.shadowMap.enabled = true;
42
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
43
+ const scene = new THREE.Scene();
44
+ scene.environment = new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(), 0.04).texture;
45
+ scene.environmentIntensity = 0.55;
46
+ const cam = new THREE.PerspectiveCamera(30, W / H, 0.1, 60);
47
+ cam.setViewOffset(W, H, W / 2 - AIM.x, H / 2 - AIM.y, W, H);
48
+ const key = new THREE.DirectionalLight(0xffffff, 2.4);
49
+ key.position.set(-3, 6, 4);
50
+ key.castShadow = true;
51
+ key.shadow.mapSize.set(2048, 2048);
52
+ key.shadow.radius = 5;
53
+ key.shadow.bias = -0.0004;
54
+ key.shadow.normalBias = 0.03;
55
+ key.shadow.camera.left = -4; key.shadow.camera.right = 4; key.shadow.camera.top = 4; key.shadow.camera.bottom = -4;
56
+ const rim = new THREE.DirectionalLight(0xffb08f, 1.1);
57
+ rim.position.set(4, 2.5, -5);
58
+ scene.add(key, rim);
59
+ const floor = new THREE.Mesh(new THREE.PlaneGeometry(30, 30), new THREE.ShadowMaterial({ opacity: 0.35 }));
60
+ floor.rotation.x = -Math.PI / 2;
61
+ floor.receiveShadow = true;
62
+ scene.add(floor);
63
+ const ring = new THREE.Mesh(new THREE.RingGeometry(0.98, 1, 128), new THREE.MeshBasicMaterial({ color: C.orange, transparent: true, opacity: 0 }));
64
+ ring.rotation.x = -Math.PI / 2;
65
+ ring.position.y = 0.002;
66
+ scene.add(ring);
67
+
68
+ // ---- HERO: the product object (a case with a hinged lid and a lit core) ----
69
+ const shell = new THREE.MeshPhysicalMaterial({ color: C.shell, roughness: 0.32, metalness: 0.05, clearcoat: 0.6, clearcoatRoughness: 0.2 });
70
+ const inner = new THREE.MeshStandardMaterial({ color: C.inner, roughness: 0.8 });
71
+ const HERO = new THREE.Group();
72
+ const base = new THREE.Mesh(new RoundedBoxGeometry(1.6, 0.62, 1.1, 6, 0.12), shell);
73
+ base.position.y = 0.31;
74
+ const well = new THREE.Mesh(new RoundedBoxGeometry(1.38, 0.5, 0.88, 4, 0.08), inner);
75
+ well.position.y = 0.4;
76
+ const hinge = new THREE.Group();
77
+ hinge.position.set(0, 0.64, -0.55);
78
+ const lid = new THREE.Mesh(new RoundedBoxGeometry(1.6, 0.16, 1.1, 6, 0.07), shell);
79
+ lid.position.set(0, 0.08, 0.55);
80
+ hinge.add(lid);
81
+ const coreMat = new THREE.MeshStandardMaterial({ color: C.orange, emissive: C.orange, emissiveIntensity: 0, roughness: 0.35 });
82
+ const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.24, 3), coreMat);
83
+ const glow = new THREE.PointLight(C.orange, 0, 4, 2);
84
+ core.add(glow);
85
+ for (const m of [base, lid, core]) m.castShadow = true;
86
+ base.receiveShadow = true;
87
+ HERO.add(base, well, hinge, core);
88
+ scene.add(HERO);
89
+
90
+ // The beat: open 0.28-0.44, hold, close 0.62-0.78; zero at both ends, so the loop closes.
91
+ const openAt = (t) => ease.smooth(span(0.28, 0.44, t)) * (1 - ease.smooth(span(0.62, 0.78, t)));
92
+
93
+ function drawWorld(t) {
94
+ HERO.rotation.y = -0.5 + TAU * t;
95
+ const open = openAt(t);
96
+ hinge.rotation.x = -1.45 * open;
97
+ const rise = ease.outBack(span(0.36, 0.5, t)) * (1 - ease.smooth(span(0.58, 0.7, t)));
98
+ core.position.y = 0.42 + 0.6 * rise;
99
+ core.rotation.y = TAU * 2 * t;
100
+ coreMat.emissiveIntensity = 1.1 * open;
101
+ glow.intensity = 3 * open;
102
+ const wave = ease.outCubic(span(0.38, 0.72, t));
103
+ ring.scale.setScalar(0.9 + 1.3 * wave);
104
+ ring.material.opacity = 0.55 * open * (1 - wave);
105
+ const breathe = Math.sin(TAU * 2 * t);
106
+ cam.position.set(5.8 * Math.sin(0.35), 2.3 + 0.12 * breathe, 5.8 * Math.cos(0.35));
107
+ cam.lookAt(0, 0.62 + 0.04 * breathe, 0);
108
+ renderer.render(scene, cam);
109
+ }
110
+
111
+ // ---- the word, set once ----
112
+ const ctx = document.getElementById("type").getContext("2d");
113
+ ctx.scale(2, 2);
114
+ function drawType(mask) {
115
+ ctx.clearRect(0, 0, W, H);
116
+ if (mask) { ctx.fillStyle = "#000"; ctx.fillRect(0, 0, W, H); }
117
+ ctx.textBaseline = "alphabetic";
118
+ ctx.fillStyle = mask ? "#fff" : C.ivory;
119
+ ctx.font = '800 84px "JetBrains Mono"';
120
+ ctx.fillText("LITMUSE", 70, 292);
121
+ if (mask) return;
122
+ ctx.font = '500 18px "JetBrains Mono"';
123
+ ctx.fillStyle = C.dim;
124
+ let mx = 74;
125
+ for (const ch of "KEEP THE WORK ") { ctx.fillText(ch, mx, 338); mx += 19.6; }
126
+ ctx.fillStyle = C.orange;
127
+ ctx.font = '700 18px "JetBrains Mono"';
128
+ for (const ch of "LIT.") { ctx.fillText(ch, mx, 338); mx += 19.6; }
129
+ }
130
+
131
+ window.renderFrame = (t, o = {}) => {
132
+ glCanvas.style.visibility = o.mask ? "hidden" : "visible";
133
+ if (!o.mask) drawWorld(fract(t));
134
+ drawType(Boolean(o.mask));
135
+ };
136
+ document.fonts.load('800 84px "JetBrains Mono"').then(() => document.fonts.load('500 18px "JetBrains Mono"')).then(() => { window.renderFrame(0); window.ready = true; });
137
+ </script>
138
+ </body>
139
+ </html>
@@ -0,0 +1,122 @@
1
+ # Depth: the 3D route
2
+
3
+ Read this when the treatment's subject is a `depth3d` device (`depth: "3d"`), or when a cover takes
4
+ the `hero-3d` recipe. A drawn 3D object is the right subject when the film is about a thing that has
5
+ a back and an inside: a product that turns, opens, unfolds or is assembled. A flat diagram, a chart or
6
+ type does not need it; keep those on the 2D page.
7
+
8
+ ## What the renderer gives you
9
+
10
+ - **A pinned three.js.** The skill ships three.js r170 (npm `three@0.170.0`, MIT) in
11
+ `engine/vendor/three/`: the unminified `three.module.js` plus two addons,
12
+ `environments/RoomEnvironment.js` and `geometries/RoundedBoxGeometry.js`. The renderer serves them at
13
+ `/lit/three/`; a page imports them through an import map:
14
+
15
+ ```html
16
+ <script type="importmap">{ "imports": { "three": "/lit/three/three.module.js", "three/addons/": "/lit/three/addons/" } }</script>
17
+ <script type="module">
18
+ import * as THREE from "three";
19
+ import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
20
+ </script>
21
+ ```
22
+
23
+ Any other path under `/lit/three/` is refused as a missing file, and the network rules still hold:
24
+ never load three.js, a model or a texture from a CDN. The file hashes and the licence are in
25
+ `engine/THIRD_PARTY_NOTICES`.
26
+ - **The GPU rung.** A `depth: 3d` film and a `hero-3d` cover start Chrome on the platform's GPU rung
27
+ (ANGLE on Metal on macOS, GL on Linux, D3D11 on Windows), with GPU rasterization of the page
28
+ itself still off. The renderer records the WebGL renderer string, and the gate's WebGL line names
29
+ the rung and the renderer. The same determinism replay runs as on the software rung: a fresh
30
+ browser on the same rung must reproduce the sampled frames pixel for pixel, or the render stops with
31
+ exit 18. On an Apple M-series Mac the 3D test page and the `hero-3d` cover replayed identically.
32
+ - **A labelled fallback.** When the GPU rung cannot start, or starts without WebGL, the render
33
+ reopens on the software rung (SwiftShader), adds a warning to the report and the manifest naming
34
+ the reason, and records `rung: "software"`. The film still renders, more slowly. Say in the reply
35
+ that it rendered without the GPU. Only when neither rung has WebGL is it exit 11.
36
+
37
+ The rung is chosen from the treatment (`depth`) or the cover recipe, never by the page.
38
+
39
+ ## The scaffold's world
40
+
41
+ `motion.mjs scaffold` on a `depth: 3d` treatment adds, on top of the 2D shots:
42
+
43
+ - `<canvas id="world">` behind every shot's section, so the type stays DOM text the QA can read;
44
+ - one renderer for the whole film (alpha, `preserveDrawingBuffer`, pixel ratio 1, ACES tone mapping,
45
+ soft shadows), a `RoomEnvironment` for reflections, a key light with a biased shadow, a hemisphere
46
+ light in the style's colours and a shadow-only floor;
47
+ - `HERO`, a group that holds the protagonist from the first frame to the last;
48
+ - `aim(layout)`, which offsets the camera so the world origin lands on the centre of the shot's subject
49
+ zone; a `hero` layout shot therefore fills the frame and a `split` shot keeps the object on its side;
50
+ - `WORLD[i](s, hero, cam, t)`, one function per shot that poses the hero, the camera and anything else
51
+ as a function of the shot (`s.p`, `s.local`, `s.enter(...)`).
52
+
53
+ Build the protagonist once, in `HERO`, and only pose it per shot: that is what makes it the same
54
+ object across the film. For a named product, start from its real logo and screens: a fetched logo or
55
+ screenshot goes on a plane or a face of the object as a texture (`TextureLoader` on the file under
56
+ `stage/assets/`); a simple logo shape can be traced into a `THREE.Shape` and given depth with
57
+ `ExtrudeGeometry`.
58
+
59
+ ## Craft and the gate
60
+
61
+ - **Turn slowly.** A full turn takes the loop (a cover) or a whole shot (a film). Fast spins, a flicker
62
+ of specular highlights and shadow acne all read as flashes and fail MO-C-03.
63
+ - **Bias the shadow.** On a turning object set `shadow.bias` about -0.0004 and `shadow.normalBias`
64
+ about 0.03; without them the self-shadow flickers frame to frame.
65
+ - **Ease every open and close.** A lid, a drawer or an exploded view eases in and out (`ease.smooth`),
66
+ holds, and returns; the move is the beat, so it happens once, where the treatment's story puts it.
67
+ - **Keep the type in the DOM.** Copy stays in the shot's type zone as HTML text over the canvas;
68
+ text drawn inside the 3D scene is invisible to the QA unless registered with `LitStage.text`.
69
+ - **Mind the dead air.** A small object on a large empty ground is `DEAD-AIR`; frame it with the
70
+ `hero` layout or move the camera in.
71
+ - **Covers at 20 fps.** The cover gate measures on a 50 ms grid; at 25 fps a smooth turn lands two
72
+ frames on one step at the seam (FLOOR), at 20 fps every step is one frame (TARGET). Encode at q94:
73
+ smooth light surfaces blotch at the default q86.
74
+
75
+ ## Camera moves and the flash audit
76
+
77
+ A rich film moves its camera in at least half its shots, and the flash audit stays exactly as it is.
78
+ When a move fails MO-C-03, keep the move and change what it passes over: slow the light change (a
79
+ highlight that sweeps over 0.5 s or more, not a frame), lower the contrast of the detail behind the
80
+ move, add motion blur (the Blender rung renders it; on the GPU rung average 4-8 sub-frame poses), or cut
81
+ instead of whipping. Removing the camera move is a downgrade.
82
+
83
+ ## The Blender rung (`render3d: "blender"`)
84
+
85
+ For path-traced light (bevels that catch it, soft global illumination, depth of field, motion blur) a
86
+ 3D film can render its world in Blender with Cycles. The page then shows the rendered plate and keeps
87
+ its type, labels and 2D layers in the DOM over it.
88
+
89
+ Take this rung when the request asks for high-quality, cinematic or photoreal 3D (고퀄리티, 고품질,
90
+ 시네마틱, 실사, cinematic, photoreal, high-quality, Blender) and Blender is installed: `scaffold` prints
91
+ `BLENDER:` with the path when both hold. On an Apple M-series GPU a full hero scene rendered at 8-10 s
92
+ per 1080p frame at 160 samples, so a 13 s plate (390 frames) takes about an hour. Settle the scene on
93
+ the preview stills first, since a fix after the final means rendering the plate again, and build the
94
+ page's panels while it renders.
95
+
96
+ 1. Set `depth: 3d`, a `depth3d` subject and `render3d: "blender"`.
97
+ 2. Write `stage/scene.py`, a Blender Python scene. It runs inside the bundled runner with `bpy`,
98
+ `math`, `Vector` and `lit` in scope; times are film seconds and the plate starts at frame 1:
99
+ `lit.camera(location, target, lens, dof=(distance, fstop))`, `lit.area_light(name, location, target,
100
+ power, size, color)`, `lit.world(color, strength)`, `lit.material(name, color, roughness, metallic,
101
+ emission, strength)`, `lit.rounded_box(name, size, radius, segments, material, location)`,
102
+ `lit.image_card(name, path, width, location)` (a fetched product image as a card) and
103
+ `lit.key(obj, prop, t, value)`. The scene must set a camera. The command line sets size, samples,
104
+ device, fps and frames; the scene sets everything else.
105
+ 3. `motion.mjs blender --out <dir>` renders preview stills at the beat midpoints (50 %, 40 samples) into
106
+ `stage/plate-preview/`. Open them, compare them with any reference frames the user gave, and fix
107
+ `scene.py` before the final.
108
+ 4. `motion.mjs blender --out <dir> --final` renders the plate (full size, 160 samples, motion blur,
109
+ 30 fps, `stage/plate/000001.png ...`) detached with a progress file; it can take most of an hour.
110
+ It writes `plate.json`, which lists every frame by name and size with the Blender version, device and
111
+ samples. Listed plate frames are not counted as page rasters; any other frame sequence is still a
112
+ flipbook (exit 17).
113
+ 5. On the page, `const show = LitStage.plate({ parent, count, fps: 30 })` returns `show(t)`; call it from
114
+ `render(t)`. The page renders on the software rung, and the stage refuses to start (exit 17) when the
115
+ plate is missing or does not cover the film.
116
+
117
+ Blender is found from `LITMUSE_BLENDER` (used only if it exists), the macOS app
118
+ (`/Applications/Blender.app/Contents/MacOS/Blender`) or `blender` on PATH, and runs as `blender -b
119
+ --factory-startup -P runner.py -- ...`, on Metal on Apple silicon, another GPU backend next, the CPU
120
+ last; the receipt says which. Without Blender the command renders nothing and prints `FALLBACK`: set
121
+ `render3d` to `three`, build the world in three.js on the GPU rung, and say so in the reply. The report
122
+ and manifest name the plate (Blender version, Cycles device, samples).
@@ -1,28 +1,67 @@
1
1
  # Sound
2
2
 
3
- Films have sound unless the user said otherwise. The treatment's `sound` object (`{ mode, plan, palette,
4
- file }`) plans it on both paths, the renderer muxes the track the plan names, and after the encode it
5
- decodes the film's audio stream and gates it. The generated bed is synthesized by LitMuse's own code from
6
- the treatment: no sample library, model or package is downloaded for it, and the same treatment always
7
- produces the same bytes.
3
+ Films have sound unless the user said otherwise. The treatment's `sound` object (`{ mode, plan, genre?,
4
+ palette?, events?, file? }`) plans it on both paths, the renderer muxes the track the plan names, and after
5
+ the encode it decodes the film's audio stream and gates it. The score and the generated bed are synthesized
6
+ by LitMuse's own code from the treatment: no sample library, model or package is downloaded for either, no
7
+ melody is borrowed, and the same treatment always produces the same bytes.
8
8
 
9
9
  ## Choosing a mode
10
10
 
11
11
  | `sound.mode` | Track | Use it when |
12
12
  | --- | --- | --- |
13
- | `generated` | A bed built from the treatment: a tempo grid, a pulse, a pad chord per beat, and the accents the beats ask for | The default under a bare `lit`, on either path |
13
+ | `score` | A layered score built for this film: drums, bass, chords, lead, arrangement FX and the on-screen events as separate stems, mixed and mastered | The default under a bare `lit` and for every rich film, on either path |
14
+ | `generated` | One of the style's preset beds: a pad chord progression with a pulse and the accents the beats ask for | Minimal films only (`register: minimal`) |
14
15
  | `supplied` | The user's file at `sound.file` (relative to the output folder), padded or trimmed to the film | The user handed over a track |
15
16
  | `authored` | A WAV you wrote into the stage folder, named by `sound.file` | The plan needs sound the bed cannot make; the file must sit inside `<dir>/stage/` |
16
17
  | `none` | No audio stream | The request asks for silence (무음, 소리 없이, silent, muted, no music …) or the `channel` plays muted by design |
17
18
 
18
19
  A supplied or authored track is always muxed, whatever the state of the optional analysis tier below.
19
- The reply calls a generated bed "a generated music bed" in plain words and names a supplied track by its
20
- file name. Switching to `none` to get past a failing check is a downgrade: the done-check records it and the
21
- reply has to admit it.
22
-
23
- ## The generated bed's palette
20
+ The reply calls the track "an original score generated for this film" (or "a generated music bed" for a
21
+ minimal film) in plain words and names a supplied track by its file name. Switching to `none` to get past a
22
+ failing check is a downgrade: the done-check records it and the reply has to admit it.
23
+
24
+ ## The score
25
+
26
+ The score is produced like a house track from the film itself, so no two films share one.
27
+
28
+ - **Genre.** `house` (120–126 BPM, four-on-the-floor), `tech-house` (124–128, rolling bass), `deep-house`
29
+ (118–122, pads, light swing) or `garage` (130–134, broken kick, heavy swing). The style picks one
30
+ (swiss-signal house, terminalcore tech-house, tidal deep-house, paper garage); `sound.genre` names
31
+ another when the film asks for it. Launch and hero films stay in house or tech-house.
32
+ - **Key, tempo, progression.** Seeded from the request, idea, turn, protagonist and beats: one of twelve
33
+ keys, major or minor, a tempo inside the genre's band, one progression per mode, the last bar home.
34
+ - **Arrangement from the beats.** The shots before `story.beat` are intro and build; the drop lands on the
35
+ turn, on a downbeat; with three or more shots after the turn the first is a break and the rest drop again;
36
+ the last shot is the outro. Risers run into every change (strongest into the drop), an impact lands on it,
37
+ a downlifter follows, and a reverse swell breathes into the other changes, with the kick out for the half
38
+ bar before the drop and a snare roll into it.
39
+ - **Stems.** `kick`, `clap`, `hats`, `perc`, `bass` (sub and line), `chords` (stab and pad), `lead` (a motif
40
+ off the key's pentatonic), `fx` (risers, impacts, downlifters, reverse swells) and `events`, each its own
41
+ pattern in its own buffer.
42
+ - **Mix.** Per-stem gain and pan, a high-pass on everything but kick and bass, bass/chords/lead ducked under
43
+ the kick (the sidechain depth is measured and reported in dB), reverb and tempo-delay sends, bus
44
+ compression, then the shared master stage (−16 LUFS, peak under −2 dBFS).
45
+
46
+ Write `sound.plan` as the producer's brief (where the drop lands, where it breaks) and put the cue words
47
+ in the beats' `sound` fields. `motion.mjs sound` writes `sound/score.wav`, every stem and both returns under
48
+ `sound/stems/`, and `sound-cues.json` with genre, key, tempo, progression, sections, FX, stem levels,
49
+ sidechain depth, bus settings and every cue.
50
+
51
+ ## Palettes (minimal films)
52
+
53
+ A minimal film's generated bed takes one of its style's beds exactly (the validator refuses anything
54
+ else); the first is the default and the other two let two films in one style differ:
55
+
56
+ | Style | Default bed | Alternatives |
57
+ | --- | --- | --- |
58
+ | `swiss-signal` | glass, A minor, 104 | pulse-synth, D minor, 112; soft-keys, C major, 96 |
59
+ | `tidal` | felt-strings, D major, 84 | glass, F# minor, 76; soft-keys, Bb major, 72 |
60
+ | `terminalcore` | pulse-synth, E minor, 120 | glass, B minor, 132; pulse-synth, A minor, 100 |
61
+ | `paper` | soft-keys, F major, 96 | felt-strings, G major, 88; glass, C major, 108 |
24
62
 
25
- A generated bed needs `sound.palette`:
63
+ Pick the bed for the film's mood and pace: a slower or tenderer film the slower bed, a launch the
64
+ brighter one. `sound.palette` is:
26
65
 
27
66
  ```json
28
67
  { "timbre": "<timbre>", "key": "<pitch class>", "mode": "<major or minor>", "tempo": "<60-160>" }
@@ -61,6 +100,24 @@ The bed adds an accent only where a beat's `sound` text asks for one. It reads t
61
100
  A beat that names none of these gets pad and pulse only. The cut times come from the beats on the stage
62
101
  path and from the brief's shots on the type path.
63
102
 
103
+ ## Events
104
+
105
+ Cue words tie accents to cuts. Events tie sounds to what moves inside a shot: a latch clicks when the lid
106
+ opens, a card lands, a cursor types, a progress bar completes. A score or generated bed on the stage path
107
+ lists them in `sound.events`:
108
+
109
+ ```json
110
+ "events": [{ "t": 6.42, "on": "the lid clicks open", "kind": "tap" }]
111
+ ```
112
+
113
+ - `t`: the second the event happens on screen, read from the page's own timing, never guessed from the beat;
114
+ - `on`: what happens at that frame, as a viewer would see it;
115
+ - `kind`: `tap` (a short click with a pitched body), `hit` (the cut hit), `whoosh` (air ending on the event)
116
+ or `chime` (two notes of the key: something completes).
117
+
118
+ At least two events, and at least half of them 0.1 s or more away from every cut, so the sound follows the
119
+ picture rather than only the edit. An event never snaps to a cut: it sounds at its own `t`.
120
+
64
121
  `<dir>/sound-cues.json` records each cue with `kind`, `t` in seconds, `beatIndex`, `target` (the boundary or
65
122
  cut it serves) and `delta` (`t - target`), alongside the timbre, key, tempo, loudness, peak, sample count
66
123
  and the bed's SHA-256. Look question 6 ("does the sound follow the cuts?") is answered from this file: every
@@ -102,10 +159,11 @@ the field named.
102
159
  node "$SKILL_ROOT/scripts/motion.mjs" sound --out <dir>
103
160
  ```
104
161
 
105
- This reads `<dir>/treatment.json` and writes `<dir>/sound/bed.wav` and `<dir>/sound-cues.json`, so the cue
106
- deltas can be checked before any frame is captured. For a supplied, authored or silent plan it only reports
107
- what the render will do. The full render rebuilds the bed itself at the film's real frame count and cut
108
- times, so the muxed bed always fits the final cut.
162
+ This reads `<dir>/treatment.json` and writes the score (`<dir>/sound/score.wav` with its stems) or a
163
+ minimal film's bed (`<dir>/sound/bed.wav`), plus `<dir>/sound-cues.json`, so the track can be heard and its
164
+ cues checked before a render. For a supplied, authored or silent plan it only reports what the render will
165
+ do. The full render rebuilds the track at the film's exact frame count and cut times, so the muxed track
166
+ always matches the film sample for sample. Two builds from the same treatment give the same bytes.
109
167
 
110
168
  ## The optional analysis tier
111
169
 
@@ -3,7 +3,10 @@
3
3
  Load this once the treatment says `path: stage`. On this path the film is a web page you write, and the
4
4
  LitMuse renderer turns it into frames: it serves the page to a headless Chrome on a private origin,
5
5
  drives a virtual clock from zero, captures every frame in order, then encodes, replays and gates the
6
- result. You draw everything the treatment promised, the subject first. The kit described below supplies
6
+ result. You draw everything the treatment promised, the subject first. The house style gives the frame
7
+ (palette, type roles, layout zones, motion tokens, hand-offs), served as `/lit/style.css` and
8
+ `/lit/style.js`. Start every page from `motion.mjs scaffold --out <dir>`, which writes the treatment's
9
+ structure for you; fill its `DRAW` functions with the drawing. The kit described below supplies
7
10
  timing and reveal primitives and nothing pictorial, so no two stage films start from the same template.
8
11
 
9
12
  ## The page contract
@@ -12,10 +15,14 @@ The page lives at `<dir>/stage/index.html`, next to the local files it uses (SVG
12
15
  CSS, WAV). Only that folder is served, under the origin `http://lit.stage`; a symlink that resolves
13
16
  outside it is a contract error.
14
17
 
15
- Two routes come from the renderer itself and are always available:
18
+ Four routes come from the renderer itself and are always available:
16
19
 
17
20
  - `<link rel="stylesheet" href="/lit/fonts.css">` declares the verified faces (table below).
18
21
  - `<script src="/lit/stage-kit.js"></script>` defines the global `LitStage`.
22
+ - `<link rel="stylesheet" href="/lit/style.css">` and `<script src="/lit/style.js"></script>` serve the
23
+ film's house style for its format: custom properties, the type-role and colour classes, `window.LitStyle`
24
+ (tokens and pixel zones) and `window.LitTimeline` (the treatment's shots). Set copy with the classes;
25
+ the gate checks the rendered result either way.
19
26
 
20
27
  The page hands the renderer its spec with `LitStage.define({ width, height, fps, duration, render })`, or
21
28
  by assigning the same object to `window.litStage`. `define` checks the spec as soon as it is called:
@@ -193,9 +200,11 @@ node "$SKILL_ROOT/scripts/motion.mjs" stage --out <dir> --round 2
193
200
  ```
194
201
 
195
202
  The stills-only round writes `<dir>/stills/`: one PNG per beat midpoint, one transition strip per cut (the
196
- frames 6 before, at and 6 after the cut, side by side at half size), a 12-frame contact sheet, and
197
- `stills/index.json`. Nothing is encoded. Open those PNGs with `read_file` and record the round as
198
- `references/look.md` describes before the full render.
203
+ frames 6 before, at and 6 after the cut, side by side at half size), a 12-frame contact sheet,
204
+ `stills/board.png` with `stills/check.json`, and `stills/index.json`. Nothing is encoded. The board round
205
+ also runs the board check, the gate's text and style rules on the board frames, and exits 13 naming each
206
+ failed rule. Open the board and those PNGs with `read_file` and record the round as `references/look.md`
207
+ describes before the full render.
199
208
 
200
209
  The full render captures the master, encodes the MP4 (with the planned sound muxed), builds the preview
201
210
  WebP or GIF, the poster and the reduced-motion still, runs the determinism replay and the text QA in
@@ -203,6 +212,21 @@ parallel, and writes `gate-report.txt`, `manifest.json` and `render.jsonl`. It r
203
212
  phase to `<dir>/.run/progress.json`. If the `bash` call times out, run the same command again: it attaches
204
213
  to the render already running and returns its result. Never cut the film short to finish sooner.
205
214
 
215
+ ## The 3D route
216
+
217
+ When the treatment says `depth: 3d`, the subject is drawn as a three.js world (`references/depth.md`).
218
+ The scaffold page already carries the shape: an import map onto the vendored build at `/lit/three/`, a
219
+ full-frame canvas behind the shots, one persistent protagonist group `HERO`, and one `WORLD` function per
220
+ shot that poses the hero, the camera and anything else as a function of the shot's progress. The renderer
221
+ serves the pinned build and its few addons; pages import them through the import map, never from a URL.
222
+ A 3D film asks for the GPU rung; the software rung is the fallback, slower, with the same frames.
223
+
224
+ A path-traced look with Blender installed takes the offline rung instead (`render3d: blender`): write the
225
+ world in `stage/scene.py`, render preview stills with `motion.mjs blender --out <dir>`, then the plate with
226
+ `--final` into `stage/plate/`, and show it under HTML type and labels with `LitStage.plate()`. The plate
227
+ frames are listed by name and size in `stage/plate/plate.json`; the scan trusts them and holds everything
228
+ else to the raster limits.
229
+
206
230
  ## The stage gate
207
231
 
208
232
  `gate-report.txt` ends with `QA gate: PASS` or lists the failed rule ids. The stage rules are: