@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,1237 @@
1
+ // Sketchbook Pencil — graphite study on cream sketchbook paper.
2
+ {
3
+ const seg = (pts, step = 3) => {
4
+ const out = [];
5
+ for (let i = 0; i < pts.length - 1; i++) {
6
+ const [x0, y0] = pts[i], [x1, y1] = pts[i + 1];
7
+ const n = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0) / step));
8
+ for (let j = 0; j < n; j++) out.push([x0 + (x1 - x0) * j / n, y0 + (y1 - y0) * j / n]);
9
+ }
10
+ out.push(pts[pts.length - 1]);
11
+ return out;
12
+ };
13
+ const normals = (P) => P.map((p, i) => {
14
+ const a = P[Math.max(0, i - 1)], b = P[Math.min(P.length - 1, i + 1)];
15
+ const dx = b[0] - a[0], dy = b[1] - a[1], d = Math.hypot(dx, dy) || 1;
16
+ return [-dy / d, dx / d];
17
+ });
18
+ const wob = (pts, nz, seed, amp = 1.2, freq = 0.01) => {
19
+ const N = normals(pts); let L = 0;
20
+ return pts.map((p, i) => {
21
+ if (i) L += Math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1]);
22
+ const o = nz.n2(L * freq + seed * 1.37, seed * 0.71) * amp + nz.n2(L * freq * 7 + seed, seed + 3.3) * amp * 0.2;
23
+ return [p[0] + N[i][0] * o, p[1] + N[i][1] * o];
24
+ });
25
+ };
26
+ const ell = (cx, cy, rx, ry, a0 = 0, a1 = Math.PI * 2, grow = 0) => {
27
+ const n = Math.max(10, Math.ceil(Math.abs(a1 - a0) * Math.max(rx, ry) / 3)), out = [];
28
+ for (let i = 0; i <= n; i++) {
29
+ const u = i / n, a = a0 + (a1 - a0) * u, g = 1 + grow * u;
30
+ out.push([cx + Math.cos(a) * rx * g, cy + Math.sin(a) * ry * g]);
31
+ }
32
+ return out;
33
+ };
34
+ const bez = (p0, p1, p2, p3, n = 40) => {
35
+ const out = [];
36
+ for (let i = 0; i <= n; i++) {
37
+ const t = i / n, m = 1 - t;
38
+ out.push([m * m * m * p0[0] + 3 * m * m * t * p1[0] + 3 * m * t * t * p2[0] + t * t * t * p3[0],
39
+ m * m * m * p0[1] + 3 * m * m * t * p1[1] + 3 * m * t * t * p2[1] + t * t * t * p3[1]]);
40
+ }
41
+ return out;
42
+ };
43
+
44
+ const GR = '38,38,44';
45
+ // a graphite line: pressure tapers at both ends, slight density flicker
46
+ // time gating: PH = sequential stroke clock, GATE = explicit progress
47
+ let PH = null, GATE = 1, TIP = null;
48
+ const pencil = (c, pts, w, a, seed, taperIn = 0.15, taperOut = 0.25, nz) => {
49
+ if (pts.length < 2) return;
50
+ let gate = GATE;
51
+ if (PH) { const k = PH.k++; gate = H.clamp((PH.t - (PH.t0 + k * PH.dt)) / (PH.dt * 2)); }
52
+ if (gate <= 0) return;
53
+ const n = gate < 1 ? Math.max(2, Math.ceil(pts.length * gate)) : pts.length;
54
+ if (gate < 1 && PH) TIP = pts[n - 1];
55
+ c.lineCap = 'butt'; c.lineJoin = 'round';
56
+ for (let i = 0; i < n - 1; i += 2) {
57
+ const u = i / (n - 1);
58
+ let p = Math.min(1, u / taperIn, (1 - u) / taperOut);
59
+ p = 0.25 + 0.75 * Math.sqrt(Math.max(0, p));
60
+ const fl = 0.8 + 0.4 * (0.5 + 0.5 * nz.n2(i * 0.07, seed * 0.13));
61
+ c.strokeStyle = `rgba(${GR},${Math.min(1, a * p * fl)})`; c.lineWidth = w * (0.55 + 0.45 * p);
62
+ c.beginPath(); c.moveTo(pts[i][0], pts[i][1]); c.lineTo(pts[i + 1][0], pts[i + 1][1]);
63
+ if (pts[i + 2]) c.lineTo(pts[i + 2][0], pts[i + 2][1]);
64
+ c.stroke();
65
+ }
66
+ };
67
+
68
+ // >>>XE-ENGINE
69
+ // ===================== explainer story engine (shared script: "How does AI learn?") =====================
70
+ // Builds the diagram as timed world-space strokes/texts; each style renders them with its own medium.
71
+ const XC = {}; // cache for static textures (output still depends only on t)
72
+ const XE = (() => {
73
+ const clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v));
74
+ const win = (t, a, b) => clamp((t - a) / (b - a));
75
+ const eio = (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2);
76
+ const camAt = (keys, t) => {
77
+ if (t <= keys[0][0]) return keys[0].slice(1);
78
+ for (let i = 0; i < keys.length - 1; i++) {
79
+ const a = keys[i], b = keys[i + 1];
80
+ if (t <= b[0]) {
81
+ const u = eio((t - a[0]) / Math.max(1e-6, b[0] - a[0]));
82
+ return [a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u, Math.exp(Math.log(a[3]) + (Math.log(b[3]) - Math.log(a[3])) * u)];
83
+ }
84
+ }
85
+ return keys[keys.length - 1].slice(1);
86
+ };
87
+ const circ = (x, y, rx, ry, a0 = 0, a1 = Math.PI * 2, n = 0) => {
88
+ n = n || Math.max(12, Math.ceil(Math.abs(a1 - a0) * Math.max(rx, ry) / 6)); const o = [];
89
+ for (let i = 0; i <= n; i++) { const a = a0 + (a1 - a0) * i / n; o.push([x + Math.cos(a) * rx, y + Math.sin(a) * ry]); }
90
+ return o;
91
+ };
92
+ const quad = (p0, p1, p2, n = 30) => { const o = []; for (let i = 0; i <= n; i++) { const t = i / n, m = 1 - t; o.push([m * m * p0[0] + 2 * m * t * p1[0] + t * t * p2[0], m * m * p0[1] + 2 * m * t * p1[1] + t * t * p2[1]]); } return o; };
93
+ const cubic = (p0, p1, p2, p3, n = 40) => { const o = []; for (let i = 0; i <= n; i++) { const t = i / n, m = 1 - t; o.push([m * m * m * p0[0] + 3 * m * m * t * p1[0] + 3 * m * t * t * p2[0] + t * t * t * p3[0], m * m * m * p0[1] + 3 * m * m * t * p1[1] + 3 * m * t * t * p2[1] + t * t * t * p3[1]]); } return o; };
94
+ const rrect = (x, y, w, h, r) => {
95
+ const o = [[x + r, y], [x + w - r, y]]; o.push(...circ(x + w - r, y + r, r, r, -Math.PI / 2, 0, 5).slice(1));
96
+ o.push([x + w, y + h - r]); o.push(...circ(x + w - r, y + h - r, r, r, 0, Math.PI / 2, 5).slice(1));
97
+ o.push([x + r, y + h]); o.push(...circ(x + r, y + h - r, r, r, Math.PI / 2, Math.PI, 5).slice(1));
98
+ o.push([x, y + r]); o.push(...circ(x + r, y + r, r, r, Math.PI, Math.PI * 1.5, 5).slice(1));
99
+ o.push([x + r + 6, y - 1]); return o;
100
+ };
101
+ const head = (P, len = 20, spread = 0.5) => {
102
+ const a = P[P.length - 1], b = P[Math.max(0, P.length - 4)], an = Math.atan2(a[1] - b[1], a[0] - b[0]);
103
+ return [[a[0] - Math.cos(an - spread) * len, a[1] - Math.sin(an - spread) * len], a, [a[0] - Math.cos(an + spread) * len, a[1] - Math.sin(an + spread) * len]];
104
+ };
105
+ const cat = (x, y, s) => [
106
+ circ(x, y + 0.06 * s, 0.3 * s, 0.27 * s, -1.2, 5.1),
107
+ [[x - 0.27 * s, y - 0.02 * s], [x - 0.24 * s, y - 0.34 * s], [x - 0.07 * s, y - 0.2 * s]],
108
+ [[x + 0.07 * s, y - 0.2 * s], [x + 0.24 * s, y - 0.34 * s], [x + 0.27 * s, y - 0.02 * s]],
109
+ [[x - 0.03 * s, y + 0.1 * s], [x, y + 0.13 * s], [x + 0.03 * s, y + 0.1 * s]],
110
+ [[x - 0.12 * s, y + 0.12 * s], [x - 0.36 * s, y + 0.08 * s]], [[x - 0.12 * s, y + 0.16 * s], [x - 0.35 * s, y + 0.2 * s]],
111
+ [[x + 0.12 * s, y + 0.12 * s], [x + 0.36 * s, y + 0.08 * s]], [[x + 0.12 * s, y + 0.16 * s], [x + 0.35 * s, y + 0.2 * s]],
112
+ ];
113
+ const dog = (x, y, s) => [
114
+ circ(x, y + 0.04 * s, 0.26 * s, 0.28 * s, -1.4, 4.9),
115
+ circ(x - 0.29 * s, y + 0.04 * s, 0.09 * s, 0.2 * s, 0, 6.4),
116
+ circ(x + 0.29 * s, y + 0.04 * s, 0.09 * s, 0.2 * s, 0, 6.4),
117
+ circ(x, y + 0.15 * s, 0.07 * s, 0.045 * s, 0, 6.4),
118
+ [[x - 0.08 * s, y + 0.23 * s], [x, y + 0.26 * s], [x + 0.08 * s, y + 0.23 * s]],
119
+ ];
120
+ const eyes = (x, y, s, dy = 0.02) => [[x - 0.1 * s, y + dy * s], [x + 0.1 * s, y + dy * s]];
121
+ const fmt = (n) => Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
122
+
123
+ // L: layout options. mode: 'same' (progress drawn where the loop was) | 'below' | 'right'
124
+ // model(cx, cy, adj, add): style supplies its model drawing; adj 0..1 = how far the knobs have been adjusted.
125
+ const story = (t, L) => {
126
+ const items = [];
127
+ const line = (pts, role, w, a, b, g) => items.push({ k: 'line', pts, role, w, a, b, g });
128
+ const text = (s, x, y, size, font, role, a, b, opt = {}) => items.push({ k: 'text', s, x, y, size, font, role, a, b, rot: opt.rot || 0, align: opt.align || 'left', g: opt.g });
129
+ const dot = (x, y, r, role, a, g) => items.push({ k: 'dot', x, y, r, role, a, b: a + 0.05, g });
130
+ const S = L.s || 1;
131
+ // ---- 1 HOOK ----
132
+ const TX = L.titleX ?? 640, TY = L.titleY ?? 350;
133
+ text('How does AI learn?', TX, TY, 84 * S, 'title', 'ink', 0.15, 1.3, { align: 'center', rot: -0.015, g: 'A' });
134
+ line([[TX - 330 * S, TY + 32 * S], [TX + 330 * S, TY + 24 * S]], 'acc1', 7, 1.3, 1.55, 'A');
135
+ // ---- 2 DATA ----
136
+ const MX = L.modelX ?? 2250, MY = L.modelY ?? 640;
137
+ const mAdj = eio(win(t, 8.95, 9.5));
138
+ L.model(MX, MY, mAdj, (it) => { it.g = it.g || 'M'; items.push(it); }, { line, text, dot }, t);
139
+ text(L.modelName || 'model', MX, MY + 228, 38, 'label', 'ink', 3.15, 3.4, { align: 'center', g: 'M' });
140
+ const CS = 118, cx0 = MX - 870, cy0 = MY - 175;
141
+ for (let i = 0; i < 6; i++) {
142
+ const col = i % 3, row = (i / 3) | 0, x = cx0 + col * 150 + (row ? 40 : 0), y = cy0 + row * 170, isCat = [1, 0, 1, 1, 0, 0][i];
143
+ const a = 3.0 + i * 0.17;
144
+ line(rrect(x - CS / 2, y - CS / 2, CS, CS, 12), 'ink', 4, a, a + 0.12, 'B');
145
+ (isCat ? cat : dog)(x, y - 6, CS * 0.82).forEach((p) => line(p, 'ink', 3.2, a + 0.1, a + 0.22, 'B'));
146
+ eyes(x, y - 6, CS * 0.82).forEach(([ex, ey]) => dot(ex, ey, 3.2, 'ink', a + 0.22, 'B'));
147
+ // label tag hanging off the card
148
+ const tx = x + 18, ty = y + CS / 2 - 4;
149
+ line([[tx, ty], [tx + 70, ty], [tx + 84, ty + 19], [tx + 70, ty + 38], [tx, ty + 38], [tx, ty]], isCat ? 'acc1' : 'acc3', 3, a + 0.2, a + 0.3, 'B');
150
+ text(isCat ? 'cat' : 'dog', tx + 8, ty + 30, 31, 'label', isCat ? 'acc1' : 'acc3', a + 0.28, a + 0.36, { g: 'B' });
151
+ }
152
+ // flow arrow into the model
153
+ const fa = cubic([cx0 + 370, MY - 10], [cx0 + 470, MY - 60], [MX - 260, MY - 40], [MX - 165, MY - 10]);
154
+ line(fa, 'ink', 5, 4.15, 4.45, 'B'); line(head(fa, 24), 'ink', 5, 4.45, 4.55, 'B');
155
+ // little example cards streaming along the arrow
156
+ if (t > 4.5 && t < 6.2) {
157
+ for (let k = 0; k < 9; k++) {
158
+ const u = ((t - 4.5) * 0.9 + k / 9) % 1, ix = Math.min(fa.length - 1, Math.floor(u * fa.length));
159
+ const [px, py] = fa[ix], sz = 26 * (1 - 0.5 * u);
160
+ if (t - 4.5 < k / 9 / 0.9) continue;
161
+ items.push({ k: 'line', pts: rrect(px - sz / 2, py - sz / 2 - 18, sz, sz, 4), role: k % 2 ? 'acc1' : 'acc3', w: 2.5, a: -1, b: 0, g: 'F', inst: true });
162
+ }
163
+ }
164
+ // counter (numerals redrawn on twos)
165
+ if (t > 4.5) {
166
+ const u = win(Math.floor(t * 12) / 12, 4.55, 5.25), n = u >= 1 ? 1e6 : (Math.pow(10, u * 6) - 1) / (1e6 - 1) * 1e6;
167
+ items.push({ k: 'text', s: fmt(n), x: cx0 + 150, y: cy0 + 425, size: 76, font: 'num', role: 'ink', a: -1, b: 0, align: 'center', g: 'B', inst: true, live: true });
168
+ text('examples', cx0 + 150, cy0 + 472, 36, 'label', 'ink', 4.55, 4.8, { align: 'center', g: 'B' });
169
+ }
170
+ if (L.model2At) L.model(MX, MY, mAdj, (it) => { it.g = 'M2'; if (!it.inst) { it.a += L.model2At - 2.5; it.b += L.model2At - 2.5; } items.push(it); }, { line: (p, r, w, a, b) => line(p, r, w, a + L.model2At - 2.5, b + L.model2At - 2.5, 'M2'), text: (s2, x, y, sz, f, r, a, b, o = {}) => text(s2, x, y, sz, f, r, a + L.model2At - 2.5, b + L.model2At - 2.5, { ...o, g: 'M2' }), dot }, t - (L.model2At - 2.5));
171
+ // ---- 3 GUESS -> CHECK -> ADJUST ----
172
+ const qx = MX, qy = MY - 300;
173
+ line(rrect(qx - 62, qy - 62, 124, 124, 12), 'acc2', 4, 6.4, 6.55, 'C');
174
+ cat(qx, qy - 4, 100).forEach((p) => line(p, 'ink', 3.2, 6.5, 6.7, 'C'));
175
+ eyes(qx, qy - 4, 100).forEach(([ex, ey]) => dot(ex, ey, 3, 'ink', 6.7, 'C'));
176
+ text('new!', qx + 72, qy - 30, 30, 'label', 'acc2', 6.65, 6.8, { g: 'C' });
177
+ const dn = [[qx, qy + 70], [qx + 2, MY - 170]];
178
+ line(dn, 'ink', 5, 6.8, 6.95, 'C'); line(head(dn, 18), 'ink', 5, 6.95, 7.02, 'C');
179
+ const gA = [[MX + 175, MY - 10], [MX + 300, MY - 14], [MX + 400, MY - 8]];
180
+ line(gA, 'ink', 5, 7.02, 7.2, 'C'); line(head(gA, 20), 'ink', 5, 7.2, 7.26, 'C');
181
+ text('Guess', MX + 290, MY - 40, 38, 'label', 'ink', 7.15, 7.4, { align: 'center', g: 'C' });
182
+ const BX = MX + 540, BY = MY - 10;
183
+ line(rrect(BX - 120, BY - 55, 240, 110, 40), 'ink', 5, 7.3, 7.5, 'C');
184
+ line([[BX - 70, BY + 52], [BX - 96, BY + 92], [BX - 40, BY + 54]], 'ink', 5, 7.48, 7.55, 'C');
185
+ text('dog?', BX, BY + 18, 52, 'title', 'ink', 7.52, 7.75, { align: 'center', g: 'C' });
186
+ text('Check', BX, BY - 82, 38, 'label', 'acc2', 7.85, 8.05, { align: 'center', g: 'C' });
187
+ line([[BX + 140, BY - 40], [BX + 200, BY + 30]], 'acc2', 8, 8.0, 8.08, 'C');
188
+ line([[BX + 200, BY - 40], [BX + 140, BY + 30]], 'acc2', 8, 8.08, 8.16, 'C');
189
+ line(circ(BX + 4, BY + 2, 150, 82, -2.2, 4.4), 'acc2', 5, 8.15, 8.45, 'C'); // emphasis ring
190
+ const back = cubic([BX - 60, BY + 70], [BX - 80, BY + 290], [MX + 290, MY + 300], [MX + 155, MY + 190]);
191
+ line(back, 'acc2', 6, 8.45, 8.95, 'C'); line(head(back, 22), 'acc2', 6, 8.95, 9.02, 'C');
192
+ text('Adjust', (BX + MX) / 2 - 10, MY + 315, 40, 'label', 'acc2', 8.8, 9.05, { align: 'center', g: 'C' });
193
+ if (t > 8.95 && t < 9.4) { // error signal travelling back
194
+ const u = win(t, 8.95, 9.35), p = back[Math.min(back.length - 1, Math.floor((1 - u) * (back.length - 1)))];
195
+ items.push({ k: 'dot', x: p[0], y: p[1], r: 13, role: 'acc2', a: -1, b: 0, g: 'C', inst: true });
196
+ }
197
+ line([[BX - 66, BY + 4], [BX - 20, BY - 6], [BX + 24, BY + 6], [BX + 66, BY - 4]], 'ink', 5, 9.5, 9.62, 'C'); // strike
198
+ text('cat', BX + 30, BY + 118, 52, 'title', 'acc3', 9.62, 9.85, { align: 'center', g: 'C' });
199
+ line([[BX + 92, BY + 96], [BX + 112, BY + 122], [BX + 156, BY + 66]], 'acc3', 8, 9.85, 10.05, 'C');
200
+ if (L.bridge) { // a connector the hand draws while the camera travels to the next part
201
+ const B = L.bridge;
202
+ line(B.pts, 'acc1', 5, B.a, B.b, 'D'); line(head(B.pts, 22), 'acc1', 5, B.b, B.b + 0.06, 'D');
203
+ text(B.label, B.lx, B.ly, 40, 'label', 'acc1', B.b + 0.06, B.b + 0.3, { align: 'center', rot: B.rot || 0, g: 'D' });
204
+ }
205
+ // ---- 4 PROGRESS ----
206
+ const D = L.D;
207
+ const ox = D[0] - 380, oy = D[1] + 150, cw = 560, ch = 330;
208
+ line([[ox, oy - ch], [ox, oy], [ox + cw, oy]], 'ink', 5, L.pT, L.pT + 0.35, 'D');
209
+ line(head([[ox, oy], [ox, oy - ch]], 16), 'ink', 5, L.pT + 0.35, L.pT + 0.4, 'D');
210
+ line(head([[ox, oy], [ox + cw, oy]], 16), 'ink', 5, L.pT + 0.4, L.pT + 0.45, 'D');
211
+ text('training rounds', ox + cw / 2, oy + 48, 34, 'label', 'ink', L.pT + 0.3, L.pT + 0.55, { align: 'center', g: 'D' });
212
+ const cpts = [];
213
+ for (let i = 0; i <= 40; i++) {
214
+ const u = i / 40, acc = 61 + 36 * (1 - Math.pow(1 - u, 2.2)) + (i % 3 === 1 ? -2.5 : i % 3 === 2 ? 1.5 : 0) * (1 - u);
215
+ cpts.push([ox + 20 + u * (cw - 50), oy - (acc - 50) / 50 * ch]);
216
+ }
217
+ line(cpts, 'acc1', 6, L.pT + 0.5, L.pT + 1.7, 'D');
218
+ text('accuracy', D[0] + 330, D[1] - 120, 40, 'label', 'ink', L.pT + 0.45, L.pT + 0.7, { align: 'center', g: 'D' });
219
+ if (t > L.pT + 0.55) {
220
+ const u = win(Math.floor(t * 12) / 12, L.pT + 0.6, L.pT + 1.7), v = 61 + 36 * (1 - Math.pow(1 - u, 2.2));
221
+ items.push({ k: 'text', s: Math.round(v) + '%', x: D[0] + 330, y: D[1] + 10, size: 110, font: 'num', role: u >= 1 ? 'acc3' : 'ink', a: -1, b: 0, align: 'center', g: 'D', inst: true, live: true });
222
+ }
223
+ line(circ(D[0] + 335, D[1] - 25, 140, 90, -2.4, 4.2), 'acc2', 6, L.pT + 1.75, L.pT + 2.05, 'D');
224
+ // ---- 5 TAKEAWAY ----
225
+ text('More examples → better guesses', D[0], D[1] + 290, 62, 'title', 'ink', L.kT, L.kT + 0.75, { align: 'center', rot: -0.01, g: 'E' });
226
+ line([[D[0] - 450, D[1] + 316], [D[0] + 455, D[1] + 308]], 'acc1', 6, L.kT + 0.75, L.kT + 0.95, 'E');
227
+ return items;
228
+ };
229
+ // the 'tip' of whatever is currently being drawn (for the hand/tool)
230
+ const tipOf = (items, t, measure) => {
231
+ let tip = null;
232
+ for (const it of items) {
233
+ if (it.inst || it.k === 'dot') continue;
234
+ const p = win(t, it.a, it.b);
235
+ if (p > 0 && p < 1) {
236
+ if (it.k === 'line') { const P = it.pts; tip = P[Math.min(P.length - 1, Math.floor(p * (P.length - 1)))]; }
237
+ else tip = measure ? measure(it, p) : [it.x, it.y];
238
+ }
239
+ }
240
+ return tip;
241
+ };
242
+ return { clamp, win, eio, camAt, circ, quad, cubic, rrect, head, cat, dog, story, tipOf, fmt };
243
+ })();
244
+ // <<<XE-ENGINE
245
+ registerStyle({
246
+ id: 'sketchbook-pencil',
247
+ name: 'Sketchbook Graphite',
248
+ category: 'Hand-Drawn',
249
+ description: 'A graphite study in a spiral sketchbook: faint construction circles and axes under confident darker contours, directional hatching that breaks on the paper tooth, smudged tones, kneaded-eraser highlights and a margin note.',
250
+ motion: 'Construction lines ghost in first, contours firm up over them, then hatching sweeps across the shadow side while the eraser lifts the highlight.',
251
+ fonts: ['700 56px "Caveat"', '400 34px "Caveat"', '400 26px "Caveat"', '700 30px "Caveat"'],
252
+ frameT: 6.4,
253
+ duration: 6.5,
254
+ readsPixels: true,
255
+ draw(ctx, W, H_, t) {
256
+ const nz = H.makeNoise(23);
257
+ const r = H.rng(99);
258
+ const PX = 74; // page left edge
259
+ const win = (a, b) => H.clamp((t - a) / (b - a));
260
+ const seq = (t0, dt) => { PH = { t, t0, dt, k: 0 }; };
261
+ PH = null; GATE = 1; TIP = null;
262
+
263
+ // ---------- desk + page ----------
264
+ const desk = ctx.createLinearGradient(0, 0, PX, 0);
265
+ desk.addColorStop(0, '#2a2420'); desk.addColorStop(1, '#3b332c');
266
+ ctx.fillStyle = desk; ctx.fillRect(0, 0, PX + 10, H_);
267
+ ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(PX - 6, 0, 10, H_);
268
+ const paper = H.layer(W, H_), pc = paper.getContext('2d');
269
+ H.paper(pc, W, H_, '#efe6d2', 31, 0.025);
270
+ ctx.drawImage(paper, PX, 0, W - PX, H_, PX, 0, W - PX, H_);
271
+ // page curvature near binding + warm lamp falloff
272
+ const cur = ctx.createLinearGradient(PX, 0, PX + 90, 0);
273
+ cur.addColorStop(0, 'rgba(90,70,40,0.28)'); cur.addColorStop(1, 'rgba(90,70,40,0)');
274
+ ctx.fillStyle = cur; ctx.fillRect(PX, 0, 90, H_);
275
+ const lamp = ctx.createRadialGradient(560, 220, 80, 640, 360, 900);
276
+ lamp.addColorStop(0, 'rgba(255,250,235,0.18)'); lamp.addColorStop(1, 'rgba(70,50,20,0.16)');
277
+ ctx.fillStyle = lamp; ctx.fillRect(PX, 0, W, H_);
278
+
279
+ // ---------- graphite layer ----------
280
+ const L = H.layer(W, H_), g = L.getContext('2d');
281
+ const S = H.layer(W, H_), sm = S.getContext('2d'); // smudge layer
282
+ const cx = 600, cy = 300, R = 172;
283
+ const draw = (fn) => fn();
284
+
285
+ // construction lines: light, long, overshooting
286
+ const con = (pts, s, a = 0.32, w = 1.2) => pencil(g, wob(seg(pts), nz, s, 1.0, 0.006), w, a, s, 0.05, 0.05, nz);
287
+ seq(0.05, 0.06);
288
+ con([[cx + 2, 70], [cx - 3, 672]], 1, 0.15);
289
+ con([[cx - 270, cy + 2], [cx + 280, cy - 3]], 2, 0.14);
290
+ con([[cx - 210, cy - 140], [cx + 230, cy + 160]], 3, 0.08);
291
+ for (let k = 0; k < 3; k++) {
292
+ const a0 = r.range(0, 6.28);
293
+ pencil(g, wob(ell(cx + r.range(-4, 4), cy + r.range(-4, 4), R + r.range(-6, 8), R + r.range(-6, 8), a0, a0 + 6.6), nz, 10 + k, 1.4, 0.02), 1.1, 0.2, 10 + k, 0.05, 0.05, nz);
294
+ }
295
+ con(ell(cx, 498, 64, 13), 4, 0.14);
296
+ con(ell(cx, 594, 58, 12), 5, 0.13);
297
+ con([[cx - 64, 460], [cx - 64, 600]], 6, 0.11);
298
+ con([[cx + 64, 460], [cx + 64, 600]], 7, 0.11);
299
+ con(ell(cx, cy, 76, 76), 8, 0.12);
300
+ con(ell(cx, cy, 58, 58), 9, 0.1);
301
+ // light source arrow guide (artist's note to self)
302
+
303
+ // ---------- tonal smudge (blended with finger/stump) ----------
304
+ {
305
+ sm.save();
306
+ sm.beginPath(); sm.arc(cx, cy, R, 0, 7); sm.clip();
307
+ const gg = sm.createRadialGradient(cx - 70, cy - 80, 20, cx + 20, cy + 20, R * 1.15);
308
+ gg.addColorStop(0, `rgba(${GR},0)`); gg.addColorStop(0.55, `rgba(${GR},0.1)`); gg.addColorStop(0.85, `rgba(${GR},0.32)`); gg.addColorStop(1, `rgba(${GR},0.2)`);
309
+ sm.fillStyle = gg; sm.fillRect(0, 0, W, H_);
310
+ sm.restore();
311
+ // cast shadow
312
+ sm.save(); sm.translate(cx + 120, 632); sm.scale(1, 0.13);
313
+ const cs = sm.createRadialGradient(0, 0, 10, 0, 0, 220);
314
+ cs.addColorStop(0, `rgba(${GR},0.5)`); cs.addColorStop(0.6, `rgba(${GR},0.22)`); cs.addColorStop(1, `rgba(${GR},0)`);
315
+ sm.fillStyle = cs; sm.beginPath(); sm.arc(0, 0, 220, 0, 7); sm.fill(); sm.restore();
316
+ // base tone
317
+ const bt = sm.createLinearGradient(cx - 64, 0, cx + 64, 0);
318
+ bt.addColorStop(0, `rgba(${GR},0.06)`); bt.addColorStop(0.6, `rgba(${GR},0.2)`); bt.addColorStop(1, `rgba(${GR},0.4)`);
319
+ sm.fillStyle = bt; sm.fillRect(cx - 64, 498, 128, 100);
320
+ }
321
+
322
+ // ---------- hatching driven by a light model ----------
323
+ const Ld = [-0.55, -0.62, 0.56];
324
+ const shadeAt = (x, y) => {
325
+ // sphere
326
+ let dx = (x - cx) / R, dy = (y - cy) / R, d2 = dx * dx + dy * dy;
327
+ if (d2 < 1) {
328
+ const nzv = Math.sqrt(1 - d2);
329
+ let lam = dx * Ld[0] + dy * Ld[1] + nzv * Ld[2];
330
+ let s = 1 - Math.max(0, lam);
331
+ // reflected light along the lower right rim
332
+ s -= 0.28 * H.smooth(0.8, 1, Math.sqrt(d2)) * H.smooth(0.2, 0.9, dx * 0.6 + dy * 0.8);
333
+ return s;
334
+ }
335
+ // screw base cylinder
336
+ if (y > 488 && y < 600 && Math.abs(x - cx) < 64) {
337
+ const u = (x - cx) / 64;
338
+ let s = 0.35 + 0.55 * u + 0.1;
339
+ const th = ((y - 500) % 24) / 24; // thread grooves
340
+ s += 0.25 * Math.max(0, Math.sin(th * Math.PI * 2 - 1));
341
+ return s;
342
+ }
343
+ // neck
344
+ if (y > 440 && y < 500 && Math.abs(x - cx) < 66 - (y - 440) * 0.05) return 0.5 + 0.5 * (x - cx) / 66;
345
+ return -1;
346
+ };
347
+ const hatchPass = (ang, sp, thr, a, w, seed, t0, t1) => {
348
+ const rr = H.rng(seed), ca = Math.cos(ang), sa = Math.sin(ang);
349
+ const ext = 520, oMax = -250 + 500 * H.ease.inOut(win(t0, t1));
350
+ for (let o = -ext; o < ext; o += sp * (0.8 + rr() * 0.4)) {
351
+ if (o > oMax) break;
352
+ // line through (cx,cy+60) offset o along normal
353
+ const ox = cx - sa * o, oy = cy + 120 + ca * o;
354
+ let run = [];
355
+ const flush = () => {
356
+ if (run.length > 4) {
357
+ // break long runs into hand-length flicks
358
+ let i = 0;
359
+ while (i < run.length - 3) {
360
+ const len = Math.floor(rr.range(9, 26));
361
+ const part = run.slice(i, i + len);
362
+ if (part.length > 3) {
363
+ const jx = rr.range(-1.5, 1.5), jy = rr.range(-1.5, 1.5);
364
+ pencil(g, part.map(([x, y]) => [x + jx, y + jy]), w, a * rr.range(0.75, 1.15), seed + i, 0.1, 0.45, nz);
365
+ }
366
+ i += len - Math.floor(rr.range(0, 3));
367
+ }
368
+ }
369
+ run = [];
370
+ };
371
+ for (let s = -ext; s < ext; s += 3) {
372
+ const x = ox + ca * s, y = oy + sa * s;
373
+ const v = shadeAt(x, y);
374
+ const jitter = nz.n2(x * 0.02, y * 0.02) * 0.06;
375
+ if (v > thr + jitter) run.push([x, y]); else flush();
376
+ }
377
+ flush();
378
+ }
379
+ };
380
+ PH = null; GATE = 1;
381
+ hatchPass(-0.85, 7.0, 0.34, 0.5, 1.5, 101, 2.0, 2.9);
382
+ hatchPass(0.32, 7.8, 0.6, 0.48, 1.45, 202, 2.6, 3.3);
383
+ hatchPass(-1.4, 8.5, 0.8, 0.5, 1.4, 303, 3.0, 3.6);
384
+ { // pencil follows the hatching sweep
385
+ const ph = [[2.0, 2.9, -0.85], [2.6, 3.3, 0.32], [3.0, 3.6, -1.4]].filter(([a, b]) => t > a && t < b).pop();
386
+ if (ph) { const u = win(ph[0], ph[1]), o = -250 + 500 * H.ease.inOut(u), ca = Math.cos(ph[2]), sa = Math.sin(ph[2]), s0 = -120 * sa + 40 + Math.sin(t * 40) * 45; TIP = [cx - sa * o + ca * s0, cy + 120 + ca * o + sa * s0]; const dx = TIP[0] - cx, dy = TIP[1] - cy, dd = Math.hypot(dx, dy), lim = R - 14; if (dd > lim) TIP = [cx + dx / dd * lim, cy + dy / dd * lim]; }
387
+ }
388
+
389
+ // lift the glass highlight with a kneaded eraser
390
+ const lift = (c, alpha) => {
391
+ c.save(); c.globalCompositeOperation = 'destination-out';
392
+ c.translate(cx - 82, cy - 86); c.rotate(-0.7);
393
+ const hg = c.createRadialGradient(0, 0, 4, 0, 0, 46), la = alpha * win(3.55, 3.9);
394
+ hg.addColorStop(0, `rgba(0,0,0,${la})`); hg.addColorStop(0.7, `rgba(0,0,0,${la * 0.8})`); hg.addColorStop(1, 'rgba(0,0,0,0)');
395
+ c.scale(1, 0.45); c.fillStyle = hg; c.beginPath(); c.arc(0, 0, 46, 0, 7); c.fill();
396
+ c.restore();
397
+ c.save(); c.globalCompositeOperation = 'destination-out';
398
+ c.beginPath(); c.arc(cx, cy, 77, 0, 7); c.fillStyle = `rgba(0,0,0,${alpha * 0.9})`; c.fill();
399
+ c.restore();
400
+ };
401
+ lift(g, 0.92); lift(sm, 0.85);
402
+ seq(0.75, 0.03);
403
+
404
+ // ---------- confident contours ----------
405
+ const contour = (pts, s, a = 0.75, w = 2.0, passes = 2) => {
406
+ for (let k = 0; k < passes; k++) {
407
+ const P = wob(seg(pts), nz, s + k * 17, 0.9 + k * 0.4, 0.012);
408
+ pencil(g, P.map(([x, y]) => [x + k * 0.8, y + k * 0.6]), w * (k ? 0.7 : 1), a * (k ? 0.55 : 1), s + k, 0.08, 0.15, nz);
409
+ }
410
+ };
411
+ // bulb: big arc from neck-left around top to neck-right
412
+ const aL = Math.PI / 2 + Math.asin(62 / R), aR = Math.PI / 2 - Math.asin(62 / R);
413
+ contour(ell(cx, cy, R, R, aL, aR + Math.PI * 2), 40, 0.75, 2.4);
414
+ // heavier accent on the shadow side
415
+ contour(ell(cx, cy, R + 1, R + 1, -0.3, 1.25), 41, 0.75, 2.8, 1);
416
+ // neck curves
417
+ const nyB = cy + Math.sqrt(R * R - 62 * 62);
418
+ contour(bez([cx - 62, nyB], [cx - 58, nyB + 14], [cx - 66, 478], [cx - 64, 498]), 42, 0.7, 2);
419
+ contour(bez([cx + 62, nyB], [cx + 58, nyB + 14], [cx + 66, 478], [cx + 64, 498]), 43, 0.8, 2.3);
420
+ // screw threads
421
+ for (let k = 0; k < 5; k++) {
422
+ const y = 500 + k * 22;
423
+ contour(ell(cx, y, 64 - k * 0.6, 12, 0.1, Math.PI - 0.1), 50 + k, 0.62, 1.8, 1);
424
+ contour(ell(cx, y + 11, 66 - k * 0.6, 12, 0.05, Math.PI - 0.05), 60 + k, 0.4, 1.3, 1);
425
+ }
426
+ contour([[cx - 64, 498], [cx - 61, 598]], 70, 0.6, 1.8);
427
+ contour([[cx + 64, 498], [cx + 61, 598]], 71, 0.8, 2.4);
428
+ contour(bez([cx - 60, 606], [cx - 46, 622], [cx - 30, 626], [cx - 22, 628]), 72, 0.7, 2);
429
+ contour(bez([cx + 60, 606], [cx + 46, 622], [cx + 30, 626], [cx + 22, 628]), 73, 0.8, 2.2);
430
+ contour(ell(cx, 630, 22, 8, 0, Math.PI), 74, 0.85, 2.6, 1);
431
+ // filament supports up into the gear
432
+ contour([[cx - 14, 498], [cx - 18, 420], [cx - 30, 362]], 75, 0.55, 1.4, 1);
433
+ contour([[cx + 14, 498], [cx + 18, 420], [cx + 32, 362]], 76, 0.6, 1.4, 1);
434
+ contour([[cx, 500], [cx, 376]], 77, 0.45, 1.3, 1);
435
+
436
+ // gear inside the bulb
437
+ const teeth = 11, Ro = 76, Ri = 63;
438
+ const gp = [];
439
+ for (let i = 0; i <= teeth * 4; i++) {
440
+ const k = i % 4, base = Math.floor(i / 4) * (Math.PI * 2 / teeth) + 0.2;
441
+ const a = base + [0, 0.07, 0.24, 0.31][k] * (Math.PI * 2 / teeth) * 1.6;
442
+ const rr = k === 1 || k === 2 ? Ro : Ri;
443
+ gp.push([cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]);
444
+ }
445
+ contour(gp, 80, 0.8, 1.9);
446
+ contour(ell(cx, cy, 44, 44, 0.3, 6.7), 81, 0.6, 1.6);
447
+ contour(ell(cx, cy, 15, 15, 1, 7.4), 82, 0.85, 2.2, 1);
448
+ for (let k = 0; k < 3; k++) {
449
+ const a = k * 2.094 + 0.5;
450
+ contour([[cx + Math.cos(a) * 17, cy + Math.sin(a) * 17], [cx + Math.cos(a) * 43, cy + Math.sin(a) * 43]], 83 + k, 0.6, 1.5, 1);
451
+ }
452
+ // gear shading (lower right of ring), tight hatching
453
+ {
454
+ const rr = H.rng(404);
455
+ const nG = Math.floor(120 * win(3.3, 3.6));
456
+ PH = null; GATE = 1;
457
+ for (let i = 0; i < nG; i++) {
458
+ const a = rr.range(-0.4, 2.0), rad = rr.range(46, 74);
459
+ const x = cx + Math.cos(a) * rad, y = cy + Math.sin(a) * rad;
460
+ pencil(g, seg([[x - 5, y + 6], [x + 5, y - 6]], 2), 1.3, 0.55, 900 + i, 0.1, 0.4, nz);
461
+ }
462
+ }
463
+
464
+ // ---------- gear study, top right ----------
465
+ const gx = 1052, gy = 205;
466
+ seq(3.6, 0.035);
467
+ con([[gx - 110, gy], [gx + 110, gy]], 90, 0.13);
468
+ con([[gx, gy - 105], [gx, gy + 105]], 91, 0.13);
469
+ con(ell(gx, gy, 82, 82, 0, 6.5), 92, 0.12);
470
+ con(ell(gx, gy, 66, 66, 0, 6.5), 93, 0.11);
471
+ con([[gx - 80, gy - 80], [gx + 80, gy + 80]], 94, 0.08);
472
+ // half-finished: teeth only along the top arc
473
+ const tp = [];
474
+ for (let i = 0; i <= 7 * 4; i++) {
475
+ const k = i % 4, base = -Math.PI - 0.1 + Math.floor(i / 4) * (Math.PI * 2 / 14);
476
+ const a = base + [0, 0.12, 0.38, 0.5][k] * (Math.PI * 2 / 14);
477
+ tp.push([gx + Math.cos(a) * (k === 1 || k === 2 ? 82 : 66), gy + Math.sin(a) * (k === 1 || k === 2 ? 82 : 66)]);
478
+ }
479
+ contour(tp, 95, 0.62, 1.6);
480
+ contour(ell(gx, gy, 22, 22, 0.4, 6.8), 96, 0.6, 1.6, 1);
481
+
482
+ seq(3.95, 0.006);
483
+ // ---------- thumbnails (quick variants) ----------
484
+ const thumb = (tx, ty, sc, s, variant) => {
485
+ con(ell(tx, ty, 44 * sc, 44 * sc, 0, 6.6), s, 0.22);
486
+ con([[tx, ty - 60 * sc], [tx, ty + 110 * sc]], s + 1, 0.18);
487
+ if (variant === 0) contour(ell(tx, ty, 44 * sc, 44 * sc, 2.1, 7.32), s + 2, 0.6, 1.5, 1);
488
+ else {
489
+ contour(bez([tx - 22 * sc, ty + 38 * sc], [tx - 70 * sc, ty - 20 * sc], [tx - 30 * sc, ty - 62 * sc], [tx, ty - 60 * sc]), s + 2, 0.6, 1.5, 1);
490
+ contour(bez([tx, ty - 60 * sc], [tx + 30 * sc, ty - 62 * sc], [tx + 70 * sc, ty - 20 * sc], [tx + 22 * sc, ty + 38 * sc]), s + 9, 0.6, 1.5, 1);
491
+ }
492
+ contour([[tx - 22 * sc, ty + 38 * sc], [tx - 20 * sc, ty + 80 * sc], [tx + 20 * sc, ty + 80 * sc], [tx + 22 * sc, ty + 38 * sc]], s + 3, 0.6, 1.4, 1);
493
+ for (let k = 0; k < 3; k++) contour([[tx - 21 * sc, ty + (50 + k * 10) * sc], [tx + 21 * sc, ty + (52 + k * 10) * sc]], s + 4 + k, 0.45, 1.1, 1);
494
+ const rr = H.rng(s);
495
+ for (let i = 0; i < 16; i++) {
496
+ const a = rr.range(-0.5, 1.4), rad = rr.range(20, 42) * sc;
497
+ const x = tx + Math.cos(a) * rad, y = ty + Math.sin(a) * rad;
498
+ pencil(g, seg([[x - 6, y + 7], [x + 6, y - 7]], 2), 1.2, 0.45, s * 10 + i, 0.1, 0.4, nz);
499
+ }
500
+ };
501
+ thumb(232, 300, 1.0, 600, 0);
502
+ thumb(232, 500, 1.0, 620, 1);
503
+ con([[160, 216], [310, 214], [312, 620], [158, 622], [160, 210]], 640, 0.2);
504
+
505
+ // ---------- value scale ----------
506
+ {
507
+ const vx = 905, vy = 588, bw = 52;
508
+ for (let k = 0; k < 5; k++) {
509
+ contour([[vx + k * bw, vy], [vx + k * bw + bw, vy + 1]], 700 + k, 0.45, 1.1, 1);
510
+ contour([[vx + k * bw, vy + 40], [vx + k * bw + bw, vy + 41]], 710 + k, 0.45, 1.1, 1);
511
+ contour([[vx + k * bw, vy - 2], [vx + k * bw + 1, vy + 43]], 720 + k, 0.45, 1.1, 1);
512
+ const rr = H.rng(730 + k), dens = [0, 9, 6, 4.2, 3][k];
513
+ if (dens) for (let o = 4; o < bw + 36; o += dens) {
514
+ const x0 = vx + k * bw + o, pts = [];
515
+ for (let q = 0; q <= 14; q++) { const u = q / 14; pts.push([x0 - u * 40, vy + 3 + u * 35]); }
516
+ const clipped = pts.filter(([x]) => x > vx + k * bw + 2 && x < vx + k * bw + bw - 2);
517
+ if (clipped.length > 2) pencil(g, clipped, 1.3, 0.35 + k * 0.1, 740 + k * 50 + o, 0.05, 0.2, nz);
518
+ if (k >= 3 && clipped.length > 2) pencil(g, clipped.map(([x, y]) => [2 * (vx + k * bw) + bw - x, y]), 1.2, 0.4, 760 + o, 0.05, 0.2, nz);
519
+ }
520
+ }
521
+ contour([[vx + bw * 5, vy - 2], [vx + bw * 5 + 1, vy + 42]], 729, 0.45, 1.1, 1);
522
+ }
523
+
524
+ // ---------- lettering ----------
525
+ const write = (str, x, y, font, a, rot, seed, t0 = 0, t1 = 0.001) => {
526
+ g.save(); g.font = font; g.translate(x, y); g.rotate(rot);
527
+ const rr = H.rng(seed); let px = 0;
528
+ const nCh = Math.ceil(str.length * win(t0, t1));
529
+ if (nCh > 0 && nCh < str.length) { const ww = g.measureText(str.slice(0, nCh)).width; TIP = [x + Math.cos(rot) * ww, y + Math.sin(rot) * ww - 8]; }
530
+ let ci = 0;
531
+ for (const ch of str) {
532
+ if (ci++ >= nCh) break;
533
+ const w = g.measureText(ch).width;
534
+ g.fillStyle = `rgba(${GR},${a * rr.range(0.8, 1.05)})`;
535
+ g.save(); g.translate(px, rr.range(-1, 1)); g.rotate(rr.range(-0.03, 0.03)); g.fillText(ch, 0, 0); g.restore();
536
+ px += w;
537
+ }
538
+ g.restore();
539
+ return px;
540
+ };
541
+ PH = null;
542
+ const tw = write('the idea engine', 130, 104, '700 56px "Caveat"', 0.82, -0.025, 7, 4.25, 4.75);
543
+ GATE = win(4.75, 4.85);
544
+ pencil(g, wob(seg([[128, 120], [128 + 290 * 0.98, 113]]), nz, 300, 1.6, 0.01), 1.6, 0.55, 300, 0.05, 0.4, nz);
545
+ GATE = 1;
546
+ write('sketch no. 14', 134, 148, '400 26px "Caveat"', 0.5, -0.02, 8, 4.85, 5.0);
547
+ // margin note with arrow to the gear
548
+ write('more teeth?', 880, 420, '400 34px "Caveat"', 0.72, -0.05, 10, 3.9, 4.1);
549
+ write('make it spin!', 892, 456, '400 34px "Caveat"', 0.72, -0.04, 11, 4.1, 4.25);
550
+ seq(3.75, 0.04);
551
+ contour(bez([872, 412], [800, 400], [740, 360], [688, 330]), 97, 0.6, 1.5, 1);
552
+ contour([[688, 330], [702, 330]], 98, 0.6, 1.5, 1);
553
+ contour([[688, 330], [697, 341]], 99, 0.6, 1.5, 1);
554
+ PH = null;
555
+ write('14T?', gx - 26, gy + 128, '400 30px "Caveat"', 0.6, 0.02, 12, 3.85, 3.9);
556
+ write('A', 166, 258, '700 30px "Caveat"', 0.6, 0, 13, 4.0, 4.02);
557
+ write('B', 166, 458, '700 30px "Caveat"', 0.6, 0, 14, 4.12, 4.14);
558
+ write('values', 905, 656, '400 26px "Caveat"', 0.55, 0.01, 15, 4.22, 4.3);
559
+
560
+ // ---------- eraser passes (partial lifts) ----------
561
+ {
562
+ const er = (x, y, len, ang, wd, a) => {
563
+ [g, sm].forEach((c) => {
564
+ c.save(); c.globalCompositeOperation = 'destination-out'; c.lineCap = 'round';
565
+ c.strokeStyle = `rgba(0,0,0,${a * win(3.45, 3.6)})`; c.lineWidth = wd; c.translate(x, y); c.rotate(ang);
566
+ c.beginPath(); c.moveTo(-len / 2, 0); c.quadraticCurveTo(0, 4, len / 2, -2); c.stroke();
567
+ c.restore();
568
+ });
569
+ // smeared graphite left at the edges of the eraser path
570
+ sm.save(); sm.translate(x, y); sm.rotate(ang); sm.filter = 'blur(3px)';
571
+ sm.strokeStyle = `rgba(${GR},${0.07 * win(3.45, 3.6)})`; sm.lineWidth = wd * 1.4; sm.lineCap = 'round';
572
+ sm.beginPath(); sm.moveTo(-len / 2, 0); sm.quadraticCurveTo(0, 4, len / 2, -2); sm.stroke(); sm.restore();
573
+ };
574
+ er(cx + 6, 96, 90, 0.08, 16, 0.78);
575
+ er(cx + 250, cy - 6, 70, -0.05, 14, 0.7);
576
+ er(cx - 250, cy + 4, 60, 0.1, 18, 0.6);
577
+ er(cx - 150, cy + 150, 80, 0.6, 14, 0.55);
578
+ }
579
+
580
+ // ---------- tooth: graphite only bites the paper's peaks ----------
581
+ {
582
+ const tooth = new Float32Array(W * H_);
583
+ const tc = H.layer(W, H_), tx = tc.getContext('2d');
584
+ const small = H.layer(W / 2, H_ / 2), sx = small.getContext('2d');
585
+ const id = sx.createImageData(W / 2, H_ / 2), rr = H.rng(5);
586
+ for (let i = 0; i < id.data.length; i += 4) { const v = rr() * 255; id.data[i] = id.data[i + 1] = id.data[i + 2] = v; id.data[i + 3] = 255; }
587
+ sx.putImageData(id, 0, 0);
588
+ tx.imageSmoothingEnabled = true; tx.drawImage(small, 0, 0, W, H_);
589
+ const td = tx.getImageData(0, 0, W, H_).data, r2 = H.rng(6);
590
+ for (let i = 0, j = 0; i < tooth.length; i++, j += 4) tooth[i] = td[j] / 255 * 0.65 + r2() * 0.35;
591
+ const apply = (c, hard) => {
592
+ const im = c.getImageData(0, 0, W, H_), d = im.data;
593
+ for (let i = 0, j = 3; i < tooth.length; i++, j += 4) {
594
+ const a = d[j] / 255; if (!a) continue;
595
+ const need = 1 - Math.min(1, a * hard); // light pressure -> only peaks catch
596
+ const k = H.clamp((tooth[i] - need * 0.75) * 3.2 + 0.15);
597
+ d[j] = 255 * a * (0.25 + 0.75 * k);
598
+ }
599
+ c.putImageData(im, 0, 0);
600
+ };
601
+ apply(g, 1.4); apply(sm, 2.2);
602
+ }
603
+ ctx.save(); ctx.globalAlpha = H.smooth(2.2, 3.7, t); ctx.drawImage(S, 0, 0); ctx.restore();
604
+ ctx.drawImage(L, 0, 0);
605
+
606
+ // ---------- eraser crumbs ----------
607
+ {
608
+ const rr = H.rng(808);
609
+ for (let i = 0; i < (t > 3.6 ? 9 : 0); i++) {
610
+ const x = rr.range(360, 480), y = rr.range(600, 680), l = rr.range(9, 18), a = rr.range(0, 3);
611
+ ctx.save(); ctx.translate(x, y); ctx.rotate(a);
612
+ ctx.fillStyle = 'rgba(60,40,30,0.22)'; ctx.beginPath(); ctx.ellipse(1.5, 2.5, l / 2, 3.4, 0, 0, 7); ctx.fill();
613
+ const cg = ctx.createLinearGradient(0, -3, 0, 3); cg.addColorStop(0, '#e9d7cf'); cg.addColorStop(1, '#a99791');
614
+ ctx.fillStyle = cg; ctx.beginPath(); ctx.ellipse(0, 0, l / 2, 3.1, 0, 0, 7); ctx.fill();
615
+ ctx.fillStyle = 'rgba(70,70,80,0.35)'; ctx.beginPath(); ctx.ellipse(l * 0.1, 0.6, l / 4, 1, 0, 0, 7); ctx.fill();
616
+ ctx.restore();
617
+ }
618
+ }
619
+
620
+ // ---------- spiral binding ----------
621
+ for (let y = 22; y < H_; y += 34) {
622
+ // punched hole (shows the desk)
623
+ ctx.fillStyle = '#2b2520'; ctx.beginPath(); ctx.ellipse(PX + 22, y, 6, 7.5, 0, 0, 7); ctx.fill();
624
+ ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.ellipse(PX + 22, y + 0.8, 6, 7.5, 0, 0.2, 2.9); ctx.stroke();
625
+ // coil shadow on page
626
+ ctx.strokeStyle = 'rgba(40,30,20,0.25)'; ctx.lineWidth = 5; ctx.lineCap = 'round';
627
+ ctx.beginPath(); ctx.moveTo(PX + 24, y + 5); ctx.quadraticCurveTo(PX - 6, y + 2, PX - 22, y + 12); ctx.stroke();
628
+ // wire
629
+ const wg = ctx.createLinearGradient(0, y - 6, 0, y + 6);
630
+ wg.addColorStop(0, '#f2f2f2'); wg.addColorStop(0.45, '#9a9ca0'); wg.addColorStop(1, '#3e3f42');
631
+ ctx.strokeStyle = wg; ctx.lineWidth = 4.2;
632
+ ctx.beginPath(); ctx.moveTo(PX + 22, y - 2); ctx.quadraticCurveTo(PX - 4, y - 14, PX - 26, y + 2); ctx.stroke();
633
+ ctx.strokeStyle = 'rgba(255,255,255,0.7)'; ctx.lineWidth = 1;
634
+ ctx.beginPath(); ctx.moveTo(PX + 16, y - 6); ctx.quadraticCurveTo(PX - 4, y - 14, PX - 20, y - 6); ctx.stroke();
635
+ }
636
+
637
+ // ---------- the pencil in hand while drawing ----------
638
+ if (TIP && t < 5.0) {
639
+ const [tx, ty] = TIP, ang = 0.72, len = 300;
640
+ ctx.save();
641
+ ctx.save(); ctx.filter = 'blur(7px)'; ctx.fillStyle = 'rgba(50,35,15,0.28)';
642
+ ctx.translate(tx + 22, ty + 26); ctx.rotate(ang + 0.05); ctx.beginPath(); ctx.roundRect(30, -8, len, 16, 6); ctx.fill(); ctx.restore();
643
+ ctx.translate(tx, ty); ctx.rotate(ang);
644
+ ctx.fillStyle = '#2c2c30'; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(12, -2.6); ctx.lineTo(12, 2.6); ctx.closePath(); ctx.fill();
645
+ const wg = ctx.createLinearGradient(0, -8, 0, 8); wg.addColorStop(0, '#f1d9b0'); wg.addColorStop(1, '#b98e5a');
646
+ ctx.fillStyle = wg; ctx.beginPath(); ctx.moveTo(11, -2.8); ctx.lineTo(40, -8); ctx.lineTo(40, 8); ctx.lineTo(11, 2.8); ctx.closePath(); ctx.fill();
647
+ const bg = ctx.createLinearGradient(0, -8, 0, 8); bg.addColorStop(0, '#5f8c6a'); bg.addColorStop(0.35, '#2f5a3c'); bg.addColorStop(0.36, '#24472f'); bg.addColorStop(1, '#16301f');
648
+ ctx.fillStyle = bg; ctx.fillRect(40, -8, len - 40, 16);
649
+ ctx.fillStyle = 'rgba(220,190,110,0.8)'; ctx.font = '600 8px "IBM Plex Mono", monospace'; ctx.fillText('2B', 120, -1);
650
+ ctx.restore();
651
+ }
652
+
653
+ // ---------- slow pull-back of the camera over the build ----------
654
+ {
655
+ const k = 1 + 0.05 * (1 - H.ease.inOut(win(0, 5.2)));
656
+ if (k > 1.0005) {
657
+ const cv = ctx.canvas, snap = H.layer(cv.width, cv.height);
658
+ snap.getContext('2d').drawImage(cv, 0, 0);
659
+ ctx.save(); ctx.translate(W * 0.47, H_ * 0.45); ctx.scale(k, k); ctx.translate(-W * 0.47, -H_ * 0.45);
660
+ ctx.drawImage(snap, 0, 0, W, H_); ctx.restore();
661
+ }
662
+ }
663
+ H.grain(ctx, W, H_, 0.035, 3 + (Math.floor(t * 12) % 3));
664
+ H.vignette(ctx, W, H_, 0.22, '40,28,10');
665
+ },
666
+ // >>>EXPLAINER
667
+ explainerDuration: 15,
668
+ explainerNote: 'A pencil works across a wide sketchbook spread (faint construction pass first, then a firm contour, crescent hatching for form) while the camera follows, then the page physically turns to a fresh sheet for the progress chart; numbers are pencilled and re-pencilled as they count.',
669
+ explainer(ctx, W, H_, t) {
670
+ const nz = H.makeNoise(23);
671
+ const D = [2560, 640];
672
+ const TURN0 = 10.35, TURN1 = 11.0;
673
+ const L = {
674
+ D, pT: 11.05, kT: 13.05, modelName: 'the model',
675
+ model: (cx, cy, adj, push, api, tt) => {
676
+ const g = 'M', by = cy - 35, R = 118;
677
+ const aL = Math.PI / 2 + Math.asin(55 / R), aR = Math.PI / 2 - Math.asin(55 / R);
678
+ api.line(XE.circ(cx, by, R, R, aL, aR + Math.PI * 2), 'ink', 5, 2.5, 2.85, g);
679
+ const ny = by + Math.sqrt(R * R - 55 * 55);
680
+ api.line([[cx - 55, ny], [cx - 52, cy + 110]], 'ink', 4.5, 2.85, 2.92, g);
681
+ api.line([[cx + 55, ny], [cx + 52, cy + 110]], 'ink', 4.5, 2.92, 2.99, g);
682
+ for (let k = 0; k < 3; k++) api.line([[cx - 54, cy + 112 + k * 18], [cx + 54, cy + 116 + k * 18]], 'ink', 4, 2.99 + k * 0.03, 3.03 + k * 0.03, g);
683
+ api.line([[cx - 40, cy + 166], [cx, cy + 176], [cx + 40, cy + 166]], 'ink', 4, 3.1, 3.15, g);
684
+ // gear inside the bulb: it turns as the model adjusts
685
+ const rot = adj * 1.6 + (tt > 9.0 && tt < 9.6 ? 0 : 0), gp = [], teeth = 10, Ro = 60, Ri = 48;
686
+ for (let i = 0; i <= teeth * 4; i++) {
687
+ const k = i % 4, base = Math.floor(i / 4) * (Math.PI * 2 / teeth) + rot;
688
+ const a = base + [0, 0.07, 0.24, 0.31][k] * (Math.PI * 2 / teeth) * 1.6, r = k === 1 || k === 2 ? Ro : Ri;
689
+ gp.push([cx + Math.cos(a) * r, by + Math.sin(a) * r]);
690
+ }
691
+ if (adj > 0) push({ k: 'line', pts: gp, role: adj < 1 ? 'acc2' : 'ink', w: 4, a: -1, b: 0, inst: true, g });
692
+ else api.line(gp, 'ink', 4, 3.15, 3.4, g);
693
+ api.line(XE.circ(cx, by, 16, 16, 0.4, 6.8), 'ink', 4, 3.35, 3.42, g);
694
+ // two tuning dials either side of the base
695
+ [-1, 1].forEach((s, i) => {
696
+ const x = cx + s * 120, y = cy + 135;
697
+ api.line(XE.circ(x, y, 24, 24, -1.2, 5.2), 'ink', 4, 3.2 + i * 0.06, 3.3 + i * 0.06, g);
698
+ if (tt > 3.32 + i * 0.06) {
699
+ const a0 = [-2.4, -0.6][i], a1 = [-0.7, -2.2][i], a = a0 + (a1 - a0) * adj + Math.sin(adj * Math.PI) * 0.3;
700
+ push({ k: 'line', pts: [[x, y], [x + Math.cos(a) * 19, y + Math.sin(a) * 19]], role: adj > 0 && adj < 1 ? 'acc2' : 'ink', w: 4.5, a: -1, b: 0, inst: true, g });
701
+ }
702
+ });
703
+ },
704
+ };
705
+ const items = XE.story(t, L);
706
+ { // smooth the progress curve's per-round jitter into a hand-drawn rise (same points, same timing)
707
+ const ci = items.find((it) => it.g === 'D' && it.k === 'line' && it.role === 'acc1' && it.pts.length > 30);
708
+ if (ci) {
709
+ let P = ci.pts;
710
+ for (const kk of [[1 / 3, 1 / 3, 1 / 3], [0.25, 0.5, 0.25]]) P = P.map((q, j) => (j === 0 || j === P.length - 1 ? q : [P[j - 1][0] * kk[0] + q[0] * kk[1] + P[j + 1][0] * kk[2], P[j - 1][1] * kk[0] + q[1] * kk[1] + P[j + 1][1] * kk[2]]));
711
+ ci.pts = P;
712
+ }
713
+ }
714
+ const camKeys = [[0, 590, 360, 1.0], [2.0, 610, 362, 1.04], [2.8, 1830, 610, 0.95], [5.95, 1860, 620, 0.97], [6.55, 2580, 640, 0.9], [10.35, 2600, 645, 0.93],
715
+ [11.0, 2560, 660, 0.92], [12.9, 2570, 670, 0.95], [13.6, 2560, 760, 0.88], [15, 2560, 760, 0.9]];
716
+ const cam = XE.camAt(camKeys, Math.min(t, TURN0)); // page 1 freezes once the page starts turning
717
+ const cam2 = XE.camAt(camKeys, Math.max(t, TURN0 + 0.3));
718
+
719
+ if (!XC.page) { const pl = H.layer(1100, 620), pc = pl.getContext('2d'); H.paper(pc, 1100, 620, '#efe6d2', 31, 0.025); XC.page = pl; }
720
+ const GR = '38,38,44';
721
+ // ---- static, page-anchored caches: paper tooth (as a lookup tile + a grain pattern) ----
722
+ if (!XC.tooth) {
723
+ const TS = 512, T = new Float32Array(TS * TS);
724
+ for (let y = 0; y < TS; y++) for (let x = 0; x < TS; x++) {
725
+ let h = Math.imul(x, 374761393) + Math.imul(y, 668265263); h = Math.imul(h ^ (h >>> 13), 1274126177); h = ((h ^ (h >>> 16)) >>> 0) / 4294967296;
726
+ T[y * TS + x] = h * 0.4 + (nz.n2(x * 0.35, y * 0.35) * 0.5 + 0.5) * 0.6;
727
+ }
728
+ XC.tooth = T;
729
+ const tc = H.layer(256, 256), tx = tc.getContext('2d'), id = tx.createImageData(256, 256);
730
+ for (let i = 0; i < 256 * 256; i++) id.data[i * 4 + 3] = 255 * H.clamp((T[((i >> 8) << 9) | (i & 255)] - 0.42) * 2.4);
731
+ tx.putImageData(id, 0, 0); XC.toothCv = tc;
732
+ }
733
+ // ---- world-space sprites (built once, then placed with the camera transform) ----
734
+ // tooth(af, wx, wy): graphite alpha after the paper's peaks take it (wx, wy in page units)
735
+ const toothK = (af, ti) => { const need = 1 - Math.min(1, af * 1.4); return 0.3 + 0.7 * H.clamp((XC.tooth[ti] - need * 0.75) * 3.2 + 0.15); };
736
+ const sprite = (key, x0, y0, w, h, fn, tooth = true) => {
737
+ const S = XC.spr || (XC.spr = {});
738
+ if (!S[key]) {
739
+ const cw = Math.ceil(w), ch = Math.ceil(h), cv = H.layer(cw, ch), c = cv.getContext('2d');
740
+ c.translate(-x0, -y0); const ph = PH, ga = GATE; PH = null; GATE = 1; fn(c); PH = ph; GATE = ga;
741
+ if (tooth) { // bake the page-anchored tooth in once, so the per-frame pass can skip these pixels
742
+ const id = c.getImageData(0, 0, cw, ch), d = id.data;
743
+ for (let y = 0, i = 3; y < ch; y++) {
744
+ const row = (((y0 + y) * 1.4 | 0) & 511) << 9;
745
+ for (let x = 0; x < cw; x++, i += 4) if (d[i]) d[i] = d[i] * toothK(d[i] / 255, row | (((x0 + x) * 1.4 | 0) & 511));
746
+ }
747
+ c.putImageData(id, 0, 0);
748
+ }
749
+ S[key] = { cv, x0, y0 };
750
+ }
751
+ return S[key];
752
+ };
753
+ const putS = (c, s) => c.drawImage(s.cv, s.x0, s.y0);
754
+ // hero-style hatching: parallel lines through a pivot, kept where shade() > threshold, broken into hand-length flicks
755
+ const hatchW = (c, shade, px, py, ang, sp, thr, a, w, seed, ext) => {
756
+ const rr = H.rng(seed), ca = Math.cos(ang), sa = Math.sin(ang);
757
+ for (let o = -ext; o < ext; o += sp * (0.8 + rr() * 0.4)) {
758
+ const ox = px - sa * o, oy = py + ca * o; let run = [];
759
+ const flush = () => {
760
+ if (run.length > 4) {
761
+ let i = 0;
762
+ while (i < run.length - 3) {
763
+ const len = Math.floor(rr.range(8, 22)), part = run.slice(i, i + len);
764
+ if (part.length > 3) { const jx = rr.range(-1.4, 1.4), jy = rr.range(-1.4, 1.4); pencil(c, part.map(([x, y]) => [x + jx, y + jy]), w, a * rr.range(0.75, 1.15), seed + i, 0.1, 0.45, nz); }
765
+ i += len - Math.floor(rr.range(0, 3));
766
+ }
767
+ }
768
+ run = [];
769
+ };
770
+ for (let s = -ext; s < ext; s += 3) {
771
+ const x = ox + ca * s, y = oy + sa * s;
772
+ if (shade(x, y) > thr + nz.n2(x * 0.02, y * 0.02) * 0.06) run.push([x, y]); else flush();
773
+ }
774
+ flush();
775
+ }
776
+ };
777
+ // reveal a sprite behind a hatch-direction sweep (u 0..1)
778
+ const sweep = (c, s, px, py, ang, u, lo, hi) => {
779
+ if (u <= 0) return;
780
+ c.save();
781
+ if (u < 1) { c.translate(px, py); c.rotate(ang); c.beginPath(); c.rect(-900, lo - 900, 1800, 900 + (hi - lo) * u); c.rotate(-ang); c.translate(-px, -py); c.clip(); }
782
+ putS(c, s); c.restore();
783
+ };
784
+ // two-pass contour with pressure: heavier on the shadow (lower-right) side, lighter where the light hits
785
+ const pstroke = (c, P, w, a, seed, gate, bb, inst) => {
786
+ const N = P.length; if (N < 2 || gate <= 0) return;
787
+ const n = gate < 1 ? Math.max(2, Math.ceil(N * gate)) : N;
788
+ const mx = (bb[0] + bb[1]) / 2, my = (bb[2] + bb[3]) / 2, hx = Math.max(30, (bb[1] - bb[0]) / 2), hy = Math.max(30, (bb[3] - bb[2]) / 2);
789
+ c.lineCap = 'butt'; c.lineJoin = 'round';
790
+ for (let k = 0; k < (inst ? 1 : 2); k++) {
791
+ const dx = k ? 0.9 : 0, dy = k ? 0.7 : 0, wk = k ? 0.6 : 1, ak = k ? 0.42 : 1;
792
+ for (let i = 0; i < n - 1; i += 3) {
793
+ const u = i / (N - 1);
794
+ let pr = Math.min(1, u / 0.08, (1 - u) / 0.15); pr = 0.3 + 0.7 * Math.sqrt(Math.max(0, pr));
795
+ const x = P[i][0], y = P[i][1];
796
+ const side = H.clamp((x - mx) / hx * 0.6 + (y - my) / hy * 0.8, -1, 1);
797
+ const press = 0.8 + 0.24 * side + 0.14 * nz.n2(i * 0.05 + k * 3.1, seed * 0.11);
798
+ c.strokeStyle = `rgba(${GR},${Math.min(1, a * ak * pr * press)})`;
799
+ c.lineWidth = w * wk * (0.5 + 0.5 * pr) * (0.75 + 0.4 * press);
800
+ c.beginPath(); c.moveTo(x + dx, y + dy); c.lineTo(P[i + 1][0] + dx, P[i + 1][1] + dy);
801
+ if (i + 2 < n) c.lineTo(P[i + 2][0] + dx, P[i + 2][1] + dy);
802
+ if (i + 3 < n) c.lineTo(P[i + 3][0] + dx, P[i + 3][1] + dy);
803
+ c.stroke();
804
+ }
805
+ }
806
+ };
807
+ // ---- the model bulb (geometry mirrors L.model): hatching + stump tone, lit from the top-left ----
808
+ const MX = 2250, MY = 640, BBX = MX, BBY = MY - 35, BR = 118, Ld = [-0.55, -0.62, 0.56];
809
+ const bulbShade = (x, y) => {
810
+ if (Math.hypot(x - BBX, y - BBY) < 68) return -1; // the gear stays clean (lifted with the eraser)
811
+ const dx = (x - BBX) / BR, dy = (y - BBY) / BR, d2 = dx * dx + dy * dy;
812
+ if (d2 < 1) {
813
+ const lam = dx * Ld[0] + dy * Ld[1] + Math.sqrt(1 - d2) * Ld[2];
814
+ return 1 - Math.max(0, lam) - 0.28 * H.smooth(0.8, 1, Math.sqrt(d2)) * H.smooth(0.2, 0.9, dx * 0.6 + dy * 0.8);
815
+ }
816
+ if (y < MY + 110 && y > BBY + 90 && Math.abs(x - BBX) < 53) return 0.45 + 0.5 * (x - BBX) / 55;
817
+ if (y >= MY + 110 && y < MY + 166 && Math.abs(x - BBX) < 52) {
818
+ const th = ((y - (MY + 112)) % 18) / 18;
819
+ return 0.42 + 0.55 * (x - BBX) / 54 + 0.25 * Math.max(0, Math.sin(th * Math.PI * 2 - 1));
820
+ }
821
+ return -1;
822
+ };
823
+ const BH = [[-0.85, 6.4, 0.34, 0.5, 1.4, 101, 3.42, 3.9], [0.32, 7.0, 0.6, 0.48, 1.35, 202, 3.75, 4.15], [-1.4, 7.8, 0.8, 0.52, 1.3, 303, 3.98, 4.37]];
824
+ const bulbSpr = BH.map((p, i) => sprite('bh' + i, BBX - 140, BBY - 140, 280, 345, (c) => {
825
+ hatchW(c, bulbShade, BBX, BBY + 60, p[0], p[1], p[2], p[3], p[4], p[5], 250);
826
+ c.save(); c.globalCompositeOperation = 'destination-out'; c.translate(BBX - 56, BBY - 59); c.rotate(-0.7); c.scale(1, 0.45);
827
+ const hg = c.createRadialGradient(0, 0, 3, 0, 0, 34); hg.addColorStop(0, 'rgba(0,0,0,0.95)'); hg.addColorStop(0.7, 'rgba(0,0,0,0.75)'); hg.addColorStop(1, 'rgba(0,0,0,0)');
828
+ c.fillStyle = hg; c.beginPath(); c.arc(0, 0, 34, 0, 7); c.fill(); c.restore();
829
+ }));
830
+ const bulbTone = sprite('bt', BBX - 140, BBY - 140, 410, 410, (c) => {
831
+ c.save(); c.translate(BBX + 88, MY + 177); c.scale(1, 0.12); // soft cast shadow, lower right
832
+ const cs = c.createRadialGradient(0, 0, 8, 0, 0, 170); cs.addColorStop(0, `rgba(${GR},0.5)`); cs.addColorStop(0.55, `rgba(${GR},0.2)`); cs.addColorStop(1, `rgba(${GR},0)`);
833
+ c.fillStyle = cs; c.beginPath(); c.arc(0, 0, 170, 0, 7); c.fill(); c.restore();
834
+ c.save(); c.beginPath(); c.arc(BBX, BBY, BR, 0, 7); c.clip(); // stump-blended sphere tone
835
+ const gg = c.createRadialGradient(BBX - 48, BBY - 55, 14, BBX + 14, BBY + 14, BR * 1.15);
836
+ gg.addColorStop(0, `rgba(${GR},0)`); gg.addColorStop(0.55, `rgba(${GR},0.1)`); gg.addColorStop(0.85, `rgba(${GR},0.32)`); gg.addColorStop(1, `rgba(${GR},0.2)`);
837
+ c.fillStyle = gg; c.fillRect(BBX - BR, BBY - BR, BR * 2, BR * 2); c.restore();
838
+ const nt = c.createLinearGradient(BBX - 54, 0, BBX + 54, 0);
839
+ nt.addColorStop(0, `rgba(${GR},0.04)`); nt.addColorStop(0.6, `rgba(${GR},0.16)`); nt.addColorStop(1, `rgba(${GR},0.34)`);
840
+ c.fillStyle = nt; c.fillRect(BBX - 53, BBY + 100, 106, MY + 168 - (BBY + 100));
841
+ c.globalCompositeOperation = 'destination-out';
842
+ c.beginPath(); c.arc(BBX, BBY, 66, 0, 7); c.fillStyle = 'rgba(0,0,0,0.85)'; c.fill();
843
+ c.translate(BBX - 56, BBY - 59); c.rotate(-0.7); c.scale(1, 0.45);
844
+ const hg = c.createRadialGradient(0, 0, 3, 0, 0, 36); hg.addColorStop(0, 'rgba(0,0,0,0.9)'); hg.addColorStop(1, 'rgba(0,0,0,0)');
845
+ c.fillStyle = hg; c.beginPath(); c.arc(0, 0, 36, 0, 7); c.fill();
846
+ }, false);
847
+ // soft graphite shadow under a card (stretched to each shape's box)
848
+ const cardSh = sprite('csh', 0, 0, 200, 200, (c) => { c.filter = 'blur(9px)'; c.fillStyle = `rgba(${GR},1)`; c.beginPath(); c.roundRect(40, 40, 120, 120, 14); c.fill(); c.filter = 'none'; }, false);
849
+ // crescent hatching on the shadow side inside a closed shape: two crossing passes, darker at the edge
850
+ const crescent = (it, a0, a1, b0, b1, seed) => sprite('cr' + seed + '_' + (a0 | 0) + '_' + (b0 | 0), a0 - 12, b0 - 12, a1 - a0 + 24, b1 - b0 + 24, (c) => {
851
+ const inside = (dx, dy, fn) => {
852
+ c.save(); c.beginPath(); it.pts.forEach(([x, y], i) => (i ? c.lineTo(x, y) : c.moveTo(x, y))); c.closePath(); c.clip();
853
+ c.beginPath(); c.rect(a0 - 80, b0 - 80, a1 - a0 + 160, b1 - b0 + 160);
854
+ it.pts.forEach(([x, y], i) => (i ? c.lineTo(x + dx, y + dy) : c.moveTo(x + dx, y + dy))); c.closePath(); c.clip('evenodd');
855
+ fn(); c.restore();
856
+ };
857
+ const ext = (a1 - a0 + b1 - b0) * 0.75, mx = (a0 + a1) / 2, my = (b0 + b1) / 2;
858
+ inside(-22, -19, () => hatchW(c, () => 1, mx, my, -0.82, 5.2, 0, 0.46, 1.3, seed, ext));
859
+ inside(-9, -8, () => hatchW(c, () => 1, mx, my, 0.3, 5.6, 0, 0.42, 1.2, seed + 7, ext));
860
+ });
861
+ // the progress chart on page 2: tone + hatching under the curve, darkest just under the line
862
+ const chartSpr = (ci) => {
863
+ const P = ci.pts, ox = L.D[0] - 380, oy = L.D[1] + 150;
864
+ const cyAt = (x) => { if (x < P[0][0] || x > P[P.length - 1][0]) return null; let i = 1; while (i < P.length - 1 && P[i][0] < x) i++; const a = P[i - 1], b = P[i], u = (x - a[0]) / ((b[0] - a[0]) || 1); return a[1] + (b[1] - a[1]) * u; };
865
+ const shade = (x, y) => { const c0 = cyAt(x); if (c0 == null || y < c0 + 5 || y > oy - 5) return -1; return 1 - (y - c0) / (oy - c0); };
866
+ const hs = sprite('chH', ox, oy - 360, 600, 370, (c) => {
867
+ hatchW(c, shade, ox + 280, oy - 160, -0.85, 7.5, 0.2, 0.34, 1.25, 505, 420);
868
+ hatchW(c, shade, ox + 280, oy - 160, 0.42, 8, 0.62, 0.36, 1.2, 606, 420);
869
+ });
870
+ const ts = sprite('chT', ox, oy - 360, 600, 370, (c) => {
871
+ c.beginPath(); c.moveTo(P[0][0], oy); P.forEach(([x, y]) => c.lineTo(x, y + 3)); c.lineTo(P[P.length - 1][0], oy); c.closePath();
872
+ const lg = c.createLinearGradient(0, oy - 330, 0, oy); lg.addColorStop(0, `rgba(${GR},0.2)`); lg.addColorStop(1, `rgba(${GR},0.02)`);
873
+ c.fillStyle = lg; c.fill();
874
+ }, false);
875
+ return { hs, ts, x0: P[0][0], x1: P[P.length - 1][0] };
876
+ };
877
+ // lettering guide lines + soft dust smears + erased ghosts (all page-anchored)
878
+ const smear = (o, x, y, rx, ry, rot, a) => {
879
+ o.save(); o.translate(x, y); o.rotate(rot); o.scale(1, ry / rx);
880
+ const sg = o.createRadialGradient(0, 0, 0, 0, 0, rx); sg.addColorStop(0, `rgba(${GR},${a})`); sg.addColorStop(1, `rgba(${GR},0)`);
881
+ o.fillStyle = sg; o.beginPath(); o.arc(0, 0, rx, 0, 7); o.fill(); o.restore();
882
+ };
883
+ const ghost = (o, pts, seed) => { // an earlier attempt, erased: a faint grey line inside a lighter rubbed streak
884
+ const P = wob(seg(pts, 4), nz, seed, 2, 0.01);
885
+ o.save(); o.lineCap = 'round'; o.lineJoin = 'round';
886
+ o.beginPath(); P.forEach(([x, y], i) => (i ? o.lineTo(x, y + 2) : o.moveTo(x, y + 2)));
887
+ o.strokeStyle = 'rgba(255,252,240,0.2)'; o.lineWidth = 12; o.stroke();
888
+ o.beginPath(); P.forEach(([x, y], i) => (i ? o.lineTo(x, y) : o.moveTo(x, y)));
889
+ o.strokeStyle = `rgba(${GR},0.075)`; o.lineWidth = 1.6; o.stroke(); o.restore();
890
+ };
891
+ const crumbs = (o, x0, x1, y0, y1, seed, n) => {
892
+ const rr = H.rng(seed);
893
+ for (let i = 0; i < n; i++) {
894
+ const x = rr.range(x0, x1), y = rr.range(y0, y1), l = rr.range(9, 18), a = rr.range(0, 3);
895
+ o.save(); o.translate(x, y); o.rotate(a);
896
+ o.fillStyle = 'rgba(60,40,30,0.22)'; o.beginPath(); o.ellipse(1.5, 2.5, l / 2, 3.4, 0, 0, 7); o.fill();
897
+ const cg = o.createLinearGradient(0, -3, 0, 3); cg.addColorStop(0, '#e9d7cf'); cg.addColorStop(1, '#a99791');
898
+ o.fillStyle = cg; o.beginPath(); o.ellipse(0, 0, l / 2, 3.1, 0, 0, 7); o.fill();
899
+ o.fillStyle = 'rgba(70,70,80,0.35)'; o.beginPath(); o.ellipse(l * 0.1, 0.6, l / 4, 1, 0, 0, 7); o.fill();
900
+ o.restore();
901
+ }
902
+ };
903
+ const chartIt = items.find((it) => it.g === 'D' && it.k === 'line' && it.role === 'acc1' && it.pts.length > 30);
904
+ const renderPage = (cm, page2) => {
905
+ const z = cm[2], out = H.layer(W, H_), o = out.getContext('2d');
906
+ const apply = (c) => { c.translate(W / 2, H_ / 2); c.scale(z, z); c.translate(-cm[0], -cm[1]); };
907
+ // paper + binding
908
+ o.save(); apply(o);
909
+ o.drawImage(XC.page, -300, -300, 4400, 2480);
910
+ o.fillStyle = '#2a2420'; o.fillRect(-600, -400, 520, 3000);
911
+ const cur = o.createLinearGradient(-80, 0, 30, 0); cur.addColorStop(0, 'rgba(90,70,40,0.3)'); cur.addColorStop(1, 'rgba(90,70,40,0)');
912
+ o.fillStyle = cur; o.fillRect(-80, -400, 110, 3000);
913
+ for (let y = -380; y < 2000; y += 34) {
914
+ o.fillStyle = 'rgba(40,30,20,0.22)'; o.beginPath(); o.ellipse(-56, y + 3, 7, 8.5, 0, 0, 7); o.fill(); // coil shadow
915
+ o.fillStyle = '#2b2520'; o.beginPath(); o.ellipse(-58, y, 6, 7.5, 0, 0, 7); o.fill();
916
+ o.strokeStyle = 'rgba(255,255,255,0.3)'; o.lineWidth = 1; o.beginPath(); o.ellipse(-58, y + 0.8, 6, 7.5, 0, 0.2, 2.9); o.stroke();
917
+ o.strokeStyle = 'rgba(40,30,20,0.25)'; o.lineWidth = 5; o.lineCap = 'round';
918
+ o.beginPath(); o.moveTo(-56, y + 5); o.quadraticCurveTo(-86, y + 2, -102, y + 12); o.stroke();
919
+ const wg = o.createLinearGradient(0, y - 6, 0, y + 6); wg.addColorStop(0, '#f2f2f2'); wg.addColorStop(0.45, '#9a9ca0'); wg.addColorStop(1, '#3e3f42');
920
+ o.strokeStyle = wg; o.lineWidth = 4.2; o.beginPath(); o.moveTo(-58, y - 2); o.quadraticCurveTo(-84, y - 14, -106, y + 2); o.stroke();
921
+ o.strokeStyle = 'rgba(255,255,255,0.65)'; o.lineWidth = 1; o.beginPath(); o.moveTo(-64, y - 6); o.quadraticCurveTo(-84, y - 14, -100, y - 6); o.stroke();
922
+ }
923
+ if (page2) {
924
+ o.font = '400 30px "Caveat"'; o.fillStyle = `rgba(${GR},0.45)`; o.fillText('p. 15', 3100, 330);
925
+ ghost(o, [[2212, 690], [2300, 585], [2420, 505], [2560, 462], [2690, 445]], 71); // a first, too-steep try at the curve
926
+ smear(o, 2470, 840, 260, 34, -0.05, 0.05); smear(o, 3010, 640, 140, 60, 0.4, 0.04);
927
+ } else {
928
+ o.font = '400 30px "Caveat"'; o.fillStyle = `rgba(${GR},0.45)`; o.fillText('p. 14', 1020, 120);
929
+ smear(o, 6, 470, 170, 46, 1.45, 0.07); smear(o, 30, 980, 120, 36, 1.5, 0.05); // graphite dust by the binding
930
+ ghost(o, [[330, 460], [600, 452], [920, 455]], 73); // an erased first underline
931
+ ghost(o, ell(BBX + 20, BBY - 16, BR + 8, BR + 4, -2.7, 1.6), 74); // an erased first try at the bulb
932
+ smear(o, 1270, 770, 170, 40, -0.1, 0.045); smear(o, 2010, 300, 140, 40, 0.2, 0.035);
933
+ }
934
+ o.restore();
935
+ // ---- tone layer (stump blending, cast shadows) ----
936
+ const Ls = H.layer(W, H_), gS = Ls.getContext('2d');
937
+ gS.save(); apply(gS);
938
+ let usedS = false, usedR = false; const late = [];
939
+ if (!page2) { if (t > 3.42) gS.globalAlpha = H.smooth(3.42, 4.37, t), putS(gS, bulbTone), gS.globalAlpha = 1, usedS = true; }
940
+ else if (chartIt) {
941
+ const cs = chartSpr(chartIt), rx = cs.x0 + (cs.x1 - cs.x0) * XE.win(t, L.pT + 0.62, L.pT + 1.85) + 8;
942
+ if (rx > cs.x0 + 8) { usedS = true; gS.save(); gS.beginPath(); gS.rect(cs.x0 - 20, 0, rx - cs.x0 + 20, 2000); gS.clip(); putS(gS, cs.ts); gS.restore(); }
943
+ }
944
+ // graphite
945
+ const Lg = H.layer(W, H_), gG = Lg.getContext('2d');
946
+ const Lr = H.layer(W, H_), gR = Lr.getContext('2d'); // red colouring pencil for corrections
947
+ gG.save(); apply(gG); gR.save(); apply(gR);
948
+ let g = gG;
949
+ PH = null;
950
+ const vx0 = cm[0] - W / 2 / z - 40, vx1 = cm[0] + W / 2 / z + 40, vy0 = cm[1] - H_ / 2 / z - 40, vy1 = cm[1] + H_ / 2 / z + 40;
951
+ const guide = (pts, seed, a, gate) => { if (gate <= 0) return; GATE = gate; pencil(gG, wob(seg(pts, 3), nz, seed, 0.6, 0.004), 1.1, a, seed, 0.03, 0.03, nz); GATE = 1; };
952
+ if (!page2) { // lettering guides under the title, construction circle + axes for the bulb
953
+ guide([[250, 352], [1030, 340]], 81, 0.13, XE.win(t, 0.02, 0.3));
954
+ guide([[262, 306], [1018, 295]], 82, 0.1, XE.win(t, 0.06, 0.34));
955
+ if (t > 2.4) {
956
+ const cg = XE.win(t, 2.42, 2.7);
957
+ guide(ell(BBX, BBY, BR + 5, BR + 3, -1.9, -1.9 + 6.7), 83, 0.15, cg);
958
+ guide([[BBX + 1, BBY - 160], [BBX - 2, MY + 200]], 84, 0.12, cg);
959
+ guide([[BBX - 175, BBY + 2], [BBX + 180, BBY - 2]], 85, 0.11, cg);
960
+ }
961
+ } else guide([[L.D[0] - 480, L.D[1] + 293], [L.D[0] + 490, L.D[1] + 287]], 86, 0.12, XE.win(t, L.kT - 0.15, L.kT + 0.15));
962
+ for (const it of items) {
963
+ const onP2 = it.g === 'D' || it.g === 'E';
964
+ if (onP2 !== page2) continue;
965
+ const p = it.inst ? 1 : XE.win(t, it.a, it.b);
966
+ if (p <= 0) continue;
967
+ g = it.role === 'acc2' ? gR : gG; if (g === gR) usedR = true;
968
+ if (it.k === 'line') {
969
+ let a0 = 1e9, a1 = -1e9, b0 = 1e9, b1 = -1e9; for (const [x, y] of it.pts) { a0 = Math.min(a0, x); a1 = Math.max(a1, x); b0 = Math.min(b0, y); b1 = Math.max(b1, y); }
970
+ if (a1 < vx0 || a0 > vx1 || b1 < vy0 || b0 > vy1) continue;
971
+ const seed = ((it.pts[0][0] * 13 + it.pts[0][1] * 7) | 0) + 9;
972
+ const dark = it.role === 'acc2' ? 1.0 : 0.8;
973
+ const closed = it.pts.length > 8 && Math.hypot(it.pts[0][0] - it.pts[it.pts.length - 1][0], it.pts[0][1] - it.pts[it.pts.length - 1][1]) < 25 && a1 - a0 > 80;
974
+ const hp = XE.win(t, it.b, it.b + 0.4);
975
+ if (closed && !it.inst && hp > 0 && a1 - a0 > 100 && it.g !== 'M') { // soft cast shadow, lower right
976
+ const sx = (a1 - a0) / 120, sy = (b1 - b0) / 120;
977
+ usedS = true; gS.globalAlpha = 0.2 * hp; gS.drawImage(cardSh.cv, a0 - 40 * sx + 7, b0 - 40 * sy + 9, 200 * sx, 200 * sy); gS.globalAlpha = 1;
978
+ }
979
+ if (!it.inst) { // construction: a light guide pass that overshoots the ends, ghosted in first
980
+ const P0 = it.pts, n0 = P0.length, e0 = P0[0], e1 = P0[1], f0 = P0[n0 - 1], f1 = P0[n0 - 2];
981
+ const ov = Math.hypot(e0[0] - f0[0], e0[1] - f0[1]) < 25 ? 0 : 9 + (seed % 9);
982
+ const ext = (q, r) => { const dx = q[0] - r[0], dy = q[1] - r[1], d = Math.hypot(dx, dy) || 1; return [q[0] + dx / d * ov, q[1] + dy / d * ov]; };
983
+ const G = wob(seg([ext(e0, e1), ...P0, ext(f0, f1)], 4), nz, (seed + 3) % 91, n0 <= 3 ? 0.7 : 1.8, 0.006), nG = Math.max(2, Math.ceil(G.length * Math.min(1, p * 2.2)));
984
+ g.beginPath(); for (let j = 0; j < nG; j++) (j ? g.lineTo(G[j][0] + 1.6, G[j][1] - 1.3) : g.moveTo(G[j][0] + 1.6, G[j][1] - 1.3));
985
+ g.strokeStyle = `rgba(${GR},0.17)`; g.lineWidth = 1.1; g.stroke();
986
+ }
987
+ pstroke(g, wob(seg(it.pts, 3), nz, seed % 91, it.inst ? 0.5 : 1.0, 0.012), Math.max(1.6, it.w * 0.5), dark, seed, p, [a0, a1, b0, b1], it.inst);
988
+ if (closed && !it.inst && hp > 0) { // crescent hatching sweeps in from the top-left
989
+ const cr = crescent(it, a0, a1, b0, b1, seed), gc = g;
990
+ late.push(() => {
991
+ gc.save();
992
+ if (hp < 1) { const C = a0 + b0 + hp * (a1 - a0 + b1 - b0 + 30); gc.beginPath(); gc.moveTo(C - (b0 - 200), b0 - 200); gc.lineTo(C - (b1 + 200), b1 + 200); gc.lineTo(a0 - 2000, b1 + 200); gc.lineTo(a0 - 2000, b0 - 200); gc.closePath(); gc.clip(); }
993
+ putS(gc, cr); gc.restore();
994
+ });
995
+ }
996
+ } else if (it.k === 'dot') {
997
+ g.fillStyle = `rgba(${GR},0.88)`; g.beginPath(); g.arc(it.x, it.y, it.r * 0.9, 0, 7); g.fill();
998
+ } else {
999
+ const font = it.font === 'label' ? `400 ${it.size * 1.25}px "Caveat"` : `700 ${it.size * (it.font === 'num' ? 1.1 : 1.05)}px "Caveat"`;
1000
+ g.save(); g.font = font; const wAll = g.measureText(it.s).width;
1001
+ g.translate(it.align === 'center' ? it.x - wAll / 2 : it.x, it.y); g.rotate(it.rot);
1002
+ const rr = H.rng((it.s.length * 31 + (it.x | 0)) % 991), n = Math.ceil(it.s.length * p);
1003
+ let px = 0;
1004
+ for (let i = 0; i < n; i++) {
1005
+ const ch = it.s[i], w = g.measureText(ch).width;
1006
+ g.fillStyle = it.role === 'acc2' ? `rgba(${GR},${rr.range(0.85, 1)})` : `rgba(${GR},${0.86 * rr.range(0.82, 1.05)})`;
1007
+ g.save(); g.translate(px, rr.range(-1, 1)); g.rotate(rr.range(-0.03, 0.03)); g.fillText(ch, 0, 0); g.restore();
1008
+ px += w;
1009
+ }
1010
+ g.restore();
1011
+ }
1012
+ }
1013
+ if (!page2 && t > 3.42) late.push(() => BH.forEach((b, i) => sweep(gG, bulbSpr[i], BBX, BBY + 60, b[0], H.ease.inOut(XE.win(t, b[6], b[7])), -200, 230)));
1014
+ if (page2 && chartIt) {
1015
+ const cs = chartSpr(chartIt), rx = cs.x0 + (cs.x1 - cs.x0) * XE.win(t, L.pT + 0.62, L.pT + 1.85) + 8;
1016
+ if (rx > cs.x0 + 8) late.push(() => { gG.save(); gG.beginPath(); gG.rect(cs.x0 - 20, 0, rx - cs.x0 + 20, 2000); gG.clip(); putS(gG, cs.hs); gG.restore(); });
1017
+ }
1018
+ gG.restore(); gR.restore();
1019
+ // paper tooth, anchored to the page: light pressure only catches the peaks
1020
+ const T = XC.tooth, iz = 1 / z;
1021
+ for (const g of usedR ? [gG, gR] : [gG]) {
1022
+ const id = g.getImageData(0, 0, W, H_), d = id.data;
1023
+ for (let y = 0, i = 3; y < H_; y++) {
1024
+ const wy = ((y - H_ / 2) * iz + cm[1]) * 1.4 | 0, row = (wy & 511) << 9;
1025
+ for (let x = 0; x < W; x++, i += 4) {
1026
+ const a = d[i]; if (!a) continue;
1027
+ const wx = ((x - W / 2) * iz + cm[0]) * 1.4 | 0, af = a * 0.0054902; // a/255*1.4
1028
+ let k = (T[row | (wx & 511)] - (af < 1 ? 1 - af : 0) * 0.75) * 3.2 + 0.15; k = k < 0 ? 0 : k > 1 ? 1 : k;
1029
+ d[i] = a * (0.3 + 0.7 * k);
1030
+ }
1031
+ }
1032
+ g.putImageData(id, 0, 0);
1033
+ }
1034
+ if (late.length) { // pre-toothed sprites (hatching) go on after the per-pixel pass
1035
+ gG.save(); apply(gG); gR.save(); apply(gR);
1036
+ late.forEach((f) => f());
1037
+ gG.restore(); gR.restore();
1038
+ }
1039
+ // tone gets the same tooth (grain pattern in page space), then sits under the lines
1040
+ if (usedS) {
1041
+ gS.globalCompositeOperation = 'destination-out'; gS.globalAlpha = 0.55;
1042
+ const pat = gS.createPattern(XC.toothCv, 'repeat'); pat.setTransform(new DOMMatrix([1 / 1.4, 0, 0, 1 / 1.4, 0, 0]));
1043
+ gS.fillStyle = pat; gS.fillRect(vx0 - 50, vy0 - 50, vx1 - vx0 + 100, vy1 - vy0 + 100);
1044
+ }
1045
+ gS.restore();
1046
+ if (usedS) o.drawImage(Ls, 0, 0);
1047
+ // stump smudge: a soft, offset ghost of all the graphite (blended by finger and the side of the hand)
1048
+ const sm = XC.sm || (XC.sm = H.layer(W / 4, H_ / 4)), smc = sm.getContext('2d');
1049
+ smc.clearRect(0, 0, W / 4, H_ / 4); smc.imageSmoothingEnabled = true; smc.imageSmoothingQuality = 'high'; smc.drawImage(Lg, 0, 0, W / 4, H_ / 4);
1050
+ o.save(); o.globalAlpha = 0.12; o.imageSmoothingQuality = 'high'; o.drawImage(sm, 2 * z, 3 * z, W, H_); o.restore();
1051
+ o.drawImage(Lg, 0, 0);
1052
+ if (usedR) { gR.globalCompositeOperation = 'source-in'; gR.fillStyle = '#b8322e'; gR.fillRect(0, 0, W, H_); o.drawImage(Lr, 0, 0); }
1053
+ o.save(); apply(o); // eraser crumbs
1054
+ if (page2) crumbs(o, 3080, 3180, 780, 840, 818, 7); else if (t > 4.4) crumbs(o, 1985, 2085, 862, 915, 808, 8);
1055
+ o.restore();
1056
+ const lamp = o.createRadialGradient(560, 220, 80, 640, 360, 900);
1057
+ lamp.addColorStop(0, 'rgba(255,250,235,0.14)'); lamp.addColorStop(1, 'rgba(70,50,20,0.16)');
1058
+ o.fillStyle = lamp; o.fillRect(0, 0, W, H_);
1059
+ return out;
1060
+ };
1061
+ const tipFor = (cm) => {
1062
+ const tp = XE.tipOf(items, t, (it, p) => {
1063
+ ctx.save(); ctx.font = `700 ${it.size}px "Caveat"`; const w = ctx.measureText(it.s).width, x0 = it.align === 'center' ? it.x - w / 2 : it.x;
1064
+ const w2 = ctx.measureText(it.s.slice(0, Math.ceil(it.s.length * p))).width; ctx.restore(); return [x0 + w2, it.y - it.size * 0.3];
1065
+ });
1066
+ return tp ? [(tp[0] - cm[0]) * cm[2] + W / 2, (tp[1] - cm[1]) * cm[2] + H_ / 2] : null;
1067
+ };
1068
+
1069
+ let tip = null;
1070
+ if (t < TURN0) { ctx.drawImage(renderPage(cam, false), 0, 0); tip = tipFor(cam); }
1071
+ else if (t >= TURN1) { ctx.drawImage(renderPage(cam2, true), 0, 0); tip = tipFor(cam2); }
1072
+ else {
1073
+ // ---------- page turn: the page lifts from the bottom-right corner and folds over to the left ----------
1074
+ const u = H.ease.inOut(XE.win(t, TURN0, TURN1));
1075
+ const P1 = renderPage(cam, false), P2 = renderPage(cam2, true);
1076
+ ctx.drawImage(P2, 0, 0);
1077
+ const xb = W * (1 - u) * 1.05 + 10, xt = xb + 180 * Math.sin(u * Math.PI) + 40 * (1 - u);
1078
+ // fold line from (xt,0) to (xb,H_); reflect points across it
1079
+ const fx = xb - xt, fy = H_, fl = Math.hypot(fx, fy), ux = fx / fl, uy = fy / fl;
1080
+ const refl = ([px, py]) => { const vx = px - xt, vy = py, d = vx * ux + vy * uy; return [xt + 2 * d * ux - vx, 2 * d * uy - vy]; };
1081
+ // shadow cast on the new page beyond the fold
1082
+ ctx.save(); ctx.beginPath(); ctx.moveTo(xt, 0); ctx.lineTo(W + 10, 0); ctx.lineTo(W + 10, H_); ctx.lineTo(xb, H_); ctx.closePath(); ctx.clip();
1083
+ const nx = uy, ny = -ux; // normal pointing right
1084
+ const sg = ctx.createLinearGradient(xb, H_ / 2, xb + nx * 60, H_ / 2 + ny * 60);
1085
+ sg.addColorStop(0, 'rgba(30,20,10,0.35)'); sg.addColorStop(1, 'rgba(30,20,10,0)'); ctx.fillStyle = sg; ctx.fillRect(0, 0, W + 10, H_); ctx.restore();
1086
+ // the part of page 1 still lying flat (left of the fold)
1087
+ ctx.save(); ctx.beginPath(); ctx.moveTo(-10, 0); ctx.lineTo(xt, 0); ctx.lineTo(xb, H_); ctx.lineTo(-10, H_); ctx.closePath(); ctx.clip();
1088
+ ctx.drawImage(P1, 0, 0); ctx.restore();
1089
+ // the flap: back of the turned page
1090
+ const c1 = refl([W, 0]), c2 = refl([W, H_]);
1091
+ ctx.save(); ctx.beginPath(); ctx.moveTo(xt, 0); ctx.lineTo(c1[0], c1[1]); ctx.lineTo(c2[0], c2[1]); ctx.lineTo(xb, H_); ctx.closePath();
1092
+ ctx.save(); ctx.filter = 'blur(10px)'; ctx.fillStyle = 'rgba(40,25,10,0.35)'; ctx.translate(-12, 6); ctx.fill(); ctx.restore();
1093
+ ctx.clip();
1094
+ ctx.drawImage(XC.page, 0, 0, 550, 310, -50, -50, W + 100, H_ + 100);
1095
+ const fg = ctx.createLinearGradient(xb, H_ / 2, xb - nx * 260, H_ / 2 - ny * 260);
1096
+ fg.addColorStop(0, 'rgba(60,40,20,0.32)'); fg.addColorStop(0.25, 'rgba(255,250,240,0.12)'); fg.addColorStop(1, 'rgba(80,60,30,0.12)');
1097
+ ctx.fillStyle = fg; ctx.fillRect(0, 0, W, H_);
1098
+ // faint show-through of the drawing on the back of the sheet
1099
+ ctx.globalAlpha = 0.08; ctx.translate(xt, 0); ctx.transform(ux * ux - uy * uy, 2 * ux * uy, 2 * ux * uy, uy * uy - ux * ux, 0, 0); ctx.translate(-xt, 0); ctx.drawImage(P1, 0, 0);
1100
+ ctx.restore();
1101
+ }
1102
+ if (tip && (t < TURN0 - 0.05 || t > TURN1)) {
1103
+ const [tx, ty] = tip, ang = 0.72, len = 300;
1104
+ ctx.save();
1105
+ ctx.save(); ctx.filter = 'blur(7px)'; ctx.fillStyle = 'rgba(50,35,15,0.28)'; ctx.translate(tx + 22, ty + 26); ctx.rotate(ang + 0.05); ctx.beginPath(); ctx.roundRect(30, -8, len, 16, 6); ctx.fill(); ctx.restore();
1106
+ ctx.translate(tx, ty); ctx.rotate(ang);
1107
+ ctx.fillStyle = '#2c2c30'; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(12, -2.6); ctx.lineTo(12, 2.6); ctx.closePath(); ctx.fill();
1108
+ const wg = ctx.createLinearGradient(0, -8, 0, 8); wg.addColorStop(0, '#f1d9b0'); wg.addColorStop(1, '#b98e5a');
1109
+ ctx.fillStyle = wg; ctx.beginPath(); ctx.moveTo(11, -2.8); ctx.lineTo(40, -8); ctx.lineTo(40, 8); ctx.lineTo(11, 2.8); ctx.closePath(); ctx.fill();
1110
+ const bg = ctx.createLinearGradient(0, -8, 0, 8); bg.addColorStop(0, '#5f8c6a'); bg.addColorStop(0.35, '#2f5a3c'); bg.addColorStop(0.36, '#24472f'); bg.addColorStop(1, '#16301f');
1111
+ ctx.fillStyle = bg; ctx.fillRect(40, -8, len - 40, 16);
1112
+ ctx.restore();
1113
+ }
1114
+ H.grain(ctx, W, H_, 0.03, 3 + (Math.floor(t * 12) % 3));
1115
+ H.vignette(ctx, W, H_, 0.22, '40,28,10');
1116
+ },
1117
+ // <<<EXPLAINER
1118
+ // Sound: a graphite pencil on a toothy sketchbook page under a desk lamp. Every draw-on gets a dry pencil scratch of
1119
+ // the same length (with a soft tap where the point lands and the page's low body under it); the red colouring pencil
1120
+ // (acc2 items) is waxier and lower; the crescent hatching that fills closed shapes is a quick back-and-forth flick;
1121
+ // the page turn (TURN0..TURN1) is the scene change. Times mirror explainer()/XE.story (pT 11.05, kT 13.05,
1122
+ // model + turn timings from explainer()).
1123
+ explainerSound(k) {
1124
+ const pT = 11.05, kT = 13.05, TURN0 = 10.35, TURN1 = 11.0;
1125
+ const rnd = (i) => { const x = Math.sin(i * 12.9898 + 78.233) * 43758.5453; return x - Math.floor(x); };
1126
+ // first time XE.eio(win(t, t0, t1)) reaches u (the gear's eased turn)
1127
+ const when = (t0, t1, u) => { for (let i = 0; i <= 400; i++) if (XE.eio(i / 400) >= u - 1e-6) return t0 + (t1 - t0) * i / 400; return t1; };
1128
+ // one pencil line: the point lands, graphite scratches for the whole draw-on, the page body hums under it
1129
+ // graphite grains (bandpassed, like the kit's pencil stroke but without its highpass grain)
1130
+ let gs = 0;
1131
+ const graphite = (a, d, g, pan, minor) => {
1132
+ for (let x = 0; x < d; x += 0.04 * (0.7 + rnd(++gs) * 0.6)) {
1133
+ const gd = 0.04 * (0.8 + rnd(++gs) * 0.9);
1134
+ k.noise(a + x, { dur: gd, filter: 'bandpass', f: 3900 * (0.8 + rnd(++gs) * 0.4), q: 0.7, gain: 0.16 * g * (0.6 + rnd(++gs) * 0.5), a: gd * 0.3, pan: pan + (rnd(++gs) - 0.5) * 0.2, minor });
1135
+ }
1136
+ };
1137
+ const LV = 0.45; // drawing sits well under the hits
1138
+ const pc = (a, b, g = 1, pan = 0, minor = true, red = false) => {
1139
+ const d = Math.max(0.03, b - a); g *= LV;
1140
+ k.noise(a, { dur: 0.018, filter: 'bandpass', f: red ? 800 : 1100, q: 1.3, gain: 0.3 * g, a: 0.001, pan, minor });
1141
+ k.tone(a, { f: red ? 150 : 190, dur: 0.03, gain: 0.05 * g, pan, minor }); // the point's little knock through the pad
1142
+ if (red) k.play('stroke', a, { material: 'chalk', dur: d, gain: 0.5 * g, pan, minor }); else graphite(a, d, 0.85 * g, pan, minor);
1143
+ k.noise(a, { dur: d + 0.03, color: 'pink', filter: 'bandpass', f: red ? 480 : 650, q: 0.8, gain: 0.08 * g, a: 0.015, hold: d * 0.6, pan, minor });
1144
+ };
1145
+ // handwriting: one stroke per word, timed by character position like the explainer's per-letter reveal
1146
+ const write = (s, a, b, g = 0.9, pan = 0, red = false, minor = true) => {
1147
+ const n = s.length, re = /\S+/g; let m;
1148
+ while ((m = re.exec(s))) pc(a + (b - a) * m.index / n, a + (b - a) * (m.index + m[0].length) / n, g, pan, minor, red);
1149
+ };
1150
+ // crescent hatching: short parallel flicks, back and forth (minor texture under the contours)
1151
+ const hatch = (a, b, g = 1, seed = 1) => {
1152
+ let i = 0;
1153
+ for (let x = a; x < b; x += 0.048 + rnd(seed + i) * 0.02, i++) {
1154
+ const fade = 1 - 0.5 * (x - a) / (b - a);
1155
+ k.noise(x, { dur: 0.035, filter: 'bandpass', f: 3000 + rnd(seed + i + 50) * 900, q: 1.1, gain: 0.07 * LV * g * fade, a: 0.006, pan: i % 2 ? 0.08 : -0.08, minor: true });
1156
+ }
1157
+ };
1158
+ const tap = (t, g = 1) => k.noise(t, { dur: 0.02, filter: 'bandpass', f: 1300, q: 1.5, gain: 0.18 * g, a: 0.001, minor: true }); // pencil point dotted onto the page
1159
+
1160
+ k.bed('room', 0, 15, { gain: 0.45 });
1161
+
1162
+ // 1 HOOK
1163
+ write('How does AI learn?', 0.15, 1.3, 1.0);
1164
+ pc(1.3, 1.55, 1.25, 0, false); // the underline: one confident sweep
1165
+
1166
+ // 2 DATA: light-bulb model, then six cards; each closed card fills with hatching right after its outline
1167
+ pc(2.5, 2.85, 1.05, 0.1, false); // bulb outline: first mark in the new scene
1168
+ pc(2.85, 2.92, 0.75, 0.1); pc(2.92, 2.99, 0.75, 0.1); // neck
1169
+ pc(2.99, 3.09, 0.65, 0.1); // screw-base bands
1170
+ for (let i = 0; i < 6; i++) { const a = 3.0 + i * 0.17; pc(a, a + (i < 5 ? 0.15 : 0.21), 0.85, (i % 3 - 1) * 0.15); }
1171
+ pc(3.15, 3.4, 0.45, 0.15); // gear inside the bulb (under the cards)
1172
+ hatch(3.12, 4.37, 0.9, 3);
1173
+ pc(4.15, 4.45, 1.0); pc(4.46, 4.55, 0.85); // flow arrow + head
1174
+ write('examples', 4.55, 4.8, 0.35);
1175
+ for (let f = 55; f < 63; f++) k.play('count', f / 12, { gain: 1.6, f: 2600 + (f - 55) * 120, minor: true }); // 0 -> 1,000,000, re-pencilled on the 12 fps grid
1176
+ k.play('tock', 63 / 12, { f: 520, gain: 0.45 }); // 1,000,000 lands
1177
+
1178
+ // 3 GUESS -> CHECK -> ADJUST
1179
+ pc(6.4, 6.53, 0.9, 0, true, true); // red frame of the new photo
1180
+ pc(6.55, 6.66, 0.85); pc(6.6, 6.7, 0.6); // cat
1181
+ tap(6.7, 0.9); // eyes
1182
+ hatch(6.55, 6.95, 0.8, 11);
1183
+ write('new!', 6.67, 6.8, 0.7, 0.2, true);
1184
+ pc(6.8, 6.94, 0.9); pc(6.96, 7.02, 0.8); // arrow down into the model
1185
+ pc(7.02, 7.15, 0.9); // guess arrow out
1186
+ write('Guess', 7.15, 7.4, 0.7);
1187
+ pc(7.3, 7.47, 0.85); pc(7.48, 7.53, 0.75); // speech bubble + tail
1188
+ hatch(7.5, 7.9, 0.75, 21);
1189
+ write('dog?', 7.54, 7.75, 1.0);
1190
+ write('Check', 7.86, 8.0, 0.7, 0, true);
1191
+ // the wrong guess: two hard red slashes + a low "nope"
1192
+ pc(8.0, 8.08, 1.8, 0.1, false, true); pc(8.08, 8.16, 1.8, 0.1, false, true);
1193
+ k.play('error', 8.0, { f: 'E3', gain: 0.55 });
1194
+ pc(8.16, 8.45, 1.1, 0, true, true); // red ring
1195
+ pc(8.45, 8.8, 1.0, -0.1, true, true); // the long arrow back to the model
1196
+ write('Adjust', 8.8, 9.05, 0.7, -0.1, true);
1197
+ // the gear turns (1.6 rad over eio(8.95..9.5)): a soft click for every ~0.27 rad, so they slow as it settles
1198
+ for (let j = 1; j <= 6; j++) k.play('click', when(8.95, 9.5, j / 6), { f: 900, gain: 0.5, pan: -0.15, minor: true });
1199
+ pc(9.5, 9.62, 1.1); // strike through "dog?"
1200
+ write('cat', 9.62, 9.85, 1.0);
1201
+ pc(9.85, 10.05, 1.15, 0, false); // check mark
1202
+ k.play('success', 9.98, { f: 'A5', gain: 0.75 });
1203
+
1204
+ // page turn: the corner lifts, the sheet sweeps over (air), and lands flat on the left
1205
+ k.play('paper', TURN0, { dur: 0.22, gain: 0.7 });
1206
+ k.noise(TURN0 + 0.05, { dur: 0.6, color: 'pink', filter: 'bandpass', f: [[0, 260], [0.32, 900], [0.6, 380]], q: 0.7, gain: 0.24, a: 0.28, pan: 0.2 });
1207
+ k.play('slap', TURN1 - 0.05, { gain: 0.35, pan: -0.3 });
1208
+ k.noise(TURN1 - 0.05, { dur: 0.18, color: 'brown', filter: 'lowpass', f: 400, gain: 0.25, pan: -0.3 });
1209
+
1210
+ // 4 PROGRESS (fresh page)
1211
+ pc(pT, pT + 0.3, 1.0, 0, false); // axes
1212
+ write('training rounds', pT + 0.3, pT + 0.55, 0.55);
1213
+ write('accuracy', pT + 0.5, pT + 0.7, 0.65, 0.3);
1214
+ pc(pT + 0.7, pT + 1.7, 0.4); // the rising curve
1215
+ { // % counter: a light pencil tick whenever the shown number changes (12 fps grid), slowing as it settles.
1216
+ // The number is graphite all the way, so the hit lands on the frame 97% is first pencilled.
1217
+ let last = -1;
1218
+ for (let f = Math.ceil((pT + 0.6) * 12); f < 160; f++) {
1219
+ const u = Math.min(1, (f / 12 - (pT + 0.6)) / 1.1), v = Math.round(61 + 36 * (1 - Math.pow(1 - u, 2.2)));
1220
+ if (v === 97) { // 97% lands: the biggest hit
1221
+ k.play('impact', f / 12, { gain: 1.0, pan: 0.15 });
1222
+ k.play('ding', f / 12 + 0.01, { f: 'C6', gain: 0.42, pan: 0.15 });
1223
+ break;
1224
+ }
1225
+ if (v !== last) { k.play('tick', f / 12, { gain: 1.1, f: 2600 + (v - 61) * 25, pan: 0.25, minor: true }); last = v; }
1226
+ }
1227
+ }
1228
+ pc(pT + 1.75, pT + 2.05, 1.25, 0.25, false, true);
1229
+
1230
+ // 5 TAKEAWAY
1231
+ write('More examples → better guesses', kT, kT + 0.75, 0.9);
1232
+ k.play('success', kT + 0.75, { f: 'G5', gain: 0.7 }); // resolving landing
1233
+ pc(kT + 0.75, kT + 0.95, 1.15, 0, false);
1234
+ k.play('tock', kT + 1.05, { f: 380, gain: 0.6, minor: true }); // pencil laid down on the desk
1235
+ },
1236
+ });
1237
+ }