agentvfx 0.0.0-stage → 0.3.5

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 (483) hide show
  1. package/AGENTS.md +8 -0
  2. package/GEMINI.md +1 -0
  3. package/LICENSE +202 -0
  4. package/README.md +228 -2
  5. package/package.json +35 -4
  6. package/skills/agentvfx/LICENSE +202 -0
  7. package/skills/agentvfx/NOTICE +50 -0
  8. package/skills/agentvfx/SKILL.md +90 -0
  9. package/skills/agentvfx/engine/core/constants.mjs +84 -0
  10. package/skills/agentvfx/engine/core/flash-constants.mjs +23 -0
  11. package/skills/agentvfx/engine/core/flash.mjs +240 -0
  12. package/skills/agentvfx/engine/core/mark.mjs +25 -0
  13. package/skills/agentvfx/engine/core/palettes.mjs +36 -0
  14. package/skills/agentvfx/engine/core/schedule.mjs +85 -0
  15. package/skills/agentvfx/engine/core/templates.mjs +43 -0
  16. package/skills/agentvfx/engine/core/util.mjs +167 -0
  17. package/skills/agentvfx/engine/core/validate.mjs +334 -0
  18. package/skills/agentvfx/engine/node/analysis.mjs +172 -0
  19. package/skills/agentvfx/engine/node/audio.mjs +621 -0
  20. package/skills/agentvfx/engine/node/board.mjs +24 -0
  21. package/skills/agentvfx/engine/node/chrome.mjs +349 -0
  22. package/skills/agentvfx/engine/node/encode.mjs +110 -0
  23. package/skills/agentvfx/engine/node/eye.mjs +77 -0
  24. package/skills/agentvfx/engine/node/footage.mjs +129 -0
  25. package/skills/agentvfx/engine/node/frame-worker.mjs +39 -0
  26. package/skills/agentvfx/engine/node/host.mjs +31 -0
  27. package/skills/agentvfx/engine/node/job.mjs +181 -0
  28. package/skills/agentvfx/engine/node/png.mjs +112 -0
  29. package/skills/agentvfx/engine/node/render.mjs +161 -0
  30. package/skills/agentvfx/engine/node/stage-capture.mjs +187 -0
  31. package/skills/agentvfx/engine/node/stage-serve.mjs +72 -0
  32. package/skills/agentvfx/engine/node/stills.mjs +100 -0
  33. package/skills/agentvfx/engine/node/ws-server.mjs +132 -0
  34. package/skills/agentvfx/engine/web/gl.mjs +319 -0
  35. package/skills/agentvfx/engine/web/glsl-common.mjs +78 -0
  36. package/skills/agentvfx/engine/web/init.js +23 -0
  37. package/skills/agentvfx/engine/web/prelude.mjs +150 -0
  38. package/skills/agentvfx/engine/web/render.html +11 -0
  39. package/skills/agentvfx/engine/web/render.mjs +388 -0
  40. package/skills/agentvfx/engine/web/scale.mjs +15 -0
  41. package/skills/agentvfx/engine/web/text.mjs +158 -0
  42. package/skills/agentvfx/plates/terminalcore/README.md +61 -0
  43. package/skills/agentvfx/plates/terminalcore/__pycache__/crt.cpython-313.pyc +0 -0
  44. package/skills/agentvfx/plates/terminalcore/__pycache__/font.cpython-313.pyc +0 -0
  45. package/skills/agentvfx/plates/terminalcore/__pycache__/gallery.cpython-313.pyc +0 -0
  46. package/skills/agentvfx/plates/terminalcore/__pycache__/gallery_mats.cpython-313.pyc +0 -0
  47. package/skills/agentvfx/plates/terminalcore/__pycache__/gallery_room.cpython-313.pyc +0 -0
  48. package/skills/agentvfx/plates/terminalcore/__pycache__/materials.cpython-313.pyc +0 -0
  49. package/skills/agentvfx/plates/terminalcore/__pycache__/nodekit.cpython-313.pyc +0 -0
  50. package/skills/agentvfx/plates/terminalcore/__pycache__/room.cpython-313.pyc +0 -0
  51. package/skills/agentvfx/plates/terminalcore/__pycache__/shots.cpython-313.pyc +0 -0
  52. package/skills/agentvfx/plates/terminalcore/__pycache__/textures.cpython-313.pyc +0 -0
  53. package/skills/agentvfx/plates/terminalcore/__pycache__/wall.cpython-313.pyc +0 -0
  54. package/skills/agentvfx/plates/terminalcore/crt.py +143 -0
  55. package/skills/agentvfx/plates/terminalcore/font.py +80 -0
  56. package/skills/agentvfx/plates/terminalcore/gallery.py +61 -0
  57. package/skills/agentvfx/plates/terminalcore/gallery_mats.py +144 -0
  58. package/skills/agentvfx/plates/terminalcore/gallery_room.py +126 -0
  59. package/skills/agentvfx/plates/terminalcore/materials.py +170 -0
  60. package/skills/agentvfx/plates/terminalcore/nodekit.py +87 -0
  61. package/skills/agentvfx/plates/terminalcore/room.py +82 -0
  62. package/skills/agentvfx/plates/terminalcore/scene.py +219 -0
  63. package/skills/agentvfx/plates/terminalcore/shots.py +48 -0
  64. package/skills/agentvfx/plates/terminalcore/textures.py +79 -0
  65. package/skills/agentvfx/plates/terminalcore/wall.py +103 -0
  66. package/skills/agentvfx/references/plates.md +22 -0
  67. package/skills/agentvfx/references/template-format.md +149 -0
  68. package/skills/agentvfx/references/web.md +63 -0
  69. package/skills/agentvfx/runtime/agentvfx.js +708 -0
  70. package/skills/agentvfx/schemas/fx-score.schema.json +98 -0
  71. package/skills/agentvfx/scripts/export-web.mjs +157 -0
  72. package/skills/agentvfx/scripts/loop.mjs +129 -0
  73. package/skills/agentvfx/scripts/make-beat.mjs +133 -0
  74. package/skills/agentvfx/scripts/plate.mjs +82 -0
  75. package/skills/agentvfx/scripts/vfx.mjs +173 -0
  76. package/skills/agentvfx/templates/_test/feedback-counter/fx.json +11 -0
  77. package/skills/agentvfx/templates/_test/feedback-counter/main.frag +4 -0
  78. package/skills/agentvfx/templates/_test/feedback-counter/pass0.frag +4 -0
  79. package/skills/agentvfx/templates/_test/gradient/fx.json +23 -0
  80. package/skills/agentvfx/templates/_test/gradient/main.frag +5 -0
  81. package/skills/agentvfx/templates/_test/identity/fx.json +10 -0
  82. package/skills/agentvfx/templates/_test/identity/main.frag +4 -0
  83. package/skills/agentvfx/templates/_test/strobe/fx.json +14 -0
  84. package/skills/agentvfx/templates/_test/strobe/main.frag +6 -0
  85. package/skills/agentvfx/templates/_test/text-probe/fx.json +15 -0
  86. package/skills/agentvfx/templates/_test/text-probe/main.frag +6 -0
  87. package/skills/agentvfx/templates/brutal/NOTES.md +14 -0
  88. package/skills/agentvfx/templates/brutal/_src/boot-log/def.mjs +57 -0
  89. package/skills/agentvfx/templates/brutal/_src/boot-log/main.src.frag +129 -0
  90. package/skills/agentvfx/templates/brutal/_src/build.mjs +33 -0
  91. package/skills/agentvfx/templates/brutal/_src/halftone-field/def.mjs +72 -0
  92. package/skills/agentvfx/templates/brutal/_src/halftone-field/main.src.frag +94 -0
  93. package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-left.src.glsl +100 -0
  94. package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-right.src.glsl +61 -0
  95. package/skills/agentvfx/templates/brutal/_src/hud-deck/deck-tiles.src.glsl +153 -0
  96. package/skills/agentvfx/templates/brutal/_src/hud-deck/def.mjs +82 -0
  97. package/skills/agentvfx/templates/brutal/_src/hud-deck/main.src.frag +110 -0
  98. package/skills/agentvfx/templates/brutal/_src/hud-deck/pass0.src.frag +38 -0
  99. package/skills/agentvfx/templates/brutal/_src/shared/defs.mjs +28 -0
  100. package/skills/agentvfx/templates/brutal/_src/shared/icons.glsl +72 -0
  101. package/skills/agentvfx/templates/brutal/_src/shared/lib.glsl +115 -0
  102. package/skills/agentvfx/templates/brutal/_src/shared/screen.glsl +43 -0
  103. package/skills/agentvfx/templates/brutal/boot-log/demo-acid.json +1 -0
  104. package/skills/agentvfx/templates/brutal/boot-log/demo-phosphor.json +1 -0
  105. package/skills/agentvfx/templates/brutal/boot-log/demo-ultramarine.json +1 -0
  106. package/skills/agentvfx/templates/brutal/boot-log/fx.json +257 -0
  107. package/skills/agentvfx/templates/brutal/boot-log/main.frag +242 -0
  108. package/skills/agentvfx/templates/brutal/collage/_src/00-lib.glsl +73 -0
  109. package/skills/agentvfx/templates/brutal/collage/_src/10-spectrum-scope.glsl +73 -0
  110. package/skills/agentvfx/templates/brutal/collage/_src/20-meter-readout-cross.glsl +87 -0
  111. package/skills/agentvfx/templates/brutal/collage/_src/30-footage-fillers.glsl +92 -0
  112. package/skills/agentvfx/templates/brutal/collage/_src/40-editorial.glsl +232 -0
  113. package/skills/agentvfx/templates/brutal/collage/_src/50-typefx.glsl +163 -0
  114. package/skills/agentvfx/templates/brutal/collage/_src/60-scene-lib.glsl +104 -0
  115. package/skills/agentvfx/templates/brutal/collage/_src/61-scene-specimen.glsl +71 -0
  116. package/skills/agentvfx/templates/brutal/collage/_src/62-scene-wiring.glsl +96 -0
  117. package/skills/agentvfx/templates/brutal/collage/_src/63-scene-fieldcut.glsl +119 -0
  118. package/skills/agentvfx/templates/brutal/collage/_src/64-scene-pattern.glsl +64 -0
  119. package/skills/agentvfx/templates/brutal/collage/_src/641-scene-anatomy.glsl +49 -0
  120. package/skills/agentvfx/templates/brutal/collage/_src/642-scene-deck.glsl +78 -0
  121. package/skills/agentvfx/templates/brutal/collage/_src/643-scene-bootlog.glsl +60 -0
  122. package/skills/agentvfx/templates/brutal/collage/_src/644-scene-tunnel.glsl +48 -0
  123. package/skills/agentvfx/templates/brutal/collage/_src/645-scene-runner.glsl +42 -0
  124. package/skills/agentvfx/templates/brutal/collage/_src/646-scene-ascii.glsl +66 -0
  125. package/skills/agentvfx/templates/brutal/collage/_src/647-scene-cubewall.glsl +80 -0
  126. package/skills/agentvfx/templates/brutal/collage/_src/648-scene-vortex.glsl +46 -0
  127. package/skills/agentvfx/templates/brutal/collage/_src/649-scene-sticker.glsl +72 -0
  128. package/skills/agentvfx/templates/brutal/collage/_src/65-scene-main.glsl +1 -0
  129. package/skills/agentvfx/templates/brutal/collage/_src/650-scene-ribbon.glsl +55 -0
  130. package/skills/agentvfx/templates/brutal/collage/_src/651-scene-slab.glsl +38 -0
  131. package/skills/agentvfx/templates/brutal/collage/_src/652-scene-framedial.glsl +79 -0
  132. package/skills/agentvfx/templates/brutal/collage/_src/653-scene-tilt.glsl +40 -0
  133. package/skills/agentvfx/templates/brutal/collage/_src/654-scene-marquee.glsl +45 -0
  134. package/skills/agentvfx/templates/brutal/collage/_src/655-scene-lens.glsl +57 -0
  135. package/skills/agentvfx/templates/brutal/collage/_src/656-scene-liquid.glsl +35 -0
  136. package/skills/agentvfx/templates/brutal/collage/_src/657-scene-panelgrid.glsl +75 -0
  137. package/skills/agentvfx/templates/brutal/collage/_src/658-scene-mirror.glsl +49 -0
  138. package/skills/agentvfx/templates/brutal/collage/_src/66-scene-main.glsl +27 -0
  139. package/skills/agentvfx/templates/brutal/collage/_src/90-main.glsl +120 -0
  140. package/skills/agentvfx/templates/brutal/collage/_src/build.mjs +24 -0
  141. package/skills/agentvfx/templates/brutal/collage/_src/sync-presets.mjs +16 -0
  142. package/skills/agentvfx/templates/brutal/collage/demo-bone-lime.json +1 -0
  143. package/skills/agentvfx/templates/brutal/collage/demo-lime-sparse.json +1 -0
  144. package/skills/agentvfx/templates/brutal/collage/demo-orange-dense.json +1 -0
  145. package/skills/agentvfx/templates/brutal/collage/demo-ultra-slow.json +1 -0
  146. package/skills/agentvfx/templates/brutal/collage/fx.json +4882 -0
  147. package/skills/agentvfx/templates/brutal/collage/layout.mjs +97 -0
  148. package/skills/agentvfx/templates/brutal/collage/layouts.json +366 -0
  149. package/skills/agentvfx/templates/brutal/collage/main.frag +2367 -0
  150. package/skills/agentvfx/templates/brutal/collage/pass0.frag +265 -0
  151. package/skills/agentvfx/templates/brutal/halftone-field/demo-acid.json +1 -0
  152. package/skills/agentvfx/templates/brutal/halftone-field/demo-ascii.json +1 -0
  153. package/skills/agentvfx/templates/brutal/halftone-field/demo-dither.json +1 -0
  154. package/skills/agentvfx/templates/brutal/halftone-field/demo-orange.json +1 -0
  155. package/skills/agentvfx/templates/brutal/halftone-field/demo-phosphor.json +1 -0
  156. package/skills/agentvfx/templates/brutal/halftone-field/demo-ultramarine.json +1 -0
  157. package/skills/agentvfx/templates/brutal/halftone-field/fx.json +474 -0
  158. package/skills/agentvfx/templates/brutal/halftone-field/main.frag +248 -0
  159. package/skills/agentvfx/templates/brutal/hud-deck/demo-acid.json +1 -0
  160. package/skills/agentvfx/templates/brutal/hud-deck/demo-night.json +1 -0
  161. package/skills/agentvfx/templates/brutal/hud-deck/demo-octagon.json +1 -0
  162. package/skills/agentvfx/templates/brutal/hud-deck/demo-ultramarine.json +1 -0
  163. package/skills/agentvfx/templates/brutal/hud-deck/fx.json +601 -0
  164. package/skills/agentvfx/templates/brutal/hud-deck/main.frag +645 -0
  165. package/skills/agentvfx/templates/brutal/hud-deck/pass0.frag +38 -0
  166. package/skills/agentvfx/templates/brutal/lattice/demo-cobalt-fine.json +1 -0
  167. package/skills/agentvfx/templates/brutal/lattice/demo-ink-yellow.json +1 -0
  168. package/skills/agentvfx/templates/brutal/lattice/demo-vermilion.json +1 -0
  169. package/skills/agentvfx/templates/brutal/lattice/demo-violet-slow.json +1 -0
  170. package/skills/agentvfx/templates/brutal/lattice/fx.json +130 -0
  171. package/skills/agentvfx/templates/brutal/lattice/main.frag +175 -0
  172. package/skills/agentvfx/templates/brutal/lattice/pass0.frag +29 -0
  173. package/skills/agentvfx/templates/glitch/block-glitch/demo-mosaic.json +1 -0
  174. package/skills/agentvfx/templates/glitch/block-glitch/demo-onset.json +1 -0
  175. package/skills/agentvfx/templates/glitch/block-glitch/demo-static.json +1 -0
  176. package/skills/agentvfx/templates/glitch/block-glitch/demo-tear.json +1 -0
  177. package/skills/agentvfx/templates/glitch/block-glitch/fx.json +54 -0
  178. package/skills/agentvfx/templates/glitch/block-glitch/main.frag +132 -0
  179. package/skills/agentvfx/templates/glitch/block-glitch/pass0.frag +12 -0
  180. package/skills/agentvfx/templates/glitch/pixel-sort/NOTES.md +13 -0
  181. package/skills/agentvfx/templates/glitch/pixel-sort/build-passes.mjs +162 -0
  182. package/skills/agentvfx/templates/glitch/pixel-sort/demo-chunk.json +1 -0
  183. package/skills/agentvfx/templates/glitch/pixel-sort/demo-hue.json +1 -0
  184. package/skills/agentvfx/templates/glitch/pixel-sort/demo-kick.json +1 -0
  185. package/skills/agentvfx/templates/glitch/pixel-sort/demo-streak.json +1 -0
  186. package/skills/agentvfx/templates/glitch/pixel-sort/demo-vertical.json +1 -0
  187. package/skills/agentvfx/templates/glitch/pixel-sort/fx.json +60 -0
  188. package/skills/agentvfx/templates/glitch/pixel-sort/main.frag +83 -0
  189. package/skills/agentvfx/templates/glitch/pixel-sort/pass0.frag +64 -0
  190. package/skills/agentvfx/templates/glitch/pixel-sort/pass1.frag +66 -0
  191. package/skills/agentvfx/templates/glitch/pixel-sort/pass2.frag +66 -0
  192. package/skills/agentvfx/templates/glitch/pixel-sort/pass3.frag +66 -0
  193. package/skills/agentvfx/templates/glitch/pixel-sort/pass4.frag +66 -0
  194. package/skills/agentvfx/templates/glitch/pixel-sort/pass5.frag +66 -0
  195. package/skills/agentvfx/templates/glitch/pixel-sort/pass6.frag +66 -0
  196. package/skills/agentvfx/templates/glitch/pixel-sort/pass7.frag +66 -0
  197. package/skills/agentvfx/templates/glitch/pixel-sort/pass8.frag +66 -0
  198. package/skills/agentvfx/templates/glitch/rgb-split/demo-diagonal-stutter.json +1 -0
  199. package/skills/agentvfx/templates/glitch/rgb-split/demo-hard.json +1 -0
  200. package/skills/agentvfx/templates/glitch/rgb-split/demo-kick.json +1 -0
  201. package/skills/agentvfx/templates/glitch/rgb-split/demo-radial.json +1 -0
  202. package/skills/agentvfx/templates/glitch/rgb-split/fx.json +46 -0
  203. package/skills/agentvfx/templates/glitch/rgb-split/main.frag +76 -0
  204. package/skills/agentvfx/templates/glitch/vhs/demo-clean.json +1 -0
  205. package/skills/agentvfx/templates/glitch/vhs/demo-crash.json +1 -0
  206. package/skills/agentvfx/templates/glitch/vhs/demo-tape.json +1 -0
  207. package/skills/agentvfx/templates/glitch/vhs/demo-worn.json +1 -0
  208. package/skills/agentvfx/templates/glitch/vhs/fx.json +46 -0
  209. package/skills/agentvfx/templates/glitch/vhs/main.frag +96 -0
  210. package/skills/agentvfx/templates/impact/impact-1bit/demo-onebit-bayer.json +1 -0
  211. package/skills/agentvfx/templates/impact/impact-1bit/fx.json +45 -0
  212. package/skills/agentvfx/templates/impact/impact-1bit/main.frag +64 -0
  213. package/skills/agentvfx/templates/impact/impact-invert/demo-invert-snap.json +1 -0
  214. package/skills/agentvfx/templates/impact/impact-invert/fx.json +37 -0
  215. package/skills/agentvfx/templates/impact/impact-invert/main.frag +44 -0
  216. package/skills/agentvfx/templates/impact/punch-shake/demo-punch-hard.json +1 -0
  217. package/skills/agentvfx/templates/impact/punch-shake/fx.json +36 -0
  218. package/skills/agentvfx/templates/impact/punch-shake/main.frag +62 -0
  219. package/skills/agentvfx/templates/impact/rgb-hit/demo-rgb-slam.json +1 -0
  220. package/skills/agentvfx/templates/impact/rgb-hit/fx.json +39 -0
  221. package/skills/agentvfx/templates/impact/rgb-hit/main.frag +62 -0
  222. package/skills/agentvfx/templates/impact/signal-cut/demo-lime-hold.json +1 -0
  223. package/skills/agentvfx/templates/impact/signal-cut/demo-lime.json +1 -0
  224. package/skills/agentvfx/templates/impact/signal-cut/demo-orange.json +1 -0
  225. package/skills/agentvfx/templates/impact/signal-cut/demo-ultramarine.json +1 -0
  226. package/skills/agentvfx/templates/impact/signal-cut/fx.json +83 -0
  227. package/skills/agentvfx/templates/impact/signal-cut/main.frag +101 -0
  228. package/skills/agentvfx/templates/impact/signal-cut/pass0.frag +68 -0
  229. package/skills/agentvfx/templates/impact/speedline-burst/demo-speed-white.json +1 -0
  230. package/skills/agentvfx/templates/impact/speedline-burst/fx.json +44 -0
  231. package/skills/agentvfx/templates/impact/speedline-burst/main.frag +68 -0
  232. package/skills/agentvfx/templates/impact/strobe-burst/demo-strobe-white-16.json +1 -0
  233. package/skills/agentvfx/templates/impact/strobe-burst/fx.json +42 -0
  234. package/skills/agentvfx/templates/impact/strobe-burst/main.frag +43 -0
  235. package/skills/agentvfx/templates/light/anamorphic-streak/demo-anamorphic-cinema.json +10 -0
  236. package/skills/agentvfx/templates/light/anamorphic-streak/demo-data.json +10 -0
  237. package/skills/agentvfx/templates/light/anamorphic-streak/fx.json +47 -0
  238. package/skills/agentvfx/templates/light/anamorphic-streak/main.frag +45 -0
  239. package/skills/agentvfx/templates/light/anamorphic-streak/pass0.frag +22 -0
  240. package/skills/agentvfx/templates/light/anamorphic-streak/pass1.frag +16 -0
  241. package/skills/agentvfx/templates/light/anamorphic-streak/pass2.frag +16 -0
  242. package/skills/agentvfx/templates/light/anamorphic-streak/pass3.frag +16 -0
  243. package/skills/agentvfx/templates/light/bloom-glow/demo-clip01-low.json +11 -0
  244. package/skills/agentvfx/templates/light/bloom-glow/demo-neon-night.json +10 -0
  245. package/skills/agentvfx/templates/light/bloom-glow/demo-phosphor.json +11 -0
  246. package/skills/agentvfx/templates/light/bloom-glow/fx.json +53 -0
  247. package/skills/agentvfx/templates/light/bloom-glow/main.frag +85 -0
  248. package/skills/agentvfx/templates/light/bloom-glow/pass0.frag +21 -0
  249. package/skills/agentvfx/templates/light/bloom-glow/pass1.frag +50 -0
  250. package/skills/agentvfx/templates/light/bloom-glow/pass2.frag +44 -0
  251. package/skills/agentvfx/templates/light/bloom-glow/pass3.frag +32 -0
  252. package/skills/agentvfx/templates/light/bloom-glow/pass4.frag +48 -0
  253. package/skills/agentvfx/templates/light/bloom-glow/pass5.frag +46 -0
  254. package/skills/agentvfx/templates/light/god-rays/demo-golden-hour.json +10 -0
  255. package/skills/agentvfx/templates/light/god-rays/demo-point-lamp.json +10 -0
  256. package/skills/agentvfx/templates/light/god-rays/fx.json +39 -0
  257. package/skills/agentvfx/templates/light/god-rays/main.frag +54 -0
  258. package/skills/agentvfx/templates/light/lens-flare/demo-clip01-bar.json +11 -0
  259. package/skills/agentvfx/templates/light/lens-flare/demo-eye-bar.json +10 -0
  260. package/skills/agentvfx/templates/light/lens-flare/demo-sun-ghosts.json +10 -0
  261. package/skills/agentvfx/templates/light/lens-flare/fx.json +49 -0
  262. package/skills/agentvfx/templates/light/lens-flare/main.frag +86 -0
  263. package/skills/agentvfx/templates/light/lens-flare/pass0.frag +24 -0
  264. package/skills/agentvfx/templates/light/light-leak/demo-slit.json +10 -0
  265. package/skills/agentvfx/templates/light/light-leak/demo-vintage-burn.json +10 -0
  266. package/skills/agentvfx/templates/light/light-leak/fx.json +40 -0
  267. package/skills/agentvfx/templates/light/light-leak/main.frag +46 -0
  268. package/skills/agentvfx/templates/light/light-stage/fx.json +29 -0
  269. package/skills/agentvfx/templates/light/light-stage/main.frag +56 -0
  270. package/skills/agentvfx/templates/light/light-sweep/demo-hard-bar.json +12 -0
  271. package/skills/agentvfx/templates/light/light-sweep/demo-logo-sweep.json +10 -0
  272. package/skills/agentvfx/templates/light/light-sweep/demo-onset.json +11 -0
  273. package/skills/agentvfx/templates/light/light-sweep/fx.json +45 -0
  274. package/skills/agentvfx/templates/light/light-sweep/main.frag +29 -0
  275. package/skills/agentvfx/templates/light/light-sweep/pass0.frag +17 -0
  276. package/skills/agentvfx/templates/light/light-sweep/pass1.frag +15 -0
  277. package/skills/agentvfx/templates/light/neon-saber/demo-hairline.json +10 -0
  278. package/skills/agentvfx/templates/light/neon-saber/demo-saber-laser.json +10 -0
  279. package/skills/agentvfx/templates/light/neon-saber/fx.json +48 -0
  280. package/skills/agentvfx/templates/light/neon-saber/main.frag +27 -0
  281. package/skills/agentvfx/templates/light/neon-saber/pass0.frag +46 -0
  282. package/skills/agentvfx/templates/light/neon-saber/pass1.frag +15 -0
  283. package/skills/agentvfx/templates/psychedelic/echo-flow/demo-trail.json +1 -0
  284. package/skills/agentvfx/templates/psychedelic/echo-flow/fx.json +38 -0
  285. package/skills/agentvfx/templates/psychedelic/echo-flow/main.frag +19 -0
  286. package/skills/agentvfx/templates/psychedelic/echo-flow/pass0.frag +16 -0
  287. package/skills/agentvfx/templates/psychedelic/echo-flow/pass1.frag +20 -0
  288. package/skills/agentvfx/templates/psychedelic/echo-flow/pass2.frag +11 -0
  289. package/skills/agentvfx/templates/psychedelic/echo-flow/pass3.frag +4 -0
  290. package/skills/agentvfx/templates/psychedelic/hologram-scan/demo-cyan.json +1 -0
  291. package/skills/agentvfx/templates/psychedelic/hologram-scan/fx.json +36 -0
  292. package/skills/agentvfx/templates/psychedelic/hologram-scan/main.frag +30 -0
  293. package/skills/agentvfx/templates/psychedelic/kaleido-fold/demo-six.json +1 -0
  294. package/skills/agentvfx/templates/psychedelic/kaleido-fold/fx.json +37 -0
  295. package/skills/agentvfx/templates/psychedelic/kaleido-fold/main.frag +10 -0
  296. package/skills/agentvfx/templates/psychedelic/kaleido-fold/pass0.frag +34 -0
  297. package/skills/agentvfx/templates/psychedelic/mosh-melt/demo-classic.json +1 -0
  298. package/skills/agentvfx/templates/psychedelic/mosh-melt/fx.json +38 -0
  299. package/skills/agentvfx/templates/psychedelic/mosh-melt/main.frag +20 -0
  300. package/skills/agentvfx/templates/psychedelic/mosh-melt/pass0.frag +22 -0
  301. package/skills/agentvfx/templates/psychedelic/sacred/NOTES.md +13 -0
  302. package/skills/agentvfx/templates/psychedelic/sacred/demo-classic.json +1 -0
  303. package/skills/agentvfx/templates/psychedelic/sacred/demo-halo.json +1 -0
  304. package/skills/agentvfx/templates/psychedelic/sacred/demo-slow-tide.json +1 -0
  305. package/skills/agentvfx/templates/psychedelic/sacred/demo-thermal.json +1 -0
  306. package/skills/agentvfx/templates/psychedelic/sacred/fx.json +263 -0
  307. package/skills/agentvfx/templates/psychedelic/sacred/main.frag +109 -0
  308. package/skills/agentvfx/templates/psychedelic/sacred/pass0.frag +25 -0
  309. package/skills/agentvfx/templates/psychedelic/sacred/pass1.frag +23 -0
  310. package/skills/agentvfx/templates/psychedelic/tidal-bands/demo-classic.json +1 -0
  311. package/skills/agentvfx/templates/psychedelic/tidal-bands/fx.json +37 -0
  312. package/skills/agentvfx/templates/psychedelic/tidal-bands/main.frag +54 -0
  313. package/skills/agentvfx/templates/scope/_chrome.glsl +242 -0
  314. package/skills/agentvfx/templates/scope/goniometer/demo-boot-green.json +1 -0
  315. package/skills/agentvfx/templates/scope/goniometer/demo-field-lime.json +1 -0
  316. package/skills/agentvfx/templates/scope/goniometer/demo-field-ultramarine.json +1 -0
  317. package/skills/agentvfx/templates/scope/goniometer/demo-glow-phosphor.json +1 -0
  318. package/skills/agentvfx/templates/scope/goniometer/demo-hud-black.json +1 -0
  319. package/skills/agentvfx/templates/scope/goniometer/fx.json +635 -0
  320. package/skills/agentvfx/templates/scope/goniometer/main.frag +33 -0
  321. package/skills/agentvfx/templates/scope/goniometer/pass0.frag +58 -0
  322. package/skills/agentvfx/templates/scope/goniometer/pass1.frag +43 -0
  323. package/skills/agentvfx/templates/scope/goniometer/pass2.frag +12 -0
  324. package/skills/agentvfx/templates/scope/goniometer/pass3.frag +12 -0
  325. package/skills/agentvfx/templates/scope/goniometer/pass4.frag +12 -0
  326. package/skills/agentvfx/templates/scope/goniometer/pass5.frag +12 -0
  327. package/skills/agentvfx/templates/scope/goniometer/pass6.frag +251 -0
  328. package/skills/agentvfx/templates/scope/scope-polar/demo-boot-green.json +1 -0
  329. package/skills/agentvfx/templates/scope/scope-polar/demo-field-lime.json +1 -0
  330. package/skills/agentvfx/templates/scope/scope-polar/demo-field-ultramarine.json +1 -0
  331. package/skills/agentvfx/templates/scope/scope-polar/demo-glow-phosphor.json +1 -0
  332. package/skills/agentvfx/templates/scope/scope-polar/demo-hud-black.json +1 -0
  333. package/skills/agentvfx/templates/scope/scope-polar/fx.json +686 -0
  334. package/skills/agentvfx/templates/scope/scope-polar/main.frag +39 -0
  335. package/skills/agentvfx/templates/scope/scope-polar/pass0.frag +84 -0
  336. package/skills/agentvfx/templates/scope/scope-polar/pass1.frag +49 -0
  337. package/skills/agentvfx/templates/scope/scope-polar/pass2.frag +12 -0
  338. package/skills/agentvfx/templates/scope/scope-polar/pass3.frag +12 -0
  339. package/skills/agentvfx/templates/scope/scope-polar/pass4.frag +12 -0
  340. package/skills/agentvfx/templates/scope/scope-polar/pass5.frag +12 -0
  341. package/skills/agentvfx/templates/scope/scope-polar/pass6.frag +251 -0
  342. package/skills/agentvfx/templates/scope/scope-triggered/demo-boot-green.json +1 -0
  343. package/skills/agentvfx/templates/scope/scope-triggered/demo-field-lime.json +1 -0
  344. package/skills/agentvfx/templates/scope/scope-triggered/demo-field-ultramarine.json +1 -0
  345. package/skills/agentvfx/templates/scope/scope-triggered/demo-glow-phosphor.json +1 -0
  346. package/skills/agentvfx/templates/scope/scope-triggered/demo-hud-black.json +1 -0
  347. package/skills/agentvfx/templates/scope/scope-triggered/fx.json +712 -0
  348. package/skills/agentvfx/templates/scope/scope-triggered/main.frag +36 -0
  349. package/skills/agentvfx/templates/scope/scope-triggered/pass0.frag +78 -0
  350. package/skills/agentvfx/templates/scope/scope-triggered/pass1.frag +50 -0
  351. package/skills/agentvfx/templates/scope/scope-triggered/pass2.frag +12 -0
  352. package/skills/agentvfx/templates/scope/scope-triggered/pass3.frag +12 -0
  353. package/skills/agentvfx/templates/scope/scope-triggered/pass4.frag +12 -0
  354. package/skills/agentvfx/templates/scope/scope-triggered/pass5.frag +12 -0
  355. package/skills/agentvfx/templates/scope/scope-triggered/pass6.frag +251 -0
  356. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-boot-green.json +1 -0
  357. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-field-lime.json +1 -0
  358. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-field-ultramarine.json +1 -0
  359. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-glow-phosphor.json +1 -0
  360. package/skills/agentvfx/templates/scope/spectrogram-scroll/demo-hud-black.json +1 -0
  361. package/skills/agentvfx/templates/scope/spectrogram-scroll/fx.json +595 -0
  362. package/skills/agentvfx/templates/scope/spectrogram-scroll/main.frag +83 -0
  363. package/skills/agentvfx/templates/scope/spectrogram-scroll/pass0.frag +52 -0
  364. package/skills/agentvfx/templates/scope/spectrogram-scroll/pass1.frag +251 -0
  365. package/skills/agentvfx/templates/scope/xy-phosphor/demo-boot-green.json +1 -0
  366. package/skills/agentvfx/templates/scope/xy-phosphor/demo-field-lime.json +1 -0
  367. package/skills/agentvfx/templates/scope/xy-phosphor/demo-field-ultramarine.json +1 -0
  368. package/skills/agentvfx/templates/scope/xy-phosphor/demo-glow-phosphor.json +1 -0
  369. package/skills/agentvfx/templates/scope/xy-phosphor/demo-hud-black.json +1 -0
  370. package/skills/agentvfx/templates/scope/xy-phosphor/fx.json +642 -0
  371. package/skills/agentvfx/templates/scope/xy-phosphor/main.frag +33 -0
  372. package/skills/agentvfx/templates/scope/xy-phosphor/pass0.frag +56 -0
  373. package/skills/agentvfx/templates/scope/xy-phosphor/pass1.frag +44 -0
  374. package/skills/agentvfx/templates/scope/xy-phosphor/pass2.frag +12 -0
  375. package/skills/agentvfx/templates/scope/xy-phosphor/pass3.frag +12 -0
  376. package/skills/agentvfx/templates/scope/xy-phosphor/pass4.frag +12 -0
  377. package/skills/agentvfx/templates/scope/xy-phosphor/pass5.frag +12 -0
  378. package/skills/agentvfx/templates/scope/xy-phosphor/pass6.frag +251 -0
  379. package/skills/agentvfx/templates/spectrum/_src/bars-linear/fx.src.json +61 -0
  380. package/skills/agentvfx/templates/spectrum/_src/bars-linear/main.src.frag +112 -0
  381. package/skills/agentvfx/templates/spectrum/_src/bars-linear/pass0.src.frag +2 -0
  382. package/skills/agentvfx/templates/spectrum/_src/bars-mirror/fx.src.json +51 -0
  383. package/skills/agentvfx/templates/spectrum/_src/bars-mirror/main.src.frag +72 -0
  384. package/skills/agentvfx/templates/spectrum/_src/bars-mirror/pass0.src.frag +2 -0
  385. package/skills/agentvfx/templates/spectrum/_src/bass-circle/fx.src.json +56 -0
  386. package/skills/agentvfx/templates/spectrum/_src/bass-circle/main.src.frag +136 -0
  387. package/skills/agentvfx/templates/spectrum/_src/bass-circle/pass0.src.frag +2 -0
  388. package/skills/agentvfx/templates/spectrum/_src/build.mjs +68 -0
  389. package/skills/agentvfx/templates/spectrum/_src/label-bars/fx.src.json +54 -0
  390. package/skills/agentvfx/templates/spectrum/_src/label-bars/main.src.frag +102 -0
  391. package/skills/agentvfx/templates/spectrum/_src/label-bars/pass0.src.frag +2 -0
  392. package/skills/agentvfx/templates/spectrum/_src/shared/bars-fn.glsl +64 -0
  393. package/skills/agentvfx/templates/spectrum/_src/shared/bars-pass.glsl +22 -0
  394. package/skills/agentvfx/templates/spectrum/_src/shared/clock.glsl +7 -0
  395. package/skills/agentvfx/templates/spectrum/_src/shared/hud.glsl +50 -0
  396. package/skills/agentvfx/templates/spectrum/_src/shared/stage.glsl +74 -0
  397. package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/fx.src.json +58 -0
  398. package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/main.src.frag +119 -0
  399. package/skills/agentvfx/templates/spectrum/_src/spectrum-particles/pass0.src.frag +33 -0
  400. package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/fx.src.json +56 -0
  401. package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/main.src.frag +95 -0
  402. package/skills/agentvfx/templates/spectrum/_src/spectrum-radial/pass0.src.frag +2 -0
  403. package/skills/agentvfx/templates/spectrum/_src/waveform-line/fx.src.json +60 -0
  404. package/skills/agentvfx/templates/spectrum/_src/waveform-line/main.src.frag +98 -0
  405. package/skills/agentvfx/templates/spectrum/_src/waveform-line/pass0.src.frag +43 -0
  406. package/skills/agentvfx/templates/spectrum/bars-linear/demo-graph-acid.json +1 -0
  407. package/skills/agentvfx/templates/spectrum/bars-linear/demo-graph-black.json +1 -0
  408. package/skills/agentvfx/templates/spectrum/bars-linear/fx.json +632 -0
  409. package/skills/agentvfx/templates/spectrum/bars-linear/main.frag +233 -0
  410. package/skills/agentvfx/templates/spectrum/bars-linear/pass0.frag +91 -0
  411. package/skills/agentvfx/templates/spectrum/bars-mirror/demo-field-acid.json +1 -0
  412. package/skills/agentvfx/templates/spectrum/bars-mirror/demo-field-ultramarine.json +1 -0
  413. package/skills/agentvfx/templates/spectrum/bars-mirror/fx.json +539 -0
  414. package/skills/agentvfx/templates/spectrum/bars-mirror/main.frag +193 -0
  415. package/skills/agentvfx/templates/spectrum/bars-mirror/pass0.frag +91 -0
  416. package/skills/agentvfx/templates/spectrum/bass-circle/demo-bracket-black.json +1 -0
  417. package/skills/agentvfx/templates/spectrum/bass-circle/demo-bracket-ultramarine.json +1 -0
  418. package/skills/agentvfx/templates/spectrum/bass-circle/fx.json +552 -0
  419. package/skills/agentvfx/templates/spectrum/bass-circle/main.frag +258 -0
  420. package/skills/agentvfx/templates/spectrum/bass-circle/pass0.frag +91 -0
  421. package/skills/agentvfx/templates/spectrum/label-bars/demo-deck-black.json +1 -0
  422. package/skills/agentvfx/templates/spectrum/label-bars/demo-deck-ultramarine.json +1 -0
  423. package/skills/agentvfx/templates/spectrum/label-bars/fx.json +552 -0
  424. package/skills/agentvfx/templates/spectrum/label-bars/main.frag +223 -0
  425. package/skills/agentvfx/templates/spectrum/label-bars/pass0.frag +91 -0
  426. package/skills/agentvfx/templates/spectrum/spectrum-particles/demo-cloud-black.json +1 -0
  427. package/skills/agentvfx/templates/spectrum/spectrum-particles/demo-cloud-ultramarine.json +1 -0
  428. package/skills/agentvfx/templates/spectrum/spectrum-particles/fx.json +584 -0
  429. package/skills/agentvfx/templates/spectrum/spectrum-particles/main.frag +240 -0
  430. package/skills/agentvfx/templates/spectrum/spectrum-particles/pass0.frag +101 -0
  431. package/skills/agentvfx/templates/spectrum/spectrum-radial/demo-starburst-black.json +1 -0
  432. package/skills/agentvfx/templates/spectrum/spectrum-radial/demo-starburst-ultramarine.json +1 -0
  433. package/skills/agentvfx/templates/spectrum/spectrum-radial/fx.json +566 -0
  434. package/skills/agentvfx/templates/spectrum/spectrum-radial/main.frag +217 -0
  435. package/skills/agentvfx/templates/spectrum/spectrum-radial/pass0.frag +91 -0
  436. package/skills/agentvfx/templates/spectrum/waveform-line/demo-strip-acid.json +1 -0
  437. package/skills/agentvfx/templates/spectrum/waveform-line/demo-strip-black.json +1 -0
  438. package/skills/agentvfx/templates/spectrum/waveform-line/fx.json +481 -0
  439. package/skills/agentvfx/templates/spectrum/waveform-line/main.frag +219 -0
  440. package/skills/agentvfx/templates/spectrum/waveform-line/pass0.frag +48 -0
  441. package/skills/agentvfx/templates/spectrum/waveform-line/pass1.frag +86 -0
  442. package/skills/agentvfx/test/_render_light.mjs +38 -0
  443. package/skills/agentvfx/test/audio-stereo.test.mjs +45 -0
  444. package/skills/agentvfx/test/audio.test.mjs +257 -0
  445. package/skills/agentvfx/test/brutal.test.mjs +190 -0
  446. package/skills/agentvfx/test/concept-collage-layout.test.mjs +236 -0
  447. package/skills/agentvfx/test/concept-collage-scenes.test.mjs +305 -0
  448. package/skills/agentvfx/test/concept-collage-typefx.test.mjs +153 -0
  449. package/skills/agentvfx/test/concept-collage.test.mjs +84 -0
  450. package/skills/agentvfx/test/concept-lattice.test.mjs +133 -0
  451. package/skills/agentvfx/test/concept-signal-cut.test.mjs +172 -0
  452. package/skills/agentvfx/test/credit-overlay.test.mjs +58 -0
  453. package/skills/agentvfx/test/credit.test.mjs +54 -0
  454. package/skills/agentvfx/test/doctor.test.mjs +31 -0
  455. package/skills/agentvfx/test/encode.test.mjs +22 -0
  456. package/skills/agentvfx/test/fixtures/r7-collage-LAYOUTS.json +5780 -0
  457. package/skills/agentvfx/test/fixtures/score-bad-duration.json +7 -0
  458. package/skills/agentvfx/test/fixtures/score-bad-safety.json +8 -0
  459. package/skills/agentvfx/test/fixtures/score-identity-footage.json +8 -0
  460. package/skills/agentvfx/test/fixtures/score-missing-template.json +7 -0
  461. package/skills/agentvfx/test/fixtures/score-ok.json +8 -0
  462. package/skills/agentvfx/test/fixtures/score-strobe-12hz.json +8 -0
  463. package/skills/agentvfx/test/fixtures/score-text-probe.json +1 -0
  464. package/skills/agentvfx/test/flash-audit.test.mjs +150 -0
  465. package/skills/agentvfx/test/flash.test.mjs +83 -0
  466. package/skills/agentvfx/test/footage.test.mjs +88 -0
  467. package/skills/agentvfx/test/glitch.test.mjs +272 -0
  468. package/skills/agentvfx/test/impact.test.mjs +231 -0
  469. package/skills/agentvfx/test/imports.test.mjs +39 -0
  470. package/skills/agentvfx/test/light.test.mjs +200 -0
  471. package/skills/agentvfx/test/loop.test.mjs +80 -0
  472. package/skills/agentvfx/test/psychedelic.test.mjs +99 -0
  473. package/skills/agentvfx/test/render.test.mjs +73 -0
  474. package/skills/agentvfx/test/sacred.test.mjs +175 -0
  475. package/skills/agentvfx/test/schema.test.mjs +103 -0
  476. package/skills/agentvfx/test/scope.test.mjs +306 -0
  477. package/skills/agentvfx/test/spectrum.test.mjs +165 -0
  478. package/skills/agentvfx/test/text.test.mjs +142 -0
  479. package/skills/agentvfx/test/web.test.mjs +400 -0
  480. package/skills/agentvfx/test-media/SOURCES.md +39 -0
  481. package/skills/agentvfx/test-media/beat-132.mp3 +0 -0
  482. package/skills/agentvfx/test-media/clip-01.mp4 +0 -0
  483. package/skills/agentvfx/test-media/clip-02.mp4 +0 -0
@@ -0,0 +1,240 @@
1
+ // Flash audit for agentvfx, adapted from lit-typographic-motion 1.0.45 engine/core/flash.mjs (MIT).
2
+ // Pure report: frames are read, never modified (spec Amendment A1: stage mode keeps full intensity).
3
+ //
4
+ // Method: each RGBA frame is area-averaged onto a 320x180 linear-light grid. A flash is a pair of
5
+ // OPPOSING luminance changes of the SAME area (WCAG 2.2 / BT.1702-3), so the test is per cell first:
6
+ // a cell registers a transition when its luminance moves at least 0.1 from the extreme it reached
7
+ // since its last transition, in the direction opposite to that last transition, while the darker of
8
+ // the two is below 0.8. Continued motion the same way is not a new transition. A frame-level
9
+ // transition exists when, on that one frame, the cells that registered a transition in one
10
+ // direction cover more than 25% of some 1/3 x 1/3 window of the grid. Cells that brighten and
11
+ // cells that darken in the same frame (a moving edge) never combine into a transition pair; a
12
+ // frame where both directions fill a window counts as a single alternating transition. Red uses
13
+ // the same method on (R-G-B)x320 over saturated-red cells (R/(R+G+B) >= 0.8). Verdicts: BT.1702-3
14
+ // (more than 6 transitions in any 1 s), any red flash, and the IRIS-style extended rule (at least 4
15
+ // transitions in each of 4 consecutive seconds, at any frame offset; reported alone, not folded
16
+ // into `exceeds`).
17
+ import { FLASH, LINEAR_LUT } from "./flash-constants.mjs";
18
+
19
+ export { LINEAR_LUT };
20
+
21
+ /**
22
+ * The audit grid for a frame: 6x6 px cells and the 640x360 window, transposed for a portrait
23
+ * frame (180x320 cells, a 60x107 window) so the 10-degree field keeps its size in either format.
24
+ */
25
+ export function gridFor(width = 1920, height = 1080) {
26
+ const [gw, gh] = FLASH.grid;
27
+ const [ww, wh] = FLASH.windowCells;
28
+ return width >= height ? { gw, gh, ww, wh } : { gw: gh, gh: gw, ww: wh, wh: ww };
29
+ }
30
+
31
+ /**
32
+ * Per-cell hysteresis. `hi`/`lo` are the extremes since the cell's last counted transition, `last`
33
+ * is that transition's direction (0 = none yet). A transition counts only against `last`.
34
+ */
35
+ class CellTracker {
36
+ constructor(cells, delta, ceiling) {
37
+ this.delta = delta;
38
+ this.ceiling = ceiling;
39
+ this.hi = new Float32Array(cells);
40
+ this.lo = new Float32Array(cells);
41
+ this.last = new Int8Array(cells);
42
+ }
43
+
44
+ start(values) {
45
+ this.hi.set(values);
46
+ this.lo.set(values);
47
+ }
48
+
49
+ /** Feed cell i's value; returns +1 / -1 when it counts a transition, else 0. */
50
+ update(i, v) {
51
+ const { hi, lo, last, delta, ceiling } = this;
52
+ let dir = 0;
53
+ if (last[i] !== 1 && v - lo[i] >= delta && Math.min(v, lo[i]) < ceiling) dir = 1;
54
+ else if (last[i] !== -1 && hi[i] - v >= delta && Math.min(v, hi[i]) < ceiling) dir = -1;
55
+ if (dir) {
56
+ hi[i] = v; lo[i] = v; last[i] = dir;
57
+ } else {
58
+ if (v > hi[i]) hi[i] = v;
59
+ if (v < lo[i]) lo[i] = v;
60
+ }
61
+ return dir;
62
+ }
63
+ }
64
+
65
+ /** Streaming detector. Each frame is { R, G, B }: linear 0..1 cell averages on the grid. */
66
+ export class FlashDetector {
67
+ constructor(grid = gridFor()) {
68
+ const { gw, gh, ww, wh } = grid;
69
+ this.grid = grid;
70
+ this.cells = gw * gh;
71
+ this.windowLimit = FLASH.windowArea * ww * wh;
72
+ this.frames = 0;
73
+ this.lum = new CellTracker(this.cells, FLASH.delta, FLASH.darkCeiling);
74
+ this.red = new CellTracker(this.cells, FLASH.redDelta, Infinity);
75
+ this.prevL = new Float32Array(this.cells);
76
+ this.sat = new Int32Array((gw + 1) * (gh + 1));
77
+ }
78
+
79
+ /**
80
+ * Feed one frame; returns { up, down, redUp, redDown, stepArea, edge }. The four flags say that,
81
+ * on this frame, the cells registering a transition that way cover a window; `edge` says this
82
+ * frame moved any cell that way.
83
+ */
84
+ push({ R, G, B }) {
85
+ const CELLS = this.cells;
86
+ const L = new Float32Array(CELLS);
87
+ const V = new Float32Array(CELLS);
88
+ for (let i = 0; i < CELLS; i++) {
89
+ L[i] = 0.2126 * R[i] + 0.7152 * G[i] + 0.0722 * B[i];
90
+ const sum = R[i] + G[i] + B[i];
91
+ V[i] = sum > 0 && R[i] / sum >= FLASH.redRatio ? Math.max(0, R[i] - G[i] - B[i]) * FLASH.redScale : 0;
92
+ }
93
+ const maps = { up: new Uint8Array(CELLS), down: new Uint8Array(CELLS), redUp: new Uint8Array(CELLS), redDown: new Uint8Array(CELLS) };
94
+ const edge = { up: false, down: false, redUp: false, redDown: false };
95
+ let stepCells = 0;
96
+ if (this.frames === 0) {
97
+ this.lum.start(L);
98
+ this.red.start(V);
99
+ } else {
100
+ for (let i = 0; i < CELLS; i++) {
101
+ if (Math.abs(L[i] - this.prevL[i]) >= FLASH.delta) stepCells += 1;
102
+ const l = this.lum.update(i, L[i]);
103
+ if (l) { maps[l > 0 ? "up" : "down"][i] = 1; edge[l > 0 ? "up" : "down"] = true; }
104
+ const r = this.red.update(i, V[i]);
105
+ if (r) { maps[r > 0 ? "redUp" : "redDown"][i] = 1; edge[r > 0 ? "redUp" : "redDown"] = true; }
106
+ }
107
+ }
108
+ this.prevL = L;
109
+ this.frames += 1;
110
+ const result = { stepArea: +(stepCells / CELLS).toFixed(4), edge };
111
+ for (const dir of ["up", "down", "redUp", "redDown"]) result[dir] = edge[dir] && this.windowHit(maps[dir]);
112
+ return result;
113
+ }
114
+
115
+ // Largest window count of this frame's transition map via a summed-area table.
116
+ windowHit(map) {
117
+ const { gw: GW, gh: GH, ww: WW, wh: WH } = this.grid;
118
+ const sat = this.sat;
119
+ const stride = GW + 1;
120
+ for (let y = 0; y < GH; y++) {
121
+ let row = 0;
122
+ for (let x = 0; x < GW; x++) {
123
+ row += map[y * GW + x];
124
+ sat[(y + 1) * stride + x + 1] = sat[y * stride + x + 1] + row;
125
+ }
126
+ }
127
+ if (sat[GH * stride + GW] <= this.windowLimit) return false;
128
+ for (let y = 0; y + WH <= GH; y++) {
129
+ for (let x = 0; x + WW <= GW; x++) {
130
+ const count = sat[(y + WH) * stride + x + WW] - sat[y * stride + x + WW] - sat[(y + WH) * stride + x] + sat[y * stride + x];
131
+ if (count > this.windowLimit) return true;
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /** Area-average an RGBA8 frame of any size onto its audit grid in linear light. */
139
+ export function cellsFromRgba(bytes, width, height, lut = LINEAR_LUT, { gw: GW, gh: GH } = gridFor(width, height)) {
140
+ const CELLS = GW * GH;
141
+ const R = new Float32Array(CELLS);
142
+ const G = new Float32Array(CELLS);
143
+ const B = new Float32Array(CELLS);
144
+ const count = new Float32Array(CELLS);
145
+ for (let y = 0; y < height; y++) {
146
+ const cy = Math.min(GH - 1, Math.floor((y * GH) / height)) * GW;
147
+ for (let x = 0; x < width; x++) {
148
+ const c = cy + Math.min(GW - 1, Math.floor((x * GW) / width));
149
+ const o = (y * width + x) * 4;
150
+ R[c] += lut[bytes[o]];
151
+ G[c] += lut[bytes[o + 1]];
152
+ B[c] += lut[bytes[o + 2]];
153
+ count[c] += 1;
154
+ }
155
+ }
156
+ for (let i = 0; i < CELLS; i++) {
157
+ const n = count[i] || 1;
158
+ R[i] /= n; G[i] /= n; B[i] /= n;
159
+ }
160
+ return { R, G, B };
161
+ }
162
+
163
+ // Consecutive same-direction transitions collapse into one edge; an edge is one "transition".
164
+ function edgesIn(events, start, span) {
165
+ let edges = 0;
166
+ let last = 0;
167
+ for (const t of events) {
168
+ if (t.frame < start || t.frame >= start + span || t.dir === last) continue;
169
+ edges += 1;
170
+ last = t.dir;
171
+ }
172
+ return edges;
173
+ }
174
+
175
+ function summarize(general, red, count, fps) {
176
+ const span = Math.round(fps);
177
+ let worst = { startFrame: 0, transitions: 0 };
178
+ for (const { frame } of general) {
179
+ const transitions = edgesIn(general, frame, span);
180
+ if (transitions > worst.transitions) worst = { startFrame: frame, transitions };
181
+ }
182
+ const perSecond = [];
183
+ for (let s = 0; s * span < count; s++) {
184
+ perSecond.push({ second: s, transitions: edgesIn(general, s * span, span), redTransitions: edgesIn(red, s * span, span) });
185
+ }
186
+ let extendedFlashing = false;
187
+ for (let s0 = 0; !extendedFlashing && s0 + FLASH.extendedSeconds * span <= count; s0++) {
188
+ extendedFlashing = Array.from({ length: FLASH.extendedSeconds }, (_, k) => edgesIn(general, s0 + k * span, span))
189
+ .every((n) => n >= FLASH.extendedMinTransitions);
190
+ }
191
+ const redFlashes = Math.floor(edgesIn(red, 0, count) / 2);
192
+ return {
193
+ exceeds: worst.transitions > FLASH.maxTransitionsPerSecond || redFlashes > 0,
194
+ worstWindow: { ...worst, startSec: +(worst.startFrame / fps).toFixed(3) },
195
+ flashesPerSecondMax: Math.floor(worst.transitions / 2),
196
+ redFlashes,
197
+ extendedFlashing,
198
+ perSecond,
199
+ limits: { ...FLASH, fps, frames: count },
200
+ };
201
+ }
202
+
203
+ // One event per frame: a lone direction is that direction; both directions filling windows on the
204
+ // same frame (opposite areas moving against each other) is one transition, alternating with the last.
205
+ function frameDir(up, down, last) {
206
+ if (up && down) return last === 1 ? -1 : 1;
207
+ return up ? 1 : down ? -1 : 0;
208
+ }
209
+
210
+ /**
211
+ * Streaming auditor: push RGBA frames one at a time (only transition events are kept, never the
212
+ * frames), then finish() for the report. Reports only; the pushed bytes are not modified.
213
+ */
214
+ export function createFlashAuditor({ fps, width, height }) {
215
+ if (!(Number.isFinite(fps) && fps > 0)) throw new TypeError("createFlashAuditor: fps must be a positive number");
216
+ const grid = gridFor(width, height);
217
+ const detector = new FlashDetector(grid);
218
+ const general = [];
219
+ const red = [];
220
+ return {
221
+ push(frame) {
222
+ const frameIndex = detector.frames;
223
+ const r = detector.push(cellsFromRgba(frame.data, frame.width, frame.height, LINEAR_LUT, grid));
224
+ const dir = frameDir(r.up, r.down, general.at(-1)?.dir ?? 0);
225
+ if (dir) general.push({ frame: frameIndex, dir });
226
+ const redDir = frameDir(r.redUp, r.redDown, red.at(-1)?.dir ?? 0);
227
+ if (redDir) red.push({ frame: frameIndex, dir: redDir });
228
+ },
229
+ finish: () => summarize(general, red, detector.frames, fps),
230
+ };
231
+ }
232
+
233
+ /** Audit a whole frame sequence ({ width, height, data } RGBA sRGB) at `fps`. */
234
+ export function auditFlash(frames, { fps }) {
235
+ if (!(Number.isFinite(fps) && fps > 0)) throw new TypeError("auditFlash: fps must be a positive number");
236
+ const first = frames[0] ?? { width: 1920, height: 1080 };
237
+ const auditor = createFlashAuditor({ fps, width: first.width, height: first.height });
238
+ for (const frame of frames) auditor.push(frame);
239
+ return auditor.finish();
240
+ }
@@ -0,0 +1,25 @@
1
+ // agentvfx (Apache-2.0, see LICENSE). Local replacement for the plugin's lib/mark.mjs: the generator
2
+ // tag and the credit line that ffmpeg writes into every MP4, plus an optional comment (the stage-mode flashing notice).
3
+ // The credit also goes to CREDIT.txt next to the film, so it is easy to paste when the film is published.
4
+ export const GENERATOR = "agentvfx";
5
+ export const CREDIT_TEXT = "Made with agentvfx by Woojin Go - https://github.com/wjgoarxiv/agentvfx";
6
+ export const CREDIT_SHORT = "VFX: agentvfx (github.com/wjgoarxiv/agentvfx)";
7
+
8
+ export function creditText() {
9
+ return `${CREDIT_TEXT}
10
+
11
+ Publishing this film? A credit is appreciated, not required. The film carries a small on-screen credit; you can
12
+ keep it in the frame, or switch it off ("credit": { "overlay": false }) and put the line above in the description,
13
+ caption or credits. Where space is short: ${CREDIT_SHORT}
14
+ Licence: Apache-2.0. See LICENSE and NOTICE in https://github.com/wjgoarxiv/agentvfx.
15
+ `;
16
+ }
17
+
18
+ export function ffmpegMetadataArgs({ comment = null } = {}) {
19
+ const args = ["-metadata", `encoder_tool=${GENERATOR}`, "-metadata", `description=${CREDIT_TEXT}`];
20
+ if (comment) args.push("-metadata", `comment=${comment}`);
21
+ return args;
22
+ }
23
+
24
+ // The on-screen credit burned into a corner of every render unless the score sets "credit": { "overlay": false }.
25
+ export const CREDIT_OVERLAY = "made with agentvfx";
@@ -0,0 +1,36 @@
1
+ // agentvfx (Apache-2.0, see LICENSE). Shared palettes, usable by any template that declares a `palette` input.
2
+ // Each palette is a list of sRGB hex stops; templates sample it as a ramp (index 0..1) or as a cyclic
3
+ // ramp. Brutal palettes are black, white and one acid accent, in the spirit of festival HUD
4
+ // colour grammar; the names are neutral and no existing colour system is copied.
5
+
6
+ export const PALETTES = Object.freeze({
7
+ "brutal-acid": ["#000000", "#ffffff", "#c8ff00"],
8
+ "brutal-red": ["#000000", "#ffffff", "#ff2a00"],
9
+ mono: ["#000000", "#ffffff"],
10
+ rainbow: ["#ff0040", "#ff8000", "#ffee00", "#30ff40", "#00e5ff", "#2040ff", "#b000ff", "#ff00c8"],
11
+ thermal: ["#0010ff", "#6a00ff", "#ff00d0", "#ff0030", "#ff7a00", "#ffee00", "#00ffe0"],
12
+ "paper-grey": ["#d9d9d6", "#0a0a0a", "#ff2a6d"],
13
+ "acid-data": ["#000000", "#b6ff00", "#1f4dff"],
14
+ "hazard-orange": ["#141414", "#8c8c88", "#ff5a00"],
15
+ "flesh-pink": ["#ffffff", "#ff3c8e", "#111111"],
16
+ "lilac-terminal": ["#0b0613", "#b69cff", "#ff6ad5"],
17
+ "blood-red": ["#000000", "#d10000", "#ffffff"],
18
+ "clinic-mint": ["#f2f4f1", "#7fe3c1", "#b8a4ff"],
19
+ });
20
+
21
+ export const PALETTE_NAMES = Object.freeze(Object.keys(PALETTES));
22
+
23
+ /** "#rrggbb" -> [r, g, b] in 0..1 (sRGB). */
24
+ export function hexToRgb(hex) {
25
+ const m = /^#([0-9a-f]{6})$/iu.exec(hex);
26
+ if (!m) throw new Error(`not a #rrggbb colour: ${hex}`);
27
+ const n = parseInt(m[1], 16);
28
+ return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
29
+ }
30
+
31
+ /** A palette name or a custom list of 2-8 hex stops -> list of [r, g, b]. */
32
+ export function resolvePalette(value) {
33
+ const stops = typeof value === "string" ? PALETTES[value] : value;
34
+ if (!Array.isArray(stops)) throw new Error(`unknown palette: ${value}`);
35
+ return stops.map(hexToRgb);
36
+ }
@@ -0,0 +1,85 @@
1
+ // agentvfx (Apache-2.0, see LICENSE). The score compositor's timeline: for each frame, which layers are active
2
+ // (base stack, cuts, hits), their parameter values (preset, params, audio bindings) and their local
3
+ // time. Pure and deterministic; the page only draws what this returns.
4
+ import { resolveParams } from "./templates.mjs";
5
+
6
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v);
7
+
8
+ /** Resolve a `t` / `beat` / `onset` reference to a frame index. */
9
+ export function resolveFrame(ref, { fps, features }, path) {
10
+ if (ref.t !== undefined) return Math.round(ref.t * fps);
11
+ const kind = ref.beat !== undefined ? "beat" : "onset";
12
+ const list = kind === "beat" ? features?.meta?.beats : features?.meta?.onsets;
13
+ if (!list) throw Object.assign(new Error(`${path}.${kind}: needs an audio source (input.audio or a video with sound)`), { path: `${path}.${kind}` });
14
+ const idx = ref[kind];
15
+ if (idx >= list.length) throw Object.assign(new Error(`${path}.${kind}: index ${idx} but the track has ${list.length} ${kind}s`), { path: `${path}.${kind}` });
16
+ return list[idx];
17
+ }
18
+
19
+ const layerRef = (l) => (typeof l === "string" ? { template: l } : l);
20
+
21
+ /** Feature value for a binding ("bands.12" or a scalar name) on one frame of features. */
22
+ function featureValue(frame, feature) {
23
+ if (!frame) return 0;
24
+ if (feature.startsWith("bands.")) return Number(frame.bands?.[Number(feature.slice(6))] ?? 0);
25
+ return Number(frame[feature] ?? 0);
26
+ }
27
+
28
+ /**
29
+ * Build the per-frame plan. Returns { frameCount, frames: [{ layers: [{key, template, params, t}] }],
30
+ * templatesUsed: Set, events: [frame...] } where events are the frames of hits and cuts (for strips).
31
+ */
32
+ export function buildSchedule(score, lib, { features = null } = {}) {
33
+ const fps = score.fps;
34
+ const frameCount = Math.round(score.durationSec * fps);
35
+ const ctx = { fps, features };
36
+ const used = new Set();
37
+ const resolve = (ref) => {
38
+ const tpl = lib.get(ref.template);
39
+ used.add(ref.template);
40
+ const text = tpl.fx.X_AGENTVFX.text;
41
+ const labels = text ? Object.fromEntries(text.map((t) => [t.key, score.labels?.[t.key] ?? t.default])) : null;
42
+ const styled = text && score.textStyle ? text.filter((t) => score.textStyle[t.key]) : [];
43
+ const textStyle = styled.length ? Object.fromEntries(styled.map((t) => [t.key, score.textStyle[t.key]])) : null;
44
+ return { template: ref.template, params: resolveParams(tpl.fx, ref), inputs: tpl.fx.INPUTS ?? [], labels, textStyle };
45
+ };
46
+ const initial = [{ template: score.template, preset: score.preset, params: score.params }, ...(score.stack ?? []).map(layerRef)].map(resolve);
47
+ const cuts = (score.cuts ?? []).map((c, i) => ({ frame: resolveFrame(c, ctx, `cuts[${i}]`), index: i, layers: c.layers.map(layerRef).map(resolve) }))
48
+ .sort((a, b) => a.frame - b.frame || a.index - b.index);
49
+ const hits = (score.hits ?? []).map((h, i) => ({ start: resolveFrame(h, ctx, `hits[${i}]`), index: i, duration: h.durationFrames, layer: resolve({ template: h.template, preset: h.preset, params: h.params }) }));
50
+
51
+ const bindings = (score.bindings ?? []).map((b) => ({ ...b, state: 0 }));
52
+ const frames = [];
53
+ for (let f = 0; f < frameCount; f++) {
54
+ let base = { id: "b", layers: initial };
55
+ for (const c of cuts) if (c.frame <= f) base = { id: `c${c.index}`, layers: c.layers };
56
+ const featureFrame = features?.frames?.[f] ?? null;
57
+ const bound = {};
58
+ for (const b of bindings) {
59
+ const raw = featureValue(featureFrame, b.feature);
60
+ const ms = raw > b.state ? b.attackMs : b.releaseMs;
61
+ const k = isNum(ms) && ms > 0 ? Math.exp(-1000 / (fps * ms)) : 0;
62
+ b.state = k * b.state + (1 - k) * raw;
63
+ bound[b.param] = b.offset + b.gain * b.state;
64
+ }
65
+ const applyBindings = (layer) => {
66
+ if (layer.template !== score.template || !bindings.length) return layer.params;
67
+ const params = { ...layer.params };
68
+ for (const input of layer.inputs) {
69
+ if (bound[input.NAME] === undefined) continue;
70
+ let v = bound[input.NAME];
71
+ if (isNum(input.MIN)) v = Math.max(input.MIN, v);
72
+ if (isNum(input.MAX)) v = Math.min(input.MAX, v);
73
+ params[input.NAME] = input.TYPE === "long" ? Math.round(v) : input.TYPE === "bool" ? v > 0.5 : v;
74
+ }
75
+ return params;
76
+ };
77
+ const layers = base.layers.map((l, i) => ({ key: `${base.id}:${i}`, template: l.template, params: applyBindings(l), t: f / fps, ...(l.labels ? { labels: l.labels } : {}), ...(l.textStyle ? { textStyle: l.textStyle } : {}) }));
78
+ for (const h of hits) {
79
+ if (f >= h.start && f < h.start + h.duration) layers.push({ key: `h${h.index}`, template: h.layer.template, params: h.layer.params, t: (f - h.start) / fps, ...(h.layer.labels ? { labels: h.layer.labels } : {}), ...(h.layer.textStyle ? { textStyle: h.layer.textStyle } : {}) });
80
+ }
81
+ frames.push({ layers });
82
+ }
83
+ const events = [...new Set([...cuts.map((c) => c.frame), ...hits.map((h) => h.start)])].filter((f) => f >= 0 && f < frameCount).sort((a, b) => a - b);
84
+ return { frameCount, fps, frames, templatesUsed: used, events };
85
+ }
@@ -0,0 +1,43 @@
1
+ // agentvfx (Apache-2.0, see LICENSE). The template library: every templates/<family>/<id>/fx.json, keyed by
2
+ // X_AGENTVFX.id. Test templates live under templates/_test/ and are loaded only on request.
3
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const TEMPLATES_ROOT = fileURLToPath(new URL("../../templates/", import.meta.url));
8
+
9
+ /** Map id -> { id, dir, rel, family, fx, passes: [file...] }. Throws on a duplicate id or unreadable JSON. */
10
+ export function loadLibrary({ root = TEMPLATES_ROOT, includeTest = false } = {}) {
11
+ const lib = new Map();
12
+ if (!existsSync(root)) return lib;
13
+ for (const family of readdirSync(root).sort()) {
14
+ const fdir = path.join(root, family);
15
+ if (!statSync(fdir).isDirectory()) continue;
16
+ if (family === "_test" && !includeTest) continue;
17
+ if (family.startsWith(".") || (family.startsWith("_") && family !== "_test")) continue;
18
+ for (const name of readdirSync(fdir).sort()) {
19
+ const dir = path.join(fdir, name);
20
+ const file = path.join(dir, "fx.json");
21
+ if (!statSync(dir).isDirectory() || !existsSync(file)) continue;
22
+ let fx;
23
+ try {
24
+ fx = JSON.parse(readFileSync(file, "utf8"));
25
+ } catch (error) {
26
+ throw new Error(`${path.relative(root, file)}: invalid JSON (${error.message})`);
27
+ }
28
+ const id = fx?.X_AGENTVFX?.id ?? name;
29
+ if (lib.has(id)) throw new Error(`duplicate template id ${id} (${path.relative(root, lib.get(id).dir)} and ${path.relative(root, dir)})`);
30
+ const passes = readdirSync(dir).filter((f) => /^(main|pass\d+)\.frag$/u.test(f)).sort();
31
+ lib.set(id, { id, dir, rel: path.relative(root, dir).split(path.sep).join("/"), family, fx, passes });
32
+ }
33
+ }
34
+ return lib;
35
+ }
36
+
37
+ /** Defaults from INPUTS, then the named preset, then explicit params. */
38
+ export function resolveParams(fx, { preset = null, params = {} } = {}) {
39
+ const out = {};
40
+ for (const input of fx.INPUTS ?? []) if (input.DEFAULT !== undefined) out[input.NAME] = input.DEFAULT;
41
+ if (preset) Object.assign(out, fx.X_AGENTVFX.presets[preset]);
42
+ return Object.assign(out, params);
43
+ }
@@ -0,0 +1,167 @@
1
+ // Pure math, easing and seeded-randomness helpers shared by the page engine and the Node side.
2
+ // Adapted from mexicat/pdoom-video app/src/engine/util.ts (MIT, see ../NOTICE). Every function is a
3
+ // deterministic function of its arguments: nothing here may read a clock or an unseeded RNG
4
+ // (MO-A-23), because the determinism gate hashes frames rendered by two separate processes.
5
+
6
+ export const clamp = (x, a = 0, b = 1) => (x < a ? a : x > b ? b : x);
7
+ export const lerp = (a, b, t) => a + (b - a) * t;
8
+ export const invLerp = (a, b, x) => (a === b ? 0 : (x - a) / (b - a));
9
+ export const remap = (x, a, b, c, d, clamped = true) => {
10
+ const t = invLerp(a, b, x);
11
+ return lerp(c, d, clamped ? clamp(t) : t);
12
+ };
13
+ export const smoothstep = (a, b, x) => {
14
+ const t = clamp((x - a) / (b - a));
15
+ return t * t * (3 - 2 * t);
16
+ };
17
+ export const fract = (x) => x - Math.floor(x);
18
+ export const mod = (x, m) => ((x % m) + m) % m;
19
+ export const TAU = Math.PI * 2;
20
+
21
+ // Cubic bezier easing, solved by a fixed 40-step bisection so the value never depends on a
22
+ // convergence tolerance.
23
+ function bezier(x1, y1, x2, y2) {
24
+ const coord = (t, a, b) => 3 * a * t * (1 - t) ** 2 + 3 * b * t * t * (1 - t) + t ** 3;
25
+ return (x) => {
26
+ if (x <= 0) return 0;
27
+ if (x >= 1) return 1;
28
+ let lo = 0;
29
+ let hi = 1;
30
+ for (let i = 0; i < 40; i++) {
31
+ const mid = (lo + hi) / 2;
32
+ if (coord(mid, x1, x2) < x) lo = mid;
33
+ else hi = mid;
34
+ }
35
+ return coord((lo + hi) / 2, y1, y2);
36
+ };
37
+ }
38
+
39
+ // The named easing set (MO-A-08). Scenes pick from these names; ad hoc curves are not allowed.
40
+ export const ease = Object.freeze({
41
+ linear: (t) => t,
42
+ outQuad: (t) => 1 - (1 - t) * (1 - t),
43
+ inOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2),
44
+ outCubic: (t) => 1 - (1 - t) ** 3,
45
+ inOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
46
+ outExpo: (t) => (t >= 1 ? 1 : 1 - 2 ** (-10 * t)),
47
+ // Film tokens from the preset bibles (MO-B-01..03).
48
+ slam: bezier(0.16, 1, 0.3, 1),
49
+ drift: bezier(0.37, 0, 0.63, 1),
50
+ surgePunch: bezier(0.16, 1, 0.3, 1),
51
+ });
52
+
53
+ /** Eased progress of x through [a, b], clamped. */
54
+ export const prog = (x, a, b, fn = ease.linear) => fn(clamp((x - a) / (b - a)));
55
+
56
+ /**
57
+ * Piecewise interpolation through keyframes `[[time, value, easeName], ...]`; the ease on key i
58
+ * shapes the segment that ends at key i. Only names from `ease` are accepted.
59
+ */
60
+ export function keys(t, ks) {
61
+ if (ks.length === 0) return 0;
62
+ if (t <= ks[0][0]) return ks[0][1];
63
+ for (let i = 1; i < ks.length; i++) {
64
+ const [time, value, name = "inOutCubic"] = ks[i];
65
+ if (t <= time) {
66
+ const [pt, pv] = ks[i - 1];
67
+ const fn = ease[name];
68
+ if (!fn) throw new Error(`unknown ease: ${name}`);
69
+ return lerp(pv, value, fn((t - pt) / (time - pt)));
70
+ }
71
+ }
72
+ return ks[ks.length - 1][1];
73
+ }
74
+
75
+ /** Damped spring response to a step at time 0 (the rare deliberate overshoot, MO-A-08). */
76
+ export const springStep = (t, freq = 4, damping = 0.35) => {
77
+ if (t <= 0) return 0;
78
+ const w = TAU * freq;
79
+ return 1 - Math.exp(-damping * w * t) * Math.cos(w * Math.sqrt(1 - damping * damping) * t);
80
+ };
81
+
82
+ /** Exponential decay after an event at t0 (1 at t0, half-life hl). */
83
+ export const pulse = (t, t0, hl = 0.12) => (t < t0 ? 0 : 0.5 ** ((t - t0) / hl));
84
+
85
+ // ---- seeded randomness (MO-SH-01, MO-A-23) ----
86
+
87
+ /** FNV-1a 32-bit over the UTF-8 bytes of `text`; offset basis 0x811c9dc5, prime 0x01000193. */
88
+ export function fnv1a32(text) {
89
+ let hash = 0x811c9dc5;
90
+ for (const byte of new TextEncoder().encode(text)) {
91
+ hash ^= byte;
92
+ hash = Math.imul(hash, 0x01000193) >>> 0;
93
+ }
94
+ return hash >>> 0;
95
+ }
96
+
97
+ /** MO-SH-01: the one seed formula for every pass in every product. */
98
+ export const passSeed = (runSeed, sceneId, shotIndex, pass) => fnv1a32(`${runSeed}:${sceneId}:${shotIndex}:${pass}`);
99
+
100
+ /** mulberry32 PRNG in unsigned 32-bit arithmetic. Returns a function yielding [0, 1). */
101
+ export function mulberry32(seed) {
102
+ let a = seed >>> 0;
103
+ return () => {
104
+ a = (a + 0x6d2b79f5) >>> 0;
105
+ let t = a;
106
+ t = Math.imul(t ^ (t >>> 15), t | 1);
107
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
108
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
109
+ };
110
+ }
111
+
112
+ /** Stateless hash of numbers to [0, 1). */
113
+ export function hash(...xs) {
114
+ let h = 2166136261 >>> 0;
115
+ for (const x of xs) {
116
+ h ^= Math.floor(x * 1000003) | 0;
117
+ h = Math.imul(h, 16777619);
118
+ h ^= h >>> 13;
119
+ h = Math.imul(h, 0x5bd1e995);
120
+ h ^= h >>> 15;
121
+ }
122
+ return (h >>> 0) / 4294967296;
123
+ }
124
+
125
+ const quintic = (t) => t * t * t * (t * (t * 6 - 15) + 10);
126
+ export function noise1(x, seed = 0) {
127
+ const i = Math.floor(x);
128
+ return lerp(hash(i, seed) * 2 - 1, hash(i + 1, seed) * 2 - 1, quintic(x - i));
129
+ }
130
+
131
+ // ---- colour ----
132
+
133
+ export const srgbToLinear = (s) => (s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4);
134
+ export const linearToSrgb = (l) => (l <= 0.0031308 ? 12.92 * l : 1.055 * l ** (1 / 2.4) - 0.055);
135
+
136
+ export function hexToRgb(hex) {
137
+ const n = Number.parseInt(hex.replace("#", ""), 16);
138
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
139
+ }
140
+ export const hexToLinear = (hex) => hexToRgb(hex).map((v) => srgbToLinear(v / 255));
141
+
142
+ /** WCAG relative luminance of an sRGB hex colour. */
143
+ export function relativeLuminance(hex) {
144
+ const [r, g, b] = hexToLinear(hex);
145
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
146
+ }
147
+ export const contrastRatio = (la, lb) => (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
148
+
149
+ /** HSL hue (degrees) and saturation (0..1) of 0..255 sRGB values. */
150
+ export function hsl(r, g, b) {
151
+ const [rn, gn, bn] = [r / 255, g / 255, b / 255];
152
+ const max = Math.max(rn, gn, bn);
153
+ const min = Math.min(rn, gn, bn);
154
+ const l = (max + min) / 2;
155
+ const d = max - min;
156
+ if (d === 0) return { h: 0, s: 0, l };
157
+ const s = d / (1 - Math.abs(2 * l - 1));
158
+ let h;
159
+ if (max === rn) h = ((gn - bn) / d) % 6;
160
+ else if (max === gn) h = (bn - rn) / d + 2;
161
+ else h = (rn - gn) / d + 4;
162
+ return { h: mod(h * 60, 360), s, l };
163
+ }
164
+ export const hueDistance = (a, b) => {
165
+ const d = Math.abs(mod(a, 360) - mod(b, 360));
166
+ return Math.min(d, 360 - d);
167
+ };