@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,713 @@
1
+ {
2
+ // ---------- soft "clay" 3D faked in 2D: extrusion + gradient face + inner shadows + rim + specular ----------
3
+ // rounded polygon with per-vertex radius (pts: [x,y,r])
4
+ function roundedPoly(pts) {
5
+ const p = new Path2D();
6
+ const n = pts.length;
7
+ const mid = (a, b) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
8
+ const start = mid(pts[n - 1], pts[0]);
9
+ p.moveTo(start[0], start[1]);
10
+ for (let i = 0; i < n; i++) {
11
+ const c = pts[i], nx = pts[(i + 1) % n];
12
+ const m = mid(c, nx);
13
+ p.arcTo(c[0], c[1], m[0], m[1], c[2] || 0);
14
+ }
15
+ p.closePath();
16
+ return p;
17
+ }
18
+ // inner shadow: the casting fill is pushed far off-canvas in device space so only its shadow lands (no hairline)
19
+ function innerShadow(ctx, path, rule, bb, color, blur, dx, dy) {
20
+ const OFF = 5000;
21
+ ctx.save();
22
+ ctx.clip(path, rule);
23
+ const p2 = new Path2D();
24
+ const m = 400;
25
+ p2.rect(bb[0] - m, bb[1] - m, bb[2] - bb[0] + 2 * m, bb[3] - bb[1] + 2 * m);
26
+ p2.addPath(path);
27
+ const M = ctx.getTransform();
28
+ ctx.setTransform(new DOMMatrix().translate(OFF, 0).multiply(M));
29
+ ctx.shadowColor = color; ctx.shadowBlur = blur; ctx.shadowOffsetX = dx - OFF; ctx.shadowOffsetY = dy;
30
+ ctx.fillStyle = '#000';
31
+ ctx.fill(p2, 'evenodd');
32
+ ctx.restore();
33
+ }
34
+ let EXT = null;
35
+ // o: {x,y,rot,s, path, rule, bb, face:[light,mid,dark], side:[light,dark], ext:[ex,ey], spec:[x,y,rx,ry]}
36
+ function puffy(ctx, o) {
37
+ const rule = o.rule || 'nonzero';
38
+ const [ex, ey] = o.ext;
39
+ const n = Math.ceil(Math.hypot(ex, ey) * 1.5);
40
+ const W = ctx.canvas.width, Hh = ctx.canvas.height;
41
+ const base = ctx.getTransform();
42
+ let lc = ctx;
43
+ if (!o.fast) {
44
+ if (!EXT || EXT.width !== W) EXT = H.layer(W, Hh);
45
+ lc = EXT.getContext('2d');
46
+ lc.setTransform(1, 0, 0, 1, 0, 0); lc.clearRect(0, 0, W, Hh);
47
+ } else ctx.save();
48
+ const bounce = H.mix(o.side[0], '#ffffff', 0.35);
49
+ for (let i = n; i >= 1; i--) {
50
+ const k = i / n;
51
+ lc.setTransform(base); lc.translate(o.x + ex * k, o.y + ey * k); lc.rotate(o.rot || 0); lc.scale(o.s, o.s);
52
+ const c = k > 0.86 ? H.mix(o.side[1], bounce, (k - 0.86) / 0.14) : k < 0.25 ? H.mix(o.face[2], o.side[0], k / 0.25) : H.mix(o.side[0], o.side[1], (k - 0.25) / 0.61);
53
+ lc.fillStyle = H.rgba(c);
54
+ lc.fill(o.path, rule);
55
+ }
56
+ if (!o.fast) { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.filter = 'blur(1.4px)'; ctx.drawImage(EXT, 0, 0); ctx.restore(); }
57
+ else { ctx.restore(); ctx.setTransform(base); }
58
+ ctx.save(); ctx.translate(o.x, o.y); ctx.rotate(o.rot || 0); ctx.scale(o.s, o.s);
59
+ const bb = o.bb;
60
+ const g = ctx.createLinearGradient(bb[0], bb[1], bb[2], bb[3]);
61
+ g.addColorStop(0, H.rgba(o.face[0])); g.addColorStop(0.5, H.rgba(o.face[1])); g.addColorStop(1, H.rgba(o.face[2]));
62
+ ctx.fillStyle = g; ctx.fill(o.path, rule);
63
+ const S = o.s * Math.hypot(base.a, base.b);
64
+ innerShadow(ctx, o.path, rule, bb, H.rgba(o.side[1], 0.5), 40 * S, -16 * S, -20 * S); // core shadow toward bottom-right
65
+ innerShadow(ctx, o.path, rule, bb, H.rgba(o.side[0], 0.35), 14 * S, 0, 0); // rounded falloff at every edge
66
+ innerShadow(ctx, o.path, rule, bb, 'rgba(255,255,255,0.72)', 26 * S, 12 * S, 14 * S); // soft top-left highlight
67
+ if (o.spec) {
68
+ ctx.save(); ctx.clip(o.path, rule);
69
+ const [sx, sy, rx, ry] = o.spec;
70
+ const sg = ctx.createRadialGradient(sx, sy, 0, sx, sy, rx);
71
+ sg.addColorStop(0, 'rgba(255,255,255,0.95)'); sg.addColorStop(0.35, 'rgba(255,255,255,0.45)'); sg.addColorStop(1, 'rgba(255,255,255,0)');
72
+ ctx.translate(sx, sy); ctx.scale(1, ry / rx); ctx.translate(-sx, -sy);
73
+ ctx.fillStyle = sg; ctx.beginPath(); ctx.arc(sx, sy, rx, 0, 7); ctx.fill();
74
+ ctx.restore();
75
+ }
76
+ ctx.restore();
77
+ }
78
+ function sphere(ctx, x, y, r, light, mid, dark) {
79
+ const g = ctx.createRadialGradient(x - r * 0.35, y - r * 0.4, r * 0.05, x, y, r * 1.05);
80
+ g.addColorStop(0, H.rgba(light)); g.addColorStop(0.55, H.rgba(mid)); g.addColorStop(1, H.rgba(dark));
81
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
82
+ const p = new Path2D(); p.arc(x, y, r, 0, 7);
83
+ innerShadow(ctx, p, 'nonzero', [x - r, y - r, x + r, y + r], H.rgba(H.mix(light, '#ffffff', 0.5), 0.8), r * 0.12, -r * 0.05, -r * 0.06);
84
+ const s = ctx.createRadialGradient(x - r * 0.38, y - r * 0.45, 0, x - r * 0.38, y - r * 0.45, r * 0.42);
85
+ s.addColorStop(0, 'rgba(255,255,255,0.9)'); s.addColorStop(1, 'rgba(255,255,255,0)');
86
+ ctx.fillStyle = s; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
87
+ }
88
+ function contactShadow(ctx, x, y, w, h, alpha, blur) {
89
+ ctx.save(); ctx.filter = `blur(${blur}px)`;
90
+ ctx.fillStyle = `rgba(110,80,170,${alpha})`;
91
+ ctx.beginPath(); ctx.ellipse(x, y, w, h, 0, 0, 7); ctx.fill();
92
+ ctx.filter = `blur(${blur * 0.4}px)`; ctx.fillStyle = `rgba(90,60,150,${alpha * 0.8})`;
93
+ ctx.beginPath(); ctx.ellipse(x, y, w * 0.55, h * 0.45, 0, 0, 7); ctx.fill();
94
+ ctx.restore();
95
+ }
96
+ // ---------- shapes (local coords) ----------
97
+ const bubblePath = () => roundedPoly([[-170, -115, 78], [170, -115, 78], [170, 95, 70], [-40, 95, 22], [-120, 160, 12], [-100, 95, 16], [-170, 95, 60]]);
98
+ const BUBBLE_BB = [-170, -115, 170, 160];
99
+ function gearPath(R, teeth, holeR) {
100
+ const pts = [];
101
+ const r0 = R * 0.78;
102
+ for (let i = 0; i < teeth; i++) {
103
+ const a = (i / teeth) * Math.PI * 2, w = (Math.PI * 2) / teeth;
104
+ pts.push([Math.cos(a - w * 0.5) * r0, Math.sin(a - w * 0.5) * r0, R * 0.12]);
105
+ pts.push([Math.cos(a - w * 0.2) * R, Math.sin(a - w * 0.2) * R, R * 0.1]);
106
+ pts.push([Math.cos(a + w * 0.2) * R, Math.sin(a + w * 0.2) * R, R * 0.1]);
107
+ }
108
+ const p = roundedPoly(pts);
109
+ p.moveTo(holeR, 0); p.arc(0, 0, holeR, 0, Math.PI * 2, true); p.closePath();
110
+ return p;
111
+ }
112
+ const cursorPath = () => roundedPoly([[0, 0, 10], [0, 150, 12], [38, 114, 8], [64, 168, 10], [92, 154, 10], [66, 102, 8], [114, 102, 12]]);
113
+ const CURSOR_BB = [0, 0, 114, 168];
114
+ function bulbGlassPath(r) {
115
+ const p = new Path2D();
116
+ // glass: big circle flowing into a neck
117
+ p.moveTo(-r * 0.5, r * 0.95);
118
+ p.bezierCurveTo(-r * 0.55, r * 0.6, -r, r * 0.45, -r, -0.05 * r);
119
+ p.arc(0, -0.05 * r, r, Math.PI, 0);
120
+ p.bezierCurveTo(r, r * 0.45, r * 0.55, r * 0.6, r * 0.5, r * 0.95);
121
+ p.closePath();
122
+ return p;
123
+ }
124
+ function ringPath(R, r) {
125
+ const p = new Path2D();
126
+ p.ellipse(0, 0, R, R * 0.42, 0, 0, Math.PI * 2);
127
+ p.moveTo(r, 0); p.ellipse(0, 0, r, r * 0.36, 0, 0, Math.PI * 2, true);
128
+ return p;
129
+ }
130
+
131
+ // ================= EXPLAINER: a wide pastel set the camera dollies across (with parallax), clay props springing in =================
132
+ const HX = (h) => H.hex(h);
133
+ const PAL = {
134
+ lav: { face: [HX('#e6e0ff'), HX('#b9abff'), HX('#9a88f5')], side: [HX('#8f7cf0'), HX('#5d48c2')] },
135
+ peach: { face: [HX('#fff0e6'), HX('#ffcfb3'), HX('#ffad8a')], side: [HX('#f79d7a'), HX('#cf6b4f')] },
136
+ mint: { face: [HX('#e6fff6'), HX('#b3eedb'), HX('#86d7bf')], side: [HX('#7fcbb3'), HX('#4f9f8a')] },
137
+ white: { face: [HX('#ffffff'), HX('#f7f3ff'), HX('#e2d9fb')], side: [HX('#d4c8f5'), HX('#9a87de')] },
138
+ pink: { face: [HX('#ffe3ea'), HX('#ffb1c2'), HX('#ff8fa8')], side: [HX('#f57f9b'), HX('#cc4f72')] },
139
+ blue: { face: [HX('#e3f1ff'), HX('#b4d6ff'), HX('#8fbaf5')], side: [HX('#86b0ee'), HX('#5a7fc7')] },
140
+ coral: { face: [HX('#ffd9d2'), HX('#ff8f7e'), HX('#f2675a')], side: [HX('#e85d4f'), HX('#b23a33')] },
141
+ butter: { face: [HX('#fffbe0'), HX('#ffe58f'), HX('#f7c45e')], side: [HX('#f2b955'), HX('#d98f3a')] },
142
+ };
143
+ const pill = (w, h) => roundedPoly([[-w / 2, -h / 2, h / 2], [w / 2, -h / 2, h / 2], [w / 2, h / 2, h / 2], [-w / 2, h / 2, h / 2]]);
144
+ const rrect = (w, h, r) => roundedPoly([[-w / 2, -h / 2, r], [w / 2, -h / 2, r], [w / 2, h / 2, r], [-w / 2, h / 2, r]]);
145
+ function clay(ctx, path, bb, x, y, pal, o = {}) {
146
+ puffy(ctx, { x, y, rot: o.rot || 0, s: o.s || 1, path, bb, face: pal.face, side: pal.side, ext: o.ext || [6, 11], spec: o.spec, fast: o.fast, z: o.z });
147
+ }
148
+ function clayPet(ctx, kind, x, y, r) {
149
+ const fur = kind === 'cat' ? [HX('#efeaff'), HX('#c4b8ff'), HX('#9a88f5')] : [HX('#fff1e2'), HX('#ffcf9e'), HX('#e9a46a')];
150
+ ctx.save();
151
+ if (kind === 'cat') for (const sx of [-1, 1]) { const g = ctx.createLinearGradient(x, y - r * 1.2, x, y); g.addColorStop(0, H.rgba(fur[0])); g.addColorStop(1, H.rgba(fur[2])); ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(x + sx * 0.88 * r, y - 0.15 * r); ctx.quadraticCurveTo(x + sx * 0.8 * r, y - 1.25 * r, x + sx * 0.6 * r, y - 1.12 * r); ctx.quadraticCurveTo(x + sx * 0.3 * r, y - 0.95 * r, x + sx * 0.1 * r, y - 0.8 * r); ctx.closePath(); ctx.fill(); }
152
+ else for (const sx of [-1, 1]) { ctx.fillStyle = H.rgba(H.mix(fur[2], '#8a5a3b', 0.3)); ctx.beginPath(); ctx.ellipse(x + sx * 0.9 * r, y + 0.05 * r, 0.3 * r, 0.62 * r, sx * 0.35, 0, 7); ctx.fill(); }
153
+ sphere(ctx, x, y, r, fur[0], fur[1], fur[2]);
154
+ ctx.fillStyle = '#3b2f5c';
155
+ for (const sx of [-1, 1]) { ctx.beginPath(); ctx.ellipse(x + sx * 0.36 * r, y - 0.08 * r, r * 0.1, r * 0.14, 0, 0, 7); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(x + sx * 0.36 * r - r * 0.03, y - 0.13 * r, r * 0.035, 0, 7); ctx.fill(); ctx.fillStyle = '#3b2f5c'; }
156
+ ctx.fillStyle = 'rgba(255,120,150,0.45)'; for (const sx of [-1, 1]) { ctx.beginPath(); ctx.ellipse(x + sx * 0.6 * r, y + 0.22 * r, r * 0.15, r * 0.09, 0, 0, 7); ctx.fill(); }
157
+ ctx.fillStyle = kind === 'cat' ? '#ff8fa8' : '#3b2f5c'; ctx.beginPath(); ctx.ellipse(x, y + 0.2 * r, r * (kind === 'cat' ? 0.09 : 0.16), r * (kind === 'cat' ? 0.07 : 0.11), 0, 0, 7); ctx.fill();
158
+ ctx.restore();
159
+ }
160
+ function clayLabel(ctx, x, y, text, pal, o = {}) {
161
+ const k = o.k ?? 1; if (k <= 0.01) return;
162
+ ctx.save(); ctx.translate(x, y); ctx.scale(k, k);
163
+ ctx.font = o.font || '700 28px "Fredoka"';
164
+ const fsz = parseInt((ctx.font.match(/(\d+)px/) || [0, 28])[1]);
165
+ const w = ctx.measureText(text).width + (o.padX || fsz * 1.5), h = o.h || Math.round(fsz * 1.8);
166
+ clay(ctx, pill(w, h), [-w / 2, -h / 2, w / 2, h / 2], 0, 0, pal, { ext: [3, 6], fast: true, z: o.z });
167
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
168
+ const col = o.col || '#4a3a8a';
169
+ if (col === '#fff') { ctx.fillStyle = 'rgba(70,45,170,0.32)'; ctx.fillText(text, fsz * 0.06, 2 + fsz * 0.09); } // pressed-in ink shadow, like the bubble text
170
+ else { ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.fillText(text, 0, 3.5); } // dark text: soft lower highlight (debossed)
171
+ ctx.fillStyle = col; ctx.fillText(text, 0, 2);
172
+ ctx.restore();
173
+ }
174
+ function clayTube(ctx, a, c, b, prog, pal, w = 14) {
175
+ if (prog <= 0) return null;
176
+ const q = (u) => [(1 - u) * (1 - u) * a[0] + 2 * u * (1 - u) * c[0] + u * u * b[0], (1 - u) * (1 - u) * a[1] + 2 * u * (1 - u) * c[1] + u * u * b[1]];
177
+ const N = 36, pts = []; for (let i = 0; i <= N; i++) pts.push(q(prog * i / N * 0.93));
178
+ const path = (off) => { ctx.beginPath(); pts.forEach((p, i) => i ? ctx.lineTo(p[0] + off[0], p[1] + off[1]) : ctx.moveTo(p[0] + off[0], p[1] + off[1])); };
179
+ ctx.save(); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
180
+ ctx.strokeStyle = 'rgba(90,60,150,0.18)'; ctx.lineWidth = w; path([3, 7]); ctx.stroke();
181
+ ctx.strokeStyle = H.rgba(pal.side[1]); ctx.lineWidth = w; path([0, 0]); ctx.stroke();
182
+ ctx.strokeStyle = H.rgba(pal.face[1]); ctx.lineWidth = w * 0.72; path([-w * 0.1, -w * 0.12]); ctx.stroke();
183
+ ctx.strokeStyle = 'rgba(255,255,255,0.75)'; ctx.lineWidth = w * 0.2; path([-w * 0.2, -w * 0.25]); ctx.stroke();
184
+ const e = q(prog), e0 = q(Math.max(0, prog - 0.05)), ang = Math.atan2(e[1] - e0[1], e[0] - e0[0]);
185
+ ctx.restore();
186
+ const hp = roundedPoly([[w * 1.3, 0, w * 0.35], [-w * 0.9, -w * 1.15, w * 0.35], [-w * 0.9, w * 1.15, w * 0.35]]);
187
+ clay(ctx, hp, [-w, -w, w, w], e[0], e[1], pal, { rot: ang, ext: [3, 6], fast: true });
188
+ return q;
189
+ }
190
+ let FG_SPHERE = null;
191
+ function fgSphere() { // the hero's blurred foreground sphere, baked once
192
+ if (FG_SPHERE) return FG_SPHERE;
193
+ const a = H.layer(320, 320), ac = a.getContext('2d');
194
+ sphere(ac, 160, 160, 100, HX('#f3eeff'), HX('#d4c8ff'), HX('#a998ec'));
195
+ const b = H.layer(320, 320), bc = b.getContext('2d');
196
+ bc.filter = 'blur(9px)'; bc.drawImage(a, 0, 0);
197
+ return (FG_SPHERE = b);
198
+ }
199
+ function softExplainer(ctx, W, H_, t) {
200
+ const ph = (a, b) => H.clamp((t - a) / (b - a));
201
+ const E = (a, b) => H.ease.inOut(ph(a, b));
202
+ const EO = (a, b) => H.ease.out(ph(a, b));
203
+ const spring = (t0, d = 0.8) => { const p = ph(t0, t0 + d); if (p <= 0) return 0; return p >= 1 ? 1 : 1 - Math.exp(-6.5 * p) * Math.cos(10 * p); };
204
+ const INK = '#4a3a8a';
205
+ // ---------- camera: [t, centreX, centreY, zoom] over a wide set ----------
206
+ const KF = [[0, 640, 360, 1], [1.6, 640, 360, 1], [2.8, 980, 360, 1], [5.8, 1020, 360, 1], [6.8, 1560, 360, 1], [10.2, 1580, 360, 1], [11.1, 2560, 360, 1], [12.8, 2580, 360, 1], [13.8, 1790, 400, 0.44], [20, 1790, 400, 0.44]];
207
+ let k = 0; while (k < KF.length - 2 && t > KF[k + 1][0]) k++;
208
+ const A = KF[k], B = KF[k + 1], u = H.ease.inOut(H.clamp((t - A[0]) / (B[0] - A[0])));
209
+ const camX = H.lerp(A[1], B[1], u), camY = H.lerp(A[2], B[2], u), zoom = H.lerp(A[3], B[3], u);
210
+ const bob = (p, a) => Math.sin(t * 1.6 + p) * a;
211
+ const FLOOR = 610;
212
+
213
+ // ---------- seamless cyclorama (screen space) + parallax far props ----------
214
+ const bg = ctx.createLinearGradient(0, 0, 0, H_);
215
+ bg.addColorStop(0, '#f3ecff'); bg.addColorStop(0.5, '#e8dffb'); bg.addColorStop(0.62, '#dccff5'); bg.addColorStop(0.78, '#e6dcf9'); bg.addColorStop(1, '#efe7fe');
216
+ ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H_);
217
+ const glow = ctx.createRadialGradient(W * 0.5, H_ * 0.35, 40, W * 0.5, H_ * 0.35, 700);
218
+ glow.addColorStop(0, 'rgba(255,250,255,0.8)'); glow.addColorStop(1, 'rgba(255,250,255,0)');
219
+ ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H_);
220
+ const world = (c, par = 1) => { c.translate(W / 2, H_ / 2); c.scale(zoom, zoom); c.translate(-camX * par, -camY); };
221
+ {
222
+ const far = H.layer(W, H_), fc = far.getContext('2d');
223
+ fc.save(); world(fc, 0.6);
224
+ const fr = H.rng(3);
225
+ for (let i = 0; i < 16; i++) { const x = i * 260 + fr() * 120, y = 90 + fr() * 200, r = 12 + fr() * 22; const p = fr.pick([[HX('#fff6c7'), HX('#ffe08a'), HX('#f3b860')], [HX('#d8f5ec'), HX('#a6e3d2'), HX('#6cbfb0')], [HX('#f3eeff'), HX('#d4c8ff'), HX('#a998ec')], [HX('#ffe3ea'), HX('#ffb1c2'), HX('#ff8fa8')]]); sphere(fc, x, y + bob(i, 6), r, ...p); }
226
+ fc.restore();
227
+ ctx.save(); ctx.filter = 'blur(3px)'; ctx.drawImage(far, 0, 0); ctx.restore();
228
+ }
229
+ ctx.save(); world(ctx);
230
+ const Z = zoom;
231
+ // warm key-light glows behind each prop as it arrives (the hero's lit-bulb aura)
232
+ const keyGlow = (x, y, r, a) => {
233
+ a = H.clamp(a); if (a <= 0.01) return;
234
+ const g = ctx.createRadialGradient(x, y, r * 0.06, x, y, r);
235
+ g.addColorStop(0, `rgba(255,214,120,${0.26 * a})`); g.addColorStop(0.45, `rgba(255,222,160,${0.11 * a})`); g.addColorStop(1, 'rgba(255,230,180,0)');
236
+ ctx.fillStyle = g; ctx.fillRect(x - r, y - r, 2 * r, 2 * r);
237
+ };
238
+ keyGlow(1330, 330, 340, spring(1.0, 0.9));
239
+ if (t > 6.8) keyGlow(1830, 260, 320, spring(6.9, 0.7));
240
+ if (t > 10.5) keyGlow(2650, 330, 460, spring(10.5, 0.7));
241
+
242
+ // ================= SET 1/2: data → the model (chip) =================
243
+ const CHIP = [1350, 360 + bob(0.3, 6)];
244
+ // contact shadows first
245
+ contactShadow(ctx, CHIP[0] + 10, FLOOR, 150 * spring(1.0), 22, 0.3, 18);
246
+ // data cards travelling into the chip (beat 2) + test card (beat 3)
247
+ const cards = [];
248
+ if (t > 2.3 && t < 6.5) for (let j = 0; j < 9; j++) {
249
+ const p = (t - 2.3) * 0.36 - j * 0.15; if (p < 0 || p > 1) continue;
250
+ cards.push({ p, kind: (j * 5 + 1) % 3 === 0 ? 'dog' : (j % 2 ? 'cat' : 'dog'), j });
251
+ }
252
+ const cardPos = (p) => { const x = H.lerp(560, CHIP[0] - 40, p), y = 420 - Math.sin(p * Math.PI) * 170 + (p > 0.85 ? (p - 0.85) * 300 * 0 : 0); return [x, y, 1 - H.smooth(0.75, 1, p) * 0.75]; };
253
+ // big puffy tube path showing the flow
254
+ if (t > 2.6 && t < 6.6) clayTube(ctx, [520, 470], [900, 140], [CHIP[0] - 150, 330], E(2.6, 3.5) * (1 - ph(6.2, 6.6)), PAL.white, 18);
255
+ // pile of examples (grows) on the floor left
256
+ {
257
+ const n = Math.floor(1 + 7 * Math.pow(EO(2.6, 5.8), 1.1));
258
+ const pk0 = spring(2.6, 0.6);
259
+ if (pk0 > 0) contactShadow(ctx, 600 + Math.min(n, 3) * 38 + (n > 3 ? 9 : 0), FLOOR + 2, (40 + Math.min(n, 3) * 34) * Math.min(1, pk0), 11, 0.3, 9);
260
+ for (let i = 0; i < n; i++) {
261
+ const k2 = spring(2.6 + i * 0.4, 0.6); if (k2 <= 0) continue;
262
+ const row = Math.floor(i / 3), x = 590 + (i % 3) * 76 + row * 18, y = FLOOR - 26 - row * 50;
263
+ ctx.save(); ctx.translate(x, y); ctx.scale(k2, k2);
264
+ clay(ctx, rrect(70, 46, 14), [-35, -23, 35, 23], 0, 0, i % 2 ? PAL.peach : PAL.lav, { ext: [3, 7], fast: true });
265
+ ctx.restore();
266
+ }
267
+ }
268
+ for (const cd of cards) {
269
+ const [x, y, sc] = cardPos(cd.p);
270
+ ctx.save(); ctx.translate(x, y); ctx.scale(sc, sc); ctx.rotate(Math.sin(cd.j * 1.7 + t * 2) * 0.12);
271
+ clay(ctx, rrect(96, 96, 26), [-48, -48, 48, 48], 0, 0, PAL.white, { ext: [5, 9], fast: true });
272
+ clayPet(ctx, cd.kind, 0, 4, 28);
273
+ if (cd.p < 0.82) clayLabel(ctx, 30, -52, cd.kind, cd.kind === 'cat' ? PAL.lav : PAL.peach, { font: '700 28px "Fredoka"', col: '#fff' });
274
+ ctx.restore();
275
+ }
276
+ let testPos = null;
277
+ if (t > 5.9 && t < 7.2) {
278
+ const p = E(5.9, 7.0); const [x, y, sc] = cardPos(p * 0.98); testPos = [x, y];
279
+ ctx.save(); ctx.translate(x, y); ctx.scale(sc * 1.1, sc * 1.1);
280
+ clay(ctx, rrect(96, 96, 26), [-48, -48, 48, 48], 0, 0, PAL.pink, { ext: [5, 9], fast: true });
281
+ clayPet(ctx, 'cat', 0, 4, 28);
282
+ clayLabel(ctx, 34, -54, '?', PAL.coral, { font: '700 28px "Fredoka"', col: '#fff' });
283
+ ctx.restore();
284
+ }
285
+ // the chip (model) with three weight sliders on its face
286
+ const ck = spring(1.0, 0.9);
287
+ const adj = E(8.6, 9.3);
288
+ const SL0 = [0.2, 0.78, 0.38], SL1 = [0.66, 0.3, 0.82];
289
+ const jitter = t > 8.6 && t < 9.3 ? Math.sin(t * 40) * 0.03 : 0;
290
+ if (ck > 0) {
291
+ ctx.save(); ctx.translate(CHIP[0], CHIP[1] + 110); ctx.scale(ck, ck); ctx.translate(-CHIP[0], -(CHIP[1] + 110));
292
+ // pins
293
+ for (let i = 0; i < 4; i++) for (const [dx, dy, w, h] of [[-118, -66 + i * 44, 34, 16], [118, -66 + i * 44, 34, 16], [-66 + i * 44, -118, 16, 34], [-66 + i * 44, 118, 16, 34]]) clay(ctx, rrect(w, h, 7), [-w / 2, -h / 2, w / 2, h / 2], CHIP[0] + dx, CHIP[1] + dy, PAL.butter, { ext: [3, 6], fast: true });
294
+ const err = ph(8.0, 8.4) * (1 - ph(9.0, 9.4));
295
+ clay(ctx, rrect(220, 220, 50), [-110, -110, 110, 110], CHIP[0], CHIP[1], err > 0 ? { face: PAL.lav.face.map(c => H.mix(c, HX('#ff8f7e'), err * 0.6)), side: PAL.lav.side } : PAL.lav, { ext: [10, 18], spec: [-60, -62, 60, 30] });
296
+ ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
297
+ ctx.font = '700 34px "Fredoka"'; ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.fillText('AI', CHIP[0] - 82, CHIP[1] - 52.5);
298
+ ctx.fillStyle = INK; ctx.fillText('AI', CHIP[0] - 82, CHIP[1] - 54);
299
+ ctx.font = '600 22px "Fredoka"'; ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fillText('weights', CHIP[0] - 30, CHIP[1] - 55);
300
+ ctx.fillStyle = 'rgba(74,58,138,0.85)'; ctx.fillText('weights', CHIP[0] - 30, CHIP[1] - 56);
301
+ for (let i = 0; i < 3; i++) {
302
+ const ty = CHIP[1] - 14 + i * 42;
303
+ ctx.fillStyle = 'rgba(70,50,160,0.28)'; ctx.beginPath(); ctx.roundRect(CHIP[0] - 76, ty - 7, 152, 14, 7); ctx.fill();
304
+ ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.beginPath(); ctx.roundRect(CHIP[0] - 76, ty + 4, 152, 3, 2); ctx.fill();
305
+ const v = H.lerp(SL0[i], SL1[i], adj) + jitter * (i + 1);
306
+ sphere(ctx, CHIP[0] - 70 + v * 140, ty, 14, err > 0 && adj < 1 && t > 8.4 ? HX('#ffe3df') : HX('#ffffff'), err > 0 && t > 8.4 ? HX('#ffb3a6') : HX('#f2ecff'), HX('#b9a8f2'));
307
+ }
308
+ ctx.restore();
309
+ }
310
+ // counter badge
311
+ if (t > 2.7 && t < 6.9) {
312
+ const cnt = Math.floor(1e6 * Math.pow(EO(2.9, 5.7), 1.6));
313
+ const kb = spring(2.7) * (1 - E(6.5, 6.9));
314
+ ctx.save(); ctx.translate(980, 98); ctx.scale(kb, kb);
315
+ clay(ctx, pill(540, 116), [-270, -58, 270, 58], 0, 0, PAL.butter, { ext: [6, 12], spec: [-150, -28, 90, 18] });
316
+ ctx.font = '700 68px "Fredoka"'; ctx.fillStyle = '#8a5a1f'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillText(cnt.toLocaleString('en-US'), 88, 4);
317
+ ctx.font = '700 34px "Fredoka"'; ctx.textAlign = 'left'; ctx.fillText('examples', 102, 10);
318
+ ctx.restore();
319
+ }
320
+ if (t > 4.0 && t < 6.6) clayLabel(ctx, CHIP[0], CHIP[1] - 175, 'the model', PAL.lav, { k: spring(4.0) * (1 - E(6.3, 6.6)), col: '#fff' });
321
+
322
+ // ================= SET 3: guess → check → adjust =================
323
+ const BUB = [1840, 250 + bob(1.1, 6)];
324
+ const wrong = t > 7.0 && t < 9.4, right = t >= 9.4;
325
+ if (t > 6.8) {
326
+ const bk = spring(6.9, 0.7);
327
+ const sk = t > 7.4 && t < 7.8 ? Math.sin(t * 70) * 6 : 0;
328
+ contactShadow(ctx, BUB[0] + 10, FLOOR, 120 * bk, 18, 0.22, 16);
329
+ ctx.save(); ctx.translate(BUB[0] + sk, BUB[1] + 100); ctx.scale(bk * 0.82, bk * 0.82); ctx.translate(-BUB[0], -(BUB[1] + 100));
330
+ const pal = right ? PAL.mint : wrong && t > 7.4 ? PAL.peach : PAL.white;
331
+ clay(ctx, bubblePath(), BUBBLE_BB, BUB[0], BUB[1], pal, { ext: [10, 18], spec: [-95, -78, 85, 40] });
332
+ clayPet(ctx, right ? 'cat' : 'dog', BUB[0] - 80, BUB[1] - 8, 46);
333
+ ctx.font = '700 64px "Fredoka"'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
334
+ ctx.fillStyle = 'rgba(70,45,170,0.25)'; ctx.fillText(right ? 'cat' : 'dog?', BUB[0] - 16, BUB[1] + 4);
335
+ ctx.fillStyle = '#ffffff'; ctx.fillText(right ? 'cat' : 'dog?', BUB[0] - 19, BUB[1]);
336
+ ctx.restore();
337
+ // verdict badge (puffy ball with ✗ / ✓)
338
+ const verdict = () => {
339
+ const vk = right ? spring(9.45, 0.7) : spring(7.4, 0.6);
340
+ const bx = BUB[0] + 140, by = BUB[1] - 100;
341
+ ctx.save(); ctx.translate(bx, by); ctx.scale(vk, vk);
342
+ sphere(ctx, 0, 0, 38, right ? HX('#d9fff0') : HX('#ffd9d2'), right ? HX('#5ee0b0') : HX('#ff7a68'), right ? HX('#2fae84') : HX('#c8403a'));
343
+ ctx.strokeStyle = '#fff'; ctx.lineWidth = 9; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath();
344
+ if (right) { ctx.moveTo(-14, 0); ctx.lineTo(-4, 11); ctx.lineTo(16, -12); } else { ctx.moveTo(-12, -12); ctx.lineTo(12, 12); ctx.moveTo(12, -12); ctx.lineTo(-12, 12); }
345
+ ctx.stroke(); ctx.restore();
346
+ };
347
+ if (t > 7.4) verdict();
348
+ const fade = 1 - E(10.3, 10.7);
349
+ ctx.save(); ctx.globalAlpha = fade;
350
+ // 1 Guess tube: chip → bubble
351
+ clayTube(ctx, [CHIP[0] + 120, CHIP[1] - 80], [1600, 120], [BUB[0] - 170, BUB[1] - 40], E(6.85, 7.25), PAL.lav, 16);
352
+ clayLabel(ctx, 1580, 150, '1 Guess', PAL.white, { k: spring(7.0) });
353
+ if (t > 7.45) clayLabel(ctx, BUB[0] + 30, BUB[1] - 175, '2 Check', PAL.white, { k: spring(7.45) });
354
+ // emphasis: pulsing ring + "answer: cat"
355
+ if (wrong && t > 7.45) {
356
+ const pr = EO(7.45, 7.9);
357
+ // a puffy coral clay torus (cast shadow, darker side, lit face, two highlights) instead of a flat stroke
358
+ ctx.save(); ctx.lineCap = 'round'; ctx.globalAlpha = fade * (0.78 + 0.22 * Math.sin(t * 9));
359
+ const rr = 1.25 - 0.25 * pr, a0 = -Math.PI / 2, a1 = a0 + Math.PI * 2 * pr;
360
+ const ring = (dx, dy, col, lw) => { ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); ctx.ellipse(BUB[0] + dx, BUB[1] + 10 + dy, 210 * rr, 150 * rr, -0.05, a0, a1); ctx.stroke(); };
361
+ ring(5, 14, 'rgba(110,70,160,0.13)', 18);
362
+ ring(0, 4, '#c8483e', 16);
363
+ ring(0, 0, '#ff7a68', 14);
364
+ ring(-1, -2, 'rgba(255,186,170,0.75)', 7);
365
+ ring(-2, -3.5, 'rgba(255,255,255,0.6)', 3);
366
+ ctx.restore();
367
+ verdict(); // keep the ✗ ball in front of the ring
368
+ clayLabel(ctx, BUB[0] + 10, BUB[1] + 205, 'answer: cat', PAL.lav, { k: spring(7.65), col: '#fff' });
369
+ }
370
+ // 3 Adjust tube: bubble → sliders (coral)
371
+ if (t > 8.0) {
372
+ const q = clayTube(ctx, [BUB[0] - 60, BUB[1] + 150], [1640, 640], [CHIP[0] + 100, CHIP[1] + 60], E(8.0, 8.6), PAL.coral, 16);
373
+ if (q && t < 8.75) { const p = q(E(8.0, 8.6) * 0.93); sphere(ctx, p[0], p[1], 15, HX('#ffd9d2'), HX('#ff7a68'), HX('#c8403a')); }
374
+ clayLabel(ctx, 1610, 600, '3 Adjust', PAL.coral, { k: spring(8.5), col: '#fff' });
375
+ if (t < 9.4) clayLabel(ctx, 1700, 500, 'error', PAL.peach, { k: spring(8.1) * (1 - E(9.1, 9.4)), font: '700 28px "Fredoka"', col: '#c8403a' });
376
+ }
377
+ if (right) clayLabel(ctx, BUB[0] + 40, BUB[1] - 175, 'Guess again', PAL.mint, { k: spring(9.5), col: '#fff' });
378
+ ctx.restore();
379
+ }
380
+
381
+ // ================= SET 4: progress — clay bar chart =================
382
+ const ACCS = [61, 72, 80, 86, 90, 93, 95, 97];
383
+ const BX = 2330, BW = 56, GAP = 22;
384
+ if (t > 10.5) {
385
+ const pk = spring(10.5, 0.7);
386
+ contactShadow(ctx, BX + 320, FLOOR + 6, 380 * pk, 22, 0.25, 18);
387
+ ctx.save(); ctx.translate(BX + 310, FLOOR - 10); ctx.scale(pk, pk);
388
+ clay(ctx, pill(700, 40), [-350, -20, 350, 20], 0, 0, PAL.white, { ext: [5, 9], fast: true });
389
+ ctx.restore();
390
+ let topP = null;
391
+ ACCS.forEach((a, i) => {
392
+ const g = spring(10.95 + i * 0.2, 0.8); if (g <= 0) return;
393
+ const hh = (a - 50) * 7 * g, x = BX + i * (BW + GAP) + 40, y = FLOOR - 30 - hh / 2;
394
+ if (hh > 4) clay(ctx, rrect(BW, hh, Math.min(26, hh / 2)), [-BW / 2, -hh / 2, BW / 2, hh / 2], x, y, i === 7 ? PAL.coral : (i % 2 ? PAL.blue : PAL.mint), { ext: [5, 9], fast: true });
395
+ topP = [x, FLOOR - 30 - hh, i];
396
+ });
397
+ if (topP) {
398
+ const cur = Math.round(H.lerp(61, 97, H.ease.inOut(ph(11.0, 12.6))));
399
+ const pop = t > 12.6 ? 1 + 0.18 * Math.sin(Math.min(1, (t - 12.6) / 0.4) * Math.PI) : 1;
400
+ // counter-scale the readout about its base during the final pull-back so 97% stays >= 64px on screen
401
+ const cz = Math.max(1, 0.93 / Z), ax = topP[0] - 30, ay = topP[1] - 70 + 52;
402
+ ctx.save(); ctx.translate(ax, ay); ctx.scale(cz, cz); ctx.translate(-ax, -ay);
403
+ clayLabel(ctx, topP[0] - 30, topP[1] - 70, `${cur}%`, cur >= 97 ? PAL.coral : PAL.butter, { k: spring(10.95) * pop, font: '700 70px "Fredoka"', h: 104, padX: 60, col: cur >= 97 ? '#fff' : '#8a5a1f' });
404
+ clayLabel(ctx, topP[0] - 30, topP[1] - 160, 'Accuracy', PAL.lav, { k: spring(10.95) * pop, font: '700 32px "Fredoka"', col: '#fff' });
405
+ ctx.restore();
406
+ if (t < 13.3) clayLabel(ctx, BX + 40, FLOOR - 30 - 11 * 7 - 36, 'round 1: 61%', PAL.white, { k: spring(11.2) * (1 - E(13.0, 13.3)), font: '700 28px "Fredoka"' });
407
+ if (t < 13.3) { ctx.save(); ctx.globalAlpha = EO(11.2, 11.6) * (1 - E(13.0, 13.3)); ctx.font = '700 28px "Fredoka"'; ctx.fillStyle = 'rgba(80,58,160,0.85)'; ctx.textAlign = 'center'; ctx.fillText('training rounds →', BX + 310, FLOOR + 44); ctx.restore(); }
408
+ if (t > 12.6 && t < 13.6) { // confetti spheres burst
409
+ const cr = H.rng(5), pp = EO(12.6, 13.6);
410
+ for (let i = 0; i < 14; i++) { const a = cr() * Math.PI * 2, d = 60 + cr() * 120; const col = cr.pick([[HX('#ffe3ea'), HX('#ffb1c2'), HX('#ff8fa8')], [HX('#fff6c7'), HX('#ffe08a'), HX('#f3b860')], [HX('#d8f5ec'), HX('#a6e3d2'), HX('#6cbfb0')]]); ctx.globalAlpha = 1 - pp; sphere(ctx, topP[0] + Math.cos(a) * d * pp, topP[1] - 52 + Math.sin(a) * d * pp + pp * pp * 40, 9, ...col); }
411
+ ctx.globalAlpha = 1;
412
+ }
413
+ }
414
+ }
415
+ ctx.restore(); // end world
416
+
417
+ // ---------- screen-space atmosphere: soft lavender vignette + an out-of-focus foreground sphere (hero DOF) ----------
418
+ {
419
+ const vg = ctx.createRadialGradient(W * 0.5, H_ * 0.45, H_ * 0.45, W * 0.5, H_ * 0.45, W * 0.78);
420
+ vg.addColorStop(0, 'rgba(120,90,200,0)'); vg.addColorStop(1, 'rgba(120,90,200,0.11)');
421
+ ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H_);
422
+ ctx.drawImage(fgSphere(), 1238 - (camX - 640) * 0.04 - 160, 678 + bob(4, 3) - 160);
423
+ }
424
+
425
+ // ---------- hook bubble → corner header pill (screen space) ----------
426
+ {
427
+ const inK = spring(0.1, 0.9), out = E(1.5, 2.2);
428
+ if (out < 1) {
429
+ const s2 = inK * (1 - out * 0.75);
430
+ const x = H.lerp(640, 170, out), y = H.lerp(330, 60, out);
431
+ if (out < 0.6 && inK > 0) { // the hook bubble floats over the set: warm glow + soft contact shadow under it
432
+ const fa = 1 - H.smooth(0, 0.6, out), g = ctx.createRadialGradient(640, 320, 30, 640, 320, 380);
433
+ g.addColorStop(0, `rgba(255,214,120,${0.22 * fa * Math.min(1, inK)})`); g.addColorStop(1, 'rgba(255,224,170,0)');
434
+ ctx.fillStyle = g; ctx.fillRect(260, 0, 760, 700);
435
+ contactShadow(ctx, 650, FLOOR, 170 * Math.min(1.1, inK) * (1 - out * 0.8), 22, 0.28 * fa, 18);
436
+ }
437
+ ctx.save(); ctx.globalAlpha = 1 - H.smooth(0.6, 1, out); ctx.translate(x, y + 100 * s2); ctx.scale(Math.max(0.001, s2) * 1.15, Math.max(0.001, s2) * 1.05); ctx.translate(0, -100);
438
+ clay(ctx, bubblePath(), BUBBLE_BB, 0, 0, PAL.lav, { ext: [12, 22], spec: [-95, -78, 85, 40] });
439
+ ctx.font = '700 40px "Fredoka"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
440
+ ctx.fillStyle = 'rgba(70,45,170,0.35)'; ctx.fillText('How does', 3, -24 + 4); ctx.fillText('AI learn?', 3, 24 + 4);
441
+ ctx.fillStyle = '#ffffff'; ctx.fillText('How does', 0, -24); ctx.fillText('AI learn?', 0, 24);
442
+ ctx.restore();
443
+ }
444
+ if (t > 1.9) clayLabel(ctx, 170, 58, 'How does AI learn?', PAL.lav, { k: spring(1.9, 0.7), font: '700 28px "Fredoka"', h: 54, padX: 44, col: '#fff' });
445
+ }
446
+ // ---------- takeaway ----------
447
+ if (t > 13.0) {
448
+ const kk = spring(13.0, 0.8);
449
+ ctx.save(); ctx.translate(640, 628); ctx.scale(kk, kk);
450
+ clay(ctx, pill(860, 100), [-430, -50, 430, 50], 0, 0, PAL.white, { ext: [7, 13], spec: [-260, -22, 140, 18] });
451
+ ctx.font = '700 46px "Fredoka"'; ctx.fillStyle = INK; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('More examples → better guesses', 0, 3);
452
+ ctx.restore();
453
+ }
454
+ ctx.font = '600 20px "Fredoka"'; ctx.fillStyle = `rgba(85,62,165,${0.78 * (1 - E(12.9, 13.2))})`; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; ctx.fillText('AI ASSISTANTS · 101', 64, 692); // steps aside for the takeaway pill
455
+ H.grain(ctx, W, H_, 0.018, 9 + Math.floor(t * 12));
456
+ }
457
+
458
+ // ---------- sound: squishy clay set (rubbery bounces as things spring on, soft puffs, gentle camera dollies) ----------
459
+ function softSound(k) {
460
+ const D = 14.5;
461
+ const ph = (t, a, b) => H.clamp((t - a) / (b - a));
462
+ const inv = (u) => (u < 0.5 ? Math.cbrt(u / 4) : 1 - Math.cbrt((1 - u) * 2) / 2); // inverse of H.ease.inOut
463
+ const invO = (u) => 1 - Math.cbrt(1 - u); // inverse of H.ease.out
464
+ // spring(t0, d) from softExplainer first reaches full size at p = pi/20 (overshoot peak at pi/10)
465
+ const sp = (t0, d = 0.8) => t0 + d * (Math.PI / 20);
466
+ // panning: world x on the set -> screen, using the camera keyframes copied from softExplainer (KF)
467
+ const KF = [[0, 640, 360, 1], [1.6, 640, 360, 1], [2.8, 980, 360, 1], [5.8, 1020, 360, 1], [6.8, 1560, 360, 1], [10.2, 1580, 360, 1], [11.1, 2560, 360, 1], [12.8, 2580, 360, 1], [13.8, 1790, 400, 0.44], [20, 1790, 400, 0.44]];
468
+ const pan = (wx, t) => { let i = 0; while (i < KF.length - 2 && t > KF[i + 1][0]) i++; const A = KF[i], B = KF[i + 1], u = H.ease.inOut(H.clamp((t - A[0]) / (B[0] - A[0]))); const sx = 640 + (wx - H.lerp(A[1], B[1], u)) * H.lerp(A[3], B[3], u); return H.clamp(sx / 640 - 1, -1, 1) * 0.6; };
469
+ const busy = [];
470
+ // squish: a soft rubbery bounce, pitch bends up as it squashes then settles with one smaller wobble
471
+ const squish = (t, f, g, p = 0, minor) => {
472
+ busy.push(t);
473
+ k.tone(t, { f: [[0, f * 0.7], [0.05, f * 1.18], [0.17, f * 0.92]], dur: 0.2, gain: 0.26 * g, a: 0.006, pan: p, minor });
474
+ k.tone(t + 0.13, { f: [[0, f * 0.95], [0.05, f * 1.06], [0.12, f]], dur: 0.13, gain: 0.08 * g, a: 0.01, pan: p, minor }); // the overshoot wobble
475
+ k.noise(t, { dur: 0.05, color: 'pink', filter: 'lowpass', f: 900, gain: 0.18 * g, a: 0.004, pan: p, minor }); // soft contact puff
476
+ };
477
+ const puff = (t, g, p = 0) => busy.push(t) && k.noise(t, { dur: 0.09, color: 'pink', filter: 'bandpass', f: [[0, 500], [0.09, 900]], q: 1, gain: 0.12 * g, a: 0.02, pan: p, minor: true });
478
+ const dolly = (t0, t1, g = 1) => { const d = t1 - t0; k.noise(t0, { dur: d + 0.15, color: 'pink', filter: 'bandpass', f: [[0, 220], [d * 0.5, 480], [d, 240]], q: 0.7, gain: 0.045 * g, a: d * 0.5, verb: 0.1 }); };
479
+ const counter = (a, b, val, f, g, p, minGap) => {
480
+ let last = -1, lastV = val(a); const top = Math.abs(val(b) - val(a)) / (b - a) * 0.22;
481
+ for (let t = a; t <= b + 1e-6; t += 1 / 120) {
482
+ const v = val(t); if (v === lastV) continue; lastV = v;
483
+ const speed = Math.abs(val(Math.min(b, t + 0.05)) - val(Math.max(a, t - 0.05)));
484
+ const gap = H.clamp(minGap / Math.max(0.2, Math.min(1, speed / top)), minGap, 0.4);
485
+ if (t - last >= gap && !busy.some((x) => t > x - 0.03 && t < x + 0.27)) { k.play('tick', t, { f, gain: g, pan: p, minor: true }); k.tone(t, { f: f / 10, dur: 0.03, gain: 0.05 * g, pan: p, minor: true }); last = t; }
486
+ }
487
+ };
488
+
489
+ k.bed('room', 0, D, { gain: 0.22 });
490
+ k.music('pad', 0.2, D, { bpm: 100, gain: 0.3, chords: [['Eb4', 'G4', 'Bb4', 'D5'], ['C4', 'Eb4', 'G4', 'Bb4'], ['Ab3', 'C4', 'Eb4', 'G4'], ['Bb3', 'D4', 'F4', 'Ab4'], ['Eb4', 'G4', 'Bb4', 'Eb5']] });
491
+
492
+ // ---- hook: the clay speech bubble bounces on (spring 0.1, 0.9), the chip springs up behind (1.0, 0.9)
493
+ squish(sp(0.1, 0.9), 300, 1.2, 0);
494
+ squish(sp(1.0, 0.9), 170, 1.1, pan(1350, 1.14)); // the AI chip
495
+ k.noise(1.5, { dur: 0.7, color: 'pink', filter: 'lowpass', f: [[0, 650], [0.7, 300]], gain: 0.05, a: 0.25, minor: true }); // bubble shrinks off to the corner
496
+ squish(sp(1.9, 0.7), 420, 0.4, -0.7, true); // header pill
497
+ dolly(1.6, 2.8);
498
+
499
+ // ---- beat 2: cards puff out of the tube, the pile grows, the counter badge
500
+ k.noise(2.6, { dur: 0.9, color: 'pink', filter: 'bandpass', f: [[0, 260], [0.9, 620]], q: 1.4, gain: 0.07, a: 0.4, pan: pan(700, 3), minor: true }); // puffy tube squeezes out (E 2.6-3.5)
501
+ for (let j = 0; j < 9; j++) { const t = 2.3 + j * 0.15 / 0.36; if (t < 6.4) puff(t, 0.9, pan(560, t)); } // a card pops out of the tube
502
+ for (let j = 0; j < 4; j++) { const t = 2.3 + (1 + j * 0.15) / 0.36; if (t < 6.5) k.tone(t, { f: [[0, 240], [0.1, 150]], dur: 0.14, gain: 0.1, pan: pan(1310, t), minor: true }); } // swallowed by the chip
503
+ // examples pile: item i springs at 2.6 + 0.4i once n = floor(1 + 7 * EO(2.6,5.8)^1.1) > i
504
+ for (let i = 0; i < 8; i++) { const tn = i === 0 ? 2.6 : 2.6 + 3.2 * invO(Math.pow(i / 7, 1 / 1.1)), t = sp(Math.max(tn, 2.6 + i * 0.4), 0.6); if (t < 6.4) squish(t, 260 + (i % 3) * 30, 0.32, pan(600 + (i % 3) * 76, t), true); }
505
+ squish(sp(2.7), 360, 0.8, pan(980, 2.8)); // butter counter badge
506
+ counter(2.95, 5.66, (t) => Math.floor(1e6 * Math.pow(H.ease.out(ph(t, 2.9, 5.7)), 1.6)), 2400, 0.75, pan(980, 4), 1 / 8);
507
+ squish(sp(4.0), 380, 0.75, pan(1350, 4.1)); // "the model"
508
+ squish(5.7, 330, 0.9, pan(980, 5.7)); k.play('ding', 5.7, { f: 'Bb5', gain: 0.35, pan: pan(980, 5.7) }); // 1,000,000 lands
509
+
510
+ // ---- beat 3
511
+ squish(5.95, 340, 0.8, pan(560, 5.95)); // pink test card with its "?"
512
+ dolly(5.8, 6.8);
513
+ k.noise(6.5, { dur: 0.4, color: 'pink', filter: 'lowpass', f: [[0, 700], [0.4, 300]], gain: 0.04, a: 0.1, minor: true }); // counter badge squashes away
514
+ k.tone(7.0, { f: [[0, 200], [0.12, 130]], dur: 0.16, gain: 0.12, pan: pan(1310, 7.0), minor: true }); // test card swallowed
515
+ k.noise(6.85, { dur: 0.4, color: 'pink', filter: 'bandpass', f: [[0, 300], [0.4, 700]], q: 1.4, gain: 0.07, a: 0.2, pan: pan(1600, 7.05), minor: true }); // Guess tube squeezes over
516
+ squish(sp(6.9, 0.7), 280, 1.1, pan(1840, 7.0)); // answer bubble "dog?" bounces up
517
+ squish(sp(7.0), 420, 0.42, pan(1580, 7.1), true); // "1 Guess"
518
+ // CHECK: wrong. bubble shakes, the X ball bounces on
519
+ k.play('error', 7.42, { f: 'G3', gain: 0.8, pan: pan(1980, 7.45) });
520
+ squish(sp(7.4, 0.6), 220, 0.8, pan(1980, 7.45));
521
+ squish(sp(7.65), 360, 0.4, pan(1850, 7.75), true); // "answer: cat"
522
+ // ADJUST: coral tube squeezes back to the chip, the error ball rolls along it
523
+ k.noise(8.0, { dur: 0.6, color: 'pink', filter: 'bandpass', f: [[0, 700], [0.6, 300]], q: 1.4, gain: 0.08, a: 0.3, pan: pan(1640, 8.3) });
524
+ k.tone(8.0, { f: [[0, 300], [0.6, 180]], type: 'triangle', dur: 0.62, gain: 0.05, a: 0.25, pan: pan(1640, 8.3), minor: true });
525
+ squish(sp(8.1), 460, 0.38, pan(1700, 8.2), true); // "error"
526
+ squish(sp(8.5), 340, 0.8, pan(1610, 8.6)); // "3 Adjust"
527
+ // sliders glide to new weights (E 8.6-9.3 with a jitter): three rubber knobs sliding
528
+ [0, 1, 2].forEach((i) => k.noise(8.62 + i * 0.05, { dur: 0.62, color: 'pink', filter: 'bandpass', f: [[0, 380 + i * 60], [0.3, 640 + i * 60], [0.62, 420 + i * 60]], q: 3, gain: 0.07, a: 0.25, pan: pan(1350, 9), minor: true }));
529
+ squish(9.3, 300, 0.5, pan(1350, 9.3), true); // knobs settle
530
+ // CORRECT: bubble turns mint "cat", the check ball bounces on
531
+ k.play('success', sp(9.45, 0.7), { f: 'Eb5', gain: 0.42, pan: pan(1980, 9.55) });
532
+ squish(sp(9.45, 0.7), 300, 0.8, pan(1980, 9.55));
533
+ squish(sp(9.5), 420, 0.38, pan(1880, 9.6), true); // "Guess again"
534
+
535
+ // ---- beat 4: clay bars bounce up one per round
536
+ dolly(10.2, 11.1);
537
+ squish(sp(10.5, 0.7), 180, 0.7, pan(2640, 10.6), true); // chart base
538
+ squish(sp(10.95), 360, 0.7, pan(2330, 11.05)); // Accuracy badge
539
+ ['Eb3', 'F3', 'G3', 'Bb3', 'C4', 'D4', 'Eb4', 'G4'].forEach((n, i) => squish(sp(10.95 + i * 0.2), k.hz(n), i === 7 ? 0.7 : 0.45, pan(2370 + i * 78, 11 + i * 0.2), i < 7));
540
+ counter(11.03, 12.56, (t) => Math.round(H.lerp(61, 97, H.ease.inOut(ph(t, 11.0, 12.6)))), 2200, 0.7, pan(2700, 12), 1 / 12);
541
+ // 97%: the big hit. badge swells coral, confetti spheres burst
542
+ k.play('impact', 12.6, { gain: 1.3, pan: pan(2950, 12.6) });
543
+ squish(12.6, 240, 1.4, pan(2950, 12.6));
544
+ ['Eb4', 'G4', 'Bb4', 'Eb5'].forEach((n, i) => k.play('ding', 12.62 + i * 0.03, { f: n, gain: 0.4 - i * 0.05, pan: (i - 1.5) * 0.25 }));
545
+ for (let i = 0; i < 6; i++) k.tone(12.68 + i * 0.07, { f: [[0, 380 + i * 30], [0.06, 520 + i * 30]], dur: 0.08, gain: 0.07, pan: Math.sin(i * 2.1) * 0.6, minor: true }); // little bouncy confetti balls
546
+ dolly(12.8, 13.8, 0.9);
547
+ // takeaway pill springs on (13.0, 0.8) and the phrase resolves
548
+ squish(sp(13.0), 260, 1.0, 0);
549
+ ['Bb4', 'Eb5'].forEach((n, i) => k.play('ding', 13.32 + i * 0.09, { f: n, gain: 0.3 - i * 0.05, pan: (i - 0.5) * 0.3 }));
550
+ k.tone(13.32, { f: 'Eb3', type: 'triangle', dur: 1.2, gain: 0.12, a: 0.02 });
551
+ }
552
+
553
+ registerStyle({
554
+ id: 'soft-3d',
555
+ name: 'Soft 3D Clay',
556
+ category: 'Dimensional',
557
+ description: 'Dribbble-style “puffy” 3D icons: pastel matte-clay objects with chunky rounded extrusions, soft core shadows, bright bounce-light rims, glossy specular blobs and blurry contact shadows on a seamless pastel sweep.',
558
+ motion: 'Clay icons drop in one by one on springy squash-and-stretch with their contact shadows growing beneath them, the bulb flickers on, then the cursor flies in and taps the bubble while everything bobs at different depths.',
559
+ duration: 5,
560
+ fonts: ['600 38px "Fredoka"', '700 38px "Fredoka"', '600 15px "Fredoka"', '700 64px "Fredoka"', '600 18px "Fredoka"'],
561
+ frameT: 4.6,
562
+ readsPixels: true,
563
+ explainerDuration: 14.5,
564
+ explainerNote: 'The camera dollies across one wide pastel set with parallax depth-of-field (cards → clay chip with weight sliders → answer bubble → clay bar chart → pull back), arrows are puffy clay tubes with blobby heads, labels are squishy pills that spring onto objects, and numbers count inside butter-yellow clay badges.',
565
+ explainer: softExplainer,
566
+ explainerSound: softSound,
567
+ draw(ctx, W, H_, t) {
568
+ // ---------- seamless cyclorama ----------
569
+ const bg = ctx.createLinearGradient(0, 0, 0, H_);
570
+ bg.addColorStop(0, '#f3ecff'); bg.addColorStop(0.55, '#e6dcfa'); bg.addColorStop(0.62, '#e4d9f8'); bg.addColorStop(1, '#f1eaff');
571
+ ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H_);
572
+ const glow = ctx.createRadialGradient(W * 0.48, H_ * 0.35, 40, W * 0.48, H_ * 0.35, 700);
573
+ glow.addColorStop(0, 'rgba(255,250,255,0.8)'); glow.addColorStop(1, 'rgba(255,250,255,0)');
574
+ ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H_);
575
+
576
+ const bob = (ph, amp) => Math.sin(t * 1.6 + ph) * amp;
577
+ const FLOOR = 610;
578
+ const ph = (a, b) => H.clamp((t - a) / (b - a));
579
+ // springy pop-in: drops in from above, overshoots, squashes and stretches, settles
580
+ const spring = (t0) => {
581
+ const p = ph(t0, t0 + 0.95);
582
+ if (p <= 0) return null;
583
+ const k = p >= 1 ? 1 : 1 - Math.exp(-6.5 * p) * Math.cos(10 * p);
584
+ const st = p >= 1 ? 0 : Math.pow(1 - p, 2) * Math.sin(p * 13) * 0.22;
585
+ return { k, sx: k * (1 - st), sy: k * (1 + st), dy: -(1 - H.ease.out(Math.min(1, p * 1.6))) * 70 };
586
+ };
587
+ const pop = (c, x, y, t0, fn) => {
588
+ const sp = spring(t0); if (!sp) return;
589
+ c.save(); c.translate(x, y + sp.dy); c.scale(Math.max(0.001, sp.sx), Math.max(0.001, sp.sy)); c.translate(-x, -y);
590
+ fn(); c.restore();
591
+ };
592
+ const cshadow = (x, y, w, h, a, bl, t0) => { const sp = spring(t0); if (!sp) return; const k = Math.min(1.1, sp.k) * (1 + sp.dy / 200); contactShadow(ctx, x, y, w * k, h * k, a * Math.min(1, sp.k), bl); };
593
+
594
+ // ---------- far layer (blurred for depth of field) ----------
595
+ const far = H.layer(W, H_), fc = far.getContext('2d');
596
+ const gy = 250 + bob(1.2, 9);
597
+ cshadow(985, FLOOR - 90, 95, 16, 0.22, 16, 0.8);
598
+ pop(fc, 975, gy, 0.8, () => puffy(fc, { x: 975, y: gy, rot: t * 0.35 + 0.2 - (1 - ph(0.8, 2.2)) * 1.2, s: 0.92, path: gearPath(118, 8, 40), rule: 'evenodd', bb: [-118, -118, 118, 118],
599
+ face: [H.hex('#e3f1ff'), H.hex('#b4d6ff'), H.hex('#8fbaf5')], side: [H.hex('#86b0ee'), H.hex('#5a7fc7')], ext: [10, 20], spec: [-50, -60, 50, 30] }));
600
+ pop(fc, 1150, 120, 1.0, () => sphere(fc, 1150, 120 + bob(2.5, 6), 22, H.hex('#fff6c7'), H.hex('#ffe08a'), H.hex('#f3b860')));
601
+ pop(fc, 180, 110, 1.2, () => sphere(fc, 180, 110 + bob(0.4, 5), 16, H.hex('#d8f5ec'), H.hex('#a6e3d2'), H.hex('#6cbfb0')));
602
+ ctx.save(); ctx.filter = 'blur(2.2px)'; ctx.drawImage(far, 0, 0); ctx.restore();
603
+
604
+ // ---------- mid layer: lightbulb (left) ----------
605
+ {
606
+ const bx = 300, by = 262 + bob(0.6, 11), r = 92, s = 1, rot = -0.14;
607
+ cshadow(318, FLOOR - 40, 80, 14, 0.22, 14, 0.5);
608
+ // glow aura: the bulb "switches on" with a flicker once it has landed
609
+ const on = t > 1.7 ? (t < 1.78 || (t > 1.86 && t < 1.92) ? 0.3 : 1) * H.ease.out(ph(1.7, 2.1)) : 0;
610
+ const a = ctx.createRadialGradient(bx, by - 10, 20, bx, by - 10, 210);
611
+ a.addColorStop(0, `rgba(255,236,150,${0.55 * on})`); a.addColorStop(1, 'rgba(255,236,150,0)');
612
+ ctx.fillStyle = a; ctx.fillRect(bx - 220, by - 230, 440, 440);
613
+ pop(ctx, bx, by + 120, 0.5, () => {
614
+ for (let i = 2; i >= 0; i--) {
615
+ const ox = Math.sin(rot) * -(r * 1.05 + i * 26), oy = Math.cos(rot) * (r * 1.05 + i * 26);
616
+ const w = 54 - i * 4;
617
+ puffy(ctx, { x: bx + ox, y: by + oy, rot, s, path: roundedPoly([[-w, -14, 14], [w, -14, 14], [w, 14, 14], [-w, 14, 14]]), bb: [-w, -14, w, 14],
618
+ face: [H.hex('#e3fbf3'), H.hex('#b3eadb'), H.hex('#86d3c0')], side: [H.hex('#7fcbb8'), H.hex('#4f9f93')], ext: [5, 9] });
619
+ }
620
+ {
621
+ const ox = Math.sin(rot) * -(r * 1.05 + 3 * 26 - 6), oy = Math.cos(rot) * (r * 1.05 + 3 * 26 - 6);
622
+ puffy(ctx, { x: bx + ox, y: by + oy, rot, s, path: roundedPoly([[-26, -12, 12], [26, -12, 12], [12, 12, 8], [-12, 12, 8]]), bb: [-26, -12, 26, 12],
623
+ face: [H.hex('#a39cc4'), H.hex('#7d74a8'), H.hex('#5f5689')], side: [H.hex('#5a5185'), H.hex('#3f3866')], ext: [4, 7] });
624
+ }
625
+ const off = 1 - on;
626
+ puffy(ctx, { x: bx, y: by, rot, s, path: bulbGlassPath(r), bb: [-r, -r, r, r],
627
+ face: [H.mix('#fffbe0', '#f4efe6', off * 0.6), H.mix('#ffe58f', '#e8dcc6', off * 0.6), H.mix('#f7c45e', '#cdbb9e', off * 0.6)], side: [H.mix('#f2b955', '#c4ae8c', off * 0.6), H.mix('#d98f3a', '#9c8566', off * 0.6)], ext: [8, 14], spec: [-38, -52, 40, 26] });
628
+ ctx.save(); ctx.translate(bx, by); ctx.rotate(rot);
629
+ ctx.strokeStyle = `rgba(232,150,60,${0.25 + 0.3 * on})`; ctx.lineWidth = 7; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
630
+ ctx.beginPath(); ctx.moveTo(-18, 70); ctx.lineTo(-18, 18); ctx.lineTo(-9, 4); ctx.lineTo(0, 18); ctx.lineTo(9, 4); ctx.lineTo(18, 18); ctx.lineTo(18, 70); ctx.stroke();
631
+ ctx.strokeStyle = `rgba(255,255,240,${0.9 * on})`; ctx.lineWidth = 2.5;
632
+ ctx.beginPath(); ctx.moveTo(-19, 68); ctx.lineTo(-19, 17); ctx.lineTo(-10, 3); ctx.lineTo(-1, 17); ctx.lineTo(8, 3); ctx.lineTo(17, 17); ctx.stroke();
633
+ ctx.restore();
634
+ });
635
+ }
636
+
637
+ // ---------- hero: chat bubble ----------
638
+ const cbx = 640, cby = 305 + bob(0, 8);
639
+ const tapAt = 2.6, tapT = t - tapAt;
640
+ const tapPress = tapT > 0 ? Math.max(0, Math.sin(tapT * 2.4 * Math.PI / 2.4 * 1.0)) : 0; // 0..1 press cycles
641
+ const squish = tapT > 0 ? Math.pow(Math.max(0, Math.sin(tapT * Math.PI * 0.8)), 6) * 0.03 : 0;
642
+ cshadow(650, FLOOR, 170, 22, 0.3, 18, 0.15);
643
+ pop(ctx, cbx, cby + 120, 0.15, () => {
644
+ ctx.save(); ctx.translate(cbx, cby + 100); ctx.scale(1 + squish, 1 - squish); ctx.translate(-cbx, -(cby + 100));
645
+ puffy(ctx, { x: cbx, y: cby, rot: -0.04, s: 1, path: bubblePath(), bb: BUBBLE_BB,
646
+ face: [H.hex('#d9d0ff'), H.hex('#a796ff'), H.hex('#8c78f5')], side: [H.hex('#806ce8'), H.hex('#5340b8')], ext: [12, 22], spec: [-95, -78, 85, 40] });
647
+ // text on bubble: each line bounces in
648
+ ctx.save(); ctx.translate(cbx, cby); ctx.rotate(-0.04);
649
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
650
+ ctx.font = '700 38px "Fredoka"';
651
+ [['Ask me', -26, 1.0], ['anything', 22, 1.25]].forEach(([str, y, t0]) => {
652
+ const sp = spring(t0); if (!sp) return;
653
+ ctx.save(); ctx.translate(0, y); ctx.scale(sp.sx, sp.sy);
654
+ ctx.fillStyle = 'rgba(70,45,170,0.35)'; ctx.fillText(str, 3, 4);
655
+ ctx.fillStyle = '#ffffff'; ctx.fillText(str, 0, 0);
656
+ ctx.restore();
657
+ });
658
+ ctx.restore();
659
+ ctx.restore();
660
+ });
661
+ // typing-dots badge
662
+ {
663
+ const px = cbx + 150, py = cby - 118;
664
+ pop(ctx, px, py, 1.6, () => {
665
+ puffy(ctx, { x: px, y: py, rot: 0.1, s: 1, path: roundedPoly([[-42, -22, 22], [42, -22, 22], [42, 22, 22], [-42, 22, 22]]), bb: [-42, -22, 42, 22],
666
+ face: [H.hex('#ffffff'), H.hex('#f4efff'), H.hex('#ddd2fb')], side: [H.hex('#cdbff5'), H.hex('#9a87de')], ext: [5, 9] });
667
+ for (let i = 0; i < 3; i++) {
668
+ const jump = Math.max(0, Math.sin(t * 6 - i * 0.9)) * 5;
669
+ sphere(ctx, px - 22 + i * 22, py + i * 2.2 - 2 - jump, 7.5, H.hex('#c8bcff'), H.hex('#9481f0'), H.hex('#6a55cf'));
670
+ }
671
+ });
672
+ }
673
+
674
+ // ---------- ring (donut) bottom-left ----------
675
+ {
676
+ const rx = 150, ry = 525 + bob(2.2, 4);
677
+ cshadow(rx + 8, FLOOR - 10, 92, 14, 0.25, 12, 1.0);
678
+ pop(ctx, rx, ry + 30, 1.0, () => puffy(ctx, { x: rx, y: ry, rot: 0.08, s: 1, path: ringPath(92, 46), rule: 'evenodd', bb: [-92, -40, 92, 40],
679
+ face: [H.hex('#fff3d6'), H.hex('#ffd59a'), H.hex('#f6b36d')], side: [H.hex('#efa760'), H.hex('#c97a3e')], ext: [4, 16], spec: [-50, -22, 34, 12] }));
680
+ }
681
+ // sphere resting on floor
682
+ cshadow(1095, FLOOR - 4, 46, 9, 0.35, 8, 1.15);
683
+ pop(ctx, 1090, 600, 1.15, () => sphere(ctx, 1090, 560, 42, H.hex('#d8f5ec'), H.hex('#a6e3d2'), H.hex('#5fb3a3')));
684
+
685
+ // ---------- near layer: cursor (flies in, then taps the bubble) ----------
686
+ {
687
+ const tap = tapPress * 10;
688
+ const fly = H.ease.out(ph(1.35, 2.2));
689
+ const cx = 790 - tap * 0.6 + (1 - fly) * 260, cy = 395 - tap + bob(3.1, 4) + (1 - fly) * 160;
690
+ const rot = -0.32 + (1 - fly) * 0.5;
691
+ if (fly > 0) {
692
+ contactShadow(ctx, 870 + (1 - fly) * 200, FLOOR + 20, 70, 14, 0.25 * fly, 14);
693
+ puffy(ctx, { x: cx, y: cy, rot, s: 1.08, path: cursorPath(), bb: CURSOR_BB,
694
+ face: [H.hex('#ffe3ea'), H.hex('#ffb1c2'), H.hex('#ff8fa8')], side: [H.hex('#f57f9b'), H.hex('#cc4f72')], ext: [10, 16], spec: [26, 40, 30, 50] });
695
+ }
696
+ }
697
+
698
+ // ---------- out-of-focus foreground element (drifts in from the corner) ----------
699
+ {
700
+ const fi = H.ease.out(ph(0.3, 1.6));
701
+ const fg = H.layer(W, H_), gc = fg.getContext('2d');
702
+ sphere(gc, 1238 + (1 - fi) * 160, 678 + (1 - fi) * 90 + bob(4, 3), 100, H.hex('#f3eeff'), H.hex('#d4c8ff'), H.hex('#a998ec'));
703
+ ctx.save(); ctx.filter = 'blur(9px)'; ctx.drawImage(fg, 0, 0); ctx.restore();
704
+ }
705
+ // ---------- caption chip ----------
706
+ ctx.font = '600 20px "Fredoka"'; ctx.fillStyle = `rgba(85,62,165,${0.8 * ph(1.8, 2.3)})`; ctx.textAlign = 'left';
707
+ try { ctx.letterSpacing = '1px'; } catch (e) {}
708
+ ctx.fillText('AI ASSISTANTS · 101', 64, 676);
709
+ try { ctx.letterSpacing = '0px'; } catch (e) {}
710
+ H.grain(ctx, W, H_, 0.018, 9 + Math.floor(t * 12));
711
+ },
712
+ });
713
+ }