@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,724 @@
1
+ {
2
+ const fieldNoise = (w, h, seed, cell, oct, sx = 1, sy = 1) => {
3
+ const nz = H.makeNoise(seed), gw = Math.ceil(w / cell) + 2, gh = Math.ceil(h / cell) + 2, g = new Float32Array(gw * gh);
4
+ for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) g[j * gw + i] = nz.fbm2(i * 0.35 * sx, j * 0.35 * sy, oct);
5
+ const out = new Float32Array(w * h);
6
+ for (let y = 0; y < h; y++) {
7
+ const fy = y / cell, j = fy | 0, ty = fy - j;
8
+ for (let x = 0; x < w; x++) {
9
+ const fx = x / cell, i = fx | 0, tx = fx - i, k = j * gw + i;
10
+ const a = g[k] + (g[k + 1] - g[k]) * tx, b = g[k + gw] + (g[k + gw + 1] - g[k + gw]) * tx;
11
+ out[y * w + x] = a + (b - a) * ty;
12
+ }
13
+ }
14
+ return out;
15
+ };
16
+
17
+ const C = {
18
+ paper: '#ecdfc2', key: '#1b2236', prussD: '#16304f', pruss: '#24507e', mid: '#4d7ca6', light: '#93b4cc', pale: '#c9d6d6',
19
+ foam: '#f1e8d2', sky: '#e3d2ab', fuji: '#6d84a0', hull: '#7d3f2c', ochre: '#d2a75c', grey: '#9aa8b0', red: '#c4432d', green: '#7a9a6a',
20
+ };
21
+
22
+ // Catmull-Rom sampling of a point list -> dense polyline
23
+ const sample = (pts, per = 14) => {
24
+ const out = [];
25
+ for (let i = 0; i < pts.length - 1; i++) {
26
+ const p0 = pts[Math.max(0, i - 1)], p1 = pts[i], p2 = pts[i + 1], p3 = pts[Math.min(pts.length - 1, i + 2)];
27
+ for (let k = 0; k < per; k++) {
28
+ const t = k / per, t2 = t * t, t3 = t2 * t;
29
+ const f = (a, b, c, d) => 0.5 * (2 * b + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
30
+ out.push([f(p0[0], p1[0], p2[0], p3[0]), f(p0[1], p1[1], p2[1], p3[1])]);
31
+ }
32
+ }
33
+ out.push(pts[pts.length - 1]);
34
+ return out;
35
+ };
36
+ const normals = (poly) => poly.map((p, i) => {
37
+ const a = poly[Math.max(0, i - 1)], b = poly[Math.min(poly.length - 1, i + 1)];
38
+ let tx = b[0] - a[0], ty = b[1] - a[1]; const l = Math.hypot(tx, ty) || 1; tx /= l; ty /= l;
39
+ return { tx, ty, nx: ty, ny: -tx }; // n = outward (left of travel)
40
+ });
41
+ const pathOf = (poly, close) => { const p = new Path2D(); poly.forEach(([x, y], i) => (i ? p.lineTo(x, y) : p.moveTo(x, y))); if (close) p.closePath(); return p; };
42
+
43
+ // keyblock line: slightly uneven carved stroke
44
+ const keyline = (c, path, w, col = C.key) => {
45
+ c.save(); c.strokeStyle = col; c.lineJoin = 'round'; c.lineCap = 'round';
46
+ c.lineWidth = w; c.stroke(path);
47
+ c.globalAlpha = 0.5; c.lineWidth = w * 0.6; c.translate(0.7, 0.5); c.stroke(path);
48
+ c.restore();
49
+ };
50
+
51
+ // one foam claw: a hooked finger plus smaller fingers
52
+ const claw = (c, x, y, nx, ny, tx, ty, L, r) => {
53
+ const fingers = r.int(3, 4);
54
+ for (let f = 0; f < fingers; f++) {
55
+ const s = L * (1 - f * 0.2) * r.range(0.85, 1.1);
56
+ const off = (f - (fingers - 1) / 2) * L * 0.24;
57
+ const bx = x + tx * off, by = y + ty * off;
58
+ const w = s * 0.22;
59
+ // direction: outward then hooking forward and down (tangent)
60
+ const cx1 = bx + nx * s * 0.9 + tx * s * 0.2, cy1 = by + ny * s * 0.9 + ty * s * 0.2;
61
+ const ex = bx + nx * s * 0.75 + tx * s * 0.75, ey = by + ny * s * 0.75 + ty * s * 0.75;
62
+ const cx2 = bx + nx * s * 0.45 + tx * s * 0.55, cy2 = by + ny * s * 0.45 + ty * s * 0.55;
63
+ const p = new Path2D();
64
+ p.moveTo(bx - tx * w, by - ty * w);
65
+ p.quadraticCurveTo(cx1, cy1, ex, ey);
66
+ p.quadraticCurveTo(cx2, cy2, bx + tx * w, by + ty * w);
67
+ p.closePath();
68
+ c.fillStyle = C.foam; c.fill(p);
69
+ c.lineWidth = 1.6; c.strokeStyle = C.key; c.stroke(p);
70
+ }
71
+ };
72
+
73
+
74
+ // broken, tapering vein lines + alternating lighter bands, following a contour (sign=-1 inward of outward normal)
75
+ const veins = (c, poly, nn, o) => {
76
+ const r = H.rng(o.seed), L = poly.length;
77
+ for (let k = 0; k < o.count; k++) {
78
+ const d = o.d0 + k * o.step + r.range(-3, 3);
79
+ const tk = (o.toK || 0) * k, i0 = Math.floor(L * r.range(o.from[0], o.from[1])), i1 = Math.min(L - 1, Math.floor(L * (r.range(o.to[0], o.to[1]) - tk)));
80
+ if (i1 - i0 < 4) continue;
81
+ const pt = (i) => { const w = d + Math.sin(i * 0.07 + k) * 2.5; return [poly[i][0] + o.sign * nn[i].nx * w, poly[i][1] + o.sign * nn[i].ny * w]; };
82
+ if (o.band && k % 2 === 0) {
83
+ c.strokeStyle = o.band; c.lineWidth = o.step * 0.62; c.lineCap = 'round';
84
+ c.beginPath(); for (let i = i0; i <= i1; i++) { const [x, y] = pt(i); i === i0 ? c.moveTo(x, y) : c.lineTo(x, y); } c.stroke();
85
+ }
86
+ c.strokeStyle = o.line; c.lineCap = 'round';
87
+ const seg = 6;
88
+ for (let a = i0; a < i1; a += seg) {
89
+ const u = (a - i0) / (i1 - i0);
90
+ c.lineWidth = H.lerp(o.w0, o.w1, u) * (u < 0.08 ? u / 0.08 : 1) + 0.3;
91
+ c.beginPath(); for (let i = a; i <= Math.min(i1, a + seg); i++) { const [x, y] = pt(i); i === a ? c.moveTo(x, y) : c.lineTo(x, y); } c.stroke();
92
+ }
93
+ }
94
+ };
95
+
96
+
97
+ // keyblock-only context: key-coloured fills/strokes print; everything else carves the key away (occlusion)
98
+ const keyOnly = (kc) => {
99
+ const P = CanvasRenderingContext2D.prototype;
100
+ const isKey = (st) => typeof st === 'string' && (st.startsWith('#1b2236') || st.startsWith('rgba(27, 34, 54'));
101
+ const wrap = (name, prop) => { kc[name] = function (...a) { if (isKey(this[prop])) return P[name].apply(this, a); const g = this.globalCompositeOperation; this.globalCompositeOperation = 'destination-out'; P[name].apply(this, a); this.globalCompositeOperation = g; }; };
102
+ wrap('fill', 'fillStyle'); wrap('fillRect', 'fillStyle'); wrap('fillText', 'fillStyle'); wrap('stroke', 'strokeStyle');
103
+ };
104
+ // finish-pass noise fields are deterministic -> compute once
105
+ let FIELDS = null;
106
+ const fields = (W, HH) => FIELDS || (FIELDS = { warp: fieldNoise(W, HH, 3, 30, 3), fine: fieldNoise(W, HH, 4, 2, 2, 0.15, 2.2), pap: fieldNoise(W, HH, 6, 3, 2, 2.5, 0.6), blot: fieldNoise(W, HH, 7, 40, 4) });
107
+
108
+ // isometric data cube
109
+ const cube = (c, x, y, s, rot) => {
110
+ c.save(); c.translate(x, y); c.rotate(rot);
111
+ const h = s * 0.58;
112
+ const top = new Path2D(); top.moveTo(0, -s); top.lineTo(s * 0.87, -s + h); top.lineTo(0, -s + 2 * h); top.lineTo(-s * 0.87, -s + h); top.closePath();
113
+ const left = new Path2D(); left.moveTo(-s * 0.87, -s + h); left.lineTo(0, -s + 2 * h); left.lineTo(0, s * 0.9); left.lineTo(-s * 0.87, s * 0.9 - h); left.closePath();
114
+ const right = new Path2D(); right.moveTo(s * 0.87, -s + h); right.lineTo(0, -s + 2 * h); right.lineTo(0, s * 0.9); right.lineTo(s * 0.87, s * 0.9 - h); right.closePath();
115
+ c.fillStyle = C.foam; c.fill(top); c.fillStyle = C.pale; c.fill(left); c.fillStyle = C.light; c.fill(right);
116
+ c.lineWidth = Math.max(1, s * 0.12); c.strokeStyle = C.key; c.lineJoin = 'round';
117
+ c.stroke(top); c.stroke(left); c.stroke(right);
118
+ c.restore();
119
+ };
120
+
121
+ // ===================== EXPLAINER: "How does AI learn?" =====================
122
+ // An emaki handscroll: the camera unrolls the scroll right-to-left, scenes are divided by golden kasumi
123
+ // clouds, flow is shown by currents with claw-foam heads, labels are paper cartouches and tanzaku slips,
124
+ // judgements are vermilion seal stamps.
125
+ const fillKey = (c, path, col, w = 1.8) => { c.save(); c.translate(1.5, 1); c.fillStyle = col; c.fill(path); c.restore(); keyline(c, path, w); };
126
+ const cartouche = (c, txt, x, y, size, p = 1, bg = '#e8d49a', align = 'center', fg = C.key) => {
127
+ if (p <= 0) return; c.save(); c.font = `700 ${size}px "Noto Serif JP"`; c.textBaseline = 'middle';
128
+ const w = c.measureText(txt).width + size * 1.1, h = size * 1.7, x0 = align === 'center' ? x - w / 2 : x;
129
+ c.globalAlpha = Math.min(1, p * 1.5);
130
+ const r = new Path2D(); r.rect(x0, y - h / 2, w, h);
131
+ if (bg[0] === '#' && bg.length === 7) { const v = H.hex(bg), lt = v.map((q) => Math.round(q + (255 - q) * 0.35)), dk = v.map((q) => Math.round(q * 0.93)); c.fillStyle = vbok(c, y - h / 2, y + h / 2, [[0, `rgb(${lt})`], [0.45, bg], [1, `rgb(${dk})`]]); } else c.fillStyle = bg;
132
+ c.fill(r); c.strokeStyle = C.key; c.lineWidth = 2; c.stroke(r); c.lineWidth = 1; c.strokeRect(x0 + 4, y - h / 2 + 4, w - 8, h - 8);
133
+ c.fillStyle = fg; c.textAlign = 'center'; c.fillText(txt, x0 + w / 2, y + 1); c.restore();
134
+ };
135
+ const seal = (c, x, y, s, glyph, p, rot = 0.05) => { // vermilion seal stamped down
136
+ if (p <= 0) return; const k = 1 + (1 - p) * 0.8; c.save(); c.translate(x, y); c.rotate(rot); c.scale(k, k); c.globalAlpha = Math.min(1, p * 2);
137
+ c.fillStyle = C.red; c.fillRect(-s / 2, -s / 2, s, s); c.strokeStyle = '#efe2c6'; c.lineWidth = s * 0.06; c.strokeRect(-s * 0.4, -s * 0.4, s * 0.8, s * 0.8);
138
+ c.fillStyle = '#efe2c6'; c.strokeStyle = '#efe2c6'; c.lineCap = 'square'; c.lineWidth = s * 0.13;
139
+ if (glyph === 'x') { c.beginPath(); c.moveTo(-s * 0.22, -s * 0.22); c.lineTo(s * 0.22, s * 0.22); c.moveTo(s * 0.22, -s * 0.22); c.lineTo(-s * 0.22, s * 0.22); c.stroke(); }
140
+ else if (glyph === 'v') { c.beginPath(); c.moveTo(-s * 0.24, 0); c.lineTo(-s * 0.06, s * 0.18); c.lineTo(s * 0.24, -s * 0.2); c.stroke(); }
141
+ else { c.font = `900 ${s * 0.62}px "Noto Serif JP"`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(glyph, 0, s * 0.03); }
142
+ c.restore();
143
+ };
144
+ const tanzaku = (c, txt, x, y, p = 1) => { // paper label slip hanging from a cord
145
+ if (p <= 0) return; c.save(); c.globalAlpha = Math.min(1, p * 2); c.strokeStyle = C.key; c.lineWidth = 1.2; c.beginPath(); c.moveTo(x, y - 16); c.lineTo(x, y - 4); c.stroke();
146
+ c.font = '700 28px "Noto Serif JP"'; const w = c.measureText(txt).width + 20; const r = new Path2D(); r.rect(x - w / 2, y - 4, w, 40);
147
+ const g = c.createLinearGradient(0, y - 4, 0, y + 36); g.addColorStop(0, '#f4ecd8'); g.addColorStop(1, '#e9dcbc'); c.fillStyle = g; c.fill(r); keyline(c, r, 1.4);
148
+ c.fillStyle = C.key; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(txt, x, y + 16); c.restore();
149
+ };
150
+ const animal = (c, kind, x, y, s) => { // flat woodblock silhouette: grey cat / ochre shiba
151
+ c.save(); c.translate(x, y); c.scale(s / 100, s / 100);
152
+ const p = new Path2D();
153
+ if (kind === 'cat') {
154
+ p.ellipse(0, 18, 30, 34, 0, 0, 7); p.moveTo(22, -14); p.arc(0, -22, 24, 0, 7);
155
+ const e = new Path2D(); e.moveTo(-22, -32); e.lineTo(-18, -58); e.lineTo(-4, -42); e.closePath(); e.moveTo(22, -32); e.lineTo(18, -58); e.lineTo(4, -42); e.closePath();
156
+ fillKey(c, e, C.grey, 2); fillKey(c, p, C.grey, 2);
157
+ c.strokeStyle = C.key; c.lineWidth = 6; c.lineCap = 'round'; c.beginPath(); c.moveTo(26, 40); c.quadraticCurveTo(58, 40, 50, 6); c.stroke(); c.strokeStyle = C.grey; c.lineWidth = 3; c.stroke();
158
+ c.fillStyle = C.key; c.beginPath(); c.ellipse(-9, -24, 2.5, 4, 0, 0, 7); c.ellipse(9, -24, 2.5, 4, 0, 0, 7); c.fill();
159
+ } else {
160
+ p.ellipse(0, 18, 32, 32, 0, 0, 7); p.moveTo(26, -18); p.arc(0, -20, 26, 0, 7); p.moveTo(10, -6); p.ellipse(0, -8, 14, 10, 0, 0, 7);
161
+ const e = new Path2D(); e.moveTo(-24, -26); e.lineTo(-22, -50); e.lineTo(-6, -40); e.closePath(); e.moveTo(24, -26); e.lineTo(22, -50); e.lineTo(6, -40); e.closePath();
162
+ fillKey(c, e, C.ochre, 2); fillKey(c, p, C.ochre, 2);
163
+ const m = new Path2D(); m.ellipse(0, -6, 12, 8, 0, 0, 7); c.fillStyle = '#f1e8d2'; c.fill(m);
164
+ c.fillStyle = C.key; c.beginPath(); c.arc(-10, -24, 3, 0, 7); c.arc(10, -24, 3, 0, 7); c.arc(0, -10, 3.5, 0, 7); c.fill();
165
+ c.strokeStyle = C.key; c.lineWidth = 6; c.lineCap = 'round'; c.beginPath(); c.arc(32, 0, 12, Math.PI * 0.6, Math.PI * 2.1); c.stroke(); c.strokeStyle = C.ochre; c.lineWidth = 3; c.stroke();
166
+ }
167
+ c.restore();
168
+ };
169
+ const print = (c, kind, x, y, s) => { const r = new Path2D(); r.rect(x - s / 2, y - s / 2, s, s * 1.15); fillKey(c, r, '#f1e8d2', 2); c.strokeStyle = C.key; c.lineWidth = 1; c.strokeRect(x - s / 2 + 5, y - s / 2 + 5, s - 10, s * 1.15 - 10); animal(c, kind, x, y + s * 0.12, s * 0.8); };
170
+ const gear = (c, x, y, r, a, col) => { const p = new Path2D(); for (let i = 0; i < 24; i++) { const rr = i % 2 ? r : r * 1.2, aa = a + i / 24 * Math.PI * 2; p.lineTo(x + Math.cos(aa) * rr, y + Math.sin(aa) * rr); } p.closePath(); p.moveTo(x + r * 0.35, y); p.arc(x, y, r * 0.35, 0, 7, true); fillKey(c, p, col, 1.6); };
171
+ const karakuri = (c, x, y, look, mood, gears, sc = 1.3) => { c.save(); c.translate(x, y); c.scale(sc, sc); karakuri0(c, 0, 0, look, mood, gears); c.restore(); };
172
+ const karakuri0 = (c, x, y, look, mood, gears) => { // Edo automaton doll on a dais, with its gear cabinet (the weights)
173
+ const dais = new Path2D(); dais.rect(x - 130, y + 70, 260, 36); fillKey(c, dais, vbok(c, y + 70, y + 106, [[0, '#94503a'], [1, '#5e2c1e']]));
174
+ const cab = new Path2D(); cab.rect(x - 230, y - 40, 110, 140); fillKey(c, cab, vbok(c, y - 40, y + 100, [[0, '#5a4132'], [0.6, '#3b2a22'], [1, '#2a1d17']]));
175
+ gears.forEach((a, i) => gear(c, x - 175 + (i % 2) * 22, y - 8 + i * 42, 17 - (i % 2) * 3, a, C.ochre));
176
+ const body = new Path2D(); body.moveTo(x - 80, y + 70); body.lineTo(x - 50, y - 30); body.lineTo(x + 50, y - 30); body.lineTo(x + 80, y + 70); body.closePath(); fillKey(c, body, vbok(c, y - 30, y + 70, [[0, '#d65438'], [0.5, C.red], [1, '#a8331f']]));
177
+ c.save(); c.clip(body); c.fillStyle = '#efe2c6'; for (let i = 0; i < 6; i++) for (let j = 0; j < 4; j++) { c.beginPath(); c.arc(x - 70 + i * 28 + (j % 2) * 14, y - 20 + j * 26, 4, 0, 7); c.fill(); } c.restore();
178
+ const obi = new Path2D(); obi.rect(x - 58, y + 6, 116, 18); fillKey(c, obi, C.ochre, 1.4);
179
+ const tray = new Path2D(); tray.rect(x + 20, y + 20, 90, 12); fillKey(c, tray, C.hull, 1.4);
180
+ const head = new Path2D(); head.arc(x, y - 72, 40, 0, 7); fillKey(c, head, vbok(c, y - 112, y - 32, [[0, '#f7f0de'], [1, '#eaddc0']]), 2);
181
+ c.save(); c.globalAlpha = 0.35; c.fillStyle = '#e8a08a'; c.beginPath(); c.ellipse(x - 24, y - 58, 8, 5, 0, 0, 7); c.ellipse(x + 24, y - 58, 8, 5, 0, 0, 7); c.fill(); c.restore();
182
+ const hair = new Path2D(); hair.arc(x, y - 82, 41, Math.PI * 1.02, Math.PI * 1.98); hair.closePath(); hair.moveTo(x + 16, y - 128); hair.arc(x, y - 128, 16, 0, 7); c.fillStyle = C.key; c.fill(hair);
183
+ const dx = (look - 0.5) * 10; c.strokeStyle = C.key; c.lineWidth = 2.5; c.lineCap = 'round';
184
+ c.beginPath(); c.moveTo(x - 20 + dx, y - 70); c.lineTo(x - 8 + dx, y - 70); c.moveTo(x + 8 + dx, y - 70); c.lineTo(x + 20 + dx, y - 70); c.stroke();
185
+ c.fillStyle = C.red; c.beginPath(); if (mood < 0) c.arc(x + dx * 0.5, y - 46, 5, Math.PI, 0); else c.arc(x + dx * 0.5, y - 50, 5, 0, Math.PI); c.fill();
186
+ };
187
+ // vertical bokashi: a colour block wiped darker at one end, as the printer grades the block with a damp cloth
188
+ const vbok = (c, y0, y1, stops) => { const g = c.createLinearGradient(0, y0, 0, y1); stops.forEach(([u, col]) => g.addColorStop(u, col)); return g; };
189
+ // a golden mist mass dividing two scenes: scalloped cloud bank printed as a gold bokashi (deep core wiping out
190
+ // pale toward the scallops), dusted with mica, outlined by a fine keyline. Static per position -> cached.
191
+ const KAS = {};
192
+ const kasumi = (c, x, top, bot) => {
193
+ const key = Math.round(x) + ':' + top + ':' + bot;
194
+ let L = KAS[key];
195
+ if (!L) {
196
+ const r = H.rng(Math.round(x)), bumps = [];
197
+ for (let y = top; y <= bot; y += 60) bumps.push([x - 90 + r.range(-20, 20), y + 30]);
198
+ for (let y = bot; y >= top; y -= 60) bumps.push([x + 90 + r.range(-20, 20), y]);
199
+ const ox = x - 160, oy = top - 50, lw = 320, lh = bot - top + 130;
200
+ L = H.layer(lw, lh); L.ox = ox; L.oy = oy; const k = L.getContext('2d'); k.translate(-ox, -oy);
201
+ const shape = new Path2D(); shape.rect(x - 90, top, 180, bot + 30 - top); bumps.forEach(([bx, by]) => { shape.moveTo(bx + 34, by); shape.arc(bx, by, 34, 0, Math.PI * 2); });
202
+ // outline of the union: stroke every lobe, then fill every lobe over the inner halves
203
+ k.strokeStyle = 'rgba(27,34,54,0.72)'; k.lineWidth = 2.6; k.lineJoin = 'round';
204
+ bumps.forEach(([bx, by]) => { k.beginPath(); k.arc(bx, by, 34, 0, Math.PI * 2); k.stroke(); }); k.strokeRect(x - 90, top, 180, bot + 30 - top);
205
+ const g = k.createLinearGradient(x - 126, 0, x + 126, 0);
206
+ g.addColorStop(0, '#ece0bc'); g.addColorStop(0.2, '#e2cd92'); g.addColorStop(0.5, '#d4b25a'); g.addColorStop(0.8, '#e2cd92'); g.addColorStop(1, '#ece0bc');
207
+ k.save(); k.translate(1.2, 0.8); k.fillStyle = g; k.fill(shape); k.restore();
208
+ k.save(); k.clip(shape);
209
+ // a second, horizontal wipe: the gold deepens toward the top of the bank
210
+ const gv = k.createLinearGradient(0, top - 34, 0, bot + 64); gv.addColorStop(0, 'rgba(176,132,48,0.28)'); gv.addColorStop(0.45, 'rgba(176,132,48,0)'); gv.addColorStop(1, 'rgba(255,248,220,0.18)');
211
+ k.fillStyle = gv; k.fillRect(x - 160, top - 50, 320, lh);
212
+ // inner echo of each scallop (the carved line printed in a darker gold, as on Hiroshige's kasumi)
213
+ k.strokeStyle = 'rgba(160,118,40,0.2)'; k.lineWidth = 1;
214
+ bumps.forEach(([bx, by], i) => { const sgn = i < bumps.length / 2 ? -1 : 1; k.beginPath(); k.arc(bx - sgn * 7, by, 24, sgn < 0 ? Math.PI * 0.62 : -Math.PI * 0.38, sgn < 0 ? Math.PI * 1.38 : Math.PI * 0.38); k.stroke(); });
215
+ // mica dust
216
+ const mr = H.rng(Math.round(x) + 991);
217
+ for (let i = 0; i < 1100; i++) { const px = mr.range(x - 130, x + 130), py = mr.range(top - 34, bot + 64), sz = mr.range(0.6, 1.6); k.fillStyle = mr() < 0.8 ? `rgba(255,248,222,${mr.range(0.35, 0.7)})` : `rgba(150,110,40,${mr.range(0.15, 0.3)})`; k.fillRect(px, py, sz, sz); }
218
+ k.restore();
219
+ KAS[key] = L;
220
+ }
221
+ c.drawImage(L, L.ox, L.oy);
222
+ };
223
+ const current = (c, pts, p, foam = true) => { // a stream line with claw-foam arrowhead
224
+ if (p <= 0) return; const poly = sample(pts, 16), n = Math.max(2, Math.floor(poly.length * p)), part = poly.slice(0, n), nn = normals(part);
225
+ for (const off of [-8, 0, 8]) { c.strokeStyle = off ? C.mid : C.pruss; c.lineWidth = off ? 2 : 5; c.lineCap = 'round'; c.beginPath(); part.forEach(([x, y], i) => { const X = x + nn[i].nx * off, Y = y + nn[i].ny * off; i ? c.lineTo(X, Y) : c.moveTo(X, Y); }); c.stroke(); }
226
+ if (foam) { const e = part[part.length - 1], q = nn[nn.length - 1], r = H.rng(7); claw(c, e[0], e[1], q.tx * 0.6 + q.nx * 0.4, q.ty * 0.6 + q.ny * 0.4, -q.nx, -q.ny, 26, r); }
227
+ };
228
+ const seaBand = (c, x0, x1, y, t) => { // water with wave lines along the foot of the scroll
229
+ const g = c.createLinearGradient(0, y, 0, 720); g.addColorStop(0, C.light); g.addColorStop(0.18, C.mid); g.addColorStop(0.6, C.pruss); g.addColorStop(1, C.prussD); c.fillStyle = g; c.fillRect(x0, y, x1 - x0, 720 - y);
230
+ c.strokeStyle = 'rgba(241,232,210,0.75)'; c.lineWidth = 1.6; for (let i = 0; i < 6; i++) { const yy = y + 14 + i * 18; c.beginPath(); for (let x = x0; x <= x1; x += 8) { const v = yy + Math.sin(x * 0.03 + i * 1.7 + t * 1.2) * 3; x === x0 ? c.moveTo(x, v) : c.lineTo(x, v); } c.stroke(); }
231
+ c.strokeStyle = C.key; c.lineWidth = 2; c.beginPath(); for (let x = x0; x <= x1; x += 8) { const v = y + Math.sin(x * 0.02 + t) * 4; x === x0 ? c.moveTo(x, v) : c.lineTo(x, v); } c.stroke();
232
+ // little foam curls riding the waterline, anchored to the scroll
233
+ for (let k = Math.ceil(x0 / 150); k * 150 <= x1; k++) { const fx = k * 150 + 40 * Math.sin(k * 2.3), fy = y + Math.sin(fx * 0.02 + t) * 4, r = H.rng(1000 + k * 7); claw(c, fx, fy, 0.25, -1, 1, 0.25, 15 + r.range(0, 5), r); }
234
+ };
235
+
236
+ // Fuji's snowcap (panel 0 shape); s scales it to narrower summits
237
+ const fujiCap = (x, sx, sy, s) => { const cap = new Path2D(), P = (a, b) => [x + sx + (a - 905) * s, sy + (b - 400)];
238
+ cap.moveTo(...P(905, 400)); cap.lineTo(...P(960, 398)); cap.lineTo(...P(990, 445)); [[970, 430], [955, 450], [935, 428], [918, 448], [895, 432], [880, 444]].forEach(([a, b]) => cap.lineTo(...P(a, b))); cap.closePath(); return cap; };
239
+ // kozo fibres floating in the sheet (static -> cached)
240
+ let FIB = null;
241
+ const fibres = (W, HH) => { if (FIB) return FIB; FIB = H.layer(W, HH); const c = FIB.getContext('2d'), rf = H.rng(55);
242
+ for (let i = 0; i < 300; i++) { const x = rf.range(0, W), y = rf.range(0, HH), l = rf.range(10, 40), a = rf.range(0, Math.PI * 2);
243
+ c.strokeStyle = rf() < 0.5 ? 'rgba(255,250,235,0.24)' : 'rgba(120,95,60,0.13)'; c.lineWidth = rf.range(0.5, 1.1);
244
+ c.beginPath(); c.moveTo(x, y); c.quadraticCurveTo(x + Math.cos(a) * l * 0.5 + rf.range(-6, 6), y + Math.sin(a) * l * 0.5 + rf.range(-6, 6), x + Math.cos(a) * l, y + Math.sin(a) * l); c.stroke(); }
245
+ return FIB; };
246
+ const explainUkiyo = (c0, W, HH, t) => {
247
+ const E = H.ease, cl = H.clamp, S = (a, b) => cl((t - a) / (b - a), 0, 1);
248
+ const keys = [[0, 0], [2.0, 0], [2.9, 1], [5.7, 1], [6.5, 2], [10.1, 2], [10.8, 3], [12.5, 3], [13.2, 4]];
249
+ let pan = 0; for (let i = 0; i < keys.length - 1; i++) { const [ta, a] = keys[i], [tb, b] = keys[i + 1]; if (t >= ta) pan = t >= tb ? b : H.lerp(a, b, E.inOut((t - ta) / (tb - ta))); }
250
+ const camX = -pan * W; // the scroll unrolls right-to-left: later scenes lie to the left
251
+ const c = c0;
252
+ c.fillStyle = C.paper; c.fillRect(0, 0, W, HH);
253
+ c.save(); c.translate(-camX, 0);
254
+ const vis = (k) => Math.abs(pan - k) < 1.05, PX = (k) => -k * W; // panel k spans [PX(k), PX(k)+W]
255
+ // continuous sky bokashi and top mist band along the whole scroll
256
+ // the ochre sky plate printed over the whole scroll, paling to a light band at the horizon above the sea
257
+ c.fillStyle = vbok(c, 0, 600, [[0, C.sky], [0.2, C.sky], [0.82, '#e7d8b4'], [1, '#efe5cc']]); c.fillRect(-5 * W, 0, 7 * W, 600);
258
+ const sk = c.createLinearGradient(0, 0, 0, 200); sk.addColorStop(0, 'rgba(46,62,84,0.85)'); sk.addColorStop(1, 'rgba(90,100,110,0)'); c.fillStyle = sk; c.fillRect(-5 * W, 0, 7 * W, 200);
259
+ seaBand(c, camX - 50, camX + W + 50, 600, t);
260
+
261
+ // ---------- panel 0: hook ----------
262
+ if (vis(0)) {
263
+ const x = PX(0);
264
+ const sun = new Path2D(); const sy = H.lerp(460, 250, E.out(S(0.1, 1.4))); sun.arc(x + 930, sy, 90, 0, 7); c.save(); c.globalAlpha = 0.95; c.fillStyle = C.red; c.fill(sun); c.restore();
265
+ const fuji = new Path2D(); fuji.moveTo(x + 700, 600); fuji.quadraticCurveTo(x + 840, 520, x + 905, 400); fuji.lineTo(x + 960, 398); fuji.quadraticCurveTo(x + 1020, 520, x + 1170, 600); fuji.closePath(); fillKey(c, fuji, vbok(c, 398, 600, [[0, C.prussD], [0.55, C.fuji], [1, C.light]]));
266
+ const cap = fujiCap(x, 905, 400, 1); fillKey(c, cap, C.foam, 1.4);
267
+ current(c, [[x + 1250, 560], [x + 1000, 540], [x + 700, 560], [x + 420, 530]], E.inOut(S(0.4, 1.6)));
268
+ }
269
+ // ---------- panel 1: data — a river of labelled prints floats to the automaton ----------
270
+ if (vis(1)) {
271
+ const x = PX(1), kx = x + 430, ky = 410, K1 = 1.3;
272
+ karakuri(c, kx, ky, 0.9, 1, [t * 0.5, -t * 0.7, t * 0.4]);
273
+ tanzaku(c, 'model', kx + 150, ky + 150, S(2.6, 2.9));
274
+ // river current (arrow) into the doll's tray
275
+ current(c, [[x + 1250, 520], [x + 950, 490], [x + 720, 520], [x + 580, 470]], E.inOut(S(2.6, 3.3)));
276
+ // boats, each carrying a print with a tanzaku label
277
+ for (let i = 0; i < 9; i++) {
278
+ const u = (t - 2.7 - i * 0.6) * 0.42; if (u < 0 || u > 1) continue;
279
+ const kind = H.rng(i * 17 + 2)() < 0.5 ? 'cat' : 'dog', bx = H.lerp(x + 1330, x + 560, u), by = 500 + Math.sin(u * 9 + i) * 8, sh = 1 - E.in(cl((u - 0.82) / 0.18, 0, 1));
280
+ if (sh < 0.05) continue;
281
+ c.save(); c.translate(bx, by); c.scale(sh * 1.35, sh * 1.35); c.rotate(Math.sin(t * 2 + i) * 0.05);
282
+ const hull = new Path2D(); hull.moveTo(-60, 0); hull.lineTo(60, 0); hull.lineTo(44, 20); hull.lineTo(-48, 20); hull.closePath(); fillKey(c, hull, vbok(c, 0, 20, [[0, '#94503a'], [1, '#5e2c1e']]), 1.6);
283
+ print(c, kind, 0, -52, 70); tanzaku(c, kind, 0, 0, 1);
284
+ c.restore();
285
+ }
286
+ // counter cartouche (numbers re-cut as they climb)
287
+ const cp = S(3.0, 5.4), n = cp < 1 ? Math.pow(cp, 3) * 1e6 : 1e6;
288
+ if (t > 2.9) { cartouche(c, Math.round(n).toLocaleString('en-US'), x + 830, 230, 66, S(2.9, 3.2)); cartouche(c, 'examples · training data', x + 830, 335, 26, S(3.1, 3.4), '#f1e8d2'); }
289
+ }
290
+ // ---------- panel 2: guess, check, adjust — the whirlpool loop ----------
291
+ if (vis(2)) {
292
+ const x = PX(2), kx = x + 430, ky = 410, adj = E.inOut(S(8.3, 9.1)), right = t >= 9.3;
293
+ karakuri(c, kx, ky, 0.95, t < 7.4 ? 0 : right ? 1 : -1, [adj * 4, -adj * 5, adj * 3]);
294
+ tanzaku(c, 'weights', kx - 228, ky + 150, S(6.5, 6.8));
295
+ // new print on a stand
296
+ const st = new Path2D(); st.rect(x + 695, 470, 10, 130); fillKey(c, st, C.hull, 1.4);
297
+ print(c, 'cat', x + 700, 390, 120 * E.out(S(6.4, 6.8)));
298
+ tanzaku(c, 'new picture', x + 700, 512, S(6.7, 7.0));
299
+ // the guess, judged by a seal
300
+ if (!right) {
301
+ cartouche(c, 'dog?', x + 560, 150, 50, S(6.9, 7.1));
302
+ seal(c, x + 675, 148, 70, 'x', S(7.45, 7.6));
303
+ const rp = S(7.55, 7.95); if (rp > 0) { c.strokeStyle = C.red; c.lineWidth = 5; c.lineCap = 'round'; c.beginPath(); c.ellipse(x + 605, 150, 145, 70, -0.05, -1.6, -1.6 + Math.PI * 1.9 * rp); c.stroke(); }
304
+ if (t > 7.8) cartouche(c, 'wrong — it is a cat', x + 600, 250, 26, S(7.8, 8.0), '#f1e8d2', 'center', C.red);
305
+ // ADJUST: the error runs back along a red thread to the gear cabinet
306
+ const ep = S(7.9, 8.5); if (ep > 0) { const pts = sample([[x + 675, 185], [x + 560, 300], [x + 330, 250], [kx - 228, ky - 20]], 16), n = Math.floor(pts.length * ep); c.strokeStyle = C.red; c.lineWidth = 3.5; c.beginPath(); pts.slice(0, Math.max(2, n)).forEach(([a, b], i) => (i ? c.lineTo(a, b) : c.moveTo(a, b))); c.stroke(); if (ep >= 1) cartouche(c, 'error', x + 470, 300, 24, 1, C.red, 'center', '#efe2c6'); }
307
+ } else {
308
+ cartouche(c, 'cat', x + 560, 150, 50, S(9.35, 9.55));
309
+ seal(c, x + 672, 148, 70, 'v', S(9.65, 9.8));
310
+ }
311
+ // whirlpool diagram (the loop)
312
+ const wx = x + 990, wy = 370, rot = t * 0.9;
313
+ const wp = E.out(S(6.6, 7.2));
314
+ if (wp > 0) {
315
+ const disk = new Path2D(); disk.arc(wx, wy, 170 * wp, 0, 7); const dg = c.createRadialGradient(wx, wy, 0, wx, wy, 170 * wp); dg.addColorStop(0, C.prussD); dg.addColorStop(0.45, C.pruss); dg.addColorStop(1, C.mid); fillKey(c, disk, dg, 2);
316
+ c.save(); c.clip(disk); c.strokeStyle = 'rgba(241,232,210,0.85)'; c.lineWidth = 3; c.beginPath(); for (let a = 0; a < 26; a += 0.05) { const r = 162 - a * 6; c.lineTo(wx + Math.cos(a + rot) * r, wy + Math.sin(a + rot) * r); } c.stroke();
317
+ c.strokeStyle = C.pruss; c.lineWidth = 5; c.beginPath(); for (let a = 0; a < 26; a += 0.05) { const r = 155 - a * 6; c.lineTo(wx + Math.cos(a + rot) * r, wy + Math.sin(a + rot) * r); } c.stroke(); c.restore();
318
+ const rc = H.rng(3); for (let i = 0; i < 18; i++) { const a = i / 18 * Math.PI * 2 + rot * 0.5; claw(c, wx + Math.cos(a) * 170 * wp, wy + Math.sin(a) * 170 * wp, Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 22 * wp, rc); }
319
+ }
320
+ const steps = [['Guess', -Math.PI / 2], ['Check', Math.PI / 6], ['Adjust', Math.PI * 5 / 6]], act = t < 7.45 ? 0 : t < 8.3 ? 1 : t < 9.3 ? 2 : Math.floor((t - 9.3) * 3) % 3;
321
+ steps.forEach(([s, a], i) => cartouche(c, s, wx + Math.cos(a) * 100, wy + Math.sin(a) * 100, 27, S(6.9 + i * 0.15, 7.15 + i * 0.15), i === act && t > 6.9 ? '#e8b07a' : '#e8d49a'));
322
+ // currents chase round the rim from step to step
323
+ const arcPts = (a0, a1) => { const P = []; for (let k = 0; k <= 6; k++) { const a = H.lerp(a0, a1, k / 6); P.push([wx + Math.cos(a) * 212, wy + Math.sin(a) * 212]); } return P; };
324
+ current(c, arcPts(-Math.PI / 2 + 0.45, Math.PI / 6 - 0.45), E.out(S(7.2, 7.5)));
325
+ current(c, arcPts(Math.PI / 6 + 0.45, Math.PI * 5 / 6 - 0.45), E.out(S(7.9, 8.2)));
326
+ current(c, arcPts(Math.PI * 5 / 6 + 0.45, Math.PI * 1.5 - 0.45), E.out(S(9.0, 9.3)));
327
+ // a cube of data rides the whirlpool round the loop
328
+ if (t > 7.0) { const a = -Math.PI / 2 + (t - 7.0) * 2.1; cube(c, wx + Math.cos(a) * 145, wy + Math.sin(a) * 145, 13, 0); }
329
+ }
330
+ // ---------- panel 3: progress — accuracy climbs like a mountain path ----------
331
+ if (vis(3)) {
332
+ const x = PX(3), gx = x + 150, gw = 680, base = 580, prog = E.inOut(S(10.8, 12.0));
333
+ const pts = []; for (let i = 0; i <= 40; i++) { const u = i / 40, acc = 61 + 36 * (1 - Math.pow(1 - u, 2.2)) + Math.sin(i * 1.7) * 1.5 * (1 - u); pts.push([gx + u * gw, base - (acc - 40) / 60 * 420]); }
334
+ const ridge = new Path2D(); ridge.moveTo(gx, base); pts.forEach((p) => ridge.lineTo(...p)); ridge.lineTo(gx + gw, base); ridge.closePath();
335
+ const sunY = H.lerp(520, 150, prog); const sun = new Path2D(); sun.arc(gx + gw * prog, sunY - 40, 42, 0, 7); c.fillStyle = C.red; c.fill(sun);
336
+ c.save(); c.beginPath(); c.rect(gx - 5, 0, gw * prog + 6, HH); c.clip();
337
+ const g = c.createLinearGradient(0, 150, 0, base); g.addColorStop(0, C.green); g.addColorStop(1, C.pruss); c.save(); c.translate(1.5, 1); c.fillStyle = g; c.fill(ridge); c.restore();
338
+ c.strokeStyle = 'rgba(241,232,210,0.7)'; c.lineWidth = 1.5; for (let k = 1; k < 6; k++) { c.beginPath(); pts.forEach(([a, b], i) => (i ? c.lineTo(a, b + k * 26) : c.moveTo(a, b + k * 26))); c.stroke(); }
339
+ keyline(c, ridge, 2.4); c.restore();
340
+ c.strokeStyle = C.key; c.lineWidth = 2; c.beginPath(); c.moveTo(gx, base + 4); c.lineTo(gx + gw, base + 4); c.stroke();
341
+ tanzaku(c, 'training rounds →', gx + gw / 2, base + 20, S(10.8, 11.1));
342
+ const acc = Math.round(H.lerp(61, 97, prog));
343
+ cartouche(c, 'accuracy', x + 990, 148, 30, S(10.7, 11.0));
344
+ cartouche(c, acc + '%', x + 990, 270, 72, S(10.7, 11.0));
345
+ cartouche(c, '61% → 97%', x + 990, 398, 30, S(11.0, 11.3), '#f1e8d2');
346
+ seal(c, x + 1110, 470, 70, '優', S(12.05, 12.2), -0.08); // emphasis: "excellent" seal stamped on 97%
347
+ const rp = S(12.1, 12.45); if (rp > 0) { c.strokeStyle = C.red; c.lineWidth = 5; c.beginPath(); c.ellipse(x + 990, 270, 158, 86, -0.04, -1.5, -1.5 + Math.PI * 1.9 * rp); c.stroke(); }
348
+ }
349
+ // ---------- panel 4: takeaway ----------
350
+ if (vis(4)) {
351
+ const x = PX(4);
352
+ const fuji = new Path2D(); fuji.moveTo(x + 380, 600); fuji.quadraticCurveTo(x + 560, 500, x + 620, 380); fuji.lineTo(x + 660, 378); fuji.quadraticCurveTo(x + 720, 500, x + 900, 600); fuji.closePath(); fillKey(c, fuji, vbok(c, 378, 600, [[0, C.prussD], [0.55, C.fuji], [1, C.light]]));
353
+ fillKey(c, fujiCap(x, 620, 380, 40 / 55), C.foam, 1.4);
354
+ cartouche(c, 'More examples', x + 640, 190, 58, S(12.9, 13.2));
355
+ current(c, [[x + 640, 245], [x + 615, 270], [x + 638, 294]], E.out(S(13.1, 13.4)), true);
356
+ cartouche(c, 'better guesses', x + 640, 365, 58, S(13.2, 13.5));
357
+ seal(c, x + 900, 440, 70, '学', S(13.5, 13.65));
358
+ }
359
+ // ---------- kasumi clouds divide the scenes ----------
360
+ for (let k = 0; k <= 4; k++) { const bx = PX(k) + (k === 0 ? W : 0); if (bx < camX - 200 || bx > camX + W + 200) continue; kasumi(c, bx, 120, 560); }
361
+ // mist bands top and bottom edge of the scroll
362
+ c.fillStyle = vbok(c, 0, 30, [[0, '#cfac55'], [0.55, 'rgba(214,184,104,0.85)'], [1, 'rgba(220,194,122,0)']]); c.fillRect(camX - 10, 0, W + 20, 30);
363
+ c.fillStyle = vbok(c, HH - 28, HH, [[0, 'rgba(220,194,122,0)'], [0.45, 'rgba(214,184,104,0.85)'], [1, '#cfac55']]); c.fillRect(camX - 10, HH - 28, W + 20, 28);
364
+ c.restore();
365
+
366
+ // ---------- pinned title cartouche (the hook shrinks into it) ----------
367
+ const hp = E.inOut(S(1.6, 2.3));
368
+ const tx = H.lerp(560, 1030, hp), ty = H.lerp(300, 58, hp), ts = H.lerp(1, 0.5, hp);
369
+ c.save(); c.translate(tx, ty); c.scale(ts, ts);
370
+ if (t > 0.15) { cartouche(c, 'How does AI learn?', 0, 0, 56, S(0.15, 0.5)); cartouche(c, 'AIはどう学ぶのか', 0, 92, 26 * H.lerp(1, 1.35, hp), S(0.5, 0.8), '#f1e8d2'); }
371
+ c.restore();
372
+ if (hp > 0.9) seal(c, 1250, 54, 30, '学', S(2.3, 2.45));
373
+
374
+ // ---------- woodblock finish ----------
375
+ const img = c.getImageData(0, 0, W, HH), d = img.data, { warp, fine, pap, blot } = fields(W, HH), rg = H.rng(2);
376
+ for (let y = 0, p = 0; y < HH; y++) for (let x = 0; x < W; x++, p++) {
377
+ const i = p * 4, lum = (d[i] * 0.3 + d[i + 1] * 0.55 + d[i + 2] * 0.15) / 255, inked = H.clamp((0.85 - lum) * 1.6, 0, 1), plate = H.clamp((0.94 - lum) * 1.3, 0, 1);
378
+ const grain = Math.sin(y * 0.55 + warp[p] * 26 + x * 0.004) * 0.5 + fine[p] * 0.8;
379
+ // wood grain and uneven take-up show in every printed block, pale plates included
380
+ let s = 1 + grain * (0.07 + 0.07 * inked) * plate; s *= 1 + pap[p] * 0.035 + (rg() - 0.5) * 0.04; s *= 1 - Math.max(0, blot[p]) * 0.12 * plate;
381
+ d[i] = d[i] * s + 6 * (1 - inked); d[i + 1] = d[i + 1] * s; d[i + 2] = d[i + 2] * s * 0.97 - 4 * (1 - inked);
382
+ }
383
+ c.putImageData(img, 0, 0);
384
+ c.drawImage(fibres(W, HH), 0, 0);
385
+ H.vignette(c, W, HH, 0.25, '90,60,30');
386
+ };
387
+
388
+ registerStyle({
389
+ id: 'ukiyo-e',
390
+ name: 'Ukiyo-e Woodblock',
391
+ category: 'Art & Design Movements',
392
+ description: 'An Edo-period woodblock print in the manner of Hokusai: Prussian-blue bokashi gradients, a towering wave with claw-like foam whose spray breaks into data cubes, a cargo ship stacked with server racks in the trough, flat colour blocks with carved keyblock outlines, wood grain and washi fibre, a vertical-text cartouche and vermilion seal.',
393
+ motion: 'Printed like a real woodblock: the black keyblock lands first, then each colour block is pressed in turn (sky ochre, pale blue, mid blue, vermilion, Prussian) and the bokashi sky is wiped on last, while the wave swells, its claws lean and data-cube spray tumbles onto the rolling ship.',
394
+ fonts: ['700 26px "Noto Serif JP"', '400 15px "Noto Serif JP"', '900 22px "Noto Serif JP"'],
395
+ frameT: 5.2,
396
+ duration: 5.5,
397
+ readsPixels: true,
398
+ explainerDuration: 14.5,
399
+ explainerNote: 'An emaki handscroll unrolled right-to-left: golden kasumi clouds divide the scenes, a river of labelled prints floats to an Edo karakuri automaton, flow is drawn as currents with claw-foam heads, the learning loop is a Naruto whirlpool, numbers sit in paper cartouches and every judgement is a vermilion seal stamp.',
400
+ explainer(ctx, W, HH, t) { explainUkiyo(ctx, W, HH, t); },
401
+ // Sound: an emaki handscroll and a woodblock workshop. The scroll unrolls between scenes (paper over a wooden
402
+ // roller), water laps under the whole scroll, currents swish as they flow, counters are a carving chisel, the
403
+ // karakuri's wooden gears ratchet, every judgement is a vermilion seal pressed down, and a koto answers each
404
+ // guess (unresolved for "dog?", bent down for wrong, resolving for right). A taiko lands the 97%.
405
+ // Times copied from explainUkiyo().
406
+ explainerSound(k) {
407
+ const D = 14.5, E = H.ease, cl = H.clamp;
408
+ const S = (t, a, b) => cl((t - a) / (b - a), 0, 1);
409
+ const pan = (px) => cl((px / 1280) * 2 - 1, -1, 1) * 0.7;
410
+ const when = (a, b, ease, u) => { for (let i = 0; i <= 400; i++) if (ease(i / 400) >= u - 1e-6) return a + ((b - a) * i) / 400; return b; };
411
+ const card = (a, b) => a + (b - a) / 1.5; // cartouches fade in fully at 2/3 of their S(a,b)
412
+ // koto pluck: bright attack that darkens, a tiny pitch settle
413
+ const koto = (t, n, g, p, minor, bend) => { const f = k.hz(n);
414
+ k.tone(t, { f: bend ? [[0, f], [0.08, f], [0.5, f * 0.89]] : [[0, f * 1.012], [0.05, f]], type: 'sawtooth', dur: 1.3, gain: 0.05 * g, a: 0.002, filter: 'lowpass', f2: [[0, 2600], [0.25, 900], [1.3, 500]], q: 1.5, pan: p, verb: 0.3, minor });
415
+ k.tone(t, { f, type: 'triangle', dur: 1.0, gain: 0.06 * g, a: 0.002, pan: p, minor }); };
416
+ // seal pressed into the paper: a firm soft thump, the paper giving a little
417
+ const sealP = (t, g, p, minor) => { k.play('stamp', t, { gain: 0.6 * g, pan: p, minor }); k.noise(t + 0.01, { dur: 0.12, color: 'pink', filter: 'bandpass', f: 1100, q: 0.7, gain: 0.08 * g, a: 0.01, pan: p, minor }); };
418
+ // carving chisel knocking out a numeral
419
+ const chisel = (t, g, p) => k.withMinor(() => { k.noise(t, { dur: 0.03, filter: 'bandpass', f: 1600, q: 2, gain: 0.3 * g, a: 0.001, pan: p }); k.tone(t, { f: 240, dur: 0.05, gain: 0.08 * g, pan: p }); });
420
+ // hyoshigi: two dry wooden clappers struck once
421
+ const clack = (t, g, p) => { k.noise(t, { dur: 0.06, filter: 'bandpass', f: 1250, q: 6, gain: 0.5 * g, a: 0.001, pan: p }); k.noise(t, { dur: 0.015, filter: 'highpass', f: 2500, gain: 0.15 * g, pan: p }); };
422
+ // a current of water flowing along
423
+ const swish = (t, dur, g, p) => k.noise(t, { dur: dur + 0.15, color: 'pink', filter: 'bandpass', f: [[0, 400], [dur * 0.6, 1100], [dur + 0.15, 500]], q: 1.1, gain: 0.08 * g, a: dur * 0.5, pan: p, minor: true });
424
+ // the scroll unrolls: paper sliding off the wooden roller
425
+ const unroll = (t, dur) => {
426
+ k.noise(t, { dur, color: 'brown', filter: 'bandpass', f: [[0, 160], [dur * 0.5, 300], [dur, 160]], q: 1, gain: 0.18, a: dur * 0.4 });
427
+ k.play('paper', t + 0.2, { dur: dur - 0.3, gain: 0.75 });
428
+ };
429
+ const IN = ['D4', 'Eb4', 'G4', 'A4', 'Bb4', 'D5'];
430
+
431
+ k.bed('room', 0, D, { gain: 0.3 });
432
+ k.noise(0, { dur: D, color: 'pink', filter: 'lowpass', f: 450, gain: 0.035, a: 1, hold: D - 2, minor: true }); // water under the scroll
433
+
434
+ // ---- panel 0: the sun rises (0.1-1.4), a current flows (0.4-1.6), title cartouches fade in, seal (2.45)
435
+ swish(0.4, 1.2, 1, 0.3);
436
+ koto(card(0.15, 0.5), 'D4', 1, -0.2); koto(card(0.15, 0.5) + 0.12, 'A4', 0.8, -0.1);
437
+ koto(card(0.5, 0.8), 'G4', 0.8, 0.1, true); koto(card(0.5, 0.8) + 0.16, 'D5', 0.6, 0.2, true);
438
+ k.noise(1.6, { dur: 0.7, color: 'pink', filter: 'bandpass', f: [[0, 400], [0.7, 900]], q: 0.7, gain: 0.06, a: 0.3, pan: 0.5, minor: true }); // title slides to the corner
439
+ sealP(2.45, 0.5, 0.9, true);
440
+
441
+ // ---- the scroll unrolls between scenes
442
+ [[2.0, 2.9], [5.7, 6.5], [10.1, 10.8], [12.5, 13.2]].forEach(([a, b]) => unroll(a, b - a));
443
+
444
+ // ---- panel 1: DATA — a river of prints floats to the automaton
445
+ swish(2.6, 0.7, 1, 0.2); // river current
446
+ const boats = []; for (let i = 0; 2.8 + i * 0.6 < 5.6; i++) boats.push(2.8 + i * 0.6);
447
+ boats.forEach((tb) => k.noise(tb, { dur: 0.22, color: 'pink', filter: 'bandpass', f: [[0, 500], [0.22, 300]], q: 1.4, gain: 0.12, a: 0.03, pan: 0.6, minor: true })); // each boat slides in on a lap of water
448
+ k.play('paper', card(2.9, 3.2), { dur: 0.12, gain: 0.25, pan: pan(830), minor: true }); // cartouche
449
+ // counter n = S(3.0,5.4)^3 * 1e6: the chisel re-cuts the numerals, faster as they climb
450
+ for (let t = 3.12; t < 5.32; ) { const s = S(t, 3.0, 5.4); if (!boats.some((b) => t > b - 0.04 && t < b + 0.06)) chisel(t, 0.7 + 0.4 * s, pan(830)); t += 0.16 - 0.075 * s; }
451
+ clack(5.4, 1, pan(830)); k.play('thud', 5.4, { f: 120, gain: 0.3, pan: pan(830) }); // 1,000,000
452
+
453
+ // ---- panel 2: GUESS / CHECK / ADJUST — the whirlpool
454
+ k.noise(6.6, { dur: 0.8, color: 'pink', filter: 'bandpass', f: [[0, 300], [0.35, 900], [0.8, 500]], q: 2, gain: 0.12, a: 0.3, pan: 0.4 }); // whirlpool swirls open
455
+ // the guess: "dog?" (6.9-7.1) — an unresolved koto pair
456
+ koto(card(6.9, 7.1), 'G4', 1, pan(560)); koto(card(6.9, 7.1) + 0.14, 'Eb4', 0.9, pan(560));
457
+ swish(7.2, 0.3, 0.8, 0.5);
458
+ // CHECK: the X seal comes down (7.45 -> 7.6), red ring brushed round (7.55-7.95) — wrong
459
+ sealP(7.6, 1.2, pan(675));
460
+ k.play('thud', 7.6, { f: 75, gain: 0.5, pan: pan(675) });
461
+ koto(7.64, 'Bb3', 1.1, pan(600), false, true); koto(7.64, 'Eb4', 0.8, pan(600), false, true);
462
+ k.play('stroke', 7.62, { material: 'brush', dur: 0.36, gain: 0.7, pan: pan(605), minor: true });
463
+ // ADJUST: the red thread runs back to the gear cabinet (7.9-8.5), "error" cartouche lands, gears ratchet (inOut 8.3-9.1)
464
+ k.play('stroke', 7.92, { material: 'brush', dur: 0.55, gain: 0.8, pan: pan(450), minor: true });
465
+ swish(7.9, 0.3, 0.7, 0.5);
466
+ clack(8.5, 0.5, pan(470));
467
+ for (let i = 1, lt = 8.5; i <= 9; i++) { const t = when(8.3, 9.1, E.inOut, i / 9); if (t - lt < 0.09) continue; lt = t; k.play('tock', t, { f: 380 + (i % 2) * 60, gain: 0.6, pan: pan(200), minor: true }); }
468
+ swish(9.0, 0.3, 0.8, 0.5);
469
+ // the correction: "cat" (9.35-9.55), the tick seal comes down (9.8) — the koto resolves
470
+ koto(card(9.35, 9.55), 'D4', 0.9, pan(560));
471
+ sealP(9.8, 0.9, pan(672));
472
+ ['G4', 'A4', 'D5'].forEach((n, i) => koto(9.82 + i * 0.1, n, 0.9 - i * 0.1, pan(672) - 0.1 * i));
473
+
474
+ // ---- panel 3: PROGRESS — the ridge is printed in as accuracy climbs (inOut 10.8-12.0)
475
+ // baren rubbing the ridge print as it appears
476
+ for (let x = 0; x < 1.15; x += 0.19) k.noise(10.82 + x, { dur: 0.2, color: 'pink', filter: 'bandpass', f: [[0, 500], [0.1, 800], [0.2, 500]], q: 1, gain: 0.06, a: 0.08, pan: pan(200 + x * 500), minor: true });
477
+ for (let t = 10.85, last = 61, lt = -1; t < 11.97; t += 1 / 120) {
478
+ const v = Math.round(H.lerp(61, 97, E.inOut(S(t, 10.8, 12.0))));
479
+ if (v !== last && t - lt >= 1 / 11) { chisel(t, 0.9, pan(990)); last = v; lt = t; }
480
+ }
481
+ // 97%: the 優 seal comes down (12.05 -> 12.2) and the ring is brushed round (12.1-12.45) — the biggest hit, a taiko
482
+ k.play('impact', 12.2, { gain: 1.3, pan: pan(1110) });
483
+ k.play('thud', 12.2, { f: 62, gain: 0.9 });
484
+ sealP(12.2, 1.2, pan(1110));
485
+ ['D3', 'A3', 'D4', 'G4'].forEach((n, i) => koto(12.22 + i * 0.03, n, 1.0, (i - 1.5) * 0.2));
486
+
487
+ // ---- panel 4: TAKEAWAY: More examples (13.1), current, better guesses (13.4), the 学 seal (13.65)
488
+ koto(card(12.9, 13.2), 'A4', 0.7, -0.1, true);
489
+ swish(13.1, 0.3, 0.7, 0);
490
+ koto(card(13.2, 13.5), 'G4', 0.7, 0.1, true);
491
+ sealP(13.65, 0.9, pan(900));
492
+ ['D3', 'A3', 'D4', 'Eb4', 'G4'].forEach((n, i) => koto(13.68 + i * 0.07, n, 0.85 - i * 0.08, (i - 2) * 0.2)); // resolves
493
+ },
494
+ draw(c0, W, HH, t) {
495
+ const swell = Math.sin(t * 0.9) * 6;
496
+ const scene = (c) => {
497
+ // ---------- paper ----------
498
+ c.fillStyle = C.paper; c.fillRect(0, 0, W, HH);
499
+
500
+ // ---------- sky: bokashi from the top ----------
501
+ c.fillStyle = C.sky; c.fillRect(0, 0, W, 560);
502
+ const sk = c.createLinearGradient(0, 0, 0, 230);
503
+ sk.addColorStop(0, 'rgba(46,62,84,0.92)'); sk.addColorStop(0.45, 'rgba(70,86,104,0.45)'); sk.addColorStop(1, 'rgba(90,100,110,0)');
504
+ c.fillStyle = sk; c.fillRect(0, 0, W, 240); // bokashi (separated out and printed last in the build)
505
+ // flat cloud bands
506
+ c.fillStyle = 'rgba(236,224,196,0.85)';
507
+ const cloud = (x, y, w) => { const p = new Path2D(); p.moveTo(x, y); p.bezierCurveTo(x + w * 0.2, y - 18, x + w * 0.45, y - 22, x + w * 0.6, y - 8); p.bezierCurveTo(x + w * 0.75, y - 20, x + w * 0.95, y - 10, x + w, y); p.closePath(); c.fill(p); keyline(c, p, 1.2, 'rgba(27,34,54,0.6)'); };
508
+ cloud(700, 300, 260); cloud(980, 250, 200);
509
+
510
+ // ---------- Mt Fuji (far) ----------
511
+ const fx = 940, fb = 520;
512
+ const fuji = new Path2D();
513
+ fuji.moveTo(fx - 170, fb); fuji.quadraticCurveTo(fx - 70, fb - 40, fx - 22, fb - 104); fuji.lineTo(fx + 22, fb - 106); fuji.quadraticCurveTo(fx + 70, fb - 40, fx + 170, fb); fuji.closePath();
514
+ c.save(); c.translate(1.5, 1); c.fillStyle = C.fuji; c.fill(fuji); c.restore();
515
+ const cap = new Path2D(); cap.moveTo(fx - 22, fb - 104); cap.lineTo(fx + 22, fb - 106); cap.lineTo(fx + 48, fb - 70);
516
+ [[34, -78], [22, -64], [10, -76], [-4, -60], [-16, -74], [-30, -62], [-44, -72]].forEach(([dx, dy]) => cap.lineTo(fx + dx, fb + dy));
517
+ cap.closePath(); c.fillStyle = C.foam; c.fill(cap); keyline(c, cap, 1.2);
518
+ keyline(c, fuji, 1.6);
519
+
520
+ // ---------- distant sea ----------
521
+ const sea = c.createLinearGradient(0, 500, 0, 720);
522
+ sea.addColorStop(0, C.mid); sea.addColorStop(1, C.pruss);
523
+ c.fillStyle = sea; c.fillRect(0, 510, W, 210);
524
+ c.strokeStyle = 'rgba(241,232,210,0.7)'; c.lineWidth = 1.5;
525
+ for (let i = 0; i < 9; i++) { const y = 528 + i * 18; c.beginPath(); for (let x = 600; x < W; x += 6) { const yy = y + Math.sin(x * 0.03 + i + t) * 3; x === 600 ? c.moveTo(x, yy) : c.lineTo(x, yy); } c.stroke(); }
526
+
527
+ // ---------- secondary wave (right foreground, a small Fuji-shaped crest) ----------
528
+ {
529
+ const pts = sample([[760, 720], [820, 640], [920, 590 + swell * 0.5], [1010, 576], [1080, 600], [1100, 630], [1080, 640]]);
530
+ const back = [...pts, [1150, 650], [1300, 640], [1300, 720]];
531
+ const body = pathOf(back, true);
532
+ const g = c.createLinearGradient(0, 580, 0, 720); g.addColorStop(0, C.mid); g.addColorStop(1, C.prussD);
533
+ c.save(); c.translate(1.5, 1); c.fillStyle = g; c.fill(body); c.restore();
534
+ const nn = normals(pts);
535
+ c.save(); c.clip(body);
536
+ veins(c, pts, nn, { count: 6, d0: 14, step: 14, sign: -1, seed: 21, band: 'rgba(147,180,204,0.4)', line: 'rgba(241,232,210,0.9)', from: [0.02, 0.25], to: [0.8, 0.98], w0: 1, w1: 2.4 });
537
+ c.restore();
538
+ keyline(c, body, 2.2);
539
+ const r = H.rng(5);
540
+ for (let i = 20; i < pts.length - 2; i += 5) { const n = nn[i]; claw(c, pts[i][0], pts[i][1], n.nx, n.ny, n.tx, n.ty, 20 + r.range(0, 6), r); }
541
+ }
542
+
543
+ // ---------- the cargo ship of servers (in the trough, under the lip) ----------
544
+ c.save(); c.translate(560, 520 + swell * 0.6); c.rotate(-0.17 + Math.sin(t * 0.9 + 1) * 0.02);
545
+ // container stacks = server racks
546
+ const rk = H.rng(9);
547
+ for (let s = 0; s < 6; s++) {
548
+ const x0 = -170 + s * 52, hgt = rk.int(2, 4);
549
+ for (let k = 0; k < hgt; k++) {
550
+ const p = new Path2D(); p.rect(x0, -28 - k * 26, 46, 24);
551
+ c.fillStyle = [C.grey, C.ochre, C.pale, C.grey][(s + k) % 4]; c.fill(p);
552
+ c.fillStyle = C.key; for (let u = 0; u < 3; u++) c.fillRect(x0 + 5, -28 - k * 26 + 5 + u * 6, 26, 1.6);
553
+ c.fillStyle = (s + k) % 3 ? C.red : C.green; c.fillRect(x0 + 36, -28 - k * 26 + 6, 4, 4);
554
+ keyline(c, p, 1.5);
555
+ }
556
+ }
557
+ // bridge
558
+ const br = new Path2D(); br.rect(150, -78, 44, 52); c.fillStyle = C.paper; c.fill(br); keyline(c, br, 1.5);
559
+ c.fillStyle = C.key; for (let u = 0; u < 3; u++) c.fillRect(156 + u * 13, -70, 8, 8);
560
+ const st = new Path2D(); st.rect(164, -104, 16, 26); c.fillStyle = C.red; c.fill(st); keyline(c, st, 1.4);
561
+ // hull
562
+ const hull = new Path2D(); hull.moveTo(-230, -30); hull.lineTo(230, -30); hull.lineTo(205, 22); hull.lineTo(-190, 22); hull.closePath();
563
+ c.save(); c.translate(1.5, 1); c.fillStyle = C.hull; c.fill(hull); c.restore();
564
+ c.fillStyle = C.ochre; c.fillRect(-222, -22, 446, 6);
565
+ keyline(c, hull, 2);
566
+ c.restore();
567
+
568
+ // ---------- main great wave ----------
569
+ const lean = Math.sin(t * 0.9) * 8;
570
+ const backPts = [[-40, 600], [10, 440], [80, 310], [175, 205], [290, 132], [405, 98 + lean * 0.3], [510, 108], [598, 146 + lean * 0.5], [650, 205 + lean], [655, 262 + lean], [625, 300 + lean], [585, 306 + lean]];
571
+ const back = sample(backPts, 16);
572
+ const facePts = sample([[585, 306 + lean], [548, 284 + lean * 0.8], [505, 268], [470, 285], [452, 340], [462, 420], [500, 500], [560, 560], [650, 600], [760, 622], [800, 720]], 12);
573
+ const body = pathOf([...back, ...facePts.slice(1), [-40, 720]], true);
574
+ const bn = normals(back);
575
+ // body fill with depth gradient
576
+ const g = c.createLinearGradient(0, 90, 300, 640);
577
+ g.addColorStop(0, C.pruss); g.addColorStop(0.5, C.prussD); g.addColorStop(1, '#10243c');
578
+ c.save(); c.translate(2, 1.5); c.fillStyle = g; c.fill(body); c.restore();
579
+ // lighter face inside the curl (bokashi from the face edge inward)
580
+ c.save(); c.clip(body);
581
+ const fg = c.createRadialGradient(560, 330, 20, 560, 330, 230);
582
+ fg.addColorStop(0, C.light); fg.addColorStop(0.55, 'rgba(77,124,166,0.85)'); fg.addColorStop(1, 'rgba(36,80,126,0)');
583
+ c.fillStyle = fg; c.fillRect(300, 100, 500, 560);
584
+ // striations following the back of the wave: lighter bands + broken white veins
585
+ veins(c, back, bn, { count: 13, d0: 30, step: 19, sign: -1, seed: 12, band: 'rgba(77,124,166,0.55)', line: 'rgba(241,232,210,0.92)', from: [0.08, 0.4], to: [0.92, 0.99], toK: 0.04, w0: 1.2, w1: 3.0 });
586
+ // face veins (parallel to the hollow)
587
+ const fnn = normals(facePts);
588
+ veins(c, facePts, fnn, { count: 4, d0: 12, step: 15, sign: 1, seed: 13, band: null, line: 'rgba(241,232,210,0.7)', from: [0.1, 0.18], to: [0.5, 0.7], w0: 2.2, w1: 0.8 });
589
+ c.restore();
590
+ // foam crest band
591
+ const crestStart = Math.floor(back.length * 0.32);
592
+ const band = [];
593
+ for (let i = crestStart; i < back.length; i++) band.push(back[i]);
594
+ for (let i = back.length - 1; i >= crestStart; i--) { const w = 26 * Math.min(1, (i - crestStart) / 18) + Math.abs(Math.sin(i * 0.45)) * 7; band.push([back[i][0] - bn[i].nx * w, back[i][1] - bn[i].ny * w]); }
595
+ const bandP = pathOf(band, true);
596
+ c.fillStyle = C.foam; c.fill(bandP);
597
+ // foam band inner edge carved as a keyline with little scalloped bumps
598
+ { const edge = new Path2D(); let first = true;
599
+ for (let i = crestStart; i < back.length; i++) { const w = 26 * Math.min(1, (i - crestStart) / 18) + Math.abs(Math.sin(i * 0.45)) * 7; const x = back[i][0] - bn[i].nx * w, y = back[i][1] - bn[i].ny * w; first ? edge.moveTo(x, y) : edge.lineTo(x, y); first = false; }
600
+ c.save(); c.clip(body); c.strokeStyle = C.key; c.lineWidth = 1.6; c.stroke(edge); c.restore(); }
601
+ keyline(c, body, 2.6);
602
+ // claws along the crest and lip
603
+ const rc = H.rng(31);
604
+ for (let i = crestStart; i < back.length - 1; i += 7) {
605
+ const n = bn[i], u = (i - crestStart) / (back.length - crestStart);
606
+ const L = H.lerp(26, 52, Math.min(1, u * 1.6)) * rc.range(0.85, 1.1);
607
+ claw(c, back[i][0], back[i][1], n.nx, n.ny, n.tx, n.ty, L, rc);
608
+ }
609
+ // lip-tip claws hanging down over the ship
610
+ for (let k = 0; k < 6; k++) {
611
+ const i = back.length - 1, x = back[i][0] - k * 22 + 8, y = back[i][1] - 4 + Math.sin(k) * 4;
612
+ claw(c, x, y, -0.15, 1, -1, 0.2, 30 + rc.range(-4, 10), rc);
613
+ }
614
+
615
+ // ---------- data-cube spray ----------
616
+ const rs = H.rng(77);
617
+ for (let i = 0; i < 46; i++) {
618
+ const u = rs();
619
+ const sx = H.lerp(300, 820, u) + rs.gauss() * 30;
620
+ const sy0 = H.lerp(70, 190, Math.abs(u - 0.6)) + rs.gauss() * 40;
621
+ const fall = ((t * 0.25 + rs()) % 1) * 60;
622
+ const s = rs.range(5, 14) * (1 - Math.abs(u - 0.55) * 0.6);
623
+ const y = sy0 + fall + (u > 0.6 ? (u - 0.6) * 380 : 0);
624
+ if (y > 470) continue;
625
+ cube(c, sx, y, s, rs.range(-0.4, 0.4) + t * rs.range(-0.5, 0.5));
626
+ }
627
+
628
+ // ---------- foreground water in front of the hull ----------
629
+ {
630
+ const pts = sample([[420, 720], [480, 668], [580, 638 + swell * 0.4], [672, 612], [732, 618], [762, 644], [748, 662]], 12);
631
+ const p = pathOf([...pts, [760, 720]], true);
632
+ const gg = c.createLinearGradient(0, 570, 0, 720); gg.addColorStop(0, C.pruss); gg.addColorStop(1, C.prussD);
633
+ c.save(); c.translate(1.5, 1); c.fillStyle = gg; c.fill(p); c.restore();
634
+ const nn = normals(pts);
635
+ c.save(); c.clip(p);
636
+ veins(c, pts, nn, { count: 5, d0: 12, step: 13, sign: -1, seed: 22, band: 'rgba(147,180,204,0.4)', line: 'rgba(241,232,210,0.88)', from: [0.0, 0.2], to: [0.75, 0.95], w0: 1, w1: 2.2 });
637
+ c.restore();
638
+ keyline(c, p, 2.2);
639
+ const r = H.rng(8);
640
+ for (let i = 10; i < pts.length - 1; i += 6) { const n = nn[i]; claw(c, pts[i][0], pts[i][1], n.nx, n.ny, n.tx, n.ty, 16 + r.range(0, 6), r); }
641
+ }
642
+
643
+ // ---------- cartouche + signature + seal ----------
644
+ const cx = 1150, cy = 40, cw = 52, ch = 228;
645
+ const cart = new Path2D(); cart.rect(cx, cy, cw, ch);
646
+ c.fillStyle = '#e8d49a'; c.fill(cart);
647
+ c.strokeStyle = C.key; c.lineWidth = 2; c.stroke(cart); c.lineWidth = 1; c.strokeRect(cx + 4, cy + 4, cw - 8, ch - 8);
648
+ c.fillStyle = C.key; c.font = '700 26px "Noto Serif JP"'; c.textAlign = 'center'; c.textBaseline = 'middle';
649
+ [...'データの大波'].forEach((chr, i) => { if (chr === 'ー') { c.save(); c.translate(cx + cw / 2, cy + 30 + i * 33); c.rotate(Math.PI / 2); c.fillText(chr, 0, 0); c.restore(); } else c.fillText(chr, cx + cw / 2, cy + 30 + i * 33); });
650
+ // second, smaller cartouche with series title
651
+ const c2 = new Path2D(); c2.rect(cx - 40, cy, 30, 120); c.fillStyle = '#d9c38a'; c.fill(c2); c.strokeStyle = C.key; c.lineWidth = 1.5; c.stroke(c2);
652
+ c.fillStyle = C.key; c.font = '400 15px "Noto Serif JP"';
653
+ [...'電脳百景'].forEach((chr, i) => c.fillText(chr, cx - 25, cy + 20 + i * 26));
654
+ // signature + seal (lower left of cartouche area)
655
+ c.font = '400 15px "Noto Serif JP"';
656
+ [...'雲斎筆'].forEach((chr, i) => c.fillText(chr, cx - 25, cy + 160 + i * 20));
657
+ c.save(); c.translate(cx - 25, cy + 236); c.rotate(0.02);
658
+ c.fillStyle = C.red; c.fillRect(-15, -15, 30, 30);
659
+ c.fillStyle = '#efe2c6'; c.font = '900 22px "Noto Serif JP"'; c.fillText('電', 0, 1);
660
+ c.restore();
661
+
662
+ };
663
+
664
+ // ---------- printing build: keyblock first, then one colour block per impression, bokashi last ----------
665
+ const COL = H.layer(W, HH), cc = COL.getContext('2d', { willReadFrequently: true });
666
+ scene(cc);
667
+ const KEY = H.layer(W, HH), kc = KEY.getContext('2d', { willReadFrequently: true });
668
+ keyOnly(kc); scene(kc);
669
+ const press = (t0) => H.clamp((t - t0) / 0.16, 0, 1); // an impression lands in ~5 frames
670
+ // plates: 0 key, 1 warm (sky/ochre), 2 pale blue, 3 mid blue, 4 vermilion/brown, 5 Prussian
671
+ const prog = [press(0.35), press(1.0), press(1.5), press(2.0), press(2.45), press(2.9)];
672
+ const cd = cc.getImageData(0, 0, W, HH).data, kd = kc.getImageData(0, 0, W, HH).data;
673
+ const out = c0.createImageData(W, HH), od = out.data;
674
+ const PA = H.hex(C.paper), SKY = H.hex(C.sky), SL = [46, 62, 84], BL = Math.hypot(SKY[0] - SL[0], SKY[1] - SL[1], SKY[2] - SL[2]), BD = [(SKY[0] - SL[0]) / BL, (SKY[1] - SL[1]) / BL, (SKY[2] - SL[2]) / BL];
675
+ // shifting registration: each fresh impression lands a couple of px off, then the printer's kento corrects it
676
+ for (let y = 0, p = 0; y < HH; y++) for (let x = 0; x < W; x++, p++) {
677
+ const i = p * 4, r = cd[i], g = cd[i + 1], b = cd[i + 2];
678
+ let pl;
679
+ if (kd[i + 3] > 110) pl = 0;
680
+ else if (Math.abs(r - PA[0]) + Math.abs(g - PA[1]) + Math.abs(b - PA[2]) < 24 || (r > 228 && g > 220 && b > 196)) pl = -1;
681
+ else if (g < r * 0.6 && r > b) pl = 4;
682
+ else if (r >= b) pl = 1;
683
+ else { const lum = (r * 0.3 + g * 0.55 + b * 0.15) / 255; pl = lum > 0.55 ? 2 : lum > 0.36 ? 3 : 5; }
684
+ let R, G, B;
685
+ // bokashi pixels lie on the sky->slate mixing line: print the sky plate, then wipe the gradation on top-down
686
+ const qr = SKY[0] - r, qg = SKY[1] - g, qb = SKY[2] - b, proj = qr * BD[0] + qg * BD[1] + qb * BD[2], tl = proj / BL;
687
+ const perp = Math.hypot(qr - BD[0] * proj, qg - BD[1] * proj, qb - BD[2] * proj);
688
+ if (pl !== 0 && y < 245 && tl > 0.03 && tl < 0.94 && perp < 16) {
689
+ const kb = H.smooth(0, 1, (t - 3.25 - y * 0.0018) / 0.45), ks = prog[1];
690
+ R = PA[0] + (SKY[0] - PA[0]) * ks + (r - SKY[0]) * kb; G = PA[1] + (SKY[1] - PA[1]) * ks + (g - SKY[1]) * kb; B = PA[2] + (SKY[2] - PA[2]) * ks + (b - SKY[2]) * kb;
691
+ } else {
692
+ const k = pl < 0 ? prog[0] : prog[pl];
693
+ R = PA[0] + (r - PA[0]) * k; G = PA[1] + (g - PA[1]) * k; B = PA[2] + (b - PA[2]) * k;
694
+ }
695
+ od[i] = R; od[i + 1] = G; od[i + 2] = B; od[i + 3] = 255;
696
+ }
697
+ c0.putImageData(out, 0, 0);
698
+ const c = c0;
699
+ // ---------- woodblock finish: wood grain in inked areas, fibres, ageing ----------
700
+ const img = c.getImageData(0, 0, W, HH), d = img.data;
701
+ const { warp, fine, pap, blot } = fields(W, HH);
702
+ const rg = H.rng(2);
703
+ for (let y = 0, p = 0; y < HH; y++) for (let x = 0; x < W; x++, p++) {
704
+ const i = p * 4, lum = (d[i] * 0.3 + d[i + 1] * 0.55 + d[i + 2] * 0.15) / 255;
705
+ const inked = H.clamp((0.85 - lum) * 1.6, 0, 1), plate = H.clamp((0.94 - lum) * 1.3, 0, 1);
706
+ const grain = Math.sin(y * 0.55 + warp[p] * 26 + x * 0.004) * 0.5 + fine[p] * 0.8;
707
+ let s = 1 + grain * (0.06 + 0.08 * inked) * plate; // wood grain shows in every printed block, the pale sky plate too
708
+ s *= 1 + pap[p] * 0.035 + (rg() - 0.5) * 0.04; // washi fibre + tooth
709
+ s *= 1 - Math.max(0, blot[p]) * 0.12 * plate; // uneven pigment take-up
710
+ // age: warm the paper, slightly fade blues
711
+ d[i] = d[i] * s * 1.0 + 6 * (1 - inked); d[i + 1] = d[i + 1] * s; d[i + 2] = d[i + 2] * s * 0.97 - 4 * (1 - inked);
712
+ }
713
+ c.putImageData(img, 0, 0);
714
+ // kozo fibres
715
+ const rf = H.rng(55);
716
+ for (let i = 0; i < 260; i++) {
717
+ const x = rf.range(0, W), y = rf.range(0, HH), l = rf.range(10, 40), a = rf.range(0, Math.PI * 2);
718
+ c.strokeStyle = rf() < 0.5 ? 'rgba(255,250,235,0.22)' : 'rgba(120,95,60,0.12)'; c.lineWidth = rf.range(0.5, 1.1);
719
+ c.beginPath(); c.moveTo(x, y); c.quadraticCurveTo(x + Math.cos(a) * l * 0.5 + rf.range(-6, 6), y + Math.sin(a) * l * 0.5 + rf.range(-6, 6), x + Math.cos(a) * l, y + Math.sin(a) * l); c.stroke();
720
+ }
721
+ H.vignette(c, W, HH, 0.25, '90,60,30');
722
+ },
723
+ });
724
+ }