@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,801 @@
1
+ {
2
+ // ================= Oscilloscope Vector Art: XY-mode beam on an analogue lab scope =================
3
+ // One P31 beam draws everything: brightness ~ 1/beam speed (bright dwell dots at corners and turns, dim fast
4
+ // retrace lines between figures), bloom + halation, internal 8x10 graticule, a lab instrument front panel.
5
+
6
+ // ---------- geometry ----------
7
+ const GX = 54, GY = 40, DIV = 80, GW = 800, GH = 640, CX = GX + GW / 2, CY = GY + GH / 2;
8
+ const GL = { x: 28, y: 22, w: 852, h: 676, r: 58 };
9
+ const P31 = [125, 255, 74], AMBER = [255, 170, 46], RED = [255, 84, 46];
10
+
11
+ // ---------- Hershey-style stroke font (cap height 10 units, y down) ----------
12
+ const arc = (cx, cy, rx, ry, a0, a1, n) => {
13
+ n = n || Math.max(6, Math.ceil(Math.abs(a1 - a0) / 9)); const o = [];
14
+ for (let i = 0; i <= n; i++) { const a = (a0 + (a1 - a0) * i / n) * Math.PI / 180; o.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]); }
15
+ return o;
16
+ };
17
+ const G = {
18
+ A: [[[0, 10], [3.6, 0], [7.2, 10]], [[1.35, 6.4], [5.85, 6.4]]],
19
+ B: [[[0, 10], [0, 0], [4, 0], ...arc(4, 2.5, 2.6, 2.5, -90, 90), [0, 5]], [[0, 5], [4.3, 5], ...arc(4.3, 7.5, 2.8, 2.5, -90, 90), [0, 10]]],
20
+ C: [arc(4, 5, 4, 5, -35, -325)],
21
+ D: [[[0, 0], [0, 10], [2.8, 10], ...arc(2.8, 5, 4.2, 5, 90, -90), [0, 0]]],
22
+ E: [[[6.4, 0], [0, 0], [0, 10], [6.4, 10]], [[0, 5], [4.4, 5]]],
23
+ F: [[[6.4, 0], [0, 0], [0, 10]], [[0, 5], [4.4, 5]]],
24
+ G: [[...arc(4, 5, 4, 5, -35, -360), [4.4, 5]]],
25
+ H: [[[0, 0], [0, 10]], [[7, 0], [7, 10]], [[0, 5], [7, 5]]],
26
+ I: [[[0, 0], [0, 10]]],
27
+ J: [[[5, 0], [5, 7.2], ...arc(2.5, 7.2, 2.5, 2.8, 0, 180)]],
28
+ K: [[[0, 0], [0, 10]], [[6.8, 0], [0, 6.6]], [[2.4, 4.2], [7, 10]]],
29
+ L: [[[0, 0], [0, 10], [6, 10]]],
30
+ M: [[[0, 10], [0, 0], [4.2, 10], [8.4, 0], [8.4, 10]]],
31
+ N: [[[0, 10], [0, 0], [7, 10], [7, 0]]],
32
+ O: [arc(4, 5, 4, 5, -90, 270)],
33
+ P: [[[0, 10], [0, 0], [4, 0], ...arc(4, 2.75, 2.8, 2.75, -90, 90), [0, 5.5]]],
34
+ Q: [arc(4, 5, 4, 5, -90, 270), [[4.6, 7], [8.2, 10.6]]],
35
+ R: [[[0, 10], [0, 0], [4, 0], ...arc(4, 2.75, 2.8, 2.75, -90, 90), [0, 5.5]], [[3.6, 5.5], [7, 10]]],
36
+ S: [[...arc(3.5, 2.55, 3.4, 2.55, -25, -270), ...arc(3.5, 7.5, 3.6, 2.5, -90, 155)]],
37
+ T: [[[0, 0], [7.2, 0]], [[3.6, 0], [3.6, 10]]],
38
+ U: [[[0, 0], [0, 6.4], ...arc(3.5, 6.4, 3.5, 3.6, 180, 0), [7, 0]]],
39
+ V: [[[0, 0], [3.6, 10], [7.2, 0]]],
40
+ W: [[[0, 0], [2.4, 10], [4.8, 0], [7.2, 10], [9.6, 0]]],
41
+ X: [[[0, 0], [7, 10]], [[7, 0], [0, 10]]],
42
+ Y: [[[0, 0], [3.6, 5], [7.2, 0]], [[3.6, 5], [3.6, 10]]],
43
+ Z: [[[0, 0], [7, 0], [0, 10], [7, 10]]],
44
+ 0: [arc(3.5, 5, 3.5, 5, -90, 270)],
45
+ 1: [[[1, 2], [3.4, 0], [3.4, 10]]],
46
+ 2: [[...arc(3.5, 3, 3.5, 3, -165, 15), [0, 10], [7, 10]]],
47
+ 3: [[...arc(3.4, 2.6, 3.2, 2.6, -160, 90), ...arc(3.4, 7.5, 3.6, 2.5, -90, 160)]],
48
+ 4: [[[5.4, 10], [5.4, 0], [0, 7], [7.4, 7]]],
49
+ 5: [[[6.4, 0], [1, 0], [0.5, 4.6], ...arc(3.5, 6.9, 3.5, 3.1, -140, 155)]],
50
+ 6: [[...arc(3.8, 5, 3.8, 5, -55, -270), ...arc(3.6, 7.1, 3.5, 2.9, 90, -270)]],
51
+ 7: [[[0, 0], [7, 0], [2.4, 10]]],
52
+ 8: [arc(3.5, 2.5, 3, 2.5, 90, 450), arc(3.5, 7.4, 3.5, 2.6, -90, 270)],
53
+ 9: [[...arc(3.4, 3, 3.4, 3, 0, 360), ...arc(3.2, 5, 3.6, 5, 0, 125)]],
54
+ '?': [[...arc(3, 2.6, 3, 2.6, -165, 60), [3, 6.6], [3, 7.4]], [[3, 9.5], [3, 10]]],
55
+ '%': [[[0, 10], [8, 0]], arc(1.6, 1.8, 1.6, 1.8, 0, 360), arc(6.4, 8.2, 1.6, 1.8, 0, 360)],
56
+ '.': [[[0.4, 9.4], [0.4, 10]]],
57
+ ',': [[[0.8, 9.4], [0.8, 10.2], [0, 11.8]]],
58
+ ':': [[[0.4, 3.4], [0.4, 4]], [[0.4, 9.4], [0.4, 10]]],
59
+ '-': [[[0, 5.5], [5, 5.5]]],
60
+ "'": [[[0.5, 0], [0.5, 3]]],
61
+ '/': [[[0, 10], [6, 0]]],
62
+ '>': [[[0, 5], [9, 5]], [[5.6, 1.8], [9, 5], [5.6, 8.2]]],
63
+ '#': [[[0, 5.8], [2.8, 9.6], [8.4, 0]]],
64
+ '+': [[[3.5, 2], [3.5, 8]], [[0.5, 5], [6.5, 5]]],
65
+ };
66
+ const GWID = {}; for (const k in G) GWID[k] = Math.max(...G[k].flat().map((p) => p[0]));
67
+ const ADV = 3.4, SPACE = 5.5;
68
+ const textW = (str, cap) => { const u = cap / 10; let w = 0; for (const ch of str) w += ch === ' ' ? SPACE : (GWID[ch] == null ? 5 : GWID[ch] + ADV); return (w - ADV) * u; };
69
+ const textStrokes = (str, x, y, cap) => {
70
+ const u = cap / 10, out = []; let px = x;
71
+ for (const ch of str) {
72
+ if (ch === ' ') { px += SPACE * u; continue; }
73
+ const g = G[ch]; if (!g) { px += 5 * u; continue; }
74
+ for (const s of g) out.push(s.map(([gx, gy]) => [px + gx * u, y + gy * u]));
75
+ px += (GWID[ch] + ADV) * u;
76
+ }
77
+ return out;
78
+ };
79
+
80
+ // ---------- beam buffer ----------
81
+ let B = null, XF = null, GA = 1;
82
+ const resetBeam = () => { B = { lines: [], segs: [], dots: [] }; XF = null; GA = 1; };
83
+ const plen = (p) => { let L = 0; for (let i = 1; i < p.length; i++) L += Math.hypot(p[i][0] - p[i - 1][0], p[i][1] - p[i - 1][1]); return L; };
84
+ const cutLen = (p, len) => {
85
+ const o = [p[0]];
86
+ for (let i = 1; i < p.length; i++) {
87
+ const d = Math.hypot(p[i][0] - p[i - 1][0], p[i][1] - p[i - 1][1]);
88
+ if (d >= len) { const k = d ? len / d : 0; o.push([p[i - 1][0] + (p[i][0] - p[i - 1][0]) * k, p[i - 1][1] + (p[i][1] - p[i - 1][1]) * k, p[i][2]]); return o; }
89
+ o.push(p[i]); len -= d;
90
+ }
91
+ return o;
92
+ };
93
+ const tailOf = (p, len) => { const L = plen(p); if (L <= len) return p; const r = p.slice().reverse(); return cutLen(r, len).reverse(); };
94
+ // emit strokes: o = { f (0..1 traced), a, col, w, timed (brightness ~ 1/speed), xf, retrace, head, dots }
95
+ function beam(strokes, o = {}) {
96
+ const a = (o.a == null ? 1 : o.a) * GA; if (a < 0.004) return;
97
+ const f = o.f == null ? 1 : o.f; if (f <= 0 || !strokes.length) return;
98
+ const col = o.col || P31, w = o.w || 2.2;
99
+ const tf = (p) => { let q = o.xf ? o.xf(p) : p; if (XF) q = XF(q); return q.length > 2 || p[2] == null ? q : [q[0], q[1], p[2]]; };
100
+ const lens = strokes.map(plen), total = lens.reduce((s, v) => s + v, 0);
101
+ let left = total * f, prev = null, last = null;
102
+ for (let k = 0; k < strokes.length && left > 0; k++) {
103
+ let s = strokes[k]; if (s.length < 2) continue;
104
+ if (lens[k] > left) s = cutLen(s, left);
105
+ left -= lens[k];
106
+ const pts = s.map(tf);
107
+ if (o.retrace !== false && prev) B.lines.push({ pts: [prev, pts[0]], a: a * 0.09, col, w: Math.max(1, w * 0.45) });
108
+ if (o.timed) {
109
+ let m = 0; const n = pts.length - 1; for (let i = 1; i <= n; i++) m += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); m /= Math.max(1, n);
110
+ const buckets = [];
111
+ for (let i = 1; i <= n; i++) {
112
+ const L = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]) + 0.01;
113
+ let fac = H.clamp(Math.pow(m / L, 0.65), 0.3, 2.2) * (pts[i][2] == null ? 1 : pts[i][2]);
114
+ const lv = Math.max(0, Math.min(9, Math.round((fac - 0.3) / 0.21)));
115
+ (buckets[lv] = buckets[lv] || []).push(pts[i - 1][0], pts[i - 1][1], pts[i][0], pts[i][1]);
116
+ }
117
+ buckets.forEach((arr, lv) => { if (arr) { const fac = 0.3 + lv * 0.21; B.segs.push({ arr, a: a * Math.min(1, fac), col, w: w * (0.75 + 0.22 * fac), hot: fac > 1.2 ? (fac - 1.2) * 0.5 * a : 0 }); } });
118
+ } else {
119
+ const L = lens[k] / Math.max(1, s.length - 1);
120
+ const sp = H.clamp(1.18 - L / 520, 0.62, 1.08);
121
+ B.lines.push({ pts, a: a * sp, col, w });
122
+ if (o.dots !== false && pts.length < 40) {
123
+ const dA = a * 0.55;
124
+ B.dots.push({ x: pts[0][0], y: pts[0][1], a: dA, r: w * 0.95, col });
125
+ B.dots.push({ x: pts[pts.length - 1][0], y: pts[pts.length - 1][1], a: dA, r: w * 0.95, col });
126
+ for (let i = 1; i < pts.length - 1; i++) {
127
+ const a1 = Math.atan2(pts[i][1] - pts[i - 1][1], pts[i][0] - pts[i - 1][0]), a2 = Math.atan2(pts[i + 1][1] - pts[i][1], pts[i + 1][0] - pts[i][0]);
128
+ let d = Math.abs(a2 - a1); if (d > Math.PI) d = 2 * Math.PI - d;
129
+ if (d > 0.85) B.dots.push({ x: pts[i][0], y: pts[i][1], a: dA * 1.1, r: w * 1.05, col });
130
+ }
131
+ }
132
+ }
133
+ prev = pts[pts.length - 1]; last = pts;
134
+ }
135
+ if (f < 1 && last && o.head !== false) {
136
+ const tl = tailOf(last, 60);
137
+ if (tl.length > 1) B.lines.push({ pts: tl, a: Math.min(1, a * 0.8), col, w: w * 1.5 });
138
+ const h = last[last.length - 1];
139
+ B.dots.push({ x: h[0], y: h[1], a: Math.min(1, a * 1.3), r: w * 2.3, col, hot: 1 });
140
+ }
141
+ }
142
+ const dot = (x, y, r, a, col = P31) => { let p = [x, y]; if (XF) p = XF(p); B.dots.push({ x: p[0], y: p[1], r, a: a * GA, col, hot: 1 }); };
143
+ const text = (str, x, y, cap, o = {}) => {
144
+ const w = textW(str, cap), x0 = o.align === 'c' ? x - w / 2 : o.align === 'r' ? x - w : x;
145
+ beam(textStrokes(str, x0, y, cap), Object.assign({ w: H.clamp(cap * 0.068, 2.1, 5.2) }, o));
146
+ return w;
147
+ };
148
+ const curve = (fn, n) => { const o = []; for (let i = 0; i <= n; i++) o.push(fn(i / n)); return o; };
149
+ const ring = (cx, cy, rx, ry, n = 28) => curve((u) => [cx + rx * Math.cos(u * 6.2832), cy + ry * Math.sin(u * 6.2832)], n);
150
+ const arrowS = (x1, y1, x2, y2, hs = 15) => { const an = Math.atan2(y2 - y1, x2 - x1); return [[[x1, y1], [x2, y2]], [[x2 + Math.cos(an + 2.55) * hs, y2 + Math.sin(an + 2.55) * hs], [x2, y2], [x2 + Math.cos(an - 2.55) * hs, y2 + Math.sin(an - 2.55) * hs]]]; };
151
+ const catmull = (P, seg = 6) => {
152
+ const o = [];
153
+ for (let i = 0; i < P.length - 1; i++) {
154
+ const p0 = P[Math.max(0, i - 1)], p1 = P[i], p2 = P[i + 1], p3 = P[Math.min(P.length - 1, i + 2)];
155
+ for (let k = 0; k < seg; k++) {
156
+ const t = k / seg, t2 = t * t, t3 = t2 * t;
157
+ o.push([0, 1].map((d) => 0.5 * (2 * p1[d] + (-p0[d] + p2[d]) * t + (2 * p0[d] - 5 * p1[d] + 4 * p2[d] - p3[d]) * t2 + (-p0[d] + 3 * p1[d] - 3 * p2[d] + p3[d]) * t3)));
158
+ }
159
+ }
160
+ o.push(P[P.length - 1]); return o;
161
+ };
162
+
163
+ // ---------- beam figures (unit-size, y down) ----------
164
+ const tx = (S, x, y, s) => S.map((st) => st.map(([px, py]) => [x + px * s, y + py * s]));
165
+ const CAT = (() => {
166
+ const head = [[0.82, -0.39], ...arc(0, 0.1, 1, 0.85, -35, 215, 34), [-0.86, -1.18], [-0.34, -0.69], ...arc(0, 0.1, 1, 0.85, -110, -70, 4), [0.86, -1.18], [0.82, -0.39]];
167
+ const eyeL = arc(-0.38, -0.02, 0.17, 0.11, 0, 360, 14), eyeR = arc(0.38, -0.02, 0.17, 0.11, 0, 360, 14);
168
+ return [head, eyeL, [[-0.38, -0.12], [-0.38, 0.08]], eyeR, [[0.38, -0.12], [0.38, 0.08]],
169
+ [[-0.1, 0.25], [0.1, 0.25], [0, 0.37], [-0.1, 0.25]], [[0, 0.37], [0, 0.44], ...arc(-0.12, 0.44, 0.12, 0.11, 0, 165, 8)], arc(0.12, 0.44, 0.12, 0.11, 180, 15, 8),
170
+ [[-0.36, 0.3], [-1.28, 0.14]], [[-0.36, 0.36], [-1.3, 0.38]], [[0.36, 0.3], [1.28, 0.14]], [[0.36, 0.36], [1.3, 0.38]]];
171
+ })();
172
+ const DOG = (() => {
173
+ const head = arc(0, 0.05, 0.74, 0.86, -90, 270, 34);
174
+ const earL = catmull([[-0.5, -0.66], [-0.92, -0.74], [-1.22, -0.42], [-1.2, 0.18], [-0.98, 0.5], [-0.78, 0.3], [-0.72, -0.15]]);
175
+ const earR = earL.map(([x, y]) => [-x, y]);
176
+ const nose = []; for (let i = 0; i <= 40; i++) { const u = i / 40, a = u * 6.2832 * 3.2, r = 1 - u * 0.75; nose.push([Math.cos(a) * 0.15 * r, 0.3 + Math.sin(a) * 0.09 * r]); }
177
+ return [head, earL, earR, arc(-0.3, -0.14, 0.09, 0.09, 0, 360, 10), arc(0.3, -0.14, 0.09, 0.09, 0, 360, 10), arc(0, 0.42, 0.36, 0.26, 200, 340, 12), nose,
178
+ [[0, 0.39], [0, 0.52], ...arc(-0.13, 0.52, 0.13, 0.1, 0, 165, 8)], arc(0.13, 0.52, 0.13, 0.1, 180, 15, 8), [[-0.09, 0.6], ...arc(0, 0.62, 0.09, 0.12, 180, 0, 8)]];
179
+ })();
180
+
181
+ // neural net wireframe: 3-4-2 nodes in 3D, rotates about Y
182
+ const LAY = [3, 4, 2];
183
+ function netGeo(cx, cy, w, h, rot) {
184
+ const nodes = [];
185
+ LAY.forEach((n, l) => { for (let k = 0; k < n; k++) {
186
+ const x = (l - 1) * w / 2, y = (k - (n - 1) / 2) * h / 3.6, z = (k % 2 ? 1 : -1) * w * 0.12 + (l - 1) * w * 0.05;
187
+ const xr = x * Math.cos(rot) - z * Math.sin(rot), zr = x * Math.sin(rot) + z * Math.cos(rot), p = 1 / (1 + zr / (w * 3.2));
188
+ nodes.push({ x: cx + xr * p, y: cy + y * p, s: p, l });
189
+ } });
190
+ const edges = []; let base = 0;
191
+ for (let l = 0; l < LAY.length - 1; l++) { for (let i = 0; i < LAY[l]; i++) for (let j = 0; j < LAY[l + 1]; j++) edges.push([base + i, base + LAY[l] + j, l]); base += LAY[l]; }
192
+ return { nodes, edges };
193
+ }
194
+ const W0 = (() => { const r = H.rng(5); return Array.from({ length: 20 }, () => r.range(0.25, 1)); })();
195
+ const W1 = (() => { const r = H.rng(9); return Array.from({ length: 20 }, () => r.range(0.25, 1)); })();
196
+ // o: { f, a, wk (0..1 blend W0->W1), act[] node flash, bp(l) backprop amber per layer, nodeR }
197
+ function net(cx, cy, w, h, rot, o = {}) {
198
+ const g = netGeo(cx, cy, w, h, rot), f = o.f == null ? 1 : o.f, a = o.a == null ? 1 : o.a, R = o.nodeR || 12;
199
+ g.edges.forEach(([i, j, l], e) => {
200
+ const A = g.nodes[i], Bn = g.nodes[j], wt = H.lerp(W0[e], W1[e], o.wk || 0);
201
+ const amb = o.bp ? o.bp(l) : 0, col = amb > 0 ? H.mix(P31, AMBER, amb) : P31;
202
+ const dx = Bn.x - A.x, dy = Bn.y - A.y, L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L;
203
+ beam([[[A.x + ux * R * A.s, A.y + uy * R * A.s], [Bn.x - ux * R * Bn.s, Bn.y - uy * R * Bn.s]]], { f: H.clamp(f * 1.6 - 0.25 - e * 0.012), a: a * (0.3 + 0.7 * wt) * (1 + amb * 0.5), col, w: 1.2 + wt * 1.5, dots: false, retrace: false });
204
+ });
205
+ g.nodes.forEach((n, i) => {
206
+ const fl = o.act ? o.act(n.l) : 0;
207
+ beam([ring(n.x, n.y, R * n.s, R * n.s, 18)], { f: H.clamp(f * 1.3 - i * 0.03), a: a * (0.9 + fl), w: 2.4, col: o.nodeCol ? o.nodeCol(n.l) : P31, dots: false });
208
+ if (fl > 0.05) dot(n.x, n.y, R * 0.7 * n.s, Math.min(1, fl) * a);
209
+ });
210
+ return g;
211
+ }
212
+
213
+ // spiral-wound sphere (one continuous beam), e = latitude extent 0..1, returns timed points
214
+ function sphere(cx, cy, R, spin, tilt, e, wob, n = 1500, turns = 22) {
215
+ const ct = Math.cos(tilt), st = Math.sin(tilt);
216
+ return curve((s) => {
217
+ const phi = Math.PI / 2 + (s - 0.5) * Math.PI * e, th = 6.2832 * turns * s + spin;
218
+ const rr = 1 + wob * Math.sin(5 * th - spin * 2 + 3 * phi);
219
+ let x = Math.sin(phi) * Math.cos(th) * rr, y = -Math.cos(phi) * rr, z = Math.sin(phi) * Math.sin(th) * rr;
220
+ const y2 = y * ct - z * st, z2 = y * st + z * ct;
221
+ return [cx + x * R, cy + y2 * R, 0.62 + 0.38 * (z2 + 1) / 2];
222
+ }, n);
223
+ }
224
+
225
+ // ---------- cached layers ----------
226
+ const C = {};
227
+ const rrect = (g, x, y, w, h, r) => { g.beginPath(); g.moveTo(x + r, y); g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r); g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath(); };
228
+ const INK = '#22313f', BLUE = '#3d6688';
229
+ const VD = ['5', '2', '1', '.5', '.2', '.1', '50', '20', '10', '5', '2'];
230
+ const TD = ['X-Y', '.5', '.2', '.1', '50', '20', '10', '5', '2', '1', '.5'];
231
+ const KA = (idx) => (-135 + idx * 27) * Math.PI / 180;
232
+ function lbl(g, s, x, y, size = 11, align = 'center', col = INK, wt = 600) { g.font = `${wt} ${size}px "Inter"`; g.fillStyle = col; g.textAlign = align; g.textBaseline = 'middle'; if (g.letterSpacing !== undefined) g.letterSpacing = '1px'; g.fillText(s, x, y); if (g.letterSpacing !== undefined) g.letterSpacing = '0px'; }
233
+ function scale(g, x, y, r, labels, mvFrom) {
234
+ labels.forEach((s, i) => {
235
+ const a = KA(i), ca = Math.sin(a), sa = -Math.cos(a);
236
+ g.strokeStyle = INK; g.lineWidth = 1.4; g.beginPath(); g.moveTo(x + ca * (r - 9), y + sa * (r - 9)); g.lineTo(x + ca * (r - 2), y + sa * (r - 2)); g.stroke();
237
+ lbl(g, s, x + ca * (r + 9), y + sa * (r + 9), 10, 'center', mvFrom != null && i >= mvFrom ? BLUE : INK, 700);
238
+ });
239
+ }
240
+ function buildPanel(W, H_) {
241
+ const c = H.layer(W, H_), g = c.getContext('2d');
242
+ // instrument panel: warm light-grey enamel
243
+ const pg = g.createLinearGradient(0, 0, 0, H_); pg.addColorStop(0, '#cdcabf'); pg.addColorStop(1, '#b3b0a5');
244
+ g.fillStyle = pg; g.fillRect(0, 0, W, H_);
245
+ const r = H.rng(31);
246
+ for (let i = 0; i < 14000; i++) { const v = r() < 0.5 ? 0 : 255; g.fillStyle = `rgba(${v},${v},${v},${0.035 + r() * 0.03})`; g.fillRect(r() * W, r() * H_, 1 + r(), 1 + r()); }
247
+ // CRT bezel: dark moulded plastic around the glass
248
+ g.save(); g.shadowColor = 'rgba(0,0,0,0.45)'; g.shadowBlur = 18; g.shadowOffsetY = 6;
249
+ rrect(g, 6, 4, 890, 712, 34); const bg = g.createLinearGradient(0, 0, 0, 720); bg.addColorStop(0, '#2e3032'); bg.addColorStop(1, '#151617'); g.fillStyle = bg; g.fill(); g.restore();
250
+ rrect(g, 6.5, 4.5, 889, 711, 34); g.strokeStyle = 'rgba(255,255,255,0.12)'; g.lineWidth = 1; g.stroke();
251
+ // stepped inner lip
252
+ rrect(g, GL.x - 10, GL.y - 10, GL.w + 20, GL.h + 20, GL.r + 8); g.fillStyle = '#0b0c0c'; g.fill();
253
+ rrect(g, GL.x - 10, GL.y - 10, GL.w + 20, GL.h + 20, GL.r + 8); g.strokeStyle = 'rgba(255,255,255,0.07)'; g.lineWidth = 1.5; g.stroke();
254
+ // bezel screws
255
+ [[18, 16], [884, 16], [18, 704], [884, 704]].forEach(([x, y]) => {
256
+ const sg = g.createRadialGradient(x - 2, y - 2, 1, x, y, 7); sg.addColorStop(0, '#d8d8d4'); sg.addColorStop(1, '#5c5c58');
257
+ g.fillStyle = sg; g.beginPath(); g.arc(x, y, 6, 0, 7); g.fill(); g.strokeStyle = '#2a2a28'; g.lineWidth = 1.6; g.beginPath(); g.moveTo(x - 4, y - 2); g.lineTo(x + 4, y + 2); g.stroke();
258
+ });
259
+ // ---- right-hand front panel
260
+ const X0 = 902;
261
+ g.font = '700 30px "Space Grotesk"'; g.fillStyle = INK; g.textAlign = 'left'; g.textBaseline = 'alphabetic'; g.fillText('XY-2', 920, 58);
262
+ lbl(g, 'OSCILLOGRAPH', 1012, 40, 12, 'left', INK, 800); lbl(g, 'DUAL TRACE · 20 MHz', 1012, 60, 10, 'left', BLUE, 600);
263
+ g.fillStyle = BLUE; g.fillRect(916, 74, 350, 3);
264
+ // row 1 labels
265
+ lbl(g, 'INTENSITY', 944, 160, 9); lbl(g, 'FOCUS', 1012, 160, 9); lbl(g, 'BEAM FIND', 1080, 160, 9); lbl(g, 'POWER', 1172, 160, 9);
266
+ // vertical section
267
+ g.strokeStyle = BLUE; g.lineWidth = 1.6; rrect(g, 914, 184, 356, 232, 8); g.stroke();
268
+ g.fillStyle = '#c4c1b6'; g.fillRect(1040, 176, 104, 16); lbl(g, 'VERTICAL', 1092, 184, 11, 'center', BLUE, 800);
269
+ scale(g, 986, 296, 58, VD, 6); scale(g, 1198, 296, 58, VD, 6);
270
+ lbl(g, 'VOLTS/DIV', 1092, 296, 10, 'center', INK, 800); lbl(g, 'mV', 1092, 316, 10, 'center', BLUE, 700);
271
+ lbl(g, 'CH 1 · X', 986, 394, 11, 'center', INK, 800); lbl(g, 'CH 2 · Y', 1198, 394, 11, 'center', INK, 800);
272
+ // horizontal section
273
+ g.strokeStyle = BLUE; rrect(g, 914, 432, 356, 182, 8); g.stroke();
274
+ g.fillStyle = '#bebbb0'; g.fillRect(1030, 424, 124, 16); lbl(g, 'HORIZONTAL', 1092, 432, 11, 'center', BLUE, 800);
275
+ scale(g, 1004, 524, 58, TD, 4); lbl(g, 'TIME/DIV', 1004, 600, 10, 'center', INK, 800);
276
+ lbl(g, 'MODE', 1186, 456, 10, 'center', INK, 800);
277
+ lbl(g, 'X-Y', 1150, 482, 10, 'right', INK, 800); lbl(g, 'ALT', 1150, 522, 10, 'right', INK, 800); lbl(g, 'CHOP', 1150, 562, 10, 'right', INK, 800);
278
+ // inputs
279
+ lbl(g, 'CH 1 OR X', 960, 634, 9); lbl(g, 'CH 2 OR Y', 1060, 634, 9); lbl(g, 'EXT TRIG', 1160, 634, 9); lbl(g, 'CAL .5V', 1242, 634, 9);
280
+ lbl(g, '1MΩ 20pF 400V PK MAX', 1060, 706, 8, 'center', BLUE, 600);
281
+ [960, 1060, 1160].forEach((x) => {
282
+ const y = 666; g.save(); g.shadowColor = 'rgba(0,0,0,.4)'; g.shadowBlur = 6; g.shadowOffsetY = 3;
283
+ g.beginPath(); for (let i = 0; i < 6; i++) { const a = i * Math.PI / 3 + 0.5; g[i ? 'lineTo' : 'moveTo'](x + Math.cos(a) * 21, y + Math.sin(a) * 21); } g.closePath();
284
+ const ng = g.createLinearGradient(x - 20, y - 20, x + 20, y + 20); ng.addColorStop(0, '#e9e9e4'); ng.addColorStop(1, '#7d7d78'); g.fillStyle = ng; g.fill(); g.restore();
285
+ const cg = g.createRadialGradient(x - 4, y - 4, 2, x, y, 15); cg.addColorStop(0, '#f2f2ee'); cg.addColorStop(1, '#8c8c87'); g.fillStyle = cg; g.beginPath(); g.arc(x, y, 14, 0, 7); g.fill();
286
+ g.fillStyle = '#1c1c1b'; g.beginPath(); g.arc(x, y, 8.5, 0, 7); g.fill(); g.fillStyle = '#d8b25a'; g.beginPath(); g.arc(x, y, 2.6, 0, 7); g.fill();
287
+ g.fillStyle = '#b8b8b2'; [[-15, 0], [15, 0]].forEach(([dx]) => { g.beginPath(); g.arc(x + dx, y, 3, 0, 7); g.fill(); });
288
+ });
289
+ g.fillStyle = '#9a9a94'; g.beginPath(); g.arc(1242, 666, 9, 0, 7); g.fill(); g.strokeStyle = '#d8d8d0'; g.lineWidth = 3; g.beginPath(); g.arc(1242, 656, 7, Math.PI, 0); g.stroke();
290
+ // panel edge lighting + seam between bezel and panel
291
+ const sh = g.createLinearGradient(896, 0, 930, 0); sh.addColorStop(0, 'rgba(0,0,0,0.25)'); sh.addColorStop(1, 'rgba(0,0,0,0)'); g.fillStyle = sh; g.fillRect(896, 0, 34, H_);
292
+ return c;
293
+ }
294
+ function buildGlass(W, H_) {
295
+ const c = H.layer(W, H_), g = c.getContext('2d');
296
+ const rg = g.createRadialGradient(CX, CY, 60, CX, CY, 560); rg.addColorStop(0, '#111a14'); rg.addColorStop(0.7, '#0a100c'); rg.addColorStop(1, '#040605');
297
+ rrect(g, GL.x, GL.y, GL.w, GL.h, GL.r); g.fillStyle = rg; g.fill();
298
+ const r = H.rng(77);
299
+ for (let i = 0; i < 26000; i++) { g.fillStyle = `rgba(150,190,150,${r() * 0.05})`; g.fillRect(GL.x + r() * GL.w, GL.y + r() * GL.h, 1, 1); }
300
+ return c;
301
+ }
302
+ function buildGrat(W, H_) {
303
+ const c = H.layer(W, H_), g = c.getContext('2d');
304
+ g.strokeStyle = 'rgba(170,205,180,0.22)'; g.lineWidth = 1;
305
+ for (let i = 0; i <= 10; i++) { g.beginPath(); g.moveTo(GX + i * DIV + 0.5, GY); g.lineTo(GX + i * DIV + 0.5, GY + GH); g.stroke(); }
306
+ for (let j = 0; j <= 8; j++) { g.beginPath(); g.moveTo(GX, GY + j * DIV + 0.5); g.lineTo(GX + GW, GY + j * DIV + 0.5); g.stroke(); }
307
+ g.strokeStyle = 'rgba(170,205,180,0.34)';
308
+ for (let i = 0; i <= 50; i++) { const x = GX + i * DIV / 5 + 0.5, l = i % 5 ? 5 : 0; if (l) { g.beginPath(); g.moveTo(x, CY - l); g.lineTo(x, CY + l); g.stroke(); } }
309
+ for (let j = 0; j <= 40; j++) { const y = GY + j * DIV / 5 + 0.5, l = j % 5 ? 5 : 0; if (l) { g.beginPath(); g.moveTo(CX - l, y); g.lineTo(CX + l, y); g.stroke(); } }
310
+ // 0% / 10% / 90% / 100% rise-time marks
311
+ g.setLineDash([2, 4]); g.strokeStyle = 'rgba(170,205,180,0.26)';
312
+ [CY - 2.5 * DIV, CY + 2.5 * DIV, CY - 2 * DIV, CY + 2 * DIV].forEach((y) => { g.beginPath(); g.moveTo(GX, y + 0.5); g.lineTo(GX + GW, y + 0.5); g.stroke(); });
313
+ g.setLineDash([]);
314
+ g.font = '600 10px "Inter"'; g.fillStyle = 'rgba(170,205,180,0.4)'; g.textAlign = 'left'; g.textBaseline = 'middle';
315
+ [['100', CY - 2.5 * DIV], ['90', CY - 2 * DIV], ['10', CY + 2 * DIV], ['0%', CY + 2.5 * DIV]].forEach(([s, y]) => g.fillText(s, GX + 4, y - 7));
316
+ return c;
317
+ }
318
+ function buildRefl(W, H_) {
319
+ const c = H.layer(W, H_), g = c.getContext('2d');
320
+ rrect(g, GL.x, GL.y, GL.w, GL.h, GL.r); g.save(); g.clip();
321
+ // curved-faceplate edge darkening
322
+ const ed = g.createRadialGradient(CX, CY, 300, CX, CY, 600); ed.addColorStop(0, 'rgba(0,0,0,0)'); ed.addColorStop(1, 'rgba(0,0,0,0.55)'); g.fillStyle = ed; g.fillRect(0, 0, W, H_);
323
+ // soft window reflection across the upper left of the glass
324
+ const rf = g.createLinearGradient(GL.x, GL.y, GL.x + 520, GL.y + 380); rf.addColorStop(0, 'rgba(220,235,255,0.075)'); rf.addColorStop(0.45, 'rgba(220,235,255,0.025)'); rf.addColorStop(0.5, 'rgba(220,235,255,0)');
325
+ g.fillStyle = rf; g.beginPath(); g.moveTo(GL.x, GL.y); g.lineTo(GL.x + 640, GL.y); g.lineTo(GL.x, GL.y + 470); g.fill();
326
+ g.restore();
327
+ rrect(g, GL.x + 1, GL.y + 1, GL.w - 2, GL.h - 2, GL.r); g.strokeStyle = 'rgba(200,230,210,0.10)'; g.lineWidth = 2; g.stroke();
328
+ return c;
329
+ }
330
+ const get = (k, fn, W, H_) => C[k] || (C[k] = fn(W, H_));
331
+
332
+ // ---------- dynamic panel parts ----------
333
+ function knob(g, x, y, r, ang, skirt) {
334
+ const sh = g.createRadialGradient(x + 3, y + 6, r * 0.6, x + 3, y + 6, r * (skirt ? 1.5 : 1.35)); sh.addColorStop(0, 'rgba(0,0,0,0.45)'); sh.addColorStop(1, 'rgba(0,0,0,0)');
335
+ g.fillStyle = sh; g.beginPath(); g.arc(x + 3, y + 6, r * (skirt ? 1.5 : 1.35), 0, 7); g.fill();
336
+ const sx = Math.sin(ang), sy = -Math.cos(ang);
337
+ if (skirt) {
338
+ const kg = g.createRadialGradient(x - r * 0.4, y - r * 0.5, 2, x, y, r * 1.3); kg.addColorStop(0, '#4a4b4c'); kg.addColorStop(1, '#151515');
339
+ g.fillStyle = kg; g.beginPath(); g.arc(x, y, r * 1.28, 0, 7); g.fill();
340
+ g.fillStyle = '#f0efe8'; g.beginPath(); g.moveTo(x + sx * r * 1.26, y + sy * r * 1.26); g.lineTo(x + sx * r * 1.05 + sy * 4, y + sy * r * 1.05 - sx * 4); g.lineTo(x + sx * r * 1.05 - sy * 4, y + sy * r * 1.05 + sx * 4); g.fill();
341
+ }
342
+ const bg = g.createRadialGradient(x - r * 0.35, y - r * 0.45, 1, x, y, r); bg.addColorStop(0, '#3e3e3e'); bg.addColorStop(1, '#0c0c0c');
343
+ g.fillStyle = bg; g.beginPath(); g.arc(x, y, r, 0, 7); g.fill();
344
+ g.strokeStyle = 'rgba(255,255,255,0.13)'; g.lineWidth = 1;
345
+ for (let i = 0; i < 40; i++) { const a = ang + i * Math.PI / 20; g.beginPath(); g.moveTo(x + Math.cos(a) * r * 0.86, y + Math.sin(a) * r * 0.86); g.lineTo(x + Math.cos(a) * r * 0.99, y + Math.sin(a) * r * 0.99); g.stroke(); }
346
+ const cg = g.createLinearGradient(x - r, y - r, x + r * 0.6, y + r * 0.6); cg.addColorStop(0, '#f4f4f0'); cg.addColorStop(1, '#8e8e8a');
347
+ g.fillStyle = cg; g.beginPath(); g.arc(x, y, r * 0.6, 0, 7); g.fill();
348
+ g.strokeStyle = 'rgba(0,0,0,0.08)'; for (let k = 2; k < 8; k++) { g.beginPath(); g.arc(x, y, r * 0.075 * k, 0, 7); g.stroke(); }
349
+ g.strokeStyle = '#fbfbf6'; g.lineWidth = Math.max(2, r * 0.08); g.lineCap = 'round';
350
+ g.beginPath(); g.moveTo(x + sx * r * 0.62, y + sy * r * 0.62); g.lineTo(x + sx * r * 0.93, y + sy * r * 0.93); g.stroke();
351
+ g.strokeStyle = '#2a2a2a'; g.lineWidth = 2; g.beginPath(); g.moveTo(x + sx * r * 0.1, y + sy * r * 0.1); g.lineTo(x + sx * r * 0.55, y + sy * r * 0.55); g.stroke(); g.lineCap = 'butt';
352
+ }
353
+ function button(g, x, y, pressed) {
354
+ g.fillStyle = 'rgba(0,0,0,0.35)'; rrect(g, x - 17, y - 9, 36, 22, 4); g.fill();
355
+ const bg = g.createLinearGradient(0, y - 11, 0, y + 11); bg.addColorStop(0, pressed ? '#9c9a92' : '#ecebe5'); bg.addColorStop(1, pressed ? '#c9c7bf' : '#a9a79f');
356
+ g.fillStyle = bg; rrect(g, x - 18, y - (pressed ? 9 : 11), 36, 20, 4); g.fill();
357
+ }
358
+ function led(g, x, y, on, col) {
359
+ if (on) { const lg = g.createRadialGradient(x, y, 0, x, y, 16); lg.addColorStop(0, H.rgba(col, 0.9)); lg.addColorStop(0.3, H.rgba(col, 0.35)); lg.addColorStop(1, H.rgba(col, 0)); g.fillStyle = lg; g.beginPath(); g.arc(x, y, 16, 0, 7); g.fill(); }
360
+ g.fillStyle = on ? H.rgba(H.mix(col, [255, 255, 255], 0.5), 1) : '#3b2a22'; g.beginPath(); g.arc(x, y, 4.5, 0, 7); g.fill();
361
+ g.strokeStyle = 'rgba(0,0,0,.5)'; g.lineWidth = 1; g.stroke();
362
+ }
363
+ function panelLive(g, st) {
364
+ const k = st.kn;
365
+ knob(g, 944, 128, 17, (k.inten * 270 - 135) * Math.PI / 180, false);
366
+ knob(g, 1012, 128, 17, 0.3, false);
367
+ button(g, 1080, 128, false);
368
+ button(g, 1172, 128, st.power > 0);
369
+ led(g, 1222, 128, st.power > 0.5, [255, 140, 40]);
370
+ knob(g, 986, 296, 34, k.ch1, true);
371
+ knob(g, 1198, 296, 34, k.ch2, true);
372
+ knob(g, 1004, 524, 34, k.tdiv, true);
373
+ button(g, 1186, 482, st.xy); led(g, 1232, 482, st.xy && st.power > 0.5, [120, 255, 90]);
374
+ button(g, 1186, 522, !st.xy); led(g, 1232, 522, !st.xy && st.power > 0.5, [120, 255, 90]);
375
+ button(g, 1186, 562, false); led(g, 1232, 562, false, [120, 255, 90]);
376
+ }
377
+ const snapKnob = (t, changes, idx0) => { // changes: [[time, idx]] -> angle with a quick detent snap
378
+ let a = KA(idx0);
379
+ for (const [tc, idx] of changes) { if (t < tc) break; const prev = a, p = H.clamp((t - tc) / 0.12); a = H.lerp(prev, KA(idx), H.ease.back(p)); }
380
+ return a;
381
+ };
382
+
383
+ // ---------- frame render ----------
384
+ const L = {};
385
+ const lay = (k, w, h) => L[k] || (L[k] = H.layer(w, h));
386
+ function renderFrame(ctx, W, H_, st) {
387
+ ctx.drawImage(get('panel', buildPanel, W, H_), 0, 0);
388
+ panelLive(ctx, st);
389
+ // beam layer
390
+ const bl = lay('beam', W, H_), b = bl.getContext('2d');
391
+ b.setTransform(1, 0, 0, 1, 0, 0); b.globalCompositeOperation = 'source-over'; b.clearRect(0, 0, W, H_);
392
+ b.globalCompositeOperation = 'lighter'; b.lineCap = 'round'; b.lineJoin = 'round';
393
+ const I = st.inten;
394
+ // phosphor core colour. Thin traces read white-green; a wide (heavily driven) trace keeps its P31 body and only
395
+ // its centre goes white-hot, so big lettering stays phosphor green instead of flat white.
396
+ const WH = [235, 255, 225];
397
+ const coreOf = (col, hot, w) => H.mix(col, WH, w > 3 ? 0.36 + 0.16 * Math.min(1, hot || 0) : 0.55 + 0.25 * Math.min(1, hot || 0));
398
+ const pathL = (it) => { b.beginPath(); it.pts.forEach((p, i) => (i ? b.lineTo(p[0], p[1]) : b.moveTo(p[0], p[1]))); };
399
+ const pathS = (it) => { b.beginPath(); const A = it.arr; for (let i = 0; i < A.length; i += 4) { b.moveTo(A[i], A[i + 1]); b.lineTo(A[i + 2], A[i + 3]); } };
400
+ const strokeAll = (pass) => {
401
+ for (const it of B.lines) {
402
+ const a = Math.min(1, it.a * I); if (a < 0.003) continue;
403
+ b.strokeStyle = pass ? H.rgba(coreOf(it.col, 0, it.w), a) : H.rgba(it.col, a * 0.2); b.lineWidth = pass ? it.w : it.w * 3.4;
404
+ pathL(it); b.stroke();
405
+ if (pass && it.w > 3) { b.strokeStyle = H.rgba(H.mix(it.col, WH, 0.82), a * 0.75); b.lineWidth = it.w * 0.42; b.stroke(); }
406
+ }
407
+ for (const it of B.segs) {
408
+ const a = Math.min(1, it.a * I); if (a < 0.003) continue;
409
+ b.strokeStyle = pass ? H.rgba(coreOf(it.col, it.hot, it.w), a) : H.rgba(it.col, a * 0.2); b.lineWidth = pass ? it.w : it.w * 3.4;
410
+ pathS(it); b.stroke();
411
+ if (pass && it.w > 3) { b.strokeStyle = H.rgba(H.mix(it.col, WH, 0.82), a * 0.75); b.lineWidth = it.w * 0.42; b.stroke(); }
412
+ }
413
+ };
414
+ strokeAll(0); strokeAll(1);
415
+ for (const d of B.dots) {
416
+ const a = Math.min(1, d.a * I); if (a < 0.01) continue;
417
+ b.fillStyle = H.rgba(d.col, a * 0.28); b.beginPath(); b.arc(d.x, d.y, d.r * 2.4, 0, 7); b.fill();
418
+ // dwell dots on wide lettering: green body with a small white-hot centre (beam head dots stay fully hot)
419
+ if (!d.hot && d.r > 3) {
420
+ b.fillStyle = H.rgba(coreOf(d.col, 0, d.r), a); b.beginPath(); b.arc(d.x, d.y, d.r, 0, 7); b.fill();
421
+ b.fillStyle = H.rgba(H.mix(d.col, WH, 0.85), a * 0.8); b.beginPath(); b.arc(d.x, d.y, d.r * 0.45, 0, 7); b.fill();
422
+ } else { b.fillStyle = H.rgba(coreOf(d.col, 1), a); b.beginPath(); b.arc(d.x, d.y, d.r, 0, 7); b.fill(); }
423
+ }
424
+ // bloom on small buffers
425
+ const s1 = lay('s1', W / 2, H_ / 2), g1 = s1.getContext('2d'), s2 = lay('s2', W / 4, H_ / 4), g2 = s2.getContext('2d');
426
+ g1.globalCompositeOperation = 'source-over'; g1.clearRect(0, 0, W / 2, H_ / 2); g1.filter = 'blur(3px)'; g1.drawImage(bl, 0, 0, W / 2, H_ / 2); g1.filter = 'none';
427
+ g2.globalCompositeOperation = 'source-over'; g2.clearRect(0, 0, W / 4, H_ / 4); g2.filter = 'blur(5px)'; g2.drawImage(bl, 0, 0, W / 4, H_ / 4); g2.filter = 'none';
428
+ // screen
429
+ ctx.save(); rrect(ctx, GL.x, GL.y, GL.w, GL.h, GL.r); ctx.clip();
430
+ ctx.drawImage(get('glass', buildGlass, W, H_), 0, 0);
431
+ // faint phosphor ambient lift from total beam energy
432
+ ctx.globalCompositeOperation = 'lighter';
433
+ ctx.globalAlpha = Math.min(1, 0.9 * st.bloom); ctx.drawImage(s2, 0, 0, W, H_);
434
+ ctx.globalAlpha = Math.min(1, 0.85 * Math.sqrt(st.bloom)); ctx.drawImage(s1, 0, 0, W, H_);
435
+ ctx.globalAlpha = 1; ctx.drawImage(bl, 0, 0);
436
+ if (st.bloom > 1.05) { ctx.globalAlpha = Math.min(1, (st.bloom - 1) * 0.5); ctx.drawImage(s2, -8, -8, W + 16, H_ + 16); }
437
+ ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over';
438
+ ctx.drawImage(get('grat', buildGrat, W, H_), 0, 0);
439
+ ctx.drawImage(get('refl', buildRefl, W, H_), 0, 0);
440
+ ctx.restore();
441
+ // phosphor light spilling onto the bezel lip
442
+ ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.globalAlpha = 0.28; ctx.drawImage(s2, -6, -6, W + 12, H_ + 12); ctx.restore();
443
+ H.vignette(ctx, W, H_, 0.35);
444
+ }
445
+ const baseState = () => ({ power: 1, xy: true, inten: 1, bloom: 1, kn: { inten: 0.55, ch1: KA(3), ch2: KA(3), tdiv: KA(0) } });
446
+
447
+ // ================= STILL / CLIP =================
448
+ function drawStill(ctx, W, H_, t) {
449
+ resetBeam();
450
+ const st = baseState(), ph = (a, d) => H.ease.inOut(H.clamp((t - a) / d));
451
+ st.power = t > 0.12 ? 1 : 0;
452
+ st.xy = t >= 1.0;
453
+ st.kn.tdiv = snapKnob(t, [[1.0, 0]], 5);
454
+ st.kn.inten = 0.3 + 0.25 * ph(0.12, 0.5);
455
+ const jit = H.rng(900 + Math.floor(t * 30));
456
+ const spin = t * 0.9, R = 178, sx = CX, sy = 296;
457
+ if (t >= 0.12) {
458
+ // power-on: dot blooms at centre, then the timebase sweep pulls it into a line
459
+ const dotA = ph(0.12, 0.25) * (1 - ph(0.4, 0.2));
460
+ if (dotA > 0) dot(CX, CY, 5 + 4 * dotA, dotA);
461
+ if (t < 1.0) {
462
+ const fx = ph(0.35, 0.3), amp = 150 * ph(0.55, 0.45);
463
+ beam([curve((u) => [GX + u * GW, CY - amp * Math.sin(6.2832 * (2 * u + t * 0.6))], 220)], { f: fx, w: 2.4, timed: true });
464
+ } else if (t < 1.65) {
465
+ // X-Y pressed: the swept sine folds into a circle
466
+ const m = H.ease.inOut(H.clamp((t - 1.0) / 0.6));
467
+ beam([curve((u) => {
468
+ const tb = [GX + u * GW, CY - 150 * Math.sin(6.2832 * (2 * u + t * 0.6))];
469
+ const xy = [sx + R * Math.cos(6.2832 * u + spin), sy - R * Math.sin(0.38) * Math.sin(6.2832 * u + spin)];
470
+ return [H.lerp(tb[0], xy[0], m), H.lerp(tb[1], xy[1], m)];
471
+ }, 260)], { w: 2.4, timed: true });
472
+ } else {
473
+ const e = H.ease.inOut(H.clamp((t - 1.65) / 1.0));
474
+ beam([sphere(sx, sy, R, spin, 0.38, e, 0.02 * e + 0.004 * Math.sin(t * 7), 1500, 22)], { w: 2.0, timed: true, a: 0.9 });
475
+ // orbit ring with oscilloscope-music ripples
476
+ const rf = ph(2.2, 0.7);
477
+ if (rf > 0) beam([curve((u) => { const th = u * 6.2832, rr = 1 + 0.006 * Math.sin(12 * th + t * 5); const x = Math.cos(th) * 300 * rr, y = Math.sin(th) * 64 * rr, c = Math.cos(-0.17), s = Math.sin(-0.17); return [sx + x * c - y * s, sy + 10 + x * s + y * c]; }, 420)], { f: rf, w: 2.0, timed: true, a: 0.85 });
478
+ }
479
+ // satellite: drifting 1:2 Lissajous
480
+ const lf = ph(3.0, 0.5);
481
+ if (lf > 0) beam([curve((u) => { const a = u * 6.2832; return [768 + 56 * Math.sin(a + t * 1.3), 128 + 46 * Math.sin(2 * a)]; }, 160)], { f: lf, w: 1.9, timed: true, a: 0.85 });
482
+ // CRT readouts
483
+ const rd = ph(2.9, 0.5);
484
+ text('CH1 .5V', GX + 18, GY + 16, 14, { f: rd, a: 0.5 }); text('CH2 .5V', GX + 18, GY + 38, 14, { f: rd, a: 0.5 });
485
+ text(t >= 1.0 ? 'X-Y' : '1MS', GX + GW - 18, GY + GH - 32, 14, { align: 'r', f: ph(0.6, 0.3), a: 0.5 });
486
+ // title
487
+ const T = 'THE SHAPE OF A THOUGHT', cap = Math.min(36, 740 / textW(T, 1));
488
+ text(T, CX, 568, cap, { align: 'c', f: H.clamp((t - 2.6) / 1.15), a: 1 });
489
+ // underline cursor
490
+ const uf = ph(3.75, 0.4);
491
+ if (uf > 0) beam([[[CX - 120, 622], [CX + 120, 622]]], { f: uf, w: 2.2, a: 0.75 });
492
+ }
493
+ st.bloom = 1 + 0.6 * Math.max(0, 1 - Math.abs(t - 0.3) / 0.25);
494
+ st.inten = 0.92 + 0.08 * jit();
495
+ renderFrame(ctx, W, H_, st);
496
+ }
497
+
498
+ // ================= EXPLAINER =================
499
+ const EXD = 15;
500
+ // timing constants (shared with the sound score)
501
+ const TX = {
502
+ title: [0.3, 1.5], shrink: [2.0, 0.55], net2: [2.5, 0.7], model2: [3.0, 0.4], conv: [2.7, 0.4], cnt: [3.0, 5.6], itemsT0: 2.75, itemsDT: 0.5, itemSpeed: 270,
503
+ col1: 5.8, dot1: 6.1, exp1: 6.25, // collapse to dot -> expand
504
+ cat3: 6.25, arr1: 6.85, row: 6.45, arr2: 7.1, guess: 7.3, check: 7.9, cross: 7.9, itsCat: 8.05, adjust: 8.5, back: [8.5, 8.75], bpNet: [8.75, 9.15], weights: [8.8, 9.3], loopArc: 8.9,
505
+ guess2: 9.3, fwd: [9.3, 9.65], cat2: 9.65, tick: 9.97, check2: 10.0,
506
+ sqX: [10.35, 10.6], sweep: [10.85, 12.75], hit: 12.75, sqY: [12.95, 13.2], take: [13.25, 14.05], liss: [14.0, 0.5],
507
+ };
508
+ const fmt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
509
+ const acc = (u) => 0.61 + 0.36 * (1 - Math.exp(-3.4 * u)) / (1 - Math.exp(-3.4));
510
+ const fade = (t, t0, d) => (t < t0 ? 1 : Math.pow(Math.max(0, 1 - (t - t0) / d), 2));
511
+
512
+ function exScene(t) {
513
+ const st = baseState();
514
+ const ph = (a, d) => H.ease.inOut(H.clamp((t - a) / d));
515
+ const lin = (a, d) => H.clamp((t - a) / d);
516
+ const frame = Math.floor(t * 30), jr = H.rng(3000 + frame);
517
+
518
+ // ---- mode + transforms
519
+ let sx = 1, sy = 1, mode = 'xy';
520
+ if (t < TX.dot1) { sx = sy = 1 - H.ease.in(lin(TX.col1, TX.dot1 - TX.col1)); }
521
+ else if (t < TX.exp1) { sx = sy = 0; }
522
+ else if (t < 6.6) { sx = sy = H.ease.out(lin(TX.exp1, 0.35)); }
523
+ if (t < 0.45) { sx = sy = Math.max(0, H.ease.out(lin(0.12, 0.33))); }
524
+ if (t >= TX.sqX[0] && t < TX.sweep[0]) { sx = t < TX.sqX[1] ? 1 - H.ease.in(lin(TX.sqX[0], TX.sqX[1] - TX.sqX[0])) : 0; }
525
+ if (t >= TX.sqX[1] && t < TX.sqY[1]) mode = 'tb';
526
+ if (t >= TX.sqY[0] && t < TX.take[0]) { sy = t < TX.sqY[1] ? 1 - H.ease.in(lin(TX.sqY[0], TX.sqY[1] - TX.sqY[0])) : 0; }
527
+ // wrong-guess tremor: the whole trace shudders as the check fails
528
+ const shud = t > TX.check && t < TX.check + 0.6 ? (1 - (t - TX.check) / 0.6) : 0;
529
+ XF = (p) => {
530
+ let x = CX + (p[0] - CX) * sx, y = CY + (p[1] - CY) * sy;
531
+ if (shud > 0) { x += shud * 7 * Math.sin(p[1] * 0.06 + t * 70); y += shud * 2 * Math.sin(p[0] * 0.05 + t * 50); }
532
+ return [x, y, p[2]];
533
+ };
534
+ if (t > TX.col1 && t < TX.exp1 + 0.35) { const k2 = 1 - sx; dot(CX, CY, 3 + 5 * k2, k2); }
535
+
536
+ // ---- BEAT 1: hook
537
+ const hdrRe = (t0) => H.clamp((t - t0) / 0.3);
538
+ if (t < TX.exp1) {
539
+ const s = ph(TX.shrink[0], TX.shrink[1]), s2 = ph(TX.shrink[0] + 0.15, TX.shrink[1]);
540
+ const capA = 66, capB = 30;
541
+ const l1 = 'HOW DOES', l2 = 'AI LEARN?';
542
+ const w1a = textW(l1, capA), w2a = textW(l2, capA);
543
+ const cap = H.lerp(capA, capB, s);
544
+ const x1 = H.lerp(CX - w1a / 2, 74, s), y1 = H.lerp(232, 56, s);
545
+ const x2 = H.lerp(CX - w2a / 2, 74 + textW(l1 + ' ', capB), s2), y2 = H.lerp(336, 56, s2), cap2 = H.lerp(capA, capB, s2);
546
+ const f = H.clamp((t - TX.title[0]) / (TX.title[1] - TX.title[0]));
547
+ const f1 = H.clamp(f * 2), f2 = H.clamp(f * 2 - 1);
548
+ text(l1, x1, y1, cap, { f: f1, w: H.lerp(4.6, 2.7, s) });
549
+ text(l2, x2, y2, cap2, { f: f2, w: H.lerp(4.6, 2.7, s2) });
550
+ // underline sine drawn under the question
551
+ const uf = ph(1.4, 0.45) * (1 - s);
552
+ if (uf > 0.01) beam([curve((u) => [CX - 250 + u * 500, 452 + 16 * Math.sin(u * 6.2832 * 4 + t * 3)], 140)], { f: ph(1.4, 0.45), a: 1 - s, w: 2.6, timed: true });
553
+ }
554
+
555
+ // ---- BEAT 2: data streams into the model
556
+ if (t >= TX.net2[0] && t < TX.exp1) {
557
+ const rot = 0.35 * Math.sin(t * 1.1);
558
+ const absorbs = [];
559
+ // examples: cat / dog faces with labels riding the stream
560
+ const pick = H.rng(44);
561
+ for (let i = 0; i < 12; i++) {
562
+ const isCat = pick() < 0.5, t0 = TX.itemsT0 + i * TX.itemsDT;
563
+ for (let gh = 0; gh < 3; gh++) {
564
+ const tt = t - gh * 0.035; if (tt < t0) continue;
565
+ const x = 30 + (tt - t0) * TX.itemSpeed * (1 + Math.max(0, tt - 4.6) * 0.5);
566
+ if (x > 590) { if (gh === 0 && x < 640) absorbs.push(1 - (x - 590) / 50); continue; }
567
+ GA = gh === 0 ? 1 : 0.16 / gh;
568
+ const shrink = x > 470 ? 1 - (x - 470) / 150 : 1, y = 214 + (x > 470 ? (x - 470) * 0.25 : 0);
569
+ beam(tx(isCat ? CAT : DOG, x, y, 40 * shrink), { w: 2.1, dots: gh === 0 });
570
+ if (shrink > 0.6 && gh === 0) text(isCat ? 'CAT' : 'DOG', x, 278, 24, { align: 'c', a: shrink });
571
+ GA = 1;
572
+ }
573
+ }
574
+ const fl = absorbs.length ? Math.max(...absorbs) : 0;
575
+ net(690, 238, 210, 250, rot, { f: H.clamp((t - TX.net2[0]) / TX.net2[1]), act: (l) => fl * (l === 0 ? 1 : l === 1 ? 0.6 : 0.3) });
576
+ text('MODEL', 690, 378, 26, { align: 'c', f: H.clamp((t - TX.model2[0]) / TX.model2[1]) });
577
+ beam(arrowS(70, 330, 545, 330, 16), { f: ph(TX.conv[0], TX.conv[1]), a: 0.8, w: 2.2 });
578
+ // counter
579
+ const cp = H.clamp((t - TX.cnt[0]) / (TX.cnt[1] - TX.cnt[0])), n = Math.round(1e6 * Math.pow(cp, 2.3));
580
+ const cf = H.clamp((t - TX.cnt[0]) / 0.3);
581
+ text(fmt(n), CX, 470, 70, { align: 'c', f: cf, w: 4.6 });
582
+ text('EXAMPLES', CX, 566, 30, { align: 'c', f: H.clamp((t - 3.2) / 0.35) });
583
+ }
584
+
585
+ // ---- BEAT 3: guess -> check -> adjust
586
+ if (t >= TX.exp1 && t < TX.sweep[0]) {
587
+ const H3 = 'HOW DOES AI LEARN?';
588
+ text(H3, 74, 56, 30, { w: 2.7, f: hdrRe(6.25) });
589
+ const catF = H.clamp((t - TX.cat3) / 0.55);
590
+ beam(tx(CAT, 158, 300, 74), { f: catF, w: 2.6 });
591
+ const pulse1 = t > TX.fwd[0] && t < TX.fwd[1] ? (t - TX.fwd[0]) / (TX.fwd[1] - TX.fwd[0]) : -1;
592
+ beam(arrowS(266, 300, 318, 300, 13), { f: ph(TX.arr1, 0.2), w: 2.2 });
593
+ // net: activation flash on guesses, amber back-propagation on adjust
594
+ const actT = (l, t0) => { const d = t - (t0 + l * 0.1); return d > 0 && d < 0.35 ? 1 - d / 0.35 : 0; };
595
+ const bpT = (l) => { const d = t - (TX.bpNet[0] + (1 - l) * 0.18); return d > 0 && d < 0.5 ? Math.sin(Math.PI * d / 0.5) : 0; };
596
+ const wk = ph(TX.weights[0], TX.weights[1] - TX.weights[0]);
597
+ const jitN = t > TX.guess && t < TX.guess2 ? 1.6 : 0;
598
+ net(432, 300, 180, 230, 0.25 * Math.sin(t * 0.9), {
599
+ f: H.clamp((t - 6.28) / 0.5), wk, bp: bpT, nodeR: 11,
600
+ act: (l) => Math.max(actT(l, 7.1), actT(l, 9.42)),
601
+ nodeCol: (l) => H.mix(P31, AMBER, Math.min(1, bpT(Math.min(l, 1)) * 0.8)),
602
+ });
603
+ text('MODEL', 432, 444, 26, { align: 'c', f: H.clamp((t - 6.6) / 0.3) });
604
+ beam(arrowS(532, 300, 578, 300, 13), { f: ph(TX.arr2, 0.2), w: 2.2 });
605
+ if (pulse1 >= 0) { const p = pulse1 < 0.4 ? H.lerp(270, 320, pulse1 / 0.4) : pulse1 < 0.75 ? H.lerp(330, 530, (pulse1 - 0.4) / 0.35) : H.lerp(535, 580, (pulse1 - 0.75) / 0.25); dot(p, 300, 6, 1); }
606
+ // wrong guess: glitching, amber-shifted trace
607
+ const dogA = fade(t, TX.guess2 - 0.05, 0.3), wrong = H.clamp((t - TX.guess) / 0.25) * 0.85;
608
+ if (t >= TX.guess && dogA > 0) {
609
+ const gl = t < TX.guess2 ? 1 - 0.7 * H.clamp((t - TX.check) / 0.3) : 0, rr = H.rng(500 + frame); // detune settles once checked, so the crossed-out word stays readable
610
+ const tears = Array.from({ length: 8 }, () => (rr() < 0.25 ? (rr() - 0.5) * 12 : 0));
611
+ const xf = (p) => [p[0] + gl * (Math.sin(p[1] * 0.21 + t * 40) * 1.5 + tears[Math.floor(((p[1] - 270) / 70) * 8) & 7]), p[1] + gl * Math.sin(p[0] * 0.13 + t * 33) * 0.8];
612
+ const col = H.mix(P31, AMBER, wrong);
613
+ text('DOG?', 596, 270, 58, { f: H.clamp((t - TX.guess) / 0.4), col, xf, a: dogA, w: 4.2 });
614
+ // ghost copy: red-shifted, offset (misconverged beam)
615
+ text('DOG?', 598, 269, 58, { f: H.clamp((t - TX.guess) / 0.4), col: RED, xf, a: dogA * 0.15 * wrong, w: 2.5, dots: false, head: false, retrace: false });
616
+ // CHECK: cross it out
617
+ const xs = H.clamp((t - TX.cross) / 0.22), w4 = textW('DOG?', 58);
618
+ if (xs > 0) beam([[[586, 258], [606 + w4, 340]], [[606 + w4, 258], [586, 340]]], { f: xs, col: RED, a: dogA, w: 3.6 });
619
+ text("IT'S A CAT", 596 + textW('DOG?', 58) / 2, 360, 28, { align: 'c', f: H.clamp((t - TX.itsCat) / 0.35), col: AMBER, a: dogA * 0.95 });
620
+ // error signal travels back from the guess into the net
621
+ if (t > TX.back[0] && t < TX.back[1]) { const p = (t - TX.back[0]) / (TX.back[1] - TX.back[0]); dot(H.lerp(590, 532, p), 300, 7, 1, AMBER); }
622
+ }
623
+ // corrected guess, snapping clean
624
+ if (t >= TX.cat2) {
625
+ text('CAT', 600, 270, 58, { f: H.clamp((t - TX.cat2) / 0.3), w: 4.4 });
626
+ const cx0 = 600 + textW('CAT', 58) + 26; beam([[[cx0, 300], [cx0 + 18, 326], [cx0 + 60, 262]]], { f: H.clamp((t - TX.tick) / 0.18), w: 4.4 });
627
+ }
628
+ // loop row
629
+ const row = ['GUESS', 'CHECK', 'ADJUST'], cap = 30, gap = 18, arL = 46;
630
+ const wds = row.map((s) => textW(s, cap)), tot = wds.reduce((a, b) => a + b, 0) + 2 * (arL + gap * 2);
631
+ let x = CX - tot / 2; const xs = [];
632
+ const starts = [TX.guess, TX.check, TX.adjust, TX.guess2, TX.check2];
633
+ const stg = t < TX.check ? 0 : t < TX.adjust ? 1 : t < TX.guess2 ? 2 : t < TX.check2 ? 3 : 4, active = [0, 1, 2, 0, 1][stg];
634
+ const rowF = H.clamp((t - TX.row) / 0.6);
635
+ row.forEach((s, i) => {
636
+ xs.push(x + wds[i] / 2);
637
+ const on = t > 7.15 && i === active;
638
+ text(s, x, 548, cap, { f: H.clamp(rowF * 3 - i), a: on ? 1 : 0.5, col: on && i > 0 && t < TX.guess2 ? H.mix(P31, AMBER, 0.6) : P31 });
639
+ if (on) beam([[[x - 12, 536], [x + wds[i] + 12, 536], [x + wds[i] + 12, 590], [x - 12, 590], [x - 12, 536]]], { f: H.clamp((t - starts[stg]) / 0.15), a: 0.8, w: 2 });
640
+ x += wds[i];
641
+ if (i < 2) { beam(arrowS(x + gap, 563, x + gap + arL, 563, 11), { f: H.clamp(rowF * 3 - i - 0.5), a: 0.55 }); x += arL + gap * 2; }
642
+ });
643
+ // loop closes: return arc from ADJUST back to GUESS
644
+ const la = H.clamp((t - TX.loopArc) / 0.4);
645
+ if (la > 0) {
646
+ const pts = curve((u) => { const x0 = xs[2], x1 = xs[0]; const xx = H.lerp(x0, x1, u); return [xx, 600 + Math.sin(u * Math.PI) * 42]; }, 30);
647
+ beam([pts, [[xs[0] - 10, 612], [xs[0], 600], [xs[0] + 10, 612]]], { f: la, a: 0.7, w: 2.2 });
648
+ }
649
+ }
650
+
651
+ // ---- BEAT 4: timebase — loss decays, accuracy climbs
652
+ if (t >= TX.sqX[1] && t < TX.take[0]) {
653
+ const H3 = 'HOW DOES AI LEARN?';
654
+ if (t >= TX.sweep[0]) text(H3, 74, 56, 30, { w: 2.7, f: hdrRe(TX.sweep[0]) });
655
+ if (t < TX.sweep[0]) { beam([[[CX, GY + 20], [CX, GY + GH - 20]]], { w: 2.4 }); }
656
+ else {
657
+ const u = H.clamp((t - TX.sweep[0]) / (TX.sweep[1] - TX.sweep[0])), xe = GX + u * GW;
658
+ const ay = (v) => 450 - (acc(v) - 0.5) / 0.5 * 300;
659
+ const nzA = H.makeNoise(17);
660
+ const accPts = curve((v) => [GX + v * GW, ay(v) + nzA.n2(v * 40, 0.5) * 9 * (1 - v)], 160).filter((p) => p[0] <= xe + 0.1);
661
+ const lossPts = curve((v) => [GX + v * GW, 556 - 66 * Math.exp(-3 * v) * Math.cos(6.2832 * 7 * v) - 6 * nzA.n2(v * 60, 3) * Math.exp(-2.5 * v)], 320).filter((p) => p[0] <= xe + 0.1);
662
+ if (accPts.length > 1) beam([accPts], { w: 3, timed: true, head: false });
663
+ if (lossPts.length > 1) beam([lossPts], { w: 2.2, timed: true, head: false, a: 0.75 });
664
+ if (u < 1) { dot(xe, ay(u), 6, 1); dot(xe, 556 - 66 * Math.exp(-3 * u) * Math.cos(6.2832 * 7 * u), 5, 0.8); }
665
+ text('ACCURACY', 356, 150, 26, { f: H.clamp((t - TX.sweep[0] - 0.05) / 0.3) });
666
+ text('ERROR', 74, 446, 28, { f: H.clamp((t - TX.sweep[0] - 0.2) / 0.3), a: 0.8 });
667
+ text('TRAINING ROUNDS >', GX + GW - 20, 634, 28, { align: 'r', f: H.clamp((t - TX.sweep[0] - 0.4) / 0.4), a: 0.7 });
668
+ const v = Math.round(acc(u) * 100), hit = t >= TX.hit;
669
+ text(v + '%', 74, 116, 86, { f: H.clamp((t - TX.sweep[0]) / 0.25), w: hit ? 6 : 5 });
670
+ if (hit) {
671
+ const k = H.clamp((t - TX.hit) / 0.2), wN = textW('97%', 86);
672
+ beam([[[60, 104], [86 + wN, 104], [86 + wN, 216], [60, 216], [60, 104]]], { f: k, w: 2.8 });
673
+ dot(GX + GW, ay(1), 9, 1);
674
+ }
675
+ }
676
+ }
677
+
678
+ // ---- BEAT 5: takeaway
679
+ if (t >= TX.take[0]) {
680
+ const f = H.clamp((t - TX.take[0]) / (TX.take[1] - TX.take[0]));
681
+ text('MORE EXAMPLES >', CX, 216, 50, { align: 'c', f: H.clamp(f * 2), w: 3.2 });
682
+ text('BETTER GUESSES', CX, 316, 50, { align: 'c', f: H.clamp(f * 2 - 1), w: 3.2 });
683
+ const lf = ph(TX.liss[0], TX.liss[1]);
684
+ if (lf > 0) beam([curve((u) => { const a = u * 6.2832; return [CX + 120 * Math.sin(3 * a + 0.5 + t * 0.4), 510 + 60 * Math.sin(2 * a)]; }, 360)], { f: lf, w: 2.2, timed: true, a: 0.9 });
685
+ }
686
+
687
+ // ---- panel + beam state
688
+ st.xy = !(t >= TX.sqX[1] && t < TX.sqY[1]);
689
+ st.kn.tdiv = snapKnob(t, [[TX.sqX[1], 6], [TX.sqY[1], 0]], 0);
690
+ st.kn.ch1 = snapKnob(t, [[8.85, 4]], 3); st.kn.ch2 = snapKnob(t, [[9.0, 2]], 3);
691
+ const hitE = t > TX.hit ? Math.exp(-(t - TX.hit) / 0.35) : 0;
692
+ const snap = t > TX.tick ? Math.exp(-(t - TX.tick) / 0.2) : 0;
693
+ st.kn.inten = 0.55 + 0.3 * hitE;
694
+ st.bloom = 1 + 1.9 * hitE + 0.5 * snap + (sx < 0.05 && sy < 0.05 && t > 0.1 ? 0.8 : 0) + 0.4 * shud;
695
+ st.inten = (0.93 + 0.07 * jr()) * (1 + 0.35 * hitE);
696
+ return st;
697
+ }
698
+
699
+ // ================= SOUND =================
700
+ function oscSound(k) {
701
+ const D = EXD, T = TX;
702
+ const px = (x) => H.clamp(x / 900) * 1.2 - 0.6;
703
+ // the picture as sound: a beam drawing a figure = low filtered XY buzz (two detuned saws, like L/R channels)
704
+ const draw = (t, dur, f, p = 0, g = 1, minor = true) => {
705
+ const o = { type: 'sawtooth', dur, a: Math.min(0.05, dur * 0.25), hold: dur * 0.65, filter: 'lowpass', f2: [[0, 500], [dur, 1100]], q: 2, pan: p, minor };
706
+ k.tone(t, Object.assign({}, o, { f, gain: 0.038 * g }));
707
+ k.tone(t, Object.assign({}, o, { f: f * 1.5, gain: 0.019 * g, detune: 6, pan: -p }));
708
+ };
709
+ // relay / rotary switch: a sharp mechanical click + a little thunk
710
+ const relay = (t, g = 1, p = 0.7) => { k.noise(t, { dur: 0.02, filter: 'bandpass', f: 2200, q: 1.5, gain: 0.35 * g, a: 0.001, pan: p }); k.noise(t + 0.03, { dur: 0.015, filter: 'bandpass', f: 1500, q: 2, gain: 0.22 * g, a: 0.001, pan: p }); k.tone(t, { f: [[0, 180], [0.05, 90]], dur: 0.07, gain: 0.18 * g, pan: p }); };
711
+ const detent = (t, g = 1, p = 0.7) => k.noise(t, { dur: 0.012, filter: 'bandpass', f: 1800, q: 2, gain: 0.22 * g, a: 0.001, pan: p, minor: true });
712
+ // beam collapse: falling filtered sweep; expand: rising
713
+ const sweepTone = (t, dur, f0, f1, g = 1) => { k.tone(t, { f: [[0, f0], [dur, f1]], type: 'sawtooth', dur, gain: 0.06 * g, a: 0.01, hold: dur * 0.6, filter: 'lowpass', f2: 900, q: 3 }); k.noise(t, { dur, color: 'brown', filter: 'lowpass', f: [[0, f0 * 4], [dur, f1 * 4]], gain: 0.12 * g, a: 0.01 }); };
714
+
715
+ k.bed('hum', 0, D, { gain: 0.6 });
716
+ k.bed('room', 0, D, { gain: 0.5 });
717
+ k.noise(0, { dur: D, color: 'pink', filter: 'bandpass', f: 1400, q: 0.7, gain: 0.012, a: 0.3, hold: D - 0.6, bus: 'amb' }); // flyback / transformer hiss
718
+ if (!k.vo) k.tone(0, { f: 'A1', type: 'triangle', dur: D, gain: 0.02, a: 0.6, hold: D - 1.2, filter: 'lowpass', f2: 300, bus: 'amb' });
719
+
720
+ // ---- BEAT 1: power-on dot, title written by the beam
721
+ k.tone(0.12, { f: [[0, 60], [0.3, 110]], type: 'sine', dur: 0.4, gain: 0.14, a: 0.01 }); relay(0.1, 0.55, 0.6);
722
+ draw(T.title[0], 0.62, 'A2', 0, 1.1); draw(T.title[0] + 0.62, 0.6, 'E2', 0, 1.1);
723
+ draw(1.4, 0.45, 'A2', 0, 0.7); // the sine underline
724
+ k.noise(T.shrink[0], { dur: 0.55, color: 'brown', filter: 'lowpass', f: [[0, 500], [0.55, 160]], gain: 0.1, a: 0.2, pan: -0.4 }); // shrink into the corner
725
+ // ---- BEAT 2: net draws, conveyor arrow, examples stream in
726
+ draw(T.net2[0], T.net2[1], 'D2', px(690), 0.9);
727
+ draw(T.conv[0], T.conv[1], 'A2', px(300), 0.6);
728
+ draw(T.model2[0], T.model2[1], 'E2', px(690), 0.6);
729
+ k.withMinor(() => {
730
+ for (let i = 0; i < 6; i++) { const ta = T.itemsT0 + i * T.itemsDT + (590 - 30) / T.itemSpeed; if (ta > T.col1 - 0.1) break; k.tone(ta - (ta > 4.6 ? 0.25 : 0), { f: [[0, 'A3'], [0.12, 'E3']], type: 'triangle', dur: 0.16, gain: 0.07, filter: 'lowpass', f2: 900, pan: px(600) }); }
731
+ // counter: ticks thinning out as the number accelerates (≤12/s)
732
+ let last = -1; for (let f = 0; f < (T.cnt[1] - T.cnt[0]) * 60; f++) { const t = T.cnt[0] + f / 60; if (t - last >= 1 / 11) { k.noise(t, { dur: 0.012, filter: 'bandpass', f: 1300, q: 3, gain: 0.1, a: 0.001, pan: 0 }); last = t; } }
733
+ });
734
+ k.tone(T.cnt[1], { f: 'E3', type: 'triangle', dur: 0.4, gain: 0.09, filter: 'lowpass', f2: 1200 }); k.tone(T.cnt[1], { f: 'A3', type: 'triangle', dur: 0.4, gain: 0.07, filter: 'lowpass', f2: 1200 });
735
+ // ---- transition 1: collapse to a dot, relay, re-expand
736
+ sweepTone(T.col1, T.dot1 - T.col1, 220, 55, 1);
737
+ relay(T.dot1 + 0.05, 1.2);
738
+ sweepTone(T.exp1, 0.35, 55, 200, 0.8);
739
+ // ---- BEAT 3
740
+ draw(T.cat3, 0.55, 'A2', px(158), 1);
741
+ draw(T.row, 0.6, 'E2', 0, 0.6);
742
+ k.withMinor(() => { k.tone(T.arr1, { f: 'E3', type: 'triangle', dur: 0.15, gain: 0.06, filter: 'lowpass', f2: 900 }); k.tone(T.arr2, { f: 'A3', type: 'triangle', dur: 0.15, gain: 0.06, filter: 'lowpass', f2: 900 }); });
743
+ // wrong guess: DOG? written by a detuned, wobbling beam + soft static burst
744
+ k.tone(T.guess, { f: [[0, 98], [0.15, 92], [0.3, 101], [0.45, 90]], type: 'sawtooth', dur: 0.5, gain: 0.08, hold: 0.3, filter: 'lowpass', f2: 900, q: 4, pan: px(680) });
745
+ k.tone(T.guess, { f: [[0, 104], [0.2, 96], [0.45, 107]], type: 'sawtooth', dur: 0.5, gain: 0.07, hold: 0.3, filter: 'lowpass', f2: 900, q: 4, pan: px(680) });
746
+ k.noise(T.guess, { dur: 0.35, color: 'pink', filter: 'bandpass', f: 1200, q: 0.8, gain: 0.08, a: 0.02, pan: px(680) });
747
+ // CHECK: the cross + shudder — dissonant low buzz and a noise hit
748
+ k.play('buzz', T.check, { f: 73, dur: 0.45, gain: 0.7, pan: px(680) });
749
+ k.noise(T.check, { dur: 0.3, color: 'pink', filter: 'lowpass', f: [[0, 2000], [0.3, 400]], gain: 0.16, a: 0.002, pan: px(680) });
750
+ k.tone(T.check, { f: [[0, 'Bb2'], [0.5, 'A2']], type: 'square', dur: 0.5, gain: 0.04, hold: 0.3, filter: 'lowpass', f2: 700 });
751
+ draw(T.itsCat, 0.35, 'G2', px(680), 0.5);
752
+ // ADJUST: error dot runs back (falling), weights retune (knob detents + gliding tones)
753
+ k.tone(T.back[0], { f: [[0, 330], [0.25, 165]], type: 'triangle', dur: 0.3, gain: 0.08, filter: 'lowpass', f2: 1200, pan: px(560) });
754
+ k.tone(T.bpNet[0], { f: [[0, 110], [0.4, 147]], type: 'sawtooth', dur: 0.45, gain: 0.05, hold: 0.25, filter: 'lowpass', f2: 700, pan: px(430) });
755
+ k.tone(T.bpNet[0] + 0.1, { f: [[0, 165], [0.4, 220]], type: 'sawtooth', dur: 0.45, gain: 0.035, hold: 0.25, filter: 'lowpass', f2: 700, pan: px(430) });
756
+ for (let i = 0; i < 3; i++) { detent(8.85 + i * 0.04, 0.9, 0.55); detent(9.0 + i * 0.04, 0.9, 0.85); }
757
+ draw(T.loopArc, 0.4, 'A2', 0, 0.5);
758
+ // GUESS again: forward pulse, then CAT written clean — the correction resolves
759
+ k.tone(T.fwd[0], { f: [[0, 'E3'], [0.3, 'A3']], type: 'triangle', dur: 0.32, gain: 0.07, filter: 'lowpass', f2: 1000, pan: px(400) });
760
+ draw(T.cat2, 0.3, 'A2', px(680), 0.8, false);
761
+ ['A3', 'C#4', 'E4'].forEach((n, i) => k.tone(T.tick + i * 0.03, { f: n, type: 'triangle', dur: 0.7, gain: 0.07, a: 0.004, hold: 0.1, verb: 0.2, pan: px(760) }));
762
+ k.tone(T.tick, { f: 'A2', type: 'sine', dur: 0.6, gain: 0.12 });
763
+ // ---- transition 2: X gain to zero (vertical line), relay to timebase
764
+ sweepTone(T.sqX[0], T.sqX[1] - T.sqX[0], 200, 70, 0.9);
765
+ relay(T.sqX[1], 1.2); detent(T.sqX[1] + 0.05, 1); detent(T.sqX[1] + 0.09, 1);
766
+ // ---- BEAT 4: sweep draws both traces; tone rises with accuracy, ticks per % (≤12/s)
767
+ const SW = T.sweep[1] - T.sweep[0];
768
+ k.tone(T.sweep[0], { f: [[0, 82], [SW * 0.3, 98], [SW, 110]], type: 'sawtooth', dur: SW, gain: 0.04, a: 0.05, hold: SW * 0.85, filter: 'lowpass', f2: [[0, 400], [SW, 1200]], q: 2 });
769
+ k.tone(T.sweep[0], { f: [[0, 55], [SW, 41]], type: 'triangle', dur: SW, gain: 0.06, a: 0.05, hold: SW * 0.5, filter: 'lowpass', f2: 300 }); // error trace: dying low wobble
770
+ k.withMinor(() => { let lastV = 61, lastT = -1; for (let f = 0; f < SW * 60; f++) { const t = T.sweep[0] + f / 60, v = Math.round(acc(f / 60 / SW) * 100); if (v > lastV && t - lastT >= 1 / 12) { k.noise(t, { dur: 0.012, filter: 'bandpass', f: 1500, q: 3, gain: 0.1, a: 0.001, pan: -0.5 }); lastV = v; lastT = t; } } });
771
+ // 97%: beam flares — the biggest hit: deep thump + a clean, open A chord
772
+ k.play('thud', T.hit, { f: 95, gain: 1.15 });
773
+ ['A2', 'E3', 'A3', 'C#4', 'E4'].forEach((n, i) => k.tone(T.hit, { f: n, type: i < 2 ? 'sawtooth' : 'triangle', dur: 1.1, gain: i < 2 ? 0.065 : 0.09, a: 0.006, hold: 0.3, filter: 'lowpass', f2: 1600, verb: 0.25, pan: (i - 2) * 0.15 }));
774
+ k.noise(T.hit, { dur: 0.6, color: 'pink', filter: 'lowpass', f: [[0, 2200], [0.6, 300]], gain: 0.17, a: 0.004, verb: 0.3 });
775
+ // ---- transition 3: Y gain to zero, relay back to X-Y
776
+ sweepTone(T.sqY[0], T.sqY[1] - T.sqY[0], 180, 70, 0.8);
777
+ relay(T.sqY[1], 1.2); detent(T.sqY[1] + 0.04, 1);
778
+ // ---- BEAT 5: takeaway written, lands on a resolved chord
779
+ draw(T.take[0], 0.4, 'A2', 0, 0.9); draw(T.take[0] + 0.4, 0.4, 'E2', 0, 0.9);
780
+ draw(T.liss[0], T.liss[1], 'A2', 0, 0.6);
781
+ ['A2', 'E3', 'A3', 'E4'].forEach((n, i) => k.tone(T.take[1], { f: n, type: 'triangle', dur: 1.0, gain: 0.07, a: 0.01, hold: 0.25, verb: 0.3, pan: (i - 1.5) * 0.2 }));
782
+ k.tone(T.take[1], { f: 'A1', type: 'sine', dur: 0.95, gain: 0.14, a: 0.01 });
783
+ }
784
+
785
+ registerStyle({
786
+ id: 'oscilloscope',
787
+ name: 'Oscilloscope Vector Art',
788
+ category: 'Retro Digital',
789
+ description: 'Oscilloscope-music XY art on an analogue lab scope: one P31 green beam draws everything, bright where it slows at turns and dim on fast retrace lines, with bloom and halation behind an internal 8x10 graticule, framed by a moulded CRT bezel and a knobbed instrument front panel.',
790
+ motion: 'Power-on: the dot blooms, the timebase pulls it into a sine sweep, the X-Y button snaps in and the waveform folds into a circle that winds itself into a spinning spiral-sphere while the beam writes the vector text.',
791
+ fonts: ['600 12px "Inter"', '700 12px "Inter"', '800 12px "Inter"', '700 30px "Space Grotesk"'],
792
+ duration: 5,
793
+ frameT: 4.7,
794
+ readsPixels: false,
795
+ explainerDuration: EXD,
796
+ explainerNote: 'Everything is drawn by one scope beam: transitions are the instrument\'s own (picture collapses to a dot and re-expands, X gain to zero then the timebase sweep, Y gain to zero back to X-Y), arrows and Hershey-style labels are beam strokes, counters are redrawn vector numerals, and the wrong guess shudders amber while the 97% flares the bloom.',
797
+ draw(ctx, W, H_, t) { drawStill(ctx, W, H_, t); },
798
+ explainer(ctx, W, H_, t) { resetBeam(); const st = exScene(t); renderFrame(ctx, W, H_, st); },
799
+ explainerSound(k) { oscSound(k); },
800
+ });
801
+ }