@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,768 @@
1
+ {
2
+ // ---------- ornate art-nouveau tarot card on velvet ----------
3
+ const TAU = Math.PI * 2;
4
+ const LINE = '#2a1c15';
5
+ function rr(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
6
+ function gold(ctx, x0, y0, x1, y1, shift = 0) {
7
+ const g = ctx.createLinearGradient(x0, y0, x1, y1);
8
+ const s = (v) => H.clamp(v + shift, 0, 1);
9
+ g.addColorStop(0, '#7a5a22'); g.addColorStop(s(0.25), '#c9a54e'); g.addColorStop(s(0.42), '#f3e3a0'); g.addColorStop(s(0.55), '#c39a44'); g.addColorStop(s(0.8), '#8d6a2a'); g.addColorStop(1, '#c4a050');
10
+ return g;
11
+ }
12
+ function star(ctx, x, y, R, r, n = 4, rot = 0) {
13
+ ctx.beginPath();
14
+ for (let k = 0; k < n * 2; k++) { const a = rot + k * Math.PI / n - Math.PI / 2, q = k % 2 ? r : R; k ? ctx.lineTo(x + Math.cos(a) * q, y + Math.sin(a) * q) : ctx.moveTo(x + Math.cos(a) * q, y + Math.sin(a) * q); }
15
+ ctx.closePath();
16
+ }
17
+ function crescent(ctx, x, y, R, off) {
18
+ ctx.beginPath(); ctx.arc(x, y, R, 0, TAU); ctx.arc(x + off, y - off * 0.3, R * 0.86, 0, TAU, true); ctx.closePath();
19
+ }
20
+ // thick outlined tube along a cubic (cable "hair")
21
+ function cable(ctx, pts, w, col, gl) {
22
+ const path = () => { ctx.beginPath(); ctx.moveTo(...pts[0]); for (let i = 1; i + 2 < pts.length + 0; i += 3) ctx.bezierCurveTo(...pts[i], ...pts[i + 1], ...pts[i + 2]); };
23
+ ctx.save(); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
24
+ path(); ctx.strokeStyle = LINE; ctx.lineWidth = w + 4.4; ctx.stroke();
25
+ path(); ctx.strokeStyle = col; ctx.lineWidth = w; ctx.stroke();
26
+ // flat highlight stripe
27
+ ctx.save(); ctx.translate(-w * 0.18, -w * 0.18); path(); ctx.strokeStyle = 'rgba(255,240,210,0.22)'; ctx.lineWidth = w * 0.28; ctx.stroke(); ctx.restore();
28
+ // gold bands
29
+ ctx.lineCap = 'butt'; ctx.setLineDash([2.2, 46]); ctx.lineDashOffset = -12; path(); ctx.strokeStyle = LINE; ctx.lineWidth = w + 4.4; ctx.stroke(); ctx.setLineDash([2.2, 3, 2.2, 38.6]); ctx.lineDashOffset = -12; path(); ctx.strokeStyle = gl; ctx.lineWidth = w + 2; ctx.stroke();
30
+ ctx.setLineDash([]);
31
+ ctx.restore();
32
+ }
33
+ function cardBack(ctx, w, h, seed) {
34
+ const r = H.rng(seed);
35
+ ctx.save();
36
+ rr(ctx, -w / 2, -h / 2, w, h, 16); ctx.fillStyle = '#1d2744'; ctx.fill();
37
+ ctx.save(); rr(ctx, -w / 2, -h / 2, w, h, 16); ctx.clip();
38
+ ctx.strokeStyle = gold(ctx, -w / 2, -h / 2, w / 2, h / 2); ctx.lineWidth = 3; rr(ctx, -w / 2 + 14, -h / 2 + 14, w - 28, h - 28, 8); ctx.stroke();
39
+ ctx.lineWidth = 1; rr(ctx, -w / 2 + 20, -h / 2 + 20, w - 40, h - 40, 6); ctx.stroke();
40
+ ctx.fillStyle = '#b8954a';
41
+ for (let y = -h / 2 + 40; y < h / 2 - 30; y += 34) for (let x = -w / 2 + 40 + ((y / 34) & 1) * 17; x < w / 2 - 30; x += 34) { star(ctx, x, y, 5, 1.6, 4); ctx.fill(); }
42
+ ctx.beginPath(); ctx.arc(0, 0, 50, 0, TAU); ctx.fillStyle = '#1d2744'; ctx.fill(); ctx.strokeStyle = '#c4a050'; ctx.lineWidth = 2.5; ctx.stroke();
43
+ star(ctx, 0, 0, 40, 10, 8); ctx.fillStyle = '#c4a050'; ctx.fill();
44
+ ctx.restore(); ctx.restore();
45
+ }
46
+
47
+ // ===== EXPLAINER HELPERS =====
48
+ let VELVET = null;
49
+ function velvet(W, H_) {
50
+ if (VELVET) return VELVET;
51
+ const nz = H.makeNoise(9), vw = 320, vh = 180, v = H.layer(vw, vh), vc = v.getContext('2d'), img = vc.createImageData(vw, vh), d = img.data;
52
+ const dark = [18, 5, 14], mid = [74, 18, 44], hi = [150, 66, 98];
53
+ for (let y = 0; y < vh; y++) for (let x = 0; x < vw; x++) {
54
+ const X = x * 4, Y = y * 4, warp = nz.fbm2(X * 0.0018, Y * 0.0018, 3) * 2.4;
55
+ const f = Math.sin(X * 0.0062 - Y * 0.0035 + warp) * 0.65 + Math.sin(X * 0.0024 + Y * 0.0078 + warp * 1.4) * 0.35;
56
+ const ridge = Math.pow(1 - Math.abs(f), 1.6), mott = nz.fbm2(X * 0.02, Y * 0.02, 3), spot = Math.exp(-(((X - 640) / 700) ** 2 + ((Y - 360) / 460) ** 2));
57
+ const b = H.clamp(0.1 + 0.55 * ridge * ridge * (0.5 + 0.5 * spot) + 0.14 * spot + 0.1 * mott);
58
+ const c = b < 0.5 ? H.mix(dark, mid, b * 2) : H.mix(mid, hi, (b - 0.5) * 2);
59
+ const i = (y * vw + x) * 4; d[i] = c[0]; d[i + 1] = c[1]; d[i + 2] = c[2]; d[i + 3] = 255;
60
+ }
61
+ vc.putImageData(img, 0, 0);
62
+ const L = H.layer(W, H_), c = L.getContext('2d'); c.imageSmoothingQuality = 'high'; c.drawImage(v, 0, 0, W, H_); H.grain(c, W, H_, 0.07, 31, false);
63
+ VELVET = L; return L;
64
+ }
65
+ const SKY = ['#2c3350', '#353a58', '#41405f', '#514765', '#62506a'];
66
+ // the hero card's aging, built once at hero card proportions and stretched over each explainer face:
67
+ // AGE = scuffs, foxing rings and worn edges (paper core showing, heavier at the corners); GRAIN = print tooth (overlay)
68
+ let AGE = null, GRAIN = null;
69
+ function ageLayers() {
70
+ if (AGE) return;
71
+ const w = 400, h = 690, hw = w / 2, hh = h / 2, nz = H.makeNoise(9), sr = H.rng(4);
72
+ AGE = H.layer(w, h); const a = AGE.getContext('2d'); a.translate(hw, hh);
73
+ for (let k = 0; k < 6; k++) { const x = sr.range(-hw, hw), y = sr.range(-hh, hh), R = sr.range(12, 44); const g = a.createRadialGradient(x, y, 0, x, y, R); g.addColorStop(0, 'rgba(150,110,60,0)'); g.addColorStop(0.85, 'rgba(150,105,55,0.13)'); g.addColorStop(1, 'rgba(150,105,55,0)'); a.fillStyle = g; a.beginPath(); a.arc(x, y, R, 0, TAU); a.fill(); }
74
+ for (let k = 0; k < 22; k++) { const x = sr.range(-hw, hw), y = sr.range(-hh, hh), an = sr.range(0, TAU), l = sr.range(6, 30); a.strokeStyle = `rgba(245,236,214,${sr.range(0.15, 0.4)})`; a.lineWidth = sr.range(0.6, 1.3); a.beginPath(); a.moveTo(x, y); a.lineTo(x + Math.cos(an) * l, y + Math.sin(an) * l); a.stroke(); }
75
+ for (let k = 0; k < 900; k++) {
76
+ const u = k / 900, per = 2 * (w + h), d = u * per;
77
+ let x, y;
78
+ if (d < w) { x = -hw + d; y = -hh; } else if (d < w + h) { x = hw; y = -hh + d - w; } else if (d < 2 * w + h) { x = hw - (d - w - h); y = hh; } else { x = -hw; y = hh - (d - 2 * w - h); }
79
+ const corner = Math.max(0, 1 - Math.min(Math.hypot(x - hw, y - hh), Math.hypot(x + hw, y - hh), Math.hypot(x - hw, y + hh), Math.hypot(x + hw, y + hh)) / 76);
80
+ const wv = (0.5 + 0.5 * nz.n2(u * 90, 3)) * 3.2 + corner * 10 * (0.5 + 0.5 * nz.n2(u * 40, 7));
81
+ if (wv < 1) continue;
82
+ a.fillStyle = `rgba(246,238,220,${0.5 + corner * 0.4})`; a.beginPath(); a.arc(x, y, wv, 0, TAU); a.fill();
83
+ }
84
+ GRAIN = H.layer(w, h); const g = GRAIN.getContext('2d'), img = g.createImageData(w, h), q = img.data, gr = H.rng(17);
85
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { const i = (y * w + x) * 4, n = 128 + (gr() - 0.5) * 30 + nz.n2(x * 0.08, y * 0.08) * 16; q[i] = n; q[i + 1] = n; q[i + 2] = n; q[i + 3] = 255; }
86
+ g.putImageData(img, 0, 0);
87
+ }
88
+ const hashS = (str) => { let v = 7; for (const ch of str) v = (v * 31 + ch.charCodeAt(0)) | 0; return Math.abs(v); };
89
+ // a full art-nouveau card face, centred at the origin
90
+ function tFace(c, w, h, numeral, title, art, sheen = 0.4) {
91
+ const hw = w / 2, hh = h / 2, s = w / 364;
92
+ const G = (x0, y0, x1, y1) => gold(c, x0, y0, x1, y1, (sheen - 0.5) * 0.4);
93
+ rr(c, -hw, -hh, w, h, 16 * s); c.fillStyle = '#eadcbc'; c.fill();
94
+ c.save(); rr(c, -hw, -hh, w, h, 16 * s); c.clip();
95
+ c.strokeStyle = LINE; c.lineWidth = 2 * s; rr(c, -hw + 9 * s, -hh + 9 * s, w - 18 * s, h - 18 * s, 9 * s); c.stroke();
96
+ rr(c, -hw + 13 * s, -hh + 13 * s, w - 26 * s, h - 26 * s, 7 * s); c.fillStyle = '#4f6a63'; c.fill();
97
+ c.strokeStyle = G(-hw, -hh, hw, hh); c.lineWidth = 2.4 * s; rr(c, -hw + 15 * s, -hh + 15 * s, w - 30 * s, h - 30 * s, 6 * s); c.stroke();
98
+ c.fillStyle = G(-hw, -hh, hw, hh);
99
+ for (let y = -hh + 50 * s; y < hh - 40 * s; y += 22 * s) [-hw + 21 * s, hw - 21 * s].forEach((x) => { c.beginPath(); c.moveTo(x, y - 5 * s); c.lineTo(x + 3 * s, y); c.lineTo(x, y + 5 * s); c.lineTo(x - 3 * s, y); c.closePath(); c.fill(); });
100
+ // gold dot row along the top and bottom of the band, as on the hero card
101
+ for (let x = -hw + 52 * s; x < hw - 44 * s; x += 22 * s) [-hh + 21 * s, hh - 21 * s].forEach((y) => { c.beginPath(); c.arc(x, y, 1.7 * s, 0, TAU); c.fill(); });
102
+ rr(c, -hw + 28 * s, -hh + 28 * s, w - 56 * s, h - 56 * s, 4 * s); c.fillStyle = '#eadcbc'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 1.8 * s; c.stroke();
103
+ // art window with arch
104
+ const AX = hw - 44 * s, AT = -hh + 92 * s, AB = hh - 92 * s;
105
+ const win = () => { c.beginPath(); c.moveTo(-AX, AB); c.lineTo(-AX, AT + 30 * s); c.quadraticCurveTo(-AX, AT - 6 * s, -30 * s, AT); c.quadraticCurveTo(0, AT - 22 * s, 30 * s, AT); c.quadraticCurveTo(AX, AT - 6 * s, AX, AT + 30 * s); c.lineTo(AX, AB); c.closePath(); };
106
+ c.save(); win(); c.clip(); SKY.forEach((col, i) => { c.fillStyle = col; c.fillRect(-AX, AT - 30 * s + i * (AB - AT + 30 * s) / 5, AX * 2, (AB - AT + 30 * s) / 5 + 1); });
107
+ // the hero's sunburst: alternating wide/narrow wedges over the stepped sky, then scattered 4/8-point stars
108
+ {
109
+ const rcx = 0, rcy = (AT + AB) / 2 - 20 * s, RL = 520 * s;
110
+ c.save(); c.globalAlpha = 0.55;
111
+ for (let k = 0; k < 36; k++) { const a = k / 36 * TAU, ww = k % 2 ? 0.02 : 0.045; c.beginPath(); c.moveTo(rcx, rcy); c.lineTo(rcx + Math.cos(a - ww) * RL, rcy + Math.sin(a - ww) * RL); c.lineTo(rcx + Math.cos(a + ww) * RL, rcy + Math.sin(a + ww) * RL); c.closePath(); c.fillStyle = k % 2 ? '#7d6a55' : '#a78a52'; c.fill(); }
112
+ c.restore();
113
+ // the burst fades into a soft dark glow at its heart, so the card's emblem stands clear (the hero's halo does this)
114
+ const hg = c.createRadialGradient(rcx, rcy, 0, rcx, rcy, 125 * s); hg.addColorStop(0, 'rgba(50,52,84,0.95)'); hg.addColorStop(0.55, 'rgba(56,56,88,0.75)'); hg.addColorStop(1, 'rgba(62,58,90,0)');
115
+ c.fillStyle = hg; c.fillRect(rcx - 125 * s, rcy - 125 * s, 250 * s, 250 * s);
116
+ const sr = H.rng(31 + hashS(numeral + title)); c.fillStyle = '#d9bd6a';
117
+ for (let k = 0; k < 22; k++) { const x = sr.range(-AX + 8 * s, AX - 8 * s), y = sr.range(AT, AB - 20 * s), R = sr.range(3, 7) * s, eight = sr.chance(0.3); if (Math.hypot(x - rcx, y - rcy) < 120 * s) continue; star(c, x, y, R, 1.2 * s, eight ? 8 : 4); c.fill(); }
118
+ }
119
+ c.save(); c.translate(0, (AT + AB) / 2); art(c, AX * 2, AB - AT, s, G); c.restore(); c.restore();
120
+ win(); c.strokeStyle = LINE; c.lineWidth = 2.4 * s; c.stroke();
121
+ // numeral cartouche
122
+ c.beginPath(); c.ellipse(0, -hh + 56 * s, 52 * s, 20 * s, 0, 0, TAU); c.fillStyle = '#eadcbc'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 2 * s; c.stroke();
123
+ c.beginPath(); c.ellipse(0, -hh + 56 * s, 47 * s, 15.5 * s, 0, 0, TAU); c.strokeStyle = G(-47 * s, -hh + 40 * s, 47 * s, -hh + 72 * s); c.lineWidth = 1.4 * s; c.stroke();
124
+ // whiplash flourishes either side of the cartouche
125
+ [-1, 1].forEach((sg) => { c.beginPath(); c.moveTo(sg * 58 * s, -hh + 60 * s); c.bezierCurveTo(sg * 80 * s, -hh + 74 * s, sg * 100 * s, -hh + 46 * s, sg * 118 * s, -hh + 60 * s); c.bezierCurveTo(sg * 128 * s, -hh + 68 * s, sg * 124 * s, -hh + 78 * s, sg * 114 * s, -hh + 74 * s); c.strokeStyle = LINE; c.lineWidth = 3.6 * s; c.stroke(); c.strokeStyle = '#c9a54e'; c.lineWidth = 1.6 * s; c.stroke(); });
126
+ // corner celestial medallions
127
+ [[-1, -1, 0], [1, -1, 1], [-1, 1, 2], [1, 1, 3]].forEach(([a, b, kind]) => {
128
+ const x = a * (hw - 21 * s), y = b * (hh - 21 * s), R = 13 * s;
129
+ c.beginPath(); c.arc(x, y, R, 0, TAU); c.fillStyle = '#2d3a52'; c.fill(); c.strokeStyle = G(x - R, y - R, x + R, y + R); c.lineWidth = 2.4 * s; c.stroke();
130
+ c.fillStyle = '#d9bd6a';
131
+ if (kind === 0) { c.beginPath(); c.arc(x, y, 4.5 * s, 0, TAU); c.fill(); }
132
+ if (kind === 1) { crescent(c, x, y, 7 * s, 3.5 * s); c.fill(); }
133
+ if (kind === 2) { star(c, x, y, 9 * s, 2.6 * s, 4); c.fill(); }
134
+ if (kind === 3) { c.beginPath(); c.arc(x, y, 4 * s, 0, TAU); c.fill(); c.beginPath(); c.ellipse(x, y, 9 * s, 2.6 * s, -0.4, 0, TAU); c.strokeStyle = '#d9bd6a'; c.lineWidth = 1.2 * s; c.stroke(); }
135
+ });
136
+ c.fillStyle = LINE; c.font = `600 ${24 * s}px "Cormorant Garamond"`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(numeral, 0, -hh + 57 * s);
137
+ // name panel
138
+ rr(c, -hw + 34 * s, hh - 82 * s, w - 68 * s, 46 * s, 5 * s); c.fillStyle = '#efe3c6'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 2 * s; c.stroke();
139
+ rr(c, -hw + 39 * s, hh - 77 * s, w - 78 * s, 36 * s, 3 * s); c.strokeStyle = G(-hw, hh - 80 * s, hw, hh); c.lineWidth = 1.3 * s; c.stroke();
140
+ c.fillStyle = LINE; let fs = Math.max(25, 30 * s); c.font = `600 ${fs}px "Cormorant Garamond"`; while (c.measureText(title).width > w - 96 * s) { fs -= 1; c.font = `600 ${fs}px "Cormorant Garamond"`; }
141
+ c.fillText(title, 0, hh - 58 * s);
142
+ // age: yellowing + worn corners
143
+ c.globalCompositeOperation = 'multiply';
144
+ const yg = c.createRadialGradient(-hw * 0.2, -hh * 0.2, w * 0.3, 0, 0, Math.hypot(hw, hh)); yg.addColorStop(0, '#fff8e8'); yg.addColorStop(1, '#c8aa78');
145
+ c.fillStyle = yg; c.fillRect(-hw, -hh, w, h); c.globalCompositeOperation = 'source-over';
146
+ [[-1, -1], [1, -1], [-1, 1], [1, 1]].forEach(([a, b]) => { const g = c.createRadialGradient(a * hw, b * hh, 0, a * hw, b * hh, 14 * s); g.addColorStop(0, 'rgba(246,238,220,0.9)'); g.addColorStop(1, 'rgba(246,238,220,0)'); c.fillStyle = g; c.fillRect(a * hw - 14 * s, b * hh - 14 * s, 28 * s, 28 * s); });
147
+ // scuffs, foxing and worn edges, then the print tooth
148
+ ageLayers();
149
+ c.drawImage(AGE, -hw, -hh, w, h);
150
+ c.globalCompositeOperation = 'overlay'; c.globalAlpha = 0.55; c.drawImage(GRAIN, -hw, -hh, w, h); c.globalAlpha = 1;
151
+ // glossy foil: a soft band of light slides across with the room's sheen
152
+ c.globalCompositeOperation = 'lighter';
153
+ const gx = H.lerp(-hw - 200 * s, hw + 200 * s, H.clamp((sheen + 0.2) / 1.2));
154
+ const lg = c.createLinearGradient(gx - 80 * s, -hh * 0.4, gx + 80 * s, hh * 0.4); lg.addColorStop(0, 'rgba(255,240,200,0)'); lg.addColorStop(0.5, 'rgba(255,236,190,0.16)'); lg.addColorStop(1, 'rgba(255,240,200,0)');
155
+ c.fillStyle = lg; c.fillRect(-hw, -hh, w, h);
156
+ c.globalCompositeOperation = 'source-over';
157
+ c.restore();
158
+ }
159
+ // card placed in the scene: position, rotation, flip (0 = back up, 1 = face up), lift
160
+ function tCard(ctx, x, y, w, h, rot, flip, faceFn, lift = 0, seed = 7) {
161
+ const fe = H.ease.inOut(H.clamp(flip)), sx = Math.max(0.02, Math.abs(Math.cos(fe * Math.PI))), up = Math.sin(fe * Math.PI) + lift;
162
+ ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
163
+ ctx.save(); ctx.filter = `blur(${10 + up * 12}px)`; ctx.fillStyle = `rgba(0,0,0,${0.62 - up * 0.2})`; ctx.scale(sx, 1); rr(ctx, -w / 2 + 8 + up * 24, -h / 2 + 14 + up * 26, w, h, 14); ctx.fill(); ctx.restore();
164
+ ctx.scale(sx * (1 + up * 0.05), 1 + up * 0.05);
165
+ if (fe < 0.5) cardBack(ctx, w, h, seed); else faceFn(ctx);
166
+ ctx.restore();
167
+ }
168
+ function plaque(ctx, x, y, w, h, lines, sizes, sheen = 0.4) {
169
+ ctx.save(); ctx.translate(x, y);
170
+ ctx.save(); ctx.filter = 'blur(10px)'; ctx.fillStyle = 'rgba(0,0,0,0.6)'; rr(ctx, -w / 2 + 6, -h / 2 + 12, w, h, 12); ctx.fill(); ctx.restore();
171
+ rr(ctx, -w / 2, -h / 2, w, h, 12); ctx.fillStyle = '#1d2744'; ctx.fill();
172
+ ctx.strokeStyle = gold(ctx, -w / 2, -h / 2, w / 2, h / 2, (sheen - 0.5) * 0.4); ctx.lineWidth = 3; rr(ctx, -w / 2 + 7, -h / 2 + 7, w - 14, h - 14, 8); ctx.stroke(); ctx.lineWidth = 1; rr(ctx, -w / 2 + 12, -h / 2 + 12, w - 24, h - 24, 6); ctx.stroke();
173
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
174
+ const tot = sizes.reduce((a, b) => a + b * 1.1, 0); let yy = -tot / 2;
175
+ lines.forEach((l, i) => { const fs = sizes[i]; yy += fs * 0.55; ctx.font = `600 ${fs}px "Cormorant Garamond"`; ctx.fillStyle = gold(ctx, -w / 2, yy - fs / 2, w / 2, yy + fs / 2, (sheen - 0.5) * 0.4); ctx.fillText(l, 0, yy); yy += fs * 0.55; });
176
+ ctx.restore();
177
+ }
178
+ // gold filigree arrow drawn on along a quadratic
179
+ function gArrow(ctx, a, m, b, p, label) {
180
+ if (p <= 0) return;
181
+ const pts = []; const n = 40, k = Math.max(2, Math.round(n * Math.min(1, p)));
182
+ for (let i = 0; i <= k; i++) { const t = i / n, u = 1 - t; pts.push([u * u * a[0] + 2 * u * t * m[0] + t * t * b[0], u * u * a[1] + 2 * u * t * m[1] + t * t * b[1]]); }
183
+ ctx.save(); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
184
+ const path = () => { ctx.beginPath(); pts.forEach((q, i) => i ? ctx.lineTo(q[0], q[1]) : ctx.moveTo(q[0], q[1])); };
185
+ path(); ctx.strokeStyle = LINE; ctx.lineWidth = 7; ctx.stroke(); path(); ctx.strokeStyle = gold(ctx, a[0], a[1], b[0], b[1]); ctx.lineWidth = 3.4; ctx.stroke();
186
+ // little spiral flourishes along the shaft
187
+ [0.3, 0.6].forEach((f) => { const i = Math.floor(f * n); if (i >= pts.length - 1) return; const q = pts[i]; ctx.beginPath(); ctx.arc(q[0], q[1] - 8, 6, Math.PI * 0.5, Math.PI * 2.2); ctx.strokeStyle = '#d9bd6a'; ctx.lineWidth = 1.8; ctx.stroke(); });
188
+ if (p >= 1) { const q = pts[pts.length - 1], o = pts[pts.length - 3], an = Math.atan2(q[1] - o[1], q[0] - o[0]); ctx.beginPath(); ctx.moveTo(q[0] + Math.cos(an) * 10, q[1] + Math.sin(an) * 10); ctx.lineTo(q[0] + Math.cos(an + 2.5) * 16, q[1] + Math.sin(an + 2.5) * 16); ctx.lineTo(q[0] - Math.cos(an) * 4, q[1] - Math.sin(an) * 4); ctx.lineTo(q[0] + Math.cos(an - 2.5) * 16, q[1] + Math.sin(an - 2.5) * 16); ctx.closePath(); ctx.fillStyle = '#d9bd6a'; ctx.fill(); ctx.strokeStyle = LINE; ctx.lineWidth = 1.5; ctx.stroke(); }
189
+ if (label && p > 0.6) { ctx.font = 'italic 600 28px "Cormorant Garamond"'; ctx.textAlign = 'center'; ctx.fillStyle = '#e8cf7a'; ctx.fillText(label, m[0], m[1] + (m[1] > a[1] ? 34 : -12)); }
190
+ ctx.restore();
191
+ }
192
+ // flat art-nouveau animal heads
193
+ function tCat(c, x, y, s) {
194
+ c.save(); c.translate(x, y); c.scale(s, s); c.lineJoin = 'round'; c.strokeStyle = LINE; c.lineWidth = 2.6;
195
+ c.beginPath(); c.moveTo(-26, -6); c.lineTo(-20, -40); c.lineTo(-4, -22); c.lineTo(4, -22); c.lineTo(20, -40); c.lineTo(26, -6); c.bezierCurveTo(30, 24, -30, 24, -26, -6); c.closePath(); c.fillStyle = '#c99a4e'; c.fill(); c.stroke();
196
+ c.fillStyle = '#6fa39a'; [-1, 1].forEach((sg) => { c.beginPath(); c.ellipse(sg * 10, -4, 5, 6, 0, 0, TAU); c.fill(); c.stroke(); c.fillStyle = LINE; c.fillRect(sg * 10 - 1, -9, 2, 10); c.fillStyle = '#6fa39a'; });
197
+ c.beginPath(); c.moveTo(-3, 6); c.lineTo(3, 6); c.lineTo(0, 10); c.closePath(); c.fillStyle = '#a2615b'; c.fill(); c.stroke();
198
+ c.lineWidth = 1.2; [-1, 1].forEach((sg) => { c.beginPath(); c.moveTo(sg * 6, 10); c.lineTo(sg * 30, 8); c.moveTo(sg * 6, 12); c.lineTo(sg * 28, 16); c.stroke(); });
199
+ c.restore();
200
+ }
201
+ function tDog(c, x, y, s) {
202
+ c.save(); c.translate(x, y); c.scale(s, s); c.lineJoin = 'round'; c.strokeStyle = LINE; c.lineWidth = 2.6;
203
+ c.beginPath(); c.ellipse(0, 0, 24, 24, 0, 0, TAU); c.fillStyle = '#a8784e'; c.fill(); c.stroke();
204
+ [-1, 1].forEach((sg) => { c.beginPath(); c.ellipse(sg * 26, 4, 9, 20, sg * 0.25, 0, TAU); c.fillStyle = '#6b4a30'; c.fill(); c.stroke(); });
205
+ c.beginPath(); c.ellipse(0, 12, 12, 9, 0, 0, TAU); c.fillStyle = '#e2cfa8'; c.fill(); c.stroke();
206
+ c.beginPath(); c.ellipse(0, 8, 5, 3.5, 0, 0, TAU); c.fillStyle = LINE; c.fill();
207
+ [-1, 1].forEach((sg) => { c.beginPath(); c.arc(sg * 9, -6, 3.5, 0, TAU); c.fillStyle = LINE; c.fill(); });
208
+ c.restore();
209
+ }
210
+
211
+ // ===== END EXPLAINER HELPERS =====
212
+ registerStyle({
213
+ id: 'tarot-card',
214
+ name: 'Tarot Card',
215
+ category: 'Illustration',
216
+ description: 'An aged art-nouveau tarot trump on crushed velvet: bold even outlines, flat muted lithograph colours, gold-foil halos and borders, celestial symbols and a worn, foxed cardstock with frayed corners.',
217
+ motion: 'The card is dealt and flips onto the velvet; a sheen of light slides across the gold foil while the orbiting planets drift around the automaton.',
218
+ fonts: ['600 34px "Cormorant Garamond"', '600 22px "Cormorant Garamond"', '400 14px "Cormorant Garamond"'],
219
+ frameT: 5.4,
220
+ duration: 5.5,
221
+ readsPixels: true,
222
+ draw(ctx, W, H_, t) {
223
+ const nz = H.makeNoise(9), r = H.rng(21);
224
+ // ================= velvet =================
225
+ {
226
+ const vw = 320, vh = 180, v = H.layer(vw, vh), vc = v.getContext('2d'), img = vc.createImageData(vw, vh), d = img.data;
227
+ const dark = [18, 5, 14], mid = [74, 18, 44], hi = [150, 66, 98];
228
+ for (let y = 0; y < vh; y++) for (let x = 0; x < vw; x++) {
229
+ const X = x * 4, Y = y * 4;
230
+ const warp = nz.fbm2(X * 0.0018, Y * 0.0018, 3) * 2.4;
231
+ const f = Math.sin(X * 0.0062 - Y * 0.0035 + warp) * 0.65 + Math.sin(X * 0.0024 + Y * 0.0078 + warp * 1.4) * 0.35;
232
+ const ridge = Math.pow(1 - Math.abs(f), 1.6);
233
+ const mott = nz.fbm2(X * 0.02, Y * 0.02, 3);
234
+ const spot = Math.exp(-(((X - 640) / 700) ** 2 + ((Y - 360) / 460) ** 2));
235
+ let b = 0.1 + 0.55 * ridge * ridge * (0.5 + 0.5 * spot) + 0.14 * spot + 0.1 * mott;
236
+ b = H.clamp(b);
237
+ const c = b < 0.5 ? H.mix(dark, mid, b * 2) : H.mix(mid, hi, (b - 0.5) * 2);
238
+ const i = (y * vw + x) * 4; d[i] = c[0]; d[i + 1] = c[1]; d[i + 2] = c[2]; d[i + 3] = 255;
239
+ }
240
+ vc.putImageData(img, 0, 0);
241
+ ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
242
+ ctx.drawImage(v, 0, 0, W, H_);
243
+ H.grain(ctx, W, H_, 0.07, 31, false);
244
+ }
245
+
246
+ // ================= peeking card backs =================
247
+ [[470, 400, -0.3, 5, 0.1], [812, 392, 0.26, 6, 0.3]].forEach(([x, y, a0, s, tk]) => {
248
+ const e = H.ease.out(H.clamp((t - tk) / 0.6)); if (e <= 0) return;
249
+ const a = a0 * (0.3 + 0.7 * e);
250
+ ctx.save(); ctx.translate(H.lerp(640, x, e), y + (1 - e) * 520); ctx.rotate(a);
251
+ ctx.save(); ctx.filter = 'blur(12px)'; ctx.fillStyle = 'rgba(0,0,0,0.6)'; rr(ctx, -170 + 10, -300 + 16, 340, 600, 16); ctx.fill(); ctx.restore();
252
+ cardBack(ctx, 340, 600, s);
253
+ const sh = ctx.createLinearGradient(-170, 0, 170, 0); sh.addColorStop(a < 0 ? 0 : 1, 'rgba(10,0,8,0.38)'); sh.addColorStop(a < 0 ? 1 : 0, 'rgba(10,0,8,0.72)'); ctx.fillStyle = sh; rr(ctx, -170, -300, 340, 600, 16); ctx.fill();
254
+ ctx.restore();
255
+ });
256
+
257
+ // ================= the card (drawn on its own layer) =================
258
+ const CW = 364, CH = 628, L = H.layer(CW + 40, CH + 40), c = L.getContext('2d');
259
+ c.translate((CW + 40) / 2, (CH + 40) / 2);
260
+ const hw = CW / 2, hh = CH / 2;
261
+ c.lineJoin = 'round'; c.lineCap = 'round';
262
+ // card stock
263
+ rr(c, -hw, -hh, CW, CH, 18); c.fillStyle = '#eadcbc'; c.fill();
264
+ c.save(); rr(c, -hw, -hh, CW, CH, 18); c.clip();
265
+ const sweep = 0.4 + 0.9 * Math.sin(Math.PI * H.smooth(2.3, 4.3, t));
266
+ const G = (x0, y0, x1, y1) => gold(c, x0, y0, x1, y1, (sweep - 0.5) * 0.4);
267
+ // outer keyline + frame band
268
+ c.strokeStyle = LINE; c.lineWidth = 2; rr(c, -hw + 10, -hh + 10, CW - 20, CH - 20, 10); c.stroke();
269
+ rr(c, -hw + 14, -hh + 14, CW - 28, CH - 28, 8); c.fillStyle = '#4f6a63'; c.fill();
270
+ c.strokeStyle = G(-hw, -hh, hw, hh); c.lineWidth = 2.4; rr(c, -hw + 16, -hh + 16, CW - 32, CH - 32, 7); c.stroke();
271
+ // band ornaments: gold lozenges
272
+ c.fillStyle = G(-hw, -hh, hw, hh);
273
+ for (let y = -hh + 60; y < hh - 50; y += 22) { [-hw + 23, hw - 23].forEach((x) => { c.beginPath(); c.moveTo(x, y - 6); c.lineTo(x + 3.5, y); c.lineTo(x, y + 6); c.lineTo(x - 3.5, y); c.closePath(); c.fill(); }); }
274
+ for (let x = -hw + 60; x < hw - 50; x += 22) { [-hh + 23, hh - 23].forEach((y) => { c.beginPath(); c.arc(x, y, 1.8, 0, TAU); c.fill(); }); }
275
+ // inner field
276
+ rr(c, -hw + 30, -hh + 30, CW - 60, CH - 60, 4); c.fillStyle = '#eadcbc'; c.fill();
277
+ c.strokeStyle = LINE; c.lineWidth = 2; c.stroke();
278
+ // art window with nouveau arch
279
+ const AX = 136, AT = -226, AB = 200;
280
+ const win = () => { c.beginPath(); c.moveTo(-AX, AB); c.lineTo(-AX, AT + 40); c.bezierCurveTo(-AX, AT - 6, -60, AT - 4, -34, AT + 6); c.quadraticCurveTo(0, AT - 26, 34, AT + 6); c.bezierCurveTo(60, AT - 4, AX, AT - 6, AX, AT + 40); c.lineTo(AX, AB); c.closePath(); };
281
+ c.save(); win(); c.clip();
282
+ // sky: flat stepped bands
283
+ ['#2c3350', '#353a58', '#41405f', '#514765', '#62506a'].forEach((col, i) => { c.fillStyle = col; c.fillRect(-AX, AT - 30 + i * 92, AX * 2, 94); });
284
+ // rays
285
+ const HC = [0, -52];
286
+ c.save(); c.globalAlpha = 0.55;
287
+ for (let k = 0; k < 36; k++) { const a = k / 36 * TAU + t * 0.03, w = k % 2 ? 0.02 : 0.045; c.beginPath(); c.moveTo(HC[0], HC[1]); c.lineTo(HC[0] + Math.cos(a - w) * 420, HC[1] + Math.sin(a - w) * 420); c.lineTo(HC[0] + Math.cos(a + w) * 420, HC[1] + Math.sin(a + w) * 420); c.closePath(); c.fillStyle = k % 2 ? '#7d6a55' : '#a78a52'; c.fill(); }
288
+ c.restore();
289
+ // stars
290
+ for (let k = 0; k < 26; k++) { const x = r.range(-AX + 8, AX - 8), y = r.range(AT, 60); if (Math.hypot(x - HC[0], y - HC[1]) < 130) continue; const tw = 0.7 + 0.3 * Math.sin(t * 3 + k); star(c, x, y, r.range(3, 7) * tw, 1.2, r.chance(0.3) ? 8 : 4); c.fillStyle = '#d9bd6a'; c.fill(); }
291
+ // crescent moon at top of arch
292
+ crescent(c, 0, AT + 20, 15, 7); c.fillStyle = G(-15, AT, 15, AT + 40); c.fill(); c.strokeStyle = LINE; c.lineWidth = 1.6; c.stroke();
293
+ // halo gear
294
+ {
295
+ const R0 = 116, n = 28;
296
+ c.beginPath();
297
+ for (let k = 0; k < n; k++) { const a = k / n * TAU - t * 0.08, p = TAU / n; [[a - p * 0.5, R0 - 7], [a - p * 0.28, R0 - 7], [a - p * 0.2, R0 + 5], [a + p * 0.2, R0 + 5], [a + p * 0.28, R0 - 7]].forEach(([aa, q], j) => { const x = HC[0] + Math.cos(aa) * q, y = HC[1] + Math.sin(aa) * q; (k || j) ? c.lineTo(x, y) : c.moveTo(x, y); }); }
298
+ c.closePath(); c.fillStyle = G(-R0, HC[1] - R0, R0, HC[1] + R0); c.fill(); c.strokeStyle = LINE; c.lineWidth = 2; c.stroke();
299
+ c.beginPath(); c.arc(HC[0], HC[1], 102, 0, TAU); c.fillStyle = '#2d3a52'; c.fill(); c.stroke();
300
+ c.fillStyle = '#d9bd6a'; for (let k = 0; k < 40; k++) { const a = k / 40 * TAU; c.beginPath(); c.arc(HC[0] + Math.cos(a) * 95, HC[1] + Math.sin(a) * 95, 1.8, 0, TAU); c.fill(); }
301
+ c.beginPath(); c.arc(HC[0], HC[1], 88, 0, TAU); c.fillStyle = G(-88, HC[1] - 88, 88, HC[1] + 88); c.fill(); c.strokeStyle = LINE; c.lineWidth = 1.8; c.stroke();
302
+ c.strokeStyle = 'rgba(60,40,15,0.55)'; c.lineWidth = 1;
303
+ for (let k = 0; k < 48; k++) { const a = k / 48 * TAU; c.beginPath(); c.moveTo(HC[0] + Math.cos(a) * 64, HC[1] + Math.sin(a) * 64); c.lineTo(HC[0] + Math.cos(a) * 86, HC[1] + Math.sin(a) * 86); c.stroke(); }
304
+ c.beginPath(); c.arc(HC[0], HC[1], 64, 0, TAU); c.strokeStyle = LINE; c.lineWidth = 1.5; c.stroke();
305
+ // zodiac-like ticks
306
+ c.fillStyle = 'rgba(60,40,15,0.7)'; for (let k = 0; k < 12; k++) { const a = k / 12 * TAU; star(c, HC[0] + Math.cos(a) * 75, HC[1] + Math.sin(a) * 75, 4, 1.2, 4, a); c.fill(); }
307
+ }
308
+ // orbit ring (back half)
309
+ const orbit = (front) => {
310
+ c.save(); c.translate(0, 70); c.rotate(-0.16);
311
+ c.beginPath(); c.ellipse(0, 0, 150, 30, 0, front ? 0 : Math.PI, front ? Math.PI : TAU); c.strokeStyle = LINE; c.lineWidth = 4.6; c.stroke(); c.strokeStyle = '#d4b45e'; c.lineWidth = 2.2; c.stroke();
312
+ [0.3, 1.9, 3.6, 5.0].forEach((a0, i) => {
313
+ const a = (a0 + t * 0.4) % TAU; const fr = Math.sin(a) > 0; if (fr !== front) return;
314
+ const x = Math.cos(a) * 150, y = Math.sin(a) * 30, R = [9, 6, 7, 5][i];
315
+ c.beginPath(); c.arc(x, y, R, 0, TAU); c.fillStyle = ['#b65a3c', '#6d9a90', '#d9bd6a', '#9c86a8'][i]; c.fill(); c.strokeStyle = LINE; c.lineWidth = 1.6; c.stroke();
316
+ if (i === 0) { c.beginPath(); c.ellipse(x, y, 15, 4, -0.3, 0, TAU); c.stroke(); }
317
+ });
318
+ c.restore();
319
+ };
320
+ orbit(false);
321
+ // cables (hair) behind figure
322
+ const GL = '#d9bd6a';
323
+ const cablesBack = [
324
+ [[-24, -104], [-80, -150], [-150, -90], [-112, -20], [-80, 40], [-150, 80], [-120, 150]],
325
+ [[24, -104], [80, -150], [150, -90], [112, -20], [80, 40], [150, 80], [120, 150]],
326
+ [[-14, -110], [-40, -170], [-110, -170], [-128, -120]],
327
+ [[14, -110], [40, -170], [110, -170], [128, -120]],
328
+ ];
329
+ cablesBack.forEach((p, i) => cable(c, p, 10, ['#a8573a', '#a8573a', '#3f6e6c', '#3f6e6c'][i], GL));
330
+ // shoulders / mantle
331
+ c.beginPath(); c.moveTo(-18, 52); c.bezierCurveTo(-50, 64, -110, 76, -132, 120); c.lineTo(-150, 230); c.lineTo(150, 230); c.lineTo(132, 120); c.bezierCurveTo(110, 76, 50, 64, 18, 52); c.closePath();
332
+ c.fillStyle = '#5c7a72'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 2.4; c.stroke();
333
+ // shoulder plates
334
+ [-1, 1].forEach((s) => { c.beginPath(); c.moveTo(s * 46, 70); c.bezierCurveTo(s * 90, 66, s * 130, 90, s * 134, 132); c.bezierCurveTo(s * 110, 120, s * 80, 112, s * 52, 112); c.closePath(); c.fillStyle = '#7f9a8f'; c.fill(); c.stroke();
335
+ c.beginPath(); c.moveTo(s * 60, 86); c.bezierCurveTo(s * 92, 84, s * 116, 98, s * 124, 116); c.strokeStyle = 'rgba(42,28,21,0.6)'; c.lineWidth = 1.2; c.stroke(); c.strokeStyle = LINE; c.lineWidth = 2.4; });
336
+ // flat shadow on mantle
337
+ c.save(); c.beginPath(); c.moveTo(20, 112); c.bezierCurveTo(60, 140, 90, 170, 100, 230); c.lineTo(150, 230); c.lineTo(134, 132); c.closePath(); c.fillStyle = 'rgba(30,50,48,0.35)'; c.fill(); c.restore();
338
+ // gold collar
339
+ c.beginPath(); c.moveTo(-58, 66); c.quadraticCurveTo(0, 128, 58, 66); c.lineTo(48, 62); c.quadraticCurveTo(0, 112, -48, 62); c.closePath(); c.fillStyle = G(-60, 60, 60, 120); c.fill(); c.lineWidth = 1.8; c.stroke();
340
+ // chest core
341
+ { const cc = [0, 150]; for (let k = 0; k < 16; k++) { const a = k / 16 * TAU; c.beginPath(); c.moveTo(cc[0] + Math.cos(a) * 24, cc[1] + Math.sin(a) * 24); c.lineTo(cc[0] + Math.cos(a) * (k % 2 ? 32 : 40), cc[1] + Math.sin(a) * (k % 2 ? 32 : 40)); c.strokeStyle = '#d9bd6a'; c.lineWidth = 2; c.stroke(); }
342
+ c.beginPath(); c.arc(cc[0], cc[1], 22, 0, TAU); c.fillStyle = G(-22, 128, 22, 172); c.fill(); c.strokeStyle = LINE; c.lineWidth = 2; c.stroke();
343
+ c.beginPath(); c.arc(cc[0], cc[1], 13, 0, TAU); c.fillStyle = '#6fa39a'; c.fill(); c.stroke();
344
+ c.beginPath(); c.arc(cc[0] - 4, cc[1] - 4, 4, 0, TAU); c.fillStyle = 'rgba(255,255,240,0.7)'; c.fill(); }
345
+ // neck (ribbed)
346
+ c.beginPath(); c.rect(-16, 8, 32, 58); c.fillStyle = '#a3aca6'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 2; c.stroke();
347
+ c.lineWidth = 1.2; for (let y = 16; y < 62; y += 7) { c.beginPath(); c.moveTo(-16, y); c.quadraticCurveTo(0, y + 3, 16, y); c.stroke(); }
348
+ c.fillStyle = 'rgba(40,50,48,0.25)'; c.fillRect(4, 8, 12, 58);
349
+ // face
350
+ const face = () => { c.beginPath(); c.moveTo(0, -106); c.bezierCurveTo(32, -106, 47, -82, 46, -48); c.bezierCurveTo(46, -14, 26, 16, 0, 24); c.bezierCurveTo(-26, 16, -46, -14, -46, -48); c.bezierCurveTo(-47, -82, -32, -106, 0, -106); c.closePath(); };
351
+ face(); c.fillStyle = '#dcd9cd'; c.fill();
352
+ c.save(); face(); c.clip();
353
+ c.beginPath(); c.moveTo(14, -100); c.bezierCurveTo(40, -70, 40, -30, 30, 0); c.bezierCurveTo(24, 14, 10, 22, 0, 26); c.lineTo(60, 30); c.lineTo(60, -110); c.closePath(); c.fillStyle = '#bebcb2'; c.fill();
354
+ // cheek blush (flat)
355
+ c.fillStyle = 'rgba(190,120,110,0.25)'; c.beginPath(); c.ellipse(-26, -18, 10, 6, 0, 0, TAU); c.fill(); c.beginPath(); c.ellipse(26, -18, 10, 6, 0, 0, TAU); c.fill();
356
+ c.restore();
357
+ face(); c.strokeStyle = LINE; c.lineWidth = 2.6; c.stroke();
358
+ // faceplate seams + rivets
359
+ c.lineWidth = 1.1; c.strokeStyle = 'rgba(42,28,21,0.75)';
360
+ [-1, 1].forEach((s) => { c.beginPath(); c.moveTo(s * 41, -70); c.bezierCurveTo(s * 34, -40, s * 36, -12, s * 22, 12); c.stroke(); c.fillStyle = LINE; c.beginPath(); c.arc(s * 38, -58, 1.6, 0, TAU); c.fill(); c.beginPath(); c.arc(s * 33, -6, 1.6, 0, TAU); c.fill(); });
361
+ // closed eyes, brows, nose, lips
362
+ c.strokeStyle = LINE; c.lineWidth = 2.2;
363
+ [-1, 1].forEach((s) => {
364
+ c.beginPath(); c.moveTo(s * 32, -44); c.quadraticCurveTo(s * 21, -36, s * 9, -43); c.stroke();
365
+ c.lineWidth = 1.2; for (let k = 0; k < 4; k++) { const x = s * (12 + k * 6), y = -40 - Math.sin((k + 0.5) / 4 * Math.PI) * 1.8; c.beginPath(); c.moveTo(x, y); c.lineTo(x + s * 1.5, y + 4); c.stroke(); }
366
+ c.lineWidth = 1.8; c.beginPath(); c.moveTo(s * 36, -58); c.quadraticCurveTo(s * 22, -66, s * 8, -59); c.stroke(); c.lineWidth = 2.2;
367
+ });
368
+ c.lineWidth = 1.6; c.beginPath(); c.moveTo(-1, -40); c.quadraticCurveTo(-4, -22, -6, -12); c.quadraticCurveTo(-1, -8, 5, -11); c.stroke();
369
+ c.beginPath(); c.moveTo(-12, 3); c.quadraticCurveTo(-5, -1, 0, 2); c.quadraticCurveTo(5, -1, 12, 3); c.quadraticCurveTo(0, 12, -12, 3); c.closePath(); c.fillStyle = '#a2615b'; c.fill(); c.stroke();
370
+ // headdress band with third-eye jewel
371
+ c.beginPath(); c.moveTo(-46, -78); c.bezierCurveTo(-30, -92, 30, -92, 46, -78); c.lineTo(44, -68); c.bezierCurveTo(30, -80, -30, -80, -44, -68); c.closePath(); c.fillStyle = G(-46, -92, 46, -68); c.fill(); c.lineWidth = 1.8; c.stroke();
372
+ c.beginPath(); c.moveTo(0, -96); c.lineTo(9, -80); c.lineTo(0, -64); c.lineTo(-9, -80); c.closePath(); c.fillStyle = '#5f9e94'; c.fill(); c.stroke();
373
+ c.beginPath(); c.arc(-2, -84, 2, 0, TAU); c.fillStyle = 'rgba(255,255,240,0.8)'; c.fill();
374
+ // cranium cap
375
+ c.beginPath(); c.moveTo(-44, -82); c.bezierCurveTo(-46, -130, 46, -130, 44, -82); c.bezierCurveTo(30, -92, -30, -92, -44, -82); c.closePath(); c.fillStyle = '#4c6b66'; c.fill(); c.lineWidth = 2.4; c.stroke();
376
+ c.lineWidth = 1.1; [-20, 0, 20].forEach((x) => { c.beginPath(); c.moveTo(x, -90); c.quadraticCurveTo(x * 1.1, -108, x * 0.6, -118); c.stroke(); });
377
+ // front cables (whiplash over shoulders)
378
+ const cablesFront = [
379
+ [[-36, -96], [-74, -60], [-70, 10], [-56, 60], [-44, 100], [-90, 140], [-70, 186]],
380
+ [[36, -96], [74, -60], [70, 10], [56, 60], [44, 100], [90, 140], [70, 186]],
381
+ ];
382
+ cablesFront.forEach((p, i) => cable(c, p, 9, '#b98f3e', '#efe0a2'));
383
+ // plugs at cable ends
384
+ [[-70, 186], [70, 186], [-120, 150], [120, 150]].forEach(([x, y]) => { c.beginPath(); c.rect(x - 7, y - 4, 14, 14); c.fillStyle = G(x - 7, y, x + 7, y + 10); c.fill(); c.strokeStyle = LINE; c.lineWidth = 1.6; c.stroke(); c.beginPath(); c.moveTo(x - 3, y + 10); c.lineTo(x - 3, y + 17); c.moveTo(x + 3, y + 10); c.lineTo(x + 3, y + 17); c.stroke(); });
385
+ orbit(true);
386
+ // nouveau clouds at bottom
387
+ const cloud = (cx, cy, s, col) => { c.beginPath(); [[-60, 0, 26], [-30, -14, 30], [6, -20, 32], [42, -10, 26], [66, 4, 20]].forEach(([x, y, R]) => { c.moveTo(cx + x * s + R * s, cy + y * s); c.arc(cx + x * s, cy + y * s, R * s, 0, TAU); }); c.rect(cx - 86 * s, cy, 172 * s, 40 * s); c.fillStyle = col; c.fill(); };
388
+ cloud(-80, 196, 0.95, '#8e7f98'); cloud(86, 202, 0.9, '#8e7f98'); cloud(0, 214, 1.05, '#a596a8');
389
+ c.strokeStyle = LINE; c.lineWidth = 1.6;
390
+ [[-80, 196, 0.95], [86, 202, 0.9], [0, 214, 1.05]].forEach(([cx, cy, s]) => { [[-30, -14, 30], [6, -20, 32], [42, -10, 26]].forEach(([x, y, R]) => { c.beginPath(); c.arc(cx + x * s, cy + y * s, R * s, Math.PI * 1.05, Math.PI * 1.85); c.stroke(); c.beginPath(); c.arc(cx + x * s + 4, cy + y * s + 4, R * s * 0.55, Math.PI * 1.1, Math.PI * 1.7); c.stroke(); }); });
391
+ c.restore(); // end art clip
392
+ win(); c.strokeStyle = LINE; c.lineWidth = 2.6; c.stroke();
393
+ c.save(); c.translate(0, 0); win(); c.strokeStyle = G(-AX, AT, AX, AB); c.lineWidth = 1.2; c.restore();
394
+ // whiplash flourishes around the arch top
395
+ [-1, 1].forEach((s) => {
396
+ c.beginPath(); c.moveTo(s * 40, AT - 4); c.bezierCurveTo(s * 70, AT - 26, s * 100, AT - 4, s * 120, AT - 22); c.bezierCurveTo(s * 132, AT - 32, s * 124, AT - 44, s * 112, AT - 38);
397
+ c.strokeStyle = LINE; c.lineWidth = 4.4; c.stroke(); c.strokeStyle = '#c9a54e'; c.lineWidth = 2; c.stroke();
398
+ c.beginPath(); c.arc(s * 112, AT - 38, 3.2, 0, TAU); c.fillStyle = '#c9a54e'; c.fill();
399
+ });
400
+ // numeral cartouche
401
+ c.save();
402
+ c.beginPath(); c.ellipse(0, -hh + 64, 46, 22, 0, 0, TAU); c.fillStyle = '#eadcbc'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 2; c.stroke();
403
+ c.beginPath(); c.ellipse(0, -hh + 64, 41, 17.5, 0, 0, TAU); c.strokeStyle = G(-40, -hh + 46, 40, -hh + 82); c.lineWidth = 1.6; c.stroke();
404
+ c.fillStyle = LINE; c.font = '600 30px "Cormorant Garamond"'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText('XXI', 0, -hh + 65);
405
+ c.restore();
406
+ // name panel
407
+ c.save();
408
+ rr(c, -128, AB + 18, 256, 48, 6); c.fillStyle = '#efe3c6'; c.fill(); c.strokeStyle = LINE; c.lineWidth = 2; c.stroke();
409
+ rr(c, -122, AB + 23, 244, 38, 4); c.strokeStyle = G(-122, AB, 122, AB + 60); c.lineWidth = 1.4; c.stroke();
410
+ c.fillStyle = LINE; c.font = '600 25px "Cormorant Garamond"'; c.textAlign = 'center'; c.textBaseline = 'middle';
411
+ const name = 'THE MACHINE'; c.fillText(name.split('').join(String.fromCharCode(8202)), 0, AB + 43);
412
+ [-1, 1].forEach((s) => { star(c, s * 110, AB + 42, 6, 1.8, 4); c.fillStyle = '#b8954a'; c.fill(); });
413
+ c.restore();
414
+ // corner celestial medallions
415
+ const med = (x, y, kind) => {
416
+ c.beginPath(); c.arc(x, y, 15, 0, TAU); c.fillStyle = '#2d3a52'; c.fill(); c.strokeStyle = G(x - 15, y - 15, x + 15, y + 15); c.lineWidth = 2.6; c.stroke();
417
+ c.fillStyle = '#d9bd6a'; c.strokeStyle = '#d9bd6a'; c.lineWidth = 1.4;
418
+ if (kind === 0) { c.beginPath(); c.arc(x, y, 5, 0, TAU); c.fill(); for (let k = 0; k < 8; k++) { const a = k / 8 * TAU; c.beginPath(); c.moveTo(x + Math.cos(a) * 7, y + Math.sin(a) * 7); c.lineTo(x + Math.cos(a) * 10.5, y + Math.sin(a) * 10.5); c.stroke(); } }
419
+ if (kind === 1) { crescent(c, x, y, 8, 4); c.fill(); }
420
+ if (kind === 2) { star(c, x, y, 10, 3, 4); c.fill(); }
421
+ if (kind === 3) { c.beginPath(); c.arc(x, y, 4.5, 0, TAU); c.fill(); c.beginPath(); c.ellipse(x, y, 10, 3, -0.4, 0, TAU); c.stroke(); }
422
+ };
423
+ med(-hw + 23, -hh + 23, 0); med(hw - 23, -hh + 23, 1); med(-hw + 23, hh - 23, 2); med(hw - 23, hh - 23, 3);
424
+ // side vines (thin nouveau linework in the margin)
425
+ [-1, 1].forEach((s) => {
426
+ c.beginPath(); for (let k = 0; k <= 60; k++) { const y = -hh + 110 + k * (CH - 220) / 60, x = s * (AX + 7) + s * Math.sin(k * 0.55) * 3; k ? c.lineTo(x, y) : c.moveTo(x, y); }
427
+ c.strokeStyle = 'rgba(42,28,21,0.8)'; c.lineWidth = 1; c.stroke();
428
+ for (let k = 0; k < 9; k++) { const y = -hh + 140 + k * 46; c.beginPath(); c.ellipse(s * (AX + 9), y, 2.4, 5, s * 0.5, 0, TAU); c.fillStyle = '#7f9a8f'; c.fill(); c.stroke(); }
429
+ });
430
+ c.restore(); // end card clip
431
+
432
+ // ---------- aging on the card layer ----------
433
+ c.save(); rr(c, -hw, -hh, CW, CH, 18); c.clip();
434
+ // overall yellowing + grime toward edges
435
+ c.globalCompositeOperation = 'multiply';
436
+ const yg = c.createRadialGradient(-30, -60, 120, 0, 0, 420); yg.addColorStop(0, 'rgba(255,248,230,1)'); yg.addColorStop(1, 'rgba(200,170,120,1)');
437
+ c.fillStyle = yg; c.fillRect(-hw, -hh, CW, CH);
438
+ // stains
439
+ const sr = H.rng(4);
440
+ for (let k = 0; k < 6; k++) { const x = sr.range(-hw, hw), y = sr.range(-hh, hh), R = sr.range(10, 40); const g = c.createRadialGradient(x, y, 0, x, y, R); g.addColorStop(0, 'rgba(190,150,100,0.0)'); g.addColorStop(0.85, 'rgba(170,125,70,0.22)'); g.addColorStop(1, 'rgba(170,125,70,0)'); c.fillStyle = g; c.beginPath(); c.arc(x, y, R, 0, TAU); c.fill(); }
441
+ c.globalCompositeOperation = 'source-over';
442
+ // crease
443
+ c.strokeStyle = 'rgba(255,248,230,0.35)'; c.lineWidth = 1.4; c.beginPath(); c.moveTo(-hw, 120); c.bezierCurveTo(-60, 100, 60, 150, hw, 118); c.stroke();
444
+ c.strokeStyle = 'rgba(80,50,20,0.18)'; c.lineWidth = 1; c.beginPath(); c.moveTo(-hw, 122); c.bezierCurveTo(-60, 102, 60, 152, hw, 120); c.stroke();
445
+ // scuffs
446
+ for (let k = 0; k < 18; k++) { const x = sr.range(-hw, hw), y = sr.range(-hh, hh), a = sr.range(0, TAU), l = sr.range(6, 30); c.strokeStyle = `rgba(245,236,214,${sr.range(0.15, 0.4)})`; c.lineWidth = sr.range(0.5, 1.2); c.beginPath(); c.moveTo(x, y); c.lineTo(x + Math.cos(a) * l, y + Math.sin(a) * l); c.stroke(); }
447
+ // worn edges: paper core showing through, heavier at the corners
448
+ for (let k = 0; k < 900; k++) {
449
+ const u = k / 900, per = 2 * (CW + CH), d = u * per;
450
+ let x, y;
451
+ if (d < CW) { x = -hw + d; y = -hh; } else if (d < CW + CH) { x = hw; y = -hh + d - CW; } else if (d < 2 * CW + CH) { x = hw - (d - CW - CH); y = hh; } else { x = -hw; y = hh - (d - 2 * CW - CH); }
452
+ const corner = Math.max(0, 1 - Math.min(Math.hypot(x - hw, y - hh), Math.hypot(x + hw, y - hh), Math.hypot(x - hw, y + hh), Math.hypot(x + hw, y + hh)) / 70);
453
+ const wv = (0.5 + 0.5 * nz.n2(u * 90, 3)) * 3 + corner * 9 * (0.5 + 0.5 * nz.n2(u * 40, 7));
454
+ if (wv < 1) continue;
455
+ c.fillStyle = `rgba(246,238,220,${0.5 + corner * 0.4})`; c.beginPath(); c.arc(x, y, wv, 0, TAU); c.fill();
456
+ }
457
+ c.restore();
458
+ // print grain on card
459
+ {
460
+ const img = c.getImageData(0, 0, L.width, L.height), d = img.data, gr = H.rng(17);
461
+ for (let i = 0; i < d.length; i += 4) { if (!d[i + 3]) continue; const p = i >> 2, x = p % L.width, y = (p / L.width) | 0; const n = (gr() - 0.5) * 16 + nz.n2(x * 0.08, y * 0.08) * 8; d[i] += n; d[i + 1] += n; d[i + 2] += n * 0.8; }
462
+ c.putImageData(img, 0, 0);
463
+ }
464
+
465
+ // ---------- place card on velvet ----------
466
+ // dealt face-down, then flipped over, then a foil sheen passes
467
+ const deal = H.ease.out(H.clamp((t - 0.55) / 0.75));
468
+ const f = H.clamp((t - 1.45) / 0.6), fe = H.ease.inOut(f);
469
+ const CX = 640, CY = 362 + (1 - deal) * 560, ROT = -0.025 + (1 - deal) * 0.25;
470
+ if (deal > 0) {
471
+ const sx = Math.max(0.02, Math.abs(Math.cos(fe * Math.PI))), lift = Math.sin(fe * Math.PI);
472
+ ctx.save(); ctx.translate(CX, CY); ctx.rotate(ROT);
473
+ ctx.save(); ctx.filter = `blur(${16 + lift * 14}px)`; ctx.fillStyle = `rgba(0,0,0,${0.7 - lift * 0.25})`; ctx.scale(sx, 1); rr(ctx, -hw + 12 + lift * 30, -hh + 20 + lift * 30, CW, CH, 18); ctx.fill(); ctx.restore();
474
+ ctx.scale(sx * (1 + lift * 0.06), 1 + lift * 0.06);
475
+ if (fe < 0.5) {
476
+ cardBack(ctx, CW, CH, 7);
477
+ ctx.fillStyle = `rgba(255,255,255,${0.12 * (1 - sx)})`; rr(ctx, -hw, -hh, CW, CH, 18); ctx.fill();
478
+ } else {
479
+ ctx.save(); ctx.filter = 'blur(3px)'; ctx.fillStyle = 'rgba(0,0,0,0.5)'; rr(ctx, -hw + 2, -hh + 4, CW, CH, 18); ctx.fill(); ctx.restore();
480
+ ctx.drawImage(L, -(CW + 40) / 2, -(CH + 40) / 2);
481
+ // glossy light band sliding across the card surface
482
+ const gb = H.clamp((t - 2.4) / 1.6);
483
+ if (gb > 0 && gb < 1) {
484
+ ctx.save(); rr(ctx, -hw, -hh, CW, CH, 18); ctx.clip(); ctx.globalCompositeOperation = 'lighter';
485
+ const gx = H.lerp(-hw - 260, hw + 260, H.ease.inOut(gb));
486
+ const lg = ctx.createLinearGradient(gx - 90, -hh * 0.4, gx + 90, hh * 0.4); lg.addColorStop(0, 'rgba(255,240,200,0)'); lg.addColorStop(0.5, 'rgba(255,236,190,0.26)'); lg.addColorStop(1, 'rgba(255,240,200,0)');
487
+ ctx.fillStyle = lg; ctx.fillRect(-hw, -hh, CW, CH); ctx.restore();
488
+ }
489
+ }
490
+ ctx.restore();
491
+ }
492
+ // gold dust on velvet
493
+ for (let k = 0; k < 90; k++) { const a = r() * TAU, d = 220 + r() * 260; const x = 640 + Math.cos(a) * d * 1.3 + Math.sin(t * 0.6 + k) * 3, y = 362 + Math.sin(a) * d * 0.7 + Math.cos(t * 0.5 + k * 2) * 2; ctx.fillStyle = `rgba(240,210,130,${r.range(0.2, 0.8) * (0.6 + 0.4 * Math.sin(t * 3 + k))})`; ctx.beginPath(); ctx.arc(x, y, r.range(0.5, 1.5), 0, TAU); ctx.fill(); }
494
+ H.vignette(ctx, W, H_, 0.55);
495
+ H.grain(ctx, W, H_, 0.03, 4 + Math.floor(t * 12));
496
+ },
497
+ // ===== EXPLAINER =====
498
+ explainerDuration: 14.5,
499
+ explainerNote: 'Told as a tarot reading: cards are dealt, flipped and gathered off the velvet as the scene changes, gold filigree arrows link the spread, brass plaques count the examples and the accuracy, and a burst of gold light marks the wrong guess and the 97%.',
500
+ explainer(ctx, W, H_, t) {
501
+ ctx.drawImage(velvet(W, H_), 0, 0);
502
+ const r = H.rng(21), ease = H.ease.inOut, cl = H.clamp;
503
+ const sheen = 0.4 + 0.6 * Math.sin(t * 0.9);
504
+ const deal = (t0, from, to) => { const k = H.ease.out(cl((t - t0) / 0.55)); return [H.lerp(from[0], to[0], k), H.lerp(from[1], to[1], k), k]; };
505
+ const glow = (x, y, R, a) => { if (a <= 0) return; const g = ctx.createRadialGradient(x, y, 0, x, y, R); g.addColorStop(0, `rgba(255,226,150,${0.55 * a})`); g.addColorStop(1, 'rgba(255,226,150,0)'); ctx.fillStyle = g; ctx.fillRect(x - R, y - R, 2 * R, 2 * R); ctx.save(); ctx.translate(x, y); ctx.rotate(t * 0.3); for (let k = 0; k < 24; k++) { ctx.rotate(TAU / 24); ctx.fillStyle = `rgba(255,226,150,${0.25 * a})`; ctx.beginPath(); ctx.moveTo(0, -4); ctx.lineTo(R * 1.1, 0); ctx.lineTo(0, 4); ctx.fill(); } ctx.restore(); };
506
+
507
+ // ===== BEAT 1: the question card =====
508
+ if (t < 2.6) {
509
+ const [x, y] = deal(0, [640, 900], [640, 360]);
510
+ const out = ease(cl((t - 1.85) / 0.6));
511
+ const fl = cl((t - 0.45) / 0.5) - cl((t - 1.85) / 0.35);
512
+ tCard(ctx, H.lerp(x, 160, out), H.lerp(y, 120, out), 300 * (1 - out * 0.7), 520 * (1 - out * 0.7), -0.02 - out * 0.2, fl, (c) => tFace(c, 300 * (1 - out * 0.7), 520 * (1 - out * 0.7), '0', 'THE QUESTION', (c2, aw, ah, s, G) => {
513
+ c2.font = `600 ${200 * s}px "Cormorant Garamond"`; c2.textAlign = 'center'; c2.textBaseline = 'middle'; c2.fillStyle = G(-60 * s, -90 * s, 60 * s, 90 * s); c2.fillText('?', 0, 0); c2.lineWidth = 3 * s; c2.strokeStyle = LINE; c2.strokeText('?', 0, 0);
514
+ for (let k = 0; k < 9; k++) { const a = k / 9 * TAU; star(c2, Math.cos(a) * 95 * s, Math.sin(a) * 120 * s, 6 * s, 1.6 * s, 4); c2.fillStyle = '#d9bd6a'; c2.fill(); }
515
+ }, sheen), 0, 3);
516
+ if (t > 0.9 && t < 2.3) { const a = cl((t - 0.9) / 0.3) * (1 - cl((t - 1.9) / 0.3)); ctx.save(); ctx.globalAlpha = a; plaque(ctx, 640, 660, 560, 72, ['How does AI learn?'], [44], sheen); ctx.restore(); }
517
+ }
518
+ // header plaque (the question, now a corner title)
519
+ if (t > 2.0) { const k = ease(cl((t - 2.0) / 0.4)); plaque(ctx, H.lerp(640, 210, k), H.lerp(660, 54, k), H.lerp(560, 350, k), H.lerp(72, 56, k), ['How does AI learn?'], [H.lerp(44, 32, k)], sheen); }
520
+
521
+ // ===== BEAT 2: examples dealt into the model =====
522
+ const MODEL = (c, w, h) => tFace(c, w, h, 'XXI', 'THE MODEL', (c2, aw, ah, s, G) => {
523
+ c2.beginPath(); for (let k = 0; k < 24; k++) { const a = k / 24 * TAU + t * 0.2; c2.lineTo(Math.cos(a) * 92 * s, -20 * s + Math.sin(a) * 92 * s); c2.lineTo(Math.cos(a + 0.13) * 104 * s, -20 * s + Math.sin(a + 0.13) * 104 * s); } c2.closePath(); c2.fillStyle = G(-100 * s, -120 * s, 100 * s, 80 * s); c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 2 * s; c2.stroke();
524
+ c2.beginPath(); c2.arc(0, -20 * s, 80 * s, 0, TAU); c2.fillStyle = '#2d3a52'; c2.fill(); c2.stroke();
525
+ c2.fillStyle = '#d9bd6a'; for (let k = 0; k < 32; k++) { const a = k / 32 * TAU; c2.beginPath(); c2.arc(Math.cos(a) * 73 * s, -20 * s + Math.sin(a) * 73 * s, 1.5 * s, 0, TAU); c2.fill(); }
526
+ // the hero oracle's cables, in hero units mapped onto this smaller head (back pair first, behind the mantle)
527
+ const hero = (fn) => { c2.save(); c2.scale(s, s); c2.translate(0, -14 + 41 * 0.9); c2.scale(0.92, 0.9); fn(); c2.restore(); };
528
+ hero(() => [
529
+ [[-24, -104], [-80, -150], [-150, -90], [-112, -20], [-80, 40], [-150, 80], [-120, 150]],
530
+ [[24, -104], [80, -150], [150, -90], [112, -20], [80, 40], [150, 80], [120, 150]],
531
+ [[-14, -110], [-40, -170], [-110, -170], [-128, -120]],
532
+ [[14, -110], [40, -170], [110, -170], [128, -120]],
533
+ ].forEach((p, i) => cable(c2, p, 10, ['#a8573a', '#a8573a', '#3f6e6c', '#3f6e6c'][i], '#d9bd6a')));
534
+ // ribbed neck
535
+ c2.beginPath(); c2.rect(-13 * s, 30 * s, 26 * s, 34 * s); c2.fillStyle = '#a3aca6'; c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 1.8 * s; c2.stroke();
536
+ c2.lineWidth = 1.1 * s; for (let y = 36; y < 62; y += 6) { c2.beginPath(); c2.moveTo(-13 * s, y * s); c2.quadraticCurveTo(0, (y + 3) * s, 13 * s, y * s); c2.stroke(); }
537
+ // face, with the hero's flat shadow side and blush
538
+ const face = () => { c2.beginPath(); c2.ellipse(0, -14 * s, 44 * s, 58 * s, 0, 0, TAU); };
539
+ face(); c2.fillStyle = '#dcd9cd'; c2.fill();
540
+ c2.save(); face(); c2.clip();
541
+ c2.beginPath(); c2.moveTo(14 * s, -72 * s); c2.bezierCurveTo(40 * s, -46 * s, 40 * s, -6 * s, 26 * s, 22 * s); c2.bezierCurveTo(20 * s, 34 * s, 8 * s, 42 * s, 0, 46 * s); c2.lineTo(60 * s, 50 * s); c2.lineTo(60 * s, -80 * s); c2.closePath(); c2.fillStyle = '#bebcb2'; c2.fill();
542
+ c2.fillStyle = 'rgba(190,120,110,0.3)'; [-1, 1].forEach((sg) => { c2.beginPath(); c2.ellipse(sg * 25 * s, 4 * s, 9 * s, 5.5 * s, 0, 0, TAU); c2.fill(); });
543
+ c2.restore();
544
+ face(); c2.strokeStyle = LINE; c2.lineWidth = 2.6 * s; c2.stroke();
545
+ // closed eyes with lashes, brows, nose
546
+ c2.lineWidth = 2 * s; [-1, 1].forEach((sg) => {
547
+ c2.beginPath(); c2.moveTo(sg * 28 * s, -16 * s); c2.quadraticCurveTo(sg * 18 * s, -8 * s, sg * 8 * s, -15 * s); c2.stroke();
548
+ c2.lineWidth = 1.1 * s; for (let k = 0; k < 4; k++) { const x = sg * (11 + k * 5), y = -13.5 + Math.sin((k + 0.5) / 4 * Math.PI) * 1.6; c2.beginPath(); c2.moveTo(x * s, y * s); c2.lineTo((x + sg * 1.4) * s, (y + 3.6) * s); c2.stroke(); }
549
+ c2.lineWidth = 1.6 * s; c2.beginPath(); c2.moveTo(sg * 31 * s, -28 * s); c2.quadraticCurveTo(sg * 19 * s, -35 * s, sg * 7 * s, -29 * s); c2.stroke(); c2.lineWidth = 2 * s;
550
+ });
551
+ c2.lineWidth = 1.4 * s; c2.beginPath(); c2.moveTo(-1 * s, -14 * s); c2.quadraticCurveTo(-3 * s, -2 * s, -5 * s, 5 * s); c2.quadraticCurveTo(-1 * s, 8 * s, 4 * s, 6 * s); c2.stroke();
552
+ c2.beginPath(); c2.moveTo(-10 * s, 20 * s); c2.quadraticCurveTo(-4 * s, 17 * s, 0, 19 * s); c2.quadraticCurveTo(4 * s, 17 * s, 10 * s, 20 * s); c2.quadraticCurveTo(0, 28 * s, -10 * s, 20 * s); c2.closePath(); c2.fillStyle = '#a2615b'; c2.fill(); c2.stroke();
553
+ // cap, gold headband and third-eye jewel
554
+ c2.lineWidth = 2.6 * s; c2.beginPath(); c2.moveTo(-44 * s, -44 * s); c2.bezierCurveTo(-46 * s, -90 * s, 46 * s, -90 * s, 44 * s, -44 * s); c2.closePath(); c2.fillStyle = '#4c6b66'; c2.fill(); c2.stroke();
555
+ c2.beginPath(); c2.moveTo(-44 * s, -48 * s); c2.bezierCurveTo(-30 * s, -60 * s, 30 * s, -60 * s, 44 * s, -48 * s); c2.lineTo(43 * s, -39 * s); c2.bezierCurveTo(30 * s, -50 * s, -30 * s, -50 * s, -43 * s, -39 * s); c2.closePath(); c2.fillStyle = G(-44 * s, -60 * s, 44 * s, -39 * s); c2.fill(); c2.lineWidth = 1.6 * s; c2.stroke();
556
+ c2.beginPath(); c2.moveTo(0, -64 * s); c2.lineTo(8 * s, -50 * s); c2.lineTo(0, -36 * s); c2.lineTo(-8 * s, -50 * s); c2.closePath(); c2.fillStyle = '#5f9e94'; c2.fill(); c2.stroke();
557
+ c2.beginPath(); c2.arc(-2 * s, -53 * s, 1.8 * s, 0, TAU); c2.fillStyle = 'rgba(255,255,240,0.8)'; c2.fill();
558
+ c2.lineWidth = 2.4 * s; c2.beginPath(); c2.moveTo(-120 * s, ah / 2); c2.bezierCurveTo(-100 * s, 70 * s, -40 * s, 56 * s, 0, 56 * s); c2.bezierCurveTo(40 * s, 56 * s, 100 * s, 70 * s, 120 * s, ah / 2); c2.closePath(); c2.fillStyle = '#5c7a72'; c2.fill(); c2.stroke();
559
+ // mantle shadow, gold collar and chest core
560
+ c2.save(); c2.beginPath(); c2.moveTo(-120 * s, ah / 2); c2.bezierCurveTo(-100 * s, 70 * s, -40 * s, 56 * s, 0, 56 * s); c2.bezierCurveTo(40 * s, 56 * s, 100 * s, 70 * s, 120 * s, ah / 2); c2.closePath(); c2.clip();
561
+ c2.beginPath(); c2.moveTo(16 * s, 90 * s); c2.bezierCurveTo(50 * s, 120 * s, 76 * s, 160 * s, 84 * s, ah / 2); c2.lineTo(140 * s, ah / 2); c2.lineTo(140 * s, 60 * s); c2.closePath(); c2.fillStyle = 'rgba(30,50,48,0.35)'; c2.fill(); c2.restore();
562
+ c2.beginPath(); c2.moveTo(-52 * s, 64 * s); c2.quadraticCurveTo(0, 116 * s, 52 * s, 64 * s); c2.lineTo(43 * s, 61 * s); c2.quadraticCurveTo(0, 102 * s, -43 * s, 61 * s); c2.closePath(); c2.fillStyle = G(-52 * s, 60 * s, 52 * s, 110 * s); c2.fill(); c2.lineWidth = 1.6 * s; c2.stroke();
563
+ { const cy = 140 * s; c2.strokeStyle = '#d9bd6a'; c2.lineWidth = 1.8 * s; for (let k = 0; k < 16; k++) { const a = k / 16 * TAU; c2.beginPath(); c2.moveTo(Math.cos(a) * 20 * s, cy + Math.sin(a) * 20 * s); c2.lineTo(Math.cos(a) * (k % 2 ? 27 : 34) * s, cy + Math.sin(a) * (k % 2 ? 27 : 34) * s); c2.stroke(); }
564
+ c2.beginPath(); c2.arc(0, cy, 18 * s, 0, TAU); c2.fillStyle = G(-18 * s, cy - 18 * s, 18 * s, cy + 18 * s); c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 1.8 * s; c2.stroke();
565
+ c2.beginPath(); c2.arc(0, cy, 10 * s, 0, TAU); c2.fillStyle = '#6fa39a'; c2.fill(); c2.stroke();
566
+ c2.beginPath(); c2.arc(-3 * s, cy - 3 * s, 3 * s, 0, TAU); c2.fillStyle = 'rgba(255,255,240,0.7)'; c2.fill(); }
567
+ // front cables whiplash over the shoulders, ending in gold plugs
568
+ hero(() => {
569
+ [[[-36, -96], [-74, -60], [-70, 10], [-56, 60], [-44, 100], [-90, 140], [-70, 186]], [[36, -96], [74, -60], [70, 10], [56, 60], [44, 100], [90, 140], [70, 186]]].forEach((p) => cable(c2, p, 9, '#b98f3e', '#efe0a2'));
570
+ [[-70, 186], [70, 186]].forEach(([x, y]) => { c2.beginPath(); c2.rect(x - 7, y - 4, 14, 14); c2.fillStyle = '#c9a54e'; c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 1.6; c2.stroke(); });
571
+ });
572
+ }, sheen);
573
+ if (t > 2.3 && t < 6.6) {
574
+ const gone = ease(cl((t - 6.0) / 0.5));
575
+ // deck of examples at the left
576
+ const dx = 190 - gone * 400;
577
+ for (let k = 0; k < 6; k++) { ctx.save(); ctx.translate(dx + k * 1.5, 420 - k * 2.5); ctx.rotate(-0.05); if (k === 0) { ctx.save(); ctx.filter = 'blur(10px)'; ctx.fillStyle = 'rgba(0,0,0,0.6)'; rr(ctx, -70 + 6, -115 + 12, 140, 230, 10); ctx.fill(); ctx.restore(); } cardBack(ctx, 140, 230, 8); ctx.restore(); }
578
+ if (t > 2.5) { ctx.save(); ctx.translate(-gone * 400, 0); plaque(ctx, 190, 580, 210, 52, ['the examples'], [28], sheen); ctx.restore(); }
579
+ // the model card
580
+ const [mx, my] = deal(2.3, [700, 900], [700, 340]);
581
+ tCard(ctx, mx, my + gone * 600, 250, 420, 0.01, cl((t - 2.6) / 0.5), (c) => MODEL(c, 250, 420), 0, 9);
582
+ gArrow(ctx, [290, 300], [430, 200], [560, 280], cl((t - 3.0) / 0.4) * (1 - gone));
583
+ // stream of labelled example cards flying into the model
584
+ for (let k = 0; k < 16; k++) {
585
+ const t0 = 3.0 + k * 0.17, u = cl((t - t0) / 0.75); if (u <= 0 || u >= 1 || gone > 0) continue;
586
+ const isCat = k % 3 !== 1, x = H.lerp(220, 700, u), y = H.lerp(400, 330, u) - Math.sin(u * Math.PI) * 220, sc = 1 - 0.75 * Math.pow(u, 3);
587
+ tCard(ctx, x, y, 120 * sc, 200 * sc, -0.3 + u * 0.3, 1, (c) => tFace(c, 120 * sc, 200 * sc, '', isCat ? 'CAT' : 'DOG', (c2, aw, ah, s) => (isCat ? tCat : tDog)(c2, 0, 0, 2.6 * s), sheen), 0.2);
588
+ }
589
+ glow(700, 330, 160, cl((t - 3.1) / 0.3) * (0.5 + 0.3 * Math.sin(t * 9)) * (1 - cl((t - 5.6) / 0.4)));
590
+ // counter plaque
591
+ const e = ease(cl((t - 3.0) / 2.6)), v = t >= 5.6 ? 1000000 : Math.round(Math.pow(10, 1 + 5 * e));
592
+ if (t > 3.0) { ctx.save(); ctx.translate(0, gone * 600); plaque(ctx, 1050, 360, 380, 170, [v.toLocaleString('en-US'), 'examples'], [72, 34], sheen); ctx.restore(); }
593
+ }
594
+
595
+ // ===== BEAT 3: three-card spread — Guess, Check, Adjust =====
596
+ if (t > 6.1 && t < 11.0) {
597
+ const gone = ease(cl((t - 10.4) / 0.5)), right = t > 9.35;
598
+ const P = [[290, 400], [640, 400], [990, 400]], CW = 230, CH = 390;
599
+ const GUESS = (c) => tFace(c, CW, CH, 'I', 'GUESS', (c2, aw, ah, s, G) => {
600
+ c2.save(); c2.translate(-50 * s, -60 * s); c2.rotate(-0.12); rr(c2, -60 * s, -56 * s, 120 * s, 112 * s, 6 * s); c2.fillStyle = '#efe3c6'; c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 2.4 * s; c2.stroke(); tCat(c2, 0, 4 * s, 1.5 * s); c2.restore();
601
+ c2.beginPath(); c2.moveTo(30 * s, 40 * s); c2.quadraticCurveTo(80 * s, 10 * s, 130 * s, 40 * s); c2.quadraticCurveTo(80 * s, 70 * s, 30 * s, 40 * s); c2.closePath(); c2.fillStyle = '#efe3c6'; c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 2.4 * s; c2.stroke();
602
+ c2.beginPath(); c2.arc(80 * s, 40 * s, 15 * s, 0, TAU); c2.fillStyle = G(60 * s, 25 * s, 100 * s, 55 * s); c2.fill(); c2.stroke(); c2.beginPath(); c2.arc(80 * s, 40 * s, 6 * s, 0, TAU); c2.fillStyle = LINE; c2.fill();
603
+ // banner with the guess
604
+ c2.beginPath(); c2.moveTo(-130 * s, 130 * s); c2.lineTo(130 * s, 130 * s); c2.lineTo(116 * s, 158 * s); c2.lineTo(130 * s, 186 * s); c2.lineTo(-130 * s, 186 * s); c2.lineTo(-116 * s, 158 * s); c2.closePath(); c2.fillStyle = '#efe3c6'; c2.fill(); c2.stroke();
605
+ c2.font = `600 ${44 * s}px "Cormorant Garamond"`; c2.textAlign = 'center'; c2.textBaseline = 'middle'; c2.fillStyle = right ? '#2f6f4a' : '#8e2a20'; c2.fillText(right ? 'cat ✓' : 'dog?', 0, 160 * s);
606
+ }, sheen);
607
+ const CHECK = (c) => tFace(c, CW, CH, 'II', 'CHECK', (c2, aw, ah, s, G) => {
608
+ const tilt = right ? 0 : 0.22 * ease(cl((t - 7.6) / 0.4));
609
+ c2.strokeStyle = LINE; c2.lineWidth = 3 * s; c2.fillStyle = G(-120 * s, -100 * s, 120 * s, 100 * s);
610
+ c2.beginPath(); c2.rect(-6 * s, -110 * s, 12 * s, 200 * s); c2.fill(); c2.stroke();
611
+ c2.save(); c2.translate(0, -90 * s); c2.rotate(tilt); c2.beginPath(); c2.rect(-110 * s, -5 * s, 220 * s, 10 * s); c2.fill(); c2.stroke();
612
+ [[-1, 'dog?'], [1, 'cat']].forEach(([sg, lb]) => { c2.save(); c2.translate(sg * 100 * s, 0); c2.rotate(-tilt); c2.beginPath(); c2.moveTo(0, 0); c2.lineTo(-30 * s, 70 * s); c2.moveTo(0, 0); c2.lineTo(30 * s, 70 * s); c2.stroke(); c2.beginPath(); c2.ellipse(0, 72 * s, 38 * s, 12 * s, 0, 0, Math.PI); c2.fill(); c2.stroke(); c2.font = `600 ${40 * s}px "Cormorant Garamond"`; c2.textAlign = 'center'; c2.lineJoin = 'round'; c2.strokeStyle = LINE; c2.lineWidth = 4 * s; c2.strokeText(lb, 0, 56 * s); c2.fillStyle = '#efe3c6'; c2.fillText(lb, 0, 56 * s); c2.strokeStyle = LINE; c2.lineWidth = 3 * s; c2.fillStyle = G(-120 * s, -100 * s, 120 * s, 100 * s); c2.restore(); });
613
+ c2.restore();
614
+ // verdict emblem
615
+ c2.beginPath(); c2.arc(0, 130 * s, 44 * s, 0, TAU); c2.fillStyle = '#efe3c6'; c2.fill(); c2.stroke();
616
+ c2.lineCap = 'round'; c2.lineWidth = 10 * s; c2.strokeStyle = right ? '#2f6f4a' : '#8e2a20';
617
+ if (t > 7.75 && !right) { c2.beginPath(); c2.moveTo(-22 * s, 108 * s); c2.lineTo(22 * s, 152 * s); c2.moveTo(22 * s, 108 * s); c2.lineTo(-22 * s, 152 * s); c2.stroke(); }
618
+ if (right) { c2.beginPath(); c2.moveTo(-22 * s, 130 * s); c2.lineTo(-6 * s, 148 * s); c2.lineTo(24 * s, 110 * s); c2.stroke(); }
619
+ }, sheen);
620
+ const ADJUST = (c) => tFace(c, CW, CH, 'III', 'ADJUST', (c2, aw, ah, s, G) => {
621
+ const tq = Math.floor(t * 12) / 12;
622
+ [[-60, -70, 52], [62, -10, 44], [-40, 90, 48]].forEach(([x, y, R], i) => {
623
+ const rot = (i % 2 ? -1 : 1) * 2.4 * ease(cl((tq - 8.65 - i * 0.12) / 0.6));
624
+ c2.save(); c2.translate(x * s, y * s); c2.rotate(rot);
625
+ c2.beginPath(); for (let k = 0; k < 14; k++) { const a = k / 14 * TAU; c2.lineTo(Math.cos(a) * R * s, Math.sin(a) * R * s); c2.lineTo(Math.cos(a + 0.22) * (R + 9) * s, Math.sin(a + 0.22) * (R + 9) * s); } c2.closePath();
626
+ c2.fillStyle = G(-R * s, -R * s, R * s, R * s); c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 2.2 * s; c2.stroke();
627
+ c2.beginPath(); c2.arc(0, 0, R * 0.55 * s, 0, TAU); c2.fillStyle = '#2d3a52'; c2.fill(); c2.stroke();
628
+ c2.beginPath(); c2.moveTo(0, 0); c2.lineTo(R * 0.5 * s, 0); c2.strokeStyle = '#e8cf7a'; c2.lineWidth = 4 * s; c2.stroke();
629
+ c2.restore();
630
+ });
631
+ }, sheen);
632
+ if (gone < 0.3) {
633
+ if (t > 7.75 && t < 9.35) glow(640, 400, 260, cl((t - 7.75) / 0.25) * (0.8 + 0.2 * Math.sin(t * 12)));
634
+ if (t > 9.35) glow(290, 400, 240, (1 - cl((t - 9.8) / 0.5)) * cl((t - 9.35) / 0.15));
635
+ }
636
+ [[GUESS, 6.2, 6.5], [CHECK, 6.35, 7.35], [ADJUST, 6.5, 8.2]].forEach(([fn, td, tf], i) => {
637
+ const [x, y] = deal(td, [640, 900], P[i]);
638
+ tCard(ctx, x + (i - 1) * gone * 200, y + gone * 600, CW, CH, (i - 1) * 0.03, cl((t - tf) / 0.5), fn, 0, 10 + i);
639
+ });
640
+ if (gone < 0.3) {
641
+ gArrow(ctx, [395, 250], [465, 170], [535, 250], cl((t - 7.3) / 0.35), 'then');
642
+ gArrow(ctx, [745, 250], [815, 170], [885, 250], cl((t - 8.15) / 0.35), 'error');
643
+ gArrow(ctx, [990, 610], [640, 760], [290, 610], cl((t - 9.0) / 0.45), 'try again');
644
+ }
645
+ }
646
+
647
+ // ===== BEAT 4 + 5: the star of progress, then the moral =====
648
+ if (t > 10.5) {
649
+ const STAR = (c) => tFace(c, 280, 470, 'XVII', 'THE STAR', (c2, aw, ah, s, G) => {
650
+ const ox = -110 * s, oy = 120 * s, gw = 220 * s, gh = 220 * s, pr = cl((t - 11.0) / 1.5);
651
+ c2.strokeStyle = '#d9bd6a'; c2.lineWidth = 2 * s; c2.beginPath(); c2.moveTo(ox, oy - gh); c2.lineTo(ox, oy); c2.lineTo(ox + gw, oy); c2.stroke();
652
+ const acc = (u) => 61 + 36 * (1 - Math.pow(1 - u, 2.3)), pts = [];
653
+ for (let k = 0; k <= 40; k++) { const u = k / 40 * pr; pts.push([ox + u * gw, oy - (acc(u) - 50) / 50 * gh]); }
654
+ c2.beginPath(); pts.forEach((q, i) => i ? c2.lineTo(q[0], q[1]) : c2.moveTo(q[0], q[1])); c2.strokeStyle = LINE; c2.lineWidth = 7 * s; c2.stroke(); c2.strokeStyle = G(ox, oy - gh, ox + gw, oy); c2.lineWidth = 4 * s; c2.stroke();
655
+ for (let k = 1; k <= 6; k++) { const u = k / 6; if (u > pr) break; star(c2, ox + u * gw, oy - (acc(u) - 50) / 50 * gh, 6 * s, 2 * s, 4); c2.fillStyle = '#efe0a2'; c2.fill(); }
656
+ const tp = pts[pts.length - 1]; star(c2, tp[0], tp[1], 22 * s, 6 * s, 8, t); c2.fillStyle = G(tp[0] - 20, tp[1] - 20, tp[0] + 20, tp[1] + 20); c2.fill(); c2.strokeStyle = LINE; c2.lineWidth = 1.5 * s; c2.stroke();
657
+ }, sheen);
658
+ const [x, y] = deal(10.5, [640, 900], [420, 370]);
659
+ tCard(ctx, x, y, 280, 470, -0.02, cl((t - 10.75) / 0.5), STAR, 0, 14);
660
+ const acc = 61 + 36 * (1 - Math.pow(1 - cl((t - 11.0) / 1.5), 2.3)), v = Math.round(acc);
661
+ if (t > 12.5) glow(900, 330, 260, cl((t - 12.5) / 0.25) * (1 - 0.5 * cl((t - 13.2) / 0.6)));
662
+ const flipP = cl((t - 13.0) / 0.5), fe = ease(flipP), sx = Math.max(0.02, Math.abs(Math.cos(fe * Math.PI)));
663
+ if (t > 10.9) {
664
+ ctx.save(); ctx.translate(900, 340); ctx.scale(sx, 1); ctx.translate(-900, -340);
665
+ if (fe < 0.5) { const pulse = t > 12.5 ? 1 + 0.12 * Math.pow(1 - cl((t - 12.5) / 0.3), 2) : 1; ctx.save(); ctx.translate(900, 340); ctx.scale(pulse, pulse); ctx.translate(-900, -340); plaque(ctx, 900, 340, 420, 220, ['Accuracy', v + '%'], [40, 110], sheen); ctx.restore(); }
666
+ else plaque(ctx, 900, 340, 460, 240, ['More examples', '→', 'better guesses'], [52, 40, 52], sheen);
667
+ ctx.restore();
668
+ }
669
+ }
670
+ H.vignette(ctx, W, H_, 0.5);
671
+ H.grain(ctx, W, H_, 0.03, 4 + Math.floor(t * 12));
672
+ },
673
+ // Sound: a reading by candlelight. Cards are dealt with a soft slide across velvet and settle with a muffled tap;
674
+ // flips are a stiff card snapping over and laying down; brass plaques and gears are small, damped metal; gold
675
+ // filigree draws in as a low, breathy shimmer (no high pings). The wrong guess is a low, sour gong; the 97% is a
676
+ // warm low bell over an impact. A quiet minor pad underneath. Times copied from explainer() above:
677
+ // deal(t0) = 0.55 s ease-out (settles about t0 + 0.4); tCard flip over [tf, tf+0.5] turns edge-on at tf + 0.25.
678
+ explainerSound(k) {
679
+ // Chrome glitch: a noise buffer that starts within ~1e-8 s of a sample boundary (float error like 4.15 + 0.1 * 1 + 0.11)
680
+ // spikes far above its gain. Put every start a quarter sample past the 48 kHz grid so none land there.
681
+ { const k0 = k, sn = (t) => (Math.round(t * 48000) + 0.25) / 48000; k = { ...k0, play: (n, t, o) => k0.play(n, sn(t), o), tone: (t, o) => k0.tone(sn(t), o), noise: (t, o) => k0.noise(sn(t), o) }; }
682
+ const D = 14.5, pan = (x) => H.clamp(x / 640 - 1, -1, 1) * 0.6;
683
+ // card slid across the velvet, then settling
684
+ const deal = (t0, x, g = 1, minor = false) => {
685
+ k.noise(t0, { dur: 0.42, color: 'pink', filter: 'bandpass', f: [[0, 900], [0.42, 500]], q: 0.7, gain: 0.16 * g, a: 0.05, pan: pan(x), minor });
686
+ k.noise(t0 + 0.4, { dur: 0.05, color: 'pink', filter: 'lowpass', f: 900, gain: 0.17 * g, a: 0.002, pan: pan(x), minor });
687
+ k.tone(t0 + 0.4, { f: [[0, 160], [0.05, 90]], dur: 0.07, gain: 0.08 * g, pan: pan(x), minor });
688
+ };
689
+ // flip over [tf, tf+0.5]: the stiff card flicks up, turns edge-on (tf+0.25), lays down (tf+0.5)
690
+ const flip = (tf, x, g = 1, minor = false) => {
691
+ k.noise(tf + 0.02, { dur: 0.03, filter: 'bandpass', f: 1800, q: 1.2, gain: 0.22 * g, a: 0.001, pan: pan(x), minor });
692
+ k.noise(tf + 0.1, { dur: 0.3, color: 'pink', filter: 'bandpass', f: [[0, 700], [0.15, 1400], [0.3, 600]], q: 0.8, gain: 0.12 * g, a: 0.12, pan: pan(x), minor });
693
+ k.noise(tf + 0.48, { dur: 0.05, color: 'pink', filter: 'lowpass', f: 1100, gain: 0.15 * g, a: 0.002, pan: pan(x), minor });
694
+ };
695
+ // cards gathered off the velvet: one long soft slide
696
+ const gather = (t0, d, x) => k.noise(t0, { dur: d, color: 'pink', filter: 'bandpass', f: [[0, 500], [d * 0.5, 1000], [d, 450]], q: 0.6, gain: 0.2, a: d * 0.4, pan: pan(x) });
697
+ // gold filigree drawing in: a low breathy shimmer
698
+ const gilt = (t0, d, x, minor = true) => {
699
+ k.noise(t0, { dur: d + 0.1, color: 'pink', filter: 'bandpass', f: [[0, 900], [d, 1700]], q: 3, gain: 0.07, a: d * 0.5, pan: pan(x), minor });
700
+ k.tone(t0, { f: 'E4', type: 'triangle', dur: d + 0.5, gain: 0.035, a: d * 0.6, verb: 0.4, pan: pan(x), minor });
701
+ k.tone(t0 + d * 0.3, { f: 'B4', type: 'triangle', dur: d + 0.4, gain: 0.025, a: d * 0.5, verb: 0.4, pan: pan(x), minor });
702
+ };
703
+ // small damped brass tick (plaque counters, gears)
704
+ const brass = (t, g, p, f = 1400) => k.withMinor(() => { k.noise(t, { dur: 0.03, filter: 'bandpass', f, q: 3, gain: 0.26 * g, a: 0.001, pan: p }); k.tone(t, { f: f * 0.5, type: 'triangle', dur: 0.06, gain: 0.04 * g, pan: p }); });
705
+ // low bell with inharmonic partials (bowl-like), kept under ~1.5 kHz
706
+ const bell = (t, f, g, p = 0) => { const F = k.hz(f); k.tone(t, { f: F, dur: 2.2, gain: 0.16 * g, a: 0.004, verb: 0.4, pan: p }); k.tone(t, { f: F * 2.0, dur: 1.2, gain: 0.05 * g, a: 0.004, verb: 0.4, pan: p }); k.tone(t, { f: F * 2.76, dur: 0.6, gain: 0.025 * g, a: 0.004, verb: 0.4, pan: p }); };
707
+
708
+ k.bed('room', 0, D, { gain: 0.35 });
709
+ k.music('pad', 0.4, D, { bpm: 60, gain: 0.32, chords: [['A2', 'E3', 'A3', 'C4'], ['F2', 'C3', 'F3', 'A3'], ['D3', 'F3', 'A3', 'C4'], ['E2', 'B2', 'E3', 'G#3']] });
710
+
711
+ // ---- BEAT 1: the question card is dealt (0), turned (0.45 -> face at 0.7), plaque glows in (0.9)
712
+ deal(0, 640, 1.1);
713
+ flip(0.45, 640, 1.1);
714
+ bell(0.95, 'A3', 0.7, 0); // "How does AI learn?" plaque
715
+ // card turns back (1.85) while being drawn up to the corner (1.85 -> 2.45); plaque slides up (2.0 -> 2.4)
716
+ flip(1.85, 400, 0.6, true);
717
+ gather(1.9, 0.55, 300);
718
+
719
+ // ---- BEAT 2: THE MODEL is dealt (2.3) and turned (2.6), arrow gilds in (3.0 -> 3.4)
720
+ deal(2.3, 700);
721
+ flip(2.6, 700);
722
+ gilt(3.0, 0.4, 430);
723
+ // sixteen example cards flick off the deck (3.0 + k*0.17) and fly into the model
724
+ for (let i = 0; i < 16; i++) k.noise(3.0 + i * 0.17, { dur: 0.04, filter: 'bandpass', f: 1500, q: 1, gain: 0.12, a: 0.002, pan: pan(220), minor: true });
725
+ k.tone(3.1, { f: 'A2', type: 'triangle', dur: 2.9, gain: 0.05, a: 0.4, hold: 2.1, verb: 0.3, pan: pan(700), minor: true }); // the model card glows while it learns
726
+ // examples plaque 10 -> 1,000,000 over 3.0..5.6 (inOut): a brass tick whose rate follows the count's speed
727
+ for (let t = 3.08; t < 5.52;) { const e = H.clamp((t - 3.0) / 2.6), sp = Math.min(1, 6 * e * (1 - e)); brass(t, 0.7 + 0.3 * sp, pan(1050), 1300 + e * 300); t += 1 / (4 + 7 * sp); }
728
+ k.play('tock', 5.6, { f: 440, gain: 0.5, pan: pan(1050) }); bell(5.6, 'E3', 0.5, pan(1050)); // 1,000,000 lands
729
+ gather(6.0, 0.5, 500); // model card, deck and plaque are swept away
730
+
731
+ // ---- BEAT 3: the spread is dealt (6.2, 6.35, 6.5) and turned one by one (6.5, 7.35, 8.2)
732
+ deal(6.2, 290, 0.9); deal(6.35, 640, 0.9, true); deal(6.5, 990, 0.9, true);
733
+ flip(6.5, 290); // GUESS: dog?
734
+ gilt(7.3, 0.35, 465); // 'then'
735
+ flip(7.35, 640); // CHECK
736
+ // WRONG: the scales tip (7.6 -> 8.0) and the red X burns in with a gold burst (7.75)
737
+ k.noise(7.6, { dur: 0.4, filter: 'bandpass', f: [[0, 500], [0.4, 330]], q: 6, gain: 0.08, a: 0.1, pan: pan(640), minor: true }); // beam creaks over
738
+ bell(7.75, 'F2', 1.1, pan(640)); bell(7.75, 'B2', 0.55, pan(640)); // sour, low gong (tritone)
739
+ k.play('thud', 7.75, { f: 90, gain: 0.65, pan: pan(640) });
740
+ gilt(8.15, 0.35, 815); // 'error'
741
+ flip(8.2, 990); // ADJUST
742
+ // gears turn on twos (8.65 + i*0.12, 0.6 s each): a brass tooth per moving frame
743
+ for (let f = Math.floor(8.65 * 12) + 1; f / 12 < 9.45; f++) brass(f / 12, 0.6, pan(990 + ((f % 3) - 1) * 40), 900 + (f % 3) * 140);
744
+ gilt(9.0, 0.45, 640); // 'try again' sweeps under the spread
745
+ // CORRECTION (9.35): "cat ✓", the scales level, the Guess card glows
746
+ k.play('success', 9.35, { f: 'A4', gain: 0.55, pan: pan(290) });
747
+ bell(9.35, 'A3', 0.45, pan(290));
748
+ gather(10.4, 0.5, 640); // spread swept off
749
+
750
+ // ---- BEAT 4: THE STAR is dealt (10.5) and turned (10.75); accuracy plaque appears (10.9)
751
+ deal(10.5, 420);
752
+ flip(10.75, 420);
753
+ k.play('tock', 10.9, { f: 380, gain: 0.35, pan: pan(900), minor: true });
754
+ gilt(11.0, 1.5, 420); // the golden curve rises (11.0 -> 12.5)
755
+ { let last = 61, lastT = -1; // a brass tick per new value, <= 12/s
756
+ for (let t = 11.0; t < 12.5; t += 1 / 120) { const v = Math.round(61 + 36 * (1 - Math.pow(1 - H.clamp((t - 11.0) / 1.5), 2.3))); if (v !== last && t - lastT >= 1 / 12) { brass(t, 0.9, pan(900), 1200 + (v - 61) * 10); last = v; lastT = t; } } }
757
+ // 97%: gold light bursts, the plaque pulses (12.5): the big hit
758
+ k.play('impact', 12.5, { gain: 1.0, pan: pan(900) });
759
+ bell(12.5, 'A2', 1.0, pan(900)); bell(12.52, 'E3', 0.7, pan(900)); bell(12.54, 'A3', 0.5, pan(900));
760
+
761
+ // ---- BEAT 5: the plaque turns over (13.0 -> 13.5, edge-on 13.25) to the moral, which resolves
762
+ flip(13.0, 900, 1.1);
763
+ ['A3', 'C#4', 'E4'].forEach((f, i) => bell(13.27 + i * 0.09, f, 0.42 - i * 0.07, pan(900) + (i - 1) * 0.2));
764
+ },
765
+
766
+ // ===== END EXPLAINER =====
767
+ });
768
+ }