@autoaiai/aicut 0.1.7 → 0.1.13

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 (306) hide show
  1. package/MANUAL.md +94 -84
  2. package/package.json +1 -1
  3. package/product.json +2 -1
  4. package/skills/aicut/SKILL.md +4 -4
  5. package/skills/aicut/references/commands.md +15 -15
  6. package/skills/aicut/references/film-format.md +1 -1
  7. package/skills/aicut/references/recipes.md +4 -4
  8. package/skills/aicut/references/troubleshooting.md +2 -2
  9. package/skills/aicut-motion/SKILL.md +75 -9
  10. package/skills/aicut-motion/THIRD_PARTY_NOTICES.md +14 -0
  11. package/skills/aicut-motion/assets/atlas/lib/harness.js +116 -0
  12. package/skills/aicut-motion/assets/atlas/lib/sound.js +274 -0
  13. package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/code.js +937 -0
  14. package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/poster.jpg +0 -0
  15. package/skills/aicut-motion/assets/atlas/styles/001-flat-vector-science/style.json +13 -0
  16. package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/code.js +909 -0
  17. package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/poster.jpg +0 -0
  18. package/skills/aicut-motion/assets/atlas/styles/002-watercolor-wash/style.json +13 -0
  19. package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/code.js +881 -0
  20. package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/poster.jpg +0 -0
  21. package/skills/aicut-motion/assets/atlas/styles/003-shadow-puppet-theatre/style.json +13 -0
  22. package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/code.js +897 -0
  23. package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/poster.jpg +0 -0
  24. package/skills/aicut-motion/assets/atlas/styles/004-whiteboard-explainer/style.json +13 -0
  25. package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/code.js +814 -0
  26. package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/poster.jpg +0 -0
  27. package/skills/aicut-motion/assets/atlas/styles/005-psychedelic-poster/style.json +13 -0
  28. package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/code.js +990 -0
  29. package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/poster.jpg +0 -0
  30. package/skills/aicut-motion/assets/atlas/styles/006-tech-glow-explainer/style.json +13 -0
  31. package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/code.js +994 -0
  32. package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/poster.jpg +0 -0
  33. package/skills/aicut-motion/assets/atlas/styles/007-papercraft-models/style.json +13 -0
  34. package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/code.js +792 -0
  35. package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/poster.jpg +0 -0
  36. package/skills/aicut-motion/assets/atlas/styles/008-1940s-film-noir/style.json +13 -0
  37. package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/code.js +840 -0
  38. package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/poster.jpg +0 -0
  39. package/skills/aicut-motion/assets/atlas/styles/009-manim-style-math/style.json +13 -0
  40. package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/code.js +1126 -0
  41. package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/poster.jpg +0 -0
  42. package/skills/aicut-motion/assets/atlas/styles/010-illuminated-manuscript/style.json +13 -0
  43. package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/code.js +854 -0
  44. package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/poster.jpg +0 -0
  45. package/skills/aicut-motion/assets/atlas/styles/011-editorial-essay-collage/style.json +13 -0
  46. package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/code.js +1124 -0
  47. package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/poster.jpg +0 -0
  48. package/skills/aicut-motion/assets/atlas/styles/012-bioluminescent-deep-sea/style.json +13 -0
  49. package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/code.js +728 -0
  50. package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/poster.jpg +0 -0
  51. package/skills/aicut-motion/assets/atlas/styles/013-product-launch-reveal/style.json +13 -0
  52. package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/code.js +664 -0
  53. package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/poster.jpg +0 -0
  54. package/skills/aicut-motion/assets/atlas/styles/014-halftone-pop-panel/style.json +13 -0
  55. package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/code.js +991 -0
  56. package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/poster.jpg +0 -0
  57. package/skills/aicut-motion/assets/atlas/styles/015-isometric-pastel/style.json +13 -0
  58. package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/code.js +801 -0
  59. package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/poster.jpg +0 -0
  60. package/skills/aicut-motion/assets/atlas/styles/016-oscilloscope-vector-art/style.json +13 -0
  61. package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/code.js +692 -0
  62. package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/poster.jpg +0 -0
  63. package/skills/aicut-motion/assets/atlas/styles/017-risograph-zine/style.json +13 -0
  64. package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/code.js +756 -0
  65. package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/poster.jpg +0 -0
  66. package/skills/aicut-motion/assets/atlas/styles/018-claymation-set/style.json +13 -0
  67. package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/code.js +922 -0
  68. package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/poster.jpg +0 -0
  69. package/skills/aicut-motion/assets/atlas/styles/019-neon-sign-at-night/style.json +13 -0
  70. package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/code.js +852 -0
  71. package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/poster.jpg +0 -0
  72. package/skills/aicut-motion/assets/atlas/styles/020-kawaii-anime/style.json +13 -0
  73. package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/code.js +724 -0
  74. package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/poster.jpg +0 -0
  75. package/skills/aicut-motion/assets/atlas/styles/021-ukiyo-e-woodblock/style.json +13 -0
  76. package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/code.js +793 -0
  77. package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/poster.jpg +0 -0
  78. package/skills/aicut-motion/assets/atlas/styles/022-16-bit-pixel-odyssey/style.json +13 -0
  79. package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/code.js +846 -0
  80. package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/poster.jpg +0 -0
  81. package/skills/aicut-motion/assets/atlas/styles/023-chalkboard-lesson/style.json +13 -0
  82. package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/code.js +1008 -0
  83. package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/poster.jpg +0 -0
  84. package/skills/aicut-motion/assets/atlas/styles/024-cut-paper-diorama/style.json +13 -0
  85. package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/code.js +680 -0
  86. package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/poster.jpg +0 -0
  87. package/skills/aicut-motion/assets/atlas/styles/025-cathedral-glass/style.json +13 -0
  88. package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/code.js +633 -0
  89. package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/poster.jpg +0 -0
  90. package/skills/aicut-motion/assets/atlas/styles/026-editorial-chart/style.json +13 -0
  91. package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/code.js +757 -0
  92. package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/poster.jpg +0 -0
  93. package/skills/aicut-motion/assets/atlas/styles/027-synthwave-outrun/style.json +13 -0
  94. package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/code.js +828 -0
  95. package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/poster.jpg +0 -0
  96. package/skills/aicut-motion/assets/atlas/styles/028-origami-studio/style.json +13 -0
  97. package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/code.js +697 -0
  98. package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/poster.jpg +0 -0
  99. package/skills/aicut-motion/assets/atlas/styles/029-linocut-relief-print/style.json +13 -0
  100. package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/code.js +687 -0
  101. package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/poster.jpg +0 -0
  102. package/skills/aicut-motion/assets/atlas/styles/030-holographic-foil-card/style.json +13 -0
  103. package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/code.js +727 -0
  104. package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/poster.jpg +0 -0
  105. package/skills/aicut-motion/assets/atlas/styles/031-voxel-diorama/style.json +13 -0
  106. package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/code.js +903 -0
  107. package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/poster.jpg +0 -0
  108. package/skills/aicut-motion/assets/atlas/styles/032-sumi-e-ink-wash/style.json +13 -0
  109. package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/code.js +662 -0
  110. package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/poster.jpg +0 -0
  111. package/skills/aicut-motion/assets/atlas/styles/033-etch-a-sketch-toy/style.json +13 -0
  112. package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/code.js +951 -0
  113. package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/poster.jpg +0 -0
  114. package/skills/aicut-motion/assets/atlas/styles/034-cosmic-star-chart/style.json +13 -0
  115. package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/code.js +759 -0
  116. package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/poster.jpg +0 -0
  117. package/skills/aicut-motion/assets/atlas/styles/035-punk-zine-collage/style.json +13 -0
  118. package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/code.js +738 -0
  119. package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/poster.jpg +0 -0
  120. package/skills/aicut-motion/assets/atlas/styles/036-thermal-camera/style.json +13 -0
  121. package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/code.js +793 -0
  122. package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/poster.jpg +0 -0
  123. package/skills/aicut-motion/assets/atlas/styles/037-engineering-blueprint/style.json +13 -0
  124. package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/code.js +673 -0
  125. package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/poster.jpg +0 -0
  126. package/skills/aicut-motion/assets/atlas/styles/038-impasto-night/style.json +13 -0
  127. package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/code.js +907 -0
  128. package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/poster.jpg +0 -0
  129. package/skills/aicut-motion/assets/atlas/styles/039-win95-desktop/style.json +13 -0
  130. package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/code.js +833 -0
  131. package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/poster.jpg +0 -0
  132. package/skills/aicut-motion/assets/atlas/styles/040-low-poly-faceted/style.json +13 -0
  133. package/skills/aicut-motion/assets/atlas/styles/041-manga-page/code.js +845 -0
  134. package/skills/aicut-motion/assets/atlas/styles/041-manga-page/poster.jpg +0 -0
  135. package/skills/aicut-motion/assets/atlas/styles/041-manga-page/style.json +13 -0
  136. package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/code.js +496 -0
  137. package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/poster.jpg +0 -0
  138. package/skills/aicut-motion/assets/atlas/styles/042-wood-type-letterpress/style.json +13 -0
  139. package/skills/aicut-motion/assets/atlas/styles/043-light-painting/code.js +635 -0
  140. package/skills/aicut-motion/assets/atlas/styles/043-light-painting/poster.jpg +0 -0
  141. package/skills/aicut-motion/assets/atlas/styles/043-light-painting/style.json +13 -0
  142. package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/code.js +799 -0
  143. package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/poster.jpg +0 -0
  144. package/skills/aicut-motion/assets/atlas/styles/044-felt-patchwork/style.json +13 -0
  145. package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/code.js +547 -0
  146. package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/poster.jpg +0 -0
  147. package/skills/aicut-motion/assets/atlas/styles/045-phosphor-terminal/style.json +13 -0
  148. package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/code.js +592 -0
  149. package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/poster.jpg +0 -0
  150. package/skills/aicut-motion/assets/atlas/styles/046-glassmorphism-ui/style.json +13 -0
  151. package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/code.js +768 -0
  152. package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/poster.jpg +0 -0
  153. package/skills/aicut-motion/assets/atlas/styles/047-tarot-card/style.json +13 -0
  154. package/skills/aicut-motion/assets/atlas/styles/048-transit-map/code.js +666 -0
  155. package/skills/aicut-motion/assets/atlas/styles/048-transit-map/poster.jpg +0 -0
  156. package/skills/aicut-motion/assets/atlas/styles/048-transit-map/style.json +13 -0
  157. package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/code.js +559 -0
  158. package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/poster.jpg +0 -0
  159. package/skills/aicut-motion/assets/atlas/styles/049-gouache-travel-poster/style.json +13 -0
  160. package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/code.js +943 -0
  161. package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/poster.jpg +0 -0
  162. package/skills/aicut-motion/assets/atlas/styles/050-napkin-sketch/style.json +13 -0
  163. package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/code.js +606 -0
  164. package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/poster.jpg +0 -0
  165. package/skills/aicut-motion/assets/atlas/styles/051-art-deco-foil/style.json +13 -0
  166. package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/code.js +730 -0
  167. package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/poster.jpg +0 -0
  168. package/skills/aicut-motion/assets/atlas/styles/052-radiograph-on-lightbox/style.json +13 -0
  169. package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/code.js +782 -0
  170. package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/poster.jpg +0 -0
  171. package/skills/aicut-motion/assets/atlas/styles/053-vaporwave/style.json +13 -0
  172. package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/code.js +504 -0
  173. package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/poster.jpg +0 -0
  174. package/skills/aicut-motion/assets/atlas/styles/054-ebru-paper-marbling/style.json +13 -0
  175. package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/code.js +517 -0
  176. package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/poster.jpg +0 -0
  177. package/skills/aicut-motion/assets/atlas/styles/055-isotype-pictograms/style.json +13 -0
  178. package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/code.js +713 -0
  179. package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/poster.jpg +0 -0
  180. package/skills/aicut-motion/assets/atlas/styles/056-soft-3d-clay/style.json +13 -0
  181. package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/code.js +717 -0
  182. package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/poster.jpg +0 -0
  183. package/skills/aicut-motion/assets/atlas/styles/057-cross-stitch-sampler/style.json +13 -0
  184. package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/code.js +540 -0
  185. package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/poster.jpg +0 -0
  186. package/skills/aicut-motion/assets/atlas/styles/058-constructivist-propaganda/style.json +13 -0
  187. package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/code.js +524 -0
  188. package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/poster.jpg +0 -0
  189. package/skills/aicut-motion/assets/atlas/styles/059-pocket-monochrome/style.json +13 -0
  190. package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/code.js +934 -0
  191. package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/poster.jpg +0 -0
  192. package/skills/aicut-motion/assets/atlas/styles/060-botanical-plate/style.json +13 -0
  193. package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/code.js +690 -0
  194. package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/poster.jpg +0 -0
  195. package/skills/aicut-motion/assets/atlas/styles/061-kinetic-type/style.json +13 -0
  196. package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/code.js +612 -0
  197. package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/poster.jpg +0 -0
  198. package/skills/aicut-motion/assets/atlas/styles/062-byzantine-mosaic/style.json +13 -0
  199. package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/code.js +666 -0
  200. package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/poster.jpg +0 -0
  201. package/skills/aicut-motion/assets/atlas/styles/063-y2k-liquid-chrome/style.json +13 -0
  202. package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/code.js +651 -0
  203. package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/poster.jpg +0 -0
  204. package/skills/aicut-motion/assets/atlas/styles/064-sticky-note-wall/style.json +13 -0
  205. package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/code.js +656 -0
  206. package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/poster.jpg +0 -0
  207. package/skills/aicut-motion/assets/atlas/styles/065-vintage-newsprint/style.json +13 -0
  208. package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/code.js +672 -0
  209. package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/poster.jpg +0 -0
  210. package/skills/aicut-motion/assets/atlas/styles/066-hidden-line-wireframe/style.json +13 -0
  211. package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/code.js +548 -0
  212. package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/poster.jpg +0 -0
  213. package/skills/aicut-motion/assets/atlas/styles/067-memphis-group/style.json +13 -0
  214. package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/code.js +989 -0
  215. package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/poster.jpg +0 -0
  216. package/skills/aicut-motion/assets/atlas/styles/068-ballpoint-notebook/style.json +13 -0
  217. package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/code.js +440 -0
  218. package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/poster.jpg +0 -0
  219. package/skills/aicut-motion/assets/atlas/styles/069-op-art-sphere/style.json +13 -0
  220. package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/code.js +895 -0
  221. package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/poster.jpg +0 -0
  222. package/skills/aicut-motion/assets/atlas/styles/070-topographic-survey/style.json +13 -0
  223. package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/code.js +563 -0
  224. package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/poster.jpg +0 -0
  225. package/skills/aicut-motion/assets/atlas/styles/071-pointillist-afternoon/style.json +13 -0
  226. package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/code.js +627 -0
  227. package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/poster.jpg +0 -0
  228. package/skills/aicut-motion/assets/atlas/styles/072-vector-arcade-1980/style.json +13 -0
  229. package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/code.js +1096 -0
  230. package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/poster.jpg +0 -0
  231. package/skills/aicut-motion/assets/atlas/styles/073-patent-drawing/style.json +13 -0
  232. package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/code.js +595 -0
  233. package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/poster.jpg +0 -0
  234. package/skills/aicut-motion/assets/atlas/styles/074-bauhaus-poster/style.json +13 -0
  235. package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/code.js +665 -0
  236. package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/poster.jpg +0 -0
  237. package/skills/aicut-motion/assets/atlas/styles/075-bitmap-paint-doodle/style.json +13 -0
  238. package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/code.js +645 -0
  239. package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/poster.jpg +0 -0
  240. package/skills/aicut-motion/assets/atlas/styles/076-nail-thread-string-art/style.json +13 -0
  241. package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/code.js +436 -0
  242. package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/poster.jpg +0 -0
  243. package/skills/aicut-motion/assets/atlas/styles/077-ridgeline/style.json +13 -0
  244. package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/code.js +1237 -0
  245. package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/poster.jpg +0 -0
  246. package/skills/aicut-motion/assets/atlas/styles/078-sketchbook-graphite/style.json +13 -0
  247. package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/code.js +622 -0
  248. package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/poster.jpg +0 -0
  249. package/skills/aicut-motion/assets/atlas/styles/079-teletext-page/style.json +13 -0
  250. package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/code.js +524 -0
  251. package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/poster.jpg +0 -0
  252. package/skills/aicut-motion/assets/atlas/styles/080-silkscreen-grid/style.json +13 -0
  253. package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/code.js +689 -0
  254. package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/poster.jpg +0 -0
  255. package/skills/aicut-motion/assets/atlas/styles/081-guilloch-security-print/style.json +13 -0
  256. package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/code.js +574 -0
  257. package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/poster.jpg +0 -0
  258. package/skills/aicut-motion/assets/atlas/styles/082-ascii-demoscene/style.json +13 -0
  259. package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/code.js +469 -0
  260. package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/poster.jpg +0 -0
  261. package/skills/aicut-motion/assets/atlas/styles/083-international-typographic-style/style.json +13 -0
  262. package/skills/aicut-motion/assets/atlas/styles/084-flow-field/code.js +531 -0
  263. package/skills/aicut-motion/assets/atlas/styles/084-flow-field/poster.jpg +0 -0
  264. package/skills/aicut-motion/assets/atlas/styles/084-flow-field/style.json +13 -0
  265. package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/code.js +675 -0
  266. package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/poster.jpg +0 -0
  267. package/skills/aicut-motion/assets/atlas/styles/085-macpaint-1984/style.json +13 -0
  268. package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/code.js +609 -0
  269. package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/poster.jpg +0 -0
  270. package/skills/aicut-motion/assets/atlas/styles/086-multi-scale-truchet/style.json +13 -0
  271. package/skills/aicut-motion/references/design-systems/README.md +1 -1
  272. package/skills/aicut-motion/references/motion-delivery.md +55 -0
  273. package/skills/aicut-motion/references/pages.md +1 -1
  274. package/skills/aicut-motion/references/style-index.json +690 -0
  275. package/skills/aicut-motion/references/style-research.md +73 -0
  276. package/skills/aicut-motion/references/style-selection.md +71 -0
  277. package/skills/aicut-motion/scripts/atlas-viewer.html +195 -0
  278. package/skills/aicut-motion/scripts/atlas.mjs +109 -0
  279. package/studio/client.mjs +3 -3
  280. package/studio/server/fetch-github.mjs +14 -3
  281. package/studio/server/server.mjs +71 -6
  282. package/studio/server/skill-summaries.mjs +60 -0
  283. package/studio/server/skills.mjs +121 -7
  284. package/studio/server/styles-store.mjs +235 -0
  285. package/studio/server/template-posters.mjs +2 -2
  286. package/studio/server/templates-page.mjs +93 -93
  287. package/studio/server/templates.mjs +32 -7
  288. package/studio/ui/dist/assets/ExportCenter-DasulhAv.js +1 -0
  289. package/studio/ui/dist/assets/SettingsModal-BZw0Hz2_.js +2 -0
  290. package/studio/ui/dist/assets/apple-touch-icon-ClxQ7j-O.png +0 -0
  291. package/studio/ui/dist/assets/es-C8XPZARI.js +1 -0
  292. package/studio/ui/dist/assets/favicon-Blwx8Trs.png +0 -0
  293. package/studio/ui/dist/assets/index-BjJVmBzB.js +1623 -0
  294. package/studio/ui/dist/assets/index-C3BfKZFO.css +1 -0
  295. package/studio/ui/dist/assets/ja-BeNkdaEj.js +1 -0
  296. package/studio/ui/dist/assets/ko-DcTVhdzK.js +1 -0
  297. package/studio/ui/dist/assets/zh-CN-Cg51ctbL.js +1 -0
  298. package/studio/ui/dist/index.html +4 -3
  299. package/studio/ui/dist/assets/ExportCenter-F-2vF-lQ.js +0 -1
  300. package/studio/ui/dist/assets/SettingsModal-BgVYguNu.js +0 -2
  301. package/studio/ui/dist/assets/es-BRUzvuoM.js +0 -1
  302. package/studio/ui/dist/assets/index-Bhg-9dA3.css +0 -1
  303. package/studio/ui/dist/assets/index-CWur1Iw9.js +0 -1625
  304. package/studio/ui/dist/assets/ja-Cth-uDcW.js +0 -1
  305. package/studio/ui/dist/assets/ko-upvw591D.js +0 -1
  306. package/studio/ui/dist/assets/zh-CN-BCfJFazB.js +0 -1
@@ -0,0 +1,792 @@
1
+ {
2
+ // ---------- 1940s film noir still ----------
3
+ const BAR = 54;
4
+
5
+ function detectivePath(x) {
6
+ // standing figure facing left; fedora, profile, turned-up collar, trench coat
7
+ x.beginPath();
8
+ x.moveTo(700, 720);
9
+ x.bezierCurveTo(706, 560, 716, 470, 742, 420); // coat front (left) edge
10
+ x.bezierCurveTo(756, 392, 770, 372, 778, 352); // lapel to collar
11
+ x.lineTo(770, 334); // collar point
12
+ x.bezierCurveTo(772, 322, 776, 312, 780, 304); // chin underside
13
+ x.lineTo(772, 300); x.lineTo(770, 292); // chin
14
+ x.lineTo(764, 284); x.lineTo(766, 276); // lips
15
+ x.lineTo(758, 264); x.lineTo(764, 258); // nose tip -> under nose
16
+ x.bezierCurveTo(766, 248, 770, 240, 772, 232); // brow
17
+ x.lineTo(730, 226); // hat brim front (tilted down)
18
+ x.bezierCurveTo(728, 220, 740, 214, 772, 212);
19
+ x.bezierCurveTo(776, 190, 786, 168, 812, 160); // crown front
20
+ x.bezierCurveTo(828, 156, 848, 160, 862, 158); // pinch
21
+ x.bezierCurveTo(880, 164, 888, 190, 890, 206);
22
+ x.bezierCurveTo(912, 208, 934, 214, 938, 222); // brim back
23
+ x.bezierCurveTo(918, 230, 896, 230, 880, 230);
24
+ x.bezierCurveTo(878, 260, 872, 290, 868, 306); // back of head / neck
25
+ x.bezierCurveTo(886, 318, 900, 322, 910, 330); // collar back
26
+ x.bezierCurveTo(960, 344, 1000, 366, 1018, 410); // shoulder
27
+ x.bezierCurveTo(1036, 470, 1044, 580, 1050, 720);
28
+ x.closePath();
29
+ }
30
+ function armPath(x) {
31
+ // forearm raised, hand holding cigarette near chest
32
+ x.beginPath();
33
+ x.moveTo(820, 470);
34
+ x.bezierCurveTo(790, 480, 760, 476, 735, 462);
35
+ x.bezierCurveTo(720, 456, 712, 446, 716, 436);
36
+ x.bezierCurveTo(722, 428, 736, 430, 746, 436);
37
+ x.bezierCurveTo(770, 446, 800, 444, 830, 432);
38
+ x.closePath();
39
+ }
40
+
41
+ // ================= EXPLAINER: a 1940s newsreel / B-picture montage =================
42
+ let corkTex = null;
43
+ function getCork(W, Hh) {
44
+ if (corkTex) return corkTex;
45
+ const c = H.layer(W, Hh), x = c.getContext('2d'), img = x.createImageData(W, Hh), d = img.data, R = H.rng(1212), nz = H.makeNoise(1213);
46
+ for (let y = 0; y < Hh; y++) for (let xx = 0; xx < W; xx++) {
47
+ const v = 112 + nz.n2(xx * 0.02, y * 0.02) * 16 + (R() - 0.5) * 46 + (R() < 0.04 ? -40 : 0);
48
+ const i = (y * W + xx) * 4; d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255;
49
+ }
50
+ x.putImageData(img, 0, 0); return (corkTex = c);
51
+ }
52
+ const gy = (v, a = 1) => `rgba(${v},${v},${v},${a})`;
53
+ // painted plaster (mean ~128, used through 'overlay' so it adds material without shifting exposure); half-res, cached
54
+ let plasterTex = null, hazeTex = null;
55
+ function getPlaster(W, Hh) {
56
+ if (plasterTex && plasterTex.width === W && plasterTex.height === Hh) return plasterTex;
57
+ const w = W >> 1, h = Hh >> 1, s = H.layer(w, h), x = s.getContext('2d'), img = x.createImageData(w, h), d = img.data, R = H.rng(1331), nz = H.makeNoise(1332);
58
+ for (let y = 0; y < h; y++) for (let xx = 0; xx < w; xx++) {
59
+ const v = 128 + nz.fbm2(xx * 0.018, y * 0.018, 3) * 30 + nz.n2(xx * 0.16, y * 0.16) * 8 + (R() - 0.5) * 12;
60
+ const i = (y * w + xx) * 4; d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255;
61
+ }
62
+ x.putImageData(img, 0, 0);
63
+ const c = H.layer(W, Hh); c.getContext('2d').drawImage(s, 0, 0, W, Hh); return (plasterTex = c);
64
+ }
65
+ // hanging cigarette-smoke strata (white, alpha = density); half-res, cached
66
+ function getHaze(W, Hh) {
67
+ if (hazeTex && hazeTex.width === (W >> 1) && hazeTex.height === (Hh >> 1)) return hazeTex;
68
+ const w = W >> 1, h = Hh >> 1, s = H.layer(w, h), x = s.getContext('2d'), img = x.createImageData(w, h), d = img.data, nz = H.makeNoise(1414);
69
+ for (let y = 0; y < h; y++) for (let xx = 0; xx < w; xx++) {
70
+ const q = nz.n2(xx * 0.004, y * 0.01) * 1.6;
71
+ const v = nz.fbm2(xx * 0.007 + q, y * 0.02, 4);
72
+ const i = (y * w + xx) * 4; d[i] = d[i + 1] = d[i + 2] = 255; d[i + 3] = H.clamp((v + 0.15) * 1.5) * 255;
73
+ }
74
+ x.putImageData(img, 0, 0); return (hazeTex = s);
75
+ }
76
+ // venetian-blind light: same slat geometry as before (34 px period, 17 px gaps, slope 0.5), drawn at half res with a
77
+ // real penumbra blur, a soft window-frame edge and an optional mullion shadow. bg/color allow a multiply shadow pass.
78
+ function slats(W, Hh, o) {
79
+ const w = W >> 1, h = Hh >> 1, l = H.layer(w, h), x = l.getContext('2d');
80
+ const x0 = o.x0, sw = o.sway || 0, X1 = W + 200, dy = (X1 - x0) * 0.5;
81
+ if (o.bg) { x.fillStyle = o.bg; x.fillRect(0, 0, w, h); }
82
+ x.save(); x.filter = `blur(${(o.blur || 3) / 2}px)`; x.scale(0.5, 0.5); x.globalAlpha = o.alpha; x.fillStyle = o.color || '#fff';
83
+ x.beginPath();
84
+ for (let k = -10; k < 48; k++) { const y = k * 34 + sw; x.moveTo(x0, y); x.lineTo(X1, y - dy); x.lineTo(X1, y - dy + 17); x.lineTo(x0, y + 17); x.closePath(); }
85
+ x.fill();
86
+ if (o.mullion) { const [mx, ms] = o.mullion; x.globalAlpha = o.bg ? Math.min(1, o.alpha * 1.4) : 1; x.globalCompositeOperation = o.bg ? 'source-over' : 'destination-out'; x.strokeStyle = o.bg ? (o.color || '#000') : '#000'; x.lineWidth = 9;
87
+ x.beginPath(); x.moveTo(mx - 60 * ms, -60); x.lineTo(mx + (Hh + 60) * ms, Hh + 60); x.stroke(); }
88
+ x.restore();
89
+ if (!o.bg && o.edge) { // window-frame penumbra on the leading edge, light strongest deep in the patch
90
+ x.save(); x.globalCompositeOperation = 'destination-in'; x.scale(0.5, 0.5);
91
+ const g = x.createLinearGradient(x0 - 30, 0, x0 + o.edge, 0); g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, 'rgba(0,0,0,1)');
92
+ x.fillStyle = g; x.fillRect(0, 0, W, Hh); x.restore();
93
+ }
94
+ return l;
95
+ }
96
+ function face(c, kind, cx, cy, s) {
97
+ const gr = c.createRadialGradient(cx - s * 0.3, cy - s * 0.3, s * 0.1, cx, cy, s * 1.1);
98
+ gr.addColorStop(0, gy(238)); gr.addColorStop(1, gy(118));
99
+ c.fillStyle = gr; c.beginPath();
100
+ if (kind === 'cat') {
101
+ c.ellipse(cx, cy + s * 0.08, s * 0.95, s * 0.8, 0, 0, 7); c.fill();
102
+ c.beginPath(); c.moveTo(cx - s * 0.9, cy - s * 0.2); c.lineTo(cx - s * 0.78, cy - s * 1.05); c.lineTo(cx - s * 0.3, cy - s * 0.65); c.fill();
103
+ c.beginPath(); c.moveTo(cx + s * 0.9, cy - s * 0.2); c.lineTo(cx + s * 0.78, cy - s * 1.05); c.lineTo(cx + s * 0.3, cy - s * 0.65); c.fill();
104
+ c.fillStyle = gy(20); c.beginPath(); c.ellipse(cx - s * 0.36, cy - s * 0.05, s * 0.14, s * 0.11, 0, 0, 7); c.ellipse(cx + s * 0.36, cy - s * 0.05, s * 0.14, s * 0.11, 0, 0, 7); c.fill();
105
+ c.beginPath(); c.moveTo(cx - s * 0.1, cy + s * 0.18); c.lineTo(cx + s * 0.1, cy + s * 0.18); c.lineTo(cx, cy + s * 0.3); c.fill();
106
+ c.strokeStyle = gy(235); c.lineWidth = 1.5; [-1, 1].forEach((sd) => { for (let k = 0; k < 3; k++) { c.beginPath(); c.moveTo(cx + sd * s * 0.3, cy + s * 0.3 + k * 4); c.lineTo(cx + sd * s * 1.05, cy + s * 0.2 + k * 10); c.stroke(); } });
107
+ } else {
108
+ c.ellipse(cx, cy, s * 0.72, s * 0.88, 0, 0, 7); c.fill();
109
+ c.fillStyle = gy(60); c.beginPath(); c.ellipse(cx - s * 0.8, cy - s * 0.05, s * 0.28, s * 0.62, 0.25, 0, 7); c.ellipse(cx + s * 0.8, cy - s * 0.05, s * 0.28, s * 0.62, -0.25, 0, 7); c.fill();
110
+ c.fillStyle = gy(215); c.beginPath(); c.ellipse(cx, cy + s * 0.42, s * 0.36, s * 0.28, 0, 0, 7); c.fill();
111
+ c.fillStyle = gy(20); c.beginPath(); c.ellipse(cx - s * 0.28, cy - s * 0.2, s * 0.1, s * 0.1, 0, 0, 7); c.ellipse(cx + s * 0.28, cy - s * 0.2, s * 0.1, s * 0.1, 0, 0, 7); c.fill();
112
+ c.beginPath(); c.ellipse(cx, cy + s * 0.3, s * 0.14, s * 0.09, 0, 0, 7); c.fill();
113
+ }
114
+ }
115
+ function mugshot(c, x, y, w, kind, num, rot = 0) {
116
+ const h = w * 1.25; c.save(); c.translate(x, y); c.rotate(rot);
117
+ // soft cast shadow on the board (key light from upper left)
118
+ c.save(); c.shadowColor = 'rgba(0,0,0,0.65)'; c.shadowBlur = 14; c.shadowOffsetX = 8; c.shadowOffsetY = 10;
119
+ c.fillStyle = gy(230); c.fillRect(-w / 2, -h / 2, w, h); c.restore();
120
+ const pg = c.createLinearGradient(-w / 2, -h / 2, w / 2, h / 2); pg.addColorStop(0, gy(248)); pg.addColorStop(1, gy(205));
121
+ c.fillStyle = pg; c.fillRect(-w / 2, -h / 2, w, h); // glossy print border
122
+ const bg = c.createLinearGradient(0, -h / 2, 0, h / 2); bg.addColorStop(0, gy(150)); bg.addColorStop(1, gy(82));
123
+ c.fillStyle = bg; c.fillRect(-w / 2 + 8, -h / 2 + 8, w - 16, h - 16); // studio backdrop, top-lit
124
+ c.strokeStyle = gy(95, 0.8); c.lineWidth = 1; for (let k = 1; k < 6; k++) { c.beginPath(); c.moveTo(-w / 2 + 8, -h / 2 + 8 + k * (h - 16) / 6); c.lineTo(w / 2 - 8, -h / 2 + 8 + k * (h - 16) / 6); c.stroke(); }
125
+ face(c, kind, 0, -h * 0.1, w * 0.26);
126
+ c.fillStyle = gy(22); c.fillRect(-w * 0.36, h * 0.18, w * 0.72, h * 0.22); // booking placard
127
+ c.fillStyle = gy(70); c.fillRect(-w * 0.36, h * 0.18, w * 0.72, 2);
128
+ c.fillStyle = gy(235); c.textAlign = 'center'; c.font = `700 ${Math.max(24, Math.round(w * 0.2))}px "Oswald"`; c.fillText(kind.toUpperCase(), 0, h * 0.3);
129
+ c.font = `400 ${Math.round(w * 0.08)}px "Courier Prime"`; c.fillText('No. ' + num, 0, h * 0.37);
130
+ c.strokeStyle = gy(60, 0.5); c.lineWidth = 1; c.strokeRect(-w / 2 + 8.5, -h / 2 + 8.5, w - 17, h - 17);
131
+ // curled bottom-right corner lifting off the board
132
+ const cs = w * 0.17, ex = w / 2, ey = h / 2;
133
+ c.fillStyle = 'rgba(0,0,0,0.35)'; c.beginPath(); c.moveTo(ex - cs, ey); c.lineTo(ex, ey - cs); c.lineTo(ex + 3, ey + 4); c.fill();
134
+ const cg = c.createLinearGradient(ex - cs, ey - cs, ex - cs * 0.4, ey - cs * 0.4); cg.addColorStop(0, gy(255)); cg.addColorStop(1, gy(170));
135
+ c.fillStyle = cg; c.beginPath(); c.moveTo(ex - cs, ey); c.lineTo(ex, ey - cs); c.quadraticCurveTo(ex - cs * 0.55, ey - cs * 0.55, ex - cs * 0.82, ey - cs * 0.82); c.closePath(); c.fill();
136
+ // push pin
137
+ c.fillStyle = 'rgba(0,0,0,0.5)'; c.beginPath(); c.arc(3, -h / 2 + 8, 5.5, 0, 7); c.fill();
138
+ const pn = c.createRadialGradient(-2, -h / 2 + 2, 0.5, 0, -h / 2 + 4, 6); pn.addColorStop(0, gy(245)); pn.addColorStop(0.4, gy(120)); pn.addColorStop(1, gy(30));
139
+ c.fillStyle = pn; c.beginPath(); c.arc(0, -h / 2 + 4, 6, 0, 7); c.fill();
140
+ c.restore();
141
+ }
142
+ function machine(c, x, y, s, dials, lampT, label = true, shadow = true) {
143
+ c.save(); c.translate(x, y); c.scale(s, s);
144
+ // sheet-metal cabinet lit from above, with a soft cast shadow
145
+ const cab = c.createLinearGradient(0, -210, 0, 210); cab.addColorStop(0, gy(118)); cab.addColorStop(0.55, gy(70)); cab.addColorStop(1, gy(35));
146
+ c.save(); if (shadow) { c.shadowColor = 'rgba(0,0,0,0.65)'; c.shadowBlur = 14; c.shadowOffsetX = 8; c.shadowOffsetY = 10; }
147
+ c.fillStyle = cab; c.fillRect(-170, -210, 340, 420); c.restore();
148
+ c.fillStyle = gy(185); c.fillRect(-170, -210, 340, 2.5); c.fillStyle = gy(150); c.fillRect(-170, -210, 2.5, 420); // bevel: lit top/left
149
+ c.fillStyle = gy(14); c.fillRect(-170, 207.5, 340, 2.5); c.fillRect(167.5, -210, 2.5, 420); // dark bottom/right
150
+ const pnl = c.createLinearGradient(0, -190, 0, 190); pnl.addColorStop(0, gy(60)); pnl.addColorStop(1, gy(26));
151
+ c.fillStyle = pnl; c.fillRect(-150, -190, 300, 380);
152
+ c.fillStyle = gy(12); c.fillRect(-150, -190, 300, 2.5); c.fillRect(-150, -190, 2.5, 380); // recessed panel
153
+ c.fillStyle = gy(120); c.fillRect(-150, 187.5, 300, 2.5); c.fillRect(147.5, -190, 2.5, 380);
154
+ for (const [rx, ry] of [[-160, -200], [160, -200], [-160, 200], [160, 200], [-160, 0], [160, 0]]) { // rivets
155
+ c.fillStyle = gy(12, 0.8); c.beginPath(); c.arc(rx + 1.2, ry + 1.5, 3.6, 0, 7); c.fill();
156
+ const rg = c.createRadialGradient(rx - 1.2, ry - 1.2, 0.3, rx, ry, 3.6); rg.addColorStop(0, gy(235)); rg.addColorStop(1, gy(75));
157
+ c.fillStyle = rg; c.beginPath(); c.arc(rx, ry, 3.4, 0, 7); c.fill();
158
+ }
159
+ for (let i = 0; i < 2; i++) { const rx = -70 + i * 140, ry = -120;
160
+ const mg = c.createRadialGradient(rx - 16, ry - 18, 4, rx, ry, 48); mg.addColorStop(0, gy(232)); mg.addColorStop(0.6, gy(160)); mg.addColorStop(1, gy(92));
161
+ c.fillStyle = gy(15); c.beginPath(); c.arc(rx + 2, ry + 3, 48, 0, 7); c.fill();
162
+ c.fillStyle = mg; c.beginPath(); c.arc(rx, ry, 46, 0, 7); c.fill();
163
+ c.strokeStyle = gy(60); c.lineWidth = 2; c.beginPath(); c.arc(rx, ry, 34, 0, 7); c.stroke();
164
+ c.strokeStyle = gy(38); c.lineWidth = 6; c.lineCap = 'round'; for (let k = 0; k < 3; k++) { const a = lampT * 3 + k * 2.09 + i; c.beginPath(); c.moveTo(rx, ry); c.lineTo(rx + Math.cos(a) * 40, ry + Math.sin(a) * 40); c.stroke(); }
165
+ c.lineCap = 'butt'; c.fillStyle = gy(40); c.beginPath(); c.arc(rx, ry, 12, 0, 7); c.fill();
166
+ c.fillStyle = gy(200); c.beginPath(); c.arc(rx - 3, ry - 3, 3, 0, 7); c.fill(); }
167
+ for (let k = 0; k < 8; k++) { const on = Math.sin(lampT * 5 + k * 1.9) > 0.2, lx = -112 + k * 32, ly = -38;
168
+ c.fillStyle = gy(10); c.beginPath(); c.arc(lx, ly, 10.5, 0, 7); c.fill();
169
+ if (on) { const lg = c.createRadialGradient(lx, ly, 1, lx, ly, 20); lg.addColorStop(0, gy(255)); lg.addColorStop(0.4, gy(245, 0.95)); lg.addColorStop(0.42, gy(220, 0.35)); lg.addColorStop(1, gy(200, 0));
170
+ c.fillStyle = lg; c.beginPath(); c.arc(lx, ly, 20, 0, 7); c.fill(); }
171
+ else { c.fillStyle = gy(78); c.beginPath(); c.arc(lx, ly, 8, 0, 7); c.fill(); c.fillStyle = gy(150, 0.7); c.beginPath(); c.arc(lx - 2.5, ly - 2.5, 2, 0, 7); c.fill(); } }
172
+ dials.forEach((a, i) => { const dx = -95 + i * 95, dy = 40;
173
+ c.fillStyle = gy(12); c.beginPath(); c.arc(dx + 2, dy + 3, 40, 0, 7); c.fill();
174
+ const bz = c.createLinearGradient(dx, dy - 40, dx, dy + 40); bz.addColorStop(0, gy(200)); bz.addColorStop(1, gy(55));
175
+ c.fillStyle = bz; c.beginPath(); c.arc(dx, dy, 40, 0, 7); c.fill(); // chrome bezel
176
+ const fg = c.createRadialGradient(dx - 10, dy - 12, 4, dx, dy, 36); fg.addColorStop(0, gy(245)); fg.addColorStop(1, gy(168));
177
+ c.fillStyle = fg; c.beginPath(); c.arc(dx, dy, 36, 0, 7); c.fill();
178
+ c.strokeStyle = gy(40); c.lineWidth = 2;
179
+ for (let k = 0; k < 10; k++) { const b = -2.4 + k * 0.53; c.beginPath(); c.moveTo(dx + Math.cos(b) * 28, dy + Math.sin(b) * 28); c.lineTo(dx + Math.cos(b) * 34, dy + Math.sin(b) * 34); c.stroke(); }
180
+ c.strokeStyle = gy(20, 0.35); c.lineWidth = 5; c.beginPath(); c.moveTo(dx + 2, dy + 3); c.lineTo(dx + 2 + Math.cos(a) * 28, dy + 3 + Math.sin(a) * 28); c.stroke(); // needle shadow
181
+ c.strokeStyle = gy(30); c.beginPath(); c.moveTo(dx, dy); c.lineTo(dx + Math.cos(a) * 28, dy + Math.sin(a) * 28); c.stroke(); c.fillStyle = gy(30); c.beginPath(); c.arc(dx, dy, 6, 0, 7); c.fill();
182
+ c.strokeStyle = 'rgba(255,255,255,0.45)'; c.lineWidth = 4; c.lineCap = 'round'; c.beginPath(); c.arc(dx, dy, 29, -2.7, -1.75); c.stroke(); c.lineCap = 'butt'; // glass glint
183
+ });
184
+ c.fillStyle = gy(8); c.fillRect(-90, 120, 180, 14); c.fillStyle = gy(130); c.fillRect(-90, 134, 180, 2); // tape slot with lit lip
185
+ if (label) { const lp = c.createLinearGradient(0, 150, 0, 184); lp.addColorStop(0, gy(228)); lp.addColorStop(1, gy(170));
186
+ c.fillStyle = gy(10, 0.6); c.fillRect(-108, 153, 220, 34);
187
+ c.fillStyle = lp; c.fillRect(-110, 150, 220, 34); c.fillStyle = gy(80); c.beginPath(); c.arc(-102, 167, 2.5, 0, 7); c.arc(102, 167, 2.5, 0, 7); c.fill();
188
+ c.fillStyle = gy(20); c.font = '700 24px "Oswald"'; c.textAlign = 'center'; c.fillText('THE MACHINE', 0, 176); }
189
+ c.restore();
190
+ }
191
+ function blindsOverlay(c, W, Hh, x0, alpha, sway) {
192
+ c.save(); c.globalCompositeOperation = 'screen';
193
+ for (let k = -10; k < 30; k++) {
194
+ const y = k * 34 + sway; c.fillStyle = `rgba(255,255,255,${alpha})`;
195
+ c.beginPath(); c.moveTo(x0, y); c.lineTo(W + 200, y - (W + 200 - x0) * 0.5); c.lineTo(W + 200, y - (W + 200 - x0) * 0.5 + 17); c.lineTo(x0, y + 17); c.fill();
196
+ }
197
+ c.restore();
198
+ }
199
+ let htPat = null;
200
+ function halftone(c) {
201
+ if (htPat) return htPat;
202
+ const p = H.layer(6, 12), x = p.getContext('2d'); x.fillStyle = 'rgba(0,0,0,0.16)';
203
+ for (const [px, py] of [[0, 0], [6, 0], [0, 12], [6, 12], [3, 6]]) { x.beginPath(); x.arc(px, py, 1.5, 0, 7); x.fill(); }
204
+ return (htPat = c.createPattern(p, 'repeat'));
205
+ }
206
+ function newspaper(c, cx, cy, sc, rot, head, sub) {
207
+ c.save(); c.translate(cx, cy); c.rotate(rot); c.scale(sc, sc);
208
+ c.fillStyle = 'rgba(0,0,0,0.1)'; for (let k = 8; k >= 0; k--) { const g = k * 2.5; c.fillRect(-380 + 12 - g, -240 + 18 - g, 760 + 2 * g, 480 + 2 * g); } // soft drop shadow
209
+ const np = c.createLinearGradient(-380, -240, 380, 240); np.addColorStop(0, gy(240)); np.addColorStop(0.5, gy(224)); np.addColorStop(1, gy(196));
210
+ c.fillStyle = np; c.fillRect(-380, -240, 760, 480);
211
+ // centre fold: shaded valley with a lit ridge
212
+ const fd = c.createLinearGradient(-30, 0, 30, 0); fd.addColorStop(0, 'rgba(0,0,0,0)'); fd.addColorStop(0.45, 'rgba(0,0,0,0.13)'); fd.addColorStop(0.55, 'rgba(255,255,255,0.18)'); fd.addColorStop(1, 'rgba(255,255,255,0)');
213
+ c.fillStyle = fd; c.fillRect(-30, -240, 60, 480);
214
+ c.fillStyle = gy(25); c.textAlign = 'center'; c.font = '400 54px "UnifrakturMaguntia"'; c.fillText('The Daily Circuit', 0, -182);
215
+ c.fillRect(-360, -168, 720, 3); c.fillRect(-360, -160, 720, 1);
216
+ c.font = '400 14px "Courier Prime"'; c.fillText('LATE CITY EDITION ★ FIVE CENTS', 0, -146);
217
+ c.font = '400 76px "Anton"'; c.fillText(head, 0, -66);
218
+ c.font = 'italic 400 26px "Playfair Display"'; c.fillText(sub, 0, -28);
219
+ const ph = c.createRadialGradient(-250, 40, 20, -210, 100, 230); ph.addColorStop(0, gy(190)); ph.addColorStop(1, gy(80));
220
+ c.fillStyle = ph; c.fillRect(-360, -10, 300, 220); // wire photo, flash-lit
221
+ machine(c, -210, 100, 0.45, [-1, 0.2, 1.2], 0, false, false);
222
+ c.fillStyle = halftone(c); c.fillRect(-360, -10, 300, 220); // coarse newsprint halftone c.strokeStyle = gy(40); c.lineWidth = 2; c.strokeRect(-360, -10, 300, 220);
223
+ c.fillStyle = gy(110); for (let col = 0; col < 2; col++) for (let k = 0; k < 20; k++) c.fillRect(-40 + col * 210, -6 + k * 11, 190 - (k % 7 === 6 ? 80 : 0), 4);
224
+ c.restore();
225
+ }
226
+ function odometer(c, x, y, value, digits, h) {
227
+ const w = h * 0.62, bw = digits * (w + 4) + 12, bh = h + 16;
228
+ c.save(); c.shadowColor = 'rgba(0,0,0,0.7)'; c.shadowBlur = 14; c.shadowOffsetX = 6; c.shadowOffsetY = 9;
229
+ const bz = c.createLinearGradient(0, y - 8, 0, y - 8 + bh); bz.addColorStop(0, gy(92)); bz.addColorStop(0.5, gy(40)); bz.addColorStop(1, gy(18));
230
+ c.fillStyle = bz; c.fillRect(x - 8, y - 8, bw, bh); c.restore(); // enamelled counter housing
231
+ c.fillStyle = gy(170); c.fillRect(x - 8, y - 8, bw, 2); c.fillStyle = gy(8); c.fillRect(x - 8, y + h + 6, bw, 2);
232
+ c.fillStyle = gy(8); c.fillRect(x - 2, y - 2, bw - 12, h + 4);
233
+ for (let k = 0; k < digits; k++) {
234
+ const place = Math.pow(10, digits - 1 - k), v = value / place, d = Math.floor(v) % 10, fr = digits - 1 - k === 0 ? v % 1 : (value % place) / place > 0.9 ? ((value % place) / place - 0.9) * 10 : 0;
235
+ const wx = x + k * (w + 4);
236
+ c.save(); c.beginPath(); c.rect(wx, y, w, h); c.clip();
237
+ const g2 = c.createLinearGradient(0, y, 0, y + h); g2.addColorStop(0, gy(120)); g2.addColorStop(0.5, gy(240)); g2.addColorStop(1, gy(120));
238
+ c.fillStyle = g2; c.fillRect(wx, y, w, h);
239
+ c.fillStyle = gy(20); c.font = `700 ${Math.round(h * 0.8)}px "Courier Prime"`; c.textAlign = 'center';
240
+ c.fillText(String(d), wx + w / 2, y + h * 0.78 - fr * h); c.fillText(String((d + 1) % 10), wx + w / 2, y + h * 1.78 - fr * h);
241
+ c.restore();
242
+ }
243
+ // glass window over the drums: a lit band across the top, darkness under the housing lip
244
+ const gl = c.createLinearGradient(0, y, 0, y + h); gl.addColorStop(0, 'rgba(0,0,0,0.35)'); gl.addColorStop(0.12, 'rgba(255,255,255,0.22)'); gl.addColorStop(0.3, 'rgba(255,255,255,0.04)'); gl.addColorStop(0.85, 'rgba(0,0,0,0)'); gl.addColorStop(1, 'rgba(0,0,0,0.3)');
245
+ c.fillStyle = gl; c.fillRect(x, y, digits * (w + 4) - 4, h);
246
+ }
247
+
248
+ function noirExplainer(ctx, W, Hh, t) {
249
+ const sm = H.smooth, E = H.ease, nz = H.makeNoise(46);
250
+ const fr24 = Math.floor(t * 24), Rg = H.rng(7000 + fr24);
251
+ const weaveX = nz.n2(t * 3.1, 0.3) * 2.2, weaveY = nz.n2(0.7, t * 2.7) * 2.8, flick = 1 + nz.n2(t * 9, 4.4) * 0.05;
252
+ // newsreel lettering: white bold caps with black keyline
253
+ const NR = (c, s, x, y, size, prog = 1, align = 'center') => { if (prog <= 0) return; c.save(); c.globalAlpha = Math.min(1, prog * 2); c.font = `700 ${size}px "Oswald"`; c.textAlign = align; c.lineJoin = 'round'; c.lineWidth = size * 0.16; c.strokeStyle = gy(0); c.strokeText(s, x, y); c.fillStyle = gy(250); c.fillText(s, x, y); c.restore(); };
254
+ const NRarrow = (c, pts, prog) => { if (prog <= 0) return; const n = Math.max(1, Math.floor((pts.length - 1) * Math.min(1, prog)));
255
+ for (const [col, lw] of [[gy(0), 13], [gy(250), 7]]) { c.strokeStyle = col; c.lineWidth = lw; c.lineCap = 'round'; c.beginPath(); c.moveTo(...pts[0]); for (let i = 1; i <= n; i++) c.lineTo(...pts[i]); c.stroke(); }
256
+ if (prog >= 1) { const e = pts[pts.length - 1], q = pts[pts.length - 3], a = Math.atan2(e[1] - q[1], e[0] - q[0]); for (const [col, grow] of [[gy(0), 5], [gy(250), 0]]) { c.fillStyle = col; c.beginPath(); c.moveTo(e[0] + Math.cos(a) * (14 + grow), e[1] + Math.sin(a) * (14 + grow)); c.lineTo(e[0] + Math.cos(a + 2.4) * (22 + grow), e[1] + Math.sin(a + 2.4) * (22 + grow)); c.lineTo(e[0] + Math.cos(a - 2.4) * (22 + grow), e[1] + Math.sin(a - 2.4) * (22 + grow)); c.fill(); } } };
257
+ const Q = (x0, y0, qx, qy, x1, y1, n = 28) => Array.from({ length: n + 1 }, (_, i) => { const u = i / n; return [(1 - u) * (1 - u) * x0 + 2 * (1 - u) * u * qx + u * u * x1, (1 - u) * (1 - u) * y0 + 2 * (1 - u) * u * qy + u * u * y1]; });
258
+
259
+ // shared set-dressing: plaster grain, soft light pools, screened slat light, drifting smoke
260
+ const tex = (c, a) => { c.save(); c.globalCompositeOperation = 'overlay'; c.globalAlpha = a; c.drawImage(getPlaster(W, Hh), 0, 0); c.restore(); };
261
+ const pool = (c, x, y, rx, ry, a) => { c.save(); c.globalCompositeOperation = 'screen'; c.translate(x, y); c.scale(1, ry / rx);
262
+ const g = c.createRadialGradient(0, 0, 0, 0, 0, rx); g.addColorStop(0, `rgba(255,255,255,${a})`); g.addColorStop(0.5, `rgba(255,255,255,${a * 0.45})`); g.addColorStop(1, 'rgba(255,255,255,0)');
263
+ c.fillStyle = g; c.fillRect(-rx, -rx, rx * 2, rx * 2); c.restore(); };
264
+ const screenL = (c, l, a = 1) => { c.save(); c.globalCompositeOperation = 'screen'; c.globalAlpha = a; c.drawImage(l, 0, 0, W, Hh); c.restore(); };
265
+ const haze = (c, mask, a) => { // smoke strata, visible only where the light passes through it
266
+ const w4 = W >> 2, h4 = Hh >> 2, hl = H.layer(w4, h4), hx = hl.getContext('2d');
267
+ const hz = getHaze(W, Hh), off = (t * 4.5) % w4;
268
+ hx.drawImage(hz, -off, 0, w4, h4); hx.drawImage(hz, w4 - off, 0, w4, h4);
269
+ hx.globalCompositeOperation = 'destination-in'; hx.drawImage(mask, 0, 0, w4, h4);
270
+ screenL(c, hl, a);
271
+ };
272
+ const figT = (c) => { c.translate(980, 0); c.scale(0.9, 0.9); c.translate(-760, 60); };
273
+
274
+ const scenes = {
275
+ hook(c) {
276
+ c.fillStyle = gy(14); c.fillRect(0, 0, W, Hh);
277
+ pool(c, 640, 380, 620, 420, 0.16);
278
+ screenL(c, slats(W, Hh, { x0: 300, sway: t * 6, alpha: 0.2, blur: 7, edge: 180 })); tex(c, 0.7);
279
+ const p = E.out(sm(0.15, 1.15, t));
280
+ newspaper(c, 640, 360, 0.04 + 0.96 * p, (1 - p) * Math.PI * 5 - 0.04, 'HOW DOES AI LEARN?', 'Thinking machine baffles the experts');
281
+ },
282
+ data(c) {
283
+ c.drawImage(getCork(W, Hh), 0, 0);
284
+ const p = sm(2.7, 5.4, t), cnt = p >= 1 ? 1000000 : Math.pow(10, 6 * p);
285
+ machine(c, 1010, 300, 0.95, [-1.2 + t * 0.4, 0.3 - t * 0.3, 1.1 + t * 0.2], t, true);
286
+ const cards = [];
287
+ for (let k = 0; k < 9; k++) { const t0 = 2.75 + k * 0.28; if (t < t0) continue; cards.push(k); }
288
+ // red-string... in b&w: white string from each file to the machine
289
+ cards.forEach((k) => { const x = 110 + (k % 3) * 175, y = 170 + Math.floor(k / 3) * 165; const sp = sm(2.75 + k * 0.28 + 0.1, 2.75 + k * 0.28 + 0.45, t);
290
+ c.strokeStyle = gy(235, 0.85); c.lineWidth = 2; c.beginPath(); c.moveTo(x, y - 70); c.lineTo(H.lerp(x, 850, sp), H.lerp(y - 70, 300, sp)); c.stroke(); });
291
+ cards.forEach((k) => { const t0 = 2.75 + k * 0.28, sl = E.out(sm(t0, t0 + 0.15, t)); const x = 110 + (k % 3) * 175, y = 170 + Math.floor(k / 3) * 165;
292
+ c.save(); c.translate(x, y); c.scale(1.4 - 0.4 * sl, 1.4 - 0.4 * sl); mugshot(c, 0, 0, 120, k % 3 === 1 ? 'dog' : 'cat', 4400 + k * 37, Math.sin(k * 2.7) * 0.06); c.restore(); });
293
+ // hard key light from a desk lamp, blind-slat shadows raked across the whole board (multiplied over everything)
294
+ c.save(); c.globalCompositeOperation = 'multiply';
295
+ const lamp = c.createRadialGradient(640, 200, 50, 640, 300, 760); lamp.addColorStop(0, gy(255)); lamp.addColorStop(0.4, gy(228)); lamp.addColorStop(0.75, gy(178)); lamp.addColorStop(1, gy(95));
296
+ c.fillStyle = lamp; c.fillRect(0, 0, W, Hh);
297
+ c.drawImage(slats(W, Hh, { x0: -200, sway: t * 3, alpha: 0.3, blur: 6, color: '#000', bg: '#fff', mullion: [560, 0.55] }), 0, 0, W, Hh);
298
+ c.restore();
299
+ pool(c, 620, 230, 430, 330, 0.3);
300
+ odometer(c, 838, 556, Math.round(cnt), 7, 72);
301
+ NR(c, 'EXAMPLES', 1010, 545, 30, sm(3.0, 3.3, t));
302
+ NR(c, '→ FED TO THE MACHINE', 640, 110, 34, sm(3.2, 3.5, t));
303
+ },
304
+ loop(c, zoom) {
305
+ c.fillStyle = gy(10); c.fillRect(0, 0, W, Hh);
306
+ pool(c, 640, 360, 640, 420, 0.07); // faint spill on the back wall
307
+ // hanging lamp: a volumetric beam through smoky air, brightest at the shade, fading toward the floor
308
+ const cl = H.layer(W >> 2, Hh >> 2), cx = cl.getContext('2d'); cx.scale(0.25, 0.25); cx.filter = 'blur(4.5px)';
309
+ const vg = cx.createLinearGradient(0, 54, 0, 720); vg.addColorStop(0, 'rgba(255,255,255,0.7)'); vg.addColorStop(0.35, 'rgba(255,255,255,0.32)'); vg.addColorStop(1, 'rgba(255,255,255,0.1)');
310
+ cx.fillStyle = vg; cx.beginPath(); cx.moveTo(600, 54); cx.lineTo(680, 54); cx.lineTo(1250, 720); cx.lineTo(30, 720); cx.fill();
311
+ cx.filter = 'none'; const core = cx.createRadialGradient(640, 60, 10, 640, 200, 260); core.addColorStop(0, 'rgba(255,255,255,0.45)'); core.addColorStop(1, 'rgba(255,255,255,0)');
312
+ cx.globalCompositeOperation = 'source-atop'; cx.fillStyle = core; cx.fillRect(0, 0, W, Hh);
313
+ screenL(c, cl, 0.75); haze(c, cl, 0.5); tex(c, 0.6);
314
+ // table: lit front edge, top falling off into darkness, an elliptical pool of lamplight
315
+ const tg = c.createLinearGradient(0, 560, 0, 720); tg.addColorStop(0, gy(58)); tg.addColorStop(1, gy(12));
316
+ c.fillStyle = tg; c.fillRect(0, 560, W, 160);
317
+ c.fillStyle = gy(150); c.fillRect(0, 556, W, 3); c.fillStyle = gy(70); c.fillRect(0, 559, W, 3);
318
+ pool(c, 640, 600, 470, 70, 0.42);
319
+ // the lamp shade itself, with a glowing bulb lip
320
+ const bg0 = c.createRadialGradient(640, 62, 2, 640, 62, 70); bg0.addColorStop(0, 'rgba(255,255,255,0.9)'); bg0.addColorStop(1, 'rgba(255,255,255,0)');
321
+ c.fillStyle = bg0; c.fillRect(570, 20, 140, 110);
322
+ const sh = c.createLinearGradient(590, 0, 690, 0); sh.addColorStop(0, gy(12)); sh.addColorStop(0.3, gy(70)); sh.addColorStop(1, gy(10));
323
+ c.fillStyle = sh; c.beginPath(); c.moveTo(590, 40); c.lineTo(690, 40); c.lineTo(670, 60); c.lineTo(610, 60); c.fill();
324
+ c.fillStyle = gy(250); c.fillRect(612, 59, 56, 3);
325
+ // dust motes drifting in the beam
326
+ const Rm = H.rng(4242);
327
+ for (let k = 0; k < 44; k++) {
328
+ const yb = Rm() * 620, ph = Rm() * 6.28, u = Rm() * 2 - 1, r = Rm() * 1.4 + 0.7, my = 80 + ((yb + t * 11) % 620), hw = 40 + (my - 54) / 666 * 570;
329
+ const mx = 640 + u * hw * 0.85 + Math.sin(t * 0.7 + ph) * 7;
330
+ c.fillStyle = `rgba(255,255,255,${(0.35 + 0.45 * Rm()) * (1 - my / 900)})`; c.beginPath(); c.arc(mx, my, r, 0, 7); c.fill();
331
+ }
332
+ const adj = sm(8.75, 9.35, t), step = Math.floor(adj * 4) / 4;
333
+ c.save(); c.translate(640, 330); c.scale(zoom, zoom); c.translate(-640, -330 - (zoom - 1) * 60);
334
+ machine(c, 640, 320, 0.85, [-1.6 + step * 1.9, 0.6 - step * 1.7, -0.2 + step * 1.3], t, true);
335
+ c.restore();
336
+ const ms = E.out(sm(6.3, 6.6, t)); mugshot(c, H.lerp(-120, 190, ms), 330, 150, 'cat', 9001, -0.05);
337
+ // tape with the answer
338
+ const tp = sm(6.95, 7.35, t);
339
+ if (tp > 0 && zoom < 1.05) {
340
+ const tpg = c.createLinearGradient(0, 0, 0, 74); tpg.addColorStop(0, gy(244)); tpg.addColorStop(1, gy(200));
341
+ c.save(); c.translate(870, 420); c.rotate(-0.06);
342
+ c.save(); c.shadowColor = 'rgba(0,0,0,0.6)'; c.shadowBlur = 12; c.shadowOffsetX = 5; c.shadowOffsetY = 8; c.fillStyle = gy(225); c.fillRect(0, 0, 330 * tp, 74); c.restore();
343
+ c.fillStyle = tpg; c.fillRect(0, 0, 330 * tp, 74);
344
+ c.fillStyle = gy(20); c.font = '400 52px "Special Elite"'; c.save(); c.beginPath(); c.rect(0, 0, 330 * tp, 74); c.clip(); c.fillText(t < 9.5 ? 'DOG?' : 'CAT.', 22, 56); c.restore();
345
+ if (t >= 9.5) { c.fillStyle = tpg; c.fillRect(0, 0, 330, 74); c.fillStyle = gy(20); c.fillText('CAT.', 22, 56); }
346
+ c.restore();
347
+ }
348
+ // rubber stamp emphasis
349
+ const st = (t0, word) => { const p = sm(t0, t0 + 0.12, t); if (p <= 0) return; c.save(); c.translate(1050, 318); c.rotate(-0.16); const s = 2.2 - 1.2 * E.out(p); c.scale(s, s); c.globalAlpha = Math.min(1, p * 1.5);
350
+ c.fillStyle = gy(10, 0.7); c.fillRect(-116, -44, 232, 88); c.strokeStyle = gy(245); c.lineWidth = 6; c.strokeRect(-110, -38, 220, 76); c.fillStyle = gy(245); c.font = '700 54px "Oswald"'; c.textAlign = 'center'; c.fillText(word, 0, 20); c.restore(); };
351
+ if (t < 9.5 && zoom < 1.05) st(7.6, 'WRONG');
352
+ if (t >= 9.75) st(9.75, 'RIGHT');
353
+ if (zoom < 1.05) {
354
+ NRarrow(c, Q(285, 280, 360, 230, 470, 260), sm(6.6, 6.9, t)); NR(c, 'GUESS', 370, 205, 40, sm(6.7, 6.95, t));
355
+ NR(c, 'CHECK', 1050, 238, 40, sm(7.7, 7.95, t));
356
+ if (t < 9.5) NR(c, '✗', 1225, 340, 70, sm(7.65, 7.75, t));
357
+ else NR(c, '✓', 1225, 340, 70, sm(9.85, 9.95, t));
358
+ NRarrow(c, Q(1000, 500, 860, 640, 720, 470), sm(8.15, 8.55, t)); NR(c, 'ADJUST', 900, 650, 40, sm(8.3, 8.55, t));
359
+ } else NR(c, 'ADJUSTING THE DIALS', 640, 640, 44, 1);
360
+ },
361
+ prog(c) {
362
+ c.fillStyle = gy(14); c.fillRect(0, 0, W, Hh);
363
+ // desk-lamp pool behind the chart, soft slat light raking the wall
364
+ pool(c, 450, 340, 560, 380, 0.2); pool(c, 1040, 330, 260, 200, 0.08);
365
+ const sl = slats(W, Hh, { x0: -200, sway: t * 4, alpha: 0.2, blur: 7, mullion: [900, 0.55] });
366
+ screenL(c, sl); haze(c, sl, 0.35); tex(c, 0.6);
367
+ c.strokeStyle = gy(90); c.lineWidth = 1; for (let k = 0; k <= 5; k++) { c.beginPath(); c.moveTo(120, 160 + k * 80); c.lineTo(760, 160 + k * 80); c.stroke(); }
368
+ c.strokeStyle = gy(235); c.lineWidth = 4; c.beginPath(); c.moveTo(120, 140); c.lineTo(120, 560); c.lineTo(780, 560); c.stroke();
369
+ NR(c, 'TRAINING ROUNDS →', 450, 612, 30);
370
+ const lp = sm(10.75, 12.2, t), pts = []; for (let r = 0; r <= 30; r++) { const a = 0.97 - 0.36 * Math.exp(-r / 7) + (r > 0 && r < 30 ? Math.sin(r * 2.7) * 0.01 : 0); pts.push([130 + r * 21, 560 - (a - 0.5) / 0.5 * 400]); }
371
+ const n = Math.floor(lp * 30); c.lineCap = 'round';
372
+ c.lineJoin = 'round';
373
+ for (const [col, lw, glow] of [[gy(0), 12, 0], [gy(250), 6, 1]]) { c.save(); if (glow) { c.shadowColor = gy(255, 0.6); c.shadowBlur = 12; }
374
+ c.strokeStyle = col; c.lineWidth = lw; c.beginPath(); c.moveTo(...pts[0]); for (let i = 1; i <= n; i++) c.lineTo(...pts[i]); c.stroke(); c.restore(); }
375
+ NR(c, 'ACCURACY', 1010, 250, 44);
376
+ const pct = Math.round(61 + 36 * E.out(lp));
377
+ odometer(c, 905, 290, pct, 2, 110); NR(c, '%', 1170, 385, 90);
378
+ },
379
+ news2(c) {
380
+ const p = E.out(sm(12.3, 13.0, t));
381
+ newspaper(c, 640, 360, 0.04 + 0.96 * p, (1 - p) * Math.PI * 5 + 0.05, 'ACCURACY HITS 97%!', 'Machine learns from a million case files');
382
+ },
383
+ end(c) {
384
+ c.fillStyle = gy(12); c.fillRect(0, 0, W, Hh);
385
+ const sw = Math.sin(t) * 4;
386
+ // real slat light: bright bands with penumbra, a mullion bar, and the detective's shadow cut out of it
387
+ const sl = slats(W, Hh, { x0: 380, sway: sw, alpha: 0.85, blur: 6, edge: 170, mullion: [640, 0.55] });
388
+ const shL = H.layer(W >> 2, Hh >> 2), shx = shL.getContext('2d'); shx.filter = 'blur(3px)'; shx.scale(0.25, 0.25); shx.translate(-130, 40); figT(shx); shx.fillStyle = '#000'; detectivePath(shx); shx.fill();
389
+ const sx = sl.getContext('2d'); sx.save(); sx.globalCompositeOperation = 'destination-out'; sx.globalAlpha = 0.85; sx.drawImage(shL, 0, 0, W >> 1, Hh >> 1); sx.restore();
390
+ screenL(c, sl, 0.78); tex(c, 0.55); // plaster shows in the lit bands
391
+ haze(c, sl, 0.4);
392
+ // a smoke wisp hanging in the beam
393
+ const wl = H.layer(W >> 2, Hh >> 2), wc = wl.getContext('2d'); wc.filter = 'blur(2px)'; wc.scale(0.25, 0.25); wc.lineCap = 'round';
394
+ for (let s = 0; s < 3; s++) { wc.strokeStyle = `rgba(255,255,255,${0.15 - s * 0.035})`; wc.lineWidth = 10 + s * 12; wc.beginPath();
395
+ const wx = 860 + s * 30 + Math.sin(t * 0.6 + s) * 10; wc.moveTo(wx, 520); wc.bezierCurveTo(wx - 60, 420 - s * 10, wx + 70 + Math.sin(t * 0.8) * 20, 300, wx - 10, 150 + s * 20); wc.stroke(); }
396
+ screenL(c, wl);
397
+ // the detective: near-black, faint slat light across the coat, a hard rim where the window light grazes him
398
+ c.save(); figT(c); c.fillStyle = gy(8); detectivePath(c); c.fill(); c.restore();
399
+ c.save(); figT(c); detectivePath(c); c.restore(); c.save(); c.clip(); screenL(c, sl, 0.12); c.restore();
400
+ const ox = 890, oy = 140, rl = H.layer(W - ox, Hh - oy), rx = rl.getContext('2d'); // rim worked only over the figure's box
401
+ rx.save(); rx.translate(-ox, -oy); figT(rx); rx.fillStyle = '#fff'; detectivePath(rx); rx.fill(); rx.restore();
402
+ rx.save(); rx.globalCompositeOperation = 'destination-out'; rx.translate(-ox - 6, -oy + 3); figT(rx); detectivePath(rx); rx.fill(); rx.restore();
403
+ const rm = H.layer(W >> 1, Hh >> 1), rmx = rm.getContext('2d'); rmx.fillStyle = 'rgba(255,255,255,0.35)'; rmx.fillRect(0, 0, W >> 1, Hh >> 1); rmx.drawImage(sl, 0, 0);
404
+ rx.globalCompositeOperation = 'destination-in'; rx.drawImage(rm, -ox, -oy, W, Hh);
405
+ c.save(); c.globalCompositeOperation = 'screen'; c.drawImage(rl, ox, oy); c.restore();
406
+ c.save(); c.textAlign = 'center'; c.shadowColor = 'rgba(0,0,0,0.9)'; c.shadowBlur = 12; c.fillStyle = gy(242);
407
+ c.font = '400 64px "Limelight"'; c.fillText('More examples', 470, 290);
408
+ // the arrow, drawn at Limelight weight (the font's '↓' glyph is spindly)
409
+ c.fillRect(466.5, 322, 7, 32); c.beginPath(); c.moveTo(452, 348); c.lineTo(488, 348); c.lineTo(470, 374); c.closePath(); c.fill();
410
+ c.font = '400 64px "Limelight"'; c.fillText('better guesses', 470, 450); c.restore();
411
+ },
412
+ };
413
+ // ---------- timeline with period transitions ----------
414
+ const L = (fn, ...a) => { const l = H.layer(W, Hh), c = l.getContext('2d'); fn(c, ...a); return l; };
415
+ let out;
416
+ if (t < 1.95) out = L(scenes.hook);
417
+ else if (t < 2.8) { // iris out on the newspaper, iris in on the case board
418
+ const a = sm(1.95, 2.35, t), b = sm(2.4, 2.8, t);
419
+ out = H.layer(W, Hh); const c = out.getContext('2d'); c.fillStyle = '#000'; c.fillRect(0, 0, W, Hh);
420
+ const src = t < 2.37 ? L(scenes.hook) : L(scenes.data), r = t < 2.37 ? 760 * (1 - E.in(a)) : 760 * E.out(b);
421
+ c.save(); c.beginPath(); c.arc(640, 360, Math.max(0.1, r), 0, 7); c.clip(); c.drawImage(src, 0, 0); c.restore();
422
+ } else if (t < 5.9) out = L(scenes.data);
423
+ else if (t < 6.35) { // hard-edged horizontal wipe
424
+ const w = E.inOut(sm(5.9, 6.35, t)) * (W + 40);
425
+ out = L(scenes.data); const c = out.getContext('2d'); c.save(); c.beginPath(); c.rect(0, 0, w, Hh); c.clip(); c.drawImage(L(scenes.loop, 1), 0, 0); c.restore();
426
+ c.fillStyle = gy(250); c.fillRect(w - 3, 0, 3, Hh);
427
+ } else if (t < 10.35) {
428
+ const z = 1 + 0.45 * E.inOut(sm(8.6, 8.85, t)) * (1 - E.inOut(sm(9.3, 9.5, t)));
429
+ out = L(scenes.loop, z);
430
+ } else if (t < 10.75) { // clock wipe
431
+ const a = sm(10.35, 10.75, t) * Math.PI * 2;
432
+ out = L(scenes.loop, 1); const c = out.getContext('2d'); c.save(); c.beginPath(); c.moveTo(640, 360); c.arc(640, 360, 900, -Math.PI / 2, -Math.PI / 2 + a); c.closePath(); c.clip(); c.drawImage(L(scenes.prog), 0, 0); c.restore();
433
+ } else if (t < 13.3) { out = L(scenes.prog); if (t > 12.3) out.getContext('2d').drawImage(L(scenes.news2), 0, 0); }
434
+ else { // iris in on the closing card
435
+ const r = 900 * E.out(sm(13.3, 13.75, t));
436
+ out = H.layer(W, Hh); const c = out.getContext('2d'); c.fillStyle = '#000'; c.fillRect(0, 0, W, Hh);
437
+ c.save(); c.beginPath(); c.arc(470, 370, Math.max(0.1, r), 0, 7); c.clip(); c.drawImage(L(scenes.end), 0, 0); c.restore();
438
+ }
439
+ // ---------- projected film finish ----------
440
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, Hh);
441
+ ctx.drawImage(out, weaveX, weaveY);
442
+ const soft = H.layer(W, Hh), sfx = soft.getContext('2d'); sfx.filter = 'blur(0.8px)'; sfx.drawImage(ctx.canvas, 0, 0); ctx.drawImage(soft, 0, 0);
443
+ const fimg = ctx.getImageData(0, 0, W, Hh), fd = fimg.data;
444
+ for (let i = 0; i < fd.length; i += 4) {
445
+ let v = Math.min(1, ((fd[i] + fd[i + 1] + fd[i + 2]) / 765) * flick);
446
+ v = v < 0.5 ? 0.5 * Math.pow(2 * v, 1.5) : 1 - 0.5 * Math.pow(2 - 2 * v, 1.3);
447
+ v = H.clamp(v + (Rg() + Rg() + Rg() - 1.5) * 0.15 * (0.5 + 0.8 * Math.sqrt(v * (1 - v) + 0.04)));
448
+ fd[i] = v * 255; fd[i + 1] = v * 252; fd[i + 2] = v * 246;
449
+ }
450
+ ctx.putImageData(fimg, 0, 0);
451
+ const hl = H.layer(W / 4, Hh / 4), hx = hl.getContext('2d'); hx.filter = 'blur(6px) brightness(1.2)'; hx.drawImage(ctx.canvas, 0, 0, W / 4, Hh / 4);
452
+ ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.globalAlpha = 0.25; ctx.drawImage(hl, 0, 0, W, Hh); ctx.restore();
453
+ H.vignette(ctx, W, Hh, 0.7, '0,0,0');
454
+ for (let k = 0; k < 50; k++) { const white = Rg() < 0.6; ctx.fillStyle = white ? `rgba(255,255,250,${Rg.range(0.4, 0.9)})` : `rgba(0,0,0,${Rg.range(0.5, 0.9)})`; const x = Rg() * W, y = Rg() * Hh, r = Rg() < 0.9 ? Rg.range(0.6, 1.8) : Rg.range(2, 4); ctx.beginPath(); ctx.ellipse(x, y, r, r * Rg.range(0.5, 1), Rg() * 3, 0, 7); ctx.fill(); }
455
+ ctx.strokeStyle = 'rgba(255,255,250,0.3)'; ctx.lineWidth = 1.2; ctx.beginPath(); for (let y = 0; y <= Hh; y += 20) ctx.lineTo(300 + Math.sin(t * 6) * 4 + nz.n2(3, y * 0.01) * 4, y); ctx.stroke();
456
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, BAR); ctx.fillRect(0, Hh - BAR, W, BAR);
457
+ }
458
+
459
+ registerStyle({
460
+ id: 'film-noir',
461
+ name: '1940s Film Noir',
462
+ category: 'Light & Imaging',
463
+ description: 'A high-contrast black-and-white still from a forties detective picture: venetian-blind light slicing across a fedora silhouette, a glowing terminal as the only other light, cigarette smoke hanging in the beams, with heavy nitrate grain, gate weave, dust and scratches.',
464
+ motion: 'Blinds sway and smoke curls in a dark office; the terminal warms up with a CRT flash, its glow rim-lights the detective, a prompt types out, and the title card dissolves in word by word over projector flicker, weave and dirt.',
465
+ fonts: ['400 66px "Limelight"', '400 28px "Limelight"', '400 25px "VT323"', 'italic 400 24px "Playfair Display"', '700 40px "Oswald"', '400 54px "UnifrakturMaguntia"', '400 76px "Anton"', '400 52px "Special Elite"', '700 30px "Courier Prime"', '400 14px "Courier Prime"'],
466
+ frameT: 4.6,
467
+ duration: 5,
468
+ readsPixels: true,
469
+ explainerDuration: 15,
470
+ explainerNote: 'Told as a 1940s newsreel / B-picture montage: spinning-newspaper headlines, iris, wipe and clock-wipe transitions, mugshot booking placards as labels, white newsreel arrows and lettering, a rolling mechanical odometer for numbers and a rubber stamp slammed down as the emphasis, all under heavy grain and gate weave.',
471
+ explainer(ctx, W, Hh, t) { noirExplainer(ctx, W, Hh, t); },
472
+ // Newsreel soundtrack: projector clatter + optical-track crackle underneath; paper, typebars, ratchets,
473
+ // rubber stamps and a desk bell on the events; muted-brass stings for the two headlines and the end card.
474
+ // Every time below is copied from noirExplainer() (same numbers, same easing) so cues land on the frame.
475
+ // Voiceover mode (k.vo): beds drop out and everything tagged { minor: true } or inside k.withMinor() plays quieter,
476
+ // so landings, transitions, WRONG/RIGHT, the 97% hit and the resolve stay on top of the narration.
477
+ explainerSound(k) {
478
+ const sm = H.smooth, E = H.ease, D = 15;
479
+ // first time (1 ms scan) at which f(t) reaches or passes v
480
+ const when = (f, v, a, b) => { for (let t = a; t <= b; t += 0.001) if (f(t) >= v) return t; return b; };
481
+ // muted newsreel brass: detuned filtered saws with a soft bite
482
+ const brass = (t, notes, dur, g) => notes.forEach((n, i) => {
483
+ for (const det of [-7, 7]) k.tone(t, { f: n, type: 'sawtooth', detune: det, dur, gain: 0.03 * g, a: 0.03, hold: dur * 0.55, filter: 'lowpass', f2: [[0, 700], [0.06, 2200], [dur, 900]], q: 0.7, pan: (i - (notes.length - 1) / 2) * 0.3, verb: 0.35 });
484
+ });
485
+ // spinning newspaper: a paper flap at every half turn (rotation = (1-p)*5π), louder as it grows toward camera
486
+ const spin = (t0, t1) => {
487
+ const p = (t) => E.out(sm(t0, t1, t));
488
+ for (let h = 1; h < 5; h++) { const th = when((t) => p(t) * 5, h, t0, t1), g = 0.35 + 0.18 * h;
489
+ k.noise(th - 0.03, { dur: 0.09, color: 'pink', filter: 'bandpass', f: [[0, 900], [0.05, 2600], [0.09, 1400]], q: 0.8, gain: 0.22 * g, a: 0.03, minor: true }); }
490
+ k.play('whoosh', t0, { dur: (t1 - t0) * 0.85, top: 1800, gain: 0.5, minor: true });
491
+ };
492
+ // iris: a soft mechanical leaf-shutter sweep
493
+ const iris = (t, dur, open) => k.noise(t, { dur, color: 'pink', filter: 'bandpass', f: open ? [[0, 500], [dur, 2400]] : [[0, 2400], [dur, 500]], q: 1.4, gain: 0.16, a: dur * 0.6 });
494
+ // throttled mechanical odometer: one click per value change, never closer than `gap`
495
+ const odo = (val, a, b, gap, f, avoid = []) => { let last = -1, prev = val(a);
496
+ for (let t = a; t <= b; t += 0.002) { const v = val(t); if (v !== prev && t - last >= gap && !avoid.some((x) => t > x - 0.03 && t < x + 0.12)) { k.play('tock', t, { f, gain: 0.32, pan: 0.4, minor: true }); last = t; } prev = v; } };
497
+
498
+ // ---- beds: projector gate clatter, optical soundtrack crackle, a little room
499
+ k.bed('projector', 0, D, { gain: 1.1 });
500
+ k.bed('vinyl', 0, D, { gain: 0.7 });
501
+ k.bed('room', 0, D, { gain: 0.6 });
502
+
503
+ // ---- 0.15–1.15 newspaper spins in, lands; newsreel fanfare
504
+ spin(0.15, 1.15);
505
+ k.play('slap', 1.15, { gain: 0.7 });
506
+ k.withMinor(() => { brass(1.2, ['G3'], 0.12, 1); brass(1.34, ['G3'], 0.12, 1); brass(1.48, ['C4', 'E4', 'G4'], 0.9, 1.1); });
507
+
508
+ // ---- iris out 1.95–2.35, iris in 2.4–2.8
509
+ iris(1.95, 0.4, false); iris(2.4, 0.4, true);
510
+
511
+ // ---- case board: mugshots slap onto the cork at 2.75 + k*0.28 (scale lands 0.15 s later)
512
+ for (let c = 0; c < 9; c++) { const t0 = 2.75 + c * 0.28; k.play('slap', t0 + 0.12, { gain: 0.32, pan: -0.6 + (c % 3) * 0.2, minor: true }); }
513
+ // examples odometer 10^(6p), p = sm(2.7, 5.4): clicks speed up to ~11/s, then it locks at 1,000,000
514
+ odo((t) => Math.round(Math.pow(10, 6 * sm(2.7, 5.4, t))), 2.72, 5.38, 0.09, 1500, Array.from({ length: 9 }, (_, c) => 2.87 + c * 0.28));
515
+ k.play('tock', 5.4, { f: 520, gain: 0.9, pan: 0.4 });
516
+
517
+ // ---- 5.9–6.35 hard horizontal wipe
518
+ k.play('swish', 5.92, { dur: 0.4, gain: 0.6 });
519
+
520
+ // ---- interrogation table
521
+ k.bed('hum', 6.0, 10.35, { gain: 0.5 }); // lamp/machine hum under the scene
522
+ k.play('paper', 6.3, { dur: 0.3, gain: 1.0, pan: -0.5, minor: true }); // mugshot slides in
523
+ k.play('tock', 6.7, { f: 700, gain: 0.35, pan: -0.3, minor: true }); // GUESS arrow + label
524
+ k.play('tape', 6.95, { dur: 0.4, gain: 0.6, pan: 0.35, minor: true }); // answer tape feeds out
525
+ k.play('typing', 6.98, { n: 4, rate: 0.09, gain: 0.7, pan: 0.35, minor: true }); // "DOG?"
526
+ k.play('stamp', 7.72, { gain: 0.85, pan: 0.5 }); // WRONG stamp hits (lands 7.6+0.12)
527
+ k.play('buzz', 7.84, { f: 98, dur: 0.28, gain: 0.85, pan: 0.5, minor: true }); // quiz-show buzzer for the wrong guess
528
+ k.play('stroke', 8.15, { material: 'pencil', dur: 0.4, gain: 1.3, minor: true }); // ADJUST arrow draws
529
+ k.play('swish', 8.6, { dur: 0.25, gain: 0.45, minor: true }); // punch in
530
+ // dials ratchet in four steps: step = floor(sm(8.75, 9.35)*4)/4
531
+ for (let s = 1; s <= 4; s++) { const ts = when((t) => sm(8.75, 9.35, t) * 4, s, 8.75, 9.36); k.play('tock', ts, { f: 420 + s * 40, gain: 0.7, minor: true }); k.play('click', ts + 0.012, { gain: 0.5, minor: true }); }
532
+ k.play('swish', 9.3, { dur: 0.2, gain: 0.35, minor: true }); // punch out
533
+ k.play('typing', 9.5, { n: 4, rate: 0.045, gain: 0.7, pan: 0.35, minor: true }); // tape now reads "CAT."
534
+ k.play('stamp', 9.87, { gain: 0.85, pan: 0.5 }); // RIGHT stamp hits (9.75+0.12)
535
+ k.play('bell', 9.95, { f: 'A6', gain: 0.9, pan: 0.5 }); // desk bell: correct
536
+
537
+ // ---- 10.35–10.75 clock wipe: a quick rotary ratchet sweep
538
+ for (let i = 0; i < 5; i++) k.play('tock', 10.37 + i * 0.08, { f: 1100 + i * 120, gain: 0.45, minor: i > 0 });
539
+ k.play('whoosh', 10.35, { dur: 0.4, top: 1600, gain: 0.5 });
540
+
541
+ // ---- 10.75–12.2 accuracy line draws while the odometer rolls 61 -> 97, slowing as it settles
542
+ k.play('stroke', 10.78, { material: 'pencil', dur: 1.4, gain: 0.3, pan: -0.4, minor: true });
543
+ odo((t) => Math.round(61 + 36 * E.out(sm(10.75, 12.2, t))), 10.76, 12.15, 0.085, 1600);
544
+ k.play('tock', 12.2, { f: 560, gain: 0.7, pan: 0.4 });
545
+
546
+ // ---- 12.3–13.0 "ACCURACY HITS 97%!" spins in: the big hit
547
+ spin(12.3, 13.0);
548
+ k.play('impact', 13.0, { gain: 1.3 }); k.play('press', 13.0, { gain: 0.8 }); k.play('slap', 13.0, { gain: 0.7, minor: true });
549
+ brass(13.04, ['C4', 'E4', 'G4', 'C5'], 0.32, 1.1);
550
+
551
+ // ---- 13.3–13.75 iris in on the closing card; resolving brass chord under the takeaway
552
+ iris(13.3, 0.45, true);
553
+ k.withMinor(() => brass(13.55, ['F3', 'C4', 'A4'], 0.55, 0.7)); // lead-in chord: quieter under voiceover
554
+ brass(14.05, ['C3', 'G3', 'E4', 'C5'], 0.92, 0.8); // the resolve, kept under voiceover too
555
+ },
556
+ draw(ctx, W, Hh, t) {
557
+ const R = H.rng(1946), nz = H.makeNoise(46);
558
+ const weaveX = nz.n2(t * 3.1, 0.3) * 2.2, weaveY = nz.n2(0.7, t * 2.7) * 2.8;
559
+ const flick = 1 + nz.n2(t * 9, 4.4) * 0.05;
560
+ const fr24 = Math.floor(t * 24), Rg = H.rng(7000 + fr24); // per-frame grain & dirt (24 fps film)
561
+ const termOn = t < 0.9 ? 0 : H.smooth(0.9, 1.35, t) * (t < 1.3 ? (Math.floor(t * 30) % 3 ? 1 : 0.6) : 1);
562
+ const sway = Math.sin(t * 0.9) * 4 + Math.sin(t * 2.3) * 1.2;
563
+
564
+ // ---------- albedo (greyscale) ----------
565
+ const A = H.layer(W, Hh), a = A.getContext('2d');
566
+ // wall: painted plaster with wainscot
567
+ a.fillStyle = 'rgb(150,150,150)'; a.fillRect(0, 0, W, Hh);
568
+ a.fillStyle = 'rgb(110,110,110)'; a.fillRect(0, 440, W, 30);
569
+ a.fillStyle = 'rgb(120,120,120)'; a.fillRect(0, 470, W, 250);
570
+ for (let x = 30; x < W; x += 140) { a.fillStyle = 'rgb(90,90,90)'; a.fillRect(x, 470, 3, 250); }
571
+ // framed picture on wall (left)
572
+ a.fillStyle = 'rgb(60,60,60)'; a.fillRect(70, 150, 150, 190);
573
+ a.fillStyle = 'rgb(170,170,170)'; a.fillRect(84, 164, 122, 162);
574
+ a.fillStyle = 'rgb(120,120,120)'; a.beginPath(); a.moveTo(84, 326); a.lineTo(130, 250); a.lineTo(160, 290); a.lineTo(206, 220); a.lineTo(206, 326); a.fill();
575
+ // desk
576
+ a.fillStyle = 'rgb(70,70,70)'; a.beginPath(); a.moveTo(0, 492); a.lineTo(690, 492); a.lineTo(712, 512); a.lineTo(0, 512); a.fill();
577
+ a.fillStyle = 'rgb(40,40,40)'; a.fillRect(0, 512, 712, 210);
578
+ a.fillStyle = 'rgb(55,55,55)'; a.fillRect(40, 540, 300, 6); a.fillRect(380, 540, 300, 6);
579
+ // terminal casing (cream metal)
580
+ a.fillStyle = 'rgb(175,175,170)';
581
+ a.beginPath(); a.roundRect(318, 282, 238, 196, 14); a.fill();
582
+ a.fillStyle = 'rgb(140,140,138)'; a.beginPath(); a.moveTo(340, 478); a.lineTo(534, 478); a.lineTo(548, 494); a.lineTo(326, 494); a.fill();
583
+ a.fillStyle = 'rgb(40,40,40)'; a.beginPath(); a.roundRect(340, 300, 194, 146, 20); a.fill();
584
+ for (let k = 0; k < 6; k++) { a.fillStyle = 'rgb(100,100,100)'; a.fillRect(470 + k * 9, 456, 5, 12); }
585
+ // keyboard
586
+ a.fillStyle = 'rgb(130,130,128)'; a.beginPath(); a.moveTo(352, 498); a.lineTo(560, 498); a.lineTo(574, 512); a.lineTo(338, 512); a.fill();
587
+ // rotary telephone (left)
588
+ a.fillStyle = 'rgb(22,22,22)';
589
+ a.beginPath(); a.ellipse(128, 478, 62, 22, 0, Math.PI, 0); a.lineTo(190, 494); a.lineTo(66, 494); a.fill();
590
+ a.beginPath(); a.roundRect(80, 436, 96, 18, 9); a.fill();
591
+ a.beginPath(); a.ellipse(84, 444, 16, 12, 0, 0, 7); a.ellipse(172, 444, 16, 12, 0, 0, 7); a.fill();
592
+ a.fillStyle = 'rgb(80,80,80)'; a.beginPath(); a.arc(128, 478, 12, 0, 7); a.fill();
593
+ // whisky glass
594
+ a.fillStyle = 'rgb(110,110,110)'; a.beginPath(); a.moveTo(226, 452); a.lineTo(264, 452); a.lineTo(260, 494); a.lineTo(230, 494); a.fill();
595
+ a.fillStyle = 'rgb(70,70,70)'; a.fillRect(229, 472, 33, 21);
596
+ // ashtray
597
+ a.fillStyle = 'rgb(60,60,60)'; a.beginPath(); a.ellipse(620, 490, 34, 8, 0, 0, 7); a.fill();
598
+ // detective (dark wool coat)
599
+ const Fm = H.layer(W, Hh), fm = Fm.getContext('2d');
600
+ fm.fillStyle = '#fff'; detectivePath(fm); fm.fill(); armPath(fm); fm.fill();
601
+ a.fillStyle = 'rgb(46,46,46)'; detectivePath(a); a.fill();
602
+ a.fillStyle = 'rgb(30,30,30)'; a.beginPath(); a.moveTo(730, 226); a.bezierCurveTo(728, 220, 740, 214, 772, 212); a.bezierCurveTo(776, 190, 786, 168, 812, 160); a.bezierCurveTo(828, 156, 848, 160, 862, 158); a.bezierCurveTo(880, 164, 888, 190, 890, 206); a.bezierCurveTo(912, 208, 934, 214, 938, 222); a.bezierCurveTo(900, 232, 780, 230, 730, 226); a.fill();
603
+ a.fillStyle = 'rgb(140,140,140)'; a.fillRect(776, 204, 112, 8); // hat band catches light
604
+ a.fillStyle = 'rgb(150,150,150)'; a.beginPath(); a.moveTo(772, 232); a.lineTo(764, 258); a.lineTo(758, 264); a.lineTo(766, 276); a.lineTo(764, 284); a.lineTo(770, 292); a.lineTo(772, 300); a.lineTo(780, 304); a.lineTo(800, 300); a.lineTo(806, 236); a.fill(); // face skin
605
+ a.fillStyle = 'rgb(56,56,56)'; armPath(a); a.fill();
606
+ a.fillStyle = 'rgb(150,150,150)'; a.beginPath(); a.ellipse(722, 440, 12, 9, 0.4, 0, 7); a.fill(); // hand
607
+ a.fillStyle = 'rgb(230,230,230)'; a.save(); a.translate(712, 434); a.rotate(-0.3); a.fillRect(-26, -2, 26, 4); a.restore(); // cigarette
608
+ // belt line
609
+ a.strokeStyle = 'rgb(30,30,30)'; a.lineWidth = 6; a.beginPath(); a.moveTo(708, 590); a.quadraticCurveTo(870, 610, 1046, 596); a.stroke();
610
+ // lapel crease
611
+ a.strokeStyle = 'rgb(25,25,25)'; a.lineWidth = 3; a.beginPath(); a.moveTo(778, 352); a.bezierCurveTo(800, 400, 790, 460, 760, 520); a.stroke();
612
+
613
+ // ---------- light ----------
614
+ const Ad = a.getImageData(0, 0, W, Hh).data, Fd = fm.getImageData(0, 0, W, Hh).data;
615
+ const fig = (x, y) => { x = x | 0; y = y | 0; if (x < 0 || y < 0 || x >= W || y >= Hh) return 0; return Fd[(y * W + x) * 4 + 3] / 255; };
616
+ const scx = 437, scy = 373; // terminal screen centre
617
+ const blindPeriod = 34, slope = 0.5;
618
+ const blinds = (x, y, shift) => {
619
+ // window light patch: parallelogram projected from upper right
620
+ const left = 470 + (y - 60) * 0.55, right = 1500;
621
+ const inPatch = H.smooth(left - 20, left + 20, x) * H.smooth(right + 20, right - 20, x) * H.smooth(20, 70, y) * (1 - H.smooth(690, 720, y));
622
+ const per = blindPeriod + (x - 600) * 0.012 + (y - 300) * 0.01;
623
+ const p = (((y + x * slope + shift + sway) / per) % 1 + 1) % 1;
624
+ const pen = 0.04 + (1 - H.smooth(300, 1300, x)) * 0.06; // penumbra widens farther from the blinds
625
+ const slat = H.smooth(0.1 - pen, 0.1 + pen, p) * (1 - H.smooth(0.52 - pen, 0.52 + pen, p));
626
+ const fall = 0.45 + 0.55 * H.smooth(350, 1250, x + (300 - y) * 0.6);
627
+ // window mullion shadow
628
+ const mull = 1 - (1 - H.smooth(5, 9, Math.abs(x - (980 + (y - 60) * 0.55)))) * 0.95;
629
+ return inPatch * slat * mull * fall;
630
+ };
631
+ const img = ctx.createImageData(W, Hh), d = img.data;
632
+ for (let y = 0; y < Hh; y++) for (let x = 0; x < W; x++) {
633
+ const i = (y * W + x) * 4;
634
+ const alb = Ad[i] / 255, onFig = Fd[i + 3] / 255;
635
+ // detective's shadow on the wall (light from upper right)
636
+ const sh = onFig > 0.5 ? 0 : fig(x + 190, y - 70) * 0.92;
637
+ let L = 0.035;
638
+ const onDesk = y > 492 && x < 712 && onFig < 0.5;
639
+ const wallStripes = blinds(x, y, 0) * (1 - sh) * (onDesk ? 0.25 : 1);
640
+ const figStripes = blinds(x + 14, y, 21) * (H.smooth(760, 900, x) * 0.75 + 0.25) * 1.3;
641
+ L += onFig > 0.5 ? figStripes * 0.9 : wallStripes;
642
+ // terminal glow (soft cool light)
643
+ const dx = x - scx, dy = y - scy, dd = dx * dx + dy * dy;
644
+ let glow = 0.55 / (1 + dd / 26000);
645
+ if (onFig > 0.5) {
646
+ // rim on the edges facing the screen
647
+ const rim = (1 - fig(x - 7, y - 2)) * 0.7 + (1 - fig(x - 3, y - 1)) * 0.6;
648
+ glow = glow * 0.15 + rim * 2.2 / (1 + dd / 120000);
649
+ }
650
+ if (y > 492 && y < 512 && x < 700) glow *= 2.4; // desk top catches it // desk top catches it
651
+ L += glow * termOn;
652
+ let v = alb * L * 2.1;
653
+ d[i] = d[i + 1] = d[i + 2] = H.clamp(v) * 255; d[i + 3] = 255;
654
+ }
655
+ ctx.save(); ctx.translate(weaveX, weaveY);
656
+ const Sc = H.layer(W, Hh); Sc.getContext('2d').putImageData(img, 0, 0);
657
+ ctx.fillStyle = '#000'; ctx.fillRect(-10, -10, W + 20, Hh + 20);
658
+ ctx.drawImage(Sc, 0, 0);
659
+
660
+ // ---------- emissive: terminal screen ----------
661
+ ctx.save();
662
+ ctx.beginPath(); ctx.roundRect(350, 310, 174, 126, 16); ctx.clip();
663
+ if (termOn > 0) {
664
+ // CRT warm-up: a bright line opens vertically into the raster
665
+ const open = H.ease.out(H.smooth(0.9, 1.2, t)), hh = 126 * open;
666
+ ctx.beginPath(); ctx.rect(350, scy - hh / 2 - 1, 174, hh + 2); ctx.clip();
667
+ const sg = ctx.createRadialGradient(scx, scy, 10, scx, scy, 120);
668
+ const b0 = 215 + (1 - open) * 40;
669
+ sg.addColorStop(0, `rgb(${b0},${b0},${b0})`); sg.addColorStop(1, 'rgb(120,120,120)');
670
+ ctx.fillStyle = sg; ctx.fillRect(340, 300, 200, 150);
671
+ ctx.fillStyle = 'rgba(255,255,255,0.95)'; ctx.font = '400 25px "VT323"';
672
+ if (t > 1.35) ctx.fillText('READY.', 364, 346);
673
+ const line = '> WHO SENT YOU?', nch = Math.max(0, Math.min(line.length, Math.floor((t - 1.7) * 13)));
674
+ if (nch > 0) ctx.fillText(line.slice(0, nch), 364, 372);
675
+ const cy0 = nch > 0 && nch < line.length ? 354 : 386, cx0 = nch > 0 && nch < line.length ? 364 + ctx.measureText(line.slice(0, nch)).width + 2 : 364;
676
+ if (t > 1.35 && (nch < line.length || Math.floor(t * 2) % 2 === 0)) ctx.fillRect(cx0, cy0, 13, 20);
677
+ } else { ctx.fillStyle = 'rgb(28,28,28)'; ctx.fillRect(340, 300, 200, 150); }
678
+ ctx.fillStyle = 'rgba(0,0,0,0.12)'; for (let y = 310; y < 436; y += 3) ctx.fillRect(350, y, 174, 1);
679
+ ctx.restore();
680
+ // specular glints on props facing the screen
681
+ ctx.globalAlpha = termOn;
682
+ ctx.strokeStyle = 'rgba(255,255,255,0.75)'; ctx.lineWidth = 2;
683
+ ctx.beginPath(); ctx.ellipse(128, 478, 58, 19, 0, Math.PI * 1.15, Math.PI * 1.75); ctx.stroke();
684
+ ctx.beginPath(); ctx.moveTo(176, 437); ctx.lineTo(168, 437); ctx.stroke();
685
+ ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(261, 455); ctx.lineTo(258, 492); ctx.stroke();
686
+ ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fillRect(230, 471, 31, 2);
687
+ ctx.lineWidth = 1.5; ctx.beginPath(); ctx.ellipse(620, 488, 32, 6, 0, Math.PI * 1.1, Math.PI * 1.6); ctx.stroke();
688
+ ctx.globalAlpha = 1;
689
+ // cigarette ember
690
+ const drag = Math.exp(-Math.pow((t - 3.9) / 0.25, 2));
691
+ ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(689, 442, 2.6 + drag * 1.6, 0, 7); ctx.fill();
692
+ if (drag > 0.05) { const eg = ctx.createRadialGradient(689, 442, 0, 689, 442, 26); eg.addColorStop(0, `rgba(255,255,255,${0.5 * drag})`); eg.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = eg; ctx.fillRect(660, 414, 58, 58); }
693
+
694
+ // ---------- smoke (lit by the blind beams) ----------
695
+ const Sm = H.layer(W, Hh), smx = Sm.getContext('2d');
696
+ smx.lineCap = 'round';
697
+ for (let s = 0; s < 5; s++) {
698
+ smx.beginPath();
699
+ let px = 689, py = 440;
700
+ smx.moveTo(px, py);
701
+ for (let k = 0; k < 70; k++) {
702
+ const u = k / 70;
703
+ px += nz.n2(u * 3 + s * 7, t * 0.3 + s) * (2 + u * 7) + 0.6 + u * 1.2;
704
+ py -= 4.2 - u * 1.3;
705
+ smx.lineTo(px, py);
706
+ }
707
+ smx.strokeStyle = `rgba(255,255,255,${0.32 - s * 0.04})`; smx.lineWidth = 2 + s * 4;
708
+ smx.stroke();
709
+ }
710
+ // hanging haze layer
711
+ for (let k = 0; k < 24; k++) {
712
+ const hx = 560 + R() * 520, hy = 120 + R() * 200, r = 40 + R() * 90;
713
+ const gg = smx.createRadialGradient(hx, hy, 0, hx, hy, r);
714
+ gg.addColorStop(0, 'rgba(255,255,255,0.05)'); gg.addColorStop(1, 'rgba(255,255,255,0)');
715
+ smx.fillStyle = gg; smx.fillRect(hx - r, hy - r, r * 2, r * 2);
716
+ }
717
+ const SmB = H.layer(W, Hh), sbx = SmB.getContext('2d');
718
+ sbx.filter = 'blur(5px)'; sbx.drawImage(Sm, 0, 0);
719
+ // modulate by beams: volumetric light shafts through smoke
720
+ const beam = H.layer(W, Hh), bmx = beam.getContext('2d');
721
+ const bimg = bmx.createImageData(W, Hh), bd = bimg.data;
722
+ for (let y = 0; y < Hh; y += 1) for (let x = 400; x < W; x += 1) {
723
+ const v = blinds(x, y, 30) * 255; const i = (y * W + x) * 4; bd[i] = bd[i + 1] = bd[i + 2] = 255; bd[i + 3] = 90 + v * 0.65;
724
+ }
725
+ bmx.putImageData(bimg, 0, 0);
726
+ sbx.globalCompositeOperation = 'destination-in'; sbx.drawImage(beam, 0, 0);
727
+ ctx.globalCompositeOperation = 'screen'; ctx.drawImage(SmB, 0, 0); ctx.globalAlpha = 0.85; ctx.filter = 'blur(2.5px)'; ctx.drawImage(Sm, 0, 0); ctx.filter = 'none'; ctx.globalAlpha = 1;
728
+ // faint beams in air
729
+ ctx.globalAlpha = 0.07; ctx.filter = 'blur(10px)'; ctx.drawImage(beam, 0, 0); ctx.filter = 'none'; ctx.globalAlpha = 1;
730
+ ctx.globalCompositeOperation = 'source-over';
731
+ ctx.restore();
732
+
733
+ // ---------- title card ----------
734
+ ctx.save();
735
+ ctx.textAlign = 'left';
736
+ ctx.shadowColor = 'rgba(0,0,0,0.9)'; ctx.shadowBlur = 12; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3;
737
+ ctx.fillStyle = '#f2f2f2';
738
+ const ti = (k) => H.smooth(2.5 + k * 0.22, 2.95 + k * 0.22, t);
739
+ ctx.globalAlpha = ti(0); ctx.font = '400 26px "Limelight"'; ctx.fillText('THE', 70, 112);
740
+ ctx.globalAlpha = ti(1); ctx.font = '400 60px "Limelight"'; ctx.fillText('GHOST', 66, 172);
741
+ ctx.globalAlpha = ti(2); ctx.font = 'italic 400 24px "Playfair Display"'; ctx.fillText('in the', 74, 204);
742
+ ctx.globalAlpha = ti(3); ctx.font = '400 60px "Limelight"'; ctx.fillText('MACHINE', 66, 262);
743
+ ctx.globalAlpha = 1;
744
+ ctx.restore();
745
+
746
+ // ---------- film finish: contrast, halation, grain, flicker ----------
747
+ const soft = H.layer(W, Hh), sfx = soft.getContext('2d');
748
+ sfx.filter = 'blur(0.8px)'; sfx.drawImage(ctx.canvas, 0, 0); ctx.drawImage(soft, 0, 0);
749
+ const fimg = ctx.getImageData(0, 0, W, Hh), fd = fimg.data;
750
+ for (let y = 0; y < Hh; y++) for (let x = 0; x < W; x++) {
751
+ const i = (y * W + x) * 4;
752
+ let v = fd[i] / 255;
753
+ v = Math.min(1, v * flick);
754
+ v = v < 0.5 ? 0.5 * Math.pow(2 * v, 1.6) : 1 - 0.5 * Math.pow(2 - 2 * v, 1.3); // S-curve
755
+ const gn = (Rg() + Rg() + Rg() - 1.5) * 0.16 * (0.5 + 0.8 * Math.sqrt(v * (1 - v) + 0.04));
756
+ v = H.clamp(v + gn);
757
+ // faint warm silver tone
758
+ fd[i] = v * 255; fd[i + 1] = v * 252; fd[i + 2] = v * 246;
759
+ }
760
+ ctx.putImageData(fimg, 0, 0);
761
+ // halation around highlights
762
+ const hl = H.layer(W / 4, Hh / 4), hx = hl.getContext('2d');
763
+ hx.filter = 'blur(6px) brightness(1.2)'; hx.drawImage(ctx.canvas, 0, 0, W / 4, Hh / 4);
764
+ ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.globalAlpha = 0.28; ctx.drawImage(hl, 0, 0, W, Hh); ctx.restore();
765
+ // projector vignette
766
+ H.vignette(ctx, W, Hh, 0.75, '0,0,0');
767
+ // dust (white = dirt on negative, black = on print), hairs, scratches
768
+ for (let k = 0; k < 70; k++) {
769
+ const white = Rg() < 0.6;
770
+ ctx.fillStyle = white ? `rgba(255,255,250,${Rg.range(0.4, 0.9)})` : `rgba(0,0,0,${Rg.range(0.5, 0.9)})`;
771
+ const x = Rg() * W, y = Rg() * Hh, r = Rg() < 0.9 ? Rg.range(0.6, 1.8) : Rg.range(2, 4);
772
+ ctx.beginPath(); ctx.ellipse(x, y, r, r * Rg.range(0.5, 1), Rg() * 3, 0, 7); ctx.fill();
773
+ }
774
+ for (let k = 0; k < 3; k++) {
775
+ ctx.strokeStyle = `rgba(250,250,245,${Rg.range(0.3, 0.6)})`; ctx.lineWidth = Rg.range(0.6, 1.2);
776
+ const x = Rg() * W, y = Rg() * Hh; ctx.beginPath(); ctx.moveTo(x, y);
777
+ ctx.bezierCurveTo(x + Rg.range(-20, 20), y + Rg.range(-20, 20), x + Rg.range(-30, 30), y + Rg.range(-10, 30), x + Rg.range(-30, 30), y + Rg.range(-30, 30)); ctx.stroke();
778
+ }
779
+ const scr = [[312 + Math.sin(t * 7) * 3, 0.35], [1043 + Math.sin(t * 5) * 2, 0.22]];
780
+ scr.forEach(([sx0, al]) => {
781
+ ctx.strokeStyle = `rgba(255,255,250,${al})`; ctx.lineWidth = 1.2; ctx.beginPath();
782
+ for (let y = 0; y <= Hh; y += 20) ctx.lineTo(sx0 + nz.n2(sx0, y * 0.01) * 4, y);
783
+ ctx.stroke();
784
+ });
785
+ ctx.strokeStyle = 'rgba(0,0,0,0.4)'; ctx.lineWidth = 1; ctx.beginPath();
786
+ for (let y = 0; y <= Hh; y += 20) ctx.lineTo(640 + nz.n2(9, y * 0.01) * 5, y); ctx.stroke();
787
+
788
+ // ---------- letterbox ----------
789
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, BAR); ctx.fillRect(0, Hh - BAR, W, BAR);
790
+ },
791
+ });
792
+ }