@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,635 @@
1
+ {
2
+ // ---------- long-exposure light painting ----------
3
+ const hsl = (h, s, l) => {
4
+ h = ((h % 360) + 360) % 360 / 360;
5
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
6
+ const f = (t) => { t = (t + 1) % 1; return t < 1 / 6 ? p + (q - p) * 6 * t : t < 0.5 ? q : t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p; };
7
+ return [f(h + 1 / 3) * 255, f(h) * 255, f(h - 1 / 3) * 255];
8
+ };
9
+ function blurSmall(src, px, s) {
10
+ const w = Math.ceil(src.width * s), h = Math.ceil(src.height * s), c = H.layer(w, h), x = c.getContext('2d');
11
+ x.filter = `blur(${Math.max(0.5, px * s)}px)`; x.drawImage(src, 0, 0, w, h); return c;
12
+ }
13
+
14
+ // light-tube ribbon: an RGB tube swept round an ellipse and twisted as it goes (shared by the still and the explainer)
15
+ function lightRibbon(rb, nz, { cx0, cy0, rx, ry, skew = 30, tyk = 0.42, len = 112, twists = 3.4, hue0 = 175, hueSpan = 150, RN = 900, stri = 10, gain = 1, prog }) {
16
+ let pv = null, tip = null;
17
+ for (let i = 0; i < RN; i++) {
18
+ const u = i / (RN - 1);
19
+ if (u > prog) break;
20
+ const a = Math.PI * (0.62 + u * 1.72);
21
+ const cx = cx0 + Math.cos(a) * rx + nz.n2(u * 6, 1) * 6, cy = cy0 + Math.sin(a) * ry - Math.cos(a) * skew + nz.n2(u * 6, 4) * 6;
22
+ const tw = Math.cos(u * Math.PI * twists + 0.4);
23
+ const ln = len * tw;
24
+ const tx = -Math.sin(a), ty = Math.cos(a) * tyk;
25
+ const nl = Math.hypot(tx, ty), nx = -ty / nl, ny = tx / nl;
26
+ const cur = [cx - nx * ln / 2, cy - ny * ln / 2, cx + nx * ln / 2, cy + ny * ln / 2];
27
+ if (pv) {
28
+ const hue = hue0 + u * hueSpan;
29
+ const dens = H.clamp(0.13 / (Math.abs(tw) + 0.15), 0.12, 0.75) * gain;
30
+ const gr = rb.createLinearGradient(cur[0], cur[1], cur[2], cur[3]);
31
+ gr.addColorStop(0, H.rgba(hsl(hue, 1, 0.6), dens)); gr.addColorStop(0.5, H.rgba(hsl(hue + 45, 1, 0.55), dens * 0.55)); gr.addColorStop(1, H.rgba(hsl(hue + 95, 1, 0.6), dens));
32
+ rb.fillStyle = gr; rb.beginPath(); rb.moveTo(pv[0], pv[1]); rb.lineTo(pv[2], pv[3]); rb.lineTo(cur[2], cur[3]); rb.lineTo(cur[0], cur[1]); rb.closePath(); rb.fill();
33
+ // individual LEDs leave fine parallel striations
34
+ for (let k = 1; k < stri; k++) {
35
+ const f = k / stri, hk = hue + f * 95;
36
+ rb.strokeStyle = H.rgba(hsl(hk, 1, 0.7), Math.min(0.5, dens * 0.55)); rb.lineWidth = 0.9;
37
+ rb.beginPath(); rb.moveTo(pv[0] + (pv[2] - pv[0]) * f, pv[1] + (pv[3] - pv[1]) * f); rb.lineTo(cur[0] + (cur[2] - cur[0]) * f, cur[1] + (cur[3] - cur[1]) * f); rb.stroke();
38
+ }
39
+ // hot tube ends trace the edges
40
+ rb.strokeStyle = H.rgba(hsl(hue, 1, 0.8), 0.5 * gain); rb.lineWidth = 1.8; rb.beginPath(); rb.moveTo(pv[0], pv[1]); rb.lineTo(cur[0], cur[1]); rb.stroke();
41
+ rb.strokeStyle = H.rgba(hsl(hue + 95, 1, 0.8), 0.5 * gain); rb.beginPath(); rb.moveTo(pv[2], pv[3]); rb.lineTo(cur[2], cur[3]); rb.stroke();
42
+ }
43
+ pv = cur;
44
+ if (prog > 0 && prog < 1) tip = [(cur[0] + cur[2]) / 2, (cur[1] + cur[3]) / 2, hsl(hue0 + u * hueSpan, 1, 0.7), cur];
45
+ }
46
+ return tip;
47
+ }
48
+
49
+ // ghost of the painter (moved during the exposure): silhouette G, trail-lit colour Gc and a rim-lit edge Rim. Static, cached.
50
+ let ghostCache = null;
51
+ function getGhost(W, Hh) {
52
+ if (ghostCache && ghostCache.W === W && ghostCache.H === Hh) return ghostCache;
53
+ const G = H.layer(W, Hh), gx = G.getContext('2d');
54
+ const ghost = (ox, oy, a) => {
55
+ gx.save(); gx.translate(ox, oy); gx.globalAlpha = a;
56
+ gx.fillStyle = 'rgb(255,255,255)'; gx.strokeStyle = 'rgb(255,255,255)'; gx.lineCap = 'round'; gx.lineJoin = 'round';
57
+ gx.beginPath(); gx.ellipse(1012, 330, 19, 24, 0.1, 0, 7); gx.fill(); // head
58
+ gx.beginPath(); gx.moveTo(1004, 352); gx.lineTo(1020, 352); gx.lineTo(1022, 362); gx.lineTo(1002, 362); gx.fill(); // neck
59
+ gx.beginPath(); gx.moveTo(976, 372); gx.quadraticCurveTo(1012, 356, 1048, 374); // shoulders
60
+ gx.quadraticCurveTo(1054, 420, 1046, 480); gx.lineTo(1040, 500); gx.lineTo(984, 500); gx.lineTo(976, 470);
61
+ gx.quadraticCurveTo(970, 420, 976, 372); gx.fill();
62
+ gx.lineWidth = 22; gx.beginPath(); gx.moveTo(996, 500); gx.lineTo(990, 580); gx.lineTo(984, 650); gx.stroke(); // legs
63
+ gx.beginPath(); gx.moveTo(1028, 500); gx.lineTo(1036, 580); gx.lineTo(1046, 650); gx.stroke();
64
+ gx.lineWidth = 15; gx.beginPath(); gx.moveTo(982, 380); gx.quadraticCurveTo(944, 330, 912, 272); gx.stroke(); // raised arm
65
+ gx.beginPath(); gx.moveTo(1044, 384); gx.quadraticCurveTo(1062, 440, 1052, 492); gx.stroke(); // other arm
66
+ gx.restore();
67
+ };
68
+ ghost(0, 0, 0.6); ghost(-8, 2, 0.25);
69
+ // lit by the coloured trails from the left/front, cool rim + warm wand light from above
70
+ const Gc = H.layer(W, Hh), gcx = Gc.getContext('2d');
71
+ gcx.drawImage(G, 0, 0); gcx.globalCompositeOperation = 'source-in';
72
+ const gl = gcx.createLinearGradient(960, 260, 1060, 640);
73
+ gl.addColorStop(0, 'rgb(150,110,120)'); gl.addColorStop(0.4, 'rgb(90,70,140)'); gl.addColorStop(1, 'rgb(30,30,60)');
74
+ gcx.fillStyle = gl; gcx.fillRect(0, 0, W, Hh);
75
+ // rim light: the edge facing the light trails (left / top) catches cyan-magenta spill, so the figure reads as a body
76
+ const Rim = H.layer(W, Hh), rx = Rim.getContext('2d');
77
+ rx.drawImage(G, 0, 0); rx.globalCompositeOperation = 'destination-out'; rx.drawImage(G, 6, 3);
78
+ rx.globalCompositeOperation = 'source-in';
79
+ const rg = rx.createLinearGradient(900, 260, 1060, 660);
80
+ rg.addColorStop(0, 'rgb(255,200,170)'); rg.addColorStop(0.35, 'rgb(150,120,255)'); rg.addColorStop(0.7, 'rgb(70,160,220)'); rg.addColorStop(1, 'rgb(20,40,70)');
81
+ rx.fillStyle = rg; rx.fillRect(0, 0, W, Hh);
82
+ // pre-blur once and keep only the figure's bounding box, so each frame is three small, filter-free draws
83
+ const BX = 860, BY = 236, BW = 240, BH = 450;
84
+ const crop = (src, b) => { const c = H.layer(BW, BH), x = c.getContext('2d'); x.filter = `blur(${b}px)`; x.drawImage(src, -BX, -BY); return c; };
85
+ return (ghostCache = { W, H: Hh, BX, BY, G: crop(G, 2), Gc: crop(Gc, 2.5), Rim: crop(Rim, 1.2) });
86
+ }
87
+ const drawGhost = (x, gh, dx, a) => {
88
+ if (a <= 0) return;
89
+ x.save();
90
+ x.globalCompositeOperation = 'multiply'; x.globalAlpha = 0.45 * a; x.drawImage(gh.G, gh.BX + dx, gh.BY);
91
+ x.globalCompositeOperation = 'lighter'; x.globalAlpha = 0.32 * a; x.drawImage(gh.Gc, gh.BX + dx, gh.BY);
92
+ x.globalAlpha = 0.5 * a; x.drawImage(gh.Rim, gh.BX + dx, gh.BY);
93
+ x.restore();
94
+ };
95
+ // ground spill mask: fades away from the horizon and breaks up in patches of wet grass (cached)
96
+ let spillMask = null;
97
+ function getSpillMask(SW, SH) {
98
+ if (spillMask && spillMask.width === SW && spillMask.height === SH) return spillMask;
99
+ const m = H.layer(SW, SH), x = m.getContext('2d'), id = x.createImageData(SW, SH), nz = H.makeNoise(4242);
100
+ for (let y = 0; y < SH; y++) for (let q = 0; q < SW; q++) {
101
+ const v = Math.pow(1 - y / SH, 1.25) * H.clamp(0.5 + 0.75 * nz.fbm2(q * 0.045, y * 0.3, 3) + 0.1, 0, 1);
102
+ const i = (y * SW + q) * 4; id.data[i] = id.data[i + 1] = id.data[i + 2] = 255; id.data[i + 3] = v * 255;
103
+ }
104
+ x.putImageData(id, 0, 0);
105
+ return (spillMask = m);
106
+ }
107
+
108
+ // static caches: eroded lettering cores (per word) and finished ribbons (per beat)
109
+ const lpCores = new Map(), lpRibbons = {};
110
+
111
+ // ================= EXPLAINER: a live long exposure; whip-pans smear everything into streaks between beats =================
112
+ let lpBg = null;
113
+ function getLpBg(W, Hh) {
114
+ if (lpBg) return lpBg;
115
+ const BW = W * 3, HZ = 560, c = H.layer(BW, Hh), x = c.getContext('2d'), R = H.rng(3131), nz = H.makeNoise(3132);
116
+ const sky = x.createLinearGradient(0, 0, 0, HZ); sky.addColorStop(0, '#02030a'); sky.addColorStop(0.6, '#060a1c'); sky.addColorStop(1, '#151a2e');
117
+ x.fillStyle = sky; x.fillRect(0, 0, BW, HZ + 2);
118
+ for (let k = 0; k < 1500; k++) {
119
+ const sx = R() * BW, sy = Math.pow(R(), 1.3) * (HZ - 30), m = Math.pow(R(), 5);
120
+ x.fillStyle = H.rgba(R.pick([[255, 255, 255], [200, 215, 255], [255, 230, 200]]), (0.25 + m * 0.75) * (1 - H.smooth(HZ - 160, HZ, sy) * 0.8));
121
+ x.beginPath(); x.arc(sx, sy, 0.6 + m * 1.3, 0, 7); x.fill();
122
+ }
123
+ x.fillStyle = '#04050a'; x.beginPath(); x.moveTo(0, HZ);
124
+ for (let px = 0; px <= BW; px += 4) x.lineTo(px, HZ - 14 - 16 * Math.sin(px * 0.004 + 1.2) - Math.max(0, nz.fbm2(px * 0.03, 2.2, 3)) * 46 - Math.max(0, nz.n2(px * 0.08, 5)) * 10);
125
+ x.lineTo(BW, Hh); x.lineTo(0, Hh); x.fill();
126
+ for (let k = 0; k < 7000; k++) { const gx = R() * BW, gy2 = HZ + Math.pow(R(), 0.7) * (Hh - HZ); x.strokeStyle = `rgba(30,36,48,${R.range(0.2, 0.6)})`; x.beginPath(); x.moveTo(gx, gy2); x.lineTo(gx + R.range(-3, 3), gy2 - R.range(3, 8)); x.stroke(); }
127
+ x.globalCompositeOperation = 'lighter';
128
+ for (let k = 0; k < 26; k++) { const bx = R() * BW, by = HZ - 10 - R() * 30, r = 8 + R() * 16, cc = R.pick([[255, 170, 90], [255, 200, 120], [170, 210, 255]]);
129
+ const g = x.createRadialGradient(bx, by, 0, bx, by, r); g.addColorStop(0, H.rgba(cc, 0.16)); g.addColorStop(0.85, H.rgba(cc, 0.28)); g.addColorStop(1, H.rgba(cc, 0)); x.fillStyle = g; x.beginPath(); x.arc(bx, by, r, 0, 7); x.fill(); }
130
+ return (lpBg = c);
131
+ }
132
+ function lpCat(x, cx, cy, s) {
133
+ x.beginPath(); x.moveTo(cx - s * 0.95, cy - s * 0.1); x.lineTo(cx - s * 0.8, cy - s * 1.05); x.lineTo(cx - s * 0.35, cy - s * 0.72);
134
+ x.quadraticCurveTo(cx, cy - s * 0.82, cx + s * 0.35, cy - s * 0.72); x.lineTo(cx + s * 0.8, cy - s * 1.05); x.lineTo(cx + s * 0.95, cy - s * 0.1);
135
+ 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);
136
+ x.moveTo(cx - s * 0.45, cy - s * 0.14); x.lineTo(cx - s * 0.2, cy - s * 0.1); x.moveTo(cx + s * 0.45, cy - s * 0.14); x.lineTo(cx + s * 0.2, cy - s * 0.1);
137
+ x.moveTo(cx - s * 0.38, cy + s * 0.18); x.lineTo(cx - s * 1.05, cy + s * 0.06); x.moveTo(cx + s * 0.38, cy + s * 0.18); x.lineTo(cx + s * 1.05, cy + s * 0.06);
138
+ x.moveTo(cx - s * 0.08, cy + s * 0.12); x.lineTo(cx + s * 0.08, cy + s * 0.12); x.lineTo(cx, cy + s * 0.22);
139
+ }
140
+ function lpDog(x, cx, cy, s) {
141
+ x.beginPath(); x.moveTo(cx - s * 0.55, cy - s * 0.7); x.quadraticCurveTo(cx, cy - s * 0.95, cx + s * 0.55, cy - s * 0.7);
142
+ x.bezierCurveTo(cx + s * 0.78, cy - s * 0.1, cx + s * 0.55, cy + s * 0.78, cx, cy + s * 0.8); 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);
143
+ 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);
144
+ 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);
145
+ 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); 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);
146
+ 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);
147
+ }
148
+ const LED = { '0': '01110100011001110101110011000101110', '1': '00100011000010000100001000010001110', '2': '01110100010000100010001000100011111', '3': '11110000010000101110000010000111110',
149
+ '4': '00010001100101010010111110001000010', '5': '11111100001111000001000011000101110', '6': '00110010001000011110100011000101110', '7': '11111000010001000100010000100001000',
150
+ '8': '01110100011000101110100011000101110', '9': '01110100011000101111000010001001100', '%': '11001110100001000100010000101110011', ',': '00000000000000000000001100010001000' };
151
+
152
+ function lightExplainer(ctx, W, Hh, t) {
153
+ const sm = H.smooth, E = H.ease, Rn = H.rng(31 + Math.floor(t * 30));
154
+ // beats live in separate exposures; whip-pans between them: [start, end-of-pan]
155
+ const pans = [[2.05, 2.5], [5.9, 6.35], [10.3, 10.75], [12.85, 13.3]];
156
+ let beat = 0; pans.forEach(([a, b]) => { if (t >= (a + b) / 2) beat++; });
157
+ const panP = pans.map(([a, b]) => sm(a, b, t));
158
+ let camX = 0; panP.forEach((p) => camX += p * 560);
159
+ const smear = pans.reduce((m, [a, b]) => Math.max(m, t > a && t < b ? Math.sin(Math.PI * (t - a) / (b - a)) : 0), 0);
160
+ const Ccol = H.layer(W, Hh), cc = Ccol.getContext('2d'), Ccore = H.layer(W, Hh), co = Ccore.getContext('2d');
161
+ [cc, co].forEach((x) => { x.globalCompositeOperation = 'lighter'; x.lineCap = 'round'; x.lineJoin = 'round'; });
162
+ let tip = null;
163
+ const C = { cyan: [60, 220, 255], pink: [255, 70, 190], amber: [255, 180, 60], violet: [170, 90, 255], red: [255, 50, 40], green: [80, 255, 120], white: [235, 240, 255] };
164
+ const pen = (id, pts, c0, c1, w, prog, gaps = 0) => {
165
+ if (prog <= 0) return; const jn = H.makeNoise(id); let prev = null;
166
+ for (let i = 0; i < pts.length; i++) {
167
+ const u = i / (pts.length - 1); if (u > prog) break;
168
+ const x = pts[i][0] + jn.n2(u * 9, 0.5) * 2.2, y = pts[i][1] + jn.n2(u * 9, 7.5) * 2.2;
169
+ if (prev && !(gaps && jn.n2(u * 6, 11) > 0.45)) {
170
+ const sp = 0.65 + 0.35 * jn.n2(u * 4, 3), c = H.mix(c0, c1, u);
171
+ cc.strokeStyle = H.rgba(c, 0.6 * sp); cc.lineWidth = w * (0.8 + sp * 0.4); cc.beginPath(); cc.moveTo(...prev); cc.lineTo(x, y); cc.stroke();
172
+ co.strokeStyle = H.rgba(H.mix(c, [255, 255, 255], 0.75), 0.55 * sp); co.lineWidth = w * 0.38; co.beginPath(); co.moveTo(...prev); co.lineTo(x, y); co.stroke();
173
+ }
174
+ prev = [x, y];
175
+ }
176
+ if (prog < 1 && prev) tip = [prev[0], prev[1], H.mix(c0, c1, prog)];
177
+ };
178
+ const line = (a, b, n = 40) => Array.from({ length: n }, (_, i) => [a[0] + (b[0] - a[0]) * i / (n - 1), a[1] + (b[1] - a[1]) * i / (n - 1)]);
179
+ const quad = (a, q, b, n = 50) => Array.from({ length: n }, (_, i) => { const u = i / (n - 1); return [(1 - u) * (1 - u) * a[0] + 2 * (1 - u) * u * q[0] + u * u * b[0], (1 - u) * (1 - u) * a[1] + 2 * (1 - u) * u * q[1] + u * u * b[1]]; });
180
+ const loop = (cx, cy, r, n = 90) => Array.from({ length: n }, (_, i) => { const a = i / (n - 1) * Math.PI * 4.3 - 1.2, rr = r * (1 + 0.07 * Math.sin(a * 0.5)) + i * 0.02; return [cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]; });
181
+ const arrowPen = (id, pts, c0, c1, w, prog) => {
182
+ pen(id, pts, c0, c1, w, prog);
183
+ if (prog >= 1) { const e = pts[pts.length - 1], q = pts[pts.length - 4], a = Math.atan2(e[1] - q[1], e[0] - q[0]);
184
+ pen(id + 500, [[e[0] + Math.cos(a + 2.5) * 24, e[1] + Math.sin(a + 2.5) * 24], ...line(e, e, 2), [e[0] + Math.cos(a - 2.5) * 24, e[1] + Math.sin(a - 2.5) * 24]], c1, c1, w, 1); }
185
+ };
186
+ const pathPen = (pathFn, col, w, prog, cx, cy, r) => { // a doodle traced with the wand, revealed in a sweep
187
+ if (prog <= 0) return;
188
+ for (const [x, k, ww] of [[cc, 0.6, w], [co, 0.55, w * 0.38]]) { x.save(); x.beginPath(); x.rect(cx - r * 1.3, cy - r * 1.3, r * 2.6 * Math.min(1, prog), r * 2.7); x.clip();
189
+ x.strokeStyle = H.rgba(x === co ? H.mix(col, [255, 255, 255], 0.75) : col, k); x.lineWidth = ww; pathFn(x); x.stroke(); x.restore(); }
190
+ if (prog < 1) tip = [cx - r * 1.3 + r * 2.6 * prog, cy + Math.sin(prog * 30) * r * 0.6, col];
191
+ };
192
+ // handwriting traced with the wand: the glyph laid down a few times with hand shake (soft coloured falloff),
193
+ // and a thin, eroded white-hot core that flickers along its length
194
+ const write = (s, x, y, size, col, prog, align = 'center') => {
195
+ if (prog <= 0) return; cc.font = co.font = `700 ${size}px "Caveat"`; const w = cc.measureText(s).width, x0 = align === 'center' ? x - w / 2 : x;
196
+ const jn = H.makeNoise(s.length * 31 + size), cw = (w + 12) * Math.min(1, prog);
197
+ cc.save(); cc.beginPath(); cc.rect(x0 - 6, y - size, cw, size * 1.5); cc.clip(); cc.textAlign = 'left';
198
+ cc.fillStyle = H.rgba(col, 0.66); cc.fillText(s, x0, y);
199
+ for (let k = 0; k < 2; k++) { cc.fillStyle = H.rgba(col, 0.32); cc.fillText(s, x0 + jn.n2(k * 3.1, 0.5) * 1.8 + (k ? 2 : -1), y + jn.n2(k * 3.1, 7.5) * 1.8); }
200
+ cc.restore();
201
+ // eroded core, sized to the word (static per word, so cached)
202
+ const bw = Math.ceil(w + 24), bh = Math.ceil(size * 1.6), er = Math.max(1.5, size * 0.03), ck = s + '|' + size + '|' + col.join(',');
203
+ let E2 = lpCores.get(ck);
204
+ if (!E2) {
205
+ const T = H.layer(bw, bh), tx = T.getContext('2d'); tx.font = cc.font; tx.textAlign = 'left'; tx.fillStyle = '#fff'; tx.fillText(s, 12, size * 1.1);
206
+ E2 = H.layer(bw, bh); const ex = E2.getContext('2d'); ex.drawImage(T, 0, 0); ex.globalCompositeOperation = 'destination-in';
207
+ for (let k = 0; k < 8; k++) ex.drawImage(T, Math.cos(k * Math.PI / 4) * er, Math.sin(k * Math.PI / 4) * er);
208
+ ex.globalCompositeOperation = 'source-in'; ex.fillStyle = H.rgba(H.mix(col, [255, 255, 255], 0.85), 0.95); ex.fillRect(0, 0, bw, bh);
209
+ // flicker: the core dims in a few places where the wand turned away
210
+ ex.globalCompositeOperation = 'destination-out';
211
+ for (let q = 0; q < bw; q += 6) { const g = jn.n2(q * 0.02, 11); if (g > 0.35) { ex.fillStyle = `rgba(0,0,0,${Math.min(0.75, (g - 0.35) * 2.5)})`; ex.fillRect(q, 0, 6, bh); } }
212
+ if (lpCores.size > 64) lpCores.clear();
213
+ lpCores.set(ck, E2);
214
+ }
215
+ co.save(); co.beginPath(); co.rect(x0 - 6, y - size, cw, size * 1.5); co.clip();
216
+ co.globalAlpha = 0.35; co.fillStyle = H.rgba(H.mix(col, [255, 255, 255], 0.6), 1); co.fillText(s, x0, y);
217
+ co.globalAlpha = 1; co.drawImage(E2, 0, 0, bw, bh, x0 - 12, y - size * 1.1, bw, bh);
218
+ co.restore();
219
+ if (prog < 1) tip = [x0 + w * prog, y - size * 0.3 + Math.sin(prog * 50) * size * 0.2, col];
220
+ };
221
+ const led = (s, x, y, dot, col, k = 1) => { // LED pixel-stick read-out
222
+ let px = x; for (const ch of s) { const gph = LED[ch]; if (!gph) { px += dot * 3; continue; }
223
+ for (let r = 0; r < 7; r++) for (let q = 0; q < 5; q++) if (gph[r * 5 + q] === '1') {
224
+ const dx = px + q * dot, dy = y + r * dot; cc.fillStyle = H.rgba(col, 0.85 * k); cc.beginPath(); cc.arc(dx, dy, dot * 0.36, 0, 7); cc.fill();
225
+ co.fillStyle = H.rgba([255, 255, 255], 0.7 * k); co.beginPath(); co.arc(dx, dy, dot * 0.16, 0, 7); co.fill(); }
226
+ px += dot * 6; }
227
+ return px - x;
228
+ };
229
+ const net = (id, ox, oy, sc, prog, cols, conP = prog) => {
230
+ const L = [[-1, 3], [0, 4], [1, 2]], P = L.map(([lx, n]) => Array.from({ length: n }, (_, k) => [ox + lx * 150 * sc, oy + (k - (n - 1) / 2) * 92 * sc]));
231
+ let ci = 0;
232
+ for (let li = 0; li < 2; li++) P[li].forEach((a) => P[li + 1].forEach((b) => { const dx = b[0] - a[0], dy = b[1] - a[1], L2 = Math.hypot(dx, dy);
233
+ pen(id + ci, line([a[0] + dx / L2 * 24 * sc, a[1] + dy / L2 * 24 * sc], [b[0] - dx / L2 * 24 * sc, b[1] - dy / L2 * 24 * sc], 30), cols(ci, li), cols(ci, li + 1), 2.6, sm(ci * 0.03, ci * 0.03 + 0.5, conP), 1); ci++; }));
234
+ P.forEach((ln, li) => ln.forEach(([x, y], k) => pen(id + 100 + li * 10 + k, loop(x, y, 18 * sc), [C.cyan, C.violet, C.amber][li], [C.cyan, C.violet, C.amber][li], 4, sm(li * 0.2 + k * 0.05, li * 0.2 + k * 0.05 + 0.4, prog))));
235
+ return P;
236
+ };
237
+ // the RGB tube goes straight into the colour layer; once a beat's ribbon is complete it is a static, cached image
238
+ const ribbon = (key, opts) => {
239
+ if (opts.prog >= 1) {
240
+ let c = lpRibbons[key];
241
+ if (!c || c.width !== W) { c = lpRibbons[key] = H.layer(W, Hh); const x = c.getContext('2d'); x.globalCompositeOperation = 'lighter'; lightRibbon(x, H.makeNoise(opts.seed), opts); }
242
+ cc.drawImage(c, 0, 0);
243
+ } else {
244
+ const rt = lightRibbon(cc, H.makeNoise(opts.seed), opts);
245
+ // the tube itself, white-hot, at the leading edge of the sweep
246
+ if (rt && rt[3]) { co.strokeStyle = 'rgba(255,255,255,0.85)'; co.lineWidth = 3; co.beginPath(); co.moveTo(rt[3][0], rt[3][1]); co.lineTo(rt[3][2], rt[3][3]); co.stroke(); }
247
+ }
248
+ };
249
+ let ghostA = 0, ghostDX = 0;
250
+ // ---------------- beats ----------------
251
+ if (beat === 0) {
252
+ write('How does', 640, 270, 96, C.cyan, sm(0.15, 0.75, t));
253
+ write('AI learn?', 640, 410, 150, C.pink, sm(0.8, 1.6, t));
254
+ pen(9, quad([380, 450], [640, 490], [920, 440]), C.amber, C.amber, 5, sm(1.6, 1.9, t));
255
+ } else if (beat === 1) {
256
+ // the RGB tube swept round the network while it is traced (same window as the net)
257
+ ribbon(1, { seed: 5101, cx0: 930, cy0: 290, rx: 330, ry: 215, skew: 24, tyk: 0.65, len: 86, twists: 3.0, RN: 170, stri: 4, gain: 0.65, prog: E.inOut(sm(2.45, 3.2, t)) });
258
+ ghostA = 0.65 * sm(2.45, 4.2, t); ghostDX = 175;
259
+ const P = net(1000, 930, 290, 1.2, sm(2.45, 3.2, t), (ci, li) => [C.cyan, C.violet, C.amber][li]);
260
+ for (let k = 0; k < 6; k++) {
261
+ const cx = 120 + (k % 2) * 210, cy = 130 + Math.floor(k / 2) * 185, kind = k % 3 === 1 ? 'dog' : 'cat', t0 = 2.55 + k * 0.32;
262
+ pathPen((x) => (kind === 'cat' ? lpCat : lpDog)(x, cx, cy, 54), kind === 'cat' ? C.amber : C.cyan, 5, sm(t0, t0 + 0.28, t), cx, cy, 54);
263
+ write(kind, cx, cy + 94, 48, C.white, sm(t0 + 0.2, t0 + 0.4, t));
264
+ const tgt = P[0][k % 3];
265
+ pen(200 + k, quad([cx + 60, cy], [(cx + tgt[0]) / 2, Math.min(cy, tgt[1]) - 90], [tgt[0] - 30, tgt[1]]), kind === 'cat' ? C.amber : C.cyan, C.white, 7, sm(t0 + 0.4, t0 + 0.85, t));
266
+ }
267
+ const p = sm(2.9, 5.4, t), v = p >= 1 ? 1000000 : Math.round(Math.pow(10, 6 * p)), s = v.toLocaleString('en-US');
268
+ const wdt = led(s, 930 - (s.length * 6 * 10) / 2, 515, 10, C.amber, sm(2.85, 3.0, t));
269
+ write('examples', 930, 640, 54, C.white, sm(3.0, 3.3, t));
270
+ } else if (beat === 2) {
271
+ pathPen((x) => lpCat(x, 200, 330, 70), C.amber, 5, sm(6.35, 6.75, t), 200, 330, 70);
272
+ const adj = sm(9.0, 9.45, t);
273
+ const P = net(2000, 620, 330, 1.05, sm(6.4, 7.0, t), (ci, li) => [C.cyan, C.violet, C.amber][li]);
274
+ if (adj > 0) net(3000, 620, 330, 1.05, 1, (ci) => (ci % 3 === 0 ? C.green : C.white), adj); // weights re-painted
275
+ arrowPen(40, quad([300, 300], [360, 250], [440, 300]), C.white, C.white, 4, sm(6.95, 7.2, t));
276
+ write('Guess', 370, 236, 54, C.white, sm(7.0, 7.3, t));
277
+ arrowPen(41, quad([800, 330], [840, 310], [880, 330]), C.white, C.white, 4, sm(7.3, 7.45, t));
278
+ write('dog?', 1010, 360, 100, C.pink, sm(7.5, 7.85, t));
279
+ pen(42, line([1130, 300], [1200, 370], 12), C.red, C.red, 7, sm(7.95, 8.05, t)); pen(43, line([1200, 300], [1130, 370], 12), C.red, C.red, 7, sm(8.05, 8.15, t));
280
+ pen(44, Array.from({ length: 70 }, (_, i) => { const a = -0.9 + i / 69 * Math.PI * 2.15; return [1005 + Math.cos(a) * 115, 330 + Math.sin(a) * 62]; }), C.red, C.red, 5, sm(8.1, 8.4, t));
281
+ write('Check', 1010, 240, 54, C.red, sm(8.15, 8.45, t));
282
+ arrowPen(45, quad([1000, 420], [880, 610], [690, 470]), C.red, C.red, 5, sm(8.45, 8.9, t));
283
+ write('Adjust', 870, 610, 54, C.red, sm(8.75, 9.05, t));
284
+ pen(46, line([940, 345], [1080, 330], 14), C.red, C.red, 6, sm(9.45, 9.55, t));
285
+ write('cat', 1010, 480, 100, C.green, sm(9.55, 9.85, t));
286
+ pen(47, [[1100, 450], [1120, 475], [1170, 410]].flatMap((p, i, a) => i ? line(a[i - 1], p, 10) : []), C.green, C.green, 7, sm(9.85, 10.0, t));
287
+ } else if (beat === 3) {
288
+ pen(50, [...line([140, 150], [140, 520], 20), ...line([140, 520], [760, 520], 30)], C.white, C.white, 3.5, sm(10.75, 11.1, t));
289
+ write('training rounds', 450, 580, 46, C.white, sm(10.95, 11.3, t));
290
+ const lp = sm(11.1, 12.3, t), pts = Array.from({ length: 60 }, (_, i) => { const r = i / 59 * 12; return [160 + r * 50, 520 - (0.97 - 0.36 * Math.exp(-r / 3.2) - 0.5) / 0.5 * 360]; });
291
+ for (let k = 0; k < 7; k++) pen(60 + k, pts.map(([x, y]) => [x, y - 9 + k * 3]), hsl(180 + k * 25, 1, 0.6), hsl(300 + k * 15, 1, 0.6), 2, lp); // RGB light-tube trace
292
+ write('Accuracy', 1010, 230, 64, C.cyan, sm(10.85, 11.15, t));
293
+ const pct = Math.round(61 + 36 * E.out(lp)), s = pct + '%';
294
+ led(s, 1010 - s.length * 6 * 11 / 2, 290, 11, pct >= 97 ? C.green : C.amber, sm(10.9, 11.05, t));
295
+ pen(70, Array.from({ length: 70 }, (_, i) => { const a = -1 + i / 69 * Math.PI * 2.2; return [1010 + Math.cos(a) * 170, 325 + Math.sin(a) * 92]; }), C.amber, C.pink, 5, sm(12.35, 12.7, t));
296
+ const burst = sm(12.3, 12.75, t);
297
+ if (burst > 0 && burst < 1) for (let k = 0; k < 24; k++) { const a = k / 24 * Math.PI * 2 + 0.3, r0 = 180 + burst * 40, r1 = r0 + 30 + (k % 3) * 18; pen(80 + k, line([1010 + Math.cos(a) * r0, 325 + Math.sin(a) * r0 * 0.6], [1010 + Math.cos(a) * r1, 325 + Math.sin(a) * r1 * 0.6], 4), C.amber, [255, 255, 255], 2.5, 1); }
298
+ } else {
299
+ ribbon(4, { seed: 5104, cx0: 640, cy0: 362, rx: 560, ry: 212, skew: 26, tyk: 0.38, len: 100, twists: 3.4, RN: 180, stri: 4, gain: 0.75, prog: E.inOut(sm(13.3, 14.4, t)) });
300
+ ghostA = 0.6 * sm(13.3, 14.6, t); ghostDX = 150;
301
+ write('More examples', 640, 250, 104, C.cyan, sm(13.3, 13.75, t));
302
+ arrowPen(90, line([640, 290], [640, 370], 20), C.amber, C.amber, 6, sm(13.75, 13.9, t));
303
+ write('better guesses', 640, 500, 132, C.pink, sm(13.9, 14.4, t));
304
+ }
305
+ // ---------- compose: landscape + accumulated light, camera whip-pan smears it all ----------
306
+ const bg = getLpBg(W, Hh);
307
+ const scene = H.layer(W, Hh), sx = scene.getContext('2d');
308
+ sx.drawImage(bg, -camX * 0.6 % (bg.width - W), 0);
309
+ if (ghostA > 0) drawGhost(sx, getGhost(W, Hh), ghostDX, ghostA);
310
+ sx.globalCompositeOperation = 'lighter';
311
+ const glow34 = blurSmall(Ccol, 34, 0.125);
312
+ // colour spill on the wet grass: the light mirrored about the horizon, squashed, fading and broken up in patches
313
+ { const HZ = 560, SW = Math.ceil(W / 4), SH = Math.ceil((Hh - HZ) / 4), sp = H.layer(SW, SH), spx = sp.getContext('2d');
314
+ spx.setTransform(0.25, 0, 0, -0.0875, 0, 0.0875 * HZ); spx.drawImage(glow34, 0, 0, W, Hh);
315
+ spx.setTransform(1, 0, 0, 1, 0, 0); spx.globalCompositeOperation = 'destination-in'; spx.drawImage(getSpillMask(SW, SH), 0, 0);
316
+ sx.globalAlpha = 1; sx.drawImage(sp, 0, HZ, W, Hh - HZ); }
317
+ sx.globalAlpha = 0.5; sx.drawImage(glow34, 0, 0, W, Hh);
318
+ sx.globalAlpha = 0.8; sx.drawImage(blurSmall(Ccol, 9, 0.25), 0, 0, W, Hh);
319
+ sx.globalAlpha = 1; sx.drawImage(Ccol, 0, 0); sx.drawImage(Ccore, 0, 0);
320
+ sx.globalAlpha = 0.6; sx.drawImage(blurSmall(Ccore, 3, 0.5), 0, 0, W, Hh);
321
+ if (tip && smear < 0.05) { const tg = sx.createRadialGradient(tip[0], tip[1], 0, tip[0], tip[1], 34); tg.addColorStop(0, 'rgba(255,255,255,0.95)'); tg.addColorStop(0.15, H.rgba(tip[2], 0.6)); tg.addColorStop(1, H.rgba(tip[2], 0)); sx.globalAlpha = 1; sx.fillStyle = tg; sx.beginPath(); sx.arc(tip[0], tip[1], 34, 0, 7); sx.fill(); }
322
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, Hh);
323
+ if (smear > 0.02) {
324
+ ctx.globalCompositeOperation = 'lighter';
325
+ for (let k = 0; k < 28; k++) { ctx.globalAlpha = 0.14 * (1 - k / 28); ctx.drawImage(scene, -k * smear * 26 - smear * 80, 0); }
326
+ ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
327
+ } else ctx.drawImage(scene, 0, 0);
328
+ H.vignette(ctx, W, Hh, 0.55, '0,0,4');
329
+ const img = ctx.getImageData(0, 0, W, Hh), d = img.data;
330
+ for (let i = 0; i < d.length; i += 4) { const l = (d[i] + d[i + 1] + d[i + 2]) / 765, k = (1 - l) * 9 + 2, n = (Rn() - 0.5) * k; d[i] += n + (Rn() - 0.5) * k * 0.8; d[i + 1] += n; d[i + 2] += n + (Rn() - 0.5) * k; }
331
+ ctx.putImageData(img, 0, 0);
332
+ }
333
+
334
+ // Sound: out in a dark field at night with a camera on a tripod. Each beat is one exposure: the shutter opens (soft
335
+ // click), the LED wand swishes through the air for exactly as long as it draws (short hand gestures, low and airy),
336
+ // orbs fly into the network and land with a soft glow tone, the pixel-stick ticks as numbers change, and the 97%
337
+ // ring goes off with a steel-wool spark crackle. Between beats the shutter closes and the tripod head is swung.
338
+ // The final exposure closes over a warm chord. A quiet night breeze and a soft pad sit underneath.
339
+ // All times are copied from lightExplainer() above (same pans table, smooth windows and doodle/orb schedule).
340
+ // Voiceover mode (k.vo): beds and pad drop out; small gestures, labels, orb flights and counter ticks are minor.
341
+ function lightSound(k) {
342
+ const sm = H.smooth, E = H.ease, D = 15;
343
+ const when = (f, v, a, b) => { for (let t = a; t <= b; t += 0.001) if (f(t) >= v) return t; return b; };
344
+ // the wand moving through air while it draws: a run of short, low, breathy hand gestures covering a..b
345
+ const wand = (a, b, g = 1, pan = 0, minor = true) => { const r = H.rng(Math.floor(a * 997)); let x = a;
346
+ while (x < b - 0.04) { const d = Math.min(b - x, 0.14 + r() * 0.1), up = r() > 0.5, f0 = 380 + r() * 120, f1 = f0 * (1.6 + r() * 0.5);
347
+ k.noise(x, { dur: d + 0.05, color: 'pink', filter: 'bandpass', f: up ? [[0, f0], [d * 0.6, f1], [d + 0.05, f0]] : [[0, f1], [d * 0.6, f0 * 1.1], [d + 0.05, f0]], q: 1.3, gain: 0.08 * g * (0.75 + r() * 0.4), a: d * 0.45, pan: pan + (r() - 0.5) * 0.3, minor });
348
+ x += d * (0.85 + r() * 0.2); } };
349
+ // camera shutter: open = light click; close = click with the mirror's soft knock
350
+ const shutterOpen = (t, g = 1) => { k.noise(t, { dur: 0.015, filter: 'bandpass', f: 1800, q: 1.5, gain: 0.25 * g, a: 0.001 }); k.tone(t, { f: [[0, 300], [0.03, 160]], dur: 0.04, gain: 0.12 * g }); };
351
+ const shutterClose = (t, g = 1) => { k.noise(t, { dur: 0.018, filter: 'bandpass', f: 1500, q: 1.3, gain: 0.3 * g, a: 0.001 }); k.tone(t, { f: [[0, 200], [0.05, 90]], dur: 0.08, gain: 0.22 * g }); k.noise(t + 0.045, { dur: 0.02, filter: 'bandpass', f: 1100, q: 2, gain: 0.15 * g, a: 0.001 }); };
352
+ // tripod head swung hard (smoothstep pan a..b): low friction drag + air, fastest in the middle
353
+ const swing = (a, b) => { const d = b - a;
354
+ k.noise(a + 0.03, { dur: d, color: 'brown', filter: 'bandpass', f: [[0, 160], [d / 2, 320], [d, 140]], q: 1.4, gain: 0.32, a: d * 0.45 });
355
+ k.noise(a + 0.03, { dur: d, color: 'pink', filter: 'bandpass', f: [[0, 400], [d / 2, 1100], [d, 450]], q: 0.9, gain: 0.09, a: d * 0.5 }); };
356
+ const transition = (a, b) => { shutterClose(a, 0.9); swing(a, b); shutterOpen(b, 0.8); };
357
+ // an orb flying along its arc and landing in the network (soft glow tone on arrival)
358
+ const orb = (a, b, pan0, pan1) => { const d = b - a;
359
+ k.noise(a, { dur: d, color: 'pink', filter: 'bandpass', f: [[0, 600], [d, 1300]], q: 2, gain: 0.045, a: d * 0.7, pan: pan0, minor: true });
360
+ k.tone(b, { f: 'A3', dur: 0.5, gain: 0.07, a: 0.01, verb: 0.3, pan: pan1, minor: true }); k.tone(b, { f: 'E4', dur: 0.35, gain: 0.03, a: 0.01, verb: 0.3, pan: pan1, minor: true }); };
361
+ // pixel-stick readout ticks, throttled
362
+ const pixel = (val, a, b, gap, pan, avoid = []) => { let last = -1, prev = val(a);
363
+ for (let t = a; t <= b; t += 0.002) { const v = val(t);
364
+ if (v !== prev && t - last >= gap && !avoid.some((x) => t > x - 0.03 && t < x + 0.1)) { k.play('click', t, { f: 1500, gain: 1.5, pan, minor: true }); last = t; }
365
+ prev = v; } };
366
+
367
+ // ---- beds: night breeze, a quiet field; a soft slow pad (64 bpm, one chord per 3.75 s)
368
+ k.bed('room', 0, D, { gain: 0.3 });
369
+ k.noise(0, { dur: D, color: 'pink', filter: 'lowpass', f: [[0, 380], [5, 520], [10, 340], [15, 480]], gain: 0.05, a: 1, hold: D - 2, bus: 'amb', minor: true });
370
+ k.music('pad', 0, D, { bpm: 64, gain: 0.22, chords: [['A3', 'C4', 'E4', 'G4'], ['F3', 'A3', 'C4', 'E4'], ['D3', 'F3', 'A3', 'C4'], ['C3', 'E3', 'G3', 'B3']] });
371
+
372
+ // ---- beat 0: exposure opens; "How does" 0.15–0.75, "AI learn?" 0.8–1.6, underline 1.6–1.9
373
+ shutterOpen(0.05, 1);
374
+ wand(0.15, 0.75, 1.0, -0.2, false);
375
+ wand(0.8, 1.6, 1.2, 0.1, false);
376
+ wand(1.6, 1.9, 0.9, 0.2);
377
+ transition(2.05, 2.5);
378
+
379
+ // ---- beat 1: network traced 2.45–3.2; six doodles at t0 = 2.55 + k*0.32 (doodle t0..t0+0.28, label +0.2..+0.4),
380
+ // each sends an orb along an arc (t0+0.4 .. t0+0.85) into the input layer
381
+ wand(2.5, 3.2, 0.35, 0.45);
382
+ const land = [];
383
+ for (let c = 0; c < 6; c++) { const t0 = 2.55 + c * 0.32, pan = c % 2 ? -0.55 : -0.8;
384
+ wand(t0, t0 + 0.38, 0.4, pan); orb(t0 + 0.4, t0 + 0.85, pan, 0.3); land.push(t0 + 0.85); }
385
+ // LED pixel-stick counter 10^(6p), p = sm(2.9, 5.4), lit 2.85; "examples" 3.0–3.3
386
+ pixel((t) => { const p = sm(2.9, 5.4, t); return p >= 1 ? 1e6 : Math.round(Math.pow(10, 6 * p)); }, 2.92, 5.35, 0.09, 0.3, land);
387
+ k.play('tick', 5.4, { f: 2400, gain: 1.0, pan: 0.3 }); k.tone(5.4, { f: 'E4', dur: 0.4, gain: 0.08, verb: 0.25, pan: 0.3 });
388
+ transition(5.9, 6.35);
389
+
390
+ // ---- beat 2: cat doodle 6.35–6.75, network 6.4–7.0, Guess arrow 6.95–7.2 + label 7.0–7.3, arrow 7.3–7.45
391
+ wand(6.38, 6.75, 0.8, -0.6);
392
+ wand(6.75, 7.0, 0.5, 0);
393
+ wand(7.0, 7.3, 0.6, -0.3); wand(7.3, 7.45, 0.5, 0.3);
394
+ wand(7.5, 7.85, 1.0, 0.45, false); // "dog?" (the guess)
395
+ // WRONG: two hard red slashes 7.95–8.05, 8.05–8.15, then a ring 8.1–8.4 and "Check" 8.15–8.45
396
+ [7.95, 8.05].forEach((ts, i) => { k.noise(ts, { dur: 0.12, color: 'pink', filter: 'bandpass', f: [[0, 1400], [0.1, 350]], q: 1.1, gain: 0.34, a: 0.01, pan: 0.6 });
397
+ k.tone(ts + 0.02, { f: [[0, i ? 150 : 175], [0.12, 70]], dur: 0.2, gain: 0.28, pan: 0.6 }); });
398
+ k.tone(8.0, { f: 'D3', type: 'triangle', dur: 0.45, gain: 0.1, a: 0.01, verb: 0.2, pan: 0.5 }); k.tone(8.0, { f: 'Ab3', type: 'triangle', dur: 0.45, gain: 0.07, a: 0.01, verb: 0.2, pan: 0.5 }); // sour, low
399
+ wand(8.15, 8.45, 0.6, 0.45);
400
+ wand(8.45, 8.9, 0.8, 0.2, false); // red arrow back
401
+ wand(8.92, 9.05, 0.5, 0.1); // "Adjust" tail
402
+ // weights re-painted 9.0–9.45: a soft rising glow swell under the net
403
+ k.tone(9.0, { f: [[0, 220], [0.45, 330]], type: 'triangle', dur: 0.55, gain: 0.06, a: 0.25, verb: 0.3 });
404
+ k.noise(9.0, { dur: 0.5, color: 'pink', filter: 'bandpass', f: [[0, 500], [0.45, 1400]], q: 1.5, gain: 0.08, a: 0.3, minor: true });
405
+ // RIGHT: strike-through 9.45–9.55, "cat" 9.55–9.85, tick 9.85–10.0
406
+ wand(9.45, 9.55, 0.6, 0.45);
407
+ wand(9.55, 9.85, 1.0, 0.45, false);
408
+ wand(9.85, 10.0, 0.9, 0.6, false);
409
+ k.play('success', 10.0, { f: 'A4', gain: 0.6, pan: 0.45 });
410
+ transition(10.3, 10.75);
411
+
412
+ // ---- beat 3: axes 10.75–11.1, "Accuracy" 10.85–11.15, labels; RGB tube sweeps the curve 11.1–12.3
413
+ wand(10.78, 11.1, 0.6, -0.5);
414
+ wand(11.1, 11.3, 0.4, 0.4);
415
+ // the light tube is swept along the curve: one long, smooth airy pass, brighter as it climbs
416
+ k.noise(11.1, { dur: 1.2, color: 'pink', filter: 'bandpass', f: [[0, 350], [1.2, 900]], q: 1.2, gain: 0.07, a: 0.3, hold: 0.7, pan: -0.4, minor: true });
417
+ k.tone(11.1, { f: [[0, 196], [1.2, 294]], type: 'triangle', dur: 1.25, gain: 0.03, a: 0.3, hold: 0.7, pan: -0.4, minor: true });
418
+ const pct = (t) => Math.round(61 + 36 * E.out(sm(11.1, 12.3, t)));
419
+ const t97 = when(pct, 97, 11.1, 12.3);
420
+ pixel(pct, 11.11, t97 - 0.04, 0.085, 0.45);
421
+ k.play('tick', t97, { f: 2400, gain: 1.0, pan: 0.45 }); k.tone(t97, { f: 'E4', dur: 0.4, gain: 0.08, verb: 0.25, pan: 0.45 });
422
+ // THE HIT 12.3: steel wool spins up — a soft ignition thump, then a spark crackle as the burst flies (12.3–12.75)
423
+ k.play('thud', 12.3, { f: 90, gain: 1.1, pan: 0.4 });
424
+ k.noise(12.3, { dur: 0.5, color: 'pink', filter: 'lowpass', f: [[0, 2600], [0.45, 400]], gain: 0.28, a: 0.005, verb: 0.35, pan: 0.4 });
425
+ { const r = H.rng(1230); for (let x = 0; x < 0.5; x += 0.012 + r() * 0.02) k.noise(12.3 + x, { dur: 0.006 + r() * 0.006, filter: 'bandpass', f: 1200 + r() * 1300, q: 1.2, gain: (0.35 + r() * 0.35) * (1 - x * 1.4), a: 0.001, pan: 0.4 + (r() - 0.5) * 0.9 }); }
426
+ wand(12.35, 12.7, 0.7, 0.4); // ring traced round 97%
427
+ ['C4', 'E4', 'G4', 'C5'].forEach((n, i) => k.tone(12.32, { f: n, type: 'triangle', dur: 1.0, gain: 0.05, a: 0.02, hold: 0.15, filter: 'lowpass', f2: 1800, verb: 0.35, pan: (i - 1.5) * 0.3 }));
428
+ transition(12.85, 13.3);
429
+
430
+ // ---- beat 4: "More examples" 13.3–13.75, arrow 13.75–13.9, "better guesses" 13.9–14.4
431
+ wand(13.3, 13.75, 1.0, -0.1, false);
432
+ wand(13.75, 13.9, 0.6, 0);
433
+ wand(13.9, 14.4, 1.1, 0.1, false);
434
+ // resolve: the last exposure closes over a warm chord
435
+ shutterClose(14.45, 1.0);
436
+ ['C3', 'G3', 'E4', 'B4'].forEach((n, i) => k.tone(14.42, { f: n, type: i ? 'sine' : 'triangle', dur: 0.58, gain: 0.08 - i * 0.012, a: 0.05, hold: 0.2, verb: 0.4, pan: (i - 1.5) * 0.25 }));
437
+ }
438
+
439
+ registerStyle({
440
+ id: 'light-painting',
441
+ name: 'Light Painting',
442
+ category: 'Light & Imaging',
443
+ description: 'A 30-second exposure in a dark field: a neural network drawn in the air with an LED wand and a twisting rainbow light-tube ribbon, hand-jittered trails with hot cores and coloured falloff, a ghost of the painter, pinpoint stars, horizon bokeh and colour spill on the grass.',
444
+ motion: 'Shot as a live-composite exposure: the wand traces node after node and fans out each layer of connections, the RGB tube sweeps the twisting ribbon around it, the word is written last, while the ghost painter slowly accumulates and star trails lengthen.',
445
+ fonts: ['700 92px "Caveat"'],
446
+ frameT: 5.6,
447
+ duration: 6,
448
+ readsPixels: true,
449
+ explainerDuration: 15,
450
+ explainerNote: 'Each beat is a fresh live long exposure: the wand traces doodles, arrows and handwriting with its glowing tip, orbs leave arcing light trails into the network, numbers come from an LED pixel-stick, the emphasis is a traced ring with a spark burst, and between beats the camera whip-pans so every trail smears into streaks.',
451
+ explainer(ctx, W, Hh, t) { lightExplainer(ctx, W, Hh, t); },
452
+ explainerSound(k) { lightSound(k); },
453
+ draw(ctx, W, Hh, t) {
454
+ const R = H.rng(3030), nz = H.makeNoise(303);
455
+ const HZ = 528; // horizon
456
+ const sch = (a, d) => H.clamp((t - a) / d); // stroke progress
457
+ const ghostA = 0.3 + 0.7 * H.smooth(0.2, 4.6, t);
458
+ const expo = 0.3 + 0.7 * H.smooth(0, 6, t); // star trails lengthen as the exposure accumulates
459
+ let tip = null;
460
+
461
+ // ---------- night landscape ----------
462
+ const sky = ctx.createLinearGradient(0, 0, 0, HZ);
463
+ sky.addColorStop(0, '#02030a'); sky.addColorStop(0.6, '#060a1c'); sky.addColorStop(1, '#14182c');
464
+ ctx.fillStyle = sky; ctx.fillRect(0, 0, W, HZ + 2);
465
+ // light-pollution glow on the horizon
466
+ const lp = ctx.createRadialGradient(980, HZ, 10, 980, HZ, 520);
467
+ lp.addColorStop(0, 'rgba(120,80,60,0.35)'); lp.addColorStop(1, 'rgba(60,40,40,0)');
468
+ ctx.fillStyle = lp; ctx.fillRect(0, 0, W, HZ + 2);
469
+ // stars: pinpoints, a few short trails from earth rotation
470
+ for (let k = 0; k < 520; k++) {
471
+ const x = R() * W, y = Math.pow(R(), 1.3) * (HZ - 30), m = Math.pow(R(), 5);
472
+ const col = R.pick([[255, 255, 255], [200, 215, 255], [255, 230, 200], [180, 200, 255]]);
473
+ const fade = 1 - H.smooth(HZ - 160, HZ, y) * 0.8;
474
+ ctx.strokeStyle = H.rgba(col, (0.25 + m * 0.75) * fade); ctx.lineWidth = 0.8 + m * 1.6; ctx.lineCap = 'round';
475
+ const len = (1 + m * 3) * expo;
476
+ ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + len, y - len * 0.35); ctx.stroke();
477
+ if (m > 0.6) { ctx.fillStyle = H.rgba(col, 0.1 * fade); ctx.beginPath(); ctx.arc(x, y, 2.5 + m * 1.5, 0, 7); ctx.fill(); }
478
+ }
479
+ // treeline / hills silhouette
480
+ ctx.fillStyle = '#04050a';
481
+ ctx.beginPath(); ctx.moveTo(0, HZ);
482
+ for (let x = 0; x <= W; x += 4) {
483
+ const hill = 18 * Math.sin(x * 0.004 + 1.2) + 10 * Math.sin(x * 0.011);
484
+ const trees = Math.max(0, nz.fbm2(x * 0.03, 2.2, 3)) * 46 * H.smooth(80, 300, x) * (1 - H.smooth(760, 940, x)) + Math.max(0, nz.n2(x * 0.08, 5)) * 10;
485
+ ctx.lineTo(x, HZ - 14 - hill - trees);
486
+ }
487
+ ctx.lineTo(W, Hh); ctx.lineTo(0, Hh); ctx.fill();
488
+ // grass field texture
489
+ for (let k = 0; k < 2600; k++) {
490
+ const x = R() * W, y = HZ + Math.pow(R(), 0.7) * (Hh - HZ);
491
+ ctx.strokeStyle = `rgba(30,36,48,${R.range(0.2, 0.6)})`; ctx.lineWidth = 1;
492
+ ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + R.range(-3, 3), y - R.range(3, 6 + (y - HZ) * 0.05)); ctx.stroke();
493
+ }
494
+ // bokeh: distant out-of-focus lights along the horizon
495
+ const bok = [[860, HZ - 30, 18, [255, 170, 90]], [915, HZ - 22, 13, [255, 200, 120]], [1040, HZ - 34, 22, [255, 150, 70]], [1100, HZ - 18, 11, [170, 210, 255]],
496
+ [1170, HZ - 28, 16, [255, 190, 110]], [120, HZ - 26, 14, [255, 160, 80]], [60, HZ - 16, 9, [255, 210, 150]], [990, HZ - 12, 9, [255, 220, 170]]];
497
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
498
+ bok.forEach(([x, y, r, c]) => {
499
+ const g = ctx.createRadialGradient(x, y, 0, x, y, r);
500
+ g.addColorStop(0, H.rgba(c, 0.18)); g.addColorStop(0.8, H.rgba(c, 0.26)); g.addColorStop(0.95, H.rgba(c, 0.42)); g.addColorStop(1, H.rgba(c, 0));
501
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
502
+ });
503
+ ctx.restore();
504
+
505
+ // ---------- ghost painter (moved during exposure: translucent) ----------
506
+ drawGhost(ctx, getGhost(W, Hh), 0, ghostA);
507
+
508
+ // ---------- light trails ----------
509
+ const Ccol = H.layer(W, Hh), cc = Ccol.getContext('2d'); // coloured trail
510
+ const Ccore = H.layer(W, Hh), co = Ccore.getContext('2d'); // hot core
511
+ [cc, co].forEach((x) => { x.globalCompositeOperation = 'lighter'; x.lineCap = 'round'; x.lineJoin = 'round'; });
512
+ let seed = 1;
513
+ // a hand-held LED pen stroke along pts with jitter, speed-dependent brightness and colour from c0->c1
514
+ const pen = (pts, c0, c1, width = 4.2, gaps = 0, pr = 1) => {
515
+ const jn = H.makeNoise(seed++);
516
+ const N = pts.length;
517
+ let prev = null;
518
+ for (let i = 0; i < N; i++) {
519
+ const u = i / (N - 1);
520
+ if (u > pr) break;
521
+ const [x0, y0] = pts[i];
522
+ const x = x0 + jn.n2(u * 9, 0.5) * 2.4, y = y0 + jn.n2(u * 9, 7.5) * 2.4;
523
+ if (prev) {
524
+ const sp = 0.65 + 0.35 * jn.n2(u * 4, 3); // dwell variation
525
+ const off = gaps && jn.n2(u * 6, 11) > 0.42; // pen flicker gap
526
+ if (!off) {
527
+ const c = H.mix(c0, c1, u);
528
+ cc.strokeStyle = H.rgba(c, 0.55 * sp); cc.lineWidth = width * (0.8 + sp * 0.4);
529
+ cc.beginPath(); cc.moveTo(prev[0], prev[1]); cc.lineTo(x, y); cc.stroke();
530
+ co.strokeStyle = H.rgba(H.mix(c, [255, 255, 255], 0.75), 0.5 * sp); co.lineWidth = width * 0.38;
531
+ co.beginPath(); co.moveTo(prev[0], prev[1]); co.lineTo(x, y); co.stroke();
532
+ }
533
+ }
534
+ prev = [x, y];
535
+ }
536
+ if (pr > 0 && pr < 1 && prev) tip = [prev[0], prev[1], H.mix(c0, c1, pr)];
537
+ };
538
+ const line = (a, b, n = 60) => Array.from({ length: n }, (_, i) => [a[0] + (b[0] - a[0]) * i / (n - 1), a[1] + (b[1] - a[1]) * i / (n - 1)]);
539
+ const loop = (cx, cy, r, turns = 2.3, n = 120) => Array.from({ length: n }, (_, i) => {
540
+ const a = i / (n - 1) * Math.PI * 2 * turns - 1.2, rr = r * (1 + 0.08 * Math.sin(a * 0.5)) + i * 0.02;
541
+ return [cx + Math.cos(a) * rr, cy + Math.sin(a) * rr * 0.96];
542
+ });
543
+
544
+ // neural net layout
545
+ const layers = [3, 4, 4, 2];
546
+ const lx = [330, 500, 670, 840];
547
+ const nodes = layers.map((n, li) => Array.from({ length: n }, (_, k) => [lx[li] + Math.sin(k * 2 + li) * 6, 300 + (k - (n - 1) / 2) * 96 + Math.cos(k + li) * 6]));
548
+ const lcol = [[60, 220, 255], [170, 90, 255], [255, 70, 190], [255, 180, 60]];
549
+ for (let li = 0; li < layers.length - 1; li++) {
550
+ let ci = 0;
551
+ nodes[li].forEach((a) => nodes[li + 1].forEach((b) => {
552
+ const cp = sch(0.62 + li * 0.6 + (ci++) * 0.022, 0.26);
553
+ const dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy);
554
+ const a2 = [a[0] + dx / L * 26, a[1] + dy / L * 26], b2 = [b[0] - dx / L * 26, b[1] - dy / L * 26];
555
+ pen(line(a2, b2, 50), lcol[li], lcol[li + 1], 2.6, 1, cp);
556
+ }));
557
+ }
558
+ nodes.forEach((ln, li) => ln.forEach(([x, y], k) => pen(loop(x, y, 20), lcol[li], H.mix(lcol[li], [255, 255, 255], 0.3), 4.5, 0, sch(0.3 + li * 0.6 + k * 0.07, 0.24))));
559
+
560
+ // light-tube ribbon: a 60cm RGB tube swept and twisted around the net
561
+ const Rb = H.layer(W, Hh), rb = Rb.getContext('2d');
562
+ rb.globalCompositeOperation = 'lighter';
563
+ const rprog = H.ease.inOut(sch(2.5, 1.1));
564
+ { const rt = lightRibbon(rb, nz, { cx0: 590, cy0: 290, rx: 430, ry: 182, prog: rprog }); if (rt) tip = rt; }
565
+
566
+ // light-written word (pen script)
567
+ co.save(); cc.save();
568
+ const word = 'learning';
569
+ const writeWord = (x, col, a, wobble) => {
570
+ x.font = '700 92px "Caveat"'; x.textAlign = 'center';
571
+ x.fillStyle = col; x.globalAlpha = a;
572
+ x.save(); x.beginPath(); x.rect(395, 440, 10 + wp * 420, 160); x.clip();
573
+ x.translate(600 + wobble, 548); x.rotate(-0.05); x.fillText(word, 0, 0); x.restore();
574
+ };
575
+ const wp = sch(3.65, 0.85);
576
+ if (wp > 0 && wp < 1) tip = [395 + 10 + wp * 420, 530 - Math.sin(wp * 40) * 18, [255, 140, 60]];
577
+ if (wp > 0) {
578
+ writeWord(cc, 'rgb(255,120,40)', 0.9, 0); writeWord(cc, 'rgb(255,80,30)', 0.4, 2);
579
+ co.globalCompositeOperation = 'source-over';
580
+ const tmp = H.layer(W, Hh), tx = tmp.getContext('2d'); writeWord(tx, '#fff', 1, 0);
581
+ // thin core: erode the glyphs
582
+ const ero = H.layer(W, Hh), ex = ero.getContext('2d'); ex.drawImage(tmp, 0, 0); ex.globalCompositeOperation = 'destination-in';
583
+ for (let k = 0; k < 8; k++) ex.drawImage(tmp, Math.round(Math.cos(k * Math.PI / 4) * 2), Math.round(Math.sin(k * Math.PI / 4) * 2));
584
+ ex.globalCompositeOperation = 'source-in'; ex.fillStyle = 'rgba(255,236,200,0.95)'; ex.fillRect(0, 0, W, Hh);
585
+ co.globalCompositeOperation = 'lighter'; co.drawImage(ero, 0, 0);
586
+ }
587
+ co.restore(); cc.restore();
588
+
589
+ // ---------- composite light (additive, with optical bloom) ----------
590
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
591
+ // spill onto the grass beneath (soft, squashed)
592
+ ctx.globalAlpha = 1.0; ctx.drawImage(blurSmall(Ccol, 40, 0.125), 0, HZ + 6, W, 190);
593
+ ctx.drawImage(blurSmall(Rb, 40, 0.125), 0, HZ + 6, W, 190);
594
+ ctx.globalAlpha = 0.5; ctx.drawImage(blurSmall(Ccol, 34, 0.125), 0, 0, W, Hh);
595
+ ctx.globalAlpha = 0.8; ctx.drawImage(blurSmall(Ccol, 9, 0.25), 0, 0, W, Hh);
596
+ ctx.globalAlpha = 0.7; ctx.drawImage(blurSmall(Rb, 22, 0.25), 0, 0, W, Hh);
597
+ ctx.globalAlpha = 1; ctx.drawImage(Rb, 0, 0); ctx.drawImage(Ccol, 0, 0);
598
+ ctx.drawImage(Ccore, 0, 0);
599
+ ctx.globalAlpha = 0.6; ctx.drawImage(blurSmall(Ccore, 3, 0.5), 0, 0, W, Hh);
600
+ ctx.restore();
601
+
602
+ // the wand itself, live at the current stroke tip
603
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
604
+ if (tip) {
605
+ const tg = ctx.createRadialGradient(tip[0], tip[1], 0, tip[0], tip[1], 34);
606
+ tg.addColorStop(0, 'rgba(255,255,255,0.95)'); tg.addColorStop(0.15, H.rgba(tip[2], 0.6)); tg.addColorStop(1, H.rgba(tip[2], 0));
607
+ ctx.fillStyle = tg; ctx.beginPath(); ctx.arc(tip[0], tip[1], 34, 0, 7); ctx.fill();
608
+ if (tip[3]) { ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(tip[3][0], tip[3][1]); ctx.lineTo(tip[3][2], tip[3][3]); ctx.stroke(); }
609
+ }
610
+ ctx.restore();
611
+ // wand light on the painter's raised hand
612
+ ctx.save(); ctx.globalCompositeOperation = 'lighter';
613
+ ctx.globalAlpha = tip ? 0.3 : 1;
614
+ const wg = ctx.createRadialGradient(910, 270, 0, 910, 270, 22);
615
+ wg.addColorStop(0, 'rgba(255,220,180,0.55)'); wg.addColorStop(1, 'rgba(255,160,80,0)');
616
+ ctx.fillStyle = wg; ctx.beginPath(); ctx.arc(910, 270, 22, 0, 7); ctx.fill();
617
+ ctx.restore();
618
+
619
+ // ---------- sensor finish: high-ISO noise, vignette ----------
620
+ // slow push-in
621
+ const push = 1 + 0.03 * H.ease.inOut(H.clamp(t / 6));
622
+ const cpy = H.layer(W, Hh); cpy.getContext('2d').drawImage(ctx.canvas, 0, 0);
623
+ ctx.save(); ctx.translate(600, 330); ctx.scale(push, push); ctx.translate(-600, -330); ctx.drawImage(cpy, 0, 0); ctx.restore();
624
+ H.vignette(ctx, W, Hh, 0.55, '0,0,4');
625
+ const Rn = H.rng(31 + Math.floor(t * 30));
626
+ const img = ctx.getImageData(0, 0, W, Hh), d = img.data;
627
+ for (let i = 0; i < d.length; i += 4) {
628
+ const l = (d[i] + d[i + 1] + d[i + 2]) / 765, k = (1 - l) * 9 + 2;
629
+ const n = (Rn() - 0.5) * k;
630
+ d[i] += n + (Rn() - 0.5) * k * 0.8; d[i + 1] += n; d[i + 2] += n + (Rn() - 0.5) * k;
631
+ }
632
+ ctx.putImageData(img, 0, 0);
633
+ },
634
+ });
635
+ }