@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,1008 @@
1
+ {
2
+ const TAU = Math.PI * 2;
3
+
4
+ const poly = (c, pts) => {
5
+ c.moveTo(pts[0][0], pts[0][1]);
6
+ for (let i = 1; i < pts.length; i++) c.lineTo(pts[i][0], pts[i][1]);
7
+ c.closePath();
8
+ };
9
+ // scissor-cut circle: slightly faceted, slightly wobbly
10
+ const circPts = (r, cx, cy, R, seg = 15, jit = 0.6) => {
11
+ const n = Math.max(18, Math.round((TAU * R) / seg)), a0 = r() * TAU, out = [];
12
+ for (let i = 0; i < n; i++) {
13
+ const a = a0 + ((i + r.range(-0.2, 0.2)) / n) * TAU, rr = R + r.gauss() * jit;
14
+ out.push([cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]);
15
+ }
16
+ return out;
17
+ };
18
+ // scissor-cut edge following y=f(x): short straight-ish snips
19
+ const ridge = (r, x0, x1, f, sMin = 12, sMax = 30, jit = 0.6) => {
20
+ const out = [];
21
+ let x = x0;
22
+ while (x < x1) { out.push([x, f(x) + r.gauss() * jit]); x += r.range(sMin, sMax); }
23
+ out.push([x1, f(x1)]);
24
+ return out;
25
+ };
26
+ // straight cut between points with tiny wobbles
27
+ const cutLines = (r, pts, seg = 22, jit = 0.7) => {
28
+ const out = [];
29
+ for (let i = 0; i < pts.length - 1; i++) {
30
+ const [ax, ay] = pts[i], [bx, by] = pts[i + 1];
31
+ const n = Math.max(1, Math.round(Math.hypot(bx - ax, by - ay) / seg));
32
+ for (let k = 0; k < n; k++) {
33
+ const u = k / n;
34
+ out.push([ax + (bx - ax) * u + (k ? r.gauss() * jit : 0), ay + (by - ay) * u + (k ? r.gauss() * jit : 0)]);
35
+ }
36
+ }
37
+ out.push(pts[pts.length - 1]);
38
+ return out;
39
+ };
40
+
41
+ // tileable paper fibre texture (alpha-only light/dark flecks)
42
+ const makeTex = (seed) => {
43
+ const S = 512, cv = H.layer(S, S), x = cv.getContext('2d'), r = H.rng(seed), nz = H.makeNoise(seed + 1);
44
+ // low-res tileable mottle
45
+ const M = 64, mc = H.layer(M, M), mx = mc.getContext('2d'), mi = mx.createImageData(M, M), f = 5 / M;
46
+ for (let j = 0; j < M; j++) for (let i = 0; i < M; i++) {
47
+ const n = (a, b) => nz.fbm2(a * f, b * f, 3);
48
+ const v = (n(i, j) * (M - i) * (M - j) + n(i - M, j) * i * (M - j) + n(i, j - M) * (M - i) * j + n(i - M, j - M) * i * j) / (M * M);
49
+ const k = (j * M + i) * 4, a = Math.min(255, Math.abs(v) * 120);
50
+ mi.data[k] = mi.data[k + 1] = mi.data[k + 2] = v > 0 ? 255 : 0; mi.data[k + 3] = a;
51
+ }
52
+ mx.putImageData(mi, 0, 0);
53
+ x.imageSmoothingEnabled = true;
54
+ for (let oy = -1; oy <= 1; oy++) for (let ox = -1; ox <= 1; ox++) x.drawImage(mc, ox * S, oy * S, S, S);
55
+ // fine grain
56
+ const g = x.getImageData(0, 0, S, S), d = g.data;
57
+ for (let i = 0; i < d.length; i += 4) {
58
+ const n = r() - 0.5;
59
+ if (r() < 0.5) continue;
60
+ const a = Math.abs(n) * 60;
61
+ // blend fleck over mottle
62
+ const c = n > 0 ? 255 : 0, aa = a / 255, oa = d[i + 3] / 255, na = aa + oa * (1 - aa);
63
+ if (na > 0) {
64
+ d[i] = (c * aa + d[i] * oa * (1 - aa)) / na; d[i + 1] = d[i]; d[i + 2] = d[i];
65
+ d[i + 3] = na * 255;
66
+ }
67
+ }
68
+ x.putImageData(g, 0, 0);
69
+ // fibres
70
+ x.lineCap = 'round';
71
+ for (let n = 0; n < 1100; n++) {
72
+ const px = r() * S, py = r() * S, len = r.range(4, 22), a = r() * TAU, bend = r.range(-5, 5);
73
+ const light = r() < 0.55;
74
+ x.strokeStyle = light ? `rgba(255,255,255,${r.range(0.08, 0.22)})` : `rgba(0,0,0,${r.range(0.05, 0.14)})`;
75
+ x.lineWidth = r.range(0.4, 1.1);
76
+ for (let oy = -1; oy <= 1; oy++) for (let ox = -1; ox <= 1; ox++) {
77
+ const sx = px + ox * S, sy = py + oy * S;
78
+ if (sx < -30 || sx > S + 30 || sy < -30 || sy > S + 30) continue;
79
+ x.beginPath(); x.moveTo(sx, sy);
80
+ x.quadraticCurveTo(sx + Math.cos(a) * len * 0.5 - Math.sin(a) * bend, sy + Math.sin(a) * len * 0.5 + Math.cos(a) * bend, sx + Math.cos(a) * len, sy + Math.sin(a) * len);
81
+ x.stroke();
82
+ }
83
+ }
84
+ return cv;
85
+ };
86
+
87
+ // =====================================================================
88
+ // EXPLAINER: "How does AI learn?" told as one long paper diorama that
89
+ // the camera pans along (parallax), with paper sprites cached once.
90
+ // =====================================================================
91
+ const PK = {};
92
+ const pkTex = () => PK.tex || (PK.tex = makeTex(77));
93
+ const paperize = (w, h, paint, o = {}) => {
94
+ const L = H.layer(Math.ceil(w), Math.ceil(h)), c = L.getContext('2d');
95
+ paint(c);
96
+ const r = H.rng(o.seed || 7);
97
+ const pat = c.createPattern(pkTex(), 'repeat');
98
+ pat.setTransform(new DOMMatrix().translateSelf(r() * 512, r() * 512).rotateSelf(r.range(-30, 30)));
99
+ c.save(); c.globalCompositeOperation = 'source-atop'; c.globalAlpha = o.tex ?? 1; c.fillStyle = pat; c.fillRect(0, 0, w, h); c.restore();
100
+ const edge = (col, dx, dy) => {
101
+ const T = H.layer(L.width, L.height), tc = T.getContext('2d');
102
+ tc.drawImage(L, 0, 0); tc.globalCompositeOperation = 'source-in'; tc.fillStyle = col; tc.fillRect(0, 0, w, h);
103
+ tc.globalCompositeOperation = 'destination-out'; tc.drawImage(L, dx, dy);
104
+ c.save(); c.globalCompositeOperation = 'source-atop'; c.drawImage(T, 0, 0); c.restore();
105
+ };
106
+ if (o.rim !== false) { edge(`rgba(255,247,232,${o.hi ?? 0.38})`, 1.3, 1.6); edge(`rgba(0,0,0,${o.lo ?? 0.28})`, -1.1, -1.3); }
107
+ return L;
108
+ };
109
+ const shadowDraw = (ctx, img, x, y, d = 1, sa = 0.5) => {
110
+ ctx.save();
111
+ if (d > 0) {
112
+ ctx.shadowColor = `rgba(14,10,30,${sa})`; ctx.shadowBlur = 6 + d * 7; ctx.shadowOffsetX = 1.5 + d * 2.4; ctx.shadowOffsetY = 2 + d * 3.6;
113
+ ctx.drawImage(img, x, y);
114
+ ctx.shadowColor = 'rgba(10,6,20,0.38)'; ctx.shadowBlur = 2.5; ctx.shadowOffsetX = 0.8; ctx.shadowOffsetY = 1.4;
115
+ }
116
+ ctx.drawImage(img, x, y); ctx.restore();
117
+ };
118
+ const put = (ctx, spr, cx, cy, o = {}) => {
119
+ ctx.save(); ctx.translate(cx, cy);
120
+ if (o.rot) ctx.rotate(o.rot);
121
+ if (o.sx != null || o.s != null) ctx.scale(o.sx ?? o.s ?? 1, o.sy ?? o.s ?? 1);
122
+ if (o.a != null) ctx.globalAlpha = o.a;
123
+ shadowDraw(ctx, spr, -spr.width / 2, -spr.height / 2, o.d ?? 1, o.sa ?? 0.5);
124
+ ctx.restore();
125
+ };
126
+ const compose = (w, h, layers) => {
127
+ const C = H.layer(w, h), c = C.getContext('2d');
128
+ layers.forEach(([paint, d, o], i) => shadowDraw(c, paperize(w, h, paint, { seed: 30 + i * 7, ...(o || {}) }), 0, 0, d, 0.4));
129
+ return C;
130
+ };
131
+ const cache = (k, f) => PK[k] || (PK[k] = f());
132
+ const AF = (px) => `400 ${px}px "Archivo Black"`;
133
+ const measure = (str, font) => { const c = H.layer(4, 4).getContext('2d'); c.font = font; return c.measureText(str).width; };
134
+ // cut letters (or a word) on a cut-around backing
135
+ const wordSpr = (str, px, col, back = '#f3ecdc', bw = 18) => cache('w' + str + px + col + back, () => {
136
+ const font = AF(px), tw = measure(str, font), w = tw + bw * 2 + 16, h = px * 1.35 + bw * 2;
137
+ const by = h / 2 + px * 0.36;
138
+ const layers = [];
139
+ if (back) layers.push([(c) => { c.font = font; c.lineJoin = 'round'; c.lineWidth = bw * 1.6; c.strokeStyle = back; c.fillStyle = back; c.strokeText(str, bw + 8, by); c.fillText(str, bw + 8, by); }, 0]);
140
+ layers.push([(c) => { c.font = font; c.fillStyle = col; c.fillText(str, bw + 8, by); }, back ? 0.45 : 0]);
141
+ return compose(w, h, layers);
142
+ });
143
+ const blobPath = (c, parts) => { c.beginPath(); parts.forEach((p) => poly(c, p)); };
144
+ // little cut-paper picture cards with a tag that names them
145
+ const cardSpr = (kind) => cache('card' + kind, () => {
146
+ const r = H.rng(kind === 'cat' ? 11 : 12);
147
+ const cat = kind === 'cat';
148
+ return compose(140, 176, [
149
+ [(c) => { c.fillStyle = '#f3ecdc'; c.beginPath(); poly(c, cutLines(r, [[8, 8], [132, 8], [132, 168], [8, 168], [8, 8]], 20, 0.5)); c.fill(); }, 0],
150
+ [(c) => { c.fillStyle = cat ? '#86bccd' : '#e8c56b'; c.fillRect(20, 20, 100, 94); }, 0.3],
151
+ [(c) => {
152
+ c.fillStyle = cat ? '#e8874c' : '#6e4428';
153
+ c.beginPath();
154
+ if (cat) { poly(c, [[44, 58], [50, 26], [66, 48]]); poly(c, [[96, 58], [90, 26], [74, 48]]); }
155
+ else { c.ellipse(42, 66, 11, 24, 0.3, 0, TAU); c.ellipse(98, 66, 11, 24, -0.3, 0, TAU); }
156
+ c.fill();
157
+ }, 0.3],
158
+ [(c) => { c.fillStyle = cat ? '#f09a5c' : '#b07a4c'; c.beginPath(); poly(c, circPts(r, 70, 70, 29, 10, 0.5)); c.fill(); }, 0.45],
159
+ [(c) => {
160
+ c.fillStyle = cat ? '#fbe3c8' : '#f3dfc2'; c.beginPath(); c.ellipse(70, 84, cat ? 13 : 16, cat ? 9 : 12, 0, 0, TAU); c.fill();
161
+ c.fillStyle = '#2b2033'; c.beginPath(); c.arc(59, 64, 4, 0, TAU); c.arc(81, 64, 4, 0, TAU); c.fill();
162
+ c.fillStyle = cat ? '#e0607a' : '#2b2033'; c.beginPath(); c.ellipse(70, 79, cat ? 4 : 6, cat ? 3 : 4.5, 0, 0, TAU); c.fill();
163
+ }, 0.25],
164
+ [(c) => { c.fillStyle = '#fbf6ea'; c.beginPath(); poly(c, [[30, 128], [100, 128], [114, 145], [100, 162], [30, 162]]); c.fill(); c.globalCompositeOperation = 'destination-out'; c.beginPath(); c.arc(103, 145, 4, 0, TAU); c.fill(); }, 0.55],
165
+ [(c) => { c.font = AF(24); c.fillStyle = cat ? '#d9473b' : '#2f7f86'; c.fillText(kind, 38, 154); }, 0.12],
166
+ ]);
167
+ });
168
+ const brainSpr = () => cache('brain', () => {
169
+ const r = H.rng(21), parts = [], inner = [];
170
+ [[-90, 10, 58], [-50, -50, 62], [15, -68, 64], [80, -40, 58], [104, 22, 50], [50, 50, 60], [-30, 52, 62], [-10, 0, 70]].forEach(([x, y, rr]) => parts.push(circPts(r, 160 + x, 128 + y, rr, 12, 0.6)));
171
+ [[-60, -10, 26], [-20, -40, 24], [30, -30, 26], [70, 0, 22], [20, 25, 24], [-30, 30, 22]].forEach(([x, y, rr]) => inner.push(circPts(r, 160 + x, 128 + y, rr, 10, 0.5)));
172
+ return compose(330, 260, [
173
+ [(c) => { c.fillStyle = '#e8839a'; blobPath(c, parts); c.fill(); }, 0],
174
+ [(c) => { c.strokeStyle = '#f3a7b6'; c.lineWidth = 13; c.lineCap = 'round'; c.lineJoin = 'round';
175
+ [[[70, 120], [95, 80], [130, 95], [120, 60]], [[70, 170], [110, 150], [100, 120]], [[195, 60], [230, 80], [210, 110], [245, 130]], [[190, 180], [225, 150], [255, 175]], [[115, 205], [140, 180]], [[205, 210], [240, 200]]].forEach((ln) => { c.beginPath(); ln.forEach(([x, y], i) => (i ? c.lineTo(x, y) : c.moveTo(x, y))); c.stroke(); });
176
+ }, 0.35],
177
+ [(c) => { c.fillStyle = '#c95f7a'; c.fillRect(156, 52, 8, 150); }, 0.2],
178
+ ]);
179
+ });
180
+ const panelSpr = () => cache('panel', () => compose(300, 96, [
181
+ [(c) => { c.fillStyle = '#2f7f86'; c.beginPath(); poly(c, cutLines(H.rng(4), [[6, 6], [294, 6], [294, 90], [6, 90], [6, 6]], 24, 0.5)); c.fill(); }, 0],
182
+ [(c) => { c.fillStyle = '#f3ecdc'; [70, 150, 230].forEach((x) => { c.beginPath(); poly(c, circPts(H.rng(x), x, 48, 28, 10, 0.4)); c.fill(); }); }, 0.45],
183
+ ]));
184
+ const pointerSpr = () => cache('ptr', () => paperize(16, 56, (c) => { c.fillStyle = '#d9473b'; c.beginPath(); poly(c, [[8, 2], [14, 30], [10, 54], [6, 54], [2, 30]]); c.fill(); }, { seed: 5 }));
185
+ const bubbleSpr = (txt) => cache('bub' + txt, () => {
186
+ const r = H.rng(txt.length + 3);
187
+ return compose(230, 150, [
188
+ [(c) => { c.fillStyle = '#fbf6ea'; c.beginPath(); c.ellipse(118, 64, 100, 54, 0, 0, TAU); c.moveTo(40, 92); c.lineTo(18, 140); c.lineTo(78, 112); c.fill(); }, 0],
189
+ [(c) => { c.font = AF(46); c.fillStyle = txt === 'cat' ? '#2f7f86' : '#2b2d5a'; const w = measure(txt, AF(46)); c.fillText(txt, 118 - w / 2, 82); }, 0.35],
190
+ ]);
191
+ });
192
+ const crossSpr = () => cache('x', () => paperize(110, 110, (c) => { c.fillStyle = '#d9473b'; c.save(); c.translate(55, 55); c.rotate(0.785); c.fillRect(-48, -11, 96, 22); c.fillRect(-11, -48, 22, 96); c.restore(); }, { seed: 9 }));
193
+ const checkSpr = () => cache('ok', () => paperize(120, 110, (c) => { c.fillStyle = '#5fae6a'; c.beginPath(); poly(c, [[8, 58], [26, 40], [46, 62], [96, 10], [114, 28], [46, 98]]); c.fill(); }, { seed: 10 }));
194
+ const ringSpr = () => cache('ring', () => paperize(300, 230, (c) => { c.fillStyle = '#f2b545'; c.beginPath(); c.ellipse(150, 115, 145, 110, 0, 0, TAU); c.ellipse(150, 115, 125, 92, 0, 0, TAU); c.fill('evenodd'); }, { seed: 12 }));
195
+ const burstSpr = () => cache('burst', () => paperize(340, 340, (c) => { c.fillStyle = '#f2b545'; c.beginPath(); for (let i = 0; i < 32; i++) { const a = (i / 32) * TAU, rr = i % 2 ? 100 : 168; const x = 170 + Math.cos(a) * rr, y = 170 + Math.sin(a) * rr; i ? c.lineTo(x, y) : c.moveTo(x, y); } c.closePath(); c.fill(); }, { seed: 13 }));
196
+ const digitSpr = (ch, px, col) => wordSpr(ch, px, col, '#f3ecdc', 10);
197
+ // cut-paper arrow (strip + head) revealed along a quadratic path
198
+ const arrow = (ctx, ax, ay, cx, cy, bx, by, p, col, seed) => {
199
+ if (p <= 0) return;
200
+ const pts = [];
201
+ for (let i = 0; i <= 40 * p; i++) { const u = i / 40; pts.push([(1 - u) * (1 - u) * ax + 2 * u * (1 - u) * cx + u * u * bx, (1 - u) * (1 - u) * ay + 2 * u * (1 - u) * cy + u * u * by]); }
202
+ if (pts.length < 2) return;
203
+ let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
204
+ pts.forEach(([x, y]) => { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y); });
205
+ x0 -= 34; y0 -= 34; x1 += 34; y1 += 34;
206
+ const [ex, ey] = pts[pts.length - 1], [px, py] = pts[pts.length - 2], a = Math.atan2(ey - py, ex - px);
207
+ const L = paperize(x1 - x0, y1 - y0, (c) => {
208
+ c.translate(-x0, -y0); c.strokeStyle = col; c.lineWidth = 13; c.lineCap = 'butt'; c.lineJoin = 'round';
209
+ c.beginPath(); pts.forEach(([x, y], i) => (i ? c.lineTo(x - Math.cos(a) * 12 * (i === pts.length - 1), y - Math.sin(a) * 12 * (i === pts.length - 1)) : c.moveTo(x, y))); c.stroke();
210
+ c.fillStyle = col; c.beginPath(); c.moveTo(ex + Math.cos(a) * 14, ey + Math.sin(a) * 14);
211
+ c.lineTo(ex + Math.cos(a + 2.5) * 22, ey + Math.sin(a + 2.5) * 22); c.lineTo(ex + Math.cos(a - 2.5) * 22, ey + Math.sin(a - 2.5) * 22); c.fill();
212
+ }, { seed });
213
+ shadowDraw(ctx, L, x0, y0, 0.6);
214
+ };
215
+ const fmt = (n) => Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
216
+ const numberRow = (ctx, str, x, y, px, col, o = {}) => {
217
+ let cx = x;
218
+ for (const ch of str) {
219
+ const s = digitSpr(ch, px, col), w = s.width - 22;
220
+ const jr = H.rng(ch.charCodeAt(0) * 13 + Math.round(cx));
221
+ put(ctx, s, cx + w / 2, y, { rot: (jr() - 0.5) * 0.08, d: o.d ?? 0.7 });
222
+ cx += w - 2;
223
+ }
224
+ return cx;
225
+ };
226
+ // drop-in animation helper (paper falls on, overshoots, settles; shadow is lifted while in the air)
227
+ const dropIn = (t, st, du = 0.4, dist = 60) => {
228
+ const u = H.clamp((t - st) / du);
229
+ if (u <= 0) return null;
230
+ const e = H.ease.back(u), lift = 1 - H.ease.out(u);
231
+ return { dy: -(1 - e) * dist, s: 1 + lift * 0.08, d: lift * 3, a: H.clamp(u * 4) };
232
+ };
233
+
234
+ const EXPLAIN = (ctx, W, H_, t) => {
235
+ // ---- camera: one long diorama, panned station to station ----
236
+ const seg = (a, b) => H.ease.inOut(H.clamp((t - a) / (b - a)));
237
+ const cam = 1280 * seg(1.8, 2.8) + 380 * seg(5.6, 6.2) + 1280 * seg(10.1, 10.8);
238
+ // backboard + stars (screen-fixed, far away)
239
+ ctx.drawImage(cache('sky', () => {
240
+ const C = H.layer(W, H_), c = C.getContext('2d');
241
+ c.drawImage(paperize(W, H_, (x) => { x.fillStyle = '#2b2d5a'; x.fillRect(0, 0, W, H_); }, { rim: false, seed: 2 }), 0, 0);
242
+ const r = H.rng(31);
243
+ const st = paperize(W, H_, (x) => { x.fillStyle = '#f6e7bf'; for (let i = 0; i < 60; i++) { const sx = r.range(10, W - 10), sy = r.range(10, 430), s = r.range(3, 7), pts = []; for (let j = 0; j < 10; j++) { const rr = j % 2 ? s * 0.42 : s, a = (j / 10) * TAU; pts.push([sx + Math.cos(a) * rr, sy + Math.sin(a) * rr]); } x.beginPath(); poly(x, pts); x.fill(); } }, { hi: 0.3 });
244
+ shadowDraw(c, st, 0, 0, 0.25);
245
+ return C;
246
+ }), 0, 0);
247
+ // far mountains (parallax 0.3)
248
+ const far = cache('far', () => {
249
+ const r = H.rng(71), f = (x) => 470 + 70 * Math.sin(x * 0.006 + 1) + 40 * Math.sin(x * 0.017);
250
+ // sprite starts 40px higher than before so the tallest peak (y 360) is no longer sliced flat by the sprite top
251
+ return paperize(2400, 400, (c) => { c.fillStyle = '#4c5590'; c.beginPath(); poly(c, [...ridge(r, 0, 2400, (x) => f(x) - 340, 14, 30, 0.6), [2400, 400], [0, 400]]); c.fill(); });
252
+ });
253
+ shadowDraw(ctx, far, -cam * 0.3, 340, 1.0);
254
+ // mid ridge (parallax 0.6): a second scissor-cut hill sheet with little paper pines glued on,
255
+ // so the backdrop reads as a stacked shadow-box like the hero rather than one ridge + a slab
256
+ const mid = cache('mid', () => {
257
+ const r = H.rng(91), f = (x) => 62 + 26 * Math.sin(x * 0.0047 + 0.6) + 14 * Math.sin(x * 0.0135 + 2.1) + 6 * Math.sin(x * 0.037);
258
+ const C = H.layer(3200, 300), c = C.getContext('2d');
259
+ shadowDraw(c, paperize(3200, 300, (x) => { x.fillStyle = '#3d5f7a'; x.beginPath(); poly(x, [...ridge(r, 0, 3200, f, 12, 28, 0.6), [3200, 300], [0, 300]]); x.fill(); }, { seed: 92 }), 0, 0, 0);
260
+ // pines: three stacked zigzag tiers on a trunk, each pine its own cut sheet
261
+ const pr = H.rng(93);
262
+ for (let px = 60; px < 3200; px += pr.range(130, 320)) {
263
+ const h = pr.range(46, 84), base = f(px) + h * 0.18, w = h * 0.5, col = pr() < 0.5 ? '#2a5560' : '#335f6a';
264
+ const pts = [];
265
+ for (let k = 0; k < 3; k++) {
266
+ const ty = base - h * 0.16 - k * h * 0.25, tw = w * (1 - k * 0.24), th = h * 0.4;
267
+ const tier = [[px, ty - th]];
268
+ for (let j = 0; j <= 4; j++) { const u = j / 4; tier.push([px + tw / 2 - u * tw, ty + (j % 2 ? -4 : 0) + pr.gauss() * 0.5]); }
269
+ pts.push(tier);
270
+ }
271
+ const pw = Math.ceil(w + 24), ph = Math.ceil(h * 1.25 + 10), ox = px - pw / 2, oy = base - h * 1.15;
272
+ const sp = paperize(pw, ph, (x) => { x.translate(-ox, -oy); x.fillStyle = '#3a2c2a'; x.fillRect(px - 3, base - h * 0.22, 6, h * 0.3); x.fillStyle = col; x.beginPath(); pts.forEach((p) => poly(x, p)); x.fill(); }, { seed: 94 + Math.round(px) });
273
+ shadowDraw(c, sp, ox, oy, 0.5, 0.4);
274
+ }
275
+ return C;
276
+ });
277
+ shadowDraw(ctx, mid, -cam * 0.6, 452, 1.15);
278
+ // ground (world, parallax 1): torn top edge showing the white fibrous core of the card, like the hero's hills
279
+ const ground = cache('ground', () => {
280
+ const nz = H.makeNoise(83), r = H.rng(81), f = (x) => 22 + 12 * Math.sin(x * 0.005 + 1.3) + 7 * Math.sin(x * 0.019);
281
+ const torn = [], core = [];
282
+ for (let x = 0; x <= 4500; x += 2.5) {
283
+ const y = f(x) + nz.fbm2(x * 0.03, 0.5, 4) * 5 + r.gauss() * 0.6;
284
+ torn.push([x, y + 3 + Math.abs(nz.n2(x * 0.045, 3.1)) * 7 + Math.abs(nz.n2(x * 0.3, 7.7)) * 2]);
285
+ core.push([x, y]);
286
+ }
287
+ return paperize(4500, 200, (c) => {
288
+ c.fillStyle = '#f3ecdc'; c.beginPath(); poly(c, [...core, [4500, 200], [0, 200]]); c.fill();
289
+ c.strokeStyle = 'rgba(248,244,236,0.95)'; c.lineCap = 'round';
290
+ for (let i = 0; i < core.length; i++) {
291
+ if (!r.chance(0.55)) continue;
292
+ const [x, y] = core[i], a = -Math.PI / 2 + r.range(-0.9, 0.9), l = r.range(1.5, 5.5);
293
+ c.lineWidth = r.range(0.4, 0.9); c.beginPath(); c.moveTo(x, y + 1); c.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); c.stroke();
294
+ }
295
+ c.fillStyle = '#2f6b66'; c.beginPath(); poly(c, [...torn, [4500, 200], [0, 200]]); c.fill();
296
+ // grass snips cut up out of the green sheet
297
+ const gr = H.rng(84);
298
+ c.beginPath();
299
+ for (let x = 30; x < 4480; x += gr.range(60, 190)) {
300
+ const n = gr.int(2, 4), by = f(x) + 40 + gr.range(0, 60);
301
+ for (let k = 0; k < n; k++) { const bx = x + k * 7; c.moveTo(bx, by); c.lineTo(bx + 2 + gr.range(-3, 4), by - gr.range(10, 22)); c.lineTo(bx + 5, by); }
302
+ }
303
+ c.fillStyle = '#285f5a'; c.fill();
304
+ }, { seed: 85 });
305
+ });
306
+ shadowDraw(ctx, ground, -cam - 100, 560, 1.3);
307
+ // nearest strip (parallax 1.25): dark foreground card with grass snips along the bottom edge
308
+ const fore = cache('fore', () => {
309
+ const r = H.rng(141), f = (x) => 30 + 10 * Math.sin(x * 0.0045 + 0.4) + 5 * Math.sin(x * 0.013), pts = [];
310
+ let x = 0;
311
+ while (x < 5200) {
312
+ if (r.chance(0.2)) {
313
+ const n = r.int(2, 4);
314
+ for (let k = 0; k < n; k++) { const bx = x + k * 7, by = f(bx); pts.push([bx, by], [bx + 2 + r.range(-3, 4), by - r.range(12, 26)], [bx + 5, by]); }
315
+ x += n * 7 + 4;
316
+ } else { pts.push([x, f(x) + r.gauss() * 0.6]); x += r.range(14, 30); }
317
+ }
318
+ return paperize(5200, 90, (c) => { c.fillStyle = '#163a3a'; c.beginPath(); poly(c, [...pts, [5200, 90], [0, 90]]); c.fill(); }, { seed: 142 });
319
+ });
320
+ shadowDraw(ctx, fore, -cam * 1.25 - 60, 660, 1.6);
321
+
322
+ ctx.save(); ctx.translate(-cam, 0); // ---------------- world space ----------------
323
+
324
+ // ---- BEAT 1: the question, cut letters dropped on the backboard ----
325
+ {
326
+ const lines = [['How does', 92, 250], ['AI learn?', 128, 400]];
327
+ let i = 0;
328
+ lines.forEach(([str, px, y]) => {
329
+ const tot = measure(str, AF(px)) + str.length * 4;
330
+ let x = 640 - tot / 2;
331
+ for (const ch of str) {
332
+ const w = measure(ch, AF(px));
333
+ if (ch !== ' ') {
334
+ const A = dropIn(t, 0.15 + i * 0.06, 0.38, 70);
335
+ const cols = ['#e8604a', '#f2b545', '#58b0a6'];
336
+ if (A && cam < 1300) put(ctx, wordSpr(ch, px, cols[i % 3], '#f3ecdc', 14), x + w / 2, y + A.dy, { s: A.s, d: 0.8 + A.d, rot: (H.rng(i + 3)() - 0.5) * 0.12 });
337
+ i++;
338
+ }
339
+ x += w + 4;
340
+ }
341
+ });
342
+ }
343
+
344
+ // ---- BEAT 2: labelled examples ride a paper conveyor into the model ----
345
+ const BX = 2290, BY = 330;
346
+ if (cam > 400) {
347
+ const belt = cache('belt', () => compose(1060, 120, [
348
+ [(c) => { // cut-card supports: tapered teal legs with a foot tab
349
+ const r = H.rng(88);
350
+ [37, 527, 1007].forEach((x) => { c.fillStyle = '#24585b'; c.beginPath(); poly(c, cutLines(r, [[x - 14, 50], [x + 14, 50], [x + 17, 112], [x + 26, 112], [x + 26, 120], [x - 26, 120], [x - 26, 112], [x - 17, 112], [x - 14, 50]], 16, 0.4)); c.fill();
351
+ c.fillStyle = 'rgba(8,24,30,0.35)'; c.beginPath(); poly(c, [[x + 3, 50], [x + 14, 50], [x + 17, 112], [x + 5, 112]]); c.fill(); });
352
+ }, 0.5],
353
+ [(c) => { c.fillStyle = '#1f4f53'; c.beginPath(); poly(c, cutLines(H.rng(8), [[0, 20], [1060, 20], [1060, 56], [0, 56], [0, 20]], 26, 0.5)); c.fill(); }, 0.4],
354
+ [(c) => { c.fillStyle = '#f2b545'; for (let x = 40; x < 1060; x += 90) { c.beginPath(); c.arc(x, 38, 11, 0, TAU); c.fill(); } }, 0.3],
355
+ ]));
356
+ shadowDraw(ctx, belt, 1110, 470, 0.9);
357
+ // flow arrow over the belt
358
+ arrow(ctx, 1380, 300, 1700, 262, 2080, 300, seg(2.7, 3.3), '#f2b545', 41);
359
+ // cards
360
+ const r = H.rng(55);
361
+ for (let k = 0; k < 9; k++) {
362
+ const s = 2.15 + k * 0.3, kind = r() < 0.5 ? 'cat' : 'dog', wob = r() * 6;
363
+ const x = 1150 + (t - s) * 650;
364
+ if (t < s || x > BX - 60) continue;
365
+ put(ctx, cardSpr(kind), x, 410 + Math.sin(t * 9 + wob) * 2, { rot: Math.sin(t * 5 + wob) * 0.03, d: 0.9 });
366
+ }
367
+ // counter: paper digits
368
+ const A = dropIn(t, 2.55, 0.4, 50);
369
+ if (A) {
370
+ const n = 1e6 * Math.pow(H.clamp((t - 2.8) / 2.3), 2.2);
371
+ const end = numberRow(ctx, fmt(n), 1440, 165 + A.dy, 64, '#f2b545');
372
+ put(ctx, wordSpr('examples', 40, '#58b0a6'), end + 120, 172 + A.dy, { d: 0.7 });
373
+ }
374
+ }
375
+ // model: brain + dial panel (stays through beat 3)
376
+ let gulp = 0;
377
+ { const r = H.rng(55); for (let k = 0; k < 9; k++) { const s = 2.15 + k * 0.3; r(); r(); const tin = s + (BX - 60 - 1150) / 650; gulp += Math.exp(-Math.pow((t - tin) * 9, 2)); } }
378
+ const PB = dropIn(t, 2.6, 0.45, 80);
379
+ if (PB) {
380
+ put(ctx, panelSpr(), BX, 560 + PB.dy, { d: 1 + PB.d });
381
+ const adj = seg(8.9, 9.5);
382
+ [0, 1, 2].forEach((i) => {
383
+ const base = [-0.9, 0.5, -0.3][i], delta = [1.3, -0.9, 1.0][i];
384
+ const ang = base + delta * adj + (adj > 0 && adj < 1 ? Math.sin(t * 40 + i) * 0.08 : 0);
385
+ put(ctx, pointerSpr(), BX - 80 + i * 80 + Math.sin(ang) * 14, 560 + PB.dy - Math.cos(ang) * 14, { rot: ang, d: 0.4 });
386
+ });
387
+ put(ctx, brainSpr(), BX, BY + PB.dy, { s: (1 + gulp * 0.05) * PB.s, d: 1.4 + PB.d });
388
+ const T1 = dropIn(t, 3.3, 0.35, 40);
389
+ if (T1) put(ctx, wordSpr('model', 30, '#2b2d5a', '#fbf6ea', 10), BX - 110, BY + 118 + T1.dy, { rot: -0.1, d: 0.8 + T1.d });
390
+ }
391
+
392
+ // ---- BEAT 3: guess -> check -> adjust loop ----
393
+ if (t > 6.0) {
394
+ const NC = dropIn(t, 6.3, 0.4, 90);
395
+ if (NC) put(ctx, cardSpr('cat'), 1880, 300 + NC.dy, { s: 1.15 * NC.s, d: 1.2 + NC.d, rot: -0.05 });
396
+ arrow(ctx, 1975, 300, 2040, 270, 2120, 300, seg(6.7, 7.0), '#f2b545', 51);
397
+ arrow(ctx, 2445, 290, 2510, 250, 2570, 262, seg(7.05, 7.35), '#f2b545', 52);
398
+ const G = dropIn(t, 7.2, 0.3, 40);
399
+ if (G) put(ctx, wordSpr('Guess', 30, '#2b2d5a', '#fbf6ea', 10), 2505, 205 + G.dy, { rot: -0.08, d: 0.8 + G.d });
400
+ // bubble flips from "dog?" to "cat"
401
+ const B = dropIn(t, 7.4, 0.35, 50);
402
+ if (B) {
403
+ const flip = H.clamp((t - 9.55) / 0.3), sx = Math.abs(Math.cos(flip * Math.PI)) + 0.001;
404
+ const wrong = flip < 0.5;
405
+ const shake = t > 7.9 && t < 8.3 ? Math.sin(t * 60) * 6 * (8.3 - t) * 2.5 : 0;
406
+ const ring = dropIn(t, 7.9, 0.3, 0);
407
+ if (ring && wrong) put(ctx, ringSpr(), 2690, 255, { s: 0.7 + 0.3 * H.ease.back(H.clamp((t - 7.9) / 0.3)), d: 0.6, rot: 0.05 });
408
+ put(ctx, bubbleSpr(wrong ? 'dog?' : 'cat'), 2690 + shake, 260 + B.dy, { sx, sy: 1, d: 1 + B.d });
409
+ const X = dropIn(t, 7.9, 0.25, 40);
410
+ if (X && wrong) put(ctx, crossSpr(), 2790, 215 + X.dy, { s: X.s, d: 0.8 + X.d, rot: 0.1 });
411
+ const C = dropIn(t, 8.05, 0.3, 40);
412
+ if (C && t < 9.55) put(ctx, wordSpr('Check', 30, '#d9473b', '#fbf6ea', 10), 2640, 345 + C.dy, { rot: 0.06, d: 0.8 + C.d });
413
+ const V = dropIn(t, 9.9, 0.3, 50);
414
+ if (V) put(ctx, checkSpr(), 2795, 215 + V.dy, { s: V.s, d: 0.8 + V.d, rot: -0.06 });
415
+ }
416
+ // error signal travels back to the dials
417
+ if (t < 9.6) arrow(ctx, 2660, 335, 2580, 560, 2420, 560, seg(8.3, 8.8), '#d9473b', 53);
418
+ const AJ = dropIn(t, 8.7, 0.3, 40);
419
+ if (AJ && t < 9.6) put(ctx, wordSpr('Adjust', 30, '#d9473b', '#fbf6ea', 10), 2560, 610 + AJ.dy, { rot: -0.05, d: 0.8 + AJ.d });
420
+ }
421
+
422
+ // ---- BEAT 4: progress chart, bars pop up from behind the ground ----
423
+ if (cam > 2000) {
424
+ const OXc = 3250, base = 590, accs = [61, 72, 80, 86, 90, 93, 95, 97];
425
+ const axis = cache('axis', () => compose(640, 400, [[(c) => { c.fillStyle = '#f3ecdc'; c.fillRect(20, 0, 12, 390); c.fillRect(20, 378, 620, 12); }, 0]]));
426
+ shadowDraw(ctx, axis, OXc - 26, base - 388, 0.6);
427
+ let shown = 61, last = -1;
428
+ ctx.save(); ctx.beginPath(); ctx.rect(OXc - 40, 0, 700, base - 6); ctx.clip();
429
+ accs.forEach((a, i) => {
430
+ const st = 10.85 + i * 0.22, u = H.clamp((t - st) / 0.32);
431
+ if (u <= 0) return;
432
+ const hgt = a * 3.4, bar = cache('bar' + i, () => paperize(56, hgt, (c) => { c.fillStyle = i === 7 ? '#e8604a' : i % 2 ? '#58b0a6' : '#f2b545'; c.beginPath(); poly(c, cutLines(H.rng(i), [[2, 2], [54, 2], [54, hgt], [2, hgt], [2, 2]], 22, 0.5)); c.fill(); }, { seed: 60 + i }));
433
+ shadowDraw(ctx, bar, OXc + 30 + i * 74, base - hgt * H.ease.back(u), 0.9);
434
+ last = i;
435
+ });
436
+ ctx.restore();
437
+ if (last >= 0) { const st = 10.85 + last * 0.22; shown = H.lerp(last ? accs[last - 1] : 55, accs[last], H.clamp((t - st) / 0.32)); }
438
+ const Lb = dropIn(t, 10.6, 0.35, 50);
439
+ if (Lb) {
440
+ if (t > 12.6) { const u = H.clamp((t - 12.6) / 0.35); put(ctx, burstSpr(), 3995, 168, { s: 0.2 + 0.8 * H.ease.back(u), rot: t * 0.3, d: 0.7 }); }
441
+ put(ctx, wordSpr('Accuracy', 44, '#58b0a6'), 3480, 170 + Lb.dy, { d: 0.8 + Lb.d, rot: -0.03 });
442
+ const pop = t > 12.6 ? 1 + 0.18 * Math.sin(H.clamp((t - 12.6) / 0.35) * Math.PI) : 1;
443
+ ctx.save(); ctx.translate(3995, 170); ctx.scale(pop, pop); ctx.translate(-3995, -170);
444
+ numberRow(ctx, Math.round(shown) + '%', 3900, 165 + Lb.dy, 72, '#e8604a');
445
+ ctx.restore();
446
+ }
447
+ const RL = dropIn(t, 11.1, 0.3, 30);
448
+ if (RL) put(ctx, wordSpr('training rounds', 26, '#2b2d5a', '#fbf6ea', 9), OXc + 330, base + 40 + RL.dy, { d: 0.6 + RL.d });
449
+ }
450
+ ctx.restore(); // ---------------- end world ----------------
451
+
452
+ // header: the question shrinks to a pinned paper tag
453
+ const HD = dropIn(t, 2.0, 0.4, 60);
454
+ if (HD) put(ctx, wordSpr('How does AI learn?', 30, '#f3ecdc', '#e8604a', 10), 225, 48 + HD.dy, { rot: -0.03, d: 1 + HD.d });
455
+
456
+ // ---- BEAT 5: takeaway banner ----
457
+ const TB = dropIn(t, 13.0, 0.45, 80);
458
+ if (TB) {
459
+ const ban = cache('banner', () => {
460
+ const a = wordSpr('More examples', 38, '#2b2d5a', null), b = wordSpr('better guesses', 38, '#e8604a', null);
461
+ const w = a.width + b.width + 110, C = H.layer(w + 40, 110), c = C.getContext('2d');
462
+ shadowDraw(c, paperize(w + 40, 110, (x) => { x.fillStyle = '#fbf6ea'; x.beginPath(); poly(x, cutLines(H.rng(3), [[6, 12], [w + 34, 6], [w + 30, 98], [10, 104], [6, 12]], 30, 0.6)); x.fill(); }, { seed: 4 }), 0, 0, 0);
463
+ shadowDraw(c, a, 20, 55 - a.height / 2, 0.4, 0.3);
464
+ arrow(c, a.width + 22, 56, a.width + 50, 56, a.width + 84, 56, 1, '#58b0a6', 77);
465
+ shadowDraw(c, b, a.width + 100, 55 - b.height / 2, 0.4, 0.3);
466
+ return C;
467
+ });
468
+ put(ctx, ban, 640, 660 + TB.dy, { s: TB.s, d: 1.4 + TB.d, rot: -0.01 });
469
+ }
470
+
471
+ // ---- photo finish ----
472
+ ctx.save(); ctx.globalCompositeOperation = 'soft-light';
473
+ const lg = ctx.createRadialGradient(260, 60, 40, 420, 200, 1100);
474
+ lg.addColorStop(0, 'rgba(255,236,200,1)'); lg.addColorStop(0.5, 'rgba(255,230,200,0.25)'); lg.addColorStop(1, 'rgba(0,0,20,0.6)');
475
+ ctx.fillStyle = lg; ctx.fillRect(0, 0, W, H_);
476
+ // second, fainter warm pool on the stage where the action is (a lamp over the shadow-box)
477
+ const lp = ctx.createRadialGradient(720, 330, 20, 720, 330, 560);
478
+ lp.addColorStop(0, 'rgba(255,214,160,0.42)'); lp.addColorStop(1, 'rgba(255,214,160,0)');
479
+ ctx.fillStyle = lp; ctx.fillRect(0, 0, W, H_); ctx.restore();
480
+ H.vignette(ctx, W, H_, 0.42, '10,6,24');
481
+ H.grain(ctx, W, H_, 0.03, 9 + Math.floor(t * 12));
482
+ };
483
+
484
+ registerStyle({
485
+ id: 'cut-paper',
486
+ name: 'Cut-Paper Diorama',
487
+ category: 'Paper & Craft',
488
+ description: 'A layered cardstock shadow-box: every shape is a separate scissor-cut sheet with fibre texture, a lit paper edge and a soft drop shadow onto the sheet behind it; torn edges show the white core of the paper.',
489
+ motion: 'Layers slide in on parallax like a pop-up book, the rocket rises on its wire while paper smoke puffs and the flame tongues flutter.',
490
+ fonts: ['400 112px "Archivo Black"', '400 50px "Archivo Black"', '400 30px "Archivo Black"'],
491
+ frameT: 4.8,
492
+ duration: 5,
493
+ readsPixels: true,
494
+ explainerDuration: 15,
495
+ explainerNote: 'One long paper diorama the camera pans along on parallax; arrows are cut-paper strips that grow, labels are paper tags dropped onto what they name, numbers are swapped cut-out digit tiles, and emphasis is a paper ring or sunburst popping up behind.',
496
+ explainer(ctx, W, H_, t) { EXPLAIN(ctx, W, H_, t); },
497
+ // Sound: a cardstock shadow-box. Every dropped piece lands as a light card tap (body + paper snap) at the moment
498
+ // its back-eased drop first reaches rest (st + 0.37*du); camera pans are big cardstock layers sliding past each
499
+ // other; arrows are paper strips pushed out with a dry slide; digit tiles flick; the sunburst is a pop-up page.
500
+ // Times mirror EXPLAIN()/dropIn above (title 0.15+i*0.06, cam pans 1.8-2.8 / 5.6-6.2 / 10.1-10.8, bars 10.85+i*0.22).
501
+ explainerSound(k) {
502
+ const LAND = 1 - 1.70158 / 2.70158; // H.ease.back first reaches 1 here: the piece touches down
503
+ const land = (st, du) => st + LAND * du;
504
+ const seg = (t, a, b) => H.ease.inOut(H.clamp((t - a) / (b - a)));
505
+ const cam = (t) => 1280 * seg(t, 1.8, 2.8) + 380 * seg(t, 5.6, 6.2) + 1280 * seg(t, 10.1, 10.8);
506
+ const pan = (wx, t) => H.clamp(((wx - cam(t)) / 1280) * 2 - 1, -1, 1) * 0.6;
507
+ // card tap: a cut piece of cardstock landing on the sheet behind it
508
+ const card = (t, g = 1, p = 0, f = 1, minor = true) => {
509
+ k.noise(t, { dur: 0.045, filter: 'bandpass', f: 1100 * f, q: 0.9, gain: 0.26 * g, a: 0.001, pan: p, minor });
510
+ k.noise(t, { dur: 0.09, color: 'brown', filter: 'lowpass', f: 650, gain: 0.3 * g, a: 0.002, pan: p, minor });
511
+ k.tone(t, { f: [[0, 160 * f], [0.07, 85]], dur: 0.09, gain: 0.16 * g, pan: p, minor });
512
+ };
513
+ // a big cardstock layer sliding against the one behind it (camera pans): smooth, low, no crinkle
514
+ const layer = (t, d, g = 1) => {
515
+ k.noise(t, { dur: d, color: 'pink', filter: 'bandpass', f: [[0, 260], [d * 0.5, 620], [d, 300]], q: 0.6, gain: 0.16 * g, a: d * 0.45, pan: 0.15 });
516
+ k.noise(t + 0.05, { dur: d, color: 'brown', filter: 'lowpass', f: 380, gain: 0.14 * g, a: d * 0.5, pan: -0.15 });
517
+ };
518
+ // paper strip pushed out (arrows): a dry, soft slide with a little tooth, then the head taps down
519
+ const strip = (t0, t1, g = 1, p0 = 0, p1 = 0) => {
520
+ const d = t1 - t0;
521
+ k.noise(t0, { dur: d, filter: 'bandpass', f: [[0, 900], [d, 1500]], q: 0.8, gain: 0.09 * g, a: d * 0.3, hold: d * 0.4, pan: (p0 + p1) / 2, minor: true });
522
+ card(t1, 0.55 * g, p1, 1.2);
523
+ };
524
+ // digit tile flick (counters)
525
+ const flick = (t, g = 1, p = 0) => {
526
+ k.noise(t, { dur: 0.022, filter: 'bandpass', f: 1800, q: 1.4, gain: 0.22 * g, a: 0.001, pan: p, minor: true });
527
+ k.tone(t, { f: 220, dur: 0.03, gain: 0.06 * g, pan: p, minor: true });
528
+ };
529
+
530
+ k.bed('room', 0, 15, { gain: 0.45 });
531
+
532
+ // ---- BEAT 1: 15 cut letters drop onto the backboard
533
+ for (let i = 0; i < 15; i++) {
534
+ const line = i < 7 ? 0 : 1, j = line ? i - 7 : i, n = line ? 8 : 7;
535
+ card(land(0.15 + i * 0.06, 0.38), i === 14 ? 0.85 : 0.42 + (i % 3) * 0.07, ((j + 0.5) / n - 0.5) * 0.8, line ? 0.9 : 1.05);
536
+ }
537
+
538
+ // ---- pan 1: the diorama slides to the conveyor; header tag pinned
539
+ layer(1.8, 1.0, 1);
540
+ card(land(2.0, 0.4), 0.7, -0.6, 1.2);
541
+ card(land(2.55, 0.4), 0.8, pan(1600, 2.7)); // counter row drops
542
+ card(land(2.6, 0.45), 1.1, pan(2290, 2.77), 0.8, false); // dial panel + brain (the model) lands
543
+ strip(2.7, 3.3, 1, pan(1380, 3), pan(2080, 3.3));
544
+ card(land(3.3, 0.35), 0.6, pan(2180, 3.4), 1.2); // 'model' tag
545
+ // cards riding the belt (soft drag) and the brain gulping each one (tin = s + (BX-60-1150)/650)
546
+ k.noise(2.3, { dur: 3.95, color: 'brown', filter: 'lowpass', f: 520, gain: 0.05, a: 0.4, hold: 3.2, pan: 0, minor: true });
547
+ for (let c = 0; c < 9; c++) {
548
+ const tin = 2.15 + c * 0.3 + (2290 - 60 - 1150) / 650;
549
+ k.noise(tin, { dur: 0.08, color: 'brown', filter: 'lowpass', f: 420, gain: 0.32, a: 0.01, pan: pan(2230, tin), minor: true });
550
+ k.tone(tin, { f: [[0, 120], [0.08, 70]], dur: 0.1, gain: 0.1, pan: pan(2230, tin), minor: true });
551
+ }
552
+ // counter 0 -> 1,000,000 over 2.8..5.1: tile flicks speeding up with the count, a firm tile at the end
553
+ for (let t = 2.86; t < 5.06;) { const s = H.clamp((t - 2.8) / 2.3); flick(t, 0.75 + 0.3 * s, pan(1600, t)); t += 0.125 - 0.04 * s; }
554
+ card(5.1, 1.1, pan(1700, 5.1), 1.3, false);
555
+
556
+ // ---- pan 2 (short) and BEAT 3: guess -> check -> adjust
557
+ layer(5.6, 0.6, 0.6);
558
+ card(land(6.3, 0.4), 1.0, pan(1880, 6.45), 0.9, false); // new cat photo drops in
559
+ strip(6.7, 7.0, 1, pan(1975, 6.8), pan(2120, 7));
560
+ strip(7.05, 7.35, 1, pan(2445, 7.1), pan(2570, 7.35));
561
+ card(land(7.2, 0.3), 0.6, pan(2505, 7.3), 1.2); // 'Guess' tag
562
+ card(land(7.4, 0.35), 0.9, pan(2690, 7.5)); // 'dog?' bubble
563
+ // the wrong guess: red paper X slapped on, ring pops behind, the bubble rattles
564
+ const tX = land(7.9, 0.25);
565
+ k.play('slap', tX, { gain: 0.6, pan: pan(2790, tX) });
566
+ card(tX, 1.3, pan(2790, tX), 0.8, false);
567
+ k.tone(tX + 0.01, { f: 'D3', type: 'triangle', dur: 0.16, gain: 0.2, filter: 'lowpass', f2: 700 }); // low wooden "nope"
568
+ k.tone(tX + 0.17, { f: 'A2', type: 'triangle', dur: 0.3, gain: 0.22, filter: 'lowpass', f2: 600 });
569
+ k.play('paper', 7.93, { dur: 0.33, gain: 0.35, pan: pan(2690, 8), minor: true }); // bubble card shaking
570
+ card(land(8.05, 0.3), 0.6, pan(2640, 8.16), 1.2); // 'Check' tag
571
+ strip(8.3, 8.8, 1.1, pan(2660, 8.3), pan(2420, 8.8)); // red error strip back to the dials
572
+ card(land(8.7, 0.3), 0.6, pan(2560, 8.8), 1.2); // 'Adjust' tag
573
+ // dial pointers pivot on their brads (seg 8.9..9.5)
574
+ for (let i = 1; i <= 6; i++) {
575
+ let t = 8.9; while (seg(t, 8.9, 9.5) < i / 6 - 1e-6 && t < 9.5) t += 0.005;
576
+ k.noise(t, { dur: 0.03, filter: 'bandpass', f: 1300 + i * 40, q: 2, gain: 0.22, a: 0.001, pan: pan(2420, t), minor: true });
577
+ k.tone(t, { f: 300, dur: 0.035, gain: 0.05, pan: pan(2420, t), minor: true });
578
+ }
579
+ // bubble flips over (9.55..9.85): two soft flaps of stiff card, then the green tick lands
580
+ k.noise(9.56, { dur: 0.13, color: 'pink', filter: 'bandpass', f: [[0, 500], [0.13, 1100]], q: 0.7, gain: 0.25, a: 0.05, pan: pan(2690, 9.6) });
581
+ k.noise(9.7, { dur: 0.14, color: 'pink', filter: 'bandpass', f: [[0, 1000], [0.14, 450]], q: 0.7, gain: 0.22, a: 0.04, pan: pan(2690, 9.7) });
582
+ const tV = land(9.9, 0.3);
583
+ card(tV, 1.1, pan(2795, tV), 1, false);
584
+ k.play('success', tV, { f: 'G5', gain: 0.6, pan: pan(2795, tV) });
585
+
586
+ // ---- pan 3 and BEAT 4: bars pop up from behind the ground
587
+ layer(10.1, 0.7, 1);
588
+ card(land(10.6, 0.35), 0.75, pan(3480, 10.73)); // 'Accuracy' header
589
+ card(land(11.1, 0.3), 0.55, pan(3580, 11.2), 1.2); // 'training rounds'
590
+ const accs = [61, 72, 80, 86, 90, 93, 95, 97];
591
+ accs.forEach((a, i) => {
592
+ const st = 10.85 + i * 0.22, p = pan(3280 + i * 74, st);
593
+ k.noise(st, { dur: 0.1, color: 'pink', filter: 'bandpass', f: [[0, 400], [0.1, 900]], q: 0.7, gain: 0.14, a: 0.05, pan: p, minor: true }); // slides up past the ground layer
594
+ card(land(st, 0.32), 0.6 + i * 0.04, p, 0.9 + i * 0.05);
595
+ });
596
+ { // % digits swap: tick on each change, at most ~12/s
597
+ let last = -1, lastT = -1;
598
+ for (let t = 10.86; t < 12.72; t += 1 / 120) {
599
+ const i = Math.min(7, Math.floor((t - 10.85) / 0.22)), st = 10.85 + i * 0.22;
600
+ const v = Math.round(H.lerp(i ? accs[i - 1] : 55, accs[i], H.clamp((t - st) / 0.32)));
601
+ if (v !== last && t - lastT >= 1 / 12) { flick(t, 0.7, pan(3995, t)); last = v; lastT = t; }
602
+ }
603
+ }
604
+ // 97%: the sunburst springs up behind the number like a pop-up page (biggest hit)
605
+ k.play('impact', 12.6, { gain: 1.3, pan: 0.5 });
606
+ k.noise(12.6, { dur: 0.12, color: 'pink', filter: 'bandpass', f: [[0, 600], [0.12, 1600]], q: 0.6, gain: 0.3, a: 0.02, pan: 0.5 });
607
+ k.play('ding', 12.6, { f: 'E5', gain: 0.65, pan: 0.4 });
608
+
609
+ // ---- BEAT 5: takeaway banner drops and resolves
610
+ card(land(13.0, 0.45), 1.0, 0, 0.8, false);
611
+ ['C4', 'G4', 'E5'].forEach((f, i) => k.play('ding', land(13.0, 0.45) + 0.16 + i * 0.09, { f, gain: 0.48 - i * 0.04, pan: (i - 1) * 0.25 }));
612
+ },
613
+ draw(ctx, W, H_, t) {
614
+ const R = H.rng(1207);
615
+ const tex = makeTex(77);
616
+ const pat = ctx.createPattern(tex, 'repeat');
617
+
618
+ // one paper sheet = shape + fibre texture + lit/dark cut edge + 2-part drop shadow
619
+ // entrance animation: 'up' slides up like a pop-up layer, 'drop' falls on with a lifted shadow, 'pop' springs out
620
+ const anim = (o) => {
621
+ if (!o.at) return { p: 1, dx: 0, dy: 0, s: 1, lift: 0 };
622
+ const [st, du, mode, dist] = o.at, u = H.clamp((t - st) / du);
623
+ if (u <= 0) return null;
624
+ if (mode === 'up') { const e = H.ease.out(u); return { p: u, dx: 0, dy: (1 - e) * (dist || 260), s: 1, lift: (1 - e) * 1.2 }; }
625
+ if (mode === 'drop') { const e = H.ease.back(u); return { p: u, dx: (1 - e) * -10, dy: (1 - e) * -(dist || 60), s: 1 + (1 - H.ease.out(u)) * 0.06, lift: (1 - H.ease.out(u)) * 3 }; }
626
+ const e = H.ease.back(u); return { p: u, dx: 0, dy: 0, s: Math.max(0.01, e), lift: (1 - H.ease.out(u)) * 2 };
627
+ };
628
+ const sheet = (paint, o = {}) => {
629
+ const tx = R.range(0, 512), ty = R.range(0, 512), tr = R.range(-30, 30);
630
+ const A = anim(o);
631
+ if (!A) return;
632
+ const pad = 6;
633
+ let [x0, y0, x1, y1] = o.bb || [0, 0, W, H_];
634
+ x0 = Math.floor(Math.max(-20, x0 - pad)); y0 = Math.floor(Math.max(-20, y0 - pad));
635
+ x1 = Math.ceil(Math.min(W + 20, x1 + pad)); y1 = Math.ceil(Math.min(H_ + 20, y1 + pad));
636
+ const w = x1 - x0, h = y1 - y0;
637
+ const L = H.layer(w, h), c = L.getContext('2d');
638
+ c.translate(-x0, -y0); paint(c); c.setTransform(1, 0, 0, 1, 0, 0);
639
+ // fibre texture
640
+ c.save(); c.globalCompositeOperation = 'source-atop'; c.globalAlpha = o.tex ?? 1;
641
+ pat.setTransform(new DOMMatrix().translateSelf(tx, ty).rotateSelf(tr));
642
+ c.fillStyle = pat; c.fillRect(0, 0, w, h); c.restore();
643
+ // edge: light catches top-left cut edge, bottom-right edge falls dark
644
+ const edge = (col, dx, dy) => {
645
+ const T = H.layer(w, h), tc = T.getContext('2d');
646
+ tc.drawImage(L, 0, 0); tc.globalCompositeOperation = 'source-in'; tc.fillStyle = col; tc.fillRect(0, 0, w, h);
647
+ tc.globalCompositeOperation = 'destination-out'; tc.drawImage(L, dx, dy);
648
+ c.save(); c.globalCompositeOperation = 'source-atop'; c.drawImage(T, 0, 0); c.restore();
649
+ };
650
+ if (o.rim !== false) {
651
+ edge(`rgba(255,247,232,${o.hi ?? 0.38})`, 1.3, 1.6);
652
+ edge(`rgba(0,0,0,${o.lo ?? 0.28})`, -1.1, -1.3);
653
+ }
654
+ ctx.save();
655
+ if (A.p < 1) {
656
+ const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
657
+ ctx.translate(cx + A.dx, cy + A.dy); ctx.scale(A.s, A.s); ctx.translate(-cx, -cy);
658
+ }
659
+ const d = (o.depth ?? 1) + (o.depth ? A.lift : 0);
660
+ if (d > 0) {
661
+ ctx.shadowColor = `rgba(14,10,30,${o.sa ?? 0.5})`; ctx.shadowBlur = 6 + d * 7;
662
+ ctx.shadowOffsetX = 1.5 + d * 2.4; ctx.shadowOffsetY = 2 + d * 3.6;
663
+ ctx.drawImage(L, x0, y0);
664
+ ctx.shadowColor = 'rgba(10,6,20,0.38)'; ctx.shadowBlur = 2.5; ctx.shadowOffsetX = 0.8; ctx.shadowOffsetY = 1.4;
665
+ }
666
+ ctx.drawImage(L, x0, y0);
667
+ ctx.restore();
668
+ };
669
+
670
+ // slow camera push once the diorama is assembled
671
+ const push = 1 + 0.035 * H.ease.inOut(H.clamp((t - 2.2) / 2.8));
672
+ ctx.translate(W * 0.52, H_ * 0.5); ctx.scale(push, push); ctx.translate(-W * 0.52, -H_ * 0.5);
673
+ const sway = Math.sin(t * 1.7) * 0.03;
674
+ const drift = t * 6;
675
+
676
+ // ---------- 1 backboard (sky) ----------
677
+ sheet((c) => { c.fillStyle = '#2b2d5a'; c.fillRect(-20, -20, W + 40, H_ + 40); }, { depth: 0, rim: false });
678
+
679
+ // stars: punched little shapes
680
+ {
681
+ const r = H.rng(31);
682
+ sheet((c) => {
683
+ c.fillStyle = '#f6e7bf';
684
+ for (let i = 0; i < 46; i++) {
685
+ const x = r.range(20, W - 20), y = r.range(20, 420);
686
+ if (Math.hypot(x - 840, y - 300) < 250) continue;
687
+ const s = r.range(3, 7.5), pts = [], k = r.chance(0.6) ? 5 : 4, a0 = r() * TAU;
688
+ for (let j = 0; j < k * 2; j++) { const rr = j % 2 ? s * 0.42 : s; const a = a0 + (j / (k * 2)) * TAU; pts.push([x + Math.cos(a) * rr, y + Math.sin(a) * rr]); }
689
+ c.beginPath(); poly(c, pts); c.fill();
690
+ }
691
+ }, { depth: 0.25, bb: [0, 0, W, 440], hi: 0.3, at: [0.1, 0.4, 'drop', 20] });
692
+ }
693
+
694
+ // ---------- 2 sun rings ----------
695
+ const sun = [[232, '#3c3f78'], [192, '#c75a5c'], [154, '#e9845b'], [116, '#f3ad62'], [78, '#f8d891']];
696
+ sun.forEach(([rad, col], i) => {
697
+ const r = H.rng(50 + i);
698
+ const pts = circPts(r, 840, 300, rad, 14, 0.7);
699
+ sheet((c) => { c.fillStyle = col; c.beginPath(); poly(c, pts); c.fill(); },
700
+ { depth: 0.55, bb: [840 - rad, 300 - rad, 840 + rad, 300 + rad], at: [0.2 + i * 0.13, 0.5, 'pop'] });
701
+ });
702
+
703
+ // ---------- paper clouds in sky ----------
704
+ const cloud = (seed, cx, cy, s, col, depth, st) => {
705
+ const r = H.rng(seed), parts = [];
706
+ const bumps = [[-1.0, 0, 0.42], [-0.45, -0.25, 0.6], [0.2, -0.35, 0.72], [0.82, -0.1, 0.5], [1.2, 0.05, 0.34]];
707
+ bumps.forEach(([bx, by, br]) => parts.push(circPts(r, cx + bx * s, cy + by * s, br * s, 12, 0.5)));
708
+ sheet((c) => {
709
+ c.fillStyle = col; c.beginPath();
710
+ parts.forEach((p) => poly(c, p));
711
+ poly(c, cutLines(r, [[cx - 1.15 * s, cy], [cx + 1.38 * s, cy], [cx + 1.38 * s, cy + 0.3 * s], [cx - 1.15 * s, cy + 0.3 * s]], 26, 0.5));
712
+ c.fill();
713
+ }, { depth, bb: [cx - 1.6 * s, cy - 1.2 * s, cx + 1.7 * s, cy + 0.5 * s], at: [st, 0.5, 'drop', 50] });
714
+ };
715
+ cloud(61, 1130 - drift * 0.6, 120, 62, '#e9e1d4', 0.8, 1.0);
716
+ cloud(62, 640 + drift, 412, 46, '#dcd3c8', 0.8, 1.15);
717
+
718
+ // ---------- 3 far mountains + snowcaps ----------
719
+ const peaks = [[-30, 520], [70, 420], [150, 470], [260, 360], [380, 455], [470, 410], [560, 480], [690, 430], [790, 490], [930, 380], [1040, 460], [1150, 395], [1310, 470]];
720
+ {
721
+ const r = H.rng(71);
722
+ const top = cutLines(r, peaks, 20, 0.8);
723
+ sheet((c) => { c.fillStyle = '#4c5590'; c.beginPath(); poly(c, [...top, [1310, 740], [-30, 740]]); c.fill(); },
724
+ { depth: 1.0, bb: [-30, 350, W + 30, H_], at: [0.35, 0.6, 'up', 380] });
725
+ // snowcaps (glued-on pieces with zigzag bottom)
726
+ const caps = [[260, 360], [930, 380], [1150, 395]];
727
+ sheet((c) => {
728
+ c.fillStyle = '#f1ece2';
729
+ caps.forEach(([px, py], ci) => {
730
+ const i = peaks.findIndex((p) => p[0] === px);
731
+ const [lx, ly] = peaks[i - 1], [rx, ry] = peaks[i + 1];
732
+ const k = 0.3;
733
+ const L1 = [px + (lx - px) * k, py + (ly - py) * k], R1 = [px + (rx - px) * k, py + (ry - py) * k];
734
+ const pts = [[px, py - 1], R1];
735
+ const n = 4;
736
+ for (let j = 1; j < n; j++) {
737
+ const u = j / n, x = R1[0] + (L1[0] - R1[0]) * u, y = R1[1] + (L1[1] - R1[1]) * u + (j % 2 ? 9 : -2) + r.range(-2, 2);
738
+ pts.push([x, y]);
739
+ }
740
+ pts.push(L1);
741
+ c.beginPath(); poly(c, pts); c.fill();
742
+ });
743
+ }, { depth: 0.35, bb: [200, 340, 1250, 440], hi: 0.7, at: [0.9, 0.4, 'drop', 40] });
744
+ }
745
+
746
+ // ---------- 4 mid hills ----------
747
+ {
748
+ const r = H.rng(81);
749
+ const f = (x) => 492 + 34 * Math.sin(x * 0.0072 + 1.3) + 18 * Math.sin(x * 0.019 + 0.4) + 10 * Math.sin(x * 0.041);
750
+ const top = ridge(r, -30, W + 30, f, 12, 26, 0.6);
751
+ sheet((c) => { c.fillStyle = '#373f73'; c.beginPath(); poly(c, [...top, [W + 30, 740], [-30, 740]]); c.fill(); },
752
+ { depth: 1.1, bb: [-30, 430, W + 30, H_], at: [0.5, 0.6, 'up', 320] });
753
+ }
754
+
755
+ // ---------- 5 server city: lit sheet behind, skyline with cut-out windows ----------
756
+ {
757
+ const r = H.rng(91), blds = [];
758
+ let x = -10;
759
+ while (x < W + 10) {
760
+ const w = r.range(42, 86), h = r.range(55, 150) * (Math.abs(x - 840) < 160 ? 0.65 : 1);
761
+ blds.push({ x, w, h, ant: r.chance(0.3), seed: r.int(1, 1e6) });
762
+ x += w + r.range(-6, 10);
763
+ }
764
+ const base = 575;
765
+ sheet((c) => {
766
+ c.fillStyle = '#f6c862';
767
+ blds.forEach((b) => c.fillRect(b.x + 4, base - b.h + 6, b.w - 8, b.h));
768
+ }, { depth: 0.2, bb: [-20, 380, W + 20, 600], rim: false, at: [0.65, 0.6, 'up', 260] });
769
+ sheet((c) => {
770
+ c.fillStyle = '#232a55';
771
+ c.beginPath();
772
+ blds.forEach((b) => {
773
+ const rr = H.rng(b.seed);
774
+ const pts = cutLines(rr, [[b.x, base + 20], [b.x, base - b.h], [b.x + b.w, base - b.h], [b.x + b.w, base + 20]], 24, 0.5);
775
+ poly(c, pts);
776
+ if (b.ant) poly(c, [[b.x + b.w * 0.5 - 2, base - b.h + 1], [b.x + b.w * 0.5 - 1, base - b.h - 26], [b.x + b.w * 0.5 + 1, base - b.h - 26], [b.x + b.w * 0.5 + 2, base - b.h + 1]]);
777
+ });
778
+ c.fill();
779
+ // cut-out windows / rack slots
780
+ c.globalCompositeOperation = 'destination-out';
781
+ blds.forEach((b) => {
782
+ const rr = H.rng(b.seed + 3), slots = rr.chance(0.45);
783
+ const cols = Math.max(2, Math.floor((b.w - 12) / 13));
784
+ const gx = (b.w - cols * 13 + 5) / 2;
785
+ for (let yy = base - b.h + 12; yy < base - 14; yy += slots ? 11 : 16) {
786
+ if (slots) { if (rr.chance(0.7)) c.fillRect(b.x + 8, yy, b.w - 16 - rr.range(0, 14), 3.5); continue; }
787
+ for (let i = 0; i < cols; i++) if (rr.chance(0.55)) c.fillRect(b.x + gx + i * 13 + rr.gauss() * 0.4, yy + rr.gauss() * 0.4, 7, 9);
788
+ }
789
+ });
790
+ }, { depth: 0.9, bb: [-20, 380, W + 20, 600], at: [0.65, 0.6, 'up', 260] });
791
+ }
792
+
793
+ // ---------- 6 rocket ----------
794
+ const launch = H.clamp((t - 1.5) / 1.7);
795
+ const rx = 840 + Math.sin(t * 1.3) * 6 * launch, ry = H.lerp(1000, 345, H.ease.out(launch)) - Math.max(0, t - 3.2) * 10; // body centre
796
+ const rocketT = (c) => { c.translate(rx, ry); c.rotate(sway + 0.06); c.scale(1.28, 1.28); };
797
+ const body = (c) => {
798
+ c.moveTo(0, -95);
799
+ c.bezierCurveTo(28, -78, 37, -35, 37, 5); c.bezierCurveTo(37, 40, 34, 70, 30, 88);
800
+ c.lineTo(-30, 88);
801
+ c.bezierCurveTo(-34, 70, -37, 40, -37, 5); c.bezierCurveTo(-37, -35, -28, -78, 0, -95);
802
+ c.closePath();
803
+ };
804
+ const rbb = [rx - 190, ry - 150, rx + 190, ry + 330];
805
+ // smoke column behind flame
806
+ {
807
+ const r = H.rng(101), parts = [];
808
+ const topY = ry + 190, botY = 590;
809
+ for (let i = 0; i < 16; i++) {
810
+ const u = i / 15, y = H.lerp(topY, botY, u), spread = 14 + u * u * 120;
811
+ parts.push(circPts(r, rx + 12 + r.range(-spread, spread) * 0.6, y + r.range(-6, 6), 18 + u * 34 + r.range(-4, 4), 12, 0.5));
812
+ }
813
+ sheet((c) => { c.fillStyle = '#d9d2c9'; c.beginPath(); parts.forEach((p) => poly(c, p)); c.fill(); },
814
+ { depth: 0.9, bb: [rx - 200, topY - 40, rx + 220, 650] });
815
+ }
816
+ // flames: three stacked cut pieces, tongues flicker with t
817
+ const flame = (w, L, col, seed, depth) => {
818
+ const r = H.rng(seed), fl = (k) => 1 + 0.12 * Math.sin(t * 19 + k * 2.1 + seed);
819
+ const y0 = 84;
820
+ const p = [[w / 2, y0], [w * 0.47, y0 + L * 0.3], [w * 0.3, y0 + L * 0.72 * fl(1)], [w * 0.14, y0 + L * 0.5], [0, y0 + L * fl(2)],
821
+ [-w * 0.15, y0 + L * 0.55], [-w * 0.31, y0 + L * 0.78 * fl(3)], [-w * 0.47, y0 + L * 0.3], [-w / 2, y0]];
822
+ sheet((c) => {
823
+ rocketT(c); c.fillStyle = col; c.beginPath();
824
+ c.moveTo(p[0][0], p[0][1]);
825
+ for (let i = 1; i < p.length; i++) {
826
+ const [ax, ay] = p[i - 1], [bx, by] = p[i];
827
+ const mx = (ax + bx) / 2 + r.gauss() * 1.2, my = (ay + by) / 2 + r.gauss() * 1.2;
828
+ c.quadraticCurveTo(ax + (mx - ax) * 0.4 + (by - ay) * 0.08, ay + (my - ay) * 0.4, mx, my);
829
+ c.lineTo(bx, by);
830
+ }
831
+ c.closePath(); c.fill();
832
+ }, { depth, bb: rbb });
833
+ };
834
+ flame(64, 150, '#e4513a', 111, 0.6);
835
+ flame(46, 112, '#f39a3c', 112, 0.45);
836
+ flame(26, 72, '#f9db78', 113, 0.35);
837
+ // back fins
838
+ sheet((c) => {
839
+ rocketT(c); c.fillStyle = '#b5372f'; c.beginPath();
840
+ c.moveTo(-28, 22); c.bezierCurveTo(-58, 40, -68, 76, -64, 112); c.lineTo(-24, 84); c.closePath();
841
+ c.moveTo(28, 22); c.bezierCurveTo(58, 40, 68, 76, 64, 112); c.lineTo(24, 84); c.closePath();
842
+ c.fill();
843
+ }, { depth: 0.5, bb: rbb });
844
+ // body
845
+ sheet((c) => { rocketT(c); c.fillStyle = '#f1e8d6'; c.beginPath(); body(c); c.fill(); }, { depth: 0.7, bb: rbb });
846
+ // nose cone + band + centre fin (glued on top)
847
+ sheet((c) => {
848
+ rocketT(c);
849
+ c.save(); c.beginPath(); body(c); c.clip();
850
+ c.fillStyle = '#d9473b';
851
+ c.beginPath(); c.moveTo(-50, -110); c.lineTo(50, -110); c.lineTo(50, -50); c.quadraticCurveTo(0, -38, -50, -50); c.closePath(); c.fill();
852
+ c.fillStyle = '#2f7f86';
853
+ c.beginPath(); c.moveTo(-50, 44); c.quadraticCurveTo(0, 54, 50, 44); c.lineTo(50, 60); c.quadraticCurveTo(0, 70, -50, 60); c.closePath(); c.fill();
854
+ c.restore();
855
+ c.fillStyle = '#c63f35';
856
+ c.beginPath(); c.moveTo(-6, 58); c.lineTo(6, 58); c.lineTo(8, 116); c.lineTo(-8, 116); c.closePath(); c.fill();
857
+ // window frame
858
+ c.fillStyle = '#9c968c'; c.beginPath(); c.arc(0, -8, 23, 0, TAU); c.fill();
859
+ }, { depth: 0.35, bb: rbb });
860
+ sheet((c) => {
861
+ rocketT(c);
862
+ c.fillStyle = '#26566a'; c.beginPath(); c.arc(0, -8, 15.5, 0, TAU); c.fill();
863
+ }, { depth: 0.3, bb: rbb, hi: 0.4 });
864
+ sheet((c) => {
865
+ rocketT(c);
866
+ c.fillStyle = '#e9f1ea'; c.beginPath(); c.arc(-4, -12, 7.5, Math.PI * 0.95, Math.PI * 1.75); c.arc(-1, -9, 6.5, Math.PI * 1.6, Math.PI * 1.05, true); c.closePath(); c.fill();
867
+ }, { depth: 0.2, bb: rbb, hi: 0.2 });
868
+
869
+ // ---------- 7 launch smoke puffs at ground ----------
870
+ {
871
+ const r = H.rng(121), parts = [];
872
+ for (let i = 0; i < 13; i++) {
873
+ const u = (i / 12) * 2 - 1, x = rx + 10 + u * 230 + r.range(-12, 12), y = 588 - (1 - Math.abs(u)) * 22 + r.range(-6, 6);
874
+ parts.push(circPts(r, x, y, 26 + (1 - Math.abs(u)) * 22 + r.range(-5, 5), 12, 0.5));
875
+ }
876
+ sheet((c) => { c.fillStyle = '#f4efe6'; c.beginPath(); parts.forEach((p) => poly(c, p)); c.fill(); },
877
+ { depth: 1.0, bb: [rx - 300, 500, rx + 320, 650], at: [1.65, 0.5, 'pop'] });
878
+ }
879
+
880
+ // ---------- 8 torn hills: white fibrous core ----------
881
+ {
882
+ const nz = H.makeNoise(131), r = H.rng(132);
883
+ const f = (x) => 606 + 22 * Math.sin(x * 0.0058 + 2.2) + 11 * Math.sin(x * 0.017 + 1);
884
+ const torn = [], core = [];
885
+ for (let x = -30; x <= W + 30; x += 2.5) {
886
+ const n = nz.fbm2(x * 0.03, 0.5, 4) * 5 + r.gauss() * 0.6;
887
+ const y = f(x) + n;
888
+ torn.push([x, y + 3.5 + Math.abs(nz.n2(x * 0.045, 3.1)) * 8 + Math.abs(nz.n2(x * 0.3, 7.7)) * 2]);
889
+ core.push([x, y]);
890
+ }
891
+ sheet((c) => {
892
+ c.fillStyle = '#f5f0e5'; c.beginPath(); poly(c, [...core, [W + 30, 740], [-30, 740]]); c.fill();
893
+ // protruding fibres along the torn core
894
+ c.strokeStyle = 'rgba(248,244,236,0.95)'; c.lineCap = 'round';
895
+ for (let i = 0; i < core.length; i += 1) {
896
+ if (!r.chance(0.55)) continue;
897
+ const [x, y] = core[i], a = -Math.PI / 2 + r.range(-0.9, 0.9), l = r.range(1.5, 5.5);
898
+ c.lineWidth = r.range(0.4, 0.9);
899
+ c.beginPath(); c.moveTo(x, y + 1); c.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); c.stroke();
900
+ }
901
+ c.fillStyle = '#2f6b66'; c.beginPath(); poly(c, [...torn, [W + 30, 740], [-30, 740]]); c.fill();
902
+ }, { depth: 1.3, bb: [-30, 560, W + 30, H_], at: [0.8, 0.6, 'up', 200] });
903
+ }
904
+
905
+ // ---------- 8b paper pines on the hills ----------
906
+ {
907
+ const r = H.rng(161);
908
+ const pine = (x, base, h, col, seed, depth, st) => {
909
+ const rr = H.rng(seed), w = h * 0.52, tiers = 3, pieces = [];
910
+ for (let k = 0; k < tiers; k++) {
911
+ const ty = base - h * 0.18 - k * h * 0.26, tw = w * (1 - k * 0.24), th = h * 0.42;
912
+ const pts = [[x, ty - th]];
913
+ const n = 4;
914
+ for (let j = 0; j <= n; j++) { const u = j / n; pts.push([x + tw / 2 - u * tw, ty + (j % 2 ? -5 : 0) + rr.gauss() * 0.6]); }
915
+ pieces.push(cutLines(rr, [...pts, [x, ty - th]], 18, 0.5));
916
+ }
917
+ sheet((c) => {
918
+ c.fillStyle = '#4a3326'; c.fillRect(x - 4, base - h * 0.25, 8, h * 0.3);
919
+ c.fillStyle = col; c.beginPath(); pieces.forEach((p) => poly(c, p)); c.fill();
920
+ }, { depth, bb: [x - w, base - h * 1.1, x + w, base + 10], at: [st, 0.45, 'pop'] });
921
+ };
922
+ pine(70, 640, 150, '#24594f', 162, 1.1, 1.15);
923
+ pine(150, 628, 108, '#2e6c5b', 163, 0.9, 1.25);
924
+ pine(1215, 640, 136, '#24594f', 164, 1.1, 1.2);
925
+ pine(1140, 636, 92, '#2e6c5b', 165, 0.9, 1.3);
926
+ }
927
+
928
+ // ---------- 9 foreground with grass snips ----------
929
+ {
930
+ const r = H.rng(141);
931
+ const f = (x) => 664 + 20 * Math.sin(x * 0.0045 + 0.4) + 8 * Math.sin(x * 0.013);
932
+ const pts = [];
933
+ let x = -30;
934
+ while (x < W + 30) {
935
+ if (r.chance(0.22)) {
936
+ const n = r.int(2, 4);
937
+ for (let k = 0; k < n; k++) {
938
+ const bx = x + k * 7, by = f(bx);
939
+ pts.push([bx, by], [bx + 2 + r.range(-3, 4), by - r.range(16, 36)], [bx + 5, by]);
940
+ }
941
+ x += n * 7 + 4;
942
+ } else { pts.push([x, f(x) + r.gauss() * 0.6]); x += r.range(14, 30); }
943
+ }
944
+ sheet((c) => { c.fillStyle = '#163a3a'; c.beginPath(); poly(c, [...pts, [W + 30, 740], [-30, 740]]); c.fill(); },
945
+ { depth: 1.6, bb: [-30, 610, W + 30, H_], at: [0.95, 0.6, 'up', 160] });
946
+ }
947
+
948
+ // ---------- 9b nearest layer: scalloped paper bushes in the corners ----------
949
+ {
950
+ const bush = (seed, cx, cy, s, col, depth, st) => {
951
+ const r = H.rng(seed), parts = [];
952
+ for (let i = 0; i < 7; i++) {
953
+ const a = Math.PI + (i / 6) * Math.PI;
954
+ parts.push(circPts(r, cx + Math.cos(a) * s * 1.1, cy + Math.sin(a) * s * 0.75, s * r.range(0.42, 0.58), 12, 0.5));
955
+ }
956
+ parts.push(circPts(r, cx, cy, s * 0.9, 14, 0.5));
957
+ sheet((c) => { c.fillStyle = col; c.beginPath(); parts.forEach((p) => poly(c, p)); c.fill(); },
958
+ { depth, bb: [cx - s * 1.8, cy - s * 1.4, cx + s * 1.8, cy + s * 1.2], at: [st, 0.5, 'up', 180] });
959
+ };
960
+ bush(171, 1210, 740, 120, '#11302f', 2.2, 1.1);
961
+ bush(172, 1040, 760, 70, '#1d4a45', 1.8, 1.2);
962
+ bush(173, 30, 735, 105, '#11302f', 2.2, 1.05);
963
+ }
964
+
965
+ // ---------- 10 title: cut letters on a cut-around backing sheet ----------
966
+ {
967
+ const r = H.rng(151);
968
+ const lines = [{ s: 'READY FOR', f: '400 50px "Archivo Black"', y: 132, x: 86 }, { s: 'LAUNCH', f: '400 112px "Archivo Black"', y: 246, x: 78 }];
969
+ const glyphs = [];
970
+ ctx.save();
971
+ lines.forEach((ln) => {
972
+ ctx.font = ln.f; let x = ln.x;
973
+ const sp = ln.s === 'LAUNCH' ? 4 : 6;
974
+ for (const ch of ln.s) {
975
+ const w = ctx.measureText(ch).width;
976
+ if (ch !== ' ') glyphs.push({ ch, f: ln.f, x, y: ln.y + r.range(-3, 3), rot: r.range(-0.06, 0.06), w });
977
+ x += w + sp;
978
+ }
979
+ });
980
+ ctx.restore();
981
+ const cols = ['#e8604a', '#f2b545', '#58b0a6', '#f2b545', '#e8604a', '#58b0a6', '#e8604a', '#f2b545'];
982
+ const tilt = (c) => { c.translate(330, 190); c.rotate(-0.045); c.translate(-330, -190); };
983
+ const drawG = (c, g, fn) => { c.save(); c.translate(g.x + g.w / 2, g.y); c.rotate(g.rot); c.font = g.f; fn(c, -g.w / 2); c.restore(); };
984
+ sheet((c) => {
985
+ tilt(c); c.fillStyle = '#f3ecdc'; c.strokeStyle = '#f3ecdc'; c.lineJoin = 'round'; c.lineWidth = 26;
986
+ glyphs.forEach((g) => drawG(c, g, (cc, x) => { cc.strokeText(g.ch, x, 0); cc.fillText(g.ch, x, 0); }));
987
+ // fill gaps so backing reads as one cut piece
988
+ c.lineWidth = 34; c.beginPath(); c.moveTo(100, 115); c.lineTo(395, 115); c.moveTo(95, 210); c.lineTo(560, 210); c.stroke();
989
+ }, { depth: 1.5, bb: [40, 50, 640, 290], at: [1.7, 0.45, 'drop', 70] });
990
+ glyphs.forEach((g, i) => {
991
+ const big = g.f.includes('112');
992
+ sheet((c) => { tilt(c); drawG(c, g, (cc, x) => { cc.fillStyle = cols[i % cols.length]; cc.fillText(g.ch, x, 0); }); },
993
+ { depth: 0.6, bb: [g.x - 24, g.y - (big ? 110 : 55), g.x + g.w + 24, g.y + 30], at: [1.95 + i * 0.06, 0.38, 'drop', big ? 60 : 40] });
994
+ });
995
+ }
996
+
997
+ // ---------- lighting / photo finish ----------
998
+ ctx.save();
999
+ ctx.globalCompositeOperation = 'soft-light';
1000
+ const lg = ctx.createRadialGradient(260, 60, 40, 420, 200, 1100);
1001
+ lg.addColorStop(0, 'rgba(255,236,200,0.9)'); lg.addColorStop(0.5, 'rgba(255,230,200,0.25)'); lg.addColorStop(1, 'rgba(0,0,20,0.6)');
1002
+ ctx.fillStyle = lg; ctx.fillRect(0, 0, W, H_);
1003
+ ctx.restore();
1004
+ H.vignette(ctx, W, H_, 0.42, '10,6,24');
1005
+ H.grain(ctx, W, H_, 0.035, 9 + Math.floor(t * 12));
1006
+ },
1007
+ });
1008
+ }