@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,895 @@
1
+ {
2
+ // Vintage survey quadrangle: marching-squares contours, hillshade, water-lined lake, dashed trail, full collar.
3
+ const PAPER = '#EEE5CB', BROWN = '#A2643A', BLUE = '#3F7FA6', WATER = [168, 204, 214], WOOD = [200, 219, 168];
4
+ const MX0 = 36, MY0 = 30, MX1 = 1244, MY1 = 620; // map neatline
5
+ const G = 3; // grid spacing (px)
6
+ const CI = 40; // contour interval (m)
7
+
8
+ function makeTerrain() {
9
+ const n = H.makeNoise(31), n2 = H.makeNoise(77), n3 = H.makeNoise(5);
10
+ return function height(x, y) {
11
+ const wx = x + 80 * n.fbm2(x * 0.003, y * 0.003, 3);
12
+ const wy = y + 80 * n.fbm2(x * 0.003 + 5.2, y * 0.003 + 1.3, 3);
13
+ let h = 560 + 620 * n.fbm2(wx * 0.0024, wy * 0.0024, 5);
14
+ const rdg = 1 - Math.abs(n2.fbm2(wx * 0.0034, wy * 0.0034, 4));
15
+ h += 260 * (rdg - 0.6);
16
+ const g1 = Math.exp(-(((wx - 860) / 250) ** 2 + ((wy - 220) / 190) ** 2));
17
+ const g2 = Math.exp(-(((wx - 1120) / 150) ** 2 + ((wy - 450) / 140) ** 2));
18
+ h += 1300 * g1 * (0.45 + 0.75 * rdg) + 650 * g2 * (0.5 + 0.6 * rdg);
19
+ const b = Math.exp(-(((x - 330) / 230) ** 2 + ((y - 400) / 150) ** 2));
20
+ h -= 640 * b;
21
+ h += 18 * n3.fbm2(x * 0.03, y * 0.03, 2); // fine texture so contours crinkle
22
+ return h;
23
+ };
24
+ }
25
+
26
+ // marching squares over a grid; calls emit(level, x1,y1,x2,y2) for every crossing
27
+ function contours(v, gw, gh, ox, oy, levels, emit) {
28
+ const L0 = levels[0], step = levels[1] - levels[0], nL = levels.length;
29
+ for (let j = 0; j < gh - 1; j++) for (let i = 0; i < gw - 1; i++) {
30
+ const a = v[j * gw + i], b = v[j * gw + i + 1], c = v[(j + 1) * gw + i + 1], d = v[(j + 1) * gw + i];
31
+ const lo = Math.min(a, b, c, d), hi = Math.max(a, b, c, d);
32
+ let k0 = Math.max(0, Math.ceil((lo - L0) / step)), k1 = Math.min(nL - 1, Math.floor((hi - L0) / step));
33
+ const x = ox + i * G, y = oy + j * G;
34
+ for (let k = k0; k <= k1; k++) {
35
+ const L = levels[k];
36
+ const idx = (a > L ? 8 : 0) | (b > L ? 4 : 0) | (c > L ? 2 : 0) | (d > L ? 1 : 0);
37
+ if (idx === 0 || idx === 15) continue;
38
+ const T = () => [x + G * (L - a) / (b - a), y];
39
+ const R = () => [x + G, y + G * (L - b) / (c - b)];
40
+ const B = () => [x + G * (L - d) / (c - d), y + G];
41
+ const Lf = () => [x, y + G * (L - a) / (d - a)];
42
+ const seg = (p, q) => emit(k, p[0], p[1], q[0], q[1]);
43
+ switch (idx) {
44
+ case 1: case 14: seg(Lf(), B()); break;
45
+ case 2: case 13: seg(B(), R()); break;
46
+ case 3: case 12: seg(Lf(), R()); break;
47
+ case 4: case 11: seg(T(), R()); break;
48
+ case 6: case 9: seg(T(), B()); break;
49
+ case 7: case 8: seg(Lf(), T()); break;
50
+ case 5: seg(Lf(), T()); seg(B(), R()); break;
51
+ case 10: seg(T(), R()); seg(Lf(), B()); break;
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ function spacedText(ctx, str, x, y, track, align = 'left') {
58
+ let w = 0; for (const ch of str) w += ctx.measureText(ch).width + track; w -= track;
59
+ let cx = align === 'center' ? x - w / 2 : align === 'right' ? x - w : x;
60
+ const sa = ctx.textAlign; ctx.textAlign = 'left';
61
+ for (const ch of str) { ctx.fillText(ch, cx, y); cx += ctx.measureText(ch).width + track; }
62
+ ctx.textAlign = sa; return w;
63
+ }
64
+
65
+ // Everything that does not depend on t is computed once and memoised (output stays a pure function of t).
66
+ let CACHE = null;
67
+ function buildStatic(W, H_) {
68
+ const height = makeTerrain();
69
+ const paperL = H.layer(W, H_);
70
+ H.paper(paperL.getContext('2d'), W, H_, PAPER, 21, 0.03);
71
+
72
+ // ---- heightfield ----
73
+ const gw = Math.ceil((MX1 - MX0) / G) + 2, gh = Math.ceil((MY1 - MY0) / G) + 2;
74
+ const hv = new Float32Array(gw * gh);
75
+ let hmax = -1e9, pk = [0, 0];
76
+ for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) {
77
+ const x = MX0 + i * G, y = MY0 + j * G, h = height(x, y);
78
+ hv[j * gw + i] = h;
79
+ if (h > hmax && x > MX0 + 60 && x < MX1 - 60 && y > MY0 + 60) { hmax = h; pk = [x, y]; }
80
+ }
81
+ const WL = height(330, 400) + 170; // lake level
82
+ const hAt = (x, y) => {
83
+ const fx = H.clamp((x - MX0) / G, 0, gw - 1.001), fy = H.clamp((y - MY0) / G, 0, gh - 1.001);
84
+ const i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j, k = j * gw + i;
85
+ return hv[k] * (1 - u) * (1 - v) + hv[k + 1] * u * (1 - v) + hv[k + gw] * (1 - u) * v + hv[k + gw + 1] * u * v;
86
+ };
87
+ const grad = (x, y) => [(hAt(x + 2, y) - hAt(x - 2, y)) / 4, (hAt(x, y + 2) - hAt(x, y - 2)) / 4];
88
+
89
+ // ---- plates at full res: water (blue), woodland (green), hillshade (grey) ----
90
+ const mw = MX1 - MX0, mh = MY1 - MY0;
91
+ const waterL = H.layer(mw, mh), woodL = H.layer(mw, mh), shadeL = H.layer(mw, mh);
92
+ const wi = waterL.getContext('2d').createImageData(mw, mh), oi = woodL.getContext('2d').createImageData(mw, mh), si = shadeL.getContext('2d').createImageData(mw, mh);
93
+ const sh = new Float32Array(gw * gh);
94
+ const Z = 0.16, lx = -0.55, ly = -0.6, lz = 0.58;
95
+ for (let j = 1; j < gh - 1; j++) for (let i = 1; i < gw - 1; i++) {
96
+ const k = j * gw + i;
97
+ const dx = (hv[k + 1] - hv[k - 1]) / (2 * G) * Z, dy = (hv[k + gw] - hv[k - gw]) / (2 * G) * Z;
98
+ sh[k] = (-dx * lx - dy * ly + lz) / Math.hypot(dx, dy, 1);
99
+ }
100
+ const woodN = H.makeNoise(909);
101
+ const wood = new Float32Array(gw * gh);
102
+ for (let j = 0; j < gh; j++) for (let i = 0; i < gw; i++) wood[j * gw + i] = woodN.fbm2(i * G * 0.0065, j * G * 0.0065, 4);
103
+ for (let py = 0; py < mh; py++) {
104
+ const fy = py / G, j = Math.min(gh - 2, Math.floor(fy)), v = fy - j;
105
+ for (let px = 0; px < mw; px++) {
106
+ const fx = px / G, i = Math.min(gw - 2, Math.floor(fx)), u = fx - i, k = j * gw + i;
107
+ const w00 = (1 - u) * (1 - v), w10 = u * (1 - v), w01 = (1 - u) * v, w11 = u * v;
108
+ const h = hv[k] * w00 + hv[k + 1] * w10 + hv[k + gw] * w01 + hv[k + gw + 1] * w11;
109
+ const s = sh[k] * w00 + sh[k + 1] * w10 + sh[k + gw] * w01 + sh[k + gw + 1] * w11;
110
+ const wd = wood[k] * w00 + wood[k + 1] * w10 + wood[k + gw] * w01 + wood[k + gw + 1] * w11;
111
+ const o = (py * mw + px) * 4;
112
+ if (h < WL) {
113
+ wi.data[o] = WATER[0]; wi.data[o + 1] = WATER[1]; wi.data[o + 2] = WATER[2]; wi.data[o + 3] = 235;
114
+ } else {
115
+ if (wd > 0.04 && h < 1350) { oi.data[o] = WOOD[0]; oi.data[o + 1] = WOOD[1]; oi.data[o + 2] = WOOD[2]; oi.data[o + 3] = 175; }
116
+ const amt = H.clamp((lz - s) * 1.25, -0.25, 0.7);
117
+ if (amt > 0) { si.data[o] = 255 - 100 * amt; si.data[o + 1] = 255 - 115 * amt; si.data[o + 2] = 255 - 128 * amt; si.data[o + 3] = 255; }
118
+ else { si.data[o] = 255; si.data[o + 1] = 255; si.data[o + 2] = 255; si.data[o + 3] = 255; }
119
+ }
120
+ }
121
+ }
122
+ waterL.getContext('2d').putImageData(wi, 0, 0);
123
+ woodL.getContext('2d').putImageData(oi, 0, 0);
124
+ shadeL.getContext('2d').putImageData(si, 0, 0);
125
+
126
+ // ---- contours ----
127
+ const levels = []; for (let L = Math.ceil(WL / CI) * CI; L < hmax + CI; L += CI) levels.push(L);
128
+ const segs = levels.map(() => []);
129
+ contours(hv, gw, gh, MX0, MY0, levels, (k, x1, y1, x2, y2) => segs[k].push(x1, y1, x2, y2));
130
+
131
+ // ---- trail: switchback descent from near the summit to the lake ----
132
+ const trail = [];
133
+ {
134
+ let x = pk[0] - 26, y = pk[1] + 14, dir = [-0.7, 0.7], side = 1, run = 0;
135
+ const tr = H.rng(17);
136
+ for (let s = 0; s < 1600; s++) {
137
+ const g = grad(x, y), gm = Math.hypot(g[0], g[1]) || 1e-6;
138
+ let dx = -g[0] / gm, dy = -g[1] / gm;
139
+ const steep = H.smooth(1.2, 4.5, gm);
140
+ const th = side * (1.15 * steep + 0.25);
141
+ let rx = dx * Math.cos(th) - dy * Math.sin(th), ry = dx * Math.sin(th) + dy * Math.cos(th);
142
+ const lxv = 330 - x, lyv = 400 - y, lm = Math.hypot(lxv, lyv) || 1;
143
+ rx += 0.45 * lxv / lm; ry += 0.45 * lyv / lm;
144
+ const rm = Math.hypot(rx, ry); rx /= rm; ry /= rm;
145
+ dir = [dir[0] * 0.8 + rx * 0.2, dir[1] * 0.8 + ry * 0.2];
146
+ const dm = Math.hypot(dir[0], dir[1]); dir = [dir[0] / dm, dir[1] / dm];
147
+ x += dir[0] * 2; y += dir[1] * 2; run += 2;
148
+ if (run > 46 + tr() * 40 * (1 - steep) + 30 * (1 - steep)) { side = -side; run = 0; }
149
+ trail.push(x, y);
150
+ if (hAt(x, y) < WL + 2) break;
151
+ }
152
+ }
153
+ const tm = Math.floor(trail.length / 4) * 2;
154
+ const tlx = trail[tm], tly = trail[tm + 1];
155
+
156
+ // ---- label placement on index contours (uphill-reading numerals) ----
157
+ const isIndex = (L) => L % 200 === 0;
158
+ const rng = H.rng(4242);
159
+ const boxes = [];
160
+ const reserve = [[pk[0], pk[1] - 4, 210, 60], [330, 402, 230, 50], [tlx + 80, tly - 10, 160, 24]];
161
+ const freeOf = (x, y, rad) => boxes.every((b) => Math.hypot(b.x - x, b.y - y) > rad) && reserve.every(([rx, ry, rw, rh]) => Math.abs(x - rx) > rw / 2 + 12 || Math.abs(y - ry) > rh / 2 + 10);
162
+ const labels = [];
163
+ levels.forEach((L, k) => {
164
+ if (!isIndex(L)) return;
165
+ const s = segs[k]; const nSeg = s.length / 4; if (!nSeg) return;
166
+ let placed = 0;
167
+ for (let tries = 0; tries < 140 && placed < 3; tries++) {
168
+ const q = rng.int(0, nSeg - 1) * 4;
169
+ const x = (s[q] + s[q + 2]) / 2, y = (s[q + 1] + s[q + 3]) / 2;
170
+ if (x < MX0 + 40 || x > MX1 - 40 || y < MY0 + 30 || y > MY1 - 30) continue;
171
+ const g = grad(x, y), gm = Math.hypot(g[0], g[1]); if (gm < 0.6 || gm > 9) continue;
172
+ const tx = -g[1] / gm, ty = g[0] / gm;
173
+ const g2a = grad(x + tx * 14, y + ty * 14), g2b = grad(x - tx * 14, y - ty * 14);
174
+ const dA = (g2a[0] * g[0] + g2a[1] * g[1]) / (Math.hypot(...g2a) * gm), dB = (g2b[0] * g[0] + g2b[1] * g[1]) / (Math.hypot(...g2b) * gm);
175
+ if (dA < 0.93 || dB < 0.93) continue;
176
+ if (!freeOf(x, y, 110)) continue;
177
+ let ang = Math.atan2(ty, tx);
178
+ if (Math.sin(ang) * g[0] - Math.cos(ang) * g[1] < 0) ang += Math.PI; // read uphill
179
+ const txt = String(L);
180
+ labels.push({ x, y, ang, txt, L });
181
+ boxes.push({ x, y, w: txt.length * 6.2 + 6, h: 11, ang });
182
+ placed++;
183
+ }
184
+ });
185
+
186
+ // ---- water lining + shoreline ----
187
+ const wlv = [WL - 3, WL - 9, WL - 17, WL - 28, WL - 42, WL - 60];
188
+ const ws = wlv.map(() => []);
189
+ wlv.forEach((L, k) => contours(hv, gw, gh, MX0, MY0, [L, L + 1000], (kk, a, b, c, d) => { if (kk === 0) ws[k].push(a, b, c, d); }));
190
+ const shore = [];
191
+ contours(hv, gw, gh, MX0, MY0, [WL, WL + 1000], (kk, a, b, c, d) => { if (kk === 0) shore.push(a, b, c, d); });
192
+
193
+ // ---- streams: steepest descent from springs that reach the lake ----
194
+ const streams = [];
195
+ {
196
+ const gradS = (x, y) => [(hAt(x + 7, y) - hAt(x - 7, y)) / 14, (hAt(x, y + 7) - hAt(x, y - 7)) / 14];
197
+ const sr = H.rng(303);
198
+ for (let tries = 0; tries < 160 && streams.length < 4; tries++) {
199
+ let x = sr.range(MX0 + 30, MX1 - 30), y = sr.range(MY0 + 30, MY1 - 30);
200
+ const h0 = hAt(x, y); if (h0 < WL + 260 || h0 > hmax - 250) continue;
201
+ if (streams.some((f) => Math.hypot(f[0][0] - x, f[0][1] - y) < 220)) continue;
202
+ let d = [0, 0]; const pts = [[x, y]]; let ok = false;
203
+ for (let st = 0; st < 1400; st++) {
204
+ const g = gradS(x, y), gm = Math.hypot(g[0], g[1]) || 1e-6;
205
+ d = [d[0] * 0.75 - g[0] / gm * 0.25, d[1] * 0.75 - g[1] / gm * 0.25];
206
+ const dm = Math.hypot(d[0], d[1]) || 1; x += d[0] / dm * 2; y += d[1] / dm * 2;
207
+ pts.push([x, y]);
208
+ if (hAt(x, y) < WL) { ok = true; break; }
209
+ if (x < MX0 || x > MX1 || y < MY0 || y > MY1) break;
210
+ }
211
+ if (ok && pts.length > 90) streams.push(pts);
212
+ }
213
+ }
214
+ const spots = [[1100, 470], [560, 230], [960, 560], [150, 150], [720, 470]].map(([x, y]) => [x, y, hAt(x, y)]).filter((s) => s[2] >= WL);
215
+ return { paperL, waterL, woodL, shadeL, hmax, pk, WL, levels, segs, isIndex, trail, tlx, tly, labels, boxes, ws, shore, streams, spots, mw, mh };
216
+ }
217
+
218
+ registerStyle({
219
+ id: 'topographic',
220
+ name: 'Topographic Survey',
221
+ category: 'Data & Information',
222
+ description: 'A vintage survey quadrangle of a "loss landscape": brown contour lines with heavier labelled index contours, soft hillshade and woodland tint, a water-lined lake, a red dashed switchback trail and a full printed collar with scale bar and north arrow.',
223
+ motion: 'Printed plate by plate: the black frame and collar, then the grey hillshade, the green woodland sliding into register, the blue lake, then brown contours rising from the shore like a tide; streams trace downhill, names land, and the red trail walks itself down the switchbacks while the camera eases in.',
224
+ duration: 7,
225
+ fonts: ['400 10px "Inter"', '600 10px "Inter"', '700 13px "Libre Baskerville"', 'italic 400 14px "Libre Baskerville"',
226
+ 'italic 400 11px "Libre Baskerville"', '700 22px "Libre Baskerville"', '400 10px "Libre Baskerville"', '400 9px "Inter"', 'italic 700 24px "Libre Baskerville"', '700 66px "Libre Baskerville"', '700 28px "Libre Baskerville"', 'italic 400 28px "Libre Baskerville"', '700 64px "Libre Baskerville"'],
227
+ frameT: 6.9,
228
+ readsPixels: true,
229
+ draw(ctx, W, H_, t) {
230
+ const C = CACHE || (CACHE = buildStatic(W, H_));
231
+ const { pk, WL, hmax, mw, mh, trail, tlx, tly } = C;
232
+ const seg = (a, b) => H.clamp((t - a) / (b - a));
233
+ const INKC = '#1E1A16';
234
+
235
+ // slow camera ease-in toward the trail across the whole shot
236
+ ctx.save();
237
+ const push = 1 + 0.02 * H.ease.inOut(seg(0.8, 7));
238
+ const fxp = 640, fyp = 360;
239
+ ctx.translate(fxp, fyp); ctx.scale(push, push); ctx.translate(-fxp, -fyp);
240
+
241
+ ctx.drawImage(C.paperL, 0, 0);
242
+
243
+ // ---- grey plate: hillshade ----
244
+ const gA = seg(0.3, 1.0);
245
+ if (gA > 0) { ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.85 * gA; ctx.drawImage(C.shadeL, MX0, MY0); ctx.restore(); }
246
+ // ---- green plate: woodland slides into registration ----
247
+ const wA = seg(0.6, 1.3);
248
+ if (wA > 0) { const off = (1 - H.ease.out(wA)) * 7; ctx.save(); ctx.globalAlpha = wA; ctx.drawImage(C.woodL, MX0 + off, MY0 - off * 0.6); ctx.restore(); }
249
+ // ---- blue plate: lake ----
250
+ const bA = seg(0.9, 1.5);
251
+ if (bA > 0) { ctx.save(); ctx.globalAlpha = bA; ctx.drawImage(C.waterL, MX0, MY0); ctx.restore(); }
252
+
253
+ // ---- red section grid ----
254
+ const rA = seg(2.4, 3.0);
255
+ if (rA > 0) {
256
+ ctx.save(); ctx.globalAlpha = rA;
257
+ ctx.strokeStyle = 'rgba(190,70,50,0.28)'; ctx.lineWidth = 0.8; ctx.setLineDash([10, 4]);
258
+ for (let x = MX0 + 201; x < MX1; x += 201) { ctx.beginPath(); ctx.moveTo(x, MY0); ctx.lineTo(x, MY1); ctx.stroke(); }
259
+ for (let y = MY0 + 196; y < MY1; y += 196) { ctx.beginPath(); ctx.moveTo(MX0, y); ctx.lineTo(MX1, y); ctx.stroke(); }
260
+ ctx.setLineDash([]);
261
+ ctx.fillStyle = 'rgba(180,60,45,0.55)'; ctx.font = '600 10px "Inter"'; ctx.textAlign = 'center';
262
+ let sec = 1;
263
+ for (let r = 0; r < 3; r++) for (let c = 0; c < 6; c++) { if ((r * 6 + c) % 2 === 0) ctx.fillText(String(sec), MX0 + 100 + c * 201, MY0 + 30 + r * 196); sec++; }
264
+ ctx.restore();
265
+ }
266
+
267
+ // ---- brown plate: contours rise from the shore upward ----
268
+ const rise = seg(1.2, 3.4);
269
+ const visibleL = rise <= 0 ? -1e9 : WL + (hmax - WL + CI) * H.ease.inOut(rise);
270
+ ctx.save();
271
+ ctx.beginPath(); ctx.rect(MX0, MY0, mw, mh);
272
+ for (const b of C.boxes) {
273
+ const c = Math.cos(b.ang), s = Math.sin(b.ang), hw = b.w / 2, hh = b.h / 2;
274
+ const P = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([u, v]) => [b.x + u * c - v * s, b.y + u * s + v * c]);
275
+ ctx.moveTo(...P[0]); P.slice(1).forEach((p) => ctx.lineTo(...p)); ctx.closePath();
276
+ }
277
+ ctx.clip('evenodd');
278
+ ctx.lineCap = 'round'; ctx.lineJoin = 'round';
279
+ C.levels.forEach((L, k) => {
280
+ if (L > visibleL) return;
281
+ const s = C.segs[k]; if (!s.length) return;
282
+ const idx = C.isIndex(L);
283
+ // the newest ring inks in over a short band
284
+ const fresh = H.clamp((visibleL - L) / (CI * 2.5));
285
+ ctx.strokeStyle = idx ? `rgba(150,88,48,${0.95 * (0.35 + 0.65 * fresh)})` : `rgba(162,100,58,${0.72 * (0.35 + 0.65 * fresh)})`;
286
+ ctx.lineWidth = idx ? 1.45 : 0.65;
287
+ ctx.beginPath();
288
+ for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); }
289
+ ctx.stroke();
290
+ });
291
+ ctx.restore();
292
+
293
+ // ---- blue linework: shoreline and water lining ripple outward from the shore ----
294
+ if (bA > 0) {
295
+ ctx.lineCap = 'round';
296
+ ctx.strokeStyle = `rgba(52,108,146,${0.95 * bA})`; ctx.lineWidth = 1.3; ctx.beginPath();
297
+ for (let q = 0; q < C.shore.length; q += 4) { ctx.moveTo(C.shore[q], C.shore[q + 1]); ctx.lineTo(C.shore[q + 2], C.shore[q + 3]); } ctx.stroke();
298
+ C.ws.forEach((s, k) => {
299
+ const a = seg(1.2 + k * 0.12, 1.5 + k * 0.12); if (a <= 0) return;
300
+ ctx.strokeStyle = `rgba(63,127,166,${(0.75 - k * 0.1) * a})`; ctx.lineWidth = 0.6; ctx.beginPath();
301
+ for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); } ctx.stroke();
302
+ });
303
+ }
304
+
305
+ // ---- streams trace downhill from their springs ----
306
+ const sP = seg(2.6, 3.8);
307
+ if (sP > 0) {
308
+ ctx.strokeStyle = 'rgba(52,108,146,0.9)'; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
309
+ C.streams.forEach((pts, si) => {
310
+ const n = Math.floor(pts.length * H.clamp(sP * 1.15 - si * 0.05));
311
+ for (let q = 1; q < n; q++) {
312
+ const f = q / pts.length;
313
+ ctx.lineWidth = 0.55 + 1.0 * f;
314
+ if (f < 0.22 && (q >> 2) % 3 === 0) continue; // intermittent near the spring
315
+ ctx.beginPath(); ctx.moveTo(...pts[q - 1]); ctx.lineTo(...pts[q]); ctx.stroke();
316
+ }
317
+ });
318
+ }
319
+
320
+ // ---- trail walks down from the summit ----
321
+ const tP = H.ease.inOut(seg(3.5, 5.0));
322
+ const nT = Math.floor((trail.length / 2) * tP);
323
+ if (nT > 2) {
324
+ ctx.save();
325
+ ctx.strokeStyle = '#B5352A'; ctx.lineWidth = 1.7; ctx.setLineDash([6, 4]); ctx.lineCap = 'butt'; ctx.lineJoin = 'round';
326
+ ctx.beginPath(); ctx.moveTo(trail[0], trail[1]);
327
+ for (let q = 2; q < nT * 2; q += 2) ctx.lineTo(trail[q], trail[q + 1]);
328
+ ctx.stroke(); ctx.restore();
329
+ ctx.fillStyle = '#B5352A'; ctx.beginPath(); ctx.arc(trail[0], trail[1], 2.6, 0, 7); ctx.fill();
330
+ // walker: leads the drawing, then keeps hiking the route on a loop
331
+ const hikeS = t < 5.0 ? nT - 1 : Math.floor(((t - 5.0) * 0.18 % 1) * (trail.length / 2 - 1));
332
+ const hx = trail[hikeS * 2], hy = trail[hikeS * 2 + 1];
333
+ ctx.fillStyle = 'rgba(238,229,203,0.9)'; ctx.beginPath(); ctx.arc(hx, hy, 4.4, 0, 7); ctx.fill();
334
+ ctx.fillStyle = '#B5352A'; ctx.beginPath(); ctx.arc(hx, hy, 3, 0, 7); ctx.fill();
335
+ }
336
+
337
+ // ---- contour numbers ink with their level ----
338
+ ctx.font = '600 10px "Inter"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
339
+ C.labels.forEach((lb) => {
340
+ const a = H.clamp((visibleL - lb.L) / (CI * 2)); if (a <= 0) return;
341
+ ctx.fillStyle = `rgba(140,80,42,${a})`;
342
+ ctx.save(); ctx.translate(lb.x, lb.y); ctx.rotate(lb.ang); ctx.fillText(lb.txt, 0, 0.5); ctx.restore();
343
+ });
344
+
345
+ // ---- black plate, part 2: names & symbols land after the relief ----
346
+ ctx.textBaseline = 'alphabetic';
347
+ const nA = seg(3.2, 3.7);
348
+ if (nA > 0) {
349
+ ctx.save(); ctx.globalAlpha = nA;
350
+ ctx.fillStyle = INKC; ctx.strokeStyle = INKC; ctx.lineWidth = 1;
351
+ ctx.beginPath(); ctx.moveTo(pk[0], pk[1] - 5); ctx.lineTo(pk[0] + 5, pk[1] + 4); ctx.lineTo(pk[0] - 5, pk[1] + 4); ctx.closePath(); ctx.stroke();
352
+ ctx.beginPath(); ctx.arc(pk[0], pk[1] + 1, 1, 0, 7); ctx.fill();
353
+ ctx.font = '700 13px "Libre Baskerville"';
354
+ const str = 'MOUNT COMPUTE';
355
+ ctx.save(); ctx.strokeStyle = 'rgba(238,229,203,0.8)'; ctx.lineWidth = 3;
356
+ let w = 0; for (const ch of str) w += ctx.measureText(ch).width + 3; w -= 3;
357
+ let cx = pk[0] - w / 2; const y0 = pk[1] - 14;
358
+ for (const ch of str) { ctx.strokeText(ch, cx, y0); cx += ctx.measureText(ch).width + 3; }
359
+ ctx.restore();
360
+ ctx.fillStyle = INKC; spacedText(ctx, str, pk[0], pk[1] - 14, 3, 'center');
361
+ ctx.font = '400 10px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillText(String(Math.round(hmax)), pk[0] + 9, pk[1] + 9);
362
+ ctx.font = '400 10px "Libre Baskerville"'; ctx.fillStyle = INKC; ctx.textAlign = 'left';
363
+ C.spots.forEach(([x, y, h]) => { ctx.fillText('×', x - 3, y + 3); ctx.fillText(String(Math.round(h)), x + 6, y + 4); });
364
+ ctx.restore();
365
+ }
366
+ const lA = seg(1.6, 2.1);
367
+ if (lA > 0) {
368
+ ctx.save(); ctx.globalAlpha = lA;
369
+ ctx.font = 'italic 400 14px "Libre Baskerville"'; ctx.fillStyle = '#2C6D96'; ctx.textAlign = 'center';
370
+ spacedText(ctx, 'Local Minimum Lake', 330, 405, 1.5, 'center');
371
+ ctx.font = 'italic 400 11px "Libre Baskerville"';
372
+ ctx.fillText('Elev ' + Math.round(WL), 330, 422);
373
+ ctx.restore();
374
+ }
375
+ const trA = seg(4.4, 4.9);
376
+ if (trA > 0 && tlx) {
377
+ ctx.save(); ctx.globalAlpha = trA; ctx.font = 'italic 400 11px "Libre Baskerville"'; ctx.fillStyle = '#8E2A22'; ctx.textAlign = 'left';
378
+ ctx.strokeStyle = 'rgba(238,229,203,0.85)'; ctx.lineWidth = 3;
379
+ ctx.strokeText('Gradient Descent Trail', tlx + 10, tly - 6); ctx.fillText('Gradient Descent Trail', tlx + 10, tly - 6);
380
+ ctx.restore();
381
+ }
382
+
383
+ // ---- black plate, part 1: neatline, graticule and collar (printed first) ----
384
+ const kA = seg(0, 0.6);
385
+ ctx.save(); ctx.globalAlpha = kA;
386
+ ctx.strokeStyle = INKC; ctx.lineWidth = 1.2; ctx.strokeRect(MX0 + 0.5, MY0 + 0.5, mw, mh);
387
+ ctx.lineWidth = 0.6; ctx.strokeRect(MX0 - 3.5, MY0 - 3.5, mw + 8, mh + 8);
388
+ ctx.font = '400 9px "Inter"'; ctx.fillStyle = INKC; ctx.lineWidth = 1;
389
+ for (let k = 1; k < 4; k++) {
390
+ const x = MX0 + mw * k / 4;
391
+ ctx.beginPath(); ctx.moveTo(x, MY0); ctx.lineTo(x, MY0 + 8); ctx.moveTo(x, MY1); ctx.lineTo(x, MY1 - 8); ctx.stroke();
392
+ ctx.textAlign = 'center'; ctx.fillText(['122°20′', '122°17′30″', '122°15′'][k - 1], x, MY0 - 7);
393
+ }
394
+ for (let k = 1; k < 3; k++) {
395
+ const y = MY0 + mh * k / 3;
396
+ ctx.beginPath(); ctx.moveTo(MX0, y); ctx.lineTo(MX0 + 8, y); ctx.moveTo(MX1, y); ctx.lineTo(MX1 - 8, y); ctx.stroke();
397
+ }
398
+ ctx.textAlign = 'left'; ctx.fillText('37°52′30″ 122°22′30″', MX0, MY0 - 7);
399
+ ctx.textAlign = 'right'; ctx.fillText('122°12′30″', MX1, MY0 - 7);
400
+ ctx.font = '400 9px "Inter"'; ctx.textAlign = 'left';
401
+ spacedText(ctx, 'THE MODEL SURVEY', 489, 16, 2, 'center');
402
+ spacedText(ctx, 'TRAINING SERIES', 1093, 16, 2, 'center');
403
+ ctx.restore();
404
+
405
+ // collar text sets in shortly after the frame
406
+ const cA = seg(0.25, 0.9);
407
+ const cy0 = MY1 + 10;
408
+ ctx.save(); ctx.globalAlpha = cA;
409
+ ctx.textAlign = 'left'; ctx.fillStyle = INKC; ctx.font = '400 9px "Inter"';
410
+ ctx.fillText('Mapped, edited and published by the Model Survey', MX0, cy0 + 18);
411
+ ctx.fillText('Topography compiled from imagined stereo pairs, 2026', MX0, cy0 + 31);
412
+ ctx.fillText('Red tint: township and section lines', MX0, cy0 + 44);
413
+ ctx.fillText('Illustrative map. Not for navigation', MX0, cy0 + 57);
414
+ {
415
+ const ax = 380, ay = cy0 + 62;
416
+ ctx.strokeStyle = INKC; ctx.lineWidth = 1;
417
+ ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax, ay - 50); ctx.stroke();
418
+ ctx.save(); ctx.translate(ax, ay - 55); ctx.beginPath();
419
+ for (let q = 0; q < 10; q++) { const r = q % 2 ? 2.2 : 5.5, a = -Math.PI / 2 + q * Math.PI / 5; ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); }
420
+ ctx.closePath(); ctx.fill(); ctx.restore();
421
+ const mxp = ax + Math.sin(0.24) * 46, myp = ay - Math.cos(0.24) * 46;
422
+ ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(mxp, myp); ctx.stroke();
423
+ ctx.beginPath(); ctx.moveTo(mxp, myp); ctx.lineTo(mxp - 1.5, myp + 10); ctx.lineTo(mxp + 3, myp + 9); ctx.closePath(); ctx.fill();
424
+ ctx.font = '400 9px "Inter"'; ctx.textAlign = 'left';
425
+ ctx.fillText('MN', mxp + 4, myp + 4); ctx.fillText('14°', ax + 5, ay - 6);
426
+ }
427
+ {
428
+ const sx = 520, sy = cy0 + 36, unit = 60;
429
+ ctx.textAlign = 'center'; ctx.fillStyle = INKC;
430
+ ctx.font = '700 13px "Libre Baskerville"'; spacedText(ctx, 'SCALE 1:24 000', sx + unit * 1.5, cy0 + 16, 1.5, 'center');
431
+ ctx.lineWidth = 0.8; ctx.strokeStyle = INKC;
432
+ for (let k = 0; k < 4; k++) { ctx.fillStyle = k % 2 ? INKC : PAPER; ctx.fillRect(sx - unit + k * unit / 4, sy, unit / 4, 5); }
433
+ for (let k = 0; k < 3; k++) { ctx.fillStyle = k % 2 ? PAPER : INKC; ctx.fillRect(sx + k * unit, sy, unit, 5); }
434
+ ctx.strokeRect(sx - unit, sy, unit * 4, 5);
435
+ ctx.fillStyle = INKC; ctx.font = '400 9px "Inter"';
436
+ ['1', '0', '1', '2', '3'].forEach((s, k) => ctx.fillText(s, sx - unit + k * unit, sy + 16));
437
+ ctx.fillText('KILOMETERS', sx + unit * 3 + 34, sy + 5);
438
+ ctx.font = '400 10px "Libre Baskerville"';
439
+ spacedText(ctx, 'CONTOUR INTERVAL 40 METERS', sx + unit * 1, sy + 34, 1.2, 'center');
440
+ }
441
+ ctx.textAlign = 'right'; ctx.fillStyle = INKC;
442
+ ctx.font = '700 22px "Libre Baskerville"'; spacedText(ctx, 'MOUNT COMPUTE', MX1, cy0 + 30, 3, 'right');
443
+ ctx.font = '400 10px "Libre Baskerville"';
444
+ spacedText(ctx, 'LOSS LANDSCAPE QUADRANGLE', MX1, cy0 + 47, 1.6, 'right');
445
+ ctx.font = '400 9px "Inter"';
446
+ spacedText(ctx, '7.5-MINUTE SERIES (TOPOGRAPHIC) · 2026', MX1, cy0 + 62, 0.8, 'right');
447
+ ctx.restore();
448
+
449
+ // ---- age: fold crease, foxing ----
450
+ ctx.save();
451
+ const fx = 640;
452
+ const gfold = ctx.createLinearGradient(fx - 10, 0, fx + 10, 0);
453
+ gfold.addColorStop(0, 'rgba(255,255,255,0)'); gfold.addColorStop(0.45, 'rgba(255,255,255,0.18)');
454
+ gfold.addColorStop(0.55, 'rgba(90,70,40,0.10)'); gfold.addColorStop(1, 'rgba(90,70,40,0)');
455
+ ctx.fillStyle = gfold; ctx.fillRect(fx - 10, -20, 20, H_ + 40);
456
+ ctx.globalCompositeOperation = 'multiply';
457
+ const fr = H.rng(77);
458
+ for (let k = 0; k < 7; k++) {
459
+ const x = fr.range(0, W), y = fr.range(0, H_), r = fr.range(30, 140);
460
+ const gg = ctx.createRadialGradient(x, y, 0, x, y, r);
461
+ gg.addColorStop(0, 'rgba(180,150,100,0.10)'); gg.addColorStop(0.8, 'rgba(180,150,100,0.04)'); gg.addColorStop(1, 'rgba(180,150,100,0)');
462
+ ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
463
+ }
464
+ ctx.restore();
465
+ ctx.restore(); // camera
466
+ H.vignette(ctx, W, H_, 0.18, '80,60,30');
467
+ H.grain(ctx, W, H_, 0.035, 31 + Math.floor(t * 12));
468
+ },
469
+ // Sound: a surveyor's sheet out in the open. A light wind under everything, a draughting pen ticks each
470
+ // survey × onto the sheet, the terrain rises as a slow low swell, the lake laps in, and the hiker's boots
471
+ // crunch gravel step by step down the trail (steps follow the eased hike). Times mirror explainer() below.
472
+ explainerSound(k) {
473
+ const D = 15, io = H.ease.inOut;
474
+ const seg = (t, a, b) => H.clamp((t - a) / (b - a));
475
+ const when = (t0, t1, ease, u) => { for (let i = 0; i <= 400; i++) { const t = t0 + ((t1 - t0) * i) / 400; if (ease(i / 400) >= u - 1e-6) return t; } return t1; };
476
+ const pan = (px) => H.clamp((px / 1280) * 2 - 1, -1, 1) * 0.6;
477
+ // draughting pen on the sheet
478
+ const pen = (a, b, g = 1, p = 0, minor = true) => { k.noise(a, { dur: 0.016, filter: 'bandpass', f: 1500, q: 1.3, gain: 0.12 * g, a: 0.001, pan: p, minor }); k.play('stroke', a, { material: 'pen', dur: b - a, gain: 2.0 * g, pan: p, minor }); };
479
+ const nib = (t, g, p) => k.withMinor(() => { k.noise(t, { dur: 0.018, filter: 'bandpass', f: 2200, q: 1.5, gain: 0.16 * g, a: 0.001, pan: p }); k.noise(t + 0.03, { dur: 0.016, filter: 'bandpass', f: 1900, q: 1.5, gain: 0.12 * g, a: 0.001, pan: p }); });
480
+ // a boot on gravel
481
+ const step = (t, g = 1, p = 0) => k.withMinor(() => {
482
+ k.noise(t, { dur: 0.08, color: 'pink', filter: 'bandpass', f: 850, q: 0.7, gain: 0.22 * g, a: 0.004, pan: p });
483
+ k.noise(t + 0.012, { dur: 0.06, filter: 'bandpass', f: 1800, q: 1, gain: 0.07 * g, a: 0.003, pan: p });
484
+ k.tone(t, { f: [[0, 95], [0.05, 55]], dur: 0.08, gain: 0.12 * g, pan: p });
485
+ });
486
+ // a soft wooden note for type landing in the collar / cartouches
487
+ const wood = (t, f, g = 1, p = 0, minor = true) => { k.tone(t, { f, dur: 0.5, gain: 0.07 * g, a: 0.006, pan: p, minor }); k.tone(t, { f: k.hz(f) * 4, dur: 0.08, gain: 0.012 * g, a: 0.002, pan: p, minor }); };
488
+ const tick = (t, g, p) => k.withMinor(() => { k.noise(t, { dur: 0.016, filter: 'bandpass', f: 1700, q: 1.8, gain: 0.18 * g, a: 0.001, pan: p }); k.tone(t, { f: 200, dur: 0.025, gain: 0.04 * g, pan: p }); });
489
+
490
+ k.bed('room', 0, D, { gain: 0.25 });
491
+ // light open-air wind, slow gusts
492
+ k.withMinor(() => { for (let t = 0, i = 0; t < D; t += 1.2, i++) k.noise(t, { dur: 2.2, color: 'pink', filter: 'bandpass', f: 380 + 120 * Math.sin(i * 1.7), q: 0.6, gain: 0.035 + 0.015 * Math.sin(i * 2.3), a: 1.0, pan: Math.sin(i * 0.9) * 0.4 }); });
493
+
494
+ // ---- the question in a cartouche, its subtitle, then it docks into the collar (1.5 -> 2.4)
495
+ wood(0.05, 'D3', 1.2, 0, false);
496
+ wood(0.5, 'A3', 0.7);
497
+ k.noise(1.5, { dur: 0.9, color: 'brown', filter: 'bandpass', f: [[0, 260], [0.9, 380]], q: 0.6, gain: 0.08, a: 0.35, pan: 0.4, minor: true });
498
+
499
+ // ---- data: survey points plotted (accelerating), terrain rises, woods, lake
500
+ wood(2.05, 'G3', 0.7, -0.6); // EXAMPLES SURVEYED box
501
+ { // one pen × per new point, throttled to 12/s; shown = 260 * ease.in(seg(2.0, 5.4))^0.7
502
+ let last = 0, lastT = -1;
503
+ for (let t = 2.0; t <= 5.4; t += 1 / 120) {
504
+ const n = Math.floor(260 * Math.pow(H.ease.in(seg(t, 2.0, 5.4)), 0.7));
505
+ if (n > last && t - lastT >= 1 / 12) { const r = H.rng(515 + n)(); nib(t, 0.7 + 0.3 * seg(t, 2, 5.4), (r - 0.5) * 1.2); last = n; lastT = t; }
506
+ }
507
+ }
508
+ k.noise(2.2, { dur: 3.8, color: 'brown', filter: 'lowpass', f: [[0, 120], [3.0, 380], [3.8, 200]], gain: 0.2, a: 2.6 }); // the land rises
509
+ k.withMinor(() => { for (let t = 4.2, i = 0; t < 5.6; t += 0.35, i++) k.noise(t, { dur: 0.6, color: 'pink', filter: 'bandpass', f: 500 + (i % 3) * 90, q: 1.4, gain: 0.05, a: 0.25, pan: pan(300) }); }); // the lake laps in
510
+ tick(5.6, 1.2, -0.6); // 1,000,000 surveyed
511
+ k.play('tock', 5.6, { f: 300, gain: 0.45, pan: -0.6 });
512
+ wood(5.25, 'E3', 0.9, 0.1); // HIGH ERROR / Low error lettered
513
+
514
+ // ---- camera to the summit; the hiker
515
+ k.noise(6.2, { dur: 0.9, color: 'pink', filter: 'bandpass', f: [[0, 280], [0.45, 560], [0.9, 260]], q: 0.7, gain: 0.09, a: 0.4 });
516
+ step(6.7, 0.8); // the hiker sets down at the summit
517
+ wood(6.85, 'G3', 0.6, -0.4); // GUESS → CHECK → ADJUST legend
518
+ pen(7.0, 7.3, 0.9, 0.2); // guess: "dog?"
519
+ pen(7.25, 7.35, 1.6, 0.3, false); pen(7.35, 7.45, 1.6, 0.3, false); // red ✗
520
+ k.tone(7.25, { f: 'D3', type: 'triangle', dur: 0.3, gain: 0.17, hold: 0.08, filter: 'lowpass', f2: 800, pan: 0.3 });
521
+ k.tone(7.43, { f: 'A2', type: 'triangle', dur: 0.5, gain: 0.17, hold: 0.12, filter: 'lowpass', f2: 600, pan: 0.3 });
522
+ pen(7.45, 7.8, 1.0, 0); // ring round the hiker
523
+ pen(7.6, 8.3, 0.7, -0.1); // check: slope arrow drawn downhill
524
+ // adjust: first leg down 8.4 -> 9.4, five steps
525
+ for (let i = 1; i <= 5; i++) step(when(8.4, 9.4, io, i / 5 - 0.1), 0.9, -0.1 + i * 0.04);
526
+ pen(9.5, 9.8, 0.9, 0.1); // guess: "cat"
527
+ pen(9.75, 9.95, 1.2, 0.2, false); // blue tick
528
+ ['G4', 'D5'].forEach((f, i) => k.play('ding', 9.82 + i * 0.08, { f, gain: 0.4, pan: 0.2 })); // right
529
+
530
+ // ---- camera out; accuracy cartouche; the hike down to the lake (10.8 -> 12.8)
531
+ k.noise(10.6, { dur: 1.1, color: 'pink', filter: 'bandpass', f: [[0, 260], [0.5, 520], [1.1, 240]], q: 0.7, gain: 0.09, a: 0.5 });
532
+ wood(10.85, 'G3', 0.7, -0.6);
533
+ { let lastT = -1; for (let f = 0.06; f < 1; f += 0.06) { const t = when(10.8, 12.8, io, f); if (t - lastT >= 0.11) { step(t, 0.75, pan(H.lerp(560, 330, f))); lastT = t; } } }
534
+ { let last = -1, lastT = -1; for (let t = 10.9; t <= 12.8 + 1e-6; t += 1 / 120) { const v = Math.round(61 + 36 * io(seg(t, 10.9, 12.8))); if (v !== last && t - lastT >= 1 / 12) { tick(t, 0.9, -0.6); last = v; lastT = t; } } }
535
+ // the hiker reaches the lake as 97% turns red: the big hit; the ring is penned round the lake
536
+ k.play('impact', 12.8, { gain: 0.8, pan: pan(300) });
537
+ k.play('ding', 12.82, { f: 'D5', gain: 0.6, pan: -0.4 });
538
+ k.noise(12.85, { dur: 0.9, color: 'pink', filter: 'bandpass', f: [[0, 700], [0.9, 400]], q: 1.2, gain: 0.07, a: 0.08, pan: pan(300), verb: 0.3 }); // water at the shore
539
+ pen(12.85, 13.3, 1.2, pan(300), false); // red ring round the lake (drawing since 12.6)
540
+ // takeaway cartouche settles, then resolves
541
+ wood(13.3, 'D3', 1.0);
542
+ ['D4', 'A4', 'F#5'].forEach((f, i) => k.play('ding', 13.5 + i * 0.09, { f, gain: 0.36 - i * 0.06, pan: (i - 1) * 0.25 }));
543
+ },
544
+ explainerDuration: 15,
545
+ explainerNote: 'The loss landscape is literally the map: survey points (examples) raise the terrain, a red hiker guesses, checks the slope and steps down a dashed switchback trail while the camera pans along it, and accuracy is lettered in a map cartouche like an elevation figure.',
546
+ explainer(ctx, W, H_, t) {
547
+ const C = CACHE || (CACHE = buildStatic(W, H_));
548
+ const { pk, WL, hmax, mw, mh, trail } = C;
549
+ const seg = (a, b) => H.clamp((t - a) / (b - a));
550
+ const INKC = '#1E1A16', RED = '#B5352A';
551
+ const nTrail = trail.length / 2;
552
+ const TP = (f) => { const i = Math.min(nTrail - 1, Math.max(0, Math.round(f * (nTrail - 1)))); return [trail[i * 2], trail[i * 2 + 1]]; };
553
+ // hiker position along the trail (fraction) over time
554
+ const STEP1 = 0.2;
555
+ const hikeF = t < 8.4 ? 0 : t < 9.4 ? STEP1 * H.ease.inOut(seg(8.4, 9.4)) : t < 10.8 ? STEP1 : STEP1 + (1 - STEP1) * H.ease.inOut(seg(10.8, 12.8));
556
+ const hiker = TP(hikeF);
557
+
558
+ // ---- camera keyframes (scale, centre) ----
559
+ const K = [
560
+ [0, 1, 640, 360], [6.2, 1, 640, 360], [7.0, 2.3, pk[0] - 40, pk[1] + 40], [8.3, 2.3, pk[0] - 40, pk[1] + 40],
561
+ [9.4, 2.3, TP(STEP1)[0] + 20, TP(STEP1)[1] - 10], [10.6, 2.3, TP(STEP1)[0] + 20, TP(STEP1)[1] - 10],
562
+ [11.6, 1.2, 600, 330], [13.0, 1, 640, 360], [15, 1, 640, 360],
563
+ ];
564
+ let cam = K[K.length - 1];
565
+ for (let i = 0; i < K.length - 1; i++) if (t >= K[i][0] && t <= K[i + 1][0]) {
566
+ const u = H.ease.inOut((t - K[i][0]) / (K[i + 1][0] - K[i][0]));
567
+ cam = [t, H.lerp(K[i][1], K[i + 1][1], u), H.lerp(K[i][2], K[i + 1][2], u), H.lerp(K[i][3], K[i + 1][3], u)];
568
+ break;
569
+ }
570
+ ctx.fillStyle = PAPER; ctx.fillRect(0, 0, W, H_);
571
+ ctx.save();
572
+ ctx.translate(W / 2, H_ / 2); ctx.scale(cam[1], cam[1]); ctx.translate(-cam[2], -cam[3]);
573
+ ctx.drawImage(C.paperL, 0, 0);
574
+
575
+ // ---- data raises the terrain: hillshade, woods, lake and contours emerge as points are surveyed ----
576
+ const dataP = seg(2.2, 5.8);
577
+ if (dataP > 0) {
578
+ ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = 0.85 * H.ease.inOut(dataP); ctx.drawImage(C.shadeL, MX0, MY0); ctx.restore();
579
+ ctx.save(); ctx.globalAlpha = seg(3.6, 5.0); ctx.drawImage(C.woodL, MX0, MY0); ctx.restore();
580
+ ctx.save(); ctx.globalAlpha = seg(4.2, 5.2); ctx.drawImage(C.waterL, MX0, MY0); ctx.restore();
581
+ }
582
+ const visibleL = dataP <= 0 ? -1e9 : WL + (hmax - WL + CI) * H.ease.inOut(seg(3.0, 6.0));
583
+ ctx.lineCap = 'round';
584
+ // contours break around their index numerals, as on the printed quad (same knockout boxes as the still)
585
+ ctx.save();
586
+ ctx.beginPath(); ctx.rect(MX0, MY0, mw, mh);
587
+ for (const b of C.boxes) {
588
+ const c = Math.cos(b.ang), s = Math.sin(b.ang), hw = b.w / 2, hh = b.h / 2;
589
+ const P = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([u, v]) => [b.x + u * c - v * s, b.y + u * s + v * c]);
590
+ ctx.moveTo(...P[0]); P.slice(1).forEach((p) => ctx.lineTo(...p)); ctx.closePath();
591
+ }
592
+ ctx.clip('evenodd');
593
+ C.levels.forEach((L, k) => {
594
+ if (L > visibleL) return;
595
+ const s = C.segs[k]; if (!s.length) return;
596
+ const idx = C.isIndex(L);
597
+ ctx.strokeStyle = idx ? 'rgba(150,88,48,0.95)' : 'rgba(162,100,58,0.72)';
598
+ ctx.lineWidth = (idx ? 1.45 : 0.65) / Math.sqrt(cam[1]);
599
+ ctx.beginPath();
600
+ for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); }
601
+ ctx.stroke();
602
+ });
603
+ ctx.restore();
604
+ // index contour numerals ink with their level (uphill-reading, from the still's placement)
605
+ if (dataP > 0) {
606
+ ctx.save(); ctx.font = '600 10px "Inter"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
607
+ C.labels.forEach((lb) => {
608
+ const a = H.clamp((visibleL - lb.L) / (CI * 2)); if (a <= 0) return;
609
+ ctx.fillStyle = `rgba(140,80,42,${a})`;
610
+ ctx.save(); ctx.translate(lb.x, lb.y); ctx.rotate(lb.ang); ctx.fillText(lb.txt, 0, 0.5); ctx.restore();
611
+ });
612
+ ctx.restore();
613
+ }
614
+ // blue streams run down to the lake once it fills (drawn with the lake's own fade)
615
+ const stA = seg(4.6, 5.4);
616
+ if (stA > 0) {
617
+ ctx.save(); ctx.globalAlpha = stA; ctx.strokeStyle = 'rgba(52,108,146,0.9)'; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
618
+ C.streams.forEach((pts) => {
619
+ for (let q = 1; q < pts.length; q++) {
620
+ const f = q / pts.length;
621
+ if (f < 0.22 && (q >> 2) % 3 === 0) continue; // intermittent near the spring
622
+ ctx.lineWidth = (0.55 + 1.0 * f) / Math.sqrt(cam[1]);
623
+ ctx.beginPath(); ctx.moveTo(...pts[q - 1]); ctx.lineTo(...pts[q]); ctx.stroke();
624
+ }
625
+ });
626
+ ctx.restore();
627
+ }
628
+ if (seg(4.2, 5.2) > 0) {
629
+ ctx.strokeStyle = `rgba(52,108,146,${0.95 * seg(4.2, 5.2)})`; ctx.lineWidth = 1.3; ctx.beginPath();
630
+ for (let q = 0; q < C.shore.length; q += 4) { ctx.moveTo(C.shore[q], C.shore[q + 1]); ctx.lineTo(C.shore[q + 2], C.shore[q + 3]); } ctx.stroke();
631
+ C.ws.forEach((s, k) => {
632
+ ctx.strokeStyle = `rgba(63,127,166,${(0.75 - k * 0.1) * seg(4.4 + k * 0.1, 5.0 + k * 0.1)})`; ctx.lineWidth = 0.6; ctx.beginPath();
633
+ for (let q = 0; q < s.length; q += 4) { ctx.moveTo(s[q], s[q + 1]); ctx.lineTo(s[q + 2], s[q + 3]); } ctx.stroke();
634
+ });
635
+ }
636
+
637
+ // ---- survey points = labelled examples, plotted rapidly ----
638
+ const NPTS = 260, pr = H.rng(515);
639
+ const shown = Math.floor(NPTS * H.ease.in(seg(2.0, 5.4)) ** 0.7);
640
+ const ptsAlpha = 1 - 0.85 * seg(6.0, 6.8);
641
+ const xCol = H.rgba(H.mix('#1E1A16', '#6b5a48', seg(6.0, 6.8)), 1), xArm = 4 / Math.sqrt(cam[1]);
642
+ ctx.font = 'italic 700 24px "Libre Baskerville"'; ctx.textAlign = 'left';
643
+ for (let i = 0; i < NPTS; i++) {
644
+ const x = pr.range(MX0 + 30, MX1 - 30), y = pr.range(MY0 + 30, MY1 - 30), cat = pr.chance(0.5);
645
+ if (i >= shown) continue;
646
+ const fresh = false;
647
+ ctx.globalAlpha = ptsAlpha;
648
+ ctx.strokeStyle = xCol; ctx.lineCap = 'round';
649
+ ctx.lineWidth = 1.6 / cam[1]; ctx.beginPath(); ctx.moveTo(x - xArm, y - xArm); ctx.lineTo(x + xArm, y + xArm); ctx.moveTo(x + xArm, y - xArm); ctx.lineTo(x - xArm, y + xArm); ctx.stroke();
650
+ if (i < 8 && x > MX0 + 150 && y > MY0 + 140 && t < 6.3) {
651
+ ctx.globalAlpha = 1 - seg(5.8, 6.3);
652
+ ctx.save(); ctx.strokeStyle = 'rgba(238,229,203,0.95)'; ctx.lineWidth = 5; ctx.lineJoin = 'round'; ctx.strokeText(cat ? 'cat' : 'dog', x + 8, y - 6);
653
+ ctx.restore(); ctx.fillStyle = cat ? '#2C6D96' : '#8E2A22'; ctx.fillText(cat ? 'cat' : 'dog', x + 8, y - 6);
654
+ }
655
+ ctx.globalAlpha = 1;
656
+ }
657
+
658
+ // ---- landscape labels land on the terrain ----
659
+ const labA = seg(5.2, 5.8) * (1 - seg(10.6, 11.0) * 0);
660
+ if (labA > 0) {
661
+ ctx.save(); ctx.globalAlpha = labA; ctx.fillStyle = INKC; ctx.font = `700 ${(30 / Math.pow(cam[1], 0.7)).toFixed(1)}px "Libre Baskerville"`;
662
+ ctx.strokeStyle = 'rgba(238,229,203,0.9)'; ctx.lineWidth = 5;
663
+ const lab = (s, x, y) => { ctx.save(); ctx.lineJoin = 'round'; let w = 0; for (const ch of s) w += ctx.measureText(ch).width + 2.5; let cx = x - w / 2; for (const ch of s) { ctx.strokeText(ch, cx, y); cx += ctx.measureText(ch).width + 2.5; } ctx.restore(); spacedText(ctx, s, x, y, 2.5, 'center'); };
664
+ lab('HIGH ERROR', pk[0], pk[1] - 22);
665
+ // spot heights, in the still's small survey type
666
+ ctx.save(); ctx.fillStyle = INKC;
667
+ ctx.font = '400 10px "Libre Baskerville"'; ctx.textAlign = 'left';
668
+ C.spots.forEach(([x, y, h]) => { ctx.fillText('×', x - 3, y + 3); ctx.fillText(String(Math.round(h)), x + 6, y + 4); });
669
+ ctx.restore();
670
+ ctx.font = 'italic 700 32px "Libre Baskerville"'; ctx.fillStyle = '#2C6D96';
671
+ spacedText(ctx, 'Low error', 300, 410, 1.5, 'center');
672
+ ctx.restore();
673
+ }
674
+
675
+ // ---- the trail: guess, check, adjust ----
676
+ const nDrawn = Math.floor(nTrail * hikeF);
677
+ if (nDrawn > 2) {
678
+ ctx.save(); ctx.strokeStyle = RED; ctx.lineWidth = 1.7 / Math.sqrt(cam[1]) * 1.3; ctx.setLineDash([6, 4]); ctx.lineJoin = 'round';
679
+ ctx.beginPath(); ctx.moveTo(trail[0], trail[1]);
680
+ for (let q = 1; q < nDrawn; q++) ctx.lineTo(trail[q * 2], trail[q * 2 + 1]);
681
+ ctx.stroke(); ctx.restore();
682
+ }
683
+ const hikerA = seg(6.6, 7.0);
684
+ const halo = (str, x, y, col) => { ctx.save(); ctx.strokeStyle = 'rgba(238,229,203,0.92)'; ctx.lineWidth = 3; ctx.lineJoin = 'round'; ctx.strokeText(str, x, y); ctx.restore(); ctx.fillStyle = col; ctx.fillText(str, x, y); };
685
+ if (hikerA > 0) {
686
+ // guess #1 at the summit
687
+ const [gx, gy] = TP(0);
688
+ const g1 = seg(7.0, 7.3) * (1 - seg(8.3, 8.6));
689
+ if (g1 > 0) {
690
+ ctx.save(); ctx.globalAlpha = g1; ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left';
691
+ halo('guess: “dog?”', gx + 26, gy - 14, INKC);
692
+ // red ✗ and a hand-drawn ring for emphasis
693
+ const xA = seg(7.25, 7.45), xx = gx + 26 + ctx.measureText('guess: “dog?”').width + 8, xy = gy - 24;
694
+ ctx.strokeStyle = RED; ctx.lineWidth = 2.2; ctx.lineCap = 'round';
695
+ ctx.beginPath(); ctx.moveTo(xx, xy); ctx.lineTo(xx + 10 * H.clamp(xA * 2), xy + 10 * H.clamp(xA * 2));
696
+ if (xA > 0.5) { ctx.moveTo(xx + 10, xy); ctx.lineTo(xx + 10 - 10 * (xA * 2 - 1), xy + 10 * (xA * 2 - 1)); } ctx.stroke();
697
+ const rr = seg(7.35, 7.8), r0 = H.rng(9);
698
+ ctx.lineWidth = 1.4; ctx.beginPath();
699
+ for (let k = 0; k <= 40 * rr; k++) { const a = -2.2 + k / 40 * 7.0, r = 20 + Math.sin(k * 0.4) * 1.2 + k * 0.08; const px = gx + Math.cos(a) * r * 1.15, py = gy + Math.sin(a) * r; k ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }
700
+ ctx.stroke();
701
+ ctx.restore();
702
+ }
703
+ // check: slope arrow drawn on, pointing downhill along the first leg
704
+ const ck = seg(7.6, 8.3) * (1 - seg(8.6, 8.9));
705
+ if (ck > 0) {
706
+ const [ax, ay] = TP(0.01), [bx, by] = TP(STEP1 * 0.92);
707
+ const ex = H.lerp(ax, bx, ck), ey = H.lerp(ay, by, ck);
708
+ ctx.save(); ctx.strokeStyle = RED; ctx.fillStyle = RED; ctx.lineWidth = 2; ctx.setLineDash([]);
709
+ ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ex, ey); ctx.stroke();
710
+ const an = Math.atan2(by - ay, bx - ax);
711
+ ctx.beginPath(); ctx.moveTo(ex + Math.cos(an) * 6, ey + Math.sin(an) * 6); ctx.lineTo(ex + Math.cos(an + 2.5) * 7, ey + Math.sin(an + 2.5) * 7); ctx.lineTo(ex + Math.cos(an - 2.5) * 7, ey + Math.sin(an - 2.5) * 7); ctx.closePath(); ctx.fill();
712
+ ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left';
713
+ halo('check: downhill', (ax + bx) / 2 + 14, (ay + by) / 2 + 4, RED);
714
+ ctx.restore();
715
+ }
716
+ // adjust label while walking
717
+ const adj = seg(8.4, 8.7) * (1 - seg(9.7, 10.0));
718
+ if (adj > 0) { ctx.save(); ctx.globalAlpha = adj; ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left'; halo('adjust: step down', hiker[0] + 12, hiker[1] + 18, INKC); ctx.restore(); }
719
+ // guess #2: lower, better
720
+ const g2 = seg(9.5, 9.8) * (1 - seg(10.9, 11.2));
721
+ if (g2 > 0) {
722
+ const [hx, hy] = TP(STEP1);
723
+ ctx.save(); ctx.globalAlpha = g2; ctx.font = 'italic 700 13.5px "Libre Baskerville"'; ctx.textAlign = 'left';
724
+ halo('guess: “cat”', hx + 12, hy - 12, INKC);
725
+ ctx.strokeStyle = '#2C6D96'; ctx.lineWidth = 2.4; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
726
+ const v = seg(9.7, 9.95);
727
+ ctx.beginPath(); ctx.moveTo(hx + 92, hy - 17); ctx.lineTo(hx + 96, hy - 12);
728
+ if (v > 0.4) ctx.lineTo(hx + 96 + 8 * (v - 0.4) / 0.6, hy - 12 - 12 * (v - 0.4) / 0.6);
729
+ ctx.stroke(); ctx.restore();
730
+ }
731
+ // hiker marker
732
+ ctx.save(); ctx.globalAlpha = hikerA;
733
+ ctx.fillStyle = 'rgba(238,229,203,0.95)'; ctx.beginPath(); ctx.arc(hiker[0], hiker[1], 5.5, 0, 7); ctx.fill();
734
+ ctx.fillStyle = RED; ctx.beginPath(); ctx.arc(hiker[0], hiker[1], 3.8, 0, 7); ctx.fill();
735
+ ctx.restore();
736
+ }
737
+ // final emphasis: red ring around the lake = the low-error goal
738
+ const ring = seg(12.6, 13.3);
739
+ if (ring > 0) {
740
+ ctx.save(); ctx.strokeStyle = RED; ctx.lineWidth = 2; ctx.beginPath();
741
+ for (let k = 0; k <= 60 * ring; k++) { const a = -1.4 + k / 60 * 6.6, r = 1 + Math.sin(k * 0.3) * 0.02; const px = 300 + Math.cos(a) * 165 * r, py = 400 + Math.sin(a) * 105 * r; k ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }
742
+ ctx.stroke(); ctx.restore();
743
+ }
744
+
745
+ // ---- neatline + collar (always present: it is the sheet) ----
746
+ ctx.strokeStyle = INKC; ctx.lineWidth = 1.2; ctx.strokeRect(MX0 + 0.5, MY0 + 0.5, mw, mh);
747
+ ctx.lineWidth = 0.6; ctx.strokeRect(MX0 - 3.5, MY0 - 3.5, mw + 8, mh + 8);
748
+ ctx.font = '400 9px "Inter"'; ctx.fillStyle = INKC; ctx.textAlign = 'center';
749
+ spacedText(ctx, 'THE MODEL SURVEY', 489, 16, 2, 'center');
750
+ spacedText(ctx, 'TRAINING SERIES', 1093, 16, 2, 'center');
751
+ const cy0 = MY1 + 10;
752
+ ctx.textAlign = 'left'; ctx.font = '400 9px "Inter"';
753
+ ctx.fillText('Each × is one labelled example', MX0, cy0 + 18);
754
+ ctx.fillText('Elevation = how wrong the model is', MX0, cy0 + 31);
755
+ ctx.fillText('Red dashes: the path of training', MX0, cy0 + 44);
756
+ ctx.fillText('Illustrative map. Not for navigation', MX0, cy0 + 57);
757
+ // sheet furniture from the still: graticule ticks and corner coordinates, north arrow, scale bar
758
+ ctx.lineWidth = 1;
759
+ for (let k = 1; k < 4; k++) {
760
+ const x = MX0 + mw * k / 4;
761
+ ctx.beginPath(); ctx.moveTo(x, MY0); ctx.lineTo(x, MY0 + 8); ctx.moveTo(x, MY1); ctx.lineTo(x, MY1 - 8); ctx.stroke();
762
+ }
763
+ for (let k = 1; k < 3; k++) {
764
+ const y = MY0 + mh * k / 3;
765
+ ctx.beginPath(); ctx.moveTo(MX0, y); ctx.lineTo(MX0 + 8, y); ctx.moveTo(MX1, y); ctx.lineTo(MX1 - 8, y); ctx.stroke();
766
+ }
767
+ ctx.textAlign = 'left'; ctx.fillText('37°52′30″ 122°22′30″', MX0, MY0 - 9);
768
+ ctx.textAlign = 'right'; ctx.fillText('122°12′30″', MX1, MY0 - 9);
769
+ {
770
+ const ax = 380, ay = cy0 + 64;
771
+ ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(ax, ay - 44); ctx.stroke();
772
+ ctx.save(); ctx.translate(ax, ay - 49); ctx.beginPath();
773
+ for (let q = 0; q < 10; q++) { const r = q % 2 ? 2.2 : 5.5, a = -Math.PI / 2 + q * Math.PI / 5; ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); }
774
+ ctx.closePath(); ctx.fill(); ctx.restore();
775
+ const mxp = ax + Math.sin(0.24) * 40, myp = ay - Math.cos(0.24) * 40;
776
+ ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(mxp, myp); ctx.stroke();
777
+ ctx.beginPath(); ctx.moveTo(mxp, myp); ctx.lineTo(mxp - 1.5, myp + 10); ctx.lineTo(mxp + 3, myp + 9); ctx.closePath(); ctx.fill();
778
+ ctx.textAlign = 'left'; ctx.fillText('MN', mxp + 4, myp + 4); ctx.fillText('14°', ax + 5, ay - 6);
779
+ }
780
+ {
781
+ const sx = 520, sy = cy0 + 39, unit = 60;
782
+ ctx.font = '700 13px "Libre Baskerville"'; spacedText(ctx, 'SCALE 1:24 000', sx + unit * 1.5, cy0 + 21, 1.5, 'center');
783
+ ctx.lineWidth = 0.8;
784
+ for (let k = 0; k < 4; k++) { ctx.fillStyle = k % 2 ? INKC : PAPER; ctx.fillRect(sx - unit + k * unit / 4, sy, unit / 4, 5); }
785
+ for (let k = 0; k < 3; k++) { ctx.fillStyle = k % 2 ? PAPER : INKC; ctx.fillRect(sx + k * unit, sy, unit, 5); }
786
+ ctx.strokeRect(sx - unit, sy, unit * 4, 5);
787
+ ctx.fillStyle = INKC; ctx.font = '400 9px "Inter"'; ctx.textAlign = 'center';
788
+ ['1', '0', '1', '2', '3'].forEach((q, k) => ctx.fillText(q, sx - unit + k * unit, sy + 16));
789
+ ctx.fillText('KILOMETERS', sx + unit * 3 + 34, sy + 5);
790
+ ctx.font = '400 10px "Libre Baskerville"';
791
+ spacedText(ctx, 'CONTOUR INTERVAL 40 METERS', sx + unit * 1, sy + 32, 1.2, 'center');
792
+ }
793
+ // ---- age: fold crease, foxing (the still's pass, so every frame shares the aged sheet) ----
794
+ {
795
+ ctx.save();
796
+ const fx = 640;
797
+ const gfold = ctx.createLinearGradient(fx - 10, 0, fx + 10, 0);
798
+ gfold.addColorStop(0, 'rgba(255,255,255,0)'); gfold.addColorStop(0.45, 'rgba(255,255,255,0.18)');
799
+ gfold.addColorStop(0.55, 'rgba(90,70,40,0.10)'); gfold.addColorStop(1, 'rgba(90,70,40,0)');
800
+ ctx.fillStyle = gfold; ctx.fillRect(fx - 10, -20, 20, H_ + 40);
801
+ ctx.globalCompositeOperation = 'multiply';
802
+ const fr = H.rng(77);
803
+ for (let k = 0; k < 7; k++) {
804
+ const x = fr.range(0, W), y = fr.range(0, H_), r = fr.range(30, 140);
805
+ const gg = ctx.createRadialGradient(x, y, 0, x, y, r);
806
+ gg.addColorStop(0, 'rgba(180,150,100,0.10)'); gg.addColorStop(0.8, 'rgba(180,150,100,0.04)'); gg.addColorStop(1, 'rgba(180,150,100,0)');
807
+ ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(x, y, r, 0, 7); ctx.fill();
808
+ }
809
+ ctx.restore();
810
+ }
811
+ ctx.restore(); // camera
812
+
813
+ // ---- the question: a big cartouche title that docks into the collar title block ----
814
+ const dock = H.ease.inOut(seg(1.5, 2.4));
815
+ {
816
+ const x = H.lerp(640, MX1, dock), y = H.lerp(350, MY1 + 40, dock), size = H.lerp(64, 28, dock);
817
+ ctx.save();
818
+ ctx.globalAlpha = seg(0, 0.4) * (1 - seg(1.4, 1.8));
819
+ ctx.fillStyle = 'rgba(238,229,203,0.96)'; ctx.fillRect(170, 236, 940, 190);
820
+ ctx.strokeStyle = INKC; ctx.lineWidth = 1.6; ctx.strokeRect(170.5, 236.5, 940, 190); ctx.lineWidth = 0.8; ctx.strokeRect(177.5, 243.5, 926, 176);
821
+ ctx.globalAlpha = seg(0.1, 0.6);
822
+ ctx.fillStyle = INKC; ctx.font = `700 ${size}px "Libre Baskerville"`;
823
+ spacedText(ctx, 'HOW DOES AI LEARN?', x, y, H.lerp(5, 3, dock), dock > 0.5 ? 'right' : 'center');
824
+ ctx.globalAlpha = seg(0.5, 0.9) * (1 - seg(1.3, 1.6));
825
+ ctx.font = 'italic 400 28px "Libre Baskerville"';
826
+ spacedText(ctx, 'A survey of the loss landscape', 640, 398, 1, 'center');
827
+ ctx.globalAlpha = dock;
828
+ ctx.font = '400 10px "Libre Baskerville"';
829
+ spacedText(ctx, 'LOSS LANDSCAPE QUADRANGLE', MX1, MY1 + 57, 1.6, 'right');
830
+ ctx.restore();
831
+ }
832
+
833
+ // ---- cartouches (screen-fixed inset boxes, printed in the map's own type) ----
834
+ const box = (x, y, w, h) => {
835
+ ctx.fillStyle = 'rgba(238,229,203,0.97)'; ctx.fillRect(x, y, w, h);
836
+ ctx.strokeStyle = INKC; ctx.lineWidth = 1.6; ctx.strokeRect(x + 0.5, y + 0.5, w, h); ctx.lineWidth = 0.7; ctx.strokeRect(x + 6.5, y + 6.5, w - 12, h - 12);
837
+ };
838
+ const exA = seg(2.0, 2.3) * (1 - seg(6.2, 6.6));
839
+ if (exA > 0) {
840
+ ctx.save(); ctx.globalAlpha = exA; box(50, 44, 420, 136);
841
+ ctx.fillStyle = INKC; ctx.font = '700 28px "Libre Baskerville"'; spacedText(ctx, 'EXAMPLES SURVEYED', 74, 86, 1, 'left');
842
+ const n = Math.round(1e6 * H.ease.in(seg(2.0, 5.6)) ** 0.8);
843
+ ctx.font = '700 66px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillText(n.toLocaleString('en-US'), 70, 158);
844
+ ctx.restore();
845
+ }
846
+ const lgA = seg(6.8, 7.1) * (1 - seg(10.8, 11.1));
847
+ if (lgA > 0) {
848
+ ctx.save(); ctx.globalAlpha = lgA; box(50, 44, 600, 80);
849
+ const steps = ['GUESS', 'CHECK', 'ADJUST'];
850
+ const active = t < 7.6 ? 0 : t < 8.4 ? 1 : t < 9.5 ? 2 : 0;
851
+ let x = 76;
852
+ steps.forEach((s, i) => {
853
+ ctx.font = '700 28px "Libre Baskerville"';
854
+ ctx.fillStyle = i === active ? RED : 'rgba(30,26,22,0.4)';
855
+ const w = spacedText(ctx, s, x, 96, 3, 'left');
856
+ if (i === active) { ctx.fillRect(x, 104, w, 3); }
857
+ x += w + 16;
858
+ if (i < 2) { ctx.fillStyle = 'rgba(30,26,22,0.6)'; ctx.font = '400 28px "Libre Baskerville"'; ctx.fillText('→', x, 96); x += 46; }
859
+ });
860
+ ctx.restore();
861
+ }
862
+ const acA = seg(10.8, 11.2);
863
+ if (acA > 0) {
864
+ ctx.save(); ctx.globalAlpha = acA; box(50, 44, 470, 176);
865
+ ctx.fillStyle = INKC; ctx.font = '700 28px "Libre Baskerville"'; spacedText(ctx, 'ACCURACY', 74, 84, 2, 'left');
866
+ const p = H.ease.inOut(seg(10.9, 12.8));
867
+ const acc = Math.round(61 + 36 * p);
868
+ ctx.font = '700 80px "Libre Baskerville"'; ctx.textAlign = 'left'; ctx.fillStyle = p >= 1 ? RED : INKC; ctx.fillText(acc + '%', 70, 164);
869
+ const px0 = 300, py0 = 192, pw = 196, ph = 110;
870
+ ctx.strokeStyle = 'rgba(30,26,22,0.4)'; ctx.lineWidth = 1;
871
+ ctx.beginPath(); ctx.moveTo(px0, py0); ctx.lineTo(px0 + pw, py0); ctx.moveTo(px0, py0); ctx.lineTo(px0, py0 - ph); ctx.stroke();
872
+ ctx.strokeStyle = RED; ctx.lineWidth = 3; ctx.beginPath();
873
+ for (let k = 0; k <= 40 * p; k++) { const u = k / 40, v = 1 - Math.exp(-u * 4); const xx = px0 + u * pw, yy = py0 - 8 - v * (ph - 18); k ? ctx.lineTo(xx, yy) : ctx.moveTo(xx, yy); }
874
+ ctx.stroke();
875
+ ctx.font = 'italic 400 24px "Libre Baskerville"'; ctx.fillStyle = INKC; ctx.textAlign = 'right'; ctx.fillText('rounds →', px0 + pw + 4, py0 + 23); ctx.textAlign = 'left';
876
+ ctx.font = 'italic 400 28px "Libre Baskerville"'; ctx.fillText('from 61%', 74, 204);
877
+ if (p >= 1) { const u = seg(12.8, 13.1); ctx.fillStyle = RED; ctx.fillRect(72, 174, 190 * u, 3.5); }
878
+ ctx.restore();
879
+ }
880
+ const tk = seg(13.2, 13.7);
881
+ if (tk > 0) {
882
+ ctx.save(); ctx.globalAlpha = tk;
883
+ const y = 600 + (1 - H.ease.out(tk)) * 12;
884
+ ctx.fillStyle = 'rgba(238,229,203,0.97)'; ctx.fillRect(150, y - 58, 980, 92);
885
+ ctx.strokeStyle = INKC; ctx.lineWidth = 1.6; ctx.strokeRect(150.5, y - 57.5, 980, 92); ctx.lineWidth = 0.7; ctx.strokeRect(157.5, y - 50.5, 966, 78);
886
+ ctx.fillStyle = INKC; ctx.font = '700 40px "Libre Baskerville"';
887
+ spacedText(ctx, 'MORE EXAMPLES → BETTER GUESSES', 640, y + 2, 2, 'center');
888
+ ctx.restore();
889
+ }
890
+
891
+ H.vignette(ctx, W, H_, 0.18, '80,60,30');
892
+ H.grain(ctx, W, H_, 0.035, 31 + Math.floor(t * 12));
893
+ },
894
+ });
895
+ }