@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,799 @@
1
+ {
2
+ const TAU = Math.PI * 2;
3
+ const hx = (h) => H.hex(h);
4
+
5
+ // ---- fabric prints (u,v local px) -> rgb ----
6
+ const PRINTS = [
7
+ { kind: 'gingham', a: hx('#c9634d'), b: hx('#f3eadb') },
8
+ { kind: 'dots', a: hx('#e2b24a'), b: hx('#f7efdc') },
9
+ { kind: 'stripes', a: hx('#8fae84'), b: hx('#eee4cf') },
10
+ { kind: 'floral', a: hx('#d98f86'), b: hx('#f6d9cf'), c: hx('#fff5e6') },
11
+ { kind: 'denim', a: hx('#5f7ea3'), b: hx('#8aa3c2') },
12
+ { kind: 'linen', a: hx('#e9dcc2'), b: hx('#d8c8aa') },
13
+ { kind: 'gingham', a: hx('#6f9a8a'), b: hx('#f1ecdf') },
14
+ { kind: 'dots', a: hx('#b9563f'), b: hx('#f1d4bf') },
15
+ ];
16
+ const printCol = (p, u, v) => {
17
+ switch (p.kind) {
18
+ case 'gingham': {
19
+ const a = Math.floor(u / 14) % 2, b = Math.floor(v / 14) % 2, n = a + b;
20
+ return n === 2 ? p.a : n === 1 ? H.mix(p.a, p.b, 0.55) : p.b;
21
+ }
22
+ case 'dots': {
23
+ const row = Math.floor(v / 20), ox = row % 2 ? 10 : 0;
24
+ const du = ((u + ox) % 20) - 10, dv = (v % 20) - 10;
25
+ return du * du + dv * dv < 20 ? p.b : p.a;
26
+ }
27
+ case 'stripes': return (u % 18) < 7 ? p.a : (u % 18) < 9 ? H.mix(p.a, p.b, 0.5) : p.b;
28
+ case 'floral': {
29
+ const row = Math.floor(v / 26), ox = row % 2 ? 13 : 0;
30
+ const du = ((u + ox) % 26) - 13, dv = (v % 26) - 13, d = du * du + dv * dv;
31
+ if (d < 5) return p.c;
32
+ const pet = [[4, 0], [-4, 0], [0, 4], [0, -4]].some(([x, y]) => (du - x) ** 2 + (dv - y) ** 2 < 8);
33
+ return pet ? p.b : p.a;
34
+ }
35
+ case 'denim': return ((u + v) % 5) < 2 ? p.b : p.a;
36
+ default: return p.a;
37
+ }
38
+ };
39
+
40
+ const HQ = {}; // hero quilt cache (per canvas size)
41
+ const feltTex = (seed) => {
42
+ const S = 256, cv = H.layer(S, S), x = cv.getContext('2d'), r = H.rng(seed);
43
+ const img = x.createImageData(S, S), d = img.data;
44
+ for (let i = 0; i < d.length; i += 4) { const n = r(); d[i] = d[i + 1] = d[i + 2] = n > 0.5 ? 255 : 0; d[i + 3] = Math.abs(n - 0.5) * 70; }
45
+ x.putImageData(img, 0, 0);
46
+ x.lineCap = 'round';
47
+ for (let n = 0; n < 1400; n++) {
48
+ const px = r() * S, py = r() * S, a = r() * TAU, l = r.range(2, 7);
49
+ x.strokeStyle = r() < 0.5 ? `rgba(255,255,255,${r.range(0.08, 0.2)})` : `rgba(0,0,0,${r.range(0.08, 0.18)})`;
50
+ x.lineWidth = r.range(0.4, 0.9);
51
+ for (let oy = -1; oy <= 1; oy++) for (let ox = -1; ox <= 1; ox++) {
52
+ x.beginPath(); x.moveTo(px + ox * S, py + oy * S);
53
+ x.quadraticCurveTo(px + ox * S + Math.cos(a + 1) * l, py + oy * S + Math.sin(a + 1) * l, px + ox * S + Math.cos(a) * l * 1.6, py + oy * S + Math.sin(a) * l * 1.6);
54
+ x.stroke();
55
+ }
56
+ }
57
+ return cv;
58
+ };
59
+
60
+ // =====================================================================
61
+ // EXPLAINER: the camera pans along one long quilt. Felt pieces drop on
62
+ // with a lifted shadow, thread sews arrows and outlines on as we watch,
63
+ // labels are woven tags stitched to what they name, buttons are the weights.
64
+ // =====================================================================
65
+ const FX = {};
66
+ const fwalk = (pts, step, fn) => {
67
+ let acc = 0, next = 0;
68
+ for (let i = 0; i < pts.length - 1; i++) {
69
+ const [ax, ay] = pts[i], [bx, by] = pts[i + 1], L = Math.hypot(bx - ax, by - ay) || 1e-6, a = Math.atan2(by - ay, bx - ax);
70
+ while (next <= acc + L) { const u = (next - acc) / L; fn(ax + (bx - ax) * u, ay + (by - ay) * u, a, next); next += step; }
71
+ acc += L;
72
+ }
73
+ };
74
+ const fthread = (ctx, x0, y0, x1, y1, col, w = 2.4) => {
75
+ const rgb = hx(col);
76
+ ctx.lineCap = 'round';
77
+ ctx.strokeStyle = 'rgba(40,25,15,0.38)'; ctx.lineWidth = w + 1.2; ctx.beginPath(); ctx.moveTo(x0 + 0.8, y0 + 1.2); ctx.lineTo(x1 + 0.8, y1 + 1.2); ctx.stroke();
78
+ const g = ctx.createLinearGradient(x0, y0, x1, y1);
79
+ g.addColorStop(0, H.rgba(rgb.map((v) => v * 0.65))); g.addColorStop(0.5, H.rgba(rgb.map((v) => Math.min(255, v * 1.1 + 10)))); g.addColorStop(1, H.rgba(rgb.map((v) => v * 0.65)));
80
+ ctx.strokeStyle = g; ctx.lineWidth = w; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
81
+ ctx.fillStyle = 'rgba(30,15,10,0.45)'; ctx.beginPath(); ctx.arc(x0, y0, 1.1, 0, TAU); ctx.arc(x1, y1, 1.1, 0, TAU); ctx.fill();
82
+ };
83
+ const frun = (ctx, pts, col, len = 12, gap = 7, w = 3, pr = 1) => {
84
+ if (pr <= 0 || pts.length < 2) return;
85
+ let total = 0; for (let i = 0; i < pts.length - 1; i++) total += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
86
+ let start = null;
87
+ fwalk(pts, (len + gap) / 2, (x, y, a, dd) => {
88
+ if (dd > total * pr) return;
89
+ const k = Math.round(dd / ((len + gap) / 2));
90
+ if (k % 2 === 0) start = [x, y]; else if (start) { fthread(ctx, start[0], start[1], start[0] + Math.cos(a) * len, start[1] + Math.sin(a) * len, col, w); start = null; }
91
+ });
92
+ };
93
+ // stitched arrow: running stitch shaft + two stitched head strokes once complete
94
+ const farrow = (ctx, pts, col, pr, w = 3.4) => {
95
+ frun(ctx, pts, col, 14, 7, w, pr);
96
+ if (pr >= 1) { const [x1, y1] = pts[pts.length - 1], [x0, y0] = pts[pts.length - 2], a = Math.atan2(y1 - y0, x1 - x0); fthread(ctx, x1, y1, x1 + Math.cos(a + 2.5) * 24, y1 + Math.sin(a + 2.5) * 24, col, w); fthread(ctx, x1, y1, x1 + Math.cos(a - 2.5) * 24, y1 + Math.sin(a - 2.5) * 24, col, w); }
97
+ };
98
+ const fcurve = (ax, ay, cx, cy, bx, by) => Array.from({ length: 31 }, (_, i) => { const u = i / 30; return [(1 - u) * (1 - u) * ax + 2 * u * (1 - u) * cx + u * u * bx, (1 - u) * (1 - u) * ay + 2 * u * (1 - u) * cy + u * u * by]; });
99
+ const fpat = () => FX.pat || (FX.pat = H.layer(2, 2).getContext('2d').createPattern(feltTex(55), 'repeat'));
100
+ // a felt piece cut to polygon pts (local coords, origin top-left), fuzz + felt texture + soft edge
101
+ const feltSpr = (key, pts, col, w, h, extra) => {
102
+ if (FX[key]) return FX[key];
103
+ const pad = 12, C = H.layer(Math.ceil(w + pad * 2), Math.ceil(h + pad * 2)), c = C.getContext('2d');
104
+ c.translate(pad, pad);
105
+ const path = () => { c.beginPath(); pts.forEach(([x, y], i) => (i ? c.lineTo(x, y) : c.moveTo(x, y))); c.closePath(); };
106
+ path(); c.fillStyle = col; c.fill();
107
+ const fr = H.rng(key.length * 31 + w);
108
+ c.lineCap = 'round';
109
+ fwalk([...pts, pts[0]], 1.6, (x, y, a) => { const nx = Math.sin(a), ny = -Math.cos(a), l = fr.range(0.5, 2.6), tw = fr.range(-0.8, 0.8); c.strokeStyle = col; c.lineWidth = fr.range(0.5, 1.1); c.beginPath(); c.moveTo(x - nx * 1.5, y - ny * 1.5); c.lineTo(x + (nx + tw * ny) * l, y + (ny - tw * nx) * l); c.stroke(); });
110
+ c.save(); c.setTransform(1, 0, 0, 1, 0, 0); c.globalCompositeOperation = 'source-atop';
111
+ const p = fpat(); p.setTransform(new DOMMatrix().translateSelf(fr() * 256, fr() * 256)); c.fillStyle = p; c.fillRect(0, 0, C.width, C.height); c.restore();
112
+ c.save(); c.globalCompositeOperation = 'source-atop'; c.filter = 'blur(3px)'; c.lineWidth = 7; c.strokeStyle = 'rgba(0,0,0,0.22)'; path(); c.stroke(); c.translate(-2, -2); c.lineWidth = 4; c.strokeStyle = 'rgba(255,255,255,0.12)'; path(); c.stroke(); c.restore();
113
+ if (extra) extra(c);
114
+ C.pad = pad; C.w = w; C.h = h;
115
+ return (FX[key] = C);
116
+ };
117
+ // felt letters: cut glyphs with fuzzy edges, a darker cut rim and a soft padded bevel.
118
+ // o.back: colour of a slightly larger felt cut-out the letters are appliqued onto (layered felt, for contrast).
119
+ const feltText = (key, str, px, col, font = '700 PXpx "Fredoka"', o = {}) => {
120
+ if (FX[key]) return FX[key];
121
+ const f = font.replace('PX', px), m = H.layer(4, 4).getContext('2d'); m.font = f;
122
+ const w = Math.ceil(m.measureText(str).width) + 8, h = Math.ceil(px * 1.2), bw = o.back ? Math.round(px * 0.14) : 0, pad = 12 + bw;
123
+ const C = H.layer(w + pad * 2, h + pad * 2), c = C.getContext('2d'), r = H.rng(key.length * 13 + px);
124
+ const tx = pad + 4, ty = pad + px * 0.95, dk = H.rgba(hx(col).map((v) => v * 0.6), 0.6);
125
+ c.font = f; c.lineJoin = 'round';
126
+ if (o.back) { // backing felt: dilated glyphs, own fuzz, texture and rim, with a small shadow from the letter layer
127
+ const B = H.layer(C.width, C.height), b = B.getContext('2d');
128
+ b.font = f; b.lineJoin = 'round'; b.fillStyle = b.strokeStyle = o.back;
129
+ for (let i = 0; i < 18; i++) { b.globalAlpha = 0.2; b.lineWidth = bw * 2 + r.gauss() * 1.6; const ox = r.gauss() * 1.2, oy = r.gauss() * 1.2; b.strokeText(str, tx + ox, ty + oy); b.fillText(str, tx + ox, ty + oy); }
130
+ b.globalAlpha = 1; b.lineWidth = bw * 2 - 1; b.strokeText(str, tx, ty); b.fillText(str, tx, ty);
131
+ b.globalCompositeOperation = 'source-atop'; const pb = fpat(); pb.setTransform(new DOMMatrix().translateSelf(r() * 256, r() * 256)); b.fillStyle = pb; b.fillRect(0, 0, B.width, B.height);
132
+ { // shaded cut edge: dark band = silhouette minus an eroded copy, softened
133
+ const D = H.layer(C.width, C.height), dd = D.getContext('2d');
134
+ dd.fillStyle = 'rgba(70,45,25,0.42)'; dd.fillRect(0, 0, D.width, D.height);
135
+ dd.globalCompositeOperation = 'destination-out'; dd.font = f; dd.lineJoin = 'round'; dd.lineWidth = bw * 2 - 7; dd.strokeText(str, tx + 0.8, ty + 0.8); dd.fillText(str, tx + 0.8, ty + 0.8);
136
+ b.filter = 'blur(2px)'; b.drawImage(D, 0, 0); b.filter = 'none';
137
+ }
138
+ c.drawImage(B, 0, 0);
139
+ c.save(); c.shadowColor = 'rgba(40,20,10,0.45)'; c.shadowBlur = 4; c.shadowOffsetX = 1.5; c.shadowOffsetY = 2.5;
140
+ }
141
+ const L = H.layer(C.width, C.height), l = L.getContext('2d');
142
+ l.font = f; l.lineJoin = 'round';
143
+ l.strokeStyle = dk; l.lineWidth = 3; l.strokeText(str, tx, ty); // darker cut edge under the fuzz
144
+ l.fillStyle = col;
145
+ for (let i = 0; i < 26; i++) { l.globalAlpha = 0.18; l.fillText(str, tx + r.gauss() * 1.4, ty + r.gauss() * 1.4); }
146
+ l.globalAlpha = 1; l.fillText(str, tx, ty);
147
+ l.globalCompositeOperation = 'source-atop'; const p = fpat(); p.setTransform(new DOMMatrix().translateSelf(r() * 256, r() * 256)); l.fillStyle = p; l.fillRect(0, 0, C.width, C.height);
148
+ // padded felt: shaded rim, light catching the top-left
149
+ l.filter = 'blur(2.5px)'; l.lineWidth = Math.max(4, px * 0.06); l.strokeStyle = 'rgba(30,15,5,0.3)'; l.strokeText(str, tx, ty);
150
+ l.lineWidth = Math.max(2, px * 0.03); l.strokeStyle = 'rgba(255,250,235,0.22)'; l.strokeText(str, tx - 1.5, ty - 1.5); l.filter = 'none';
151
+ c.drawImage(L, 0, 0);
152
+ if (o.back) c.restore();
153
+ C.pad = pad; C.w = w; C.h = h;
154
+ return (FX[key] = C);
155
+ };
156
+ // satin-stitched embroidery floss: close slanted stitches across a thick band (heavy, raised, catches the light)
157
+ const fsatin = (ctx, x0, y0, x1, y1, col, w = 13) => {
158
+ const L = Math.hypot(x1 - x0, y1 - y0); if (L < 0.5) return;
159
+ const rgb = hx(col), ux = (x1 - x0) / L, uy = (y1 - y0) / L, nx = -uy, ny = ux, hw = w / 2, sl = w * 0.35;
160
+ ctx.save(); ctx.lineCap = 'round';
161
+ ctx.strokeStyle = 'rgba(40,20,10,0.42)'; ctx.lineWidth = w + 3; ctx.filter = 'blur(1.5px)';
162
+ ctx.beginPath(); ctx.moveTo(x0 + 1.5, y0 + 2.5); ctx.lineTo(x1 + 1.5, y1 + 2.5); ctx.stroke(); ctx.filter = 'none';
163
+ ctx.strokeStyle = H.rgba(rgb.map((v) => v * 0.55)); ctx.lineWidth = w;
164
+ ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
165
+ const dark = H.rgba(rgb.map((v) => v * 0.72)), lite = H.rgba(rgb.map((v) => Math.min(255, v * 1.18 + 18))), hi = 'rgba(255,255,255,0.35)';
166
+ for (let d = -hw * 0.5; d <= L + hw * 0.5; d += 2.1) {
167
+ const cx = x0 + ux * d, cy = y0 + uy * d;
168
+ const ax = cx - nx * hw - ux * sl / 2, ay = cy - ny * hw - uy * sl / 2, bx = cx + nx * hw + ux * sl / 2, by = cy + ny * hw + uy * sl / 2;
169
+ ctx.strokeStyle = dark; ctx.lineWidth = 2.3; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
170
+ ctx.strokeStyle = lite; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.moveTo(ax - 0.4, ay - 0.6); ctx.lineTo(bx - 0.4, by - 0.6); ctx.stroke();
171
+ ctx.strokeStyle = hi; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(cx - nx * hw * 0.5 - 0.5, cy - ny * hw * 0.5 - 0.7); ctx.lineTo(cx - nx * hw * 0.1 - 0.5, cy - ny * hw * 0.1 - 0.7); ctx.stroke();
172
+ }
173
+ ctx.restore();
174
+ };
175
+ // woven label with satin-stitched text
176
+ const tagSpr = (str, px, ink = '#2f5f9a') => {
177
+ const key = 'tag' + str + px + ink;
178
+ if (FX[key]) return FX[key];
179
+ const m = H.layer(4, 4).getContext('2d'); m.font = `700 ${px}px "Fredoka"`;
180
+ const lw = Math.ceil(m.measureText(str).width + px * 1.4), lh = Math.ceil(px * 1.75), pad = 10;
181
+ const C = H.layer(lw + pad * 2, lh + pad * 2), c = C.getContext('2d');
182
+ c.translate(pad, pad);
183
+ c.fillStyle = '#f3ecdc'; c.fillRect(0, 0, lw, lh);
184
+ c.save(); c.beginPath(); c.rect(0, 0, lw, lh); c.clip();
185
+ c.strokeStyle = 'rgba(120,100,70,0.12)'; c.lineWidth = 1; for (let k = -lw; k < lw; k += 3) { c.beginPath(); c.moveTo(k, 0); c.lineTo(k + lh, lh); c.stroke(); }
186
+ c.fillStyle = '#c8443a'; c.fillRect(0, 4, lw, 2.5); c.fillRect(0, lh - 6.5, lw, 2.5);
187
+ const fg = c.createLinearGradient(0, 0, 12, 0); fg.addColorStop(0, 'rgba(90,60,30,0.3)'); fg.addColorStop(1, 'rgba(90,60,30,0)'); c.fillStyle = fg; c.fillRect(0, 0, 12, lh);
188
+ const fg2 = c.createLinearGradient(lw, 0, lw - 12, 0); fg2.addColorStop(0, 'rgba(90,60,30,0.3)'); fg2.addColorStop(1, 'rgba(90,60,30,0)'); c.fillStyle = fg2; c.fillRect(lw - 12, 0, 12, lh);
189
+ c.restore();
190
+ const M = H.layer(lw, lh), mc = M.getContext('2d');
191
+ mc.font = `700 ${px}px "Fredoka"`; mc.textAlign = 'center'; mc.textBaseline = 'middle'; mc.fillStyle = '#fff'; mc.fillText(str, lw / 2, lh / 2 + 2);
192
+ mc.globalCompositeOperation = 'source-in'; mc.fillStyle = ink; mc.fillRect(0, 0, lw, lh);
193
+ mc.globalCompositeOperation = 'source-atop'; mc.lineWidth = 1;
194
+ for (let k = -lh; k < lw + lh; k += 2) { mc.strokeStyle = 'rgba(255,255,255,0.28)'; mc.beginPath(); mc.moveTo(k, 0); mc.lineTo(k + lh * 0.35, lh); mc.stroke(); mc.strokeStyle = 'rgba(0,0,30,0.25)'; mc.beginPath(); mc.moveTo(k + 1, 0); mc.lineTo(k + 1 + lh * 0.35, lh); mc.stroke(); }
195
+ c.save(); c.shadowColor = 'rgba(20,10,5,0.45)'; c.shadowBlur = 1.5; c.shadowOffsetX = 0.8; c.shadowOffsetY = 1.2; c.drawImage(M, 0, 0); c.restore();
196
+ frun(c, [[7, 4], [7, lh - 4]], '#c8443a', 6, 4, 1.8); frun(c, [[lw - 7, 4], [lw - 7, lh - 4]], '#c8443a', 6, 4, 1.8);
197
+ C.pad = pad; C.w = lw; C.h = lh;
198
+ return (FX[key] = C);
199
+ };
200
+ const circPts = (cx, cy, rx, ry, n = 90, wob = 0.02, seed = 0) => { const nz = FX.nz || (FX.nz = H.makeNoise(21)); return Array.from({ length: n }, (_, i) => { const a = (i / n) * TAU, k = 1 + nz.n2(Math.cos(a) + seed, Math.sin(a)) * wob; return [cx + Math.cos(a) * rx * k, cy + Math.sin(a) * ry * k]; }); };
201
+
202
+ const EXPLAIN = (ctx, W, H_, t) => {
203
+ const seg = (a, b) => H.ease.inOut(H.clamp((t - a) / (b - a)));
204
+ const prog = (a, b) => H.clamp((t - a) / (b - a));
205
+ // drop-on: falls in large with lifted shadow, squashes, settles
206
+ const put = (spr, x, y, st, o = {}) => {
207
+ if (!spr || t < st) return;
208
+ const u = H.clamp((t - st) / 0.45), lift = 1 - H.ease.out(u), sq = u > 0.45 && u < 1 ? Math.sin(((u - 0.45) / 0.55) * Math.PI) * 0.04 : 0;
209
+ const cx = x + spr.w / 2, cy = y + spr.h / 2;
210
+ ctx.save();
211
+ ctx.shadowColor = `rgba(40,20,10,${0.45 - lift * 0.2})`; ctx.shadowBlur = (o.sb ?? 10) + lift * 26; ctx.shadowOffsetX = (o.sx ?? 4) + lift * 18; ctx.shadowOffsetY = (o.sy ?? 6) + lift * 26;
212
+ ctx.globalAlpha = H.clamp(u * 5);
213
+ ctx.translate(cx, cy - lift * 30); if (o.rot) ctx.rotate(o.rot); ctx.scale((1 + lift * 0.14 + sq) * (o.s || 1), (1 + lift * 0.14 - sq) * (o.s || 1)); ctx.translate(-cx, -cy);
214
+ ctx.drawImage(spr, x - spr.pad, y - spr.pad); ctx.restore();
215
+ };
216
+ // ---- camera pan along the quilt ----
217
+ const cam = 1280 * seg(1.9, 2.6) + 700 * seg(5.6, 6.3) + 1300 * seg(10.0, 10.7);
218
+ // ---- quilt (baked once, 2560 wide, tiled) ----
219
+ if (!FX.quilt) {
220
+ const QW = 2560, P = 160, C = H.layer(QW, H_), c = C.getContext('2d'), img = c.createImageData(QW, H_), d = img.data;
221
+ const nz = H.makeNoise(21), r = H.rng(314), cols = QW / P, rows = Math.ceil(H_ / P) + 1, pick = [];
222
+ for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) { let k; do { k = r.int(0, PRINTS.length - 1); } while ((i > 0 && pick[j * cols + i - 1] === k) || (j > 0 && pick[(j - 1) * cols + i] === k)); pick.push(k); }
223
+ const grain = H.rng(9), lnz = H.makeNoise(77);
224
+ for (let y = 0; y < H_; y++) {
225
+ const yy = y + 30, j = Math.floor(yy / P), v = yy - j * P;
226
+ // broad uneven light, periodic over the 2560px tile (sampled on a cylinder so the tile wrap has no seam), every 8px
227
+ if (y % 8 === 0) { // coarse 8px grid rows, blended vertically below
228
+ FX.lg0 = FX.lg1 || null; FX.lg1 = []; const y8 = y + 8;
229
+ for (let x = 0; x <= QW; x += 8) { const a = (x / QW) * TAU; FX.lg1.push(0.85 + 0.15 * lnz.fbm3(Math.cos(a) * 0.9, Math.sin(a) * 0.9, y8 * 0.0035, 3)); }
230
+ if (!FX.lg0) { FX.lg0 = []; for (let x = 0; x <= QW; x += 8) { const a = (x / QW) * TAU; FX.lg0.push(0.85 + 0.15 * lnz.fbm3(Math.cos(a) * 0.9, Math.sin(a) * 0.9, y * 0.0035, 3)); } }
231
+ }
232
+ const yt = (y % 8) / 8, lrow = FX.lg0.map((v, q) => v + (FX.lg1[q] - v) * yt);
233
+ for (let x = 0; x < QW; x++) {
234
+ const i = Math.floor(x / P), u = x - i * P, pr = PRINTS[pick[j * cols + i]];
235
+ const col = H.mix(printCol(pr, Math.max(0, u + nz.n2(x * 0.02, y * 0.02) * 1.2), Math.max(0, v + nz.n2(x * 0.02 + 9, y * 0.02) * 1.2)), [214, 200, 178], 0.3);
236
+ const wx = x % 3, wy = y % 3, over = (Math.floor(x / 3) + Math.floor(y / 3)) & 1;
237
+ const wv = 1 + (over ? (wx === 1 ? 0.05 : -0.02) : (wy === 1 ? 0.05 : -0.02)) - (wx === 0 && wy === 0 ? 0.06 : 0) + (grain() - 0.5) * 0.06;
238
+ const e = Math.min(u, P - u, v, P - v), puff = 0.93 - 0.36 * Math.exp(-e / 14) + (0.08 * (P / 2 - u + (P / 2 - v))) / P, seam = Math.exp(-(e * e) / 2.5) * 0.45;
239
+ const lq = x >> 3, lf = lrow[lq] + ((lrow[lq + 1] ?? lrow[0]) - lrow[lq]) * ((x & 7) / 8);
240
+ const s = wv * puff * (1 - seam) * lf, k = (y * QW + x) * 4;
241
+ d[k] = col[0] * s; d[k + 1] = col[1] * s; d[k + 2] = col[2] * s; d[k + 3] = 255;
242
+ }
243
+ }
244
+ c.putImageData(img, 0, 0);
245
+ for (let gx = 9; gx < QW; gx += P) frun(c, [[gx, -10], [gx, H_ + 10]], '#f4ead6', 10, 7, 2.4);
246
+ for (let gy = -21; gy < H_ + P; gy += P) frun(c, [[-10, gy], [QW + 10, gy]], '#f4ead6', 10, 7, 2.4);
247
+ FX.quilt = C;
248
+ }
249
+ const qx = -(cam % 2560);
250
+ ctx.drawImage(FX.quilt, qx, 0); ctx.drawImage(FX.quilt, qx + 2560, 0);
251
+
252
+ ctx.save(); ctx.translate(-cam, 0); // ======== world ========
253
+
254
+ // ---- BEAT 1: felt letters dropped on ----
255
+ {
256
+ const cols = ['#d9564a', '#2f5f9a', '#e8b04a', '#6fb3a6', '#ef9aa6'];
257
+ const line = (str, px, y, st, off) => {
258
+ const m = H.layer(4, 4).getContext('2d'); m.font = `700 ${px}px "Fredoka"`;
259
+ let x = 640 - (m.measureText(str).width + str.length * 6) / 2, i = 0;
260
+ for (const ch of str) { const w = m.measureText(ch).width; if (ch !== ' ') put(feltText('L' + ch + px + (i + off) % 5, ch, px, cols[(i + off) % 5]), x, y, st + i * 0.07, { rot: (H.rng(i + off)() - 0.5) * 0.12 }); x += w + 6; i++; }
261
+ };
262
+ // felt backing ovals the letters sit on
263
+ put(feltSpr('b1back', circPts(330, 150, 330, 150, 120, 0.03, 1), '#f3ecdc', 660, 300), 310, 150, 0.0, { sb: 16 });
264
+ line('How does', 100, 178, 0.15, 0);
265
+ line('AI learn?', 116, 296, 0.75, 2);
266
+ // blanket stitch around the backing, sewn on
267
+ const bp = prog(0.4, 1.8), ring = circPts(640, 300, 340, 160, 160, 0.03, 1);
268
+ for (let i = 0; i < ring.length * bp; i += 2) { const [x, y] = ring[i], dx = x - 640, dy = y - 300, d = Math.hypot(dx / 340, dy / 160) || 1, ux = dx / 340 / d, uy = dy / 160 / d, n = Math.hypot(ux, uy); fthread(ctx, x - (ux / n) * 14, y - (uy / n) * 14, x + (ux / n) * 2, y + (uy / n) * 2, '#c8443a', 2.6); if (i > 1) fthread(ctx, ring[i - 2][0], ring[i - 2][1], x, y, '#c8443a', 2.6); }
269
+ }
270
+
271
+ // ---- BEAT 2: labelled patches slide along a stitched path into the felt brain ----
272
+ const BX = 2350, BY = 380; // brain centre (world)
273
+ const brain = feltSpr('brain', (() => { const pts = []; for (let i = 0; i < 100; i++) { const a = (i / 100) * TAU, k = 1 + 0.08 * Math.sin(a * 7) + 0.04 * Math.sin(a * 13); pts.push([150 + Math.cos(a) * 145 * k, 115 + Math.sin(a) * 112 * k]); } return pts; })(), '#e9879a', 300, 230,
274
+ (c) => { [[[150, 10], [150, 220]], [[60, 70], [100, 95], [80, 130], [110, 165]], [[200, 60], [240, 85], [215, 120], [250, 150]], [[105, 40], [125, 75]], [[185, 175], [215, 200]], [[40, 120], [70, 150]]].forEach((ln) => frun(c, ln, '#a8455c', 9, 5, 2.4)); });
275
+ put(brain, BX - 150, BY - 115, 2.3, { sb: 20, sx: 8, sy: 12 });
276
+ put(tagSpr('model', 34), BX - 70, BY + 140, 2.6, { rot: -0.05 });
277
+ // buttons = the model's knobs (thread X turns when it adjusts)
278
+ const turn = H.ease.inOut(prog(8.9, 9.6));
279
+ [[-80, '#e8b04a'], [0, '#6fb3a6'], [80, '#d9564a']].forEach(([dx, col], i) => {
280
+ const x = BX + dx, y = BY + 222, R = 30, st = 2.5 + i * 0.1;
281
+ if (t < st) return;
282
+ const s = Math.max(0.15, H.ease.back(prog(st, st + 0.35))), rgb = hx(col);
283
+ ctx.save(); ctx.translate(x, y); ctx.scale(s, s);
284
+ ctx.shadowColor = 'rgba(40,20,10,0.5)'; ctx.shadowBlur = 8; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 5;
285
+ const g = ctx.createRadialGradient(-R * 0.35, -R * 0.4, R * 0.1, 0, 0, R); g.addColorStop(0, H.rgba(rgb.map((v) => Math.min(255, v * 1.25 + 20)))); g.addColorStop(0.7, H.rgba(rgb)); g.addColorStop(1, H.rgba(rgb.map((v) => v * 0.7)));
286
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0, 0, R, 0, TAU); ctx.fill(); ctx.restore();
287
+ if (s < 0.99) return;
288
+ ctx.strokeStyle = H.rgba(rgb.map((v) => v * 0.6), 0.6); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, R * 0.72, 0, TAU); ctx.stroke();
289
+ const a = [0.2, 0.9, -0.4][i] + [1.4, -1.1, 1.6][i] * turn, hs = R * 0.3;
290
+ const hp = [0, 1, 2, 3].map((k) => [x + Math.cos(a + k * Math.PI / 2 + Math.PI / 4) * hs, y + Math.sin(a + k * Math.PI / 2 + Math.PI / 4) * hs]);
291
+ fthread(ctx, hp[0][0], hp[0][1], hp[2][0], hp[2][1], '#f4ead2', 3); fthread(ctx, hp[1][0], hp[1][1], hp[3][0], hp[3][1], '#f4ead2', 3);
292
+ ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.beginPath(); ctx.ellipse(x - R * 0.45, y - R * 0.5, R * 0.18, R * 0.08, -0.6, 0, TAU); ctx.fill();
293
+ });
294
+ // example patches
295
+ const patch = (kind) => FX['patch' + kind] || (FX['patch' + kind] = (() => {
296
+ const cat = kind === 'cat';
297
+ const base = feltSpr('pbase' + kind, [[0, 0], [150, 0], [150, 150], [0, 150]], cat ? '#9fc6d6' : '#f1d27a', 150, 150, (c) => {
298
+ const head = cat ? [[30, 70], [38, 22], [62, 48], [88, 48], [112, 22], [120, 70], [112, 105], [75, 122], [38, 105]] : circPts(75, 75, 46, 42, 60, 0.02, 3);
299
+ c.save(); c.shadowColor = 'rgba(40,20,10,0.4)'; c.shadowBlur = 5; c.shadowOffsetX = 2; c.shadowOffsetY = 3;
300
+ c.beginPath(); head.forEach(([x, y], i) => (i ? c.lineTo(x, y) : c.moveTo(x, y))); c.closePath(); c.fillStyle = cat ? '#f08a3c' : '#a8673c'; c.fill();
301
+ if (!cat) { c.fillStyle = '#6e4428'; c.beginPath(); c.ellipse(32, 72, 13, 30, 0.25, 0, TAU); c.fill(); c.beginPath(); c.ellipse(118, 72, 13, 30, -0.25, 0, TAU); c.fill(); }
302
+ c.restore();
303
+ c.fillStyle = '#2a2433'; [[60, 70], [90, 70]].forEach(([x, y]) => { c.beginPath(); c.arc(x, y, 5.5, 0, TAU); c.fill(); });
304
+ c.fillStyle = cat ? '#e0607a' : '#2a2433'; c.beginPath(); c.ellipse(75, 88, 7, 5, 0, 0, TAU); c.fill();
305
+ frun(c, [[8, 8], [142, 8], [142, 142], [8, 142], [8, 8]], '#f4ead2', 8, 5, 2);
306
+ });
307
+ const tg = tagSpr(kind, 30, cat ? '#c8443a' : '#2f5f9a');
308
+ const C = H.layer(base.width, base.height + 50), c = C.getContext('2d');
309
+ c.drawImage(base, 0, 0);
310
+ c.save(); c.shadowColor = 'rgba(40,20,10,0.4)'; c.shadowBlur = 4; c.shadowOffsetX = 2; c.shadowOffsetY = 3; c.translate(base.pad + 75, base.pad + 150); c.rotate(-0.04); c.drawImage(tg, -tg.width / 2, -tg.height / 2); c.restore();
311
+ C.pad = base.pad; C.w = 150; C.h = 190;
312
+ return C;
313
+ })());
314
+ if (t < 6.4) {
315
+ // counter: felt digit tiles re-cut as the number climbs
316
+ if (t >= 2.45) {
317
+ const n = Math.round(1e6 * Math.pow(prog(2.7, 5.1), 2)), str = n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
318
+ let x = 1280 + 50;
319
+ put(feltSpr('cback', [[0, 6], [440, 0], [446, 118], [4, 124]], '#2f4f7a', 446, 124, (c) => frun(c, [[10, 14], [434, 9], [438, 112], [12, 116], [10, 14]], '#f4ead2', 8, 5, 2)), 1310, 72, 2.4, { sb: 12 });
320
+ for (const ch of str) { const s = feltText('D' + ch, ch, 96, '#e8b04a'); put(s, x, 70, 2.45); x += s.w - 6; }
321
+ put(tagSpr('examples', 36, '#3f8c86'), x + 18, 104, 2.6);
322
+ }
323
+ farrow(ctx, fcurve(1300, 250, 1650, 205, 2140, 270), '#c8443a', prog(2.6, 3.4));
324
+ const KINDS = ['cat', 'dog', 'dog', 'cat', 'dog', 'cat', 'cat', 'dog'];
325
+ KINDS.forEach((k, i) => {
326
+ const s0 = 2.55 + i * 0.36, x = 1160 + (t - s0) * 640;
327
+ if (t < s0 || x > BX - 110) return;
328
+ const sh = H.clamp((BX - 110 - x) / 130);
329
+ ctx.save(); ctx.shadowColor = 'rgba(40,20,10,0.45)'; ctx.shadowBlur = 12; ctx.shadowOffsetX = 6; ctx.shadowOffsetY = 9;
330
+ const p = patch(k); ctx.translate(x + 75, 390 + 95); ctx.rotate(Math.sin(t * 6 + i) * 0.03); ctx.scale(sh, sh); ctx.drawImage(p, -75 - p.pad, -95 - p.pad); ctx.restore();
331
+ });
332
+ // brain re-drawn on top so patches go "into" it
333
+ if (t > 2.75) { ctx.save(); ctx.shadowColor = 'rgba(40,20,10,0.4)'; ctx.shadowBlur = 20; ctx.shadowOffsetX = 8; ctx.shadowOffsetY = 12; ctx.drawImage(brain, BX - 150 - brain.pad, BY - 115 - brain.pad); ctx.restore(); }
334
+ }
335
+
336
+ // ---- BEAT 3: guess -> check -> adjust (a stitched loop) ----
337
+ if (t > 5.9) {
338
+ put(patch('cat'), 2000, 300, 6.25, { rot: -0.04 });
339
+ put(tagSpr('new photo', 28), 1990, 255, 6.4, { rot: -0.05 });
340
+ farrow(ctx, [[2160, 400], [2196, 400]], '#c8443a', prog(6.5, 6.7));
341
+ farrow(ctx, fcurve(2510, 330, 2600, 280, 2700, 320), '#c8443a', prog(6.8, 7.2));
342
+ put(tagSpr('Guess', 36), 2540, 210, 7.0, { rot: -0.06 });
343
+ const bub = feltSpr('bub', (() => { const pts = []; for (let i = 0; i <= 80; i++) { const a = 0.62 * Math.PI + (i / 80) * (2 * Math.PI - 0.22 * Math.PI); pts.push([185 + Math.cos(a) * 180, 105 + Math.sin(a) * 100]); } pts.push([55, 236]); return pts; })(), '#f6f1e7', 370, 240);
344
+ const wrong = t < 9.55, shake = t > 7.9 && t < 8.35 ? Math.sin(t * 70) * 7 : 0;
345
+ put(bub, 2740 + shake, 220, 7.25, { sb: 14 });
346
+ const ans = wrong ? feltText('Tdog', 'dog?', 104, '#2f5f9a') : feltText('Tcat', 'cat', 112, '#4f9a52');
347
+ put(ans, 2925 - ans.w / 2 + shake, 262, wrong ? 7.45 : 9.6);
348
+ if (wrong && t > 7.85) { // emphasis: red X + blanket-stitch ring sewn around the wrong guess
349
+ const xr = prog(7.85, 8.05);
350
+ if (xr > 0) { fsatin(ctx, 3060, 200, 3060 + 80 * xr, 200 + 80 * xr, '#c8443a', 14); if (xr > 0.5) fsatin(ctx, 3140, 200, 3140 - 80 * (xr - 0.5) * 2, 200 + 80 * (xr - 0.5) * 2, '#c8443a', 14); }
351
+ const ring = circPts(2925, 330, 225, 140, 120, 0.02, 9), rp = prog(7.9, 8.4);
352
+ for (let i = 0; i < ring.length * rp; i += 2) { const [x, y] = ring[i]; const dx = x - 2925, dy = y - 330, d = Math.hypot(dx, dy); fthread(ctx, x - (dx / d) * 12, y - (dy / d) * 12, x, y, '#c8443a', 2.6); if (i > 1) fthread(ctx, ring[i - 2][0], ring[i - 2][1], x, y, '#c8443a', 2.6); }
353
+ put(tagSpr('Check', 38, '#c8443a'), 2840, 485, 8.05, { rot: 0.05 });
354
+ farrow(ctx, fcurve(2860, 540, 2700, 660, 2480, 610), '#c8443a', prog(8.35, 8.85));
355
+ put(tagSpr('Adjust', 38, '#c8443a'), 2560, 640, 8.7, { rot: -0.03 });
356
+ }
357
+ if (!wrong) { const vr = prog(9.85, 10.05); if (vr > 0) { fsatin(ctx, 3070, 240, 3070 + 26 * Math.min(1, vr * 2), 240 + 26 * Math.min(1, vr * 2), '#4f9a52', 14); if (vr > 0.5) fsatin(ctx, 3096, 266, 3096 + 60 * (vr - 0.5) * 2, 266 - 70 * (vr - 0.5) * 2, '#4f9a52', 14); } }
358
+ }
359
+
360
+ // ---- BEAT 4: felt bar chart, buttons on top, accuracy counts up ----
361
+ if (t > 9.9) {
362
+ const OXw = 3600, base = 600, accs = [61, 72, 80, 86, 90, 93, 95, 97], cols = ['#e8b04a', '#6fb3a6'];
363
+ let shown = 61;
364
+ frun(ctx, [[OXw, base + 8], [OXw + 760, base + 8]], '#3d3548', 14, 7, 3.4, prog(10.5, 11.0));
365
+ accs.forEach((a, i) => {
366
+ const st = 10.65 + i * 0.25, hgt = Math.round(a * 3.6);
367
+ const bar = feltSpr('bar' + i, [[0, 0], [62, 0], [62, hgt], [0, hgt]], i === 7 ? '#d9564a' : cols[i % 2], 62, hgt, (c) => frun(c, [[8, 8], [54, 8], [54, hgt - 6], [8, hgt - 6], [8, 8]], '#f4ead2', 7, 5, 1.8));
368
+ put(bar, OXw + 30 + i * 90, base - hgt, st, { sb: 12 });
369
+ if (t >= st) shown = H.lerp(i ? accs[i - 1] : 61, a, prog(st, st + 0.25));
370
+ });
371
+ put(tagSpr('Accuracy', 44, '#3f8c86'), 3320, 140, 10.45, { rot: -0.03 });
372
+ const str = Math.round(shown) + '%', hot = t > 12.6;
373
+ const bounce = hot ? 1 + 0.18 * Math.sin(prog(12.6, 12.95) * Math.PI) : 1;
374
+ let x = 3315;
375
+ ctx.save(); ctx.translate(3430, 290); ctx.scale(bounce, bounce); ctx.translate(-3430, -290);
376
+ { const s = feltText('A' + str + hot, str, 130, hot ? '#d9564a' : '#2f5f9a', undefined, { back: '#f4ecdc' }); put(s, x, 225, 10.5, { sb: 18, sx: 6, sy: 10 }); }
377
+ ctx.restore();
378
+ if (hot) { const star = feltSpr('star', Array.from({ length: 10 }, (_, i) => { const a = -Math.PI / 2 + (i / 10) * TAU, r = i % 2 ? 26 : 60; return [62 + Math.cos(a) * r, 62 + Math.sin(a) * r]; }), '#e8b04a', 124, 124, (c) => frun(c, Array.from({ length: 11 }, (_, i) => { const a = -Math.PI / 2 + (i / 10) * TAU, r = i % 2 ? 16 : 40; return [62 + Math.cos(a) * r, 62 + Math.sin(a) * r]; }), '#c8443a', 5, 4, 1.8)); put(star, OXw + 30 + 7 * 90 - 30, base - 97 * 3.6 - 130, 12.6, { rot: 0.1 }); }
379
+ put(tagSpr('training rounds', 32), 3830, 622, 11.0);
380
+ }
381
+ ctx.restore(); // ======== end world ========
382
+
383
+ // header tag + takeaway (screen space)
384
+ put(tagSpr('How does AI learn?', 30), 24, 16, 2.25, { rot: -0.02 });
385
+ if (t >= 13.0) {
386
+ const tg = tagSpr('More examples → better guesses', 46, '#2f5f9a');
387
+ put(tg, 640 - tg.w / 2, 600, 13.0, { sb: 16, rot: -0.01 });
388
+ }
389
+ // studio light + grain
390
+ ctx.save(); ctx.globalCompositeOperation = 'soft-light';
391
+ const lg = ctx.createRadialGradient(380 + t * 10, 190, 60, 600, 360, 900); lg.addColorStop(0, 'rgba(255,245,220,0.6)'); lg.addColorStop(1, 'rgba(30,20,10,0.62)');
392
+ ctx.fillStyle = lg; ctx.fillRect(0, 0, W, H_); ctx.restore();
393
+ H.vignette(ctx, W, H_, 0.55, '35,20,10');
394
+ H.grain(ctx, W, H_, 0.025, 6 + Math.floor(t * 12));
395
+ };
396
+
397
+ registerStyle({
398
+ id: 'felt-quilt',
399
+ name: 'Felt & Patchwork',
400
+ category: 'Paper & Craft',
401
+ description: 'A cosy patchwork quilt of cotton prints with puffy quilted squares, topped with fuzzy felt appliqué edged in blanket stitch, button "nodes" sewn on and joined by running-stitch thread, plus an embroidered label.',
402
+ motion: 'Thread draws itself stitch by stitch between the buttons, felt pieces drop on with a soft squash, and the quilt breathes with a slow puff of the padding.',
403
+ fonts: ['700 42px "Fredoka"'],
404
+ frameT: 4.8,
405
+ duration: 5,
406
+ readsPixels: true,
407
+ explainerDuration: 15,
408
+ explainerNote: 'The camera pans along one long patchwork quilt; felt pieces drop on with a lifted shadow and soft squash, arrows and emphasis rings are thread sewn on stitch by stitch, labels are woven tags stitched onto what they name, the counter is cut felt digits swapped as it climbs, and the model\'s weights are buttons whose thread-X turns when it adjusts.',
409
+ explainer(ctx, W, H_, t) { EXPLAIN(ctx, W, H_, t); },
410
+ // Sound: felt and buttons on a quilt. Felt pieces fall in with a lifted shadow and land as a soft, dull "fump" at
411
+ // the moment they touch (put(): squash starts at u = 0.45 of 0.45 s, so st + 0.2); thread sewn stitch by stitch is
412
+ // a needle through thick felt with the thread drawn after it (one pass about every 0.12 s); buttons click on as
413
+ // small wooden toks; the camera pans are the quilt drawn across the table. Times mirror EXPLAIN() above.
414
+ explainerSound(k) {
415
+ const LAND = 0.2;
416
+ const seg = (t, a, b) => H.ease.inOut(H.clamp((t - a) / (b - a)));
417
+ const cam = (t) => 1280 * seg(t, 1.9, 2.6) + 700 * seg(t, 5.6, 6.3) + 1300 * seg(t, 10.0, 10.7);
418
+ const pan = (wx, t) => H.clamp(((wx - cam(t)) / 1280) * 2 - 1, -1, 1) * 0.6;
419
+ let seed = 11; const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
420
+ const fump = (t, g = 1, p = 0, minor = true) => { // felt landing on quilted cotton: soft and dull
421
+ k.noise(t, { dur: 0.09, color: 'brown', filter: 'lowpass', f: 420, gain: 0.42 * g, a: 0.006, pan: p, minor });
422
+ k.noise(t, { dur: 0.05, color: 'pink', filter: 'bandpass', f: 700, q: 0.8, gain: 0.1 * g, a: 0.004, pan: p, minor });
423
+ k.tone(t, { f: [[0, 120], [0.07, 70]], dur: 0.09, gain: 0.12 * g, pan: p, minor });
424
+ };
425
+ const pass = (t, g = 1, p = 0, minor = true) => { // needle through felt, thread drawn after it
426
+ k.noise(t, { dur: 0.025, filter: 'bandpass', f: 800, q: 1.3, gain: 0.26 * g, a: 0.002, pan: p, minor });
427
+ const pd = 0.07 + rnd() * 0.03;
428
+ k.noise(t + 0.03, { dur: pd, filter: 'bandpass', f: [[0, 1400], [pd, 950]], q: 0.9, gain: 0.15 * g, a: pd * 0.6, pan: p, minor });
429
+ };
430
+ const sew = (t0, t1, g, p0, p1) => { for (let t = t0; t < t1 - 0.03; t += 0.115 + rnd() * 0.02) pass(t, g, p0 + (p1 - p0) * ((t - t0) / (t1 - t0))); };
431
+ const button = (t, g = 1, p = 0, minor = true) => { k.play('tock', t, { f: 520, gain: 0.8 * g, pan: p, minor }); k.noise(t, { dur: 0.04, color: 'brown', filter: 'lowpass', f: 500, gain: 0.2 * g, pan: p, minor }); };
432
+ const drag = (t, d, g = 1) => { // the quilt drawn along the table
433
+ k.noise(t, { dur: d + 0.1, color: 'pink', filter: 'bandpass', f: [[0, 320], [d * 0.5, 700], [d + 0.1, 320]], q: 0.6, gain: 0.1 * g, a: d * 0.45 });
434
+ k.noise(t, { dur: d + 0.1, color: 'brown', filter: 'lowpass', f: 280, gain: 0.075 * g, a: d * 0.5 });
435
+ };
436
+
437
+ k.bed('room', 0, 15, { gain: 0.4 });
438
+ k.music('pad', 0.2, 14.8, { bpm: 62, gain: 0.22, chords: [['C3', 'E3', 'G3', 'D4'], ['A2', 'E3', 'G3', 'C4'], ['F2', 'A2', 'C3', 'G3'], ['G2', 'B2', 'D3', 'A3']] });
439
+
440
+ // BEAT 1: backing oval, felt letters dropped on (0.15 + i*0.07, 0.75 + i*0.07), blanket stitch sewn round (0.4..1.8)
441
+ fump(0.0 + LAND, 1.1, 0, false);
442
+ {
443
+ const ts = [];
444
+ [...'How does'].forEach((ch, i) => ch !== ' ' && ts.push([0.15 + i * 0.07 + LAND, i / 8]));
445
+ [...'AI learn?'].forEach((ch, i) => ch !== ' ' && ts.push([0.75 + i * 0.07 + LAND, i / 9]));
446
+ let last = -1; ts.forEach(([t, u]) => { if (t - last >= 0.09) { fump(t, 0.55, (u - 0.5) * 0.8); last = t; } });
447
+ }
448
+ sew(0.4, 1.8, 0.6, -0.4, 0.4);
449
+
450
+ // BEAT 2
451
+ drag(1.9, 0.7, 1);
452
+ fump(2.25 + LAND, 0.5, -0.8); // header tag
453
+ fump(2.3 + LAND, 1.3, pan(2350, 2.5), false); // the felt brain
454
+ fump(2.4 + LAND, 0.8, pan(1530, 2.6)); fump(2.45 + LAND, 0.6, pan(1500, 2.65)); // counter backing + digits
455
+ [0, 1, 2].forEach((i) => button(2.5 + i * 0.1 + 0.13, 0.8, pan(2270 + i * 80, 2.7))); // buttons
456
+ fump(2.6 + LAND, 0.55, pan(2280, 2.8)); // 'model' + 'examples' tags
457
+ sew(2.6, 3.4, 0.75, pan(1300, 2.6), pan(2140, 3.4)); // red thread arrow
458
+ for (let i = 0; i < 8; i++) { const tin = 2.55 + i * 0.36 + (2350 - 110 - 1160) / 640; fump(tin, 0.4, pan(2240, tin)); } // patches tucked into the brain
459
+ { // counter: felt digits swapped as n = 1e6 * prog(2.7, 5.1)^2 climbs
460
+ for (let t = 2.78; t < 5.04;) { const s = H.clamp((t - 2.7) / 2.4); k.noise(t, { dur: 0.04, color: 'brown', filter: 'lowpass', f: 600, gain: 0.3, a: 0.002, pan: pan(1500, t), minor: true }); k.noise(t, { dur: 0.02, filter: 'bandpass', f: 1100, q: 1.2, gain: 0.1, a: 0.001, pan: pan(1500, t), minor: true }); t += 0.15 - 0.06 * s; }
461
+ fump(5.1, 1.0, pan(1500, 5.1), false); button(5.1, 0.6, pan(1500, 5.1));
462
+ }
463
+
464
+ // BEAT 3
465
+ drag(5.6, 0.7, 0.8);
466
+ fump(6.25 + LAND, 1.0, pan(2075, 6.45), false); // new cat patch
467
+ fump(6.4 + LAND, 0.5, pan(2050, 6.6));
468
+ sew(6.5, 6.7, 0.7, pan(2160, 6.5), pan(2196, 6.7));
469
+ sew(6.8, 7.2, 0.7, pan(2510, 6.8), pan(2700, 7.2));
470
+ fump(7.0 + LAND, 0.5, pan(2580, 7.2)); // 'Guess'
471
+ fump(7.25 + LAND, 0.9, pan(2925, 7.45)); // bubble
472
+ fump(7.45 + LAND, 0.75, pan(2925, 7.65)); // 'dog?'
473
+ // wrong guess (7.85): thick red thread yanked through as an X, a blanket-stitch ring sewn round it, the bubble jiggles
474
+ k.noise(7.85, { dur: 0.2, filter: 'bandpass', f: [[0, 1500], [0.2, 700]], q: 0.8, gain: 0.32, a: 0.01, pan: pan(3100, 7.9) });
475
+ k.play('thud', 7.86, { f: 110, gain: 0.6, pan: pan(3000, 7.9) });
476
+ k.tone(7.88, { f: 'D3', type: 'triangle', dur: 0.16, gain: 0.2, filter: 'lowpass', f2: 700 });
477
+ k.tone(8.04, { f: 'A2', type: 'triangle', dur: 0.32, gain: 0.22, filter: 'lowpass', f2: 600 });
478
+ sew(8.1, 8.4, 0.6, 0.3, 0.6);
479
+ fump(8.05 + LAND, 0.55, pan(2900, 8.25)); // 'Check'
480
+ sew(8.35, 8.85, 0.75, pan(2860, 8.35), pan(2480, 8.85)); // red thread arrow back to the buttons
481
+ fump(8.7 + LAND, 0.55, pan(2620, 8.9)); // 'Adjust'
482
+ for (let i = 1; i <= 5; i++) { let t = 8.9; while (seg(t, 8.9, 9.6) < i / 5 - 1e-6) t += 0.005; button(t, 0.45, pan(2350, t)); } // buttons twist
483
+ // correction: 'cat' dropped on, green check sewn (9.85..10.05)
484
+ fump(9.6 + LAND, 1.0, pan(2925, 9.8), false);
485
+ pass(9.85, 0.9, pan(3090, 9.9), false); pass(9.97, 0.7, pan(3110, 10));
486
+ k.play('success', 9.86, { f: 'G5', gain: 0.45, pan: pan(3090, 9.9) });
487
+
488
+ // BEAT 4
489
+ drag(10.0, 0.7, 1);
490
+ fump(10.45 + LAND, 0.55, pan(3420, 10.65)); fump(10.5 + LAND, 0.8, pan(3420, 10.7)); // 'Accuracy' tag + % digits
491
+ sew(10.5, 11.0, 0.5, pan(3600, 10.5), pan(4360, 11.0)); // axis thread
492
+ fump(11.0 + LAND, 0.45, pan(3950, 11.2)); // 'training rounds'
493
+ for (let i = 0; i < 8; i++) fump(10.65 + i * 0.25 + LAND, 0.6 + i * 0.04, pan(3660 + i * 90, 11 + i * 0.25)); // felt bars
494
+ { // % digits swapped as `shown` climbs, at most ~7/s
495
+ const accs = [61, 72, 80, 86, 90, 93, 95, 97];
496
+ let last = -1, lt = -1;
497
+ for (let t = 10.66; t < 12.6; t += 1 / 120) {
498
+ const i = Math.min(7, Math.floor((t - 10.65) / 0.25)), st = 10.65 + i * 0.25;
499
+ const v = Math.round(H.lerp(i ? accs[i - 1] : 61, accs[i], H.clamp((t - st) / 0.25)));
500
+ if (v !== last && t - lt >= 0.14) { k.noise(t, { dur: 0.03, filter: 'bandpass', f: 1000, q: 1.2, gain: 0.16, a: 0.001, pan: -0.5, minor: true }); last = v; lt = t; }
501
+ }
502
+ }
503
+ // 97% (12.6): number turns red and bounces, a felt star drops on top: biggest hit
504
+ k.play('impact', 12.6, { gain: 1.55, pan: -0.2 });
505
+ k.play('ding', 12.6, { f: 'E5', gain: 0.6, pan: -0.1 });
506
+ fump(12.6 + LAND, 1.6, pan(4230, 12.8), false);
507
+ button(12.6 + LAND, 0.5, pan(4230, 12.8));
508
+
509
+ // BEAT 5: takeaway label drops on and resolves
510
+ fump(13.0 + LAND, 1.3, 0, false);
511
+ ['C4', 'G4', 'E5'].forEach((f, i) => k.play('ding', 13.32 + i * 0.09, { f, gain: 0.38 - i * 0.03, pan: (i - 1) * 0.25 }));
512
+ },
513
+ draw(ctx, W, H_, t) {
514
+ const nz = H.makeNoise(21), r = H.rng(314);
515
+ const P = 160, OY = -30, OX = -40;
516
+ const breathe = 1 + 0.12 * Math.sin(t * 1.4 - 1.5);
517
+ const prog = (a, b) => H.clamp((t - a) / (b - a));
518
+
519
+ // ---------- quilt: per-pixel cotton prints, weave, puff & seams ----------
520
+ // The static part (print colour x weave x broad light falloff) and the puff/seam terms are cached per size;
521
+ // each frame only re-applies the breathing puff.
522
+ {
523
+ const key = W + 'x' + H_;
524
+ if (!HQ.key || HQ.key !== key) {
525
+ const N = W * H_, base = new Float32Array(N * 3), A = new Float32Array(N), B = new Float32Array(N);
526
+ const cols = Math.ceil((W - OX) / P) + 1, rows = Math.ceil((H_ - OY) / P) + 1;
527
+ const pick = [];
528
+ for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) {
529
+ let k;
530
+ do { k = r.int(0, PRINTS.length - 1); } while ((i > 0 && pick[j * cols + i - 1] === k) || (j > 0 && pick[(j - 1) * cols + i] === k));
531
+ pick.push(k);
532
+ }
533
+ const grain = H.rng(9), lnz = H.makeNoise(77);
534
+ for (let y = 0; y < H_; y++) {
535
+ const yy = y - OY, j = Math.floor(yy / P), v = yy - j * P;
536
+ // broad, uneven light across the quilt (a real quilt never lies perfectly flat or evenly lit), sampled every 8px
537
+ const lrow = []; for (let x = 0; x <= W + 8; x += 8) lrow.push(0.9 + 0.1 * lnz.fbm2(x * 0.0022, y * 0.0035, 3));
538
+ for (let x = 0; x < W; x++) {
539
+ const xx = x - OX, i = Math.floor(xx / P), u = xx - i * P;
540
+ const pr = PRINTS[pick[j * cols + i]];
541
+ // slight fabric wobble in the print
542
+ const wu = u + nz.n2(x * 0.02, y * 0.02) * 1.2, wv = v + nz.n2(x * 0.02 + 9, y * 0.02) * 1.2;
543
+ const c = H.mix(printCol(pr, Math.max(0, wu), Math.max(0, wv)), [214, 200, 178], 0.3);
544
+ // plain weave: warp/weft over-under
545
+ const wx = x % 3, wy = y % 3, over = ((Math.floor(x / 3) + Math.floor(y / 3)) & 1);
546
+ let wv2 = 1 + (over ? (wx === 1 ? 0.05 : -0.02) : (wy === 1 ? 0.05 : -0.02)) - (wx === 0 && wy === 0 ? 0.06 : 0);
547
+ wv2 += (grain() - 0.5) * 0.06;
548
+ const lq = x >> 3, lt = (x & 7) / 8, m = wv2 * (lrow[lq] + (lrow[lq + 1] - lrow[lq]) * lt), n = y * W + x;
549
+ base[n * 3] = c[0] * m; base[n * 3 + 1] = c[1] * m; base[n * 3 + 2] = c[2] * m;
550
+ // puff: pillowy patches, lit from top-left, sinking into seams
551
+ const eu = Math.min(u, P - u), ev = Math.min(v, P - v), e = Math.min(eu, ev);
552
+ const seam = Math.exp(-(e * e) / 2.5) * 0.45;
553
+ A[n] = (0.95 + 0.08 * ((P / 2 - u) + (P / 2 - v)) / P) * (1 - seam);
554
+ B[n] = 0.36 * Math.exp(-e / 14) * (1 - seam);
555
+ }
556
+ }
557
+ HQ.key = key; HQ.base = base; HQ.A = A; HQ.B = B;
558
+ }
559
+ const img = ctx.createImageData(W, H_), d = img.data, base = HQ.base, A = HQ.A, B = HQ.B;
560
+ for (let n = 0, N = W * H_; n < N; n++) {
561
+ const s = A[n] - B[n] * breathe, k = n * 4, b = n * 3;
562
+ d[k] = base[b] * s; d[k + 1] = base[b + 1] * s; d[k + 2] = base[b + 2] * s; d[k + 3] = 255;
563
+ }
564
+ ctx.putImageData(img, 0, 0);
565
+ }
566
+
567
+ // ---------- thread helpers ----------
568
+ const walk = (pts, step, fn) => {
569
+ // walk along polyline, calling fn(x,y,angle,dist)
570
+ let acc = 0, next = 0;
571
+ for (let i = 0; i < pts.length - 1; i++) {
572
+ const [ax, ay] = pts[i], [bx, by] = pts[i + 1], L = Math.hypot(bx - ax, by - ay), a = Math.atan2(by - ay, bx - ax);
573
+ while (next <= acc + L) { const u = (next - acc) / L; fn(ax + (bx - ax) * u, ay + (by - ay) * u, a, next); next += step; }
574
+ acc += L;
575
+ }
576
+ };
577
+ const threadSeg = (x0, y0, x1, y1, col, w = 2.4) => {
578
+ const rgb = hx(col);
579
+ ctx.lineCap = 'round';
580
+ ctx.strokeStyle = 'rgba(40,25,15,0.38)'; ctx.lineWidth = w + 1.2;
581
+ ctx.beginPath(); ctx.moveTo(x0 + 0.8, y0 + 1.2); ctx.lineTo(x1 + 0.8, y1 + 1.2); ctx.stroke();
582
+ const g = ctx.createLinearGradient(x0, y0, x1, y1);
583
+ g.addColorStop(0, H.rgba(rgb.map((v) => v * 0.65))); g.addColorStop(0.5, H.rgba(rgb.map((v) => Math.min(255, v * 1.1 + 10)))); g.addColorStop(1, H.rgba(rgb.map((v) => v * 0.65)));
584
+ ctx.strokeStyle = g; ctx.lineWidth = w;
585
+ ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
586
+ ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 0.7;
587
+ const nx = -(y1 - y0), ny = x1 - x0, L = Math.hypot(nx, ny) || 1;
588
+ ctx.beginPath(); ctx.moveTo(x0 * 0.8 + x1 * 0.2 - (nx / L) * 0.5, y0 * 0.8 + y1 * 0.2 - (ny / L) * 0.5); ctx.lineTo(x0 * 0.3 + x1 * 0.7 - (nx / L) * 0.5, y0 * 0.3 + y1 * 0.7 - (ny / L) * 0.5); ctx.stroke();
589
+ // holes where thread enters the cloth
590
+ ctx.fillStyle = 'rgba(30,15,10,0.45)';
591
+ ctx.beginPath(); ctx.arc(x0, y0, 1.1, 0, TAU); ctx.arc(x1, y1, 1.1, 0, TAU); ctx.fill();
592
+ };
593
+ const running = (pts, col, len = 9, gap = 6, w = 2.4, pr = 1) => {
594
+ if (pr <= 0) return;
595
+ let start = null, total = 0;
596
+ for (let i = 0; i < pts.length - 1; i++) total += Math.hypot(pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]);
597
+ walk(pts, (len + gap) / 2, (x, y, a, dd) => {
598
+ const k = Math.round(dd / ((len + gap) / 2));
599
+ if (dd > total * pr) return;
600
+ if (k % 2 === 0) start = [x, y]; else if (start) { threadSeg(start[0], start[1], start[0] + Math.cos(a) * len, start[1] + Math.sin(a) * len, col, w); start = null; }
601
+ });
602
+ };
603
+
604
+ // quilting: running stitches echoing every seam
605
+ {
606
+ for (let gx = OX; gx < W + P; gx += P) for (const off of [9]) {
607
+ const pts = [[gx + off, -10], [gx + off, H_ + 10]];
608
+ running(pts, '#f4ead6', 10, 7, 2.4);
609
+ }
610
+ for (let gy = OY; gy < H_ + P; gy += P) for (const off of [9]) {
611
+ const pts = [[-10, gy + off], [W + 10, gy + off]];
612
+ running(pts, '#f4ead6', 10, 7, 2.4);
613
+ }
614
+ }
615
+
616
+ // ---------- felt appliqué ----------
617
+ const ftex = feltTex(55), fpat = ctx.createPattern(ftex, 'repeat');
618
+ const felt = (pts, col, o = {}) => {
619
+ if (o.at != null && t < o.at) return;
620
+ // layer sized to the piece (not the whole frame) to keep the fuzz/blur passes cheap
621
+ let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9; pts.forEach(([x, y]) => { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y); });
622
+ const LX = Math.floor(x0) - 24, LY = Math.floor(y0) - 24, L = H.layer(Math.ceil(x1 - LX) + 24, Math.ceil(y1 - LY) + 24), c = L.getContext('2d');
623
+ c.translate(-LX, -LY);
624
+ const path = (cc) => { cc.beginPath(); pts.forEach(([x, y], i) => (i ? cc.lineTo(x, y) : cc.moveTo(x, y))); cc.closePath(); };
625
+ path(c); c.fillStyle = col; c.fill();
626
+ // fuzzy fibres poking past the cut edge
627
+ const fr = H.rng(o.seed || 3);
628
+ c.lineCap = 'round';
629
+ walk([...pts, pts[0]], 1.6, (x, y, a) => {
630
+ const nx = Math.sin(a), ny = -Math.cos(a), l = fr.range(0.5, 2.6), tw = fr.range(-0.8, 0.8);
631
+ c.strokeStyle = col; c.lineWidth = fr.range(0.5, 1.1);
632
+ c.beginPath(); c.moveTo(x - nx * 1.5, y - ny * 1.5); c.lineTo(x + (nx + tw * ny) * l * (o.outward ?? 1), y + (ny - tw * nx) * l * (o.outward ?? 1)); c.stroke();
633
+ });
634
+ c.save(); c.globalCompositeOperation = 'source-atop';
635
+ fpat.setTransform(new DOMMatrix().translateSelf(fr() * 256, fr() * 256));
636
+ c.fillStyle = fpat; c.fillRect(LX, LY, L.width, L.height);
637
+ // soft rounded felt edge: darker rim, lighter top-left
638
+ c.filter = 'blur(3px)'; c.lineWidth = 7; c.strokeStyle = 'rgba(0,0,0,0.22)'; path(c); c.stroke();
639
+ c.translate(-2, -2); c.lineWidth = 4; c.strokeStyle = 'rgba(255,255,255,0.12)'; path(c); c.stroke();
640
+ c.restore();
641
+ // drop-on: falls in slightly large with a lifted shadow, squashes and settles
642
+ const u = o.at == null ? 1 : H.clamp((t - o.at) / 0.5);
643
+ const lift = 1 - H.ease.out(u);
644
+ const sq = u < 1 ? Math.sin(Math.min(1, u * 1.6) * Math.PI) * 0.035 * (u > 0.45 ? 1 : 0) : 0;
645
+ const cx = pts.reduce((a, p) => a + p[0], 0) / pts.length, cy = pts.reduce((a, p) => a + p[1], 0) / pts.length;
646
+ ctx.save(); ctx.shadowColor = `rgba(40,20,10,${0.45 - lift * 0.2})`; ctx.shadowBlur = (o.sb ?? 12) + lift * 30; ctx.shadowOffsetX = (o.sx ?? 4) + lift * 22; ctx.shadowOffsetY = (o.sy ?? 6) + lift * 30;
647
+ ctx.globalAlpha = H.clamp(u * 5);
648
+ ctx.translate(cx, cy - lift * 24); ctx.scale(1 + lift * 0.14 + sq, 1 + lift * 0.14 - sq); ctx.translate(-cx, -cy);
649
+ ctx.drawImage(L, LX, LY); ctx.restore();
650
+ };
651
+ const circlePts = (cx, cy, R, n = 120, wob = 0.008, seed = 0) => Array.from({ length: n }, (_, i) => { const a = (i / n) * TAU; const k = 1 + nz.n2(Math.cos(a) + seed, Math.sin(a)) * wob; return [cx + Math.cos(a) * R * k, cy + Math.sin(a) * R * k]; });
652
+ const blobPts = (cx, cy, R, seed, amp, sx = 1, sy = 1, n = 70) => Array.from({ length: n }, (_, i) => { const a = (i / n) * TAU; const k = 1 + nz.fbm2(Math.cos(a) * 1.2 + seed, Math.sin(a) * 1.2 + seed, 3) * amp; return [cx + Math.cos(a) * R * k * sx, cy + Math.sin(a) * R * k * sy]; });
653
+
654
+ const G = { x: 560, y: 368, R: 206 };
655
+ // orbit ellipse thread (behind globe)
656
+ const orbit = (a0, a1) => { const pts = []; for (let a = a0; a <= a1; a += 0.02) { const x = Math.cos(a) * 360, y = Math.sin(a) * 118, rr = -0.22; pts.push([G.x + x * Math.cos(rr) - y * Math.sin(rr), G.y + 10 + x * Math.sin(rr) + y * Math.cos(rr)]); } return pts; };
657
+ running(orbit(Math.PI, TAU), '#f2c14e', 13, 8, 3.2, prog(1.6, 2.0));
658
+ // globe
659
+ const gpts = circlePts(G.x, G.y, G.R, 160, 0.01, 2);
660
+ felt(gpts, '#2f5f9a', { seed: 5, sb: 24, sx: 9, sy: 14, at: 0.1 });
661
+ // continents (green felt, running-stitched edges)
662
+ const conts = [
663
+ blobPts(G.x - 70, G.y - 60, 70, 3, 0.35, 1.0, 1.15), blobPts(G.x + 75, G.y + 40, 62, 8, 0.4, 0.9, 1.3), blobPts(G.x - 50, G.y + 110, 38, 12, 0.35, 1.3, 0.7), blobPts(G.x + 110, G.y - 110, 30, 17, 0.3, 1.2, 0.8),
664
+ ];
665
+ // clip continents to the globe disc
666
+ const inside = (pts) => pts.map(([x, y]) => { const dx = x - G.x, dy = y - G.y, d = Math.hypot(dx, dy), m = G.R - 16; return d > m ? [G.x + (dx / d) * m, G.y + (dy / d) * m] : [x, y]; });
667
+ conts.forEach((c0, i) => {
668
+ const c = inside(c0);
669
+ felt(c, i % 2 ? '#8dbb5e' : '#73a650', { seed: 30 + i, sb: 6, sx: 2.5, sy: 3.5, at: 0.55 + i * 0.13 });
670
+ const inset = c.map(([x, y]) => { const cx = c.reduce((s, p) => s + p[0], 0) / c.length, cy = c.reduce((s, p) => s + p[1], 0) / c.length; const dx = x - cx, dy = y - cy, dd = Math.hypot(dx, dy) || 1; return [x - (dx / dd) * 7, y - (dy / dd) * 7]; });
671
+ running([...inset, inset[0]], '#f2c14e', 8, 5, 2.5, prog(1.3 + i * 0.12, 1.9 + i * 0.12));
672
+ });
673
+ // blanket stitch around the globe
674
+ {
675
+ const n = gpts.length, step = 2;
676
+ let prev = null;
677
+ const bp = prog(0.9, 1.9);
678
+ for (let i = 0; i < n * bp; i += step) {
679
+ const [x, y] = gpts[i], dx = x - G.x, dy = y - G.y, d = Math.hypot(dx, dy), ux = dx / d, uy = dy / d;
680
+ const ox = x + ux * 2, oy = y + uy * 2, ix = x - ux * 15, iy = y - uy * 15;
681
+ if (prev) threadSeg(prev[0], prev[1], ox, oy, '#f4ead2', 2.8);
682
+ threadSeg(ix, iy, ox, oy, '#f4ead2', 2.8);
683
+ prev = [ox, oy];
684
+ }
685
+ if (bp >= 1) threadSeg(prev[0], prev[1], gpts[0][0] + ((gpts[0][0] - G.x) / G.R) * 1.5, gpts[0][1] + ((gpts[0][1] - G.y) / G.R) * 1.5, '#f4ead2', 2.2);
686
+ }
687
+ // orbit front half
688
+ running(orbit(0, Math.PI), '#f2c14e', 13, 8, 3.2, prog(2.0, 2.4));
689
+
690
+ // ---------- buttons as network nodes, linked by thread ----------
691
+ const nodes = [
692
+ [G.x - 300, G.y - 70, 26, '#e8b04a', 4], [G.x + 330, G.y - 120, 30, '#d9564a', 4], [G.x + 250, G.y + 120, 22, '#f4efe4', 2],
693
+ [G.x - 210, G.y + 140, 24, '#6fb3a6', 4], [G.x - 40, G.y - 40, 20, '#f4efe4', 4], [G.x + 95, G.y + 45, 18, '#e8b04a', 2],
694
+ [G.x - 60, G.y + 115, 16, '#ef9aa6', 2], [G.x + 490, G.y + 20, 24, '#6fb3a6', 4], [G.x + 120, G.y - 115, 15, '#d9564a', 2],
695
+ ];
696
+ const links = [[0, 4], [4, 5], [5, 2], [4, 8], [8, 1], [1, 7], [2, 7], [6, 3], [6, 5], [0, 3], [4, 6]];
697
+ const grow = prog(2.55, 3.7);
698
+ links.forEach(([a, b], li) => {
699
+ const A = nodes[a], B = nodes[b];
700
+ const mx = (A[0] + B[0]) / 2, my = (A[1] + B[1]) / 2 - 28 - (li % 3) * 8;
701
+ const pts = [];
702
+ const frac = H.clamp(grow * links.length - li * 0.6, 0, 1);
703
+ for (let k = 0; k <= 30 * frac; k++) { const u = k / 30; pts.push([(1 - u) * (1 - u) * A[0] + 2 * u * (1 - u) * mx + u * u * B[0], (1 - u) * (1 - u) * A[1] + 2 * u * (1 - u) * my + u * u * B[1]]); }
704
+ if (pts.length > 1) running(pts, '#c8443a', 12, 7, 3.2);
705
+ });
706
+ nodes.forEach(([x, y, R0, col, holes], i) => {
707
+ const bu = prog(2.05 + i * 0.07, 2.35 + i * 0.07);
708
+ if (bu <= 0) return;
709
+ const R = R0 * Math.max(0.15, H.ease.back(bu));
710
+ const rgb = hx(col);
711
+ ctx.save();
712
+ ctx.shadowColor = 'rgba(40,20,10,0.5)'; ctx.shadowBlur = 8; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 5;
713
+ const g = ctx.createRadialGradient(x - R * 0.35, y - R * 0.4, R * 0.1, x, y, R);
714
+ g.addColorStop(0, H.rgba(rgb.map((v) => Math.min(255, v * 1.25 + 20)))); g.addColorStop(0.7, H.rgba(rgb)); g.addColorStop(1, H.rgba(rgb.map((v) => v * 0.7)));
715
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, R, 0, TAU); ctx.fill();
716
+ ctx.restore();
717
+ // rim + dished centre
718
+ ctx.strokeStyle = H.rgba(rgb.map((v) => v * 0.6), 0.6); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, R * 0.72, 0, TAU); ctx.stroke();
719
+ ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.arc(x, y, R * 0.76, 0.2, 1.8); ctx.stroke();
720
+ ctx.beginPath(); ctx.arc(x, y, Math.max(0.5, R - 1.5), 3.4, 4.6); ctx.stroke();
721
+ // holes + thread
722
+ const hp = holes === 4 ? [[-1, -1], [1, -1], [1, 1], [-1, 1]] : [[-1, 0], [1, 0]];
723
+ const hs = R * 0.24;
724
+ const tc = col === '#d9564a' || col === '#ef9aa6' ? '#f4ead2' : '#c8443a';
725
+ if (bu < 1) return;
726
+ if (holes === 4) { threadSeg(x - hs, y - hs, x + hs, y + hs, tc, 2.2); threadSeg(x + hs, y - hs, x - hs, y + hs, tc, 2.2); }
727
+ else threadSeg(x - hs * 1.1, y, x + hs * 1.1, y, tc, 2.4);
728
+ hp.forEach(([a, b]) => { ctx.fillStyle = 'rgba(30,15,10,0.55)'; ctx.beginPath(); ctx.arc(x + a * hs * (holes === 2 ? 1.1 : 1), y + b * hs, R * 0.09, 0, TAU); ctx.fill(); });
729
+ // specular glint
730
+ ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.beginPath(); ctx.ellipse(x - R * 0.45, y - R * 0.5, R * 0.18, R * 0.08, -0.6, 0, TAU); ctx.fill();
731
+ });
732
+
733
+ // ---------- felt heart + star accents ----------
734
+ {
735
+ const heart = []; for (let i = 0; i < 80; i++) { const a = (i / 80) * TAU; const x = 16 * Math.pow(Math.sin(a), 3), y = -(13 * Math.cos(a) - 5 * Math.cos(2 * a) - 2 * Math.cos(3 * a) - Math.cos(4 * a)); heart.push([1080 + x * 3.2, 150 + y * 3.2]); }
736
+ felt(heart, '#d9564a', { seed: 61, sb: 10, at: 0.3 });
737
+ const hi = heart.map(([x, y]) => [1080 + (x - 1080) * 0.78, 150 + (y - 150) * 0.78 + 3]);
738
+ running([...hi, hi[0]], '#f4ead2', 6, 5, 1.8, prog(0.9, 1.4));
739
+ const star = []; for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + (i / 10) * TAU, rr = i % 2 ? 17 : 38; star.push([175 + Math.cos(a) * rr, 120 + Math.sin(a) * rr]); }
740
+ felt(star, '#f2c14e', { seed: 62, sb: 9, at: 0.42 });
741
+ const si = star.map(([x, y]) => [175 + (x - 175) * 0.62, 120 + (y - 120) * 0.62 + 1]);
742
+ running([...si, si[0]], '#c8443a', 5, 4, 1.7, prog(1.0, 1.5));
743
+ }
744
+
745
+ // ---------- sewn-on woven label with satin-stitched lettering ----------
746
+ {
747
+ const lx = 1010, ly = 612, lw = 390, lh = 100, rot = -0.05;
748
+ const lu = prog(1.55, 2.0);
749
+ if (lu > 0) {
750
+ ctx.save(); ctx.translate(lx + (1 - H.ease.out(lu)) * 420, ly); ctx.rotate(rot - (1 - H.ease.out(lu)) * 0.25);
751
+ ctx.save(); ctx.shadowColor = 'rgba(40,20,10,0.45)'; ctx.shadowBlur = 10; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 5;
752
+ ctx.fillStyle = '#f3ecdc'; ctx.fillRect(-lw / 2, -lh / 2, lw, lh); ctx.restore();
753
+ // twill weave
754
+ ctx.save(); ctx.beginPath(); ctx.rect(-lw / 2, -lh / 2, lw, lh); ctx.clip();
755
+ ctx.strokeStyle = 'rgba(120,100,70,0.12)'; ctx.lineWidth = 1;
756
+ for (let k = -lw; k < lw; k += 3) { ctx.beginPath(); ctx.moveTo(k, -lh / 2); ctx.lineTo(k + lh, lh / 2); ctx.stroke(); }
757
+ // folded-under ends
758
+ const fg = ctx.createLinearGradient(-lw / 2, 0, -lw / 2 + 16, 0); fg.addColorStop(0, 'rgba(90,60,30,0.3)'); fg.addColorStop(1, 'rgba(90,60,30,0)');
759
+ ctx.fillStyle = fg; ctx.fillRect(-lw / 2, -lh / 2, 16, lh);
760
+ const fg2 = ctx.createLinearGradient(lw / 2, 0, lw / 2 - 16, 0); fg2.addColorStop(0, 'rgba(90,60,30,0.3)'); fg2.addColorStop(1, 'rgba(90,60,30,0)');
761
+ ctx.fillStyle = fg2; ctx.fillRect(lw / 2 - 16, -lh / 2, 16, lh);
762
+ // woven border lines
763
+ ctx.fillStyle = '#c8443a'; ctx.fillRect(-lw / 2, -lh / 2 + 6, lw, 3); ctx.fillRect(-lw / 2, lh / 2 - 9, lw, 3);
764
+ ctx.restore();
765
+ // satin-stitched text: thread lines clipped to glyphs
766
+ const M = H.layer(lw, lh), mc = M.getContext('2d');
767
+ mc.font = '700 42px "Fredoka"'; mc.textAlign = 'center'; mc.textBaseline = 'middle'; mc.fillStyle = '#fff';
768
+ mc.fillText('WORLD WIDE WEB', lw / 2, lh / 2 + 2);
769
+ mc.globalCompositeOperation = 'source-in';
770
+ mc.fillStyle = '#2f5f9a'; mc.fillRect(0, 0, lw, lh);
771
+ mc.globalCompositeOperation = 'source-atop';
772
+ mc.lineWidth = 1;
773
+ for (let k = -lh; k < lw + lh; k += 2) {
774
+ mc.strokeStyle = 'rgba(255,255,255,0.28)'; mc.beginPath(); mc.moveTo(k, 0); mc.lineTo(k + lh * 0.35, lh); mc.stroke();
775
+ mc.strokeStyle = 'rgba(0,0,30,0.25)'; mc.beginPath(); mc.moveTo(k + 1, 0); mc.lineTo(k + 1 + lh * 0.35, lh); mc.stroke();
776
+ }
777
+ // satin stitch fills the letters left to right
778
+ const tp = prog(2.0, 2.7);
779
+ ctx.save(); ctx.beginPath(); ctx.rect(-lw / 2, -lh / 2, lw * (0.12 + 0.76 * tp) + (tp >= 1 ? lw : 0), lh); ctx.clip();
780
+ ctx.shadowColor = 'rgba(20,10,5,0.45)'; ctx.shadowBlur = 1.5; ctx.shadowOffsetX = 0.8; ctx.shadowOffsetY = 1.2;
781
+ if (tp > 0) ctx.drawImage(M, -lw / 2, -lh / 2); ctx.restore();
782
+ ctx.restore();
783
+ // stitched down at both ends (in world coords)
784
+ const cr = Math.cos(rot), sr = Math.sin(rot), tw = (x, y) => [lx + x * cr - y * sr, ly + x * sr + y * cr];
785
+ running([tw(-lw / 2 + 9, -lh / 2 + 4), tw(-lw / 2 + 9, lh / 2 - 4)], '#c8443a', 7, 5, 2, prog(2.7, 2.9));
786
+ running([tw(lw / 2 - 9, -lh / 2 + 4), tw(lw / 2 - 9, lh / 2 - 4)], '#c8443a', 7, 5, 2, prog(2.9, 3.1));
787
+ }
788
+ }
789
+
790
+ // ---------- soft studio light ----------
791
+ ctx.save(); ctx.globalCompositeOperation = 'soft-light';
792
+ const lg = ctx.createRadialGradient(380 + t * 10, 190 + Math.sin(t * 0.7) * 15, 60, 600, 360, 900);
793
+ lg.addColorStop(0, 'rgba(255,245,220,0.55)'); lg.addColorStop(1, 'rgba(30,20,10,0.5)');
794
+ ctx.fillStyle = lg; ctx.fillRect(0, 0, W, H_); ctx.restore();
795
+ H.vignette(ctx, W, H_, 0.4, '35,20,10');
796
+ H.grain(ctx, W, H_, 0.025, 6 + Math.floor(t * 12));
797
+ },
798
+ });
799
+ }