@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,922 @@
1
+ {
2
+ // ---------- neon sign on a night brick wall ----------
3
+ const COL = {
4
+ pink: { core: [255, 236, 246], tube: [255, 70, 165], glow: [255, 25, 125], dark: [52, 26, 40] },
5
+ cyan: { core: [228, 255, 255], tube: [70, 228, 255], glow: [0, 150, 255], dark: [22, 38, 48] },
6
+ warm: { core: [255, 250, 228], tube: [255, 196, 80], glow: [255, 120, 15], dark: [48, 38, 26] },
7
+ };
8
+ const WALL_BASE = 596;
9
+
10
+ function mask(W, Hh, fn) {
11
+ const c = H.layer(W, Hh), x = c.getContext('2d');
12
+ x.fillStyle = '#fff'; x.strokeStyle = '#fff'; x.lineCap = 'round'; x.lineJoin = 'round';
13
+ fn(x); return c;
14
+ }
15
+ function colorize(m, rgb, a = 1) {
16
+ const c = H.layer(m.width, m.height), x = c.getContext('2d');
17
+ x.drawImage(m, 0, 0); x.globalCompositeOperation = 'source-in';
18
+ x.fillStyle = H.rgba(rgb, a); x.fillRect(0, 0, c.width, c.height); return c;
19
+ }
20
+ function erode(m, r) {
21
+ const c = H.layer(m.width, m.height), x = c.getContext('2d');
22
+ x.drawImage(m, 0, 0); x.globalCompositeOperation = 'destination-in';
23
+ for (let a = 0; a < 8; a++) x.drawImage(m, Math.round(Math.cos(a * Math.PI / 4) * r), Math.round(Math.sin(a * Math.PI / 4) * r));
24
+ return c;
25
+ }
26
+ // cheap big blur: downsample, blur, return small canvas (caller scales it back up)
27
+ function blurSmall(src, px, s = 0.25) {
28
+ const w = Math.ceil(src.width * s), h = Math.ceil(src.height * s), c = H.layer(w, h), x = c.getContext('2d');
29
+ x.filter = `blur(${Math.max(0.5, px * s)}px)`; x.drawImage(src, 0, 0, w, h); return c;
30
+ }
31
+ function addBlur(g, src, px, a, W, Hh) {
32
+ const s = px > 24 ? 0.125 : px > 8 ? 0.25 : 0.5;
33
+ g.globalAlpha = a; g.drawImage(blurSmall(src, px, s), 0, 0, W, Hh);
34
+ }
35
+
36
+ // ================= EXPLAINER: camera pans along one long wall of neon signs =================
37
+ const NX = { pink: [255, 70, 165], cyan: [70, 228, 255], warm: [255, 190, 70], red: [255, 48, 36], green: [90, 255, 120], violet: [175, 110, 255] };
38
+ let brickTile = null;
39
+ function getBrickTile() {
40
+ if (brickTile) return brickTile;
41
+ const TW = 92 * 14, TH = 600, rowH = 30, brW = 92, mort = 5;
42
+ const c = H.layer(TW, TH), x = c.getContext('2d'), img = x.createImageData(TW, TH), d = img.data;
43
+ const R = H.rng(4401), nz = H.makeNoise(4402);
44
+ const cols = [[230, 106, 76], [200, 90, 66], [245, 130, 92], [180, 96, 78], [220, 120, 90], [140, 76, 62]];
45
+ const bh = (a, b) => { const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5453; return s - Math.floor(s); };
46
+ for (let y = 0; y < TH; y++) {
47
+ const row = Math.floor(y / rowH), ly = y - row * rowH, off = (row % 2) * (brW / 2);
48
+ for (let xx = 0; xx < TW; xx++) {
49
+ const col = Math.floor((xx + off) / brW) % 14, lxp = (xx + off) % brW;
50
+ const h1 = bh(row, col), h2 = bh(col + 9, row + 3), g = R() - 0.5;
51
+ const ang = xx / TW * Math.PI * 2, st = nz.n3(Math.cos(ang) * 3, Math.sin(ang) * 3, y * 0.012) * 0.3;
52
+ const ej = (bh(xx >> 2, row) - 0.5) * 2.2;
53
+ let r, gg, b;
54
+ if (ly < mort + ej * 0.5 || lxp < mort + ej * 0.6) { const m = 0.5 + g * 0.3; r = 190 * m; gg = 182 * m; b = 172 * m; }
55
+ else {
56
+ const c0 = cols[Math.floor(h1 * cols.length)];
57
+ const k = (0.85 + h2 * 0.3 + st) * (1 + g * 0.3) * (bh(xx * 0.37 | 0, y * 0.41 | 0) > 0.985 ? 0.55 : 1) * (ly - mort < 3 ? 1.15 : rowH - ly < 3 ? 0.75 : 1);
58
+ r = c0[0] * k; gg = c0[1] * k; b = c0[2] * k;
59
+ }
60
+ const i = (y * TW + xx) * 4; d[i] = r; d[i + 1] = gg; d[i + 2] = b; d[i + 3] = 255;
61
+ }
62
+ }
63
+ x.putImageData(img, 0, 0);
64
+ return (brickTile = c);
65
+ }
66
+ // brick-edge masks matching getBrickTile(): top-edge and bottom-edge alpha, so the sign light can catch brick arrises
67
+ let edgeTiles = null;
68
+ function getEdgeTiles() {
69
+ if (edgeTiles) return edgeTiles;
70
+ const TW = 92 * 14, TH = 600, rowH = 30, brW = 92, mort = 5;
71
+ const bh = (a, b) => { const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5453; return s - Math.floor(s); };
72
+ const mk = () => { const c = H.layer(TW, TH), x = c.getContext('2d'); return [c, x, x.createImageData(TW, TH)]; };
73
+ const [tc, tcx, ti] = mk(), [bc, bcx, bi] = mk(), td = ti.data, bd = bi.data;
74
+ for (let y = 0; y < TH; y++) {
75
+ const row = Math.floor(y / rowH), ly = y - row * rowH, off = (row % 2) * (brW / 2);
76
+ for (let xx = 0; xx < TW; xx++) {
77
+ const lxp = (xx + off) % brW, ej = (bh(xx >> 2, row) - 0.5) * 2.2;
78
+ if (ly < mort + ej * 0.5 || lxp < mort + ej * 0.6) continue;
79
+ const top = ly - (mort + ej * 0.5), bot = rowH - ly, i = (y * TW + xx) * 4;
80
+ if (top < 3.5) { td[i] = td[i + 1] = td[i + 2] = 255; td[i + 3] = 255 * (1 - top / 3.5) * (lxp - mort < 2 ? 0.5 : 1); }
81
+ if (bot < 3) { bd[i] = bd[i + 1] = bd[i + 2] = 255; bd[i + 3] = 255 * (1 - bot / 3); }
82
+ }
83
+ }
84
+ tcx.putImageData(ti, 0, 0); bcx.putImageData(bi, 0, 0);
85
+ tcx.globalAlpha = 0.55; tcx.drawImage(bc, 0, 0);
86
+ return (edgeTiles = { both: tc });
87
+ }
88
+ // wet sidewalk tile (concrete, aggregate speckle) + puddle alpha, periodic in x so it pans with the wall
89
+ let paveTile = null;
90
+ function getPaveTile(W) {
91
+ if (paveTile && paveTile.w === W) return paveTile;
92
+ const PW = W * 2, PH = 120, nz = H.makeNoise(4410), R = H.rng(4411);
93
+ const c = H.layer(PW, PH), x = c.getContext('2d'), img = x.createImageData(PW, PH), d = img.data;
94
+ const p = H.layer(PW, PH), px = p.getContext('2d'), pimg = px.createImageData(PW, PH), pd = pimg.data;
95
+ for (let y = 0; y < PH; y++) for (let xx = 0; xx < PW; xx++) {
96
+ const a = xx / PW * Math.PI * 2, cx0 = Math.cos(a), sx0 = Math.sin(a);
97
+ const g = R() - 0.5, st = nz.n3(cx0 * 9, sx0 * 9, y * 0.06) * 0.5 + nz.n3(cx0 * 40, sx0 * 40, y * 0.25) * 0.3;
98
+ let base = 22 + g * 12 + st * 9;
99
+ if (y < 4) base = 54 + g * 14 - y * 3; else if (y < 12) base = 40 + g * 14 - (y - 4) * 1.6; // curb lip
100
+ base *= 1 - y / PH * 0.35;
101
+ const i = (y * PW + xx) * 4; d[i] = base * 0.92; d[i + 1] = base * 0.95; d[i + 2] = base * 1.1; d[i + 3] = 255;
102
+ const v = nz.n3(cx0 * 4.5, sx0 * 4.5, y * 0.03 + 20) + 0.4 * nz.n3(cx0 * 12, sx0 * 12, y * 0.08);
103
+ pd[i] = pd[i + 1] = pd[i + 2] = 255; pd[i + 3] = 255 * H.smooth(-0.12, 0.08, v) * H.smooth(6, 16, y);
104
+ }
105
+ x.putImageData(img, 0, 0); px.putImageData(pimg, 0, 0);
106
+ // aggregate speckle catching light
107
+ for (let k = 0; k < 1400; k++) { x.fillStyle = `rgba(200,200,220,${R() * 0.16})`; x.fillRect(R() * PW, 12 + Math.pow(R(), 0.8) * (PH - 12), 1 + R(), 1); }
108
+ return (paveTile = { w: W, c, p, PW });
109
+ }
110
+ function catPath(x, cx, cy, s) {
111
+ x.beginPath();
112
+ x.moveTo(cx - s * 0.95, cy - s * 0.1);
113
+ x.lineTo(cx - s * 0.8, cy - s * 1.05); x.lineTo(cx - s * 0.35, cy - s * 0.72);
114
+ x.quadraticCurveTo(cx, cy - s * 0.82, cx + s * 0.35, cy - s * 0.72);
115
+ x.lineTo(cx + s * 0.8, cy - s * 1.05); x.lineTo(cx + s * 0.95, cy - s * 0.1);
116
+ x.bezierCurveTo(cx + s * 1.0, cy + s * 0.75, cx - s * 1.0, cy + s * 0.75, cx - s * 0.95, cy - s * 0.1);
117
+ x.moveTo(cx - s * 0.45, cy - s * 0.14); x.lineTo(cx - s * 0.2, cy - s * 0.1);
118
+ x.moveTo(cx + s * 0.45, cy - s * 0.14); x.lineTo(cx + s * 0.2, cy - s * 0.1);
119
+ x.moveTo(cx - s * 0.09, cy + s * 0.1); x.lineTo(cx + s * 0.09, cy + s * 0.1); x.lineTo(cx, cy + s * 0.2);
120
+ x.quadraticCurveTo(cx - s * 0.1, cy + s * 0.34, cx - s * 0.24, cy + s * 0.28);
121
+ x.moveTo(cx, cy + s * 0.2); x.quadraticCurveTo(cx + s * 0.1, cy + s * 0.34, cx + s * 0.24, cy + s * 0.28);
122
+ x.moveTo(cx - s * 0.38, cy + s * 0.18); x.lineTo(cx - s * 1.05, cy + s * 0.06);
123
+ x.moveTo(cx + s * 0.38, cy + s * 0.18); x.lineTo(cx + s * 1.05, cy + s * 0.06);
124
+ }
125
+ function dogPath(x, cx, cy, s) {
126
+ x.beginPath();
127
+ x.moveTo(cx - s * 0.55, cy - s * 0.7);
128
+ x.quadraticCurveTo(cx, cy - s * 0.95, cx + s * 0.55, cy - s * 0.7);
129
+ x.bezierCurveTo(cx + s * 0.78, cy - s * 0.1, cx + s * 0.55, cy + s * 0.78, cx, cy + s * 0.8);
130
+ x.bezierCurveTo(cx - s * 0.55, cy + s * 0.78, cx - s * 0.78, cy - s * 0.1, cx - s * 0.55, cy - s * 0.7);
131
+ x.moveTo(cx - s * 0.55, cy - s * 0.7); x.bezierCurveTo(cx - s * 1.2, cy - s * 0.6, cx - s * 1.15, cy + s * 0.3, cx - s * 0.8, cy + s * 0.25);
132
+ x.moveTo(cx + s * 0.55, cy - s * 0.7); x.bezierCurveTo(cx + s * 1.2, cy - s * 0.6, cx + s * 1.15, cy + s * 0.3, cx + s * 0.8, cy + s * 0.25);
133
+ x.moveTo(cx - s * 0.2, cy - s * 0.2); x.arc(cx - s * 0.26, cy - s * 0.2, s * 0.06, 0, 7);
134
+ x.moveTo(cx + s * 0.32, cy - s * 0.2); x.arc(cx + s * 0.26, cy - s * 0.2, s * 0.06, 0, 7);
135
+ x.moveTo(cx - s * 0.16, cy + s * 0.18); x.quadraticCurveTo(cx, cy + s * 0.06, cx + s * 0.16, cy + s * 0.18); x.quadraticCurveTo(cx, cy + s * 0.32, cx - s * 0.16, cy + s * 0.18);
136
+ x.moveTo(cx, cy + s * 0.3); x.lineTo(cx, cy + s * 0.44);
137
+ x.moveTo(cx - s * 0.22, cy + s * 0.48); x.quadraticCurveTo(cx, cy + s * 0.6, cx + s * 0.22, cy + s * 0.48);
138
+ }
139
+ function brainPath(x, cx, cy, s) {
140
+ x.beginPath();
141
+ for (let i = 0; i <= 120; i++) {
142
+ const a = i / 120 * Math.PI * 2, r = 1 + 0.07 * Math.max(0, Math.cos(a * 9));
143
+ const px = cx + Math.cos(a) * s * 1.15 * r, py = cy + Math.sin(a) * s * 0.85 * r;
144
+ if (i === 0) x.moveTo(px, py); else x.lineTo(px, py);
145
+ }
146
+ x.moveTo(cx, cy - s * 0.8);
147
+ for (let i = 1; i <= 20; i++) x.lineTo(cx + Math.sin(i * 1.3) * s * 0.06, cy - s * 0.8 + i / 20 * s * 1.55);
148
+ [[-1, -0.35], [-1, 0.2], [1, -0.3], [1, 0.25]].forEach(([sd, oy]) => {
149
+ x.moveTo(cx + sd * s * 0.2, cy + oy * s);
150
+ for (let i = 1; i <= 16; i++) { const u = i / 16; x.lineTo(cx + sd * s * (0.2 + u * 0.7), cy + oy * s + Math.sin(u * Math.PI * 3) * s * 0.1); }
151
+ });
152
+ }
153
+
154
+ function neonExplainer(ctx, W, Hh, t) {
155
+ const hs = (n) => { const v = Math.sin(n * 91.731 + 7.13) * 43758.5453; return v - Math.floor(v); };
156
+ const ign = (t0, len = 0.3) => t < t0 ? 0 : t - t0 > len ? 1 : (hs(Math.floor(t * 40) + t0 * 977) > 0.42 ? 1 : 0);
157
+ const sm = H.smooth, E = H.ease;
158
+ // ---- camera: whip-pans along the wall between beats ----
159
+ const pans = [[2.0, 2.8], [5.8, 6.5], [10.4, 11.1], [12.9, 13.5]];
160
+ let cam = 0; pans.forEach(([a, b]) => { cam += 1280 * E.inOut(sm(a, b, t)); });
161
+ const col = H.layer(W, Hh), cx = col.getContext('2d');
162
+ const txt = H.layer(W, Hh), tx = txt.getContext('2d');
163
+ const txtB = H.layer(W, Hh), tbx = txtB.getContext('2d'); // fat script glyphs: eroded harder so the hot core stays thin
164
+ const core = H.layer(W, Hh), kx = core.getContext('2d');
165
+ const off = H.layer(W, Hh), ox = off.getContext('2d');
166
+ [cx, tx, tbx, kx, ox].forEach((c) => { c.lineCap = 'round'; c.lineJoin = 'round'; c.translate(-cam, 0); });
167
+ // physical hardware (world coords): glass-tube clips, electrode housings, painted-out blockout runs
168
+ const HW = { clips: [], elec: [] }, USED = { thin: null, fat: null };
169
+ const clip = (x, y, a = 0) => HW.clips.push([x, y, a]);
170
+ const elec = (x, y, a) => HW.elec.push([x, y, a]);
171
+ const blockout = (pathFn, w = 7) => {
172
+ ox.save(); ox.strokeStyle = 'rgb(13,11,14)'; ox.lineWidth = w; pathFn(ox); ox.stroke();
173
+ ox.strokeStyle = 'rgba(150,110,130,0.28)'; ox.lineWidth = w * 0.28; ox.translate(-1, -1.5); pathFn(ox); ox.stroke(); ox.restore();
174
+ };
175
+ const quadPt = (x0, y0, qx, qy, x1, y1, u) => [(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];
176
+ const ellClip = (ex, ey, rx, ry, a) => clip(ex + Math.cos(a) * rx, ey + Math.sin(a) * ry, Math.atan2(Math.cos(a) * ry, -Math.sin(a) * rx));
177
+ // blockout run between two words of a lettered line: dips under the baseline, electrodes where it enters the glass
178
+ const wordGap = (s, cxw, y, font, splitAt, dip = 30) => {
179
+ tx.font = font; const wd = tx.measureText(s).width, x0 = cxw - wd / 2;
180
+ const xa = x0 + tx.measureText(s.slice(0, splitAt)).width - 10, xb = x0 + tx.measureText(s.slice(0, splitAt + 1)).width + 10;
181
+ blockout((g) => { g.beginPath(); g.moveTo(xa, y - 4); g.bezierCurveTo(xa + 6, y + dip, xb - 6, y + dip, xb, y - 4); });
182
+ elec(xa, y - 4, -1.9); elec(xb, y - 4, -1.25);
183
+ return [x0, x0 + wd];
184
+ };
185
+ cx.globalCompositeOperation = 'lighter'; kx.globalCompositeOperation = 'lighter';
186
+ const tube = (pathFn, c, k, w = 7) => {
187
+ ox.strokeStyle = 'rgb(44,36,48)'; ox.lineWidth = w; pathFn(ox); ox.stroke();
188
+ ox.strokeStyle = 'rgba(170,160,180,0.2)'; ox.lineWidth = w * 0.3; ox.save(); ox.translate(-1, -1.5); pathFn(ox); ox.stroke(); ox.restore();
189
+ if (k <= 0) return;
190
+ cx.strokeStyle = H.rgba(c, k); cx.lineWidth = w; pathFn(cx); cx.stroke();
191
+ kx.strokeStyle = H.rgba(H.mix(c, [255, 255, 255], 0.82), k); kx.lineWidth = w * 0.36; pathFn(kx); kx.stroke();
192
+ };
193
+ const ntext = (s, x, y, font, c, k, align = 'center', lw = 3, ghost = true) => {
194
+ if (ghost) { ox.font = font; ox.textAlign = align; ox.lineWidth = lw; ox.fillStyle = ox.strokeStyle = 'rgb(44,36,48)'; ox.fillText(s, x, y); ox.strokeText(s, x, y); }
195
+ if (k <= 0) return;
196
+ const fat = font.includes('Gloria');
197
+ { // screen-space bbox of the lit text, so the core erosion only touches that region
198
+ tx.font = font; const fsz = +(font.match(/(\d+)px/) || [0, 60])[1], w = tx.measureText(s).width, pad = lw + 8;
199
+ const x0 = (align === 'center' ? x - w / 2 : x) - cam - pad, b = [x0, y - fsz * 1.15 - pad, x0 + w + 2 * pad, y + fsz * 0.55 + pad];
200
+ const key = fat ? 'fat' : 'thin', o = USED[key];
201
+ USED[key] = o ? [Math.min(o[0], b[0]), Math.min(o[1], b[1]), Math.max(o[2], b[2]), Math.max(o[3], b[3])] : b;
202
+ }
203
+ for (const [g, cc] of [[cx, c], [fat ? tbx : tx, H.mix(c, [255, 255, 255], 0.85)]]) {
204
+ g.font = font; g.textAlign = align; g.lineWidth = fat && g === cx ? lw + 3 : lw; g.fillStyle = g.strokeStyle = H.rgba(cc, k);
205
+ g.fillText(s, x, y); g.strokeText(s, x, y);
206
+ }
207
+ };
208
+ const wordByLetter = (s, x, y, font, c, t0, dt, align = 'center') => {
209
+ tx.font = font; const wd = tx.measureText(s).width; let x0 = align === 'center' ? x - wd / 2 : x;
210
+ for (let i = 0; i < s.length; i++) { if (s[i] !== ' ') ntext(s[i], x0 + tx.measureText(s.slice(0, i)).width, y, font, c, ign(t0 + i * dt), 'left'); }
211
+ };
212
+ // segmented chase arrow along a quadratic; draws on segment by segment
213
+ const arrow = (x0, y0, qx, qy, x1, y1, c, prog, n = 12, w = 6) => {
214
+ const P = (u) => [(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];
215
+ for (let i = 0; i < n; i++) {
216
+ const a = P(i / n + 0.012), b = P((i + 1) / n - 0.012);
217
+ const lit = prog * n > i + 0.5 ? 0.45 + 0.55 * ((((i - t * 14) % 4) + 4) % 4 < 1.4 ? 1 : 0) : 0;
218
+ tube((g) => { g.beginPath(); g.moveTo(...a); g.lineTo(...b); }, c, lit, w);
219
+ }
220
+ const e = P(1), pe = P(0.94), ang = Math.atan2(e[1] - pe[1], e[0] - pe[0]);
221
+ tube((g) => { g.beginPath(); g.moveTo(e[0] + Math.cos(ang + 2.5) * 22, e[1] + Math.sin(ang + 2.5) * 22); g.lineTo(...e); g.lineTo(e[0] + Math.cos(ang - 2.5) * 22, e[1] + Math.sin(ang - 2.5) * 22); }, c, prog >= 1 ? 1 : 0, w);
222
+ };
223
+ const frame = (x, y, w, h, c, k) => tube((g) => { g.beginPath(); g.roundRect(x, y, w, h, 14); }, c, k, 6);
224
+ const icon = (kind, x, y, s, c, k) => tube((g) => (kind === 'cat' ? catPath : dogPath)(g, x, y, s), c, k, 5);
225
+ const seen = (X) => cam > X - 1400 && cam < X + 1400;
226
+
227
+ // ---------------- A: HOOK ----------------
228
+ if (seen(0)) {
229
+ ntext('How does', 640, 250, '400 74px "Poiret One"', NX.cyan, ign(0.15, 0.35), 'center', 4.5);
230
+ wordByLetter('AI learn', 600, 430, '400 160px "Gloria Hallelujah"', NX.pink, 0.55, 0.09);
231
+ const q = t > 1.4 ? (t < 2.3 && hs(Math.floor(t * 12)) > 0.6 ? 0.25 : 1) : 0;
232
+ ntext('?', 990, 430, '400 160px "Gloria Hallelujah"', NX.warm, q, 'left');
233
+ tube((g) => { g.beginPath(); g.moveTo(330, 470); g.quadraticCurveTo(640, 500, 960, 466); }, NX.violet, ign(1.25), 6);
234
+ wordGap('How does', 640, 250, '400 74px "Poiret One"', 3, 22);
235
+ wordGap('AI learn', 600, 430, '400 160px "Gloria Hallelujah"', 2, 40);
236
+ elec(330, 470, Math.PI); elec(960, 466, 0);
237
+ for (const u of [0.2, 0.5, 0.8]) { const q = quadPt(330, 470, 640, 500, 960, 466, u); clip(q[0], q[1], 0); }
238
+ }
239
+ // ---------------- B: DATA ----------------
240
+ const XB = 1280;
241
+ if (seen(XB)) {
242
+ for (let k = 0; k < 6; k++) {
243
+ const c0 = k % 2, r0 = Math.floor(k / 2);
244
+ const fx = XB + 110 + c0 * 200, fy = 70 + r0 * 170, t0 = 2.85 + k * 0.17;
245
+ const cyc = Math.max(0, Math.floor((t - 3.6 - k * 0.11) * 2.6));
246
+ const kind = (k + cyc) % 2 ? 'dog' : 'cat';
247
+ const swapOff = t > 3.6 && ((t - 3.6 - k * 0.11) * 2.6) % 1 < 0.12 ? 0 : 1;
248
+ const kk = ign(t0) * swapOff;
249
+ frame(fx, fy, 160, 104, NX.violet, ign(t0));
250
+ icon(kind, fx + 80, fy + 56, 34, NX.warm, kk);
251
+ tube((g) => { g.beginPath(); g.moveTo(fx + 80, fy + 104); g.lineTo(fx + 80, fy + 116); }, NX.cyan, ign(t0 + 0.05), 3);
252
+ tube((g) => { g.beginPath(); g.roundRect(fx + 36, fy + 115, 88, 38, 14); }, NX.cyan, ign(t0 + 0.05), 3.5);
253
+ ntext(kind, fx + 80, fy + 144, '400 31px "Poiret One"', NX.warm, ign(t0 + 0.05) * swapOff, 'center', 2.2);
254
+ clip(fx + 34, fy, 0); clip(fx + 126, fy, 0); clip(fx, fy + 52, Math.PI / 2); clip(fx + 160, fy + 52, Math.PI / 2);
255
+ }
256
+ elec(XB + 540, 320, Math.PI); clip(XB + 650, 309, 0);
257
+ for (const a of [-2.3, -1.0, 0.5, 2.5]) ellClip(XB + 960, 290, 161, 119, a);
258
+ arrow(XB + 540, 320, XB + 640, 300, XB + 760, 312, NX.warm, sm(3.05, 3.45, t), 10);
259
+ const bk = ign(3.3) * (0.85 + 0.15 * Math.sin(t * 9));
260
+ tube((g) => brainPath(g, XB + 960, 290, 140), NX.pink, bk, 7);
261
+ const p = sm(3.4, 5.25, t), val = p >= 1 ? 1000000 : Math.round(Math.pow(10, 6 * p));
262
+ ntext(val.toLocaleString('en-US'), XB + 960, 520, '400 66px "Poiret One"', NX.warm, ign(3.35), 'center', 4, false);
263
+ ntext('examples', XB + 960, 572, '400 34px "Poiret One"', NX.cyan, ign(3.5), 'center', 2.4);
264
+ }
265
+ // ---------------- C: GUESS -> CHECK -> ADJUST loop ----------------
266
+ const XC = 2560;
267
+ if (seen(XC)) {
268
+ frame(XC + 80, 222, 180, 132, NX.violet, ign(6.45));
269
+ icon('cat', XC + 170, 292, 44, NX.warm, ign(6.5));
270
+ arrow(XC + 280, 290, XC + 380, 284, XC + 455, 290, NX.cyan, sm(6.65, 6.95, t), 6);
271
+ ntext('Guess', XC + 368, 246, '400 46px "Poiret One"', NX.warm, ign(6.7), 'center', 3);
272
+ const adj = sm(8.85, 9.45, t);
273
+ const bglow = ign(6.5) * (adj > 0 && adj < 1 ? 0.6 + 0.4 * hs(Math.floor(t * 20)) : 1);
274
+ tube((g) => brainPath(g, XC + 600, 290, 112), adj >= 1 ? NX.pink : H.mix(NX.pink, NX.violet, adj), bglow, 7);
275
+ arrow(XC + 745, 290, XC + 800, 284, XC + 862, 290, NX.cyan, sm(7.0, 7.25, t), 5);
276
+ tube((g) => { g.beginPath(); g.roundRect(XC + 872, 222, 230, 132, 30); g.moveTo(XC + 900, 354); g.lineTo(XC + 884, 384); g.lineTo(XC + 940, 354); }, NX.warm, ign(7.2), 6);
277
+ const wrong = t < 9.55 ? ign(7.4) : 0;
278
+ ntext('dog?', XC + 987, 312, '400 78px "Gloria Hallelujah"', NX.pink, wrong, 'center', 2);
279
+ const xOn = t >= 7.85 && t < 9.55 ? (t < 8.6 ? (Math.floor(t * 7) % 2 ? 1 : 0.15) : 1) : 0;
280
+ tube((g) => { g.beginPath(); g.moveTo(XC + 1152, 252); g.lineTo(XC + 1212, 322); g.moveTo(XC + 1212, 252); g.lineTo(XC + 1152, 322); }, NX.red, xOn, 9);
281
+ tube((g) => { g.beginPath(); g.arc(XC + 1182, 287, 58, 0, Math.PI * 2); }, NX.red, t < 9.55 ? ign(7.95) : 0, 5);
282
+ ntext('Check', XC + 1160, 196, '400 46px "Poiret One"', NX.warm, ign(7.95), 'center', 3);
283
+ // error signal travels back to the model
284
+ arrow(XC + 1150, 360, XC + 980, 600, XC + 715, 470, NX.red, sm(8.2, 8.8, t), 14);
285
+ // knobs (weights) step to new settings
286
+ for (let i = 0; i < 3; i++) {
287
+ const kx0 = XC + 520 + i * 70, ky0 = 482;
288
+ const a0 = [-2.2, -0.6, -1.4][i], a1 = [-0.9, -1.9, -0.3][i];
289
+ const st = Math.floor(adj * 3 + 0.0001) / 3; // stepped: tube banks switch
290
+ const a = H.lerp(a0, a1, st);
291
+ tube((g) => { g.beginPath(); g.arc(kx0, ky0, 22, 0, Math.PI * 2); }, NX.cyan, ign(6.6 + i * 0.05), 4);
292
+ tube((g) => { g.beginPath(); g.moveTo(kx0, ky0); g.lineTo(kx0 + Math.cos(a) * 18, ky0 + Math.sin(a) * 18); }, NX.warm, ign(6.6 + i * 0.05), 4);
293
+ }
294
+ ntext('Adjust', XC + 600, 564, '400 46px "Poiret One"', NX.warm, ign(8.8), 'center', 3);
295
+ clip(XC + 125, 222, 0); clip(XC + 215, 222, 0); clip(XC + 125, 354, 0); clip(XC + 215, 354, 0);
296
+ elec(XC + 280, 290, Math.PI); elec(XC + 745, 290, Math.PI); elec(XC + 1150, 360, -2.2);
297
+ for (const a of [-2.2, -0.8, 0.9, 2.4]) ellClip(XC + 600, 290, 129, 95, a);
298
+ clip(XC + 935, 222, 0); clip(XC + 1040, 222, 0); clip(XC + 1040, 354, 0);
299
+ clip(XC + 1182, 229, 0); clip(XC + 1182, 345, 0);
300
+ for (let i = 0; i < 3; i++) clip(XC + 520 + i * 70, 504, 0);
301
+ const right = ign(9.6);
302
+ ntext('cat', XC + 987, 312, '400 78px "Gloria Hallelujah"', NX.green, right, 'center', 2);
303
+ tube((g) => { g.beginPath(); g.moveTo(XC + 1150, 290); g.lineTo(XC + 1175, 318); g.lineTo(XC + 1220, 256); }, NX.green, ign(9.8), 9);
304
+ }
305
+ // ---------------- D: PROGRESS ----------------
306
+ const XD = 3840;
307
+ if (seen(XD)) {
308
+ tube((g) => { g.beginPath(); g.moveTo(XD + 140, 110); g.lineTo(XD + 140, 500); g.lineTo(XD + 780, 500); }, NX.cyan, ign(10.95), 5);
309
+ ntext('training rounds', XD + 460, 552, '400 38px "Poiret One"', NX.cyan, ign(11.05), 'center', 2.6);
310
+ const pts = []; for (let r = 0; r <= 12; r++) { const acc = 0.97 - 0.36 * Math.exp(-r / 3.2) + (r > 0 && r < 12 ? (hs(r) - 0.5) * 0.03 : 0); pts.push([XD + 160 + r * 50, 500 - (acc - 0.5) / 0.5 * 380]); }
311
+ const lp = sm(11.2, 12.45, t) * 12;
312
+ tube((g) => { g.beginPath(); g.moveTo(...pts[0]); for (let r = 1; r <= Math.floor(lp); r++) g.lineTo(...pts[r]); const f = lp - Math.floor(lp); if (f > 0 && lp < 12) { const a = pts[Math.floor(lp)], b = pts[Math.floor(lp) + 1]; g.lineTo(a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f); } }, NX.warm, lp > 0 ? 1 : 0, 6);
313
+ for (let r = 0; r <= 12; r += 2) if (lp >= r) tube((g) => { g.beginPath(); g.arc(pts[r][0], pts[r][1], 7, 0, Math.PI * 2); }, NX.pink, 1, 4);
314
+ ntext('Accuracy', XD + 1010, 210, '400 50px "Poiret One"', NX.cyan, ign(11.0), 'center', 3);
315
+ const pct = Math.round(61 + 36 * E.out(sm(11.2, 12.45, t)));
316
+ const flash = t > 12.45 && t < 12.95 ? (Math.floor(t * 10) % 2 ? 1 : 0.2) : 1;
317
+ ntext(pct + '%', XD + 1010, 390, '400 150px "Gloria Hallelujah"', pct >= 97 ? NX.green : NX.pink, ign(11.05) * flash, 'center', 3, false);
318
+ tube((g) => { g.beginPath(); g.ellipse(XD + 1010, 335, 190, 105, -0.05, 0, Math.PI * 2); }, NX.warm, ign(12.55, 0.35), 6);
319
+ elec(XD + 140, 110, -Math.PI / 2); elec(XD + 780, 500, 0);
320
+ clip(XD + 140, 230, Math.PI / 2); clip(XD + 140, 400, Math.PI / 2); clip(XD + 300, 500, 0); clip(XD + 620, 500, 0);
321
+ for (const a of [-2.0, -0.9, 0.9, 2.2]) ellClip(XD + 1010, 335, 190, 105, a);
322
+ }
323
+ // ---------------- E: TAKEAWAY ----------------
324
+ const XE = 5120;
325
+ if (seen(XE)) {
326
+ ntext('More examples', XE + 640, 230, '400 82px "Poiret One"', NX.cyan, ign(13.4, 0.3), 'center', 4.5);
327
+ arrow(XE + 640, 262, XE + 646, 300, XE + 640, 345, NX.warm, sm(13.6, 13.85, t), 4, 7);
328
+ wordByLetter('better guesses', XE + 640, 470, '400 120px "Gloria Hallelujah"', NX.pink, 13.85, 0.04);
329
+ wordGap('More examples', XE + 640, 230, '400 82px "Poiret One"', 4, 24);
330
+ wordGap('better guesses', XE + 640, 470, '400 120px "Gloria Hallelujah"', 6, 36);
331
+ elec(XE + 640, 262, -Math.PI / 2);
332
+ }
333
+
334
+ // ---------- compose: brick wall lit by the signs ----------
335
+ kx.setTransform(1, 0, 0, 1, 0, 0); for (const [m, r, b] of [[txt, 2, USED.thin], [txtB, 5, USED.fat]]) { if (!b) continue; const x0 = Math.max(r, Math.floor(b[0])), y0 = Math.max(r, Math.floor(b[1])), x1 = Math.min(W - r, Math.ceil(b[2])), y1 = Math.min(Hh - r, Math.ceil(b[3])); if (x1 - x0 < 2 || y1 - y0 < 2) continue; const bw = x1 - x0, bh = y1 - y0, c = H.layer(bw, bh), e = c.getContext('2d'); e.drawImage(m, x0, y0, bw, bh, 0, 0, bw, bh); e.globalCompositeOperation = 'destination-in'; for (let a = 0; a < 8; a++) { const dx = Math.round(Math.cos(a * Math.PI / 4) * r), dy = Math.round(Math.sin(a * Math.PI / 4) * r); e.drawImage(m, x0 - dx, y0 - dy, bw, bh, 0, 0, bw, bh); } kx.drawImage(c, x0, y0); }
336
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, Hh);
337
+ const tile = getBrickTile(), tw = tile.width, tx0 = -(((cam % tw) + tw) % tw);
338
+ for (let x = tx0; x < W; x += tw) ctx.drawImage(tile, x, 0);
339
+ const LW8 = W / 8, LH8 = Hh / 8;
340
+ const L = H.layer(LW8, LH8), lx = L.getContext('2d');
341
+ lx.fillStyle = 'rgb(30,34,52)'; lx.fillRect(0, 0, LW8, LH8);
342
+ lx.globalCompositeOperation = 'lighter';
343
+ lx.filter = 'blur(10px)'; lx.drawImage(col, 0, 0, LW8, LH8); lx.drawImage(col, 0, 0, LW8, LH8);
344
+ lx.filter = 'blur(3px)'; lx.globalAlpha = 0.9; lx.drawImage(col, 0, 0, LW8, LH8);
345
+ ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.drawImage(L, 0, 0, W, Hh); ctx.restore();
346
+ { // brick arrises catch the sign light: top edges where the light is above, bottom edges where it is below
347
+ const Ld = lx.getImageData(0, 0, LW8, LH8).data;
348
+ const rimC = H.layer(LW8, LH8), ri = rimC.getContext('2d').createImageData(LW8, LH8);
349
+ const lum = (x, y) => { const k = (H.clamp(y, 0, LH8 - 1) * LW8 + x) * 4; return Ld[k] + Ld[k + 1] + Ld[k + 2]; };
350
+ for (let y = 0; y < LH8; y++) for (let x = 0; x < LW8; x++) {
351
+ const k = (y * LW8 + x) * 4, dir = H.clamp((lum(x, y - 1) - lum(x, y + 1)) / 90, -1, 1);
352
+ const up = 0.3 + Math.abs(dir) * 1.3;
353
+ ri.data[k] = Math.max(0, Ld[k] - 30) * up; ri.data[k + 1] = Math.max(0, Ld[k + 1] - 34) * up; ri.data[k + 2] = Math.max(0, Ld[k + 2] - 52) * up; ri.data[k + 3] = 255;
354
+ }
355
+ rimC.getContext('2d').putImageData(ri, 0, 0);
356
+ const et = getEdgeTiles(), rim = H.layer(W, 600), rx = rim.getContext('2d');
357
+ for (let x = tx0; x < W; x += tw) rx.drawImage(et.both, x, 0);
358
+ rx.globalCompositeOperation = 'source-in'; rx.drawImage(rimC, 0, 0, LW8, 600 / 8, 0, 0, W, 600);
359
+ ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.drawImage(rim, 0, 0); ctx.restore();
360
+ }
361
+ // tubes stand ~4cm off the wall: soft contact shadow, then the glass itself
362
+ ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.6;
363
+ ctx.drawImage(blurSmall(off, 4, 0.25), 5, 9, W, Hh); ctx.restore();
364
+ ctx.drawImage(off, 0, 0);
365
+ // electrode housings where runs dive into the wall
366
+ ctx.save(); ctx.translate(-cam, 0);
367
+ for (const [x, y, a] of HW.elec) {
368
+ if (x - cam < -40 || x - cam > W + 40) continue;
369
+ ctx.save(); ctx.translate(x, y); ctx.rotate(a);
370
+ ctx.fillStyle = '#0d0c0f'; ctx.beginPath(); ctx.roundRect(-7, -6, 22, 12, 4); ctx.fill();
371
+ ctx.fillStyle = 'rgba(255,255,255,0.18)'; ctx.fillRect(-4, -5, 15, 2);
372
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(13, -4, 4, 8);
373
+ ctx.restore();
374
+ }
375
+ ctx.restore();
376
+ // wet sidewalk: concrete tile panned with the wall
377
+ const pave = getPaveTile(W), px0 = -(((cam % pave.PW) + pave.PW) % pave.PW);
378
+ for (let x = px0; x < W; x += pave.PW) ctx.drawImage(pave.c, x, 600);
379
+ ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.fillRect(0, 600, W, 3);
380
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
381
+ // halos are blurred at low res with extra saturation, so the glass rim stays deep colour around a thin hot core
382
+ const blurSat = (src, px, sc, sat) => { const w = Math.ceil(src.width * sc), h = Math.ceil(src.height * sc), c = H.layer(w, h), x = c.getContext('2d'); x.filter = `blur(${Math.max(0.5, px * sc)}px) saturate(${sat})`; x.drawImage(src, 0, 0, w, h); return c; };
383
+ ctx.globalAlpha = 0.55; ctx.drawImage(blurSat(col, 60, 0.125, 1.6), 0, 0, W, Hh);
384
+ ctx.globalAlpha = 0.75; ctx.drawImage(blurSat(col, 18, 0.25, 1.8), 0, 0, W, Hh);
385
+ ctx.globalAlpha = 0.6; ctx.drawImage(blurSat(col, 5, 0.5, 2.2), 0, 0, W, Hh);
386
+ ctx.globalAlpha = 1; ctx.drawImage(col, 0, 0); ctx.drawImage(core, 0, 0);
387
+ ctx.globalAlpha = 0.5; ctx.drawImage(blurSmall(core, 2.5, 0.5), 0, 0, W, Hh);
388
+ ctx.restore();
389
+ // mounting clips over the glass
390
+ ctx.save(); ctx.translate(-cam, 0);
391
+ for (const [x, y, a] of HW.clips) {
392
+ if (x - cam < -20 || x - cam > W + 20) continue;
393
+ ctx.save(); ctx.translate(x, y); ctx.rotate(a);
394
+ ctx.fillStyle = 'rgba(30,28,34,0.92)'; ctx.beginPath(); ctx.roundRect(-2.5, -8, 5, 16, 2); ctx.fill();
395
+ ctx.fillStyle = 'rgba(255,255,255,0.28)'; ctx.fillRect(-1.5, -7, 1, 14);
396
+ ctx.restore();
397
+ }
398
+ ctx.restore();
399
+ // reflection in the wet pavement: rippled mirror rows, vertical streak smear, full mirror only inside puddles
400
+ {
401
+ const RS = 0.25, src = blurSmall(col, 6, RS), GH = Hh - 600, refl = H.layer(W, GH), rfx = refl.getContext('2d');
402
+ for (let y = 604; y < Hh; y += 2) {
403
+ const gy = y - 600, sy = 600 - gy / 0.3;
404
+ if (sy < 0) break;
405
+ const offx = Math.sin(y * 0.35 + t * 6) * (1.5 + gy * 0.035) + Math.sin(y * 0.11 - t * 2.3 + 1.7) * (1 + gy * 0.03);
406
+ rfx.drawImage(src, 0, sy * RS, src.width, (2 / 0.3) * RS, offx, gy, W, 2);
407
+ }
408
+ const smear = H.layer(W, 10), smx = smear.getContext('2d');
409
+ smx.filter = 'blur(1.5px)'; smx.drawImage(refl, 0, 4, W, GH - 4, 0, 0, W, 10);
410
+ const rm = H.layer(W, GH), rmx = rm.getContext('2d');
411
+ for (let x = px0; x < W; x += pave.PW) rmx.drawImage(pave.p, x, 0);
412
+ rmx.globalCompositeOperation = 'source-in'; rmx.drawImage(refl, 0, 0);
413
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
414
+ ctx.globalAlpha = 0.45; ctx.drawImage(smear, 0, 0, W, 10, 0, 604, W, GH - 4);
415
+ ctx.globalAlpha = 0.22; ctx.drawImage(refl, 0, 600);
416
+ ctx.globalAlpha = 0.6; ctx.drawImage(rm, 0, 600);
417
+ ctx.restore();
418
+ }
419
+ H.vignette(ctx, W, Hh, 0.55, '4,2,10');
420
+ H.grain(ctx, W, Hh, 0.045, 31 + Math.floor(t * 24), false);
421
+ }
422
+
423
+ // Sound: a night street after rain. Transformer hum and light drizzle underneath; every tube that lights makes the
424
+ // real ignition noise (contactor click, then a 120 Hz buzz that stutters exactly with the on-screen flicker, then
425
+ // settles); chase arrows tick relay by relay; whip-pans are a fast air smear with the passing sign hum bending past.
426
+ // All times are copied from neonExplainer() above (same ign() flicker hash, same smooth/ease windows).
427
+ // Voiceover mode (k.vo): beds drop out, letter-by-letter crackle, chase ticks, counter re-lights and the per-scene
428
+ // ballast hum are minor (quieter); scene transitions, WRONG, the correction, the 97% hit and the resolve stay full.
429
+ function neonSound(k) {
430
+ const sm = H.smooth, E = H.ease, D = 15;
431
+ const hs = (n) => { const v = Math.sin(n * 91.731 + 7.13) * 43758.5453; return v - Math.floor(v); }; // = neonExplainer hs
432
+ const when = (f, v, a, b) => { for (let t = a; t <= b; t += 0.001) if (f(t) >= v) return t; return b; };
433
+ // contactor / relay click as a sign's circuit closes: dull, low, no ring
434
+ const relay = (t, g = 1, pan = 0) => {
435
+ k.noise(t, { dur: 0.022, filter: 'bandpass', f: 1300, q: 1.4, gain: 0.32 * g, a: 0.001, pan });
436
+ k.tone(t, { f: [[0, 190], [0.04, 75]], dur: 0.06, gain: 0.2 * g, pan });
437
+ };
438
+ // one stretch of lit tube: 120 Hz gas buzz (bright band + body) with a tiny spark at the strike
439
+ const buzzRun = (t, dur, g, pan, f = 120) => {
440
+ k.tone(t, { f, type: 'sawtooth', dur: dur + 0.012, gain: 0.075 * g, a: 0.002, hold: dur * 0.85, filter: 'bandpass', f2: 1700, q: 2, pan });
441
+ k.tone(t, { f, type: 'sawtooth', dur: dur + 0.02, gain: 0.06 * g, a: 0.002, hold: dur * 0.85, filter: 'lowpass', f2: 480, pan });
442
+ k.noise(t, { dur: 0.008, filter: 'highpass', f: 2600, gain: 0.12 * g, a: 0.001, pan });
443
+ };
444
+ // ign(t0, len) from the explainer, reproduced slot by slot (40 slots/s): buzz while lit, silence while dark,
445
+ // then the latch tail where the tube holds and the buzz settles into the hum.
446
+ const ignite = (t0, o = {}) => {
447
+ const len = o.len || 0.3, g = o.gain == null ? 1 : o.gain, pan = o.pan || 0, f = o.f || 120;
448
+ if (!o.noClick) relay(t0, g * (o.click || 1), pan);
449
+ let s = Math.floor(t0 * 40), run = null;
450
+ const flush = (end) => { if (run != null && end - run > 0.004) buzzRun(run, end - run, g, pan, f); run = null; };
451
+ for (; s / 40 < t0 + len; s++) {
452
+ const a = Math.max(t0, s / 40), b = Math.min((s + 1) / 40, t0 + len), on = hs(s + t0 * 977) > 0.42;
453
+ if (on && run == null) run = a; else if (!on) flush(a);
454
+ if (b >= t0 + len) break;
455
+ }
456
+ flush(t0 + len);
457
+ k.tone(t0 + len, { f, type: 'sawtooth', dur: o.tail || 0.35, gain: 0.06 * g, a: 0.004, filter: 'bandpass', f2: 900, q: 1.5, pan });
458
+ };
459
+ // chase-light arrow: one relay tick per segment that latches (segment i lit when prog*n > i+0.5), throttled
460
+ const chase = (a, b, n, pan, gap = 0.08) => {
461
+ let last = -1;
462
+ for (let i = 0; i < n; i++) { const ti = when((t) => sm(a, b, t) * n, i + 0.5 + 1e-6, a, b + 0.01);
463
+ if (ti - last >= gap) { k.noise(ti, { dur: 0.016, filter: 'bandpass', f: 1500 + i * 25, q: 2, gain: 0.22, a: 0.001, pan, minor: true }); last = ti; } }
464
+ ignite(b, { len: 0.06, gain: 0.55, pan, noClick: true, tail: 0.18 }); // arrowhead latches when prog hits 1
465
+ };
466
+ // neon digits re-lit as the number changes: a short dry relay tick per change, throttled
467
+ const relight = (val, a, b, gap, avoid = []) => { let last = -1, prev = val(a);
468
+ for (let t = a; t <= b; t += 0.002) { const v = val(t);
469
+ if (v !== prev && t - last >= gap && !avoid.some((x) => t > x - 0.03 && t < x + 0.1)) {
470
+ k.noise(t, { dur: 0.014, filter: 'bandpass', f: 1150, q: 2.5, gain: 0.42, a: 0.001, pan: 0.35, minor: true });
471
+ k.tone(t, { f: 120, type: 'sawtooth', dur: 0.03, gain: 0.03, filter: 'bandpass', f2: 1600, q: 2, pan: 0.35, minor: true });
472
+ last = t; }
473
+ prev = v; } };
474
+ // whip-pan along the wall (eased inOut, fastest at the middle): an air smear plus the hum of the sign we pass
475
+ const whip = (a, b) => { const d = b - a, m = d * 0.5;
476
+ k.noise(a, { dur: d, color: 'pink', filter: 'bandpass', f: [[0, 260], [m, 1300], [d, 320]], q: 0.8, gain: 0.19, a: m * 0.9 });
477
+ k.tone(a + d * 0.2, { f: [[0, 112], [d * 0.3, 138], [d * 0.6, 96]], type: 'sawtooth', dur: d * 0.6, gain: 0.05, a: d * 0.25, filter: 'lowpass', f2: 700 });
478
+ };
479
+ // ballast hum of the sign in view: rises when it lights, falls away during the pan off it (minor, low)
480
+ const ballast = (t0, t1, f = 120) => k.tone(t0, { f, type: 'sawtooth', dur: t1 - t0, gain: 0.013, a: 0.3, hold: t1 - t0 - 0.7, filter: 'lowpass', f2: 380, bus: 'amb', minor: true });
481
+
482
+ // ---- beds: transformer hum, drizzle on wet pavement, a little night air
483
+ k.bed('hum', 0, D, { gain: 0.55 });
484
+ k.bed('rain', 0, D, { gain: 0.18 });
485
+ k.bed('room', 0, D, { gain: 0.35 });
486
+
487
+ // ---- A: hook. "How does" (0.15, len .35), "AI learn" letter by letter at 0.55 + i*0.09, underline 1.25
488
+ ignite(0.15, { len: 0.35, gain: 1.0, pan: 0, click: 1.3 });
489
+ k.withMinor(() => [...'AI learn'].forEach((c, i) => { if (c !== ' ') ignite(0.55 + i * 0.09, { gain: 0.55, click: 0.6, pan: -0.45 + i * 0.1, tail: 0.15 }); }));
490
+ ignite(1.25, { gain: 0.7, pan: 0 });
491
+ // "?" lights at 1.4 and keeps cutting out (hs(floor(t*12)) > 0.6 until 2.3): a crackle each time it comes back
492
+ ignite(1.4, { len: 0.05, gain: 0.8, pan: 0.5, f: 118 });
493
+ k.withMinor(() => { for (let s = Math.floor(1.4 * 12); s / 12 < 2.3; s++) if (hs(s) > 0.6 && !(hs(s + 1) > 0.6) && (s + 1) / 12 < 2.3) buzzRun((s + 1) / 12, 0.05, 0.6, 0.5, 118); });
494
+ ballast(0.15, 2.6);
495
+
496
+ // ---- pan 1 (2.0–2.8)
497
+ whip(2.0, 2.8);
498
+
499
+ // ---- B: data. six frames at 2.85 + k*0.17 (labels +0.05), arrow 3.05–3.45, brain 3.3, counter 3.35, "examples" 3.5
500
+ k.withMinor(() => { for (let c = 0; c < 6; c++) ignite(2.85 + c * 0.17, { gain: 0.5, click: 0.8, pan: -0.6 + (c % 2) * 0.2, tail: 0.2 }); });
501
+ chase(3.05, 3.45, 10, 0);
502
+ ignite(3.3, { gain: 1.0, pan: 0.4, f: 116, tail: 0.5 }); // the brain sign, the big one in this beat
503
+ ignite(3.5, { gain: 0.45, pan: 0.4, tail: 0.15, noClick: true });
504
+ // 10^(6p), p = sm(3.4, 5.25): digits re-light faster and faster, then lock at 1,000,000
505
+ relight((t) => { const p = sm(3.4, 5.25, t); return p >= 1 ? 1e6 : Math.round(Math.pow(10, 6 * p)); }, 3.62, 5.2, 0.09, [3.69]);
506
+ relay(5.25, 1.2, 0.35); ignite(5.25, { len: 0.08, gain: 0.7, pan: 0.35, noClick: true, tail: 0.3 });
507
+ ballast(2.85, 6.1, 116);
508
+
509
+ // ---- pan 2 (5.8–6.5)
510
+ whip(5.8, 6.5);
511
+
512
+ // ---- C: guess -> check -> adjust
513
+ ignite(6.45, { gain: 0.85, pan: -0.5 }); // image frame + cat, brain (6.5)
514
+ ignite(6.5, { gain: 0.6, pan: 0, noClick: true, f: 116 });
515
+ k.withMinor(() => [6.6, 6.65, 6.7].forEach((t, i) => ignite(t, { gain: 0.3, click: 0.6, pan: -0.1 + i * 0.1, tail: 0.1 }))); // knobs, "Guess"
516
+ chase(6.65, 6.95, 6, -0.25);
517
+ chase(7.0, 7.25, 5, 0.2);
518
+ ignite(7.2, { gain: 0.6, pan: 0.4 }); // speech bubble
519
+ ignite(7.4, { gain: 1.0, pan: 0.45, f: 124 }); // the guess: "dog?"
520
+ // WRONG: red X blinks on at 55/7, 57/7, 59/7 s (floor(t*7) odd), then holds from 8.6; ring + "Check" at 7.95
521
+ relay(7.857, 1.6, 0.7); k.tone(7.857, { f: [[0, 140], [0.12, 55]], dur: 0.22, gain: 0.4, pan: 0.7 });
522
+ [[55 / 7, 56 / 7], [57 / 7, 58 / 7], [59 / 7, 60 / 7], [8.6, 8.85]].forEach(([a, b], i) => {
523
+ k.play('buzz', a, { f: 98, dur: b - a, gain: i === 0 ? 1.0 : 0.75, pan: 0.7 });
524
+ if (i) relay(a, 0.7, 0.7);
525
+ });
526
+ ignite(7.95, { gain: 0.5, pan: 0.7, noClick: true, f: 98, tail: 0.15 });
527
+ chase(8.2, 8.8, 14, 0.2, 0.1); // error signal runs back to the model
528
+ ignite(8.8, { gain: 0.4, pan: -0.1, tail: 0.15 }); // "Adjust"
529
+ // brain stutters while the weights change (8.85–9.45, hs(floor(t*20)))
530
+ k.withMinor(() => { for (let s = Math.ceil(8.85 * 20); s / 20 < 9.45; s++) if (hs(s) > 0.55) buzzRun(s / 20, 0.04, 0.45, 0, 116); });
531
+ // knob tube banks switch at adj = 1/3, 2/3, 1 (adj = sm(8.85, 9.45))
532
+ for (let st = 1; st <= 3; st++) { const ts = when((t) => sm(8.85, 9.45, t) * 3 + 0.0001, st, 8.85, 9.46);
533
+ k.play('tock', ts, { f: 300 + st * 30, gain: 0.55, pan: -0.15 }); relay(ts, 0.8, -0.15); }
534
+ // RIGHT: green "cat" at 9.6, tick at 9.8; a low warm two-note answer under it
535
+ ignite(9.6, { gain: 1.0, pan: 0.45, f: 120 });
536
+ ignite(9.8, { gain: 0.8, pan: 0.7, noClick: false });
537
+ k.play('success', 9.8, { f: 'G4', gain: 0.75, pan: 0.4 });
538
+ ballast(6.45, 10.7, 98);
539
+
540
+ // ---- pan 3 (10.4–11.1)
541
+ whip(10.4, 11.1);
542
+
543
+ // ---- D: progress. axes 10.95, "Accuracy" 11.0, % and caption 11.05; line 11.2–12.45 with dots every 2 rounds
544
+ ignite(10.95, { gain: 0.7, pan: -0.4 });
545
+ k.withMinor(() => ignite(11.0, { gain: 0.4, pan: 0.5, tail: 0.15 }));
546
+ ignite(11.05, { gain: 0.8, pan: 0.5, noClick: true });
547
+ // the plotted tube lights along its length: a low buzz that climbs as it goes
548
+ k.tone(11.2, { f: 120, type: 'sawtooth', dur: 1.3, gain: 0.04, a: 0.15, hold: 1.0, filter: 'bandpass', f2: [[0, 500], [1.25, 1500]], q: 1.5, pan: -0.3, minor: true });
549
+ const lp = (t) => sm(11.2, 12.45, t) * 12, dots = [];
550
+ for (let r = 2; r <= 12; r += 2) { const td = when(lp, r, 11.2, 12.46); dots.push(td); buzzRun(td, 0.04, 0.5, -0.5 + r * 0.05); }
551
+ // pct = round(61 + 36 * E.out(sm(11.2, 12.45))): re-lit digits, slowing until it locks on 97 (turns green)
552
+ const pct = (t) => Math.round(61 + 36 * E.out(sm(11.2, 12.45, t)));
553
+ const t97 = when(pct, 97, 11.2, 12.45);
554
+ relight(pct, 11.21, t97 - 0.05, 0.085, dots);
555
+ relay(t97, 1.2, 0.5); ignite(t97, { len: 0.1, gain: 0.8, pan: 0.5, noClick: true, f: 124, tail: 0.4 });
556
+ // THE HIT: 97% flashes from 12.45 (on at 12.5, 12.7, 12.9) and the ring tube ignites at 12.55
557
+ k.play('thud', 12.5, { f: 90, gain: 1.1 }); relay(12.5, 1.8, 0.4);
558
+ [[12.5, 12.6], [12.7, 12.8], [12.9, 12.95]].forEach(([a, b], i) => k.tone(a, { f: 124, type: 'sawtooth', dur: b - a + 0.02, gain: 0.11 - i * 0.02, a: 0.002, hold: (b - a) * 0.8, filter: 'bandpass', f2: 1400, q: 1.2, pan: 0.4 }));
559
+ ignite(12.55, { len: 0.35, gain: 1.2, pan: 0.4, f: 120, tail: 0.6 });
560
+ [120, 240, 360].forEach((f, i) => k.tone(12.5, { f, type: 'sawtooth', dur: 1.2, gain: 0.03 / (i + 1), a: 0.02, hold: 0.3, filter: 'lowpass', f2: 900, verb: 0.3, pan: 0.3 }));
561
+ ballast(10.95, 13.3, 124);
562
+
563
+ // ---- pan 4 (12.9–13.5)
564
+ whip(12.9, 13.5);
565
+
566
+ // ---- E: takeaway. "More examples" 13.4 (len .3), arrow 13.6–13.85, "better guesses" letters 13.85 + i*0.04
567
+ ignite(13.4, { gain: 0.9, pan: 0 });
568
+ chase(13.6, 13.85, 4, 0);
569
+ k.withMinor(() => [...'better guesses'].forEach((c, i) => { if (c !== ' ') ignite(13.85 + i * 0.04, { gain: 0.32, click: i % 3 ? 0 : 0.5, pan: -0.5 + i * 0.075, tail: 0.1 }); }));
570
+ // resolve: as the last letters settle, the hum blooms (slow attack) into a chord on its own harmonics
571
+ const tEnd = 13.85 + 13 * 0.04 + 0.12; // last letter (13 = index of the final 's') is mid-flicker, settling
572
+ [240, 300, 360, 480].forEach((f, i) => k.tone(tEnd - 0.05, { f, type: i ? 'sine' : 'triangle', dur: D - tEnd + 0.05, gain: 0.075 - i * 0.01, a: 0.2, hold: 0.3, verb: 0.35, pan: (i - 1.5) * 0.25 }));
573
+ k.tone(tEnd - 0.05, { f: 120, type: 'sawtooth', dur: D - tEnd + 0.05, gain: 0.04, a: 0.1, hold: 0.2, filter: 'lowpass', f2: 600 });
574
+ }
575
+
576
+ registerStyle({
577
+ id: 'neon-sign',
578
+ name: 'Neon Sign at Night',
579
+ category: 'Light & Imaging',
580
+ description: 'Hand-bent glass neon on an old brick wall after rain: white-hot tube cores, saturated colour halos, painted-out blockout tube, clips and electrodes, light spilling across the brickwork and smearing in the wet pavement.',
581
+ motion: 'Tubes buzz on letter by letter with a stutter, one failing letter keeps flickering, and the puddle reflection shivers as drops land.',
582
+ fonts: ['400 150px "Gloria Hallelujah"', '400 40px "Poiret One"'],
583
+ frameT: 4.68,
584
+ duration: 5,
585
+ readsPixels: true,
586
+ explainerDuration: 15,
587
+ explainerNote: 'One long brick wall the camera whip-pans along: every beat is a new neon sign that buzzes on tube by tube, arrows are segmented chase-light tubes that ignite toward their target, numbers are neon digits re-lit as they change, and the emphasis is a blinking red tube with a ring.',
588
+ explainer(ctx, W, Hh, t) { neonExplainer(ctx, W, Hh, t); },
589
+ explainerSound(k) { neonSound(k); },
590
+ draw(ctx, W, Hh, t) {
591
+ const R = H.rng(1207), nz = H.makeNoise(77);
592
+ // ---- flicker of the failing letter ----
593
+ const hsh = (n) => { const v = Math.sin(n * 91.731 + 7.13) * 43758.5453; return v - Math.floor(v); };
594
+ // ignition: a tube buzzes on/off for a moment, then latches on
595
+ const ign = (t0, len = 0.34) => t < t0 ? 0 : t - t0 > len ? 1 : (hsh(Math.floor(t * 40) + t0 * 977) > 0.42 ? 1 : 0);
596
+ const ON = {
597
+ bulb: ign(0.12, 0.45), fil: ign(0.8),
598
+ rays: rayIdx => ign(1.05 + rayIdx * 0.08, 0.22),
599
+ letter: li => ign(1.45 + li * 0.16, 0.26),
600
+ cyan: ign(2.75, 0.4),
601
+ };
602
+ // the failing letter: tries to strike, then lives with intermittent dropouts
603
+ let flick = 0;
604
+ if (t >= 2.45) {
605
+ const r = hsh(Math.floor(t * 14) + 3);
606
+ flick = t < 3.1 ? (r > 0.55 ? 1 : r > 0.3 ? 0.3 : 0) : (r > 0.74 ? 0.3 : 1);
607
+ }
608
+ const hum = 0.97 + 0.03 * hsh(Math.floor(t * 30) + 11);
609
+
610
+ // ---------- geometry (masks) ----------
611
+ const bx = 268, by = 262, BR = 104;
612
+ const bulbPath = (x) => {
613
+ x.beginPath();
614
+ const nk = 0.42 * BR, ny = by + 1.28 * BR;
615
+ x.moveTo(bx - nk, ny);
616
+ x.bezierCurveTo(bx - nk, by + 0.95 * BR, bx - BR * 0.98, by + 0.62 * BR, bx - BR * 0.98, by);
617
+ x.arc(bx, by, BR * 0.98, Math.PI, 0);
618
+ x.bezierCurveTo(bx + BR * 0.98, by + 0.62 * BR, bx + nk, by + 0.95 * BR, bx + nk, ny);
619
+ x.stroke();
620
+ };
621
+ const threads = (x) => {
622
+ const ny = by + 1.28 * BR;
623
+ [[0, 0.44], [22, 0.40], [44, 0.34]].forEach(([dy, w]) => {
624
+ x.beginPath(); x.moveTo(bx - w * BR, ny + 16 + dy); x.lineTo(bx + w * BR, ny + 16 + dy); x.stroke();
625
+ });
626
+ x.beginPath(); x.moveTo(bx - 14, ny + 80); x.lineTo(bx + 14, ny + 80); x.stroke();
627
+ };
628
+ const filament = (x) => {
629
+ const y0 = by + 0.16 * BR, ny = by + 1.28 * BR;
630
+ x.beginPath();
631
+ x.moveTo(bx - 0.26 * BR, ny - 8); x.lineTo(bx - 0.30 * BR, y0);
632
+ const loops = 5, span = 0.6 * BR, rr = 11;
633
+ for (let i = 0; i <= 120; i++) {
634
+ const u = i / 120, th = u * loops * Math.PI * 2;
635
+ x.lineTo(bx - 0.30 * BR + u * span + Math.sin(th) * rr * 0.9, y0 - (1 - Math.cos(th)) * rr * 0.9);
636
+ }
637
+ x.lineTo(bx + 0.26 * BR, ny - 8);
638
+ x.stroke();
639
+ };
640
+ const rayAngles = [-158, -132, -108, -90, -72, -48, -22];
641
+ const rays = (x, which) => {
642
+ rayAngles.forEach((d, i) => {
643
+ if (which === 'lit' && (i === 5 || !ON.rays(i))) return;
644
+ if (which === 'off' && (i === 5 || ON.rays(i))) return;
645
+ if (which === 'dead' && i !== 5) return;
646
+ const a = d * Math.PI / 180;
647
+ x.beginPath();
648
+ x.moveTo(bx + Math.cos(a) * BR * 1.28, by + Math.sin(a) * BR * 1.28);
649
+ x.lineTo(bx + Math.cos(a) * BR * (i === 3 ? 1.72 : 1.58), by + Math.sin(a) * BR * (i === 3 ? 1.72 : 1.58));
650
+ x.stroke();
651
+ });
652
+ };
653
+
654
+ const word = 'big ideas', FLK = 7; // flickering letter index ('a')
655
+ const tx = 470, ty = 338;
656
+ const wordMask = (x, which) => {
657
+ x.font = '400 150px "Gloria Hallelujah"';
658
+ for (let i = 0; i < word.length; i++) {
659
+ if (word[i] === ' ') continue;
660
+ if ((which === 'flk') !== (i === FLK)) continue;
661
+ if (which === 'main' && !ON.letter(i)) continue;
662
+ if (which === 'off' && ON.letter(i)) continue;
663
+ const px = tx + x.measureText(word.slice(0, i)).width;
664
+ x.lineWidth = 3.2; x.fillText(word[i], px, ty); x.strokeText(word[i], px, ty);
665
+ }
666
+ };
667
+ const boxX = 586, boxY = 424, boxW = 432, boxH = 74;
668
+ const cyanMask = (x) => {
669
+ x.font = '400 40px "Poiret One"';
670
+ x.letterSpacing = '9px';
671
+ x.textAlign = 'center';
672
+ x.lineWidth = 4.2;
673
+ x.fillText('OPEN ALL NIGHT', boxX + boxW / 2 + 4, boxY + 51);
674
+ x.strokeText('OPEN ALL NIGHT', boxX + boxW / 2 + 4, boxY + 51);
675
+ x.lineWidth = 7;
676
+ x.beginPath(); x.roundRect(boxX, boxY, boxW, boxH, 22); x.stroke();
677
+ };
678
+
679
+ const mPink = mask(W, Hh, (x) => wordMask(x, 'main'));
680
+ const mFlk = mask(W, Hh, (x) => wordMask(x, 'flk'));
681
+ const mCyan = mask(W, Hh, cyanMask);
682
+ const mWarm = mask(W, Hh, (x) => { x.lineWidth = 8; if (ON.bulb) { bulbPath(x); threads(x); } rays(x, 'lit'); });
683
+ const mFil = mask(W, Hh, (x) => { x.lineWidth = 5.5; filament(x); });
684
+ const mDead = mask(W, Hh, (x) => { x.lineWidth = 8; rays(x, 'dead'); });
685
+ // every tube that is currently dark (rendered as unlit glass)
686
+ const mOff = mask(W, Hh, (x) => {
687
+ x.lineWidth = 8; if (!ON.bulb) { bulbPath(x); threads(x); } rays(x, 'off');
688
+ if (!ON.fil) { x.lineWidth = 5.5; filament(x); }
689
+ wordMask(x, 'off'); if (!ON.cyan) cyanMask(x);
690
+ });
691
+ const anyLit = { pink: [...word].some((c, i) => c !== ' ' && i !== FLK && ON.letter(i)), cyan: !!ON.cyan, warm: true, fil: !!ON.fil };
692
+
693
+ // blockout (painted black) tubes connecting the runs + electrode ends
694
+ const blockout = (x) => {
695
+ x.lineWidth = 9;
696
+ x.beginPath(); // bulb neck -> under word
697
+ x.moveTo(bx + 0.42 * BR, by + 1.28 * BR + 6);
698
+ x.bezierCurveTo(bx + 120, by + 170, 400, 392, 478, 360);
699
+ x.stroke();
700
+ x.beginPath(); // 'g' to 'i' of ideas, behind
701
+ x.moveTo(668, 372); x.bezierCurveTo(700, 398, 724, 392, 742, 330); x.stroke();
702
+ x.beginPath(); // end of ideas down to box
703
+ x.moveTo(1105, 330); x.bezierCurveTo(1112, 380, 1060, 410, 1018, 440); x.stroke();
704
+ };
705
+
706
+ // ---------- neon light layer (additive) ----------
707
+ const neon = H.layer(W, Hh), g = neon.getContext('2d');
708
+ g.fillStyle = '#000'; g.fillRect(0, 0, W, Hh);
709
+ g.globalCompositeOperation = 'lighter';
710
+ const groups = [
711
+ [mPink, COL.pink, anyLit.pink ? hum : 0], [mFlk, flick < 1 ? { core: [255, 150, 200], tube: [190, 25, 105], glow: [200, 10, 90] } : COL.pink, flick],
712
+ [mCyan, COL.cyan, anyLit.cyan ? hum : 0], [mWarm, COL.warm, (ON.bulb || ON.rays(0)) ? hum : 0], [mFil, COL.pink, ON.fil ? 0.95 * hum : 0],
713
+ ].filter((g0) => g0[2] > 0);
714
+ // light map for the wall (very blurred colour)
715
+ const light = H.layer(W / 4, Hh / 4), lx = light.getContext('2d');
716
+ lx.fillStyle = '#000'; lx.fillRect(0, 0, W / 4, Hh / 4);
717
+ lx.globalCompositeOperation = 'lighter';
718
+ for (const [m, c, k] of groups) {
719
+ const gs = colorize(m, c.glow);
720
+ addBlur(g, gs, 70, 0.55 * k, W, Hh);
721
+ addBlur(g, gs, 26, 0.75 * k, W, Hh);
722
+ addBlur(g, colorize(m, c.tube), 7, 0.9 * k, W, Hh);
723
+ g.globalAlpha = k; g.drawImage(colorize(m, c.tube), 0, 0);
724
+ const core = colorize(erode(m, 2), c.core);
725
+ g.globalAlpha = Math.min(1, k * k * 1.1); g.drawImage(core, 0, 0);
726
+ addBlur(g, core, 2.5, 0.6 * k, W, Hh);
727
+ // wall light contributions
728
+ lx.globalAlpha = k; lx.filter = 'blur(40px)'; lx.drawImage(gs, 0, 0, W / 4, Hh / 4); lx.filter = 'blur(18px)'; lx.drawImage(gs, 0, 0, W / 4, Hh / 4);
729
+ lx.filter = 'blur(7px)'; lx.globalAlpha = 0.8 * k; lx.drawImage(gs, 0, 0, W / 4, Hh / 4);
730
+ }
731
+ g.globalAlpha = 1;
732
+
733
+ // ---------- wall + ground per-pixel pass ----------
734
+ const Ld = lx.getImageData(0, 0, W / 4, Hh / 4).data, LW = W / 4, LH = Hh / 4;
735
+ const sampleL = (x, y, o) => {
736
+ const fx = H.clamp(x / 4 - 0.5, 0, LW - 1.001), fy = H.clamp(y / 4 - 0.5, 0, LH - 1.001);
737
+ const ix = fx | 0, iy = fy | 0, ax = fx - ix, ay = fy - iy, i = (iy * LW + ix) * 4 + o;
738
+ return (Ld[i] * (1 - ax) + Ld[i + 4] * ax) * (1 - ay) + (Ld[i + LW * 4] * (1 - ax) + Ld[i + LW * 4 + 4] * ax) * ay;
739
+ };
740
+ // low-res stain field
741
+ const SW = 161, SH = 91, stain = new Float32Array(SW * SH);
742
+ for (let y = 0; y < SH; y++) for (let x = 0; x < SW; x++) stain[y * SW + x] = nz.fbm2(x * 0.09, y * 0.09, 4);
743
+ const sampleS = (x, y) => {
744
+ const fx = H.clamp(x / 8, 0, SW - 1.01), fy = H.clamp(y / 8, 0, SH - 1.01), ix = fx | 0, iy = fy | 0, ax = fx - ix, ay = fy - iy, i = iy * SW + ix;
745
+ return (stain[i] * (1 - ax) + stain[i + 1] * ax) * (1 - ay) + (stain[i + SW] * (1 - ax) + stain[i + SW + 1] * ax) * ay;
746
+ };
747
+ const brickCols = [[122, 56, 40], [104, 46, 34], [138, 72, 50], [92, 50, 40], [116, 64, 48], [70, 38, 32]];
748
+ const rowH = 30, brW = 92, mort = 5;
749
+ const bhash = (a, b) => { const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5453; return s - Math.floor(s); };
750
+ const img = ctx.createImageData(W, Hh), d = img.data;
751
+ const amb = [16, 19, 30];
752
+ for (let y = 0; y < Hh; y++) {
753
+ const row = Math.floor(y / rowH), ly = y - row * rowH, off = (row % 2) * (brW / 2) + (row % 3 === 0 ? 7 : 0);
754
+ for (let x = 0; x < W; x++) {
755
+ const i = (y * W + x) * 4;
756
+ const lr = sampleL(x, y, 0), lg = sampleL(x, y, 1), lb = sampleL(x, y, 2);
757
+ const grain = R() - 0.5;
758
+ if (y < WALL_BASE) {
759
+ const col = Math.floor((x + off) / brW), lxp = x + off - col * brW;
760
+ const h1 = bhash(row, col), h2 = bhash(col + 9, row + 3);
761
+ const st = sampleS(x, y);
762
+ let ar, ag, ab, shade = 1;
763
+ // chipped / irregular mortar edges
764
+ const edgeJit = (bhash(x >> 2, row) - 0.5) * 2.2 + (h2 - 0.5) * 1.5;
765
+ const inMortar = ly < mort + edgeJit * 0.5 || lxp < mort + edgeJit * 0.6;
766
+ if (inMortar) {
767
+ ar = 96; ag = 90; ab = 84; shade = 0.5 + grain * 0.35;
768
+ } else {
769
+ const bc = brickCols[Math.floor(h1 * brickCols.length)];
770
+ const k = 0.85 + h2 * 0.3 + st * 0.35;
771
+ ar = bc[0] * k; ag = bc[1] * k; ab = bc[2] * k;
772
+ // pitted surface
773
+ shade = 1 + grain * 0.32 + (bhash(x * 0.37 | 0, y * 0.41 | 0) > 0.985 ? -0.45 : 0);
774
+ // bevel: light comes from where the sign is (gradient of light map)
775
+ const gL = (sampleL(x, y - 10, 0) + sampleL(x, y - 10, 2)) - (sampleL(x, y + 10, 0) + sampleL(x, y + 10, 2));
776
+ const dir = H.clamp(gL / 60, -1, 1);
777
+ const top = ly - mort, bot = rowH - ly;
778
+ if (top < 3) shade *= 1 + 0.55 * dir;
779
+ else if (bot < 3) shade *= 1 - 0.45 * dir;
780
+ if (lxp - mort < 2) shade *= 0.85;
781
+ }
782
+ // soot streaks darken lower wall a bit
783
+ const soot = 1 - H.smooth(380, WALL_BASE, y) * 0.25;
784
+ shade *= soot;
785
+ d[i] = ar * shade * (amb[0] + lr * 3.4) / 255;
786
+ d[i + 1] = ag * shade * (amb[1] + lg * 3.4) / 255;
787
+ d[i + 2] = ab * shade * (amb[2] + lb * 3.4) / 255;
788
+ } else {
789
+ // wet concrete sidewalk
790
+ const gy = y - WALL_BASE;
791
+ let base = 26 + grain * 14 + sampleS((x * 1.7) % 1280, (y * 3) % 720) * 10;
792
+ if (gy < 12) base = 52 + grain * 18 - gy * 1.2; // footing/curb lip
793
+ const fall = 1 / (1 + gy * 0.012);
794
+ d[i] = base * 0.9 + lr * 0.9 * fall;
795
+ d[i + 1] = base * 0.95 + lg * 0.9 * fall;
796
+ d[i + 2] = base * 1.1 + lb * 0.9 * fall;
797
+ }
798
+ d[i + 3] = 255;
799
+ }
800
+ }
801
+ ctx.putImageData(img, 0, 0);
802
+
803
+ // ---------- physical hardware (unlit) ----------
804
+ ctx.save();
805
+ // cable from the bulb base down the wall to transformer
806
+ ctx.strokeStyle = '#0b0a0c'; ctx.lineWidth = 4; ctx.lineCap = 'round';
807
+ ctx.beginPath(); ctx.moveTo(bx, by + 1.28 * BR + 84); ctx.bezierCurveTo(bx + 6, 470, bx - 30, 520, bx - 40, 560); ctx.lineTo(bx - 42, 566); ctx.stroke();
808
+ ctx.strokeStyle = 'rgba(255,150,90,0.25)'; ctx.lineWidth = 1; ctx.stroke();
809
+ // transformer box
810
+ const tbx = bx - 92, tby = 546;
811
+ ctx.fillStyle = '#121216'; ctx.fillRect(tbx, tby, 110, 46);
812
+ const tg = ctx.createLinearGradient(tbx, tby, tbx, tby + 46);
813
+ tg.addColorStop(0, 'rgba(255,170,90,0.35)'); tg.addColorStop(0.1, 'rgba(255,170,90,0.04)'); tg.addColorStop(1, 'rgba(0,0,0,0.3)');
814
+ ctx.fillStyle = tg; ctx.fillRect(tbx, tby, 110, 46);
815
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(tbx + 2, tby + 46, 110, 4);
816
+ ctx.fillStyle = 'rgba(255,200,150,0.18)'; ctx.font = '600 8px monospace'; ctx.fillText('15kV 30mA', tbx + 10, tby + 28);
817
+ // blockout tubes: painted black glass
818
+ const mBlk = mask(W, Hh, blockout);
819
+ ctx.drawImage(colorize(mBlk, [14, 12, 14]), 0, 0);
820
+ ctx.globalAlpha = 0.5; ctx.drawImage(colorize(erode(mBlk, 3), [120, 70, 90]), -1, -2); ctx.globalAlpha = 1;
821
+ // dead tube ray (unlit glass, catches some colour)
822
+ ctx.drawImage(colorize(mDead, COL.warm.dark), 0, 0);
823
+ ctx.globalAlpha = 0.55; ctx.drawImage(colorize(erode(mDead, 3), [200, 160, 110]), -1, -1); ctx.globalAlpha = 1;
824
+ // all tubes not lit right now: pale unlit glass catching a little street light
825
+ ctx.drawImage(colorize(mOff, [34, 30, 38]), 0, 0);
826
+ ctx.globalAlpha = 0.22; ctx.drawImage(colorize(erode(mOff, 2), [170, 150, 170]), -1, -2); ctx.globalAlpha = 1;
827
+ // unlit glass under the flickering letter
828
+ ctx.drawImage(colorize(mFlk, COL.pink.dark), 0, 0);
829
+ ctx.globalAlpha = 0.18; ctx.drawImage(colorize(erode(mFlk, 2), [190, 110, 150]), -1, -2); ctx.globalAlpha = 1;
830
+ // electrode housings where runs dive into the wall
831
+ const electrode = (x, y, a) => {
832
+ ctx.save(); ctx.translate(x, y); ctx.rotate(a);
833
+ ctx.fillStyle = '#0d0c0f'; ctx.beginPath(); ctx.roundRect(-7, -6, 22, 12, 4); ctx.fill();
834
+ ctx.fillStyle = 'rgba(255,255,255,0.18)'; ctx.fillRect(-4, -5, 15, 2);
835
+ ctx.restore();
836
+ };
837
+ electrode(478, 360, -0.4); electrode(1105, 330, 1.5); electrode(1018, 440, 2.5); electrode(742, 330, -1.4); electrode(668, 372, 0.6);
838
+ ctx.restore();
839
+
840
+ // shadow of tubes on wall (tubes stand ~4cm off the wall, lit by each other + street glow)
841
+ ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.55;
842
+ const shadowM = mask(W, Hh, (x) => { x.drawImage(mPink, 0, 0); x.drawImage(mFlk, 0, 0); x.drawImage(mCyan, 0, 0); x.drawImage(mWarm, 0, 0); });
843
+ const sh = colorize(shadowM, [20, 10, 20]);
844
+ ctx.filter = 'blur(3px)'; ctx.drawImage(sh, 5, 9); ctx.filter = 'none';
845
+ ctx.restore();
846
+
847
+ // ---------- add neon light ----------
848
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
849
+ ctx.drawImage(neon, 0, 0);
850
+ ctx.restore();
851
+
852
+ // mounting clips (glass tube supports) over the tubes
853
+ const clips = [[520, 300], [610, 270], [850, 262], [1000, 300], [1080, 318], [586, 462], [1018, 461], [800, 424], [bx - BR * 0.98, by - 10], [bx + BR * 0.98, by - 10], [bx, by - BR * 0.98]];
854
+ for (const [cx, cy] of clips) {
855
+ ctx.fillStyle = 'rgba(30,28,34,0.92)'; ctx.beginPath(); ctx.roundRect(cx - 2.5, cy - 8, 5, 16, 2); ctx.fill();
856
+ ctx.fillStyle = 'rgba(255,255,255,0.28)'; ctx.fillRect(cx - 1.5, cy - 7, 1, 14);
857
+ }
858
+
859
+ // ---------- wet-pavement reflection ----------
860
+ const soft = H.layer(W, Hh), sfx = soft.getContext('2d');
861
+ sfx.filter = 'blur(2.5px)'; sfx.drawImage(neon, 0, 0); sfx.filter = 'none';
862
+ const refl = H.layer(W, Hh), rx = refl.getContext('2d');
863
+ const squash = 0.36, srcTop = 40;
864
+ // rippled mirror: 1px rows with wobbling offsets
865
+ for (let y = WALL_BASE + 4; y < Hh; y++) {
866
+ const gy = y - WALL_BASE;
867
+ const sy = WALL_BASE - 14 - gy / squash;
868
+ if (sy < srcTop) break;
869
+ const off = nz.n2(y * 0.035, t * 1.2) * (4 + gy * 0.22) + nz.n2(y * 0.12, 9 + t * 2) * (1 + gy * 0.05);
870
+ rx.drawImage(soft, 0, sy, W, 1 / squash, off, y, W, 1);
871
+ }
872
+ // vertical streak smear: squash rows hard, scale back up
873
+ const smear = H.layer(W, 12), smx = smear.getContext('2d');
874
+ smx.filter = 'blur(1.5px)'; smx.drawImage(refl, 0, WALL_BASE, W, Hh - WALL_BASE, 0, 0, W, 12);
875
+ // puddle mask
876
+ const pud = H.layer(W, Hh), px = pud.getContext('2d');
877
+ const pimg = px.createImageData(W, Hh - WALL_BASE), pd = pimg.data;
878
+ for (let y = 0; y < Hh - WALL_BASE; y++) for (let x = 0; x < W; x++) {
879
+ const v = nz.fbm2(x * 0.0045, y * 0.02 + 20, 3);
880
+ const a = H.smooth(-0.08, 0.1, v);
881
+ const j = (y * W + x) * 4; pd[j] = pd[j + 1] = pd[j + 2] = 255; pd[j + 3] = a * 255 * H.smooth(3, 12, y);
882
+ }
883
+ px.putImageData(pimg, 0, WALL_BASE);
884
+ // diffuse wet sheen everywhere (streaks), mirror only in puddles
885
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
886
+ ctx.globalAlpha = 0.95; ctx.drawImage(smear, 0, 0, W, 12, 0, WALL_BASE + 4, W, Hh - WALL_BASE - 4);
887
+ const rm = H.layer(W, Hh), rmx = rm.getContext('2d');
888
+ rmx.drawImage(refl, 0, 0);
889
+ rmx.globalCompositeOperation = 'destination-in'; rmx.drawImage(pud, 0, 0);
890
+ ctx.globalAlpha = 0.7; ctx.drawImage(rm, 0, 0);
891
+ ctx.restore();
892
+ // concrete aggregate speckle catching the light
893
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
894
+ for (let k = 0; k < 900; k++) {
895
+ const x = R() * W, y = WALL_BASE + 14 + Math.pow(R(), 0.8) * (Hh - WALL_BASE - 14);
896
+ const near = x < 520 ? [255, 170, 80] : x < 1100 ? [255, 60, 150] : [60, 200, 255];
897
+ ctx.fillStyle = H.rgba(near, R() * 0.18);
898
+ ctx.fillRect(x, y, 1 + R(), 1);
899
+ }
900
+ ctx.restore();
901
+ // expansion joint
902
+ ctx.strokeStyle = 'rgba(0,0,0,0.55)'; ctx.lineWidth = 2;
903
+ ctx.beginPath(); ctx.moveTo(150, Hh); ctx.lineTo(205, WALL_BASE + 12); ctx.stroke();
904
+ ctx.beginPath(); ctx.moveTo(1210, Hh); ctx.lineTo(1120, WALL_BASE + 12); ctx.stroke();
905
+
906
+ // rain drops / ripples rings in puddles
907
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
908
+ for (let k = 0; k < 9; k++) {
909
+ const x = R.range(80, 1200), y = R.range(WALL_BASE + 30, Hh - 10), ph = (t * 0.8 + R()) % 1;
910
+ ctx.strokeStyle = `rgba(255,140,200,${0.18 * (1 - ph)})`; ctx.lineWidth = 1;
911
+ ctx.beginPath(); ctx.ellipse(x, y, 4 + ph * 26, (4 + ph * 26) * 0.22, 0, 0, 7); ctx.stroke();
912
+ }
913
+ ctx.restore();
914
+
915
+ // ---------- atmosphere: bloom haze, vignette, grain ----------
916
+ ctx.save(); ctx.globalCompositeOperation = 'screen'; ctx.globalAlpha = 0.35;
917
+ ctx.drawImage(blurSmall(neon, 120, 0.0625), 0, 0, W, Hh); ctx.restore();
918
+ H.vignette(ctx, W, Hh, 0.6, '4,2,10');
919
+ H.grain(ctx, W, Hh, 0.05, 31 + Math.floor(t * 24), false);
920
+ },
921
+ });
922
+ }