@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,738 @@
1
+ {
2
+ // ---------- FLIR-style thermal image ----------
3
+ const TMIN = 16, TMAX = 72;
4
+ const IRONBOW = [
5
+ [0.00, [0, 0, 6]], [0.08, [16, 4, 64]], [0.20, [72, 6, 130]], [0.33, [140, 12, 140]],
6
+ [0.46, [196, 34, 96]], [0.58, [232, 70, 40]], [0.70, [248, 120, 12]], [0.82, [252, 184, 24]],
7
+ [0.92, [254, 232, 110]], [1.00, [255, 255, 236]],
8
+ ];
9
+ function buildLUT() {
10
+ const lut = new Uint8ClampedArray(1024 * 3);
11
+ for (let i = 0; i < 1024; i++) {
12
+ const v = i / 1023;
13
+ let k = 0; while (k < IRONBOW.length - 2 && v > IRONBOW[k + 1][0]) k++;
14
+ const [a, ca] = IRONBOW[k], [b, cb] = IRONBOW[k + 1], f = H.clamp((v - a) / (b - a));
15
+ for (let c = 0; c < 3; c++) lut[i * 3 + c] = ca[c] + (cb[c] - ca[c]) * f;
16
+ }
17
+ return lut;
18
+ }
19
+ const g = (T) => { const v = H.clamp((T - TMIN) / (TMAX - TMIN)) * 255; return `rgb(${v},${v},${v})`; };
20
+ const ga = (T, a) => { const v = H.clamp((T - TMIN) / (TMAX - TMIN)) * 255; return `rgba(${v},${v},${v},${a})`; };
21
+
22
+ // ================= EXPLAINER: the IR camera films a model learning; heat = activity, cold = error =================
23
+ let exLUT = null;
24
+ function catSil(f, cx, cy, s, T, Tf) {
25
+ f.fillStyle = g(T);
26
+ f.beginPath(); f.ellipse(cx, cy + s * 0.08, s * 0.95, s * 0.78, 0, 0, 7); f.fill();
27
+ f.beginPath(); f.moveTo(cx - s * 0.9, cy - s * 0.2); f.lineTo(cx - s * 0.78, cy - s * 1.05); f.lineTo(cx - s * 0.3, cy - s * 0.62); f.fill();
28
+ f.beginPath(); f.moveTo(cx + s * 0.9, cy - s * 0.2); f.lineTo(cx + s * 0.78, cy - s * 1.05); f.lineTo(cx + s * 0.3, cy - s * 0.62); f.fill();
29
+ f.fillStyle = g(Tf); f.beginPath(); f.ellipse(cx - s * 0.35, cy - s * 0.05, s * 0.14, s * 0.1, 0, 0, 7); f.ellipse(cx + s * 0.35, cy - s * 0.05, s * 0.14, s * 0.1, 0, 0, 7); f.fill();
30
+ f.beginPath(); f.ellipse(cx, cy + s * 0.25, s * 0.12, s * 0.08, 0, 0, 7); f.fill();
31
+ }
32
+ function dogSil(f, cx, cy, s, T, Tf) {
33
+ f.fillStyle = g(T);
34
+ f.beginPath(); f.ellipse(cx, cy, s * 0.7, s * 0.85, 0, 0, 7); f.fill();
35
+ f.beginPath(); f.ellipse(cx - s * 0.78, cy - s * 0.05, s * 0.26, s * 0.6, 0.25, 0, 7); f.ellipse(cx + s * 0.78, cy - s * 0.05, s * 0.26, s * 0.6, -0.25, 0, 7); f.fill();
36
+ f.beginPath(); f.ellipse(cx, cy + s * 0.42, s * 0.34, s * 0.26, 0, 0, 7); f.fill();
37
+ f.fillStyle = g(Tf); f.beginPath(); f.ellipse(cx - s * 0.28, cy - s * 0.2, s * 0.11, s * 0.09, 0, 0, 7); f.ellipse(cx + s * 0.28, cy - s * 0.2, s * 0.11, s * 0.09, 0, 0, 7); f.fill();
38
+ f.fillStyle = g(T + 6); f.beginPath(); f.ellipse(cx, cy + s * 0.32, s * 0.13, s * 0.09, 0, 0, 7); f.fill();
39
+ }
40
+ // static sensor layers, built once: noise, sensor canvases, slow convective mottling field, column FPN
41
+ const MOT_PAD = 48;
42
+ let exNz = null, exF = null, exB = null, exMot = null, exFPN = null, exEdge = null, exSoft = null, exIX = null, exAX = null;
43
+ function exCache() {
44
+ if (exNz) return;
45
+ exNz = H.makeNoise(17); exLUT = buildLUT();
46
+ exF = H.layer(640, 360); exB = H.layer(640, 360); exB.getContext('2d').filter = 'blur(3px)';
47
+ // same temperature mottling as the still (fbm2(x*.02, y*.02 + t*.05) * 3.2), pre-sampled; t scrolls it by 2.5 px/s
48
+ exMot = new Float32Array(640 * (360 + MOT_PAD));
49
+ for (let y = 0; y < 360 + MOT_PAD; y++) for (let x = 0; x < 640; x++) exMot[y * 640 + x] = exNz.fbm2(x * 0.02, y * 0.02, 3) * 5.4;
50
+ const R = H.rng(4243); exFPN = new Float32Array(4096);
51
+ for (let x = 0; x < 4096; x++) exFPN[x] = (R() - 0.5) * 0.012 + Math.sin(x * 0.9) * 0.002;
52
+ exEdge = new Float32Array(640 * 360); exSoft = new Float32Array(640 * 360);
53
+ }
54
+ function thermalExplainer(ctx, W, Hh, t) {
55
+ exCache();
56
+ const sm = H.smooth, E = H.ease, nz = exNz, Rn = H.rng(5000 + Math.floor(t * 30));
57
+ const FW = 640, FH = 360, F = exF, f = F.getContext('2d');
58
+ f.setTransform(1, 0, 0, 1, 0, 0);
59
+ // scenes separated by the camera's shutter calibration click (NUC)
60
+ const NUC = [[0, 0.3], [6.0, 6.22], [10.45, 10.67]];
61
+ const inNUC = NUC.some(([a, b]) => t >= a && t < b);
62
+ const scene = t < 6.1 ? 1 : t < 10.55 ? 2 : 3;
63
+ const sceneStart = scene === 1 ? 0.3 : scene === 2 ? 6.22 : 10.67;
64
+ const settle = sm(sceneStart, sceneStart + 0.6, t);
65
+ const shakeX = nz.n2(t * 0.8, 50) * 5, shakeY = nz.n2(60, t * 0.7) * 3.5;
66
+ f.scale(FW / W, FH / Hh); f.translate(shakeX, shakeY);
67
+ // room: warm ceiling, cool floor
68
+ const wg = f.createLinearGradient(0, 0, 0, Hh); wg.addColorStop(0, g(24)); wg.addColorStop(0.65, g(21.5)); wg.addColorStop(1, g(20));
69
+ f.fillStyle = wg; f.fillRect(-20, -20, W + 40, Hh + 40);
70
+ // ceiling light strip (warm) and the wall/floor junction with a cooler floor, as in the still's room
71
+ f.fillStyle = g(26.5); f.fillRect(-20, -20, W + 40, 30);
72
+ const fl = f.createLinearGradient(0, 604, 0, Hh); fl.addColorStop(0, g(21.2)); fl.addColorStop(1, g(19.6));
73
+ f.fillStyle = fl; f.fillRect(-20, 604, W + 40, Hh - 584);
74
+ f.fillStyle = g(22.6); f.fillRect(-20, 598, W + 40, 6);
75
+ const hud = []; // deferred HUD drawing (needs screen coords incl. shake)
76
+ // ---- neural net as heat: nodes = hot spots, connections = warm wires whose temperature is the weight ----
77
+ const net = (ox, oy, sc, heat, wts, pulses, outHot) => {
78
+ const L = [[-110, [-80, 0, 80]], [0, [-110, -37, 37, 110]], [110, [-40, 40]]];
79
+ const P = (li, k) => [ox + L[li][0] * sc, oy + L[li][1][k] * sc];
80
+ let ci = 0;
81
+ for (let li = 0; li < 2; li++) for (let a = 0; a < L[li][1].length; a++) for (let b = 0; b < L[li + 1][1].length; b++) {
82
+ const w = wts(ci++), A = P(li, a), B = P(li + 1, b);
83
+ f.strokeStyle = g(22 + w * 20 + heat * 4); f.lineWidth = 3 + w * 6;
84
+ f.beginPath(); f.moveTo(...A); f.lineTo(...B); f.stroke();
85
+ for (const [lay, u0, T] of pulses) { const u = H.clamp(u0 * 1.35 - ((ci * 7) % 5) * 0.08); if (lay === li && u > 0 && u < 1) {
86
+ const x = A[0] + (B[0] - A[0]) * u, y = A[1] + (B[1] - A[1]) * u, gr = f.createRadialGradient(x, y, 0, x, y, 16 * sc);
87
+ gr.addColorStop(0, ga(T, 0.95)); gr.addColorStop(1, ga(T, 0)); f.fillStyle = gr; f.fillRect(x - 20 * sc, y - 20 * sc, 40 * sc, 40 * sc);
88
+ } }
89
+ }
90
+ L.forEach((l, li) => l[1].forEach((_, k) => {
91
+ const [x, y] = P(li, k), hot = li === 2 ? (outHot ? outHot(k) : 0) : 0;
92
+ const gr = f.createRadialGradient(x, y, 0, x, y, 26 * sc);
93
+ gr.addColorStop(0, g(32 + heat * 6 + hot * 14)); gr.addColorStop(0.55, g(29 + heat * 4 + hot * 8)); gr.addColorStop(1, ga(25, 0));
94
+ f.fillStyle = gr; f.beginPath(); f.arc(x, y, 26 * sc, 0, 7); f.fill();
95
+ }));
96
+ return P;
97
+ };
98
+ const card = (x, y, kind, T, sc = 1) => {
99
+ f.fillStyle = g(27); f.beginPath(); f.roundRect(x - 52 * sc, y - 46 * sc, 104 * sc, 92 * sc, 6); f.fill();
100
+ (kind === 'cat' ? catSil : dogSil)(f, x, y + 6 * sc, 30 * sc, T, T - 5);
101
+ };
102
+
103
+ let header = 0;
104
+ if (scene === 1) {
105
+ // conveyor belt feeding labelled examples into the model
106
+ f.fillStyle = g(23); f.fillRect(-20, 486, 860, 22);
107
+ for (let k = 0; k < 18; k++) { f.fillStyle = g(25.5); f.fillRect(((k * 52 + t * 380) % 900) - 40, 488, 12, 18); }
108
+ f.fillStyle = g(38); f.beginPath(); f.arc(830, 497, 20, 0, 7); f.fill(); // warm drive motor
109
+ const fed = H.clamp((t - 2.6) / 0.32, 0, 9);
110
+ const heat = sm(2.6, 6, t);
111
+ // model housing (server chassis) with the net inside
112
+ f.fillStyle = g(25 + heat * 3); f.beginPath(); f.roundRect(830, 140, 330, 330, 18); f.fill();
113
+ f.fillStyle = g(23 + heat * 2); f.beginPath(); f.roundRect(848, 158, 294, 294, 12); f.fill();
114
+ net(995, 305, 1.05, heat, (c) => 0.3 + 0.4 * Math.abs(Math.sin(c * 1.7)), [], null);
115
+ // exhaust above the model grows with work
116
+ const ex = f.createLinearGradient(0, 60, 0, 140); ex.addColorStop(0, ga(26, 0)); ex.addColorStop(1, ga(30 + heat * 6, 0.8));
117
+ f.fillStyle = ex; f.fillRect(860, 60, 270, 80);
118
+ for (let k = 0; k < 9; k++) {
119
+ const sk = 2.6 + k * 0.32; if (t < sk) continue;
120
+ const x = -60 + (t - sk) * 380; if (x > 800) continue;
121
+ const kind = k % 3 === 1 ? 'dog' : 'cat';
122
+ card(x, 420, kind, 41, 1.25);
123
+ hud.push(['tag', x + shakeX, 352 + shakeY, kind]);
124
+ }
125
+ if (t > 2.7) hud.push(['counter', sm(2.7, 5.3, t)]);
126
+ header = sm(1.55, 2.25, t);
127
+ } else if (scene === 2) {
128
+ const cin = E.out(sm(6.25, 6.7, t));
129
+ card(-140 + cin * 360, 330, 'cat', 41, 1.8);
130
+ // weights before / after the adjustment
131
+ const adj = sm(8.85, 9.3, t);
132
+ const wts = (c) => H.lerp(0.15 + 0.75 * Math.abs(Math.sin(c * 2.3)), 0.15 + 0.75 * Math.abs(Math.cos(c * 1.1 + 0.4)), adj);
133
+ const pulses = [];
134
+ const fw1 = sm(6.75, 7.05, t), fw2 = sm(7.05, 7.35, t); // first guess: heat flows forward
135
+ if (fw1 > 0 && fw1 < 1) pulses.push([0, fw1, 44]); if (fw2 > 0 && fw2 < 1) pulses.push([1, fw2, 44]);
136
+ const bk1 = sm(8.2, 8.5, t), bk2 = sm(8.5, 8.8, t); // error: a cold wave runs backwards
137
+ if (bk1 > 0 && bk1 < 1) pulses.push([1, 1 - bk1, 14]); if (bk2 > 0 && bk2 < 1) pulses.push([0, 1 - bk2, 14]);
138
+ const g1 = sm(9.35, 9.6, t), g2 = sm(9.6, 9.85, t); // second guess
139
+ if (g1 > 0 && g1 < 1) pulses.push([0, g1, 44]); if (g2 > 0 && g2 < 1) pulses.push([1, g2, 44]);
140
+ const outHot = (k) => k === 1 ? sm(7.3, 7.45, t) * (1 - sm(8.2, 8.5, t)) : sm(9.8, 9.95, t);
141
+ net(640, 330, 1.25, 0.6, wts, pulses, outHot);
142
+ hud.push(['loop']);
143
+ header = 1;
144
+ } else {
145
+ const lp = sm(10.9, 12.4, t), heat = 0.4 + lp * 0.9;
146
+ f.fillStyle = g(25 + heat * 2); f.beginPath(); f.roundRect(110, 170, 340, 330, 18); f.fill();
147
+ f.fillStyle = g(23 + heat * 2); f.beginPath(); f.roundRect(128, 188, 304, 294, 12); f.fill();
148
+ net(280, 335, 1.1, heat, (c) => H.lerp(0.15 + 0.75 * Math.abs(Math.sin(c * 2.3)), 0.75 + 0.2 * Math.sin(c), lp), [], () => lp * 0.6);
149
+ const ex = f.createLinearGradient(0, 80, 0, 170); ex.addColorStop(0, ga(26, 0)); ex.addColorStop(1, ga(30 + heat * 8, 0.85));
150
+ f.fillStyle = ex; f.fillRect(140, 80, 280, 90);
151
+ hud.push(['chart', lp]);
152
+ header = 1;
153
+ }
154
+ // ---- optics + palette (same pipeline as the still) ----
155
+ f.setTransform(1, 0, 0, 1, 0, 0);
156
+ const sharp = f.getImageData(0, 0, FW, FH).data;
157
+ const B = exB, b = B.getContext('2d'); b.clearRect(0, 0, FW, FH); b.drawImage(F, 0, 0);
158
+ const soft8 = b.getImageData(0, 0, FW, FH).data;
159
+ // convective mottling + emissivity variation (slowly drifting, as in the still), added in float so the
160
+ // equalisation below cannot posterise it into 8-bit contour steps
161
+ const soft = exSoft, mOff = t * 2.5, mRow = Math.floor(mOff) % MOT_PAD, mA = mOff - Math.floor(mOff);
162
+ for (let y = 0; y < FH; y++) {
163
+ const r0 = (y + mRow) * FW, r1 = r0 + FW, o = y * FW;
164
+ for (let x = 0; x < FW; x++) {
165
+ const v = soft8[(o + x) * 4] + exMot[r0 + x] * (1 - mA) + exMot[r1 + x] * mA;
166
+ soft[o + x] = v < 0 ? 0 : v > 254.99 ? 254.99 : v;
167
+ }
168
+ }
169
+ // MSX-style edge map from the sharp field (Sobel)
170
+ const edge = exEdge;
171
+ for (let y = 1; y < FH - 1; y++) {
172
+ const o = y * FW;
173
+ for (let x = 1; x < FW - 1; x++) {
174
+ const a = (o - FW + x) * 4, c = (o + x) * 4, e = (o + FW + x) * 4;
175
+ const gx = -sharp[a - 4] - 2 * sharp[c - 4] - sharp[e - 4] + sharp[a + 4] + 2 * sharp[c + 4] + sharp[e + 4];
176
+ const gy = -sharp[a - 4] - 2 * sharp[a] - sharp[a + 4] + sharp[e - 4] + 2 * sharp[e] + sharp[e + 4];
177
+ const mg = Math.sqrt(gx * gx + gy * gy) / 90; edge[o + x] = mg > 1 ? 1 : mg;
178
+ }
179
+ }
180
+ // plateau histogram equalisation, blended with the fixed linear span the scale bar reports
181
+ const hist = new Float32Array(256);
182
+ for (let i = 0; i < soft.length; i++) hist[soft[i] | 0]++;
183
+ const plateau = (FW * FH) / 70;
184
+ let tot = 0; for (let i = 0; i < 256; i++) { hist[i] = Math.min(hist[i], plateau); tot += hist[i]; }
185
+ const cdf = new Float32Array(257); let acc = 0;
186
+ for (let i = 0; i < 256; i++) { acc += hist[i]; cdf[i] = acc / tot; } cdf[256] = 1;
187
+ const img = ctx.createImageData(W, Hh), d = img.data, sx = FW / W, sy = FH / Hh;
188
+ const lo = 19.5, hi = 47, kT = (TMAX - TMIN) / (hi - lo), kO = (TMIN - lo) / (hi - lo);
189
+ const cont = 0.3 + 0.7 * settle;
190
+ if (!exIX || exIX.length !== W) { exIX = new Int32Array(W); exAX = new Float32Array(W);
191
+ for (let x = 0; x < W; x++) { const fx = H.clamp(x * sx - 0.5, 0, FW - 1.001); exIX[x] = fx | 0; exAX[x] = fx - (fx | 0); } }
192
+ for (let y = 0; y < Hh; y++) {
193
+ const fy = H.clamp(y * sy - 0.5, 0, FH - 1.001), iy = fy | 0, ay = fy - iy, rowN = (Rn() - 0.5) * 0.006;
194
+ for (let x = 0; x < W; x++) {
195
+ const ix = exIX[x], ax = exAX[x], j = iy * FW + ix;
196
+ const v = ((soft[j] * (1 - ax) + soft[j + 1] * ax) * (1 - ay) + (soft[j + FW] * (1 - ax) + soft[j + FW + 1] * ax) * ay) / 255;
197
+ const e = (edge[j] * (1 - ax) + edge[j + 1] * ax) * (1 - ay) + (edge[j + FW] * (1 - ax) + edge[j + FW + 1] * ax) * ay;
198
+ const v255 = v * 255, ci = v255 | 0, cf = v255 - ci, q = cdf[ci] * (1 - cf) + cdf[ci + 1] * cf;
199
+ let n = 0.4 * q + 0.6 * (v * kT + kO);
200
+ n = 0.4 + (n - 0.4) * cont;
201
+ if (inNUC) n = 0.36 + (x / W - 0.5) * 0.02;
202
+ else n += e * 0.05 + exFPN[x];
203
+ n += (Rn() + Rn() - 1) * 0.02 + rowN;
204
+ const li = (n < 0 ? 0 : n > 1 ? 1 : n) * 1023 | 0, i = (y * W + x) * 4;
205
+ d[i] = exLUT[li * 3]; d[i + 1] = exLUT[li * 3 + 1]; d[i + 2] = exLUT[li * 3 + 2]; d[i + 3] = 255;
206
+ }
207
+ }
208
+ ctx.putImageData(img, 0, 0);
209
+ H.vignette(ctx, W, Hh, 0.35, '0,0,10');
210
+
211
+ // ---- HUD (the camera's own annotation layer does the explaining) ----
212
+ ctx.save(); ctx.textBaseline = 'middle';
213
+ const T_ = (s, x, y, font, align = 'left', col = '#fff') => { ctx.font = font; ctx.textAlign = align; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillText(s, x + 1.5, y + 1.5); ctx.fillStyle = col; ctx.fillText(s, x, y); };
214
+ const Ln = (pts, w = 2, col = '#fff', prog = 1) => {
215
+ let total = 0; for (let i = 1; i < pts.length; i++) total += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
216
+ const draw = (c, lw) => { ctx.strokeStyle = c; ctx.lineWidth = lw; ctx.beginPath(); ctx.moveTo(...pts[0]); let acc = 0;
217
+ for (let i = 1; i < pts.length; i++) { const L = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); if (acc + L <= total * prog) { ctx.lineTo(...pts[i]); acc += L; } else { const u = (total * prog - acc) / L; ctx.lineTo(pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * u, pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * u); break; } } ctx.stroke(); };
218
+ draw('rgba(0,0,0,0.55)', w + 2); draw(col, w);
219
+ };
220
+ const Arw = (pts, prog, col = '#fff') => {
221
+ if (prog <= 0) return; Ln(pts, 2.5, col, prog);
222
+ if (prog >= 1) { const e = pts[pts.length - 1], p = pts[pts.length - 2], a = Math.atan2(e[1] - p[1], e[0] - p[0]);
223
+ Ln([[e[0] + Math.cos(a + 2.6) * 14, e[1] + Math.sin(a + 2.6) * 14], e, [e[0] + Math.cos(a - 2.6) * 14, e[1] + Math.sin(a - 2.6) * 14]], 2.5, col); }
224
+ };
225
+ const curve = (x0, y0, qx, qy, x1, y1, n = 24) => Array.from({ length: n + 1 }, (_, i) => { const u = i / n; return [(1 - u) * (1 - u) * x0 + 2 * (1 - u) * u * qx + u * u * x1, (1 - u) * (1 - u) * y0 + 2 * (1 - u) * u * qy + u * u * y1]; });
226
+ const box = (x, y, w, h, prog, col = '#fff') => Ln([[x + w / 2, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y], [x + w / 2, y]], 2, col, prog);
227
+ const tri = (x, y, col) => { ctx.fillStyle = col; ctx.beginPath(); ctx.moveTo(x - 9, y + 8); ctx.lineTo(x + 9, y + 8); ctx.lineTo(x, y - 8); ctx.fill(); };
228
+ if (!inNUC) {
229
+ // status bar
230
+ T_('ε 0.95 IRONBOW', 1090, 34, '400 16px "Inter"', 'right');
231
+ ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.strokeRect(1112, 25, 34, 17); ctx.fillStyle = '#fff'; ctx.fillRect(1146, 30, 3, 7); ctx.fillRect(1115, 28, 21, 11);
232
+ T_('REC', 1176, 34, '600 16px "Inter"'); if (Math.floor(t * 2) % 2 === 0) { ctx.fillStyle = '#ff3b2a'; ctx.beginPath(); ctx.arc(1166, 34, 5, 0, 7); ctx.fill(); }
233
+ // colour scale bar
234
+ const sg = ctx.createLinearGradient(0, 600, 0, 112); IRONBOW.forEach(([p, c]) => sg.addColorStop(p, H.rgba(c, 1)));
235
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(1216, 110, 26, 494); ctx.fillStyle = sg; ctx.fillRect(1218, 112, 22, 490);
236
+ T_('47.0', 1240, 92, '600 18px "Inter"', 'right'); T_('19.5', 1240, 622, '600 18px "Inter"', 'right'); T_('°C', 1229, 646, '400 15px "Inter"', 'center');
237
+ // the question: big annotation, then docks into the measurement table as the header
238
+ if (t > 0.35) {
239
+ const h = E.inOut(header), typed = 'How does AI learn?'.slice(0, Math.floor((t - 0.35) * 22));
240
+ const fs = H.lerp(56, 28, h), x = H.lerp(640, 30, h), y = H.lerp(300, 42, h);
241
+ if (h > 0.02) { ctx.fillStyle = `rgba(0,0,0,${0.38 * h})`; ctx.fillRect(18, 18, 340 * h, 50); }
242
+ T_(typed, x, y, `600 ${fs}px "Inter"`, h < 0.5 ? 'center' : 'left');
243
+ if (h < 0.05 && t > 0.6) { ctx.strokeStyle = '#fff'; Ln([[640 - 30, 360], [640 - 9, 360]]); Ln([[649, 360], [670, 360]]); Ln([[640, 330 + 10], [640, 351]]); Ln([[640, 369], [640, 390]]); }
244
+ }
245
+ }
246
+ for (const it of hud) {
247
+ if (inNUC) break;
248
+ if (it[0] === 'tag') {
249
+ const [, x, y, kind] = it;
250
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(x - 42, y - 40, 84, 36);
251
+ T_(kind, x, y - 22, '800 26px "Inter"', 'center'); Ln([[x, y - 4], [x, y + 12]], 1.5);
252
+ } else if (it[0] === 'counter') {
253
+ const p = it[1], v = p >= 1 ? 1000000 : Math.round(Math.pow(10, 6 * p));
254
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(18, 548, 560, 156);
255
+ T_('TRAINING DATA', 34, 576, '600 24px "Inter"', 'left', 'rgba(255,255,255,0.85)');
256
+ T_(v.toLocaleString('en-US'), 34, 642, '800 68px "Inter"'); T_('examples', 560, 660, '700 30px "Inter"', 'right');
257
+ Arw([[560, 330], [700, 330], [820, 300]], sm(2.7, 3.1, t));
258
+ T_('model', 995, 110, '800 30px "Inter"', 'center'); box(838, 132, 314, 346, sm(2.9, 3.4, t));
259
+ } else if (it[0] === 'loop') {
260
+ T_('new picture', 220, 214, '800 28px "Inter"', 'center', 'rgba(255,255,255,0.9)');
261
+ // GUESS arrow -> answer
262
+ Arw([[790, 330], [900, 330]], sm(7.3, 7.55, t));
263
+ T_('Guess', 845, 296, '800 32px "Inter"', 'center');
264
+ const ans = t < 9.85 ? 'dog?' : 'cat';
265
+ if (t > 7.5) T_(ans, 1010, 330, '800 54px "Inter"', 'center', t < 9.85 ? '#fff' : '#7dff8a');
266
+ // CHECK: red box flashes round the wrong answer
267
+ if (t > 7.75 && t < 9.85) {
268
+ const fl = t < 8.4 ? (Math.floor(t * 8) % 2 ? 1 : 0.25) : 1;
269
+ ctx.globalAlpha = fl; box(930, 286, 160, 88, sm(7.75, 8.0, t), '#ff3b2a');
270
+ T_('✗', 1120, 330, '800 46px "Inter"', 'center', '#ff3b2a'); ctx.globalAlpha = 1;
271
+ T_('Check', 1010, 256, '800 32px "Inter"', 'center', '#ff8a7a');
272
+ }
273
+ if (t >= 9.85) { box(930, 286, 160, 88, 1, '#7dff8a'); T_('✓', 1120, 330, '800 46px "Inter"', 'center', '#7dff8a'); T_('Check', 1010, 256, '800 32px "Inter"', 'center', '#7dff8a'); }
274
+ // ADJUST: error flows back (the cold wave), weights retune
275
+ Arw(curve(1010, 390, 860, 560, 660, 480), sm(8.2, 8.8, t), '#8fd3ff');
276
+ if (t > 8.5) T_('Adjust', 830, 556, '800 32px "Inter"', 'center', '#8fd3ff');
277
+ if (t > 8.85 && t < 9.4) { tri(640, 470, '#28a8ff'); T_('weights retuned', 640, 506, '800 26px "Inter"', 'center'); }
278
+ tri(780, 380 - 0, t > 7.4 && t < 8.6 ? '#ff2a1a' : 'rgba(0,0,0,0)');
279
+ } else if (it[0] === 'chart') {
280
+ const lp = it[1];
281
+ ctx.fillStyle = 'rgba(0,0,0,0.42)'; ctx.fillRect(560, 130, 620, 380);
282
+ T_('LINE PROFILE · accuracy per training round', 578, 154, '600 20px "Inter"', 'left', 'rgba(255,255,255,0.8)');
283
+ Ln([[600, 180], [600, 470], [1150, 470]], 2, '#fff', sm(10.7, 10.95, t));
284
+ T_('rounds →', 1150, 492, '700 24px "Inter"', 'right', 'rgba(255,255,255,0.85)');
285
+ const pts = []; for (let r = 0; r <= 24; r++) { const acc = 0.97 - 0.36 * Math.exp(-r / 6) + (r > 0 && r < 24 ? Math.sin(r * 3.1) * 0.012 : 0); pts.push([610 + r * 22, 470 - (acc - 0.5) / 0.5 * 280]); }
286
+ if (lp > 0) Ln(pts, 3, '#ffd23a', lp);
287
+ const pct = Math.round(61 + 36 * E.out(lp));
288
+ ctx.fillStyle = 'rgba(0,0,0,0.45)'; ctx.fillRect(18, 80, 300, 46);
289
+ T_('Accuracy', 30, 103, '600 28px "Inter"'); T_(pct + ' %', 306, 102, '800 30px "Inter"', 'right', pct >= 97 ? '#7dff8a' : '#fff');
290
+ ctx.fillStyle = 'rgba(8,0,18,0.7)'; ctx.fillRect(915, 210, 170, 80); // readout plate: the profile runs behind it
291
+ T_(pct + '%', 1000, 250, '800 64px "Inter"', 'center', pct >= 97 ? '#7dff8a' : '#fff');
292
+ if (lp >= 1) { const fl = t < 12.9 ? (Math.floor(t * 8) % 2 ? 1 : 0.3) : 1; ctx.globalAlpha = fl; box(915, 210, 170, 80, sm(12.4, 12.65, t), '#7dff8a'); ctx.globalAlpha = 1; tri(pts[24][0], pts[24][1] - 16, '#ff2a1a'); }
293
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(222, 129, 116, 36);
294
+ T_('model', 280, 146, '800 30px "Inter"', 'center');
295
+ if (t > 13.0) {
296
+ const tp = sm(13.0, 13.3, t);
297
+ ctx.fillStyle = `rgba(0,0,0,${0.6 * tp})`; ctx.fillRect(110, 548, 1060, 100);
298
+ const s = 'More examples → better guesses'; T_(s.slice(0, Math.floor((t - 13.0) * 40)), 640, 598, '800 52px "Inter"', 'center');
299
+ box(110, 548, 1060, 100, tp);
300
+ }
301
+ }
302
+ }
303
+ if (inNUC && t > 0.05) T_('NUC', W / 2, Hh / 2, '600 22px "Inter"', 'center', 'rgba(255,255,255,0.85)');
304
+ T_(`2026-10-09 23:41:${String(7 + Math.floor(t)).padStart(2, '0')}`, 1180, 700, '600 15px "IBM Plex Mono"', 'right', 'rgba(255,255,255,0.8)');
305
+ ctx.restore();
306
+ }
307
+
308
+ // Sound: we are holding the IR camera. Its electronics whisper under everything; scene changes are the shutter-flag
309
+ // NUC calibration (solenoid clack in, clack out); the HUD speaks in soft low beeps and data ticks; heat moving through
310
+ // the net is a warm low swell, the error is a cold falling hiss; the measurement-box alarm beeps in step with its
311
+ // flashing; the model's fan spins up as it works. The takeaway lands as a snapshot capture + save tone.
312
+ // All times are copied from thermalExplainer() above (same NUC table, smooth/ease windows and flash formulas).
313
+ // Voiceover mode (k.vo): beds drop out; typing ticks, counter ticks, card thumps, HUD line draws and fan are minor.
314
+ function thermalSound(k) {
315
+ const sm = H.smooth, E = H.ease, D = 15;
316
+ const when = (f, v, a, b) => { for (let t = a; t <= b; t += 0.001) if (f(t) >= v) return t; return b; };
317
+ // shutter flag solenoid: a dry plastic clack with a tiny rattle, no ring
318
+ const clack = (t, g = 1) => {
319
+ k.noise(t, { dur: 0.018, filter: 'bandpass', f: 2100, q: 1.1, gain: 0.4 * g, a: 0.001 });
320
+ k.tone(t, { f: [[0, 260], [0.035, 110]], dur: 0.05, gain: 0.26 * g });
321
+ k.noise(t + 0.022, { dur: 0.012, filter: 'bandpass', f: 1600, q: 2, gain: 0.14 * g, a: 0.001 });
322
+ };
323
+ const nuc = (a, b) => { clack(a, 1); k.noise(a + 0.02, { dur: b - a - 0.02, filter: 'bandpass', f: 900, q: 3, gain: 0.03 }); clack(b, 0.8); };
324
+ // HUD beep: soft, low, slightly square (camera piezo through a lowpass)
325
+ const beep = (t, f = 1050, dur = 0.06, g = 1, pan = 0, minor = false) => k.tone(t, { f, type: 'square', dur, gain: 0.05 * g, a: 0.003, hold: dur * 0.7, filter: 'lowpass', f2: 1800, q: 0.7, pan, minor });
326
+ // data tick: what a HUD annotation "sounds" like as it writes; throttled so it stays under ~12/s
327
+ const ticks = (t0, n, rate, every, pan = 0, g = 1) => { for (let i = 0; i < n; i += every)
328
+ k.noise(t0 + i / rate, { dur: 0.012, filter: 'bandpass', f: 1700 + (i % 3) * 120, q: 3, gain: 0.2 * g, a: 0.001, pan, minor: true }); };
329
+ // HUD line drawing on: a faint electronic trace that rises slightly, then a beep when it completes
330
+ const trace = (a, b, pan = 0, f0 = 420) => k.tone(a, { f: [[0, f0], [b - a, f0 * 1.35]], type: 'triangle', dur: b - a, gain: 0.03, a: 0.03, hold: (b - a) * 0.7, pan, minor: true });
331
+ // heat flowing forward through a layer: warm, low, breathy swell
332
+ const heat = (a, b, pan = 0) => { const d = b - a;
333
+ k.noise(a, { dur: d + 0.1, color: 'brown', filter: 'lowpass', f: [[0, 220], [d, 700]], gain: 0.35, a: d * 0.6, pan });
334
+ k.tone(a, { f: [[0, 90], [d, 140]], dur: d + 0.1, gain: 0.07, a: d * 0.6, pan }); };
335
+ // cold wave running back: an airy hiss that falls
336
+ const cold = (a, b, pan = 0) => { const d = b - a;
337
+ k.noise(a, { dur: d + 0.08, color: 'pink', filter: 'bandpass', f: [[0, 2200], [d, 650]], q: 1.4, gain: 0.22, a: d * 0.3, pan }); };
338
+ // a dry counter tick per change, throttled
339
+ const counter = (val, a, b, gap, pan, avoid = []) => { let last = -1, prev = val(a);
340
+ for (let t = a; t <= b; t += 0.002) { const v = val(t);
341
+ if (v !== prev && t - last >= gap && !avoid.some((x) => t > x - 0.03 && t < x + 0.1)) { k.play('tick', t, { f: 2300, gain: 1.15, pan, minor: true }); last = t; }
342
+ prev = v; } };
343
+ // the model's cooling fan: noise band that rises with the heat (amb bus, minor, kept low)
344
+ const fan = (a, b, f0, f1, g) => k.noise(a, { dur: b - a, color: 'pink', filter: 'bandpass', f: [[0, f0], [b - a - 0.3, f1]], q: 2.5, gain: g, a: 0.6, hold: b - a - 1.0, bus: 'amb', pan: 0.3, minor: true });
345
+
346
+ // ---- beds: the camera's electronics, the room
347
+ k.bed('room', 0, D, { gain: 0.45 });
348
+ k.bed('hiss', 0, D, { gain: 0.9 });
349
+ k.tone(0.3, { f: 165, type: 'triangle', dur: D - 0.3, gain: 0.006, a: 0.5, hold: D - 1.3, bus: 'amb', minor: true }); // faint core whine, low
350
+
351
+ // ---- 0–0.3 opens on a NUC; 0.3–0.9 auto-gain settles (a short soft rise)
352
+ nuc(0.0, 0.3);
353
+ k.tone(0.32, { f: [[0, 300], [0.5, 460]], type: 'sine', dur: 0.55, gain: 0.03, a: 0.3 });
354
+ // title types at 22 chars/s from 0.35 (18 chars); spot-meter crosshair appears at 0.6
355
+ ticks(0.35, 18, 22, 2, 0, 0.9);
356
+ beep(0.6, 880, 0.05, 0.8, 0, true);
357
+ // header docks 1.55–2.25 (eased inOut): a soft smooth low slide, fastest in the middle
358
+ k.noise(1.55, { dur: 0.7, color: 'pink', filter: 'lowpass', f: [[0, 300], [0.35, 900], [0.7, 300]], gain: 0.06, a: 0.35, pan: -0.4 });
359
+
360
+ // ---- scene 1: conveyor runs from 0.3 (belt rumble + drive motor, right side)
361
+ k.noise(0.3, { dur: 5.75, color: 'brown', filter: 'bandpass', f: 180, q: 1.2, gain: 0.16, a: 0.4, hold: 5.0, bus: 'amb', pan: -0.2, minor: true });
362
+ k.tone(0.3, { f: 98, type: 'sawtooth', dur: 5.75, gain: 0.012, a: 0.4, hold: 5.0, filter: 'lowpass', f2: 400, pan: 0.5, minor: true });
363
+ // cards drop onto the belt at 2.6 + k*0.32 (short soft tap each)
364
+ k.withMinor(() => { for (let c = 0; c < 9; c++) { const tc = 2.6 + c * 0.32;
365
+ k.tone(tc, { f: [[0, 150], [0.06, 70]], dur: 0.08, gain: 0.15, pan: -0.8 }); k.noise(tc, { dur: 0.03, filter: 'bandpass', f: 700, q: 1, gain: 0.14, pan: -0.8 }); } });
366
+ // arrow draws 2.7–3.1, model box 2.9–3.4
367
+ trace(2.7, 3.1, 0.2); beep(3.1, 940, 0.05, 0.7, 0.2, true);
368
+ trace(2.9, 3.4, 0.5, 520); beep(3.4, 1180, 0.08, 1.0, 0.5);
369
+ // TRAINING DATA counter 10^(6p), p = sm(2.7, 5.3); locks on 1,000,000
370
+ counter((t) => { const p = sm(2.7, 5.3, t); return p >= 1 ? 1e6 : Math.round(Math.pow(10, 6 * p)); }, 2.72, 5.25, 0.09, -0.5, [2.92, 3.24, 3.4, 3.56, 3.88, 4.2, 4.52, 4.84, 5.16]);
371
+ beep(5.3, 1180, 0.05, 1, -0.5); beep(5.37, 1580, 0.09, 1, -0.5);
372
+ // the chassis heats sm(2.6, 6): its fan spins up
373
+ fan(2.6, 6.15, 500, 1100, 0.05);
374
+
375
+ // ---- NUC 6.0–6.22 (scene change)
376
+ nuc(6.0, 6.22);
377
+
378
+ // ---- scene 2: cat card slides in E.out(sm(6.25, 6.7)) — a soft smooth slide
379
+ k.noise(6.25, { dur: 0.45, color: 'pink', filter: 'lowpass', f: [[0, 800], [0.45, 250]], gain: 0.14, a: 0.02, pan: -0.5 });
380
+ beep(6.4, 880, 0.04, 0.6, -0.6, true); // "new picture" tag
381
+ heat(6.75, 7.05, -0.1); heat(7.05, 7.35, 0.15); // first guess: heat flows forward
382
+ k.tone(7.3, { f: 180, dur: 0.25, gain: 0.08, a: 0.1, pan: 0.25 }); // output node glows hot (7.3–7.45)
383
+ trace(7.3, 7.55, 0.3); beep(7.4, 740, 0.04, 0.7, 0.3, true); // Guess arrow + hot-spot marker
384
+ beep(7.5, 1050, 0.09, 1.1, 0.45); // the answer "dog?" posts
385
+ // WRONG: red box + ✗ flash at 8 Hz (on when floor(t*8) is odd): on at 7.875, 8.125, then steady from 8.375
386
+ k.play('thud', 7.875, { f: 110, gain: 0.6, pan: 0.5 });
387
+ [7.875, 8.125, 8.375].forEach((ta, i) => { beep(ta, 620, 0.1, 1.6, 0.5); beep(ta, 930, 0.1, 0.8, 0.5); });
388
+ // error flows back as a cold wave 8.2–8.5, 8.5–8.8 (arrow 8.2–8.8), "Adjust" 8.5
389
+ cold(8.2, 8.5, 0.25); cold(8.5, 8.8, -0.1);
390
+ beep(8.5, 820, 0.04, 0.6, 0.0, true);
391
+ // weights retuned 8.85–9.3: a low servo-like retune sweep under the label
392
+ k.tone(8.85, { f: [[0, 200], [0.45, 320]], type: 'sawtooth', dur: 0.5, gain: 0.045, a: 0.05, hold: 0.35, filter: 'lowpass', f2: 700, q: 2 });
393
+ beep(8.85, 700, 0.05, 0.8, 0, true);
394
+ heat(9.35, 9.6, -0.1); heat(9.6, 9.85, 0.15); // second guess
395
+ // RIGHT: "cat", green box and ✓ at 9.85: the camera's confirm tones
396
+ k.play('success', 9.85, { f: 'C5', gain: 0.7, pan: 0.45 });
397
+ beep(9.85, 1050, 0.05, 0.8, 0.45);
398
+
399
+ // ---- NUC 10.45–10.67
400
+ nuc(10.45, 10.67);
401
+
402
+ // ---- scene 3: axes 10.7–10.95; line profile lp = sm(10.9, 12.4) with % counting 61 -> 97
403
+ trace(10.7, 10.95, 0.3);
404
+ const pct = (t) => Math.round(61 + 36 * E.out(sm(10.9, 12.4, t)));
405
+ const t97 = when(pct, 97, 10.9, 12.4);
406
+ k.tone(10.9, { f: [[0, 260], [t97 - 10.9, 520]], type: 'triangle', dur: t97 - 10.9, gain: 0.022, a: 0.2, hold: t97 - 11.2, pan: 0.3, minor: true }); // trace rising with the line
407
+ counter(pct, 10.92, t97 - 0.04, 0.085, 0.3);
408
+ beep(t97, 1180, 0.06, 1, 0.3); beep(t97 + 0.07, 1580, 0.08, 0.9, 0.3); // number turns green
409
+ fan(10.67, 14.95, 700, 1500, 0.055); // model heats with lp
410
+ // THE HIT at 12.4: lp reaches 1, measurement box locks on 97% and flashes (on 12.4, 12.625, steady from 12.875)
411
+ k.play('thud', 12.4, { f: 85, gain: 1.2, pan: 0.2 }); clack(12.4, 1.1);
412
+ k.noise(12.4, { dur: 0.5, color: 'pink', filter: 'lowpass', f: [[0, 2000], [0.45, 300]], gain: 0.22, verb: 0.3 });
413
+ [12.4, 12.625, 12.875].forEach((ta, i) => { beep(ta, 784, 0.11, 1.9 - i * 0.3, 0.3); beep(ta, 1175, 0.11, 1.1 - i * 0.2, 0.3); });
414
+
415
+ // ---- 13.0 takeaway panel: box draws sm(13.0, 13.3), text types at 40 chars/s (30 chars, done 13.75)
416
+ trace(13.0, 13.3, 0, 360);
417
+ ticks(13.0, 30, 40, 3, 0, 0.9);
418
+ // resolve: snapshot captured + saved (shutter flag, then a low two-note save tone)
419
+ clack(13.78, 0.9); k.noise(13.84, { dur: 0.04, filter: 'bandpass', f: 1500, q: 1, gain: 0.2 }); clack(13.86, 0.5);
420
+ k.tone(13.95, { f: 'G4', dur: 0.5, gain: 0.12, a: 0.005, verb: 0.3 });
421
+ k.tone(14.1, { f: 'C5', dur: 0.85, gain: 0.13, a: 0.005, verb: 0.35 });
422
+ k.tone(14.1, { f: 'E5', dur: 0.85, gain: 0.06, a: 0.01, verb: 0.35 });
423
+ }
424
+
425
+ registerStyle({
426
+ id: 'thermal-camera',
427
+ name: 'Thermal Camera',
428
+ category: 'Light & Imaging',
429
+ description: 'A handheld IR camera frame in ironbow: soft low-res heat field, cold glasses and warm breath, hot coffee and GPU racks, edge-enhanced outlines, sensor noise and the camera’s own HUD with spot meter, box measurement and scale bar.',
430
+ motion: 'Camera clicks through its shutter calibration, the auto-gain settles, coffee is hot-poured and steam rises, rack bands heat up bottom to top while the spot meter glides onto the mug and the readouts tick live.',
431
+ fonts: ['600 22px "Inter"', '400 18px "Inter"', '600 16px "IBM Plex Mono"'],
432
+ frameT: 4.6,
433
+ duration: 5,
434
+ readsPixels: true,
435
+ explainerDuration: 15,
436
+ explainerNote: 'Everything is filmed through the IR camera: scenes change with the shutter-calibration (NUC) click, heat is activity and a cold wave is the error signal, while the camera\u2019s own HUD does the explaining with spot-meter arrows, measurement-box emphasis, tag labels riding on objects and live readouts.',
437
+ explainer(ctx, W, Hh, t) { thermalExplainer(ctx, W, Hh, t); },
438
+ explainerSound(k) { thermalSound(k); },
439
+ draw(ctx, W, Hh, t) {
440
+ const R = H.rng(4242), nz = H.makeNoise(17);
441
+ const Rn = H.rng(9000 + Math.floor(t * 30)); // per-frame sensor noise
442
+ const coffee = H.ease.out(H.smooth(0.55, 1.7, t)), mugWarm = H.smooth(0.8, 2.6, t), steamOn = H.smooth(1.1, 2.4, t);
443
+ const settle = H.smooth(0.42, 1.3, t);
444
+ const shakeX = nz.n2(t * 0.9, 50) * 4, shakeY = nz.n2(60, t * 0.8) * 3;
445
+ const FW = 640, FH = 360; // sensor resolution
446
+ const F = H.layer(FW, FH), f = F.getContext('2d');
447
+ f.scale(FW / W, FH / Hh); f.translate(shakeX, shakeY);
448
+
449
+ // ---- room: ceiling warmer than floor ----
450
+ const wg = f.createLinearGradient(0, 0, 0, Hh);
451
+ wg.addColorStop(0, g(24.5)); wg.addColorStop(0.6, g(22)); wg.addColorStop(1, g(20.5));
452
+ f.fillStyle = wg; f.fillRect(0, 0, W, Hh);
453
+ // ceiling light fixture warm streak, radiator glow lower-left
454
+ f.fillStyle = g(27); f.fillRect(0, 0, W, 10);
455
+ // window (cold glass, warmer frame)
456
+ f.fillStyle = g(19.5); f.fillRect(40, 40, 260, 300);
457
+ const wn = f.createLinearGradient(0, 52, 0, 330);
458
+ wn.addColorStop(0, g(11)); wn.addColorStop(1, g(13.5));
459
+ f.fillStyle = wn; f.fillRect(54, 54, 108, 272); f.fillRect(176, 54, 110, 272);
460
+ // reflection of the ceiling lamp in the glass (IR mirror)
461
+ f.fillStyle = ga(17, 0.6); f.beginPath(); f.ellipse(120, 120, 24, 70, 0.2, 0, 7); f.fill();
462
+ // radiator under window
463
+ for (let i = 0; i < 9; i++) { f.fillStyle = g(44 - (i % 2) * 6); f.fillRect(60 + i * 26, 382, 16, 70); }
464
+ const rh = f.createLinearGradient(0, 230, 0, 382); rh.addColorStop(0, ga(30, 0)); rh.addColorStop(1, ga(30, 0.5));
465
+ f.fillStyle = rh; f.fillRect(50, 230, 250, 152);
466
+
467
+ // ---- server racks (right) ----
468
+ const racks = [[880, 70, 170], [1062, 50, 190]];
469
+ racks.forEach(([x, y, w], ri) => {
470
+ f.fillStyle = g(27); f.fillRect(x, y, w, 560 - y);
471
+ f.fillStyle = g(25); f.fillRect(x, y, 10, 560 - y); f.fillRect(x + w - 10, y, 10, 560 - y);
472
+ for (let u = y + 20, k = 0; u < 540; u += 24, k++) {
473
+ const load = 0.5 + 0.5 * nz.n2(k * 0.7 + ri * 5, t * 0.6);
474
+ const spin = H.smooth(0.7 + (20 - k) * 0.07 + ri * 0.15, 1.3 + (20 - k) * 0.07 + ri * 0.15, t); // servers spin up bottom->top
475
+ const T = 27 + spin * (3 + load * 18 + (k % 5 === 2 ? 6 : 0));
476
+ f.fillStyle = g(T); f.fillRect(x + 16, u, w - 32, 18);
477
+ // vent perforation: hotter slots
478
+ f.fillStyle = g(T + 5);
479
+ for (let s = 0; s < 8; s++) f.fillRect(x + 26 + s * ((w - 50) / 8), u + 5, (w - 50) / 16, 8);
480
+ f.fillStyle = g(T - 6); f.fillRect(x + 16, u + 18, w - 32, 6);
481
+ }
482
+ // warm exhaust rising above rack
483
+ const ex = f.createLinearGradient(0, y - 60, 0, y);
484
+ ex.addColorStop(0, ga(30, 0)); ex.addColorStop(1, ga(36, 0.7));
485
+ f.fillStyle = ex; f.fillRect(x + 10, y - 60, w - 20, 60);
486
+ });
487
+
488
+ // ---- person (behind laptop) ----
489
+ const hx = 520, hy = 200;
490
+ // body heat halo on wall
491
+ const halo = f.createRadialGradient(hx, hy + 140, 60, hx, hy + 140, 300);
492
+ halo.addColorStop(0, ga(27, 0.6)); halo.addColorStop(1, ga(24, 0));
493
+ f.fillStyle = halo; f.fillRect(hx - 320, hy - 160, 640, 520);
494
+ // shoulders / torso (shirt ~30)
495
+ f.fillStyle = g(28.2);
496
+ f.beginPath(); f.moveTo(hx - 210, 560); f.bezierCurveTo(hx - 215, 380, hx - 150, 320, hx - 60, 300);
497
+ f.lineTo(hx + 60, 300); f.bezierCurveTo(hx + 150, 320, hx + 215, 380, hx + 210, 560); f.closePath(); f.fill();
498
+ // shirt folds cooler/warmer
499
+ f.strokeStyle = ga(27, 0.7); f.lineWidth = 10; f.lineCap = 'round';
500
+ f.beginPath(); f.moveTo(hx - 140, 380); f.quadraticCurveTo(hx - 120, 450, hx - 150, 520); f.stroke();
501
+ f.beginPath(); f.moveTo(hx + 135, 370); f.quadraticCurveTo(hx + 120, 440, hx + 150, 520); f.stroke();
502
+ // arms reaching forward to keyboard
503
+ f.fillStyle = g(29);
504
+ f.beginPath(); f.ellipse(hx - 190, 470, 48, 110, 0.35, 0, 7); f.fill();
505
+ f.beginPath(); f.ellipse(hx + 190, 470, 48, 110, -0.35, 0, 7); f.fill();
506
+ // neck
507
+ const nk = f.createLinearGradient(hx - 40, 0, hx + 40, 0);
508
+ nk.addColorStop(0, g(32)); nk.addColorStop(0.5, g(35)); nk.addColorStop(1, g(32));
509
+ f.fillStyle = nk; f.fillRect(hx - 38, 250, 76, 64);
510
+ // collar V hot skin
511
+ f.fillStyle = g(34.5); f.beginPath(); f.moveTo(hx - 34, 300); f.lineTo(hx, 345); f.lineTo(hx + 34, 300); f.fill();
512
+ // head: hot face, limb-darkened edges
513
+ const hd = f.createRadialGradient(hx, hy + 10, 10, hx, hy, 82);
514
+ hd.addColorStop(0, g(36)); hd.addColorStop(0.65, g(35)); hd.addColorStop(1, g(31.5));
515
+ f.fillStyle = hd; f.beginPath(); f.ellipse(hx, hy, 70, 86, 0, 0, 7); f.fill();
516
+ // ears (cooler)
517
+ f.fillStyle = g(32); f.beginPath(); f.ellipse(hx - 70, hy + 6, 12, 22, 0, 0, 7); f.ellipse(hx + 70, hy + 6, 12, 22, 0, 0, 7); f.fill();
518
+ // hair (insulating -> cool)
519
+ f.fillStyle = g(27.5);
520
+ f.beginPath(); f.moveTo(hx - 74, hy + 10); f.bezierCurveTo(hx - 90, hy - 80, hx - 20, hy - 112, hx + 30, hy - 100);
521
+ f.bezierCurveTo(hx + 90, hy - 90, hx + 86, hy - 20, hx + 74, hy + 4);
522
+ f.bezierCurveTo(hx + 60, hy - 40, hx + 20, hy - 58, hx - 20, hy - 54);
523
+ f.bezierCurveTo(hx - 50, hy - 50, hx - 66, hy - 20, hx - 74, hy + 10); f.fill();
524
+ // glasses: glass blocks LWIR -> look cold
525
+ f.fillStyle = g(25.5);
526
+ f.beginPath(); f.roundRect(hx - 60, hy - 12, 50, 36, 10); f.roundRect(hx + 10, hy - 12, 50, 36, 10); f.fill();
527
+ f.strokeStyle = g(29); f.lineWidth = 4;
528
+ f.beginPath(); f.roundRect(hx - 60, hy - 12, 50, 36, 10); f.roundRect(hx + 10, hy - 12, 50, 36, 10); f.stroke();
529
+ f.beginPath(); f.moveTo(hx - 10, hy - 2); f.lineTo(hx + 10, hy - 2); f.stroke();
530
+ // nose tip slightly cool, inner eye corners hottest (seen as bright dots at bridge)
531
+ f.fillStyle = g(32.6); f.beginPath(); f.ellipse(hx, hy + 40, 11, 9, 0, 0, 7); f.fill();
532
+ f.fillStyle = g(36.4); f.beginPath(); f.arc(hx - 7, hy + 6, 5, 0, 7); f.arc(hx + 7, hy + 6, 5, 0, 7); f.fill();
533
+ // mouth warm line
534
+ f.fillStyle = g(36); f.beginPath(); f.ellipse(hx, hy + 62, 16, 4, 0, 0, 7); f.fill();
535
+ // warm breath plume
536
+ for (let i = 0; i < 6; i++) {
537
+ f.fillStyle = ga(31, 0.18 - i * 0.025);
538
+ f.beginPath(); f.ellipse(hx + 6 + i * 6, hy + 82 + i * 10 + Math.sin(t * 2 + i) * 3, 14 + i * 7, 8 + i * 3, 0.3, 0, 7); f.fill();
539
+ }
540
+
541
+ // ---- desk ----
542
+ const dg = f.createLinearGradient(0, 520, 0, Hh);
543
+ dg.addColorStop(0, g(22.8)); dg.addColorStop(1, g(21.5));
544
+ f.fillStyle = dg; f.fillRect(0, 520, W, Hh - 520);
545
+ f.fillStyle = g(21); f.fillRect(0, 520, W, 5);
546
+ // warm patch under laptop (conducted heat)
547
+ const lp = f.createRadialGradient(540, 590, 20, 540, 590, 300);
548
+ lp.addColorStop(0, ga(31, 0.85)); lp.addColorStop(1, ga(24, 0));
549
+ f.fillStyle = lp; f.beginPath(); f.ellipse(540, 600, 320, 70, 0, 0, 7); f.fill();
550
+ // residual handprint on desk (thermal ghost)
551
+ const hpx = 230, hpy = 640;
552
+ f.fillStyle = ga(26.5, 0.75);
553
+ f.beginPath(); f.ellipse(hpx, hpy, 36, 22, -0.2, 0, 7); f.fill();
554
+ [[-34, -26, 0.5], [-12, -36, 0.25], [10, -36, 0], [30, -28, -0.25]].forEach(([dx, dy, a]) => {
555
+ f.beginPath(); f.ellipse(hpx + dx, hpy + dy, 7, 20, a, 0, 7); f.fill();
556
+ });
557
+ f.beginPath(); f.ellipse(hpx + 46, hpy + 2, 7, 18, 1.2, 0, 7); f.fill();
558
+
559
+ // ---- laptop: lid back toward camera ----
560
+ const lx0 = 340, lx1 = 720, ly0 = 318, ly1 = 560;
561
+ f.fillStyle = g(25.8);
562
+ f.beginPath(); f.moveTo(lx0 + 14, ly0); f.lineTo(lx1 - 14, ly0); f.lineTo(lx1, ly1); f.lineTo(lx0, ly1); f.closePath(); f.fill();
563
+ // warmer toward hinge/bottom (heat from the base)
564
+ const lg = f.createLinearGradient(0, ly0, 0, ly1);
565
+ lg.addColorStop(0, ga(26, 0)); lg.addColorStop(0.6, ga(28, 0.35)); lg.addColorStop(1, ga(36, 0.9));
566
+ f.fillStyle = lg; f.fill();
567
+ // lid logo-less emblem: cooler circle (different emissivity metal)
568
+ f.fillStyle = g(24.5); f.beginPath(); f.arc((lx0 + lx1) / 2, 420, 22, 0, 7); f.fill();
569
+ // base / hinge: hot exhaust slot
570
+ f.fillStyle = g(41); f.fillRect(lx0 - 6, ly1 - 4, lx1 - lx0 + 12, 14);
571
+ f.fillStyle = g(47); f.fillRect(lx0 + 40, ly1 + 2, 120, 6);
572
+ // exhaust plume
573
+ const pl = f.createLinearGradient(0, ly1 + 10, 0, ly1 + 80);
574
+ pl.addColorStop(0, ga(38, 0.7)); pl.addColorStop(1, ga(30, 0));
575
+ f.fillStyle = pl; f.beginPath(); f.ellipse(lx0 + 100, ly1 + 30, 90, 30, 0, 0, 7); f.fill();
576
+ // hands on keyboard visible either side of lid
577
+ f.fillStyle = g(32.5);
578
+ f.beginPath(); f.ellipse(lx0 - 18, 548, 34, 20, -0.3, 0, 7); f.fill();
579
+ f.beginPath(); f.ellipse(lx1 + 18, 548, 34, 20, 0.3, 0, 7); f.fill();
580
+ f.fillStyle = g(29.5); // fingertips cooler
581
+ for (let i = 0; i < 4; i++) { f.beginPath(); f.arc(lx0 + 6 + i * 6, 560 + i, 6, 0, 7); f.arc(lx1 - 6 - i * 6, 560 + i, 6, 0, 7); f.fill(); }
582
+
583
+ // ---- coffee mug ----
584
+ const mx = 820, my = 470, mw = 92, mh = 108;
585
+ const mg = f.createLinearGradient(mx - mw / 2, 0, mx + mw / 2, 0);
586
+ const mT = (T) => g(23 + (T - 23) * mugWarm);
587
+ mg.addColorStop(0, mT(44)); mg.addColorStop(0.35, mT(56)); mg.addColorStop(0.6, mT(57)); mg.addColorStop(1, mT(43));
588
+ f.fillStyle = mg; f.fillRect(mx - mw / 2, my, mw, mh);
589
+ f.beginPath(); f.ellipse(mx, my + mh, mw / 2, 12, 0, 0, Math.PI); f.fill();
590
+ // handle
591
+ f.strokeStyle = mT(46); f.lineWidth = 14;
592
+ f.beginPath(); f.ellipse(mx + mw / 2 + 4, my + 50, 26, 32, 0, -1.3, 1.3); f.stroke();
593
+ // liquid surface: hottest
594
+ f.fillStyle = mT(52); f.beginPath(); f.ellipse(mx, my, mw / 2, 13, 0, 0, 7); f.fill();
595
+ f.fillStyle = g(23 + 44 * coffee); f.beginPath(); f.ellipse(mx, my + 1, mw / 2 - 7, 9, 0, 0, 7); f.fill();
596
+ // mug heat reflected on desk
597
+ f.fillStyle = ga(30, 0.5 * mugWarm); f.beginPath(); f.ellipse(mx, my + mh + 14, 70, 14, 0, 0, 7); f.fill();
598
+ // steam plume: soft rising puffs, cooling and spreading with height
599
+ for (let k = 0; k < 110; k++) {
600
+ const strand = k % 2, u = (k >> 1) / 55, yy = my - 6 - u * 260 + (R() - 0.5) * 14;
601
+ const xx = mx + (strand ? 14 : -14) * (1 - u) + Math.sin(u * 5.2 + t * 1.3 + strand * 2.2) * (6 + u * 34)
602
+ + nz.n2(u * 2.5 + strand * 4, t * 0.4) * 34 * u + (R() - 0.5) * 18 * u;
603
+ const r = 10 + u * 30 + R() * 8;
604
+ const dens = H.clamp(0.6 + nz.n2(u * 6 + strand * 3, t * 0.8) * 1.2, 0.1, 1) * steamOn * (1 - H.smooth((t - 1.1) * 0.9 - 0.15, (t - 1.1) * 0.9 + 0.05, u));
605
+ const gr = f.createRadialGradient(xx, yy, 0, xx, yy, r);
606
+ gr.addColorStop(0, ga(46 - u * 16, 0.2 * dens * (1 - u * 0.75))); gr.addColorStop(1, ga(30, 0));
607
+ f.fillStyle = gr; f.beginPath(); f.arc(xx, yy, r, 0, 7); f.fill();
608
+ }
609
+
610
+ // ---- optics: blur the field (lens + low-res microbolometer) ----
611
+ const sharpImg = f.getImageData(0, 0, FW, FH), sharp = sharpImg.data;
612
+ // organic temperature mottling (convection, emissivity variation)
613
+ for (let y = 0; y < FH; y++) for (let x = 0; x < FW; x++) {
614
+ const i = (y * FW + x) * 4, m = nz.fbm2(x * 0.02, y * 0.02 + t * 0.05, 3) * 3.2;
615
+ sharp[i] = sharp[i + 1] = sharp[i + 2] = H.clamp(sharp[i] + m, 0, 255);
616
+ }
617
+ f.setTransform(1, 0, 0, 1, 0, 0); f.putImageData(sharpImg, 0, 0);
618
+ const B = H.layer(FW, FH), b = B.getContext('2d');
619
+ b.filter = 'blur(3.2px)'; b.drawImage(F, 0, 0);
620
+ const soft = b.getImageData(0, 0, FW, FH).data;
621
+ // MSX-style edge map from sharp field (Sobel)
622
+ const edge = new Float32Array(FW * FH);
623
+ for (let y = 1; y < FH - 1; y++) for (let x = 1; x < FW - 1; x++) {
624
+ const p = (yy, xx) => sharp[((y + yy) * FW + x + xx) * 4];
625
+ const gx = -p(-1, -1) - 2 * p(0, -1) - p(1, -1) + p(-1, 1) + 2 * p(0, 1) + p(1, 1);
626
+ const gy = -p(-1, -1) - 2 * p(-1, 0) - p(-1, 1) + p(1, -1) + 2 * p(1, 0) + p(1, 1);
627
+ edge[y * FW + x] = Math.min(1, Math.hypot(gx, gy) / 90);
628
+ }
629
+ // auto-range from scene (with slight pumping over time)
630
+ let lo = 255, hi = 0;
631
+ for (let i = 0; i < soft.length; i += 4) { const v = soft[i]; if (v < lo) lo = v; if (v > hi) hi = v; }
632
+ const tLo = TMIN + lo / 255 * (TMAX - TMIN), tHi = TMIN + hi / 255 * (TMAX - TMIN) + Math.sin(t * 0.7) * 0.3;
633
+
634
+ // plateau histogram equalisation (what real IR cameras do so faces don't go dull next to a hot mug)
635
+ const hist = new Float32Array(256);
636
+ for (let i = 0; i < soft.length; i += 4) hist[soft[i]]++;
637
+ const plateau = (FW * FH) / 70;
638
+ let tot = 0; for (let i = 0; i < 256; i++) { hist[i] = Math.min(hist[i], plateau); tot += hist[i]; }
639
+ const cdf = new Float32Array(256); let acc = 0;
640
+ for (let i = 0; i < 256; i++) { acc += hist[i]; cdf[i] = acc / tot; }
641
+ const eq = (v255) => { const i = Math.min(254, v255 | 0), f2 = v255 - i; return cdf[i] * (1 - f2) + cdf[i + 1] * f2; };
642
+ // ---- palette mapping at output resolution ----
643
+ const lut = buildLUT();
644
+ const img = ctx.createImageData(W, Hh), d = img.data;
645
+ const colFPN = new Float32Array(W); for (let x = 0; x < W; x++) colFPN[x] = (R() - 0.5) * 0.012 + Math.sin(x * 0.9) * 0.002;
646
+ const sx = FW / W, sy = FH / Hh;
647
+ for (let y = 0; y < Hh; y++) {
648
+ const fy = H.clamp(y * sy - 0.5, 0, FH - 1.001), iy = fy | 0, ay = fy - iy;
649
+ const rowN = (Rn() - 0.5) * 0.006;
650
+ for (let x = 0; x < W; x++) {
651
+ const fx = H.clamp(x * sx - 0.5, 0, FW - 1.001), ix = fx | 0, ax = fx - ix, j = iy * FW + ix;
652
+ const v = ((soft[j * 4] * (1 - ax) + soft[j * 4 + 4] * ax) * (1 - ay) + (soft[(j + FW) * 4] * (1 - ax) + soft[(j + FW) * 4 + 4] * ax) * ay) / 255;
653
+ const e = (edge[j] * (1 - ax) + edge[j + 1] * ax) * (1 - ay) + (edge[j + FW] * (1 - ax) + edge[j + FW + 1] * ax) * ay;
654
+ const T = TMIN + v * (TMAX - TMIN);
655
+ let n = 0.55 * eq(v * 255) + 0.45 * (T - tLo) / (tHi - tLo);
656
+ n = 0.42 + (n - 0.42) * (0.25 + 0.75 * settle); // AGC settling after calibration
657
+ n += (Rn() + Rn() - 1) * 0.022 + colFPN[x] + rowN;
658
+ n += e * 0.035;
659
+ if (t < 0.42) n = 0.36 + (x / W - 0.5) * 0.02 + (Rn() - 0.5) * 0.03; // shutter closed (NUC): flat field
660
+ const li = H.clamp(n, 0, 1) * 1023 | 0;
661
+ const i = (y * W + x) * 4;
662
+ d[i] = lut[li * 3]; d[i + 1] = lut[li * 3 + 1]; d[i + 2] = lut[li * 3 + 2]; d[i + 3] = 255;
663
+ }
664
+ }
665
+ ctx.putImageData(img, 0, 0);
666
+ // lens vignetting (microbolometer narcissus/ fall-off)
667
+ H.vignette(ctx, W, Hh, 0.35, '0,0,10');
668
+
669
+ // ---- HUD ----
670
+ const tempAt = (x, y) => { const j = ((y * sy) | 0) * FW + ((x * sx) | 0); return TMIN + soft[j * 4] / 255 * (TMAX - TMIN); };
671
+ ctx.save();
672
+ ctx.textBaseline = 'middle';
673
+ const hudText = (s, x, y, font, align = 'left', col = '#fff') => {
674
+ ctx.font = font; ctx.textAlign = align;
675
+ ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillText(s, x + 1.5, y + 1.5);
676
+ ctx.fillStyle = col; ctx.fillText(s, x, y);
677
+ };
678
+ const line = (pts, w = 2) => {
679
+ ctx.lineCap = 'butt';
680
+ ctx.strokeStyle = 'rgba(0,0,0,0.55)'; ctx.lineWidth = w + 2; ctx.beginPath(); pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.stroke();
681
+ ctx.strokeStyle = '#fff'; ctx.lineWidth = w; ctx.stroke();
682
+ };
683
+ // spot meter crosshair on the mug
684
+ const hudOn = t >= 0.45;
685
+ const glide = H.ease.inOut(H.smooth(1.9, 2.7, t));
686
+ const cx = Math.round(H.lerp(640, mx + 4, glide)), cy = Math.round(H.lerp(360, my + 4, glide));
687
+ const spotT = tempAt(cx, cy) + Math.sin(t * 3.1) * 0.1;
688
+ if (!hudOn) { ctx.font = '600 22px "Inter"'; ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.textAlign = 'center'; ctx.fillText('CALIBRATING\u2026', W / 2, Hh / 2); ctx.restore(); return; }
689
+ line([[cx - 30, cy], [cx - 9, cy]]); line([[cx + 9, cy], [cx + 30, cy]]);
690
+ line([[cx, cy - 30], [cx, cy - 9]]); line([[cx, cy + 9], [cx, cy + 30]]);
691
+ ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(cx, cy, 2, 0, 7); ctx.stroke();
692
+ hudText(spotT.toFixed(1) + '\u00b0', cx + 36, cy - 22, '600 20px "Inter"');
693
+ // measurement box on GPU rack
694
+ const bx0 = 1074, by0 = 160, bx1 = 1200, by1 = 360;
695
+ const boxP = H.ease.out(H.smooth(2.85, 3.15, t));
696
+ const bl = (a, b) => H.lerp(a, b, boxP), mxb = bx0 + 63, myb = by0 + 100;
697
+ ctx.setLineDash([]);
698
+ if (boxP > 0) line([[bl(mxb, bx0), bl(myb, by0)], [bl(mxb, bx1), bl(myb, by0)], [bl(mxb, bx1), bl(myb, by1)], [bl(mxb, bx0), bl(myb, by1)], [bl(mxb, bx0), bl(myb, by0) - 1]], 2);
699
+ let bmax = -99, bmx = 0, bmy = 0;
700
+ for (let y = by0; y < by1; y += 4) for (let x = bx0; x < bx1; x += 4) { const T = tempAt(x, y); if (T > bmax) { bmax = T; bmx = x; bmy = y; } }
701
+ if (boxP >= 1) hudText('Bx1', bx0 + 4, by0 - 14, '600 16px "Inter"');
702
+ // hot/cold markers
703
+ const tri = (x, y, col, up) => {
704
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.moveTo(x - 9, y + (up ? 8 : -8) + 1); ctx.lineTo(x + 10, y + (up ? 8 : -8) + 1); ctx.lineTo(x + 1, y + (up ? -8 : 8) + 1); ctx.fill();
705
+ ctx.fillStyle = col; ctx.beginPath(); ctx.moveTo(x - 9, y + (up ? 8 : -8)); ctx.lineTo(x + 9, y + (up ? 8 : -8)); ctx.lineTo(x, y + (up ? -8 : 8)); ctx.fill();
706
+ };
707
+ if (boxP >= 1) tri(bmx, bmy, '#ff2a1a', true);
708
+ tri(110, 300, '#28a8ff', false);
709
+
710
+ // top-left measurement table
711
+ ctx.fillStyle = 'rgba(0,0,0,0.38)'; ctx.fillRect(18, 18, 238, boxP >= 1 ? 84 : 46);
712
+ hudText('Sp1', 30, 40, '600 22px "Inter"'); hudText(spotT.toFixed(1) + ' °C', 244, 40, '600 22px "Inter"', 'right');
713
+ if (boxP >= 1) { hudText('Bx1 Max', 30, 78, '600 22px "Inter"'); hudText(bmax.toFixed(1) + ' °C', 244, 78, '600 22px "Inter"', 'right'); }
714
+ // top-right status: emissivity + battery
715
+ hudText('ε 0.95 IRONBOW', 1090, 34, '400 16px "Inter"', 'right');
716
+ ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.strokeRect(1112, 25, 34, 17); ctx.fillStyle = '#fff'; ctx.fillRect(1146, 30, 3, 7);
717
+ ctx.fillRect(1115, 28, 21, 11);
718
+ hudText('AUTO', 1172, 34, '600 16px "Inter"');
719
+
720
+ // colour scale bar (right edge)
721
+ const sbx = 1218, sbt = 112, sbb = 600;
722
+ const sg = ctx.createLinearGradient(0, sbb, 0, sbt);
723
+ IRONBOW.forEach(([p, c]) => sg.addColorStop(p, H.rgba(c, 1)));
724
+ ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(sbx - 2, sbt - 2, 26, sbb - sbt + 4);
725
+ ctx.fillStyle = sg; ctx.fillRect(sbx, sbt, 22, sbb - sbt);
726
+ hudText(tHi.toFixed(1), sbx + 22, sbt - 20, '600 20px "Inter"', 'right');
727
+ hudText(tLo.toFixed(1), sbx + 22, sbb + 22, '600 20px "Inter"', 'right');
728
+ hudText('°C', sbx + 11, sbb + 48, '400 16px "Inter"', 'center');
729
+
730
+ // annotation tag (on-screen label)
731
+ ctx.fillStyle = 'rgba(0,0,0,0.42)'; ctx.fillRect(18, t >= 3.1 ? 640 : 670, 330, t >= 3.1 ? 62 : 32);
732
+ if (t >= 3.1) hudText('GPU LOAD: ' + Math.round(98 * H.ease.out(H.smooth(3.1, 3.8, t))) + '%', 32, 662, '600 22px "Inter"');
733
+ const hh = 23 + Math.floor(t / 60), ss = (41 * 60 + 7 + Math.floor(t)) % 3600;
734
+ hudText(`2026-10-09 ${hh}:${String(Math.floor(ss / 60)).padStart(2, '0')}:${String(ss % 60).padStart(2, '0')}`, 32, 688, '600 16px "IBM Plex Mono"', 'left', 'rgba(255,255,255,0.85)');
735
+ ctx.restore();
736
+ },
737
+ });
738
+ }