@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,1096 @@
1
+ {
2
+ // ---------- Vintage US patent drawing: "Thinking Machine" ----------
3
+ let budget = Infinity, used = 0;
4
+ const ok = () => used++ < budget;
5
+ const drew = () => used <= budget; // was the last budgeted mark actually inked?
6
+ const free = (fn) => { const u = used, b = budget; budget = Infinity; fn(); used = u; budget = b; }; // extras that ride along with it
7
+ const SQ = Math.sqrt;
8
+ const V = [1 / SQ(3), 1 / SQ(3), 1 / SQ(3)];
9
+ const Ln = (() => { const v = [-0.5, 0.62, 0.6], m = Math.hypot(...v); return v.map((a) => a / m); })();
10
+ const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
11
+
12
+ function path(ctx, pts, close) {
13
+ ctx.beginPath(); ctx.moveTo(pts[0][0], pts[0][1]);
14
+ for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]);
15
+ if (close) ctx.closePath();
16
+ }
17
+ function stroke(ctx, pts, w = 1.2, close = false, dash) {
18
+ if (!pts.length || !ok()) return;
19
+ path(ctx, pts, close); ctx.lineWidth = w; if (dash) ctx.setLineDash(dash); ctx.stroke(); if (dash) ctx.setLineDash([]);
20
+ }
21
+ function erase(ctx, pts) {
22
+ ctx.save(); ctx.globalCompositeOperation = 'destination-out'; path(ctx, pts, true); ctx.fill(); ctx.restore();
23
+ }
24
+ function contours(ctx, x0, y0, w, h, cell, f, lev) {
25
+ const nx = Math.ceil(w / cell), ny = Math.ceil(h / cell), v = new Float32Array((nx + 1) * (ny + 1));
26
+ for (let j = 0; j <= ny; j++) for (let i = 0; i <= nx; i++) v[j * (nx + 1) + i] = f(x0 + i * cell, y0 + j * cell) - lev;
27
+ ctx.beginPath();
28
+ for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
29
+ const a = v[j * (nx + 1) + i], b = v[j * (nx + 1) + i + 1], cc = v[(j + 1) * (nx + 1) + i + 1], d = v[(j + 1) * (nx + 1) + i];
30
+ const X = x0 + i * cell, Y = y0 + j * cell, pts = [];
31
+ if ((a > 0) !== (b > 0)) pts.push([X + cell * a / (a - b), Y]);
32
+ if ((b > 0) !== (cc > 0)) pts.push([X + cell, Y + cell * b / (b - cc)]);
33
+ if ((d > 0) !== (cc > 0)) pts.push([X + cell * d / (d - cc), Y + cell]);
34
+ if ((a > 0) !== (d > 0)) pts.push([X, Y + cell * a / (a - d)]);
35
+ for (let k = 0; k + 1 < pts.length; k += 2) { ctx.moveTo(pts[k][0], pts[k][1]); ctx.lineTo(pts[k + 1][0], pts[k + 1][1]); }
36
+ }
37
+ }
38
+ function hull(pts) {
39
+ pts = pts.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
40
+ const cr = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
41
+ const lo = [], up = [];
42
+ for (const p of pts) { while (lo.length >= 2 && cr(lo[lo.length - 2], lo[lo.length - 1], p) <= 0) lo.pop(); lo.push(p); }
43
+ for (let i = pts.length - 1; i >= 0; i--) { const p = pts[i]; while (up.length >= 2 && cr(up[up.length - 2], up[up.length - 1], p) <= 0) up.pop(); up.push(p); }
44
+ up.pop(); lo.pop(); return lo.concat(up);
45
+ }
46
+ // parallel hatch lines inside a clip polygon
47
+ function hatch(ctx, clip, ang, gap, w = 0.8, bbox, grade) {
48
+ if (!ok()) return;
49
+ ctx.save(); path(ctx, clip, true); ctx.clip();
50
+ let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
51
+ for (const p of clip) { x0 = Math.min(x0, p[0]); y0 = Math.min(y0, p[1]); x1 = Math.max(x1, p[0]); y1 = Math.max(y1, p[1]); }
52
+ const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2, R = Math.hypot(x1 - x0, y1 - y0) / 2 + 2;
53
+ const dx = Math.cos(ang), dy = Math.sin(ang), nx = -dy, ny = dx;
54
+ ctx.beginPath();
55
+ let o = -R;
56
+ while (o < R) {
57
+ const px = cx + nx * o, py = cy + ny * o;
58
+ ctx.moveTo(px - dx * R, py - dy * R); ctx.lineTo(px + dx * R, py + dy * R);
59
+ const u = (o + R) / (2 * R);
60
+ o += grade ? gap / Math.max(0.15, grade(u)) : gap;
61
+ }
62
+ ctx.lineWidth = w; ctx.stroke(); ctx.restore();
63
+ }
64
+ // graded shade lining on a vertical face: lines crowd toward the shadow (right) edge, open out toward the light
65
+ function vhatch(ctx, x0, x1, y0, y1, gmin, gmax, w = 0.55, xlo = -1e9) {
66
+ if (!ok()) return;
67
+ ctx.beginPath();
68
+ for (let x = x1 - gmin * 0.5; x > x0; ) { if (x >= xlo) { ctx.moveTo(x, y0); ctx.lineTo(x, y1); } const u = (x1 - x) / (x1 - x0); x -= gmin + (gmax - gmin) * Math.pow(u, 1.4); }
69
+ ctx.lineWidth = w; ctx.stroke();
70
+ }
71
+ function stipple(ctx, clip, n, dens, r, rad = 0.75) {
72
+ if (!ok()) return;
73
+ let x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9;
74
+ for (const p of clip) { x0 = Math.min(x0, p[0]); y0 = Math.min(y0, p[1]); x1 = Math.max(x1, p[0]); y1 = Math.max(y1, p[1]); }
75
+ ctx.save(); path(ctx, clip, true); ctx.clip(); ctx.beginPath();
76
+ for (let i = 0; i < n; i++) {
77
+ const x = r.range(x0, x1), y = r.range(y0, y1);
78
+ if (r() < dens(x, y)) { const rr = rad * r.range(0.7, 1.25); ctx.moveTo(x + rr, y); ctx.arc(x, y, rr, 0, 6.283); }
79
+ }
80
+ ctx.fill(); ctx.restore();
81
+ }
82
+ function ring(c, axis, r, n = 64, a0 = 0, a1 = Math.PI * 2) {
83
+ const out = [];
84
+ for (let i = 0; i <= n; i++) {
85
+ const a = a0 + (a1 - a0) * i / n, ca = Math.cos(a) * r, sa = Math.sin(a) * r;
86
+ if (axis === 'z') out.push([c[0] + ca, c[1] + sa, c[2]]);
87
+ else if (axis === 'y') out.push([c[0] + ca, c[1], c[2] + sa]);
88
+ else out.push([c[0], c[1] + ca, c[2] + sa]);
89
+ }
90
+ return out;
91
+ }
92
+ const nrm = (axis, a) => axis === 'z' ? [Math.cos(a), Math.sin(a), 0] : axis === 'y' ? [Math.cos(a), 0, Math.sin(a)] : [0, Math.cos(a), Math.sin(a)];
93
+ const add = (c, axis, h) => axis === 'z' ? [c[0], c[1], c[2] + h] : axis === 'y' ? [c[0], c[1] + h, c[2]] : [c[0] + h, c[1], c[2]];
94
+
95
+ // solid of revolution: profile [[r,h],...]
96
+ function lathe(ctx, P, c, axis, prof, o = {}) {
97
+ const rings3 = prof.map(([r, h]) => ring(add(c, axis, h), axis, r, 72));
98
+ const all = [];
99
+ rings3.forEach((rg) => rg.forEach((p) => all.push(P(...p))));
100
+ const sil = hull(all);
101
+ erase(ctx, sil);
102
+ // shading generator lines
103
+ if (o.shade !== false) {
104
+ for (let i = 0; i < prof.length - 1; i++) {
105
+ if (Math.abs(prof[i][0] - prof[i + 1][0]) > Math.abs(prof[i][1] - prof[i + 1][1]) * 1.5) continue; // skip flat steps
106
+ let ink = 0.5;
107
+ ctx.beginPath(); let any = false;
108
+ for (let a = -Math.PI; a < Math.PI; a += 0.01) {
109
+ const nn = nrm(axis, a); if (dot3(nn, V) < 0.02) continue;
110
+ const b = dot3(nn, Ln); const sh = H.clamp((0.35 - b) * 1.35);
111
+ ink += sh * 0.01 * (o.dens || 34);
112
+ if (ink >= 1) {
113
+ ink -= 1; any = true;
114
+ const ca = Math.cos(a), sa = Math.sin(a);
115
+ const p1 = add(c, axis, prof[i][1]), p2 = add(c, axis, prof[i + 1][1]);
116
+ const off = (pp, r) => axis === 'z' ? [pp[0] + ca * r, pp[1] + sa * r, pp[2]] : axis === 'y' ? [pp[0] + ca * r, pp[1], pp[2] + sa * r] : [pp[0], pp[1] + ca * r, pp[2] + sa * r];
117
+ const A = P(...off(p1, prof[i][0])), B = P(...off(p2, prof[i + 1][0]));
118
+ ctx.moveTo(A[0], A[1]); ctx.lineTo(B[0], B[1]);
119
+ }
120
+ }
121
+ if (any && ok()) { ctx.lineWidth = o.hw || 0.75; ctx.stroke(); }
122
+ }
123
+ }
124
+ // visible rim edges (front halves)
125
+ rings3.forEach((rg, i) => {
126
+ if (o.rims && !o.rims.includes(i)) return;
127
+ const vis = [];
128
+ let seg = [];
129
+ for (let k = 0; k < rg.length; k++) {
130
+ const a = Math.PI * 2 * k / 72, nn = nrm(axis, a);
131
+ if (dot3(nn, V) > -0.02) seg.push(P(...rg[k])); else if (seg.length) { vis.push(seg); seg = []; }
132
+ }
133
+ if (seg.length) vis.push(seg);
134
+ vis.forEach((s) => stroke(ctx, s, 1.05));
135
+ });
136
+ // visible cap (full ellipse)
137
+ const capIdx = (axis === 'z' || axis === 'y' || axis === 'x') ? prof.length - 1 : 0;
138
+ if (o.cap !== false) stroke(ctx, rings3[capIdx].map((p) => P(...p)), 1.1, true);
139
+ // heavy shade line on lower-right silhouette
140
+ stroke(ctx, sil, 1.25, true);
141
+ const heavy = sil.filter((p) => true);
142
+ return sil;
143
+ }
144
+
145
+ function box(ctx, P, x0, x1, y0, y1, z0, z1, o = {}) {
146
+ const q = (pts) => pts.map((p) => P(...p));
147
+ const sil = q([[x0, y0, z1], [x1, y0, z1], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], [x0, y1, z1]]);
148
+ erase(ctx, sil);
149
+ const right = q([[x1, y0, z1], [x1, y1, z1], [x1, y1, z0], [x1, y0, z0]]);
150
+ if (o.shadeRight) hatch(ctx, right, Math.PI / 2, o.shadeRight, 0.75);
151
+ stroke(ctx, sil, 1.25, true);
152
+ stroke(ctx, q([[x0, y1, z1], [x1, y1, z1], [x1, y0, z1]]), 1.15);
153
+ stroke(ctx, q([[x1, y1, z1], [x1, y1, z0]]), 1.15);
154
+ // shade lines: heavier lower-right edges
155
+ stroke(ctx, q([[x1, y0, z1], [x1, y0, z0], [x1, y1, z0]]), 2.4);
156
+ stroke(ctx, q([[x1, y1, z0], [x0, y1, z0]]), 1.9);
157
+ return { sil, right, front: q([[x0, y1, z1], [x1, y1, z1], [x1, y1, z0], [x0, y1, z0]]), top: q([[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]]) };
158
+ }
159
+
160
+ function catmull(pts, n = 8) {
161
+ const out = [];
162
+ for (let i = 0; i < pts.length - 1; i++) {
163
+ const p0 = pts[Math.max(0, i - 1)], p1 = pts[i], p2 = pts[i + 1], p3 = pts[Math.min(pts.length - 1, i + 2)];
164
+ for (let k = 0; k < n; k++) {
165
+ const t = k / n, t2 = t * t, t3 = t2 * t;
166
+ out.push(p1.map((_, j) => 0.5 * ((2 * p1[j]) + (-p0[j] + p2[j]) * t + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * t2 + (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * t3)));
167
+ }
168
+ }
169
+ out.push(pts[pts.length - 1]);
170
+ return out;
171
+ }
172
+
173
+ function leader(ctx, txt, tx, ty, px, py, bend = 0.18, r) {
174
+ if (!ok()) return;
175
+ const mx = (tx + px) / 2, my = (ty + py) / 2, dx = px - tx, dy = py - ty;
176
+ const cx = mx - dy * bend, cy = my + dx * bend;
177
+ const ang = Math.atan2(ty - py, tx - px);
178
+ const sx = tx - Math.cos(ang) * 0, sy = ty - Math.sin(ang) * 0;
179
+ ctx.beginPath(); ctx.moveTo(sx + Math.cos(Math.atan2(cy - ty, cx - tx)) * 13, sy + Math.sin(Math.atan2(cy - ty, cx - tx)) * 11);
180
+ ctx.quadraticCurveTo(cx, cy, px, py); ctx.lineWidth = 0.9; ctx.stroke();
181
+ ctx.font = '400 17px "Libre Baskerville"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
182
+ ctx.fillText(txt, tx, ty + 1);
183
+ }
184
+
185
+ // signature-like cursive scrawl
186
+ function scrawl(ctx, x, y, w, seed, h = 16) {
187
+ const r = H.rng(seed); ctx.beginPath();
188
+ let px = x, py = y; ctx.moveTo(px, py);
189
+ const n = Math.floor(w / 9);
190
+ for (let i = 0; i < n; i++) {
191
+ const up = r.range(0.4, 1.0) * h * (r.chance(0.18) ? 1.7 : 1);
192
+ const nx = x + (i + 1) * (w / n);
193
+ ctx.bezierCurveTo(px + 4, py - up, nx - 6 + r.range(-3, 3), py - up * 0.9, nx - 2, y + r.range(-2, 3));
194
+ if (r.chance(0.2)) ctx.bezierCurveTo(nx, y + h * 0.7, nx - 7, y + h * 0.7, nx - 3, y);
195
+ px = nx; py = y;
196
+ }
197
+ ctx.stroke();
198
+ }
199
+
200
+ // laid/wove paper base, cached per size. H.paper's 4px cells carry a vertical fibre streak that read as a stretched
201
+ // texture (worse under the explainer's 1.3x zoom): soften it ~30% with a light blur, keep the broad mottling.
202
+ const PAPERS = {};
203
+ function paperBase(w, h) {
204
+ const key = w + 'x' + h; if (PAPERS[key]) return PAPERS[key];
205
+ const T = H.layer(w, h), tc = T.getContext('2d'); H.paper(tc, w, h, '#ece0c0', 11, 0.045);
206
+ const L = H.layer(w, h), c = L.getContext('2d');
207
+ c.fillStyle = '#ece0c0'; c.fillRect(0, 0, w, h);
208
+ c.filter = 'blur(2.6px)'; c.drawImage(T, 0, 0); c.filter = 'none';
209
+ c.globalAlpha = 0.28; c.drawImage(T, 0, 0); c.globalAlpha = 1; // a little of the crisp tooth back
210
+ PAPERS[key] = L; return L;
211
+ }
212
+ // ===== EXPLAINER HELPERS =====
213
+ const PXW = 3840;
214
+ let PAPERW = null, INKV = null;
215
+ function paperWide(W, H_) {
216
+ if (PAPERW) return PAPERW;
217
+ const L = H.layer(PXW, H_), c = L.getContext('2d'), r = H.rng(1931);
218
+ c.drawImage(paperBase(PXW, H_), 0, 0);
219
+ const g = c.createLinearGradient(0, 0, 0, H_); g.addColorStop(0, 'rgba(150,100,35,0.22)'); g.addColorStop(0.3, 'rgba(150,100,35,0)'); g.addColorStop(0.7, 'rgba(150,100,35,0)'); g.addColorStop(1, 'rgba(150,100,35,0.25)');
220
+ c.fillStyle = g; c.fillRect(0, 0, PXW, H_);
221
+ for (let i = 0; i < 200; i++) {
222
+ const x = r() * PXW, y = r.chance(0.6) ? (r.chance(0.5) ? r() * 110 : H_ - r() * 110) : r() * H_, rad = r.range(1.5, r.chance(0.15) ? 13 : 5);
223
+ const fg = c.createRadialGradient(x, y, 0, x, y, rad); fg.addColorStop(0, `rgba(150,95,40,${r.range(0.18, 0.4)})`); fg.addColorStop(1, 'rgba(160,110,50,0)');
224
+ c.fillStyle = fg; c.beginPath(); c.arc(x, y, rad, 0, 7); c.fill();
225
+ }
226
+ c.strokeStyle = 'rgba(120,90,40,0.16)'; c.lineWidth = 1.2; c.beginPath(); c.moveTo(0, 361); c.lineTo(PXW, 357); c.stroke();
227
+ [1280, 2560].forEach((x) => { c.strokeStyle = 'rgba(120,90,40,0.13)'; c.beginPath(); c.moveTo(x, 0); c.lineTo(x + 3, H_); c.stroke(); c.strokeStyle = 'rgba(255,250,230,0.3)'; c.beginPath(); c.moveTo(x + 2, 0); c.lineTo(x + 5, H_); c.stroke(); });
228
+ PAPERW = L; return L;
229
+ }
230
+ const circ = (cx, cy, rx, ry = rx, n = 40, a0 = 0, a1 = Math.PI * 2) => { const o = []; for (let k = 0; k <= n; k++) { const a = a0 + (a1 - a0) * k / n; o.push([cx + Math.cos(a) * rx, cy + Math.sin(a) * ry]); } return o; };
231
+ function partial(pts, p) { if (p >= 1) return pts; const n = Math.max(2, Math.round(pts.length * p)); return pts.slice(0, n); }
232
+ function arrowP(c, pts, p, w = 1.3, dash) {
233
+ if (p <= 0) return;
234
+ stroke(c, partial(pts, p), w, false, dash);
235
+ if (p >= 1 && ok()) { const a = pts[pts.length - 1], b = pts[pts.length - 2], an = Math.atan2(a[1] - b[1], a[0] - b[0]); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(a[0] - Math.cos(an - 0.28) * 15, a[1] - Math.sin(an - 0.28) * 15); c.lineTo(a[0] - Math.cos(an + 0.28) * 15, a[1] - Math.sin(an + 0.28) * 15); c.closePath(); c.fill(); }
236
+ }
237
+ const qp = (a, m, b, n = 30) => { const o = []; for (let k = 0; k <= n; k++) { const t = k / n, u = 1 - t; o.push([u * u * a[0] + 2 * u * t * m[0] + t * t * b[0], u * u * a[1] + 2 * u * t * m[1] + t * t * b[1]]); } return o; };
238
+ function catLine(c, x, y, s) {
239
+ stroke(c, circ(x, y, 20 * s, 17 * s), 1.2, true);
240
+ stroke(c, [[x - 17 * s, y - 8 * s], [x - 14 * s, y - 28 * s], [x - 4 * s, y - 16 * s]], 1.2);
241
+ stroke(c, [[x + 17 * s, y - 8 * s], [x + 14 * s, y - 28 * s], [x + 4 * s, y - 16 * s]], 1.2);
242
+ stroke(c, circ(x - 7 * s, y - 3 * s, 2.4 * s), 1, true); stroke(c, circ(x + 7 * s, y - 3 * s, 2.4 * s), 1, true);
243
+ [[-1, 5, 3], [1, 5, 3], [-1, 9, 11], [1, 9, 11]].forEach(([sg, dy, ey]) => stroke(c, [[x + sg * 6 * s, y + dy * s], [x + sg * 27 * s, y + ey * s]], 0.8));
244
+ stroke(c, [[x - 3 * s, y + 4 * s], [x, y + 7 * s], [x + 3 * s, y + 4 * s]], 1);
245
+ }
246
+ function dogLine(c, x, y, s) {
247
+ stroke(c, circ(x, y, 19 * s, 18 * s), 1.2, true);
248
+ [-1, 1].forEach((sg) => { const ear = circ(x + sg * 20 * s, y, 7 * s, 15 * s); stroke(c, ear, 1.2, true); hatch(c, ear, Math.PI / 2, 2.2, 0.6); });
249
+ stroke(c, circ(x, y + 8 * s, 9 * s, 6 * s), 1, true);
250
+ if (ok()) { c.beginPath(); c.ellipse(x, y + 5 * s, 3.5 * s, 2.5 * s, 0, 0, 7); c.fill(); }
251
+ stroke(c, circ(x - 7 * s, y - 5 * s, 2.2 * s), 1, true); stroke(c, circ(x + 7 * s, y - 5 * s, 2.2 * s), 1, true);
252
+ }
253
+ // punched example card with a drawing and its typed label
254
+ function exCard(c, x, y, rot, isCat, lbl) {
255
+ c.save(); c.translate(x, y); c.rotate(rot);
256
+ const pts = [[-62, -38], [62, -38], [62, 38], [-62, 38]];
257
+ erase(c, pts); stroke(c, pts, 1.2, true); if (drew()) free(() => stroke(c, [[63.4, -36.5], [63.4, 39.4], [-60.5, 39.4]], 1.3)); stroke(c, [[-62, -26], [-50, -38]], 0.8);
258
+ (isCat ? catLine : dogLine)(c, -26, 2, 0.95);
259
+ if (ok()) { c.font = '400 17px "Courier Prime"'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(lbl || (isCat ? 'cat' : 'dog'), 30, 0); }
260
+ stroke(c, [[10, 12], [50, 12]], 0.7);
261
+ for (let k = 0; k < 6; k++) if (ok()) { c.beginPath(); c.rect(14 + k * 7, 24, 3, 6); c.fill(); }
262
+ c.restore();
263
+ }
264
+ // odometer: row of digit drums
265
+ function odo(c, x, y, str, roll = 0, rollFrom = 0) {
266
+ let cx = x, di = -1;
267
+ for (const ch of str) {
268
+ if (ch === ',' || ch === '%') { if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(ch, cx + 7, y + (ch === ',' ? 14 : 2)); } cx += ch === ',' ? 14 : 30; continue; }
269
+ di++; const rl = di >= rollFrom ? roll : 0;
270
+ const b = [[cx, y - 22], [cx + 28, y - 22], [cx + 28, y + 22], [cx, y + 22]];
271
+ erase(c, b); stroke(c, b, 1.2, true); if (drew()) free(() => stroke(c, [[cx + 29.3, y - 21], [cx + 29.3, y + 23.3], [cx + 1, y + 23.3]], 1.1));
272
+ hatch(c, [[cx, y - 22], [cx + 28, y - 22], [cx + 28, y - 14], [cx, y - 14]], 0, 2, 0.5); hatch(c, [[cx, y + 14], [cx + 28, y + 14], [cx + 28, y + 22], [cx, y + 22]], 0, 2, 0.5);
273
+ if (ok()) { c.save(); c.beginPath(); c.rect(cx, y - 14, 28, 28); c.clip(); c.font = '700 24px "Libre Baskerville"'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(ch, cx + 14, y + 1 - rl * 28); if (rl) c.fillText(String((+ch + 1) % 10), cx + 14, y + 29 - rl * 28); c.restore(); }
274
+ cx += 32;
275
+ }
276
+ return cx;
277
+ }
278
+ function labelL(c, txt, tx, ty, px, py, bend = 0.15, p = 1, size = 28) {
279
+ if (p <= 0 || !ok()) return;
280
+ const mx = (tx + px) / 2, my = (ty + py) / 2, dx = px - tx, dy = py - ty, cx = mx - dy * bend, cy = my + dx * bend;
281
+ const above = txt && py < ty - size * 0.5;
282
+ const L = qp([tx + Math.sign(dx) * 6, above ? ty - size * 0.95 : ty + 10], [cx, cy], [px, py], 20);
283
+ stroke(c, partial(L, H.clamp(p * 1.4)), 0.9);
284
+ c.font = `italic 400 ${size}px "Libre Baskerville"`; c.textAlign = 'center'; c.textBaseline = 'alphabetic'; c.fillText(txt, tx, ty);
285
+ }
286
+ function redPencil(ctx, pts, p, seed) {
287
+ if (p <= 0) return; const r = H.rng(seed); const P = partial(pts, p);
288
+ ctx.save(); ctx.strokeStyle = 'rgba(178,52,40,0.85)'; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
289
+ [2.6, 1.2].forEach((w, k) => { ctx.lineWidth = w; ctx.beginPath(); P.forEach((q, i) => { const x = q[0] + r.range(-0.7, 0.7) + k * 1.2, y = q[1] + r.range(-0.7, 0.7) + k * 0.8; i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); }); ctx.stroke(); });
290
+ ctx.restore();
291
+ }
292
+
293
+ // ===== END EXPLAINER HELPERS =====
294
+ registerStyle({
295
+ id: 'patent-drawing',
296
+ name: 'Patent Drawing',
297
+ category: 'Illustration',
298
+ description: 'A 1930s US patent sheet: ruling-pen black ink line art with shade lines, parallel-line and stipple shading, section hatching, reference numerals on curved leaders, typeset header and inked signatures on yellowed paper.',
299
+ motion: 'Lines are inked one stroke at a time like a draughtsman at work — outlines first, then hatching, then numerals — while the FIG. 3 gear train slowly turns.',
300
+ fonts: ['400 17px "Libre Baskerville"', '700 26px "Libre Baskerville"', 'italic 400 15px "Libre Baskerville"', 'italic 400 34px "Cormorant Garamond"', '600 15px "Cormorant Garamond"'],
301
+ frameT: 6.4,
302
+ duration: 6.5,
303
+ readsPixels: true,
304
+ draw(ctx, W, H_, t) {
305
+ used = 0; budget = t >= 5 ? Infinity : Math.floor(Math.max(0, t - 0.25) * 158);
306
+ const r = H.rng(1931);
307
+ // slow documentary push-in toward FIG. 1
308
+ const zm = 1 + 0.018 * H.ease.inOut(H.clamp(t / 6.5));
309
+ ctx.translate(640, 360); ctx.scale(zm, zm); ctx.translate(-640, -360);
310
+ // ---------- paper ----------
311
+ ctx.drawImage(paperBase(W, H_), 0, 0);
312
+ const nz = H.makeNoise(77);
313
+ // age toning at edges
314
+ const g = ctx.createRadialGradient(W / 2, H_ / 2, 260, W / 2, H_ / 2, 820);
315
+ g.addColorStop(0, 'rgba(160,120,50,0)'); g.addColorStop(1, 'rgba(150,100,35,0.38)');
316
+ ctx.fillStyle = g; ctx.fillRect(0, 0, W, H_);
317
+ // tide stain bottom-left
318
+ ctx.save(); ctx.globalCompositeOperation = 'multiply';
319
+ for (let i = 0; i < 3; i++) {
320
+ const sg = ctx.createRadialGradient(40, 700, 10, 40, 700, 260 - i * 50);
321
+ sg.addColorStop(0, 'rgba(190,150,80,0.10)'); sg.addColorStop(0.92, 'rgba(180,135,60,0.12)'); sg.addColorStop(1, 'rgba(180,135,60,0)');
322
+ ctx.fillStyle = sg; ctx.fillRect(0, 380, 320, 340);
323
+ }
324
+ // foxing spots
325
+ for (let i = 0; i < 70; i++) {
326
+ let x = r() * W, y = r() * H_;
327
+ if (r.chance(0.6)) { if (r.chance(0.5)) x = r.chance(0.5) ? r() * 160 : W - r() * 160; else y = r.chance(0.5) ? r() * 110 : H_ - r() * 110; }
328
+ const rad = r.range(1.5, r.chance(0.15) ? 14 : 5);
329
+ const fg = ctx.createRadialGradient(x, y, 0, x, y, rad);
330
+ fg.addColorStop(0, `rgba(150,95,40,${r.range(0.18, 0.4)})`); fg.addColorStop(0.6, 'rgba(160,110,50,0.12)'); fg.addColorStop(1, 'rgba(160,110,50,0)');
331
+ ctx.fillStyle = fg; ctx.beginPath(); ctx.arc(x, y, rad, 0, 7); ctx.fill();
332
+ }
333
+ ctx.restore();
334
+ // fold crease
335
+ ctx.save();
336
+ ctx.strokeStyle = 'rgba(120,90,40,0.16)'; ctx.lineWidth = 1.2; ctx.beginPath(); ctx.moveTo(0, 361); ctx.lineTo(W, 357); ctx.stroke();
337
+ ctx.strokeStyle = 'rgba(255,250,230,0.35)'; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.moveTo(0, 363.5); ctx.lineTo(W, 359.5); ctx.stroke();
338
+ ctx.restore();
339
+
340
+ // ---------- ink layer ----------
341
+ const L = H.layer(W, H_), c = L.getContext('2d');
342
+ c.strokeStyle = '#000'; c.fillStyle = '#000'; c.lineCap = 'round'; c.lineJoin = 'round';
343
+
344
+ // border + header
345
+ stroke(c, [[18, 18], [W - 18, 18], [W - 18, H_ - 18], [18, H_ - 18]], 1.6, true);
346
+ stroke(c, [[23, 23], [W - 23, 23], [W - 23, H_ - 23], [23, H_ - 23]], 0.7, true);
347
+ c.textBaseline = 'alphabetic';
348
+ if (ok()) {
349
+ c.font = '400 15px "Libre Baskerville"';
350
+ c.textAlign = 'left'; c.fillText('Dec. 5, 1933.', 48, 52);
351
+ c.textAlign = 'right'; c.fillText('Patent No. 1,938,412', W - 48, 52);
352
+ c.textAlign = 'center';
353
+ c.font = '400 15px "Libre Baskerville"'; c.fillText('E. M. HOLLOWAY', W / 2, 52);
354
+ c.font = '700 19px "Libre Baskerville"';
355
+ const title = 'THINKING MACHINE'; c.fillText(title.split('').join(String.fromCharCode(8202)), W / 2, 78);
356
+ c.font = 'italic 400 13px "Libre Baskerville"'; c.fillText('Filed Aug. 2, 1931', W / 2, 98);
357
+ c.textAlign = 'right'; c.font = '400 13px "Libre Baskerville"'; c.fillText('2 Sheets-Sheet 1', W - 48, 78);
358
+ }
359
+
360
+ // ================= FIG. 1 : the machine (isometric) =================
361
+ const s = 1.16, ox = 322, oy = 338, cs = Math.cos(Math.PI / 6), sn = 0.5;
362
+ const P = (x, y, z) => [ox + (x - y) * cs * s, oy + (x + y) * sn * s - z * s];
363
+ const q = (pts) => pts.map((p) => P(...p));
364
+ const X1 = 230, Y1 = 130, Z1 = 150;
365
+
366
+ // plinth
367
+ box(c, P, -12, X1 + 12, -12, Y1 + 12, -20, 0, { shadeRight: 3.2 });
368
+ // plinth front moulding line
369
+ stroke(c, q([[-12, Y1 + 12, -6], [X1 + 12, Y1 + 12, -6], [X1 + 12, -12, -6]]), 0.8);
370
+ // cabinet
371
+ const cab = box(c, P, 0, X1, 0, Y1, 0, Z1, { shadeRight: 4.2 });
372
+ // top lid lip
373
+ stroke(c, q([[0, Y1, Z1 - 7], [X1, Y1, Z1 - 7], [X1, 0, Z1 - 7]]), 0.9);
374
+
375
+ // --- front panel (y = Y1) ---
376
+ const FY = Y1;
377
+ stroke(c, q([[14, FY, 16], [X1 - 14, FY, 16], [X1 - 14, FY, 132], [14, FY, 132]]), 1.0, true);
378
+ stroke(c, q([[19, FY, 21], [X1 - 19, FY, 21], [X1 - 19, FY, 127], [19, FY, 127]]), 0.7, true);
379
+ // panel bevel shading (lower and right inner strip)
380
+ hatch(c, q([[14, FY, 16], [X1 - 14, FY, 16], [X1 - 19, FY, 21], [19, FY, 21]]), Math.PI / 2, 2.2, 0.6);
381
+ // dials
382
+ const dials = [[58, 102], [115, 102], [172, 102]];
383
+ dials.forEach(([dx, dz], i) => {
384
+ const rim = q(ring([dx, FY + 3, dz], 'y', 19, 60));
385
+ erase(c, rim);
386
+ lathe(c, P, [dx, FY, dz], 'y', [[20, 0], [20, 3], [17, 3.5]], { cap: true, dens: 26 });
387
+ stroke(c, q(ring([dx, FY + 3.6, dz], 'y', 14.5, 60)), 0.7, true);
388
+ // ticks
389
+ c.beginPath();
390
+ for (let k = 0; k <= 10; k++) {
391
+ const a = Math.PI * (1.15 + 0.07 * k * 1.0) + Math.PI * 0.02;
392
+ const aa = Math.PI * 0.85 + k * (Math.PI * 1.3 / 10);
393
+ const p1 = P(dx + Math.cos(aa) * 13.5, FY + 3.6, dz - Math.sin(aa) * 13.5), p2 = P(dx + Math.cos(aa) * (k % 5 ? 11.5 : 10), FY + 3.6, dz - Math.sin(aa) * (k % 5 ? 11.5 : 10));
394
+ c.moveTo(p1[0], p1[1]); c.lineTo(p2[0], p2[1]);
395
+ }
396
+ if (ok()) { c.lineWidth = 0.7; c.stroke(); }
397
+ const na = Math.PI * 0.85 + (0.25 + 0.5 * (0.5 + 0.5 * Math.sin(t * 1.3 + i * 2))) * Math.PI * 1.3;
398
+ stroke(c, [P(dx, FY + 3.8, dz), P(dx + Math.cos(na) * 12, FY + 3.8, dz - Math.sin(na) * 12)], 1.4);
399
+ const hp = P(dx, FY + 3.8, dz); if (ok()) { c.beginPath(); c.arc(hp[0], hp[1], 2, 0, 7); c.fill(); }
400
+ });
401
+ // keys row
402
+ for (let k = 0; k < 8; k++) {
403
+ const kx = 34 + k * 23;
404
+ lathe(c, P, [kx, FY, 66], 'y', [[6.2, 0], [6.2, 7], [7.4, 7.5], [7.4, 10]], { dens: 30 });
405
+ }
406
+ // tape slot
407
+ stroke(c, q([[78, FY, 28], [152, FY, 28], [152, FY, 36], [78, FY, 36]]), 1.1, true);
408
+ if (ok()) { path(c, q([[80, FY, 30], [150, FY, 30], [150, FY, 34], [80, FY, 34]]), true); c.fill(); }
409
+ // screws in corners
410
+ [[10, 8], [X1 - 10, 8], [10, Z1 - 16], [X1 - 10, Z1 - 16]].forEach(([sx, sz]) => {
411
+ const sc = q(ring([sx, FY, sz], 'y', 3, 20)); stroke(c, sc, 0.8, true);
412
+ stroke(c, [P(sx - 2, FY, sz - 1), P(sx + 2, FY, sz + 1)], 0.8);
413
+ });
414
+
415
+ // --- right face (x = X1): louvres + crank ---
416
+ for (let k = 0; k < 6; k++) {
417
+ const z = 18 + k * 7.5;
418
+ const sl = q([[X1, 18, z], [X1, 62, z], [X1, 62, z + 3.6], [X1, 18, z + 3.6]]);
419
+ erase(c, sl); if (ok()) { path(c, sl, true); c.fill(); }
420
+ }
421
+ // hidden shaft (dashed)
422
+ stroke(c, [P(X1, 92, 92), P(X1 - 120, 92, 92)], 0.8, false, [6, 4]);
423
+ lathe(c, P, [X1, 92, 92], 'x', [[17, 0], [17, 6], [11, 6], [11, 12]], { dens: 24 });
424
+ // crank arm
425
+ const ca0 = P(X1 + 12, 92, 92);
426
+ const ang = -2.3 + t * 0.6;
427
+ const ce = [X1 + 14, 92 + Math.cos(ang) * 34, 92 + Math.sin(ang) * 34];
428
+ const pa = P(X1 + 12, 92, 92), pb = P(...ce);
429
+ {
430
+ const dx = pb[0] - pa[0], dy = pb[1] - pa[1], m = Math.hypot(dx, dy), nx = -dy / m * 5, ny = dx / m * 5;
431
+ const arm = [[pa[0] + nx, pa[1] + ny], [pb[0] + nx * 0.7, pb[1] + ny * 0.7], [pb[0] - nx * 0.7, pb[1] - ny * 0.7], [pa[0] - nx, pa[1] - ny]];
432
+ erase(c, arm); stroke(c, arm, 1.2, true);
433
+ hatch(c, arm, Math.atan2(dy, dx), 2.2, 0.6);
434
+ }
435
+ lathe(c, P, [X1 + 14, ce[1], ce[2]], 'x', [[4.5, 0], [4.5, 4], [3.6, 4], [3.6, 26], [4.2, 27], [2, 30]], { dens: 40 });
436
+
437
+ // --- top: tubes + dome ---
438
+ const tube = (tx, ty) => {
439
+ lathe(c, P, [tx, ty, Z1], 'z', [[12, 0], [12, 9], [9, 9]], { dens: 30 });
440
+ const prof = [[8.5, 9], [9, 14], [9, 40]];
441
+ for (let a = 0.2; a <= Math.PI / 2; a += 0.2) prof.push([9 * Math.cos(a), 40 + 9 * Math.sin(a)]);
442
+ prof.push([0.01, 49]);
443
+ lathe(c, P, [tx, ty, Z1], 'z', prof, { rims: [], cap: false, dens: 18 });
444
+ // inner plate (seen through glass)
445
+ stroke(c, q([[tx - 4, ty + 4, Z1 + 18], [tx - 4, ty + 4, Z1 + 36]]), 0.7);
446
+ stroke(c, q([[tx + 4, ty - 4, Z1 + 18], [tx + 4, ty - 4, Z1 + 36]]), 0.7);
447
+ stroke(c, q([[tx - 4, ty + 4, Z1 + 36], [tx + 4, ty - 4, Z1 + 36]]), 0.7);
448
+ stroke(c, q([[tx, ty, Z1 + 36], [tx, ty, Z1 + 47]]), 0.6);
449
+ // glass glint
450
+ const g1 = P(tx - 5, ty + 6, Z1 + 38), g2 = P(tx - 5, ty + 6, Z1 + 22);
451
+ stroke(c, [[g1[0] - 1, g1[1]], [g2[0] - 1, g2[1]]], 0.6);
452
+ };
453
+ tube(30, 22); tube(205, 26);
454
+
455
+ // dome base
456
+ const DC = [115, 65, Z1];
457
+ lathe(c, P, DC, 'z', [[60, 0], [60, 10], [56, 10], [56, 16], [52, 16]], { dens: 30 });
458
+ // glass dome (hemisphere)
459
+ const prof = [];
460
+ const JH = 38; prof.push([52, 16]);
461
+ for (let a = 0; a <= Math.PI / 2 + 0.001; a += Math.PI / 24) prof.push([52 * Math.cos(a) + 0.01, 16 + JH + 52 * Math.sin(a)]);
462
+ const dome = (() => {
463
+ const all = [];
464
+ prof.forEach(([rr, h]) => ring([DC[0], DC[1], Z1 + h], 'z', rr, 72).forEach((p) => all.push(P(...p))));
465
+ return hull(all);
466
+ })();
467
+ erase(c, dome);
468
+ // brain inside dome (side view)
469
+ const bc = P(115, 65, Z1 + 62);
470
+ const brx = 50 * s, bry = 33 * s;
471
+ const brain = [];
472
+ for (let k = 0; k < 120; k++) {
473
+ const a = k / 120 * Math.PI * 2;
474
+ let x = Math.cos(a), y = Math.sin(a);
475
+ if (y > 0) y *= 0.78 - 0.18 * Math.max(0, -x) ; // flatter base, frontal tuck
476
+ if (x > 0.2 && y > 0) y *= 0.92;
477
+ if (y < 0) y *= 1 + 0.08 * Math.cos(a * 2 + 0.6);
478
+ const w = 1 + 0.025 * Math.sin(a * 11) + 0.015 * Math.sin(a * 23 + 1);
479
+ brain.push([bc[0] + x * brx * w, bc[1] + y * bry * w]);
480
+ }
481
+ // cerebellum (back-bottom) + stem
482
+ const cbc = [bc[0] + brx * 0.48, bc[1] + bry * 0.62];
483
+ const cereb = ring([cbc[0], cbc[1], 0], 'z', 1, 50).map((p3) => [cbc[0] + (p3[0] - cbc[0]) * 22 * s * 0.9, cbc[1] + (p3[1] - cbc[1]) * 12 * s * 0.9]);
484
+ const stemTop = [bc[0] + brx * 0.18, bc[1] + bry * 0.5], stemBot = [bc[0] + brx * 0.22, P(115, 65, Z1 + 16)[1] + 4];
485
+ // electrode stalks
486
+ [[-0.5, 8], [0.62, 10]].forEach(([ex, ey]) => {
487
+ const top = [bc[0] + ex * brx, bc[1] + bry * 0.55], bot = [bc[0] + ex * brx * 1.2, P(115, 65, Z1 + 16)[1] + ey];
488
+ stroke(c, [top, bot], 1.4);
489
+ c.beginPath(); for (let k = 0; k < 6; k++) { const yy = H.lerp(top[1] + 8, bot[1] - 5, k / 6); const xx = H.lerp(top[0], bot[0], k / 6); c.moveTo(xx - 4, yy); c.lineTo(xx + 4, yy + 2.5); }
490
+ if (ok()) { c.lineWidth = 0.8; c.stroke(); }
491
+ });
492
+ {
493
+ const st = [[stemTop[0] - 7, stemTop[1]], [stemTop[0] + 7, stemTop[1]], [stemBot[0] + 6, stemBot[1]], [stemBot[0] - 6, stemBot[1]]];
494
+ erase(c, st); stroke(c, [st[0], st[3]], 1.2); stroke(c, [st[1], st[2]], 1.6);
495
+ hatch(c, st, Math.PI / 2 + 0.05, 2.0, 0.55);
496
+ }
497
+ erase(c, cereb);
498
+ if (ok()) {
499
+ c.save(); path(c, cereb, true); c.clip(); c.beginPath();
500
+ for (let k = -6; k <= 6; k++) { const yy = cbc[1] + k * 2.3; c.moveTo(cbc[0] - 30, yy + 4); c.quadraticCurveTo(cbc[0], yy - 3, cbc[0] + 30, yy + 2); }
501
+ c.lineWidth = 0.6; c.stroke(); c.restore();
502
+ }
503
+ stroke(c, cereb, 1.2, true);
504
+ erase(c, brain);
505
+ // gyri: contour lines of a domain-warped stripe field
506
+ if (ok()) {
507
+ c.save(); path(c, brain, true); c.clip();
508
+ const bn = H.makeNoise(5);
509
+ const f = (x, y) => {
510
+ const u = (x - bc[0]) / brx, v = (y - bc[1]) / bry;
511
+ const rr = Math.hypot(u * 1.0, v * 1.25);
512
+ const wx = 6 * bn.fbm2(x * 0.03, y * 0.03, 2), wy = 6 * bn.fbm2(x * 0.03 + 7, y * 0.03 + 3, 2);
513
+ return bn.n2((x + wx) * 0.105, (y + wy) * 0.15) + 0.12 * Math.sin(u * 3);
514
+ };
515
+ contours(c, bc[0] - brx - 4, bc[1] - bry - 6, brx * 2 + 8, bry * 2 + 10, 1.8, f, 0.04);
516
+ c.lineWidth = 1.15; c.stroke();
517
+ contours(c, bc[0] - brx - 4, bc[1] - bry - 6, brx * 2 + 8, bry * 2 + 10, 1.8, f, -0.2);
518
+ c.lineWidth = 0.6; c.stroke();
519
+ // lateral (Sylvian) fissure + central sulcus
520
+ c.beginPath(); c.moveTo(bc[0] - brx * 0.55, bc[1] + bry * 0.32); c.bezierCurveTo(bc[0] - brx * 0.2, bc[1] + bry * 0.05, bc[0] + brx * 0.1, bc[1] - bry * 0.02, bc[0] + brx * 0.38, bc[1] - bry * 0.12);
521
+ c.moveTo(bc[0] - brx * 0.02, bc[1] - bry * 0.98); c.bezierCurveTo(bc[0] + brx * 0.06, bc[1] - bry * 0.5, bc[0] - brx * 0.08, bc[1] - bry * 0.3, bc[0] + brx * 0.02, bc[1] + bry * 0.05);
522
+ c.lineWidth = 1.7; c.stroke();
523
+ c.restore();
524
+ stipple(c, brain, 2200, (x, y) => { const u = (x - bc[0]) / brx, v = (y - bc[1]) / bry; return H.clamp((Math.hypot(u, v) - 0.55) * 1.6) * H.clamp(0.25 + 0.6 * (u * 0.4 + v)) * 0.9; }, H.rng(9), 0.7);
525
+ }
526
+ stroke(c, brain, 1.5, true);
527
+ // dome outline + glass glints
528
+ stroke(c, dome, 1.35, true);
529
+ {
530
+ // glass glints: vertical strokes on the jar's left wall + arcs on the crown
531
+ const glL = (h0, h1, aa) => { const p0 = P(115 + Math.cos(aa) * 50, 65 + Math.sin(aa) * 50, Z1 + h0), p1 = P(115 + Math.cos(aa) * 50, 65 + Math.sin(aa) * 50, Z1 + h1); return [p0, p1]; };
532
+ [[2.2, 24, 70], [2.32, 28, 64], [2.44, 34, 60]].forEach(([aa, h0, h1]) => stroke(c, glL(h0, h1, aa), 0.8));
533
+ const cc0 = P(115, 65, Z1 + 16 + JH);
534
+ c.beginPath();
535
+ for (let k = 0; k < 3; k++) { const rr = 50 * s * 1.06 - k * 5; c.moveTo(cc0[0] + Math.cos(Math.PI * 1.18) * rr, cc0[1] + Math.sin(Math.PI * 1.18) * rr); c.arc(cc0[0], cc0[1], rr, Math.PI * 1.18, Math.PI * (1.42 - k * 0.04)); }
536
+ if (ok()) { c.lineWidth = 0.8; c.stroke(); }
537
+ // glass edge stipple on the shadow side
538
+ stipple(c, dome, 1800, (x, y) => H.clamp((x - (cc0[0] + 30)) / 40) * 0.35, H.rng(4), 0.6);
539
+ // hidden far rim of base seen through glass (dashed)
540
+ const far = []; for (let k = 0; k <= 36; k++) { const a = Math.PI * 0.75 + k / 36 * Math.PI; far.push(P(115 + Math.cos(a) * 52, 65 + Math.sin(a) * 52, Z1 + 16)); }
541
+ stroke(c, far, 0.6, false, [4, 3]);
542
+ }
543
+ // top knob on dome
544
+ lathe(c, P, [115, 65, Z1 + 16 + JH + 51], 'z', [[6, 0], [6, 5], [2.5, 5], [2.5, 11], [5, 12], [0.1, 16]], { dens: 30 });
545
+
546
+ // cable from dome base to right tube
547
+ {
548
+ const p0 = P(160, 70, Z1 + 6), p1 = P(205, 38, Z1 + 4);
549
+ c.beginPath(); c.moveTo(p0[0], p0[1]); c.bezierCurveTo(p0[0] + 30, p0[1] + 6, p1[0] - 10, p1[1] + 22, p1[0] - 2, p1[1] + 6);
550
+ if (ok()) { c.lineWidth = 3.4; c.stroke(); c.save(); c.globalCompositeOperation = 'destination-out'; c.lineWidth = 1.4; c.stroke(); c.restore(); }
551
+ }
552
+
553
+ // --- punched tape ---
554
+ {
555
+ const cl = catmull([[130, 32], [160, 31.5], [186, 24], [204, 6], [214, -10], [226, -20], [258, -20], [282, -17], [292, -6], [286, 4], [272, 0], [272, -12]], 10);
556
+ const xa = 88, xb = 142;
557
+ for (let i = 0; i < cl.length - 1; i++) {
558
+ const [y0, z0] = cl[i], [y1, z1] = cl[i + 1];
559
+ const quad = q([[xa, y0, z0], [xb, y0, z0], [xb, y1, z1], [xa, y1, z1]]);
560
+ const cr = (quad[1][0] - quad[0][0]) * (quad[3][1] - quad[0][1]) - (quad[1][1] - quad[0][1]) * (quad[3][0] - quad[0][0]);
561
+ erase(c, quad.map((p, j) => [p[0] + (j < 2 ? -0.3 : 0.3), p[1]]));
562
+ if (cr < 0) hatch(c, quad, 0.52, 2.2, 0.55); // underside
563
+ else if (i % 2 === 0) {
564
+ // punched holes: 5 lanes
565
+ const tr = H.rng(100 + i);
566
+ c.beginPath();
567
+ for (let ln = 0; ln < 6; ln++) {
568
+ if (ln === 2) { const pp = P(H.lerp(xa, xb, 0.42), (y0 + y1) / 2, (z0 + z1) / 2); c.moveTo(pp[0] + 0.9, pp[1]); c.arc(pp[0], pp[1], 0.9, 0, 7); continue; }
569
+ if (tr.chance(0.5)) { const pp = P(H.lerp(xa + 6, xb - 6, ln / 5), (y0 + y1) / 2, (z0 + z1) / 2); c.moveTo(pp[0] + 2, pp[1]); c.ellipse(pp[0], pp[1], 2.1, 1.6, 0, 0, 7); }
570
+ }
571
+ if (ok()) c.fill();
572
+ }
573
+ stroke(c, [quad[0], quad[3]], 1.1); stroke(c, [quad[1], quad[2]], 1.1);
574
+ if (i === cl.length - 2) stroke(c, [quad[2], quad[3]], 1.1);
575
+ }
576
+ }
577
+
578
+ // ---- FIG.1 numerals ----
579
+ c.fillStyle = '#000';
580
+ const L1 = [
581
+ ['10', P(8, Y1, 140), -62, -18, 0.15],
582
+ ['12', P(70, 40, Z1 + 60), -82, -20, -0.15],
583
+ ['14', [0, 0], 0, 0, 0],
584
+ ['16', P(58, Y1 + 3, 118), -84, -6, 0.18],
585
+ ['18', P(34, Y1 + 10, 60), -66, 26, -0.15],
586
+ ['20', P(128, 262, -20), 66, 52, 0.15],
587
+ ['22', P(X1 + 26, ce[1], ce[2]), 66, -30, 0.2],
588
+ ['24', P(212, 30, Z1 + 40), 60, -42, 0.2],
589
+ ['26', P(X1, 40, 22), 78, 44, 0.2],
590
+ ['28', P(X1 + 12, 70, -16), 56, 60, -0.18],
591
+ ];
592
+ L1[2] = ['14', [bc[0] - brx * 0.35, bc[1] - bry * 0.55], -58, -92, 0.12];
593
+ L1.forEach(([n, p, dx, dy, b]) => leader(c, n, p[0] + dx, p[1] + dy, p[0], p[1], b, r));
594
+ // section line 2—2 through tube 24
595
+ {
596
+ const a = P(205, 26, Z1 + 25);
597
+ stroke(c, [[a[0] - 30, a[1]], [a[0] - 14, a[1]]], 1.3); stroke(c, [[a[0] + 14, a[1]], [a[0] + 30, a[1]]], 1.3);
598
+ if (ok()) {
599
+ [[a[0] - 30, -1], [a[0] + 30, -1]].forEach(([x]) => { c.beginPath(); c.moveTo(x, a[1]); c.lineTo(x, a[1] - 9); c.stroke(); c.beginPath(); c.moveTo(x - 3, a[1] - 5); c.lineTo(x, a[1] - 10); c.lineTo(x + 3, a[1] - 5); c.fill(); });
600
+ c.font = '400 13px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('2', a[0] - 30, a[1] + 13); c.fillText('2', a[0] + 30, a[1] + 13);
601
+ }
602
+ }
603
+ if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'left'; c.fillText('FIG. 1.', 470, 668); }
604
+
605
+ // ================= FIG. 2 : section of cerebral valve =================
606
+ const cx = 872, sy = 1.0;
607
+ const env = (y) => { // outer half width
608
+ if (y >= 440) return 50; if (y >= 405) return H.lerp(50, 62, (440 - y) / 35);
609
+ if (y >= 262) return 62; const u = (262 - y) / 104; return 62 * SQ(Math.max(0, 1 - u * u));
610
+ };
611
+ const outer = [], inner = [];
612
+ for (let y = 440; y >= 158; y -= 3) outer.push([cx + env(y), y]);
613
+ for (let y = 158; y <= 440; y += 3) outer.push([cx - env(y), y]);
614
+ for (let y = 436; y >= 164; y -= 3) inner.push([cx + Math.max(0, env(y) - 4.5), y]);
615
+ for (let y = 164; y <= 436; y += 3) inner.push([cx - Math.max(0, env(y) - 4.5), y]);
616
+ // glass wall (section) — thin, lightly stippled
617
+ stroke(c, outer, 1.4, true); stroke(c, inner, 0.9, true);
618
+ // tip pip
619
+ stroke(c, [[cx - 4, 160], [cx - 3, 148], [cx, 145], [cx + 3, 148], [cx + 4, 160]], 1.1);
620
+ // base (bakelite) cross-hatched section
621
+ const base = [[cx - 66, 440], [cx + 66, 440], [cx + 66, 486], [cx + 60, 494], [cx - 60, 494], [cx - 66, 486]];
622
+ stroke(c, base, 1.4, true);
623
+ hatch(c, base, Math.PI / 4, 6, 0.7); hatch(c, base, -Math.PI / 4, 6, 0.7);
624
+ // pins
625
+ [-38, -14, 14, 38].forEach((px, i) => {
626
+ const pin = [[cx + px - 4.5, 494], [cx + px + 4.5, 494], [cx + px + 4.5, 528], [cx + px, 534], [cx + px - 4.5, 528]];
627
+ erase(c, pin); stroke(c, pin, 1.2, true);
628
+ hatch(c, pin, Math.PI / 4, 2.6, 0.6);
629
+ });
630
+ // stem press
631
+ const press = [[cx - 30, 440], [cx + 30, 440], [cx + 16, 404], [cx + 16, 388], [cx - 16, 388], [cx - 16, 404]];
632
+ stroke(c, press, 1.1, true);
633
+ stipple(c, press, 900, () => 0.35, H.rng(21), 0.6);
634
+ // lead wires
635
+ [-38, -14, 14, 38].forEach((px, i) => {
636
+ const tx2 = [-44, -28, 0, 44][i];
637
+ stroke(c, [[cx + px, 494], [cx + px * 0.5, 440], [cx + tx2 * 0.25, 404], [cx + tx2, 378]], 0.9);
638
+ });
639
+ // micas
640
+ [[222, 228], [372, 378]].forEach(([a, b]) => {
641
+ const m = [[cx - 55, a], [cx + 55, a], [cx + 55, b], [cx - 55, b]];
642
+ erase(c, m); stroke(c, m, 1.0, true);
643
+ stipple(c, m, 500, () => 0.5, H.rng(a), 0.55);
644
+ });
645
+ // plate walls sectioned
646
+ [[-48, -40], [40, 48]].forEach(([a, b]) => {
647
+ const pl = [[cx + a, 236], [cx + b, 236], [cx + b, 364], [cx + a, 364]];
648
+ stroke(c, pl, 1.3, true); hatch(c, pl, Math.PI / 4, 3.4, 0.7);
649
+ });
650
+ // plate top/bottom tabs
651
+ stroke(c, [[cx - 44, 236], [cx - 44, 228]], 1); stroke(c, [[cx + 44, 236], [cx + 44, 228]], 1);
652
+ stroke(c, [[cx - 44, 364], [cx - 44, 372]], 1); stroke(c, [[cx + 44, 364], [cx + 44, 372]], 1);
653
+ // grid side rods + helix wire sections
654
+ stroke(c, [[cx - 30, 222], [cx - 30, 378]], 0.9); stroke(c, [[cx + 30, 222], [cx + 30, 378]], 0.9);
655
+ if (ok()) {
656
+ c.beginPath();
657
+ for (let y = 242; y <= 360; y += 7.5) { [cx - 26, cx + 26].forEach((x) => { c.moveTo(x + 2.2, y); c.arc(x, y, 2.2, 0, 7); }); }
658
+ c.fill();
659
+ // helix visible behind (thin diagonal)
660
+ c.beginPath(); for (let y = 242; y <= 352; y += 7.5) { c.moveTo(cx - 24, y); c.lineTo(cx + 24, y + 3.7); }
661
+ c.lineWidth = 0.4; c.setLineDash([2, 2.5]); c.stroke(); c.setLineDash([]);
662
+ }
663
+ // cathode
664
+ [[-7, -3], [3, 7]].forEach(([a, b]) => { const k = [[cx + a, 230], [cx + b, 230], [cx + b, 370], [cx + a, 370]]; if (ok()) { path(c, k, true); c.fill(); } });
665
+ stroke(c, (() => { const z = []; for (let y = 236; y <= 364; y += 8) { z.push([cx - 1.6, y]); z.push([cx + 1.6, y + 4]); } return z; })(), 0.7);
666
+ // getter
667
+ stroke(c, [[cx, 222], [cx, 204]], 0.9);
668
+ const get = [[cx - 12, 196], [cx + 12, 196], [cx + 9, 204], [cx - 9, 204]]; stroke(c, get, 1.1, true); hatch(c, get, Math.PI / 4, 2.4, 0.6);
669
+ // center line (dash-dot)
670
+ stroke(c, [[cx, 128], [cx, 552]], 0.6, false, [16, 4, 2, 4]);
671
+ // numerals
672
+ [['30', cx + 112, 196, [cx + 47, 215], 0.15], ['32', cx + 112, 268, [cx + 45, 280], -0.1], ['34', cx - 112, 300, [cx - 27, 302], 0.12],
673
+ ['36', cx - 112, 220, [cx - 5, 250], -0.15], ['38', cx + 112, 372, [cx + 52, 375], 0.1], ['40', cx + 112, 470, [cx + 54, 470], -0.12],
674
+ ['42', cx - 96, 545, [cx - 38, 520], 0.15], ['44', cx - 82, 168, [cx - 10, 199], -0.12]]
675
+ .forEach(([n, tx, ty, p, b]) => leader(c, n, tx, ty, p[0], p[1], b, r));
676
+ if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('FIG. 2.', cx, 590); }
677
+
678
+ // ================= FIG. 3 : gear train =================
679
+ const gear = (gx, gy, R, n, th, holes, spokes) => {
680
+ const pts = [], p = Math.PI * 2 / n, Ro = R + 5.5, Rr = R - 6;
681
+ for (let k = 0; k < n; k++) {
682
+ const a = th + k * p;
683
+ [[a - 0.27 * p, Rr], [a - 0.15 * p, Ro], [a + 0.15 * p, Ro], [a + 0.27 * p, Rr], [a + 0.5 * p, Rr]].forEach(([aa, rr]) => pts.push([gx + Math.cos(aa) * rr, gy + Math.sin(aa) * rr]));
684
+ }
685
+ erase(c, pts); stroke(c, pts, 1.3, true);
686
+ // rim
687
+ const rim = ring([gx, gy, 0], 'z', R - 14, 64).map((p3) => [p3[0], p3[1]]);
688
+ stroke(c, rim, 1.0, true);
689
+ // shading on rim (concentric lines lower-right)
690
+ c.save(); path(c, pts, true); c.clip();
691
+ c.beginPath(); for (let k = 1; k < 4; k++) { c.moveTo(gx + Math.cos(-0.3) * (R - 14 + k * 2.4), gy + Math.sin(-0.3) * (R - 14 + k * 2.4)); c.arc(gx, gy, R - 14 + k * 2.4, -0.3, 1.9); }
692
+ if (ok()) { c.lineWidth = 0.6; c.stroke(); } c.restore();
693
+ // hub
694
+ stroke(c, ring([gx, gy, 0], 'z', 15, 40).map((p3) => [p3[0], p3[1]]), 1.1, true);
695
+ const bore = ring([gx, gy, 0], 'z', 6.5, 30).map((p3) => [p3[0], p3[1]]);
696
+ if (ok()) { path(c, bore, true); c.fill(); }
697
+ // spokes / holes
698
+ if (spokes) {
699
+ for (let k = 0; k < spokes; k++) {
700
+ const a = th * 1 + k * Math.PI * 2 / spokes, ca = Math.cos(a), sa = Math.sin(a), nx = -sa * 4.5, ny = ca * 4.5;
701
+ const sp = [[gx + ca * 15 + nx, gy + sa * 15 + ny], [gx + ca * (R - 14) + nx, gy + sa * (R - 14) + ny], [gx + ca * (R - 14) - nx, gy + sa * (R - 14) - ny], [gx + ca * 15 - nx, gy + sa * 15 - ny]];
702
+ stroke(c, [sp[0], sp[1]], 1.0); stroke(c, [sp[2], sp[3]], 1.6);
703
+ }
704
+ // fill openings between spokes with dark? keep open (paper)
705
+ } else {
706
+ for (let k = 0; k < holes; k++) {
707
+ const a = th + k * Math.PI * 2 / holes + 0.4, hr = (R - 14 + 15) / 2;
708
+ const hc = ring([gx + Math.cos(a) * hr, gy + Math.sin(a) * hr, 0], 'z', (R - 14 - 15) / 2 - 3, 28).map((p3) => [p3[0], p3[1]]);
709
+ stroke(c, hc, 1.0, true);
710
+ c.save(); path(c, hc, true); c.clip(); c.beginPath(); const hx = gx + Math.cos(a) * hr, hy = gy + Math.sin(a) * hr; c.arc(hx - 1.4, hy - 1.4, (R - 14 - 15) / 2 - 3, -2.2, -0.2); if (ok()) { c.lineWidth = 2.2; c.stroke(); } c.restore();
711
+ }
712
+ }
713
+ // pitch circle dash-dot
714
+ stroke(c, ring([gx, gy, 0], 'z', R, 90).map((p3) => [p3[0], p3[1]]), 0.5, true, [12, 3, 2, 3]);
715
+ stroke(c, [[gx - R - 16, gy], [gx + R + 16, gy]], 0.5, false, [12, 3, 2, 3]);
716
+ stroke(c, [[gx, gy - R - 16], [gx, gy + R + 16]], 0.5, false, [12, 3, 2, 3]);
717
+ };
718
+ const g1 = [1108, 228], R1 = 62, n1 = 24, R2 = 40, n2 = 16, al = 1.15;
719
+ const g2 = [g1[0] + Math.cos(al) * (R1 + R2), g1[1] + Math.sin(al) * (R1 + R2)];
720
+ const th1 = t * 0.35, th2 = al + Math.PI + Math.PI / n2 - (th1 - al) * n1 / n2;
721
+ gear(g2[0], g2[1], R2, n2, th2, 4, 0);
722
+ gear(g1[0], g1[1], R1, n1, th1, 0, 5);
723
+ [['46', g1[0] - 98, g1[1] - 70, [g1[0] - 44, g1[1] - 44], 0.15], ['48', g2[0] + 74, g2[1] + 42, [g2[0] + 30, g2[1] + 22], -0.15], ['50', g1[0] + 94, g1[1] - 60, [g1[0] + 9, g1[1] - 9], 0.1]]
724
+ .forEach(([n, tx, ty, p, b]) => leader(c, n, tx, ty, p[0], p[1], b, r));
725
+ if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('FIG. 3.', 1112, 428); }
726
+
727
+ // ================= signatures =================
728
+ if (ok()) {
729
+ c.textAlign = 'left';
730
+ c.font = '600 15px "Cormorant Garamond"'; c.fillText('INVENTOR.', 1012, 540);
731
+ c.font = '600 15px "Cormorant Garamond"'; c.fillText('BY', 1012, 636);
732
+ c.font = '600 15px "Cormorant Garamond"'; c.fillText('ATTORNEYS.', 1150, 664);
733
+ // witnesses
734
+ c.fillText('WITNESSES:', 54, 628);
735
+ }
736
+
737
+ // ---------- composite ink onto paper ----------
738
+ ctx.save();
739
+ ctx.globalCompositeOperation = 'multiply';
740
+ ctx.filter = 'blur(1.1px)'; ctx.globalAlpha = 0.28; ctx.drawImage(L, 0, 0);
741
+ ctx.filter = 'none'; ctx.globalAlpha = 0.9; ctx.drawImage(L, 0, 0);
742
+ ctx.restore();
743
+ // re-tint ink to warm iron-black & add slight unevenness
744
+ const img = ctx.getImageData(0, 0, W, H_), d = img.data;
745
+ for (let i = 0; i < d.length; i += 4) {
746
+ const lum = (d[i] + d[i + 1] + d[i + 2]) / 3;
747
+ if (lum < 150) {
748
+ const p = i / 4, x = p % W, y = (p / W) | 0;
749
+ const k = 1 - lum / 150;
750
+ const v = 0.82 + 0.18 * (nz.n2(x * 0.03, y * 0.03) * 0.5 + 0.5);
751
+ d[i] = H.lerp(d[i], 30, k * 0.4) + (1 - v) * 40 * k; d[i + 1] = H.lerp(d[i + 1], 24, k * 0.4) + (1 - v) * 34 * k; d[i + 2] = H.lerp(d[i + 2], 18, k * 0.4) + (1 - v) * 24 * k;
752
+ }
753
+ }
754
+ ctx.putImageData(img, 0, 0);
755
+ // hand-signed names in brown iron-gall ink (separate from the printed drawing)
756
+ if (t > 4.7) {
757
+ ctx.save(); ctx.fillStyle = 'rgba(52,34,20,0.9)'; ctx.strokeStyle = 'rgba(52,34,20,0.85)'; ctx.lineCap = 'round';
758
+ // each signature is written on left-to-right (pen speed ~ 0.6 s per name)
759
+ const sign = (txt, x, y, size, sk, flo, t0) => {
760
+ const pr = H.clamp((t - t0) / 0.6); if (pr <= 0) return;
761
+ ctx.save(); ctx.font = `italic 400 ${size}px "Cormorant Garamond"`;
762
+ const w = ctx.measureText(txt).width;
763
+ ctx.beginPath(); ctx.rect(x - 30, y - size * 1.2, (w + 60) * pr, size * 2); ctx.clip();
764
+ ctx.save(); ctx.translate(x, y); ctx.transform(1, 0, sk, 1, 0, 0);
765
+ ctx.fillText(txt, 0, 0); ctx.globalAlpha = 0.5; ctx.fillText(txt, 0.6, 0.3); ctx.globalAlpha = 1;
766
+ ctx.restore();
767
+ if (flo) { ctx.beginPath(); ctx.moveTo(x - 6, y + 9); ctx.bezierCurveTo(x + w * 0.3, y + 16, x + w * 0.8, y + 2, x + w + 8, y + 6); ctx.bezierCurveTo(x + w + 18, y + 8, x + w + 4, y + 16, x + w - 10, y + 13); ctx.lineWidth = 1.2; ctx.stroke(); }
768
+ ctx.restore();
769
+ };
770
+ sign('Elias M. Holloway', 1008, 590, 36, -0.34, true, 4.7);
771
+ sign('Pell & Garrow', 1040, 640, 24, -0.32, false, 5.2);
772
+ sign('J. R. Whitcomb', 70, 662, 24, -0.32, true, 4.9);
773
+ sign('Ada L. Pryor', 64, 692, 22, -0.22, false, 5.3);
774
+ ctx.restore();
775
+ }
776
+ H.grain(ctx, W, H_, 0.05, 13 + Math.floor(t * 12));
777
+ },
778
+ // ===== EXPLAINER =====
779
+ explainerDuration: 14.5,
780
+ explainerNote: 'The camera travels along one long patent sheet as FIG. 4–6 are inked stroke by stroke: ruled arrows and leader lines tie labels to parts, odometer drums count the examples and the accuracy, and an examiner\'s red pencil rings the wrong guess and the 97%.',
781
+ explainer(ctx, W, H_, t) {
782
+ // camera along the sheet
783
+ const K = [[0, 640, 360, 1], [6.0, 640, 360, 1], [6.8, 1280 + 690, 400, 1.3], [10.3, 1280 + 690, 400, 1.3], [11.0, 2560 + 640, 360, 1], [99, 2560 + 640, 360, 1]];
784
+ let ki = 0; while (ki < K.length - 2 && t > K[ki + 1][0]) ki++;
785
+ const ke = H.ease.inOut(H.clamp((t - K[ki][0]) / (K[ki + 1][0] - K[ki][0])));
786
+ const ccx = H.lerp(K[ki][1], K[ki + 1][1], ke), ccy = H.lerp(K[ki][2], K[ki + 1][2], ke), cz = H.lerp(K[ki][3], K[ki + 1][3], ke);
787
+ ctx.drawImage(paperWide(W, H_), ccx - W / 2 / cz, ccy - H_ / 2 / cz, W / cz, H_ / cz, 0, 0, W, H_);
788
+ const Lr = H.layer(W, H_), c = Lr.getContext('2d');
789
+ c.setTransform(cz, 0, 0, cz, W / 2 - ccx * cz, H_ / 2 - ccy * cz); c.strokeStyle = '#000'; c.fillStyle = '#000'; c.lineCap = 'round'; c.lineJoin = 'round';
790
+ const fig = (t0, rate) => { used = 0; budget = t < t0 ? 0 : Math.floor((t - t0) * rate); };
791
+ const after = (t0) => H.clamp(t - t0);
792
+ fig(0, 1e9);
793
+ stroke(c, [[18, 18], [PXW - 18, 18], [PXW - 18, H_ - 18], [18, H_ - 18]], 1.6, true);
794
+ stroke(c, [[23, 23], [PXW - 23, 23], [PXW - 23, H_ - 23], [23, H_ - 23]], 0.7, true);
795
+ c.font = '400 15px "Libre Baskerville"'; c.textBaseline = 'alphabetic';
796
+ c.textAlign = 'left'; c.fillText('Dec. 5, 1933.', 48, 52);
797
+ c.textAlign = 'right'; c.fillText('Patent No. 1,938,413', 1232, 52);
798
+ [['2', 1280], ['3', 2560]].forEach(([n, x]) => { c.textAlign = 'right'; c.font = '400 13px "Libre Baskerville"'; c.fillText(`3 Sheets-Sheet ${n}`, x + 1232, 52); });
799
+ c.textAlign = 'right'; c.font = '400 13px "Libre Baskerville"'; c.fillText('3 Sheets-Sheet 1', 1232, 78);
800
+
801
+ // ===== BEAT 1: the question, lettered large, then set into the title slot =====
802
+ {
803
+ const k = H.ease.inOut(H.clamp((t - 1.35) / 0.85));
804
+ const x = 640, y = H.lerp(350, 78, k), sc = H.lerp(1, 19 / 50, k);
805
+ c.save(); c.translate(x, y); c.scale(sc, sc);
806
+ c.font = '700 50px "Libre Baskerville"'; c.textAlign = 'center'; c.textBaseline = 'alphabetic';
807
+ const txt = 'HOW DOES AI LEARN?', w = c.measureText(txt).width;
808
+ c.save(); c.beginPath(); c.rect(-w / 2 - 10, -60, (w + 20) * H.clamp(t / 1.0), 90); c.clip(); c.fillText(txt, 0, 0); c.restore();
809
+ if (k < 1) { c.lineWidth = 1.2; const pr = H.clamp((t - 0.9) / 0.4); c.beginPath(); c.moveTo(-w / 2, 16); c.lineTo(-w / 2 + w * pr, 16); c.stroke(); c.beginPath(); c.moveTo(-w / 2, 22); c.lineTo(-w / 2 + w * pr, 22); c.lineWidth = 0.6; c.stroke(); }
810
+ c.restore();
811
+ if (k >= 1) { c.font = '400 15px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('E. M. HOLLOWAY', 640, 52); c.font = 'italic 400 13px "Libre Baskerville"'; c.fillText('Filed Aug. 2, 1931', 640, 98); }
812
+ }
813
+
814
+ // ===== BEAT 2: FIG. 4 — the example hopper =====
815
+ fig(2.25, 140);
816
+ {
817
+ // card tray (stack of examples)
818
+ const tray = [[140, 470], [420, 470], [420, 560], [140, 560]];
819
+ stroke(c, tray, 1.3, true);
820
+ if (drew()) free(() => { // hero conventions: shade line on the lower/right edges, a drawer pull
821
+ stroke(c, [[420, 470], [420, 560], [140, 560]], 2.4);
822
+ stroke(c, circ(280, 520, 13, 9, 20, Math.PI, Math.PI * 2), 1.1); stroke(c, [[264, 520], [296, 520]], 1.1);
823
+ });
824
+ hatch(c, [[400, 470], [420, 470], [420, 560], [400, 560]], Math.PI / 2, 3, 0.6);
825
+ if (drew()) free(() => vhatch(c, 366, 420, 470, 560, 2.2, 9));
826
+ for (let k = 0; k < 7; k++) stroke(c, [[150 + k * 3, 466 - k * 9], [410 + k * 3, 466 - k * 9]], 1);
827
+ exCard(c, 220, 404, -0.06, true); exCard(c, 340, 410, 0.05, false);
828
+ // hopper + cabinet (front elevation)
829
+ const hop = [[600, 220], [900, 220], [790, 340], [710, 340]];
830
+ stroke(c, hop, 1.4, true);
831
+ if (drew()) free(() => {
832
+ stroke(c, [[616, 231], [884, 231]], 0.8); // the far rim seen inside the mouth
833
+ stipple(c, [[616, 231], [884, 231], [793, 336], [707, 336]], 2600, (x, y) => H.clamp((y - 231) / 105) * 0.55 + H.clamp((x - 760) / 120) * 0.3, H.rng(43), 0.62);
834
+ stroke(c, [[900, 220], [790, 340]], 2.4); // shade line on the lit hopper's shadow side
835
+ });
836
+ hatch(c, [[860, 220], [900, 220], [790, 340], [770, 340]], Math.PI / 2, 2.6, 0.6);
837
+ const cab = [[620, 340], [880, 340], [880, 590], [620, 590]];
838
+ stroke(c, cab, 1.4, true); stroke(c, [[880, 340], [880, 590], [620, 590]], 2.4);
839
+ hatch(c, [[858, 340], [880, 340], [880, 590], [858, 590]], Math.PI / 2, 3, 0.6);
840
+ if (drew()) free(() => { vhatch(c, 800, 880, 340, 418, 2.2, 11); vhatch(c, 800, 880, 482, 590, 2.2, 11); vhatch(c, 800, 880, 418, 482, 2.2, 11, 0.55, 869); });
841
+ stroke(c, [[600, 590], [900, 590], [900, 612], [600, 612]], 1.2, true); hatch(c, [[600, 590], [900, 590], [900, 612], [600, 612]], Math.PI / 4, 4, 0.6);
842
+ if (drew()) free(() => { hatch(c, [[600, 590], [900, 590], [900, 612], [600, 612]], -Math.PI / 4, 4, 0.5); stroke(c, [[900, 590], [900, 612], [600, 612]], 2.2); });
843
+ // odometer window
844
+ stroke(c, [[632, 418], [868, 418], [868, 482], [632, 482]], 1.2, true);
845
+ if (drew()) free(() => { stroke(c, [[632, 418], [868, 418]], 2.2); stroke(c, [[632, 418], [632, 482]], 2.2); }); // a recessed window: shade on its upper/left reveal
846
+ const e = H.ease.inOut(H.clamp((t - 2.9) / 2.7)), v = t < 2.9 ? 0 : Math.round(Math.pow(10, 6 * e)) * (e > 0.999 ? 1 : 1);
847
+ const val = e >= 1 ? 1000000 : Math.min(999999, v);
848
+ const s7 = String(val).padStart(7, '0'), str = s7[0] + ',' + s7.slice(1, 4) + ',' + s7.slice(4);
849
+ odo(c, 640, 450, str, e > 0 && e < 1 ? ((t * 9) % 1) : 0, 7 - String(val).length + 1); // leading zeros and the leading digit hold still
850
+ if (ok()) { c.font = '400 13px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('EXAMPLES', 750, 506); }
851
+ // gears glimpsed through a port
852
+ stroke(c, circ(750, 548, 22), 1, true); if (drew()) free(() => stroke(c, circ(750, 548, 23.2, 23.2, 20, -0.9, 0.9 + Math.PI * 0.6), 1.8)); stroke(c, circ(750, 548, 5), 1, true);
853
+ for (let k = 0; k < 8; k++) { const a = k / 8 * Math.PI * 2 + t * 2; stroke(c, [[750 + Math.cos(a) * 6, 548 + Math.sin(a) * 6], [750 + Math.cos(a) * 20, 548 + Math.sin(a) * 20]], 0.8); }
854
+ // flow arrow
855
+ arrowP(c, qp([300, 330], [420, 170], [610, 210]), H.clamp((t - 2.9) / 0.5), 1.4);
856
+ if (t > 3.3 && ok()) { c.font = 'italic 400 28px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('training data', 430, 188); }
857
+ // labels landing
858
+ if (t > 2.9 && ok()) { c.font = '700 64px "Libre Baskerville"'; c.textAlign = 'center'; c.textBaseline = 'alphabetic'; c.fillText(val.toLocaleString('en-US'), 1070, 300); c.font = 'italic 400 30px "Libre Baskerville"'; c.fillText('examples', 1070, 344); stroke(c, [[915, 318], [1225, 318]], 0.8); }
859
+ labelL(c, '', 915, 320, 868, 452, 0.15, after(3.0) * 2.5); // ties the big number to the drums
860
+ labelL(c, 'labels', 200, 296, 240, 404, -0.2, after(3.5) * 2.5);
861
+ if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('FIG. 4.', 1060, 640); }
862
+ }
863
+ // cards in flight (drawn without budget, on top)
864
+ if (t > 2.7 && t < 6.2) {
865
+ used = 0; budget = 1e9;
866
+ const path = qp([300, 380], [440, 120], [750, 236], 60);
867
+ for (let k = 0; k < 5; k++) {
868
+ const u = ((t - 2.7) * 1.3 - k * 0.2); if (u < 0 || u > 1) continue;
869
+ const uf = u - Math.floor(u), i = Math.min(59, Math.floor(uf * 60)), p = path[i], q = path[i + 1];
870
+ if (uf > 0.95) continue;
871
+ exCard(c, p[0], p[1], Math.atan2(q[1] - p[1], q[0] - p[0]) * 0.4, (k + Math.floor((t - 2.7) * 1.3)) % 2 === 0);
872
+ }
873
+ }
874
+
875
+ // ===== BEAT 3: FIG. 5 — guess, check, adjust =====
876
+ const O = 1280;
877
+ fig(6.25, 170);
878
+ {
879
+ const cab = [[O + 430, 290], [O + 760, 290], [O + 760, 560], [O + 430, 560]];
880
+ stroke(c, cab, 1.4, true); stroke(c, [[O + 760, 290], [O + 760, 560], [O + 430, 560]], 2.4);
881
+ hatch(c, [[O + 738, 290], [O + 760, 290], [O + 760, 560], [O + 738, 560]], Math.PI / 2, 3, 0.6);
882
+ if (drew()) free(() => vhatch(c, O + 716, O + 760, 290, 560, 2.2, 10));
883
+ stroke(c, [[O + 410, 560], [O + 780, 560], [O + 780, 584], [O + 410, 584]], 1.2, true);
884
+ if (drew()) free(() => { const bs = [[O + 410, 560], [O + 780, 560], [O + 780, 584], [O + 410, 584]]; hatch(c, bs, Math.PI / 4, 4, 0.6); hatch(c, bs, -Math.PI / 4, 4, 0.5); stroke(c, [[O + 780, 560], [O + 780, 584], [O + 410, 584]], 2.2); });
885
+ if (ok()) { c.font = '400 13px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('MODEL', O + 595, 318); }
886
+ // input slot + output window
887
+ stroke(c, [[O + 430, 380], [O + 444, 380], [O + 444, 440], [O + 430, 440]], 1.2);
888
+ if (drew()) free(() => { stroke(c, [[O + 431, 381], [O + 443, 381]], 2.2); hatch(c, [[O + 430, 380], [O + 444, 380], [O + 444, 440], [O + 430, 440]], Math.PI / 2, 2.2, 0.5); });
889
+ const ow = [[O + 760, 330], [O + 960, 330], [O + 960, 400], [O + 760, 400]];
890
+ stroke(c, ow, 1.3, true);
891
+ if (drew()) free(() => stroke(c, [[O + 960, 330], [O + 960, 400], [O + 762, 400]], 2.4)); // shade line on the viewer's lower/right edges
892
+ // knobs (weights)
893
+ const tq = Math.floor(t * 12) / 12;
894
+ [0, 1, 2].forEach((i) => {
895
+ const kx = O + 510 + i * 85, ky = 470;
896
+ stroke(c, circ(kx, ky, 26), 1.3, true); stroke(c, circ(kx, ky, 20), 0.8, true);
897
+ hatch(c, circ(kx, ky, 26, 26, 20, -0.4, 1.6), Math.PI / 4, 2.4, 0.5);
898
+ if (drew()) free(() => {
899
+ stroke(c, circ(kx, ky, 27.2, 27.2, 20, -0.5, 1.9), 2); // shade line round the knob's lower right
900
+ const cast = circ(kx + 4, ky + 4, 27, 27, 24, -0.5, 2.1).concat(circ(kx, ky, 27, 27, 24, 2.1, -0.5));
901
+ stipple(c, cast, 520, (x, y) => H.clamp(1.15 - Math.hypot(x - kx, y - ky) / 31) * 0.95 + 0.25, H.rng(60 + i), 0.5); // fine-dot shadow on the face
902
+ });
903
+ const a0 = [-2.2, -0.6, -1.5][i], a1 = [-0.9, -2.3, -0.4][i], a = a0 + (a1 - a0) * H.ease.inOut(H.clamp((tq - 8.75 - i * 0.12) / 0.5));
904
+ stroke(c, [[kx, ky], [kx + Math.cos(a) * 19, ky + Math.sin(a) * 19]], 2.2);
905
+ for (let k = 0; k < 9; k++) { const aa = -Math.PI + k / 8 * Math.PI; stroke(c, [[kx + Math.cos(aa) * 29, ky + Math.sin(aa) * 29], [kx + Math.cos(aa) * 34, ky + Math.sin(aa) * 34]], 0.7); }
906
+ });
907
+ if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('FIG. 5.', O + 640, 650); }
908
+ }
909
+ // input card slides into the slot
910
+ if (t > 6.5) { used = 0; budget = 1e9; const k = H.ease.out(H.clamp((t - 6.5) / 0.6)); exCard(c, O + H.lerp(170, 360, k), 410, 0, true, '?'); if (ok()) { c.font = 'italic 400 28px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('new picture', O + H.lerp(170, 360, k), 482); } }
911
+ fig(6.25, 1e9);
912
+ {
913
+ // guess in the window
914
+ const right = t > 9.35;
915
+ if (t > 7.15) {
916
+ c.font = '700 30px "Courier Prime"'; c.textAlign = 'center'; c.textBaseline = 'middle';
917
+ c.save(); c.beginPath(); c.rect(O + 762, 332, 196 * H.clamp((t - (right ? 9.35 : 7.15)) / 0.35), 66); c.clip();
918
+ c.fillText(right ? 'cat' : 'dog?', O + 840, 366);
919
+ c.restore();
920
+ if (right && t > 9.65) stroke(c, [[O + 895, 364], [O + 908, 380], [O + 940, 344]], 3.2);
921
+ c.textBaseline = 'alphabetic';
922
+ }
923
+ labelL(c, 'Guess', O + 880, 250, O + 860, 330, 0.2, after(7.25) * 3, 32);
924
+ // check: reference tag vs guess
925
+ const tag = [[O + 985, 440], [O + 1152, 440], [O + 1152, 500], [O + 985, 500], [O + 968, 470]];
926
+ if (t > 7.5) {
927
+ stroke(c, tag, 1.3, true); if (drew()) free(() => stroke(c, [[O + 1153.4, 441], [O + 1153.4, 501.4], [O + 986, 501.4]], 1.4));
928
+ stroke(c, circ(O + 983, 470, 5), 1, true); c.font = '700 22px "Courier Prime"'; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText('label: cat', O + 1071, 471); c.textBaseline = 'alphabetic';
929
+ }
930
+ arrowP(c, qp([O + 930, 401], [O + 932, 452], [O + 962, 466]), H.clamp((t - 7.45) / 0.3), 1.4);
931
+ labelL(c, 'Check', O + 1110, 586, O + 1095, 502, -0.2, after(7.6) * 3, 32);
932
+ if (t > 7.8 && !right) { const p = H.clamp((t - 7.8) / 0.25); stroke(c, partial([[O + 800, 340], [O + 920, 394]], p), 4.5); stroke(c, partial([[O + 920, 340], [O + 800, 394]], H.clamp(p * 2 - 1)), 4.5); }
933
+ // error signal travels back to the knobs
934
+ const fb = qp([O + 1100, 440], [O + 1080, 160], [O + 640, 200], 40).concat(qp([O + 640, 200], [O + 560, 220], [O + 560, 430], 20));
935
+ arrowP(c, fb, H.clamp((t - 8.25) / 0.5), 1.4, [10, 6]);
936
+ if (t > 8.4) { const pk = fb[Math.min(fb.length - 1, Math.floor(H.clamp((t - 8.25) / 0.5) * fb.length))]; c.font = 'italic 400 28px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('error signal', O + 960, 188); if (t < 8.9) { c.beginPath(); c.arc(pk[0], pk[1], 6, 0, 7); c.fill(); } }
937
+ labelL(c, 'Adjust', O + 330, 606, O + 488, 492, 0.2, after(8.6) * 3, 32);
938
+ // loop return arrow (adjust -> guess again)
939
+ arrowP(c, qp([O + 700, 520], [O + 820, 540], [O + 850, 404]), H.clamp((t - 9.15) / 0.3), 1.4);
940
+ }
941
+
942
+ // ===== BEAT 4: FIG. 6 — accuracy over training rounds =====
943
+ const G = 2560;
944
+ fig(10.75, 260);
945
+ {
946
+ const ox = G + 180, oy = 540, gw = 640, gh = 380;
947
+ arrowP(c, [[ox, oy + 10], [ox, oy - gh - 10]], 1); arrowP(c, [[ox - 10, oy], [ox + gw + 20, oy]], 1);
948
+ for (let k = 1; k <= 5; k++) { stroke(c, [[ox, oy - k * gh / 5], [ox + gw, oy - k * gh / 5]], 0.5, false, [2, 5]); stroke(c, [[ox - 8, oy - k * gh / 5], [ox, oy - k * gh / 5]], 1); }
949
+ for (let k = 1; k <= 8; k++) stroke(c, [[ox + k * gw / 8, oy], [ox + k * gw / 8, oy + 8]], 1);
950
+ if (ok()) { c.font = '400 13px "Libre Baskerville"'; c.textAlign = 'right'; c.textBaseline = 'middle'; [50, 60, 70, 80, 90, 100].forEach((v, k) => c.fillText(v + '%', ox - 12, oy - k * gh / 5)); c.textBaseline = 'alphabetic'; c.textAlign = 'center'; c.font = '400 22px "Libre Baskerville"'; c.fillText('TRAINING ROUNDS', ox + gw / 2, oy + 38); c.save(); c.translate(ox - 64, oy - gh / 2); c.rotate(-Math.PI / 2); c.fillText('ACCURACY', 0, 0); c.restore(); }
951
+ if (ok()) { c.font = '700 26px "Libre Baskerville"'; c.textAlign = 'center'; c.fillText('FIG. 6.', G + 1150, 600); }
952
+ used = 0; budget = 1e9;
953
+ const acc = (u) => 61 + 36 * (1 - Math.pow(1 - u, 2.3)), pr = H.clamp((t - 11.0) / 1.5);
954
+ const pts = []; for (let k = 0; k <= 60; k++) { const u = k / 60 * pr; pts.push([ox + u * gw, oy - (acc(u) - 50) / 50 * gh]); }
955
+ if (pr > 0) {
956
+ const area = pts.concat([[pts[pts.length - 1][0], oy], [ox, oy]]);
957
+ hatch(c, area, Math.PI / 2, 4, 0.6); stroke(c, pts, 2.2);
958
+ const tip = pts[pts.length - 1]; stroke(c, circ(tip[0], tip[1], 6), 1.4, true);
959
+ for (let k = 1; k <= 8; k++) { const u = k / 8; if (u <= pr) stroke(c, circ(ox + u * gw, oy - (acc(u) - 50) / 50 * gh, 3.5), 1, true); }
960
+ const v = Math.round(acc(pr));
961
+ c.font = 'italic 400 36px "Libre Baskerville"'; c.textAlign = 'left'; c.fillText('Accuracy', G + 925, 214);
962
+ c.save(); c.translate(G + 925, 300); c.scale(2.8, 2.8); odo(c, 0, 0, String(v) + '%', 0); c.restore();
963
+ labelL(c, '', G + 930, 236, tip[0] + 6, tip[1] - 4, 0.2, 0);
964
+ }
965
+ }
966
+ // ===== BEAT 5: the claim =====
967
+ if (t > 13.0) {
968
+ const txt = 'I claim: More examples → better guesses.', n = Math.floor(H.clamp((t - 13.0) / 0.7) * txt.length);
969
+ c.font = 'italic 400 44px "Libre Baskerville"'; c.textAlign = 'center'; c.textBaseline = 'alphabetic';
970
+ const full = c.measureText(txt).width; c.textAlign = 'left'; c.fillText(txt.slice(0, n), G + 640 - full / 2, 640);
971
+ if (t > 13.75) stroke(c, [[G + 640 - full / 2, 652], [G + 640 - full / 2 + full * H.clamp((t - 13.75) / 0.3), 652]], 1.4);
972
+ }
973
+ // composite ink (warm iron-black) onto the paper
974
+ c.setTransform(1, 0, 0, 1, 0, 0); c.globalCompositeOperation = 'source-in'; c.fillStyle = '#1e1810'; c.fillRect(0, 0, W, H_);
975
+ ctx.save(); ctx.globalCompositeOperation = 'multiply';
976
+ ctx.filter = 'blur(1.1px)'; ctx.globalAlpha = 0.28; ctx.drawImage(Lr, 0, 0); ctx.filter = 'none'; ctx.globalAlpha = 0.95; ctx.drawImage(Lr, 0, 0);
977
+ ctx.restore();
978
+ // examiner's red pencil (emphasis)
979
+ ctx.save(); ctx.translate(W / 2 - ccx * cz, H_ / 2 - ccy * cz); ctx.scale(cz, cz);
980
+ if (t > 7.95 && t < 9.4) redPencil(ctx, circ(O + 860, 366, 120, 52, 40, -0.6, -0.6 + Math.PI * 2.15), H.clamp((t - 7.95) / 0.35), 5);
981
+ if (t > 8.1 && t < 9.4) { ctx.save(); ctx.fillStyle = 'rgba(178,52,40,0.85)'; ctx.font = 'italic 400 32px "Libre Baskerville"'; ctx.fillText('wrong!', O + 990, 296); ctx.restore(); }
982
+ if (t > 12.6) redPencil(ctx, circ(G + 1060, 300, 170, 96, 40, -0.4, -0.4 + Math.PI * 2.15), H.clamp((t - 12.6) / 0.35), 6);
983
+ if (t > 12.8) { ctx.save(); ctx.fillStyle = 'rgba(178,52,40,0.85)'; ctx.font = 'italic 400 30px "Libre Baskerville"'; ctx.fillText('97% — allowed', G + 930, 430); ctx.restore(); }
984
+ ctx.restore();
985
+ H.grain(ctx, W, H_, 0.05, 13 + Math.floor(t * 12));
986
+ },
987
+ // Sound: a quiet drafting room. Everything inked is a steel ruling pen scratching across hard paper (thin, dry,
988
+ // no squeak); the machine's odometer drums clack; the examiner's red pencil is a softer, grainier graphite drag.
989
+ // Camera moves along the sheet are the long sheet sliding under the lamp (a low smooth slide, no rustle).
990
+ // All times copied from explainer() above.
991
+ explainerSound(k) {
992
+ // Chrome glitch: a noise buffer that starts within ~1e-8 s of a sample boundary (float error like 4.15 + 0.1 * 1 + 0.11)
993
+ // spikes far above its gain. Put every start a quarter sample past the 48 kHz grid so none land there.
994
+ { const k0 = k, sn = (t) => (Math.round(t * 48000) + 0.25) / 48000; k = { ...k0, play: (n, t, o) => k0.play(n, sn(t), o), tone: (t, o) => k0.tone(sn(t), o), noise: (t, o) => k0.noise(sn(t), o) }; }
995
+ const D = 14.5;
996
+ // screen pan for a sheet x at the camera's resting framings (FIG.4: x, FIG.5: zoom 1.3 on 1970, FIG.6: x-2560)
997
+ const p4 = (x) => H.clamp(x / 640 - 1, -1, 1) * 0.6, p5 = (x) => H.clamp(((640 + (x - 1970) * 1.3) / 640) - 1, -1, 1) * 0.6, p6 = (x) => p4(x - 2560);
998
+ // ruling pen: nib touches down (tiny dry tap), then a thin steel scratch for the stroke's length
999
+ const pen = (a, d, g = 1, pan = 0, minor = true) => {
1000
+ k.noise(a, { dur: 0.018, filter: 'bandpass', f: 1900, q: 1.3, gain: 0.16 * g, a: 0.001, pan, minor });
1001
+ k.play('stroke', a, { material: 'pen', dur: d, gain: 2.7 * g, pan, minor });
1002
+ k.noise(a, { dur: d, filter: 'bandpass', f: 1200, q: 0.8, gain: 0.05 * g, a: Math.min(0.02, d / 3), hold: d * 0.6, pan, minor }); // a little body: the pen dragging on the rule
1003
+ };
1004
+ // red pencil: grainier, lower, softer than the steel pen
1005
+ const redPencil = (a, d, g = 1, pan = 0, minor = false) => k.play('stroke', a, { material: 'pencil', dur: d, gain: 0.9 * g, pan, minor });
1006
+ // odometer drum: a small, damped brass clack
1007
+ const drum = (t, g = 1, pan = 0, f = 1500) => k.withMinor(() => { k.noise(t, { dur: 0.03, filter: 'bandpass', f, q: 2.2, gain: 0.3 * g, a: 0.001, pan }); k.tone(t, { f: f * 0.2, type: 'triangle', dur: 0.04, gain: 0.07 * g, pan }); });
1008
+ // the long sheet sliding under the lamp: low, smooth, no paper crinkle
1009
+ const slide = (a, d, g = 1) => k.noise(a, { dur: d, color: 'brown', filter: 'bandpass', f: [[0, 220], [d * 0.5, 520], [d, 260]], q: 0.6, gain: 0.34 * g, a: d * 0.45, verb: 0.05 });
1010
+
1011
+ k.bed('room', 0, D, { gain: 0.35 });
1012
+ k.bed('hiss', 0, D, { gain: 0.5 });
1013
+
1014
+ // ---- BEAT 1: "HOW DOES AI LEARN?" letters in left to right over 0..1.0 (21 chars), double rule 0.9..1.3
1015
+ [[0, 3], [5, 9], [11, 13], [15, 21]].forEach(([s, e], i) => pen(0.02 + (s / 21) * 0.98, ((e - s) / 21) * 0.98, 1.1, [-0.35, -0.12, 0.1, 0.3][i], i > 0));
1016
+ pen(0.9, 0.4, 1.3, 0, false); // ruled double underline: one long confident pull along the T-square
1017
+ pen(1.02, 0.28, 0.6, 0.05);
1018
+ slide(1.35, 0.85, 0.5); // title shrinks up into the title slot (1.35 -> 2.2)
1019
+
1020
+ // ---- BEAT 2: FIG. 4 inks at 140 marks/s from 2.25 (tray + cards, then hopper, cabinet, odometer, gears ~3.05)
1021
+ pen(2.25, 0.14, 1.1, p4(280), false); // first mark of the figure
1022
+ { const xs = [280, 230, 340, 750, 750, 700, 750, 800, 750]; for (let i = 1; i < xs.length; i++) pen(2.25 + i * 0.09, 0.07, 0.75, p4(xs[i])); }
1023
+ // cards fly the arc into the hopper (5 cards, period 1/1.3 s, card k offset k*0.2/1.3); a light card flick as each drops in
1024
+ for (let c = 0; c < 5; c++) for (let n = 0; ; n++) {
1025
+ const t0 = 2.7 + (c * 0.2 + n) / 1.3, drop = t0 + 0.95 / 1.3;
1026
+ if (drop > 6.15) break;
1027
+ if ((c + n) % 2 === 0) k.noise(drop, { dur: 0.05, filter: 'bandpass', f: 1500, q: 0.8, gain: 0.11, a: 0.003, pan: p4(720), minor: true });
1028
+ }
1029
+ pen(2.9, 0.5, 0.9, p4(450)); // flow arrow arcs over to the hopper
1030
+ pen(3.0, 0.29, 0.6, p4(930)); // leader to the odometer
1031
+ pen(3.5, 0.24, 0.6, p4(220)); // 'labels' leader
1032
+ // odometer drums roll at 9 turns/s while the count runs (2.9 -> 5.6); clack per turn, softer while it barely moves
1033
+ for (let n = Math.ceil(2.9 * 9); n / 9 < 5.6 - 0.03; n++) {
1034
+ const t = n / 9, e = H.ease.inOut(H.clamp((t - 2.9) / 2.7)), sp = Math.min(1, 6 * e * (1 - e) + 0.25);
1035
+ drum(t, 0.55 + 0.45 * sp, p4(750), 1400 + e * 300);
1036
+ }
1037
+ // 1,000,000 locks in
1038
+ k.play('tock', 5.6, { f: 520, gain: 0.55, pan: p4(900) });
1039
+ k.play('thud', 5.6, { f: 110, gain: 0.22, pan: p4(900) });
1040
+
1041
+ // ---- camera travels the sheet to FIG. 5 (6.0 -> 6.8)
1042
+ slide(6.0, 0.85, 1);
1043
+ // FIG. 5 inks at 170/s from 6.25 (cabinet, windows, three knobs ~6.53)
1044
+ pen(6.25, 0.12, 0.9, p5(1880));
1045
+ [1990, 1790, 1875, 1960].forEach((x, i) => pen(6.33 + i * 0.06, 0.05, 0.6, p5(x)));
1046
+ // the "?" card slides into the input slot (6.5 -> ~7.1, ease out), seats with a little click
1047
+ k.noise(6.5, { dur: 0.42, color: 'pink', filter: 'bandpass', f: [[0, 700], [0.4, 1300]], q: 0.8, gain: 0.12, a: 0.05, pan: p5(1560), minor: true });
1048
+ k.play('tock', 6.95, { f: 900, gain: 0.3, pan: p5(1640), minor: true });
1049
+ // guess window reveals "dog?" (7.15): the readout drum flips over
1050
+ drum(7.15, 1.2, p5(2120), 1200); k.play('tock', 7.17, { f: 640, gain: 0.45, pan: p5(2120) });
1051
+ pen(7.25, 0.24, 0.55, p5(2150)); // Guess leader
1052
+ pen(7.45, 0.3, 0.7, p5(2280)); // arrow to the reference tag
1053
+ pen(7.5, 0.12, 0.6, p5(2380)); // tag outline
1054
+ pen(7.6, 0.24, 0.55, p5(2380)); // Check leader
1055
+ // WRONG: two heavy ruled strokes cross out "dog?" (7.8 -> 7.925 -> 8.05), then the examiner's red ring (7.95 -> 8.3)
1056
+ pen(7.8, 0.12, 2.0, p5(2140), false); pen(7.925, 0.12, 2.0, p5(2140), false);
1057
+ k.play('error', 7.8, { f: 'F3', gain: 0.55, pan: p5(2140) });
1058
+ redPencil(7.95, 0.35, 1.2, p5(2140));
1059
+ // error signal: a dashed line ruled back to the knobs (8.25 -> 8.75), dash by dash
1060
+ for (let i = 0; i < 6; i++) pen(8.25 + i * 0.08, 0.045, 0.6, p5([2380, 2300, 2150, 1960, 1860, 1840][i]));
1061
+ pen(8.6, 0.24, 0.55, p5(1690)); // Adjust leader
1062
+ // knobs turn (quantized to 12 fps, 8.75 + i*0.12 over 0.5 s): one detent click per moving frame
1063
+ for (let f = Math.ceil(8.76 * 12); f / 12 < 9.49; f++) k.play('click', f / 12, { f: 900 + (f % 3) * 90, gain: 0.5, pan: p5(1875 + ((f % 3) - 1) * 85), minor: true });
1064
+ pen(9.15, 0.3, 0.7, p5(2080)); // loop return arrow
1065
+ // CORRECTION: "cat" turns up in the window (9.35), check mark inked (9.65)
1066
+ drum(9.35, 1.2, p5(2120), 1200); k.play('tock', 9.36, { f: 700, gain: 0.5, pan: p5(2120) });
1067
+ pen(9.65, 0.12, 1.4, p5(2200), false);
1068
+ k.play('success', 9.66, { f: 'G5', gain: 0.6, pan: p5(2200) });
1069
+
1070
+ // ---- camera travels to FIG. 6 (10.3 -> 11.0)
1071
+ slide(10.3, 0.75, 1);
1072
+ pen(10.75, 0.1, 0.9, p6(2740), false); pen(10.85, 0.12, 0.6, p6(3060)); // axes and grid (inked at 260/s)
1073
+ pen(11.0, 1.5, 0.45, p6(3060)); // the curve inks along with the hatching under it (11.0 -> 12.5)
1074
+ { // accuracy drums: one clack per new value, at most 12/s, slowing as it settles
1075
+ const acc = (u) => 61 + 36 * (1 - Math.pow(1 - u, 2.3));
1076
+ let last = 61, lastT = -1;
1077
+ for (let t = 11.0; t < 12.5; t += 1 / 120) {
1078
+ const v = Math.round(acc(H.clamp((t - 11.0) / 1.5)));
1079
+ if (v !== last && t - lastT >= 1 / 12) { drum(t, 0.9, p6(3490), 1300 + (v - 61) * 12); last = v; lastT = t; }
1080
+ }
1081
+ }
1082
+ // 97 locks in (12.5) and the examiner rings it in red (12.6 -> 12.95): the big hit
1083
+ k.play('impact', 12.5, { gain: 1.1, pan: p6(3490) });
1084
+ k.play('ding', 12.52, { f: 'E5', gain: 0.5, pan: p6(3490) });
1085
+ redPencil(12.6, 0.35, 2.2, p6(3620));
1086
+
1087
+ // ---- BEAT 5: the claim letters in (13.0 -> 13.7, 43 chars), then a ruled underline (13.75 -> 14.05)
1088
+ { const txt = 'I claim: More examples → better guesses.', n = txt.length, re = /\S+/g; let m;
1089
+ while ((m = re.exec(txt))) pen(13.0 + 0.7 * m.index / n, Math.max(0.04, 0.7 * m[0].length / n), 0.75, (m.index / n - 0.5) * 0.6); }
1090
+ pen(13.75, 0.3, 1.2, 0, false);
1091
+ ['C5', 'G5', 'E6'].forEach((f, i) => k.play('ding', 13.78 + i * 0.08, { f, gain: 0.4 - i * 0.07, pan: (i - 1) * 0.25 })); // resolve
1092
+ },
1093
+
1094
+ // ===== END EXPLAINER =====
1095
+ });
1096
+ }