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,334 @@
1
+ // agentvfx (Apache-2.0, see LICENSE). Hand-rolled validators for templates (fx.json) and FX scores. Every error
2
+ // names the JSON path that failed, so the model can fix the exact field.
3
+ import { PALETTE_NAMES } from "./palettes.mjs";
4
+ import { FAMILY_PATTERN } from "../web/text.mjs";
5
+
6
+ export const SPDX_ALLOWED = Object.freeze(["MIT", "BSD-2-Clause", "BSD-3-Clause", "Apache-2.0", "CC0-1.0"]);
7
+ export const INPUT_TYPES = Object.freeze(["float", "long", "bool", "color", "point2D", "image", "audio", "audioFFT", "event"]);
8
+ export const TEMPLATE_INPUT_KINDS = Object.freeze(["generative", "footage", "either"]);
9
+ export const FLASH_KINDS = Object.freeze(["none", "possible", "strobe"]);
10
+ export const SAFETY_MODES = Object.freeze(["stage", "broadcast-safe"]);
11
+ export const FPS_ALLOWED = Object.freeze([24, 25, 30, 50, 60]);
12
+ export const SCALAR_FEATURES = Object.freeze([
13
+ "rms", "peak", "rmsDb", "bass", "mid", "treb", "bass_att", "mid_att", "treb_att",
14
+ "flux", "onset", "beat", "beatPhase", "bpm", "corr", "width", "balance",
15
+ ]);
16
+ const BAND_FEATURE = /^bands\.(?:[0-9]|[12][0-9]|3[01])$/u;
17
+ const SCORE_KEYS = Object.freeze(["version", "template", "preset", "params", "stack", "input", "durationSec", "fps", "seed", "bindings", "hits", "cuts", "palette", "safety", "credit", "out", "labels", "textStyle"]);
18
+ export const TEXT_FONTS = Object.freeze(["display", "tall", "grotesk", "mono", "serif"]);
19
+ const TEXT_KEYS = Object.freeze(["key", "default", "x", "y", "size", "font", "weight", "style", "align", "tracking", "box", "reveal", "revealStart", "revealDur", "maxWidth", "lineHeight", "channel", "family", "set"]);
20
+ export const TEXT_STYLE_KEYS = Object.freeze(["font", "family", "size", "x", "y", "tracking", "align", "weight"]);
21
+
22
+ /** Text item keys declared by any template the score uses (labels and textStyle must name one). */
23
+ function scoreTextKeys(score, lib) {
24
+ const id = (l) => (typeof l === "string" ? l : l?.template);
25
+ const ids = [score.template, ...(score.stack ?? []).map(id), ...(score.cuts ?? []).flatMap((c) => (c.layers ?? []).map(id)), ...(score.hits ?? []).map((h) => h.template)];
26
+ return new Set(ids.flatMap((t) => lib.get(t)?.fx.X_AGENTVFX.text ?? []).map((t) => t.key));
27
+ }
28
+
29
+ /** X_AGENTVFX.text: engine-drawn text items (references/template-format.md "Text layer"). */
30
+ function checkTextSpec(list, err) {
31
+ if (!Array.isArray(list)) return err("X_AGENTVFX.text", "must be a list of text items");
32
+ const keys = new Set();
33
+ list.forEach((t, i) => {
34
+ const p = `X_AGENTVFX.text[${i}]`;
35
+ if (!isObj(t)) return err(p, "must be an object");
36
+ for (const k of Object.keys(t)) if (!TEXT_KEYS.includes(k)) err(`${p}.${k}`, `unknown key (allowed: ${TEXT_KEYS.join(", ")})`);
37
+ if (typeof t.key !== "string" || !/^[a-z][a-zA-Z0-9_]*$/u.test(t.key)) err(`${p}.key`, "required identifier");
38
+ else if (keys.has(t.key)) err(`${p}.key`, `duplicate key ${t.key}`);
39
+ else keys.add(t.key);
40
+ if (typeof t.default !== "string") err(`${p}.default`, "required string (\\n for new lines)");
41
+ for (const k of ["x", "y"]) if (!(isNum(t[k]) && t[k] >= -0.5 && t[k] <= 1.5)) err(`${p}.${k}`, "required 0..1 (fraction of width/height, top-left origin)");
42
+ if (!(isNum(t.size) && t.size > 0 && t.size <= 1)) err(`${p}.size`, "required 0..1 (fraction of frame height)");
43
+ if (t.font !== undefined && !TEXT_FONTS.includes(t.font)) err(`${p}.font`, `must be one of ${TEXT_FONTS.join(", ")}`);
44
+ if (t.style !== undefined && !["fill", "outline"].includes(t.style)) err(`${p}.style`, "fill or outline");
45
+ if (t.align !== undefined && !["left", "center", "right"].includes(t.align)) err(`${p}.align`, "left, center or right");
46
+ if (t.box !== undefined && !["none", "frame", "tab", "bar"].includes(t.box)) err(`${p}.box`, "none, frame, tab or bar");
47
+ if (t.family !== undefined && !(typeof t.family === "string" && FAMILY_PATTERN.test(t.family))) err(`${p}.family`, "an installed font family name (letters, digits, spaces, . _ -; up to 64 characters)");
48
+ if (t.channel !== undefined && !["r", "g", "b"].includes(t.channel)) err(`${p}.channel`, "r, g or b (the mask channel the glyph ink is drawn into)");
49
+ if (t.set !== undefined && !(Number.isInteger(t.set) && t.set >= 0 && t.set <= 31)) err(`${p}.set`, "integer 0..31 (the item is drawn only while the template input textSet equals it; default 0)");
50
+ if (t.reveal !== undefined && !["none", "type", "decode"].includes(t.reveal)) err(`${p}.reveal`, "none, type or decode");
51
+ for (const k of ["weight", "tracking", "revealStart", "revealDur", "maxWidth", "lineHeight"]) if (t[k] !== undefined && !isNum(t[k])) err(`${p}.${k}`, "must be a number");
52
+ });
53
+ }
54
+ const HEX = /^#[0-9a-f]{6}$/iu;
55
+ /** Names an INPUT may not take: GLSL ES 3.00 built-ins and keywords, and the prelude's own names. */
56
+ export const RESERVED_NAMES = Object.freeze(new Set([
57
+ "step", "mix", "clamp", "min", "max", "abs", "sign", "floor", "ceil", "fract", "mod", "modf", "round", "trunc", "pow", "exp", "exp2",
58
+ "log", "log2", "sqrt", "inversesqrt", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "radians", "degrees",
59
+ "length", "distance", "dot", "cross", "normalize", "reflect", "refract", "faceforward", "smoothstep", "texture", "textureSize",
60
+ "texelFetch", "textureLod", "dFdx", "dFdy", "fwidth", "isnan", "isinf", "any", "all", "not", "equal", "lessThan", "greaterThan",
61
+ "transpose", "inverse", "determinant", "outerProduct", "matrixCompMult", "packHalf2x16", "unpackHalf2x16",
62
+ "float", "int", "uint", "bool", "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "mat2", "mat3", "mat4", "sampler2D",
63
+ "in", "out", "inout", "uniform", "const", "if", "else", "for", "while", "do", "break", "continue", "return", "discard", "true", "false",
64
+ "struct", "void", "main", "precision", "highp", "mediump", "lowp", "layout", "flat", "smooth", "centroid", "invariant", "switch", "case", "default",
65
+ "TIME", "TIMEDELTA", "FRAMEINDEX", "PASSINDEX", "RENDERSIZE", "SEED", "HAS_INPUT", "PALETTE", "PALETTE_COUNT", "vUv", "fragColor",
66
+ "inputImage", "audio", "audioFFT", "AUDIO_RMS", "AUDIO_PEAK", "AUDIO_BASS", "AUDIO_MID", "AUDIO_TREB", "AUDIO_BASS_ATT", "AUDIO_MID_ATT",
67
+ "AUDIO_TREB_ATT", "AUDIO_FLUX", "AUDIO_ONSET", "AUDIO_BEAT", "AUDIO_BEATPHASE", "AUDIO_BPM", "AUDIO_BANDS",
68
+ "hash12", "hash22", "vnoise", "fbm", "luma", "rgb2hsv", "hsv2rgb", "cosPalette", "paletteRamp", "paletteCycle", "srgbToLinear",
69
+ "linearToSrgb", "rot", "isf_FragNormCoord", "TEXTMASK", "HAS_TEXT",
70
+ ]));
71
+
72
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v);
73
+ const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
74
+ const isInt = (v) => Number.isInteger(v);
75
+
76
+ /** Check one parameter value against its ISF input declaration. Returns an error message or null. */
77
+ export function checkParamValue(input, value) {
78
+ switch (input.TYPE) {
79
+ case "float":
80
+ if (!isNum(value)) return "must be a number";
81
+ break;
82
+ case "long":
83
+ if (!isInt(value)) return "must be an integer";
84
+ if (Array.isArray(input.VALUES) && !input.VALUES.includes(value)) return `must be one of ${input.VALUES.join(", ")}`;
85
+ break;
86
+ case "bool":
87
+ if (typeof value !== "boolean") return "must be true or false";
88
+ return null;
89
+ case "color":
90
+ if (!Array.isArray(value) || value.length !== 4 || !value.every((c) => isNum(c) && c >= 0 && c <= 1)) return "must be [r, g, b, a] with each 0..1";
91
+ return null;
92
+ case "point2D":
93
+ if (!Array.isArray(value) || value.length !== 2 || !value.every(isNum)) return "must be [x, y]";
94
+ return null;
95
+ default:
96
+ return `${input.TYPE} inputs cannot be set by a parameter`;
97
+ }
98
+ if (isNum(input.MIN) && value < input.MIN) return `must be >= ${input.MIN}`;
99
+ if (isNum(input.MAX) && value > input.MAX) return `must be <= ${input.MAX}`;
100
+ return null;
101
+ }
102
+
103
+ /** Validate one template's fx.json. `folderId` is the expected id (the folder name). */
104
+ export function validateTemplate(fx, folderId = null) {
105
+ const errors = [];
106
+ const err = (path, message) => errors.push({ path, message });
107
+ if (!isObj(fx)) return { ok: false, errors: [{ path: "", message: "fx.json must be an object" }] };
108
+ if (typeof fx.DESCRIPTION !== "string" || !fx.DESCRIPTION) err("DESCRIPTION", "required string");
109
+ if (fx.CATEGORIES !== undefined && !(Array.isArray(fx.CATEGORIES) && fx.CATEGORIES.every((c) => typeof c === "string"))) err("CATEGORIES", "must be a list of strings");
110
+ const inputs = fx.INPUTS ?? [];
111
+ if (!Array.isArray(inputs)) err("INPUTS", "must be a list");
112
+ const byName = new Map();
113
+ (Array.isArray(inputs) ? inputs : []).forEach((input, i) => {
114
+ const p = `INPUTS[${i}]`;
115
+ if (!isObj(input) || typeof input.NAME !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(input.NAME)) return err(`${p}.NAME`, "required GLSL identifier");
116
+ if (byName.has(input.NAME)) err(`${p}.NAME`, `duplicate input ${input.NAME}`);
117
+ if (RESERVED_NAMES.has(input.NAME) || input.NAME.startsWith("gl_")) err(`${p}.NAME`, `${input.NAME} is a GLSL built-in or prelude name; pick another`);
118
+ byName.set(input.NAME, input);
119
+ if (!INPUT_TYPES.includes(input.TYPE)) return err(`${p}.TYPE`, `must be one of ${INPUT_TYPES.join(", ")}`);
120
+ if ((input.TYPE === "float" || input.TYPE === "long") && (!isNum(input.MIN) || !isNum(input.MAX) || input.MIN > input.MAX)) err(p, "float/long inputs need numeric MIN <= MAX");
121
+ if (input.DEFAULT !== undefined) {
122
+ const m = checkParamValue(input, input.DEFAULT);
123
+ if (m) err(`${p}.DEFAULT`, m);
124
+ }
125
+ });
126
+ if (fx.PASSES !== undefined) {
127
+ if (!Array.isArray(fx.PASSES) || fx.PASSES.length === 0) err("PASSES", "must be a non-empty list");
128
+ else fx.PASSES.forEach((pass, i) => {
129
+ if (!isObj(pass)) return err(`PASSES[${i}]`, "must be an object");
130
+ if (pass.TARGET !== undefined && (typeof pass.TARGET !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(pass.TARGET) || RESERVED_NAMES.has(pass.TARGET))) err(`PASSES[${i}].TARGET`, "must be a GLSL identifier that is not a built-in or prelude name");
131
+ if (pass.PERSISTENT !== undefined && typeof pass.PERSISTENT !== "boolean") err(`PASSES[${i}].PERSISTENT`, "must be boolean");
132
+ if (pass.FLOAT !== undefined && typeof pass.FLOAT !== "boolean") err(`PASSES[${i}].FLOAT`, "must be boolean");
133
+ });
134
+ }
135
+ const x = fx.X_AGENTVFX;
136
+ if (!isObj(x)) {
137
+ err("X_AGENTVFX", "required object");
138
+ return { ok: false, errors };
139
+ }
140
+ if (typeof x.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/u.test(x.id)) err("X_AGENTVFX.id", "required kebab-case id");
141
+ else if (folderId && x.id !== folderId && !folderId.endsWith(`/${x.id}`)) err("X_AGENTVFX.id", `must match the folder (${folderId})`);
142
+ if (typeof x.family !== "string" || !x.family) err("X_AGENTVFX.family", "required string");
143
+ if (!TEMPLATE_INPUT_KINDS.includes(x.input)) err("X_AGENTVFX.input", `must be one of ${TEMPLATE_INPUT_KINDS.join(", ")}`);
144
+ if (!SPDX_ALLOWED.includes(x.spdx)) err("X_AGENTVFX.spdx", `must be one of ${SPDX_ALLOWED.join(", ")}`);
145
+ if (!FLASH_KINDS.includes(x.flash)) err("X_AGENTVFX.flash", `must be one of ${FLASH_KINDS.join(", ")}`);
146
+ if (x.fps !== undefined && !FPS_ALLOWED.includes(x.fps)) err("X_AGENTVFX.fps", `must be one of ${FPS_ALLOWED.join(", ")}`);
147
+ if (x.palette !== undefined && typeof x.palette !== "boolean") err("X_AGENTVFX.palette", "must be boolean");
148
+ if (x.credits !== undefined && !(Array.isArray(x.credits) && x.credits.every((c) => typeof c === "string"))) err("X_AGENTVFX.credits", "must be a list of strings");
149
+ if (x.audioBind !== undefined && !isObj(x.audioBind)) err("X_AGENTVFX.audioBind", "must be an object of param -> feature hint");
150
+ if (x.safeSubstitute !== undefined && !isObj(x.safeSubstitute)) err("X_AGENTVFX.safeSubstitute", "must be an object { template?, params? }");
151
+ if (x.text !== undefined) checkTextSpec(x.text, err);
152
+ if (!isObj(x.presets) || Object.keys(x.presets).length < 3) err("X_AGENTVFX.presets", "needs at least 3 named presets");
153
+ else for (const [name, values] of Object.entries(x.presets)) {
154
+ if (!isObj(values)) {
155
+ err(`X_AGENTVFX.presets.${name}`, "must be an object of parameter values");
156
+ continue;
157
+ }
158
+ for (const [param, value] of Object.entries(values)) {
159
+ const input = byName.get(param);
160
+ if (!input) err(`X_AGENTVFX.presets.${name}.${param}`, "is not a declared input");
161
+ else {
162
+ const m = checkParamValue(input, value);
163
+ if (m) err(`X_AGENTVFX.presets.${name}.${param}`, m);
164
+ }
165
+ }
166
+ }
167
+ return { ok: errors.length === 0, errors };
168
+ }
169
+
170
+ function checkLayer(layer, path, lib, err) {
171
+ const ref = typeof layer === "string" ? { template: layer } : layer;
172
+ if (!isObj(ref) || typeof ref.template !== "string") return err(path, "must be a template id or { template, preset?, params? }");
173
+ const tpl = lib.get(ref.template);
174
+ if (!tpl) return err(`${path}.template`, `unknown template ${ref.template}`);
175
+ checkPresetParams(tpl, ref, path, err);
176
+ }
177
+
178
+ function checkPresetParams(tpl, ref, base, err) {
179
+ const pre = base ? `${base}.` : "";
180
+ if (ref.preset !== undefined && !(typeof ref.preset === "string" && tpl.fx.X_AGENTVFX.presets[ref.preset])) {
181
+ err(`${pre}preset`, `unknown preset for ${tpl.id}: ${ref.preset} (have ${Object.keys(tpl.fx.X_AGENTVFX.presets).join(", ")})`);
182
+ }
183
+ if (ref.params !== undefined) {
184
+ if (!isObj(ref.params)) return err(`${pre}params`, "must be an object");
185
+ const inputs = new Map((tpl.fx.INPUTS ?? []).map((i) => [i.NAME, i]));
186
+ for (const [name, value] of Object.entries(ref.params)) {
187
+ const input = inputs.get(name);
188
+ if (!input) err(`${pre}params.${name}`, `not an input of ${tpl.id}`);
189
+ else {
190
+ const m = checkParamValue(input, value);
191
+ if (m) err(`${pre}params.${name}`, m);
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ function checkWhen(obj, path, err, keys) {
198
+ const present = keys.filter((k) => obj[k] !== undefined);
199
+ if (present.length !== 1) return err(path, `needs exactly one of ${keys.join(" | ")}`);
200
+ const k = present[0];
201
+ if (k === "t" ? !(isNum(obj.t) && obj.t >= 0) : !(isInt(obj[k]) && obj[k] >= 0)) err(`${path}.${k}`, k === "t" ? "must be seconds >= 0" : "must be an index >= 0");
202
+ }
203
+
204
+ /**
205
+ * Validate an FX score against the library. Returns { ok, errors: [{path, message}], value } where
206
+ * value is the score with defaults resolved (fps, safety).
207
+ */
208
+ export function validateScore(score, lib) {
209
+ const errors = [];
210
+ const err = (path, message) => errors.push({ path, message });
211
+ if (!isObj(score)) return { ok: false, errors: [{ path: "", message: "the score must be a JSON object" }], value: null };
212
+ for (const key of Object.keys(score)) if (!SCORE_KEYS.includes(key)) err(key, `unknown key (allowed: ${SCORE_KEYS.join(", ")})`);
213
+ if (score.version !== 1) err("version", "must be 1");
214
+ const tpl = typeof score.template === "string" ? lib.get(score.template) : null;
215
+ if (!tpl) err("template", `unknown template ${JSON.stringify(score.template)} (run vfx.mjs list)`);
216
+ else checkPresetParams(tpl, score, "", err);
217
+ if (score.stack !== undefined) {
218
+ if (!Array.isArray(score.stack) || score.stack.length > 2) err("stack", "must be a list of at most 2 layers (3 with the base template)");
219
+ else score.stack.forEach((l, i) => checkLayer(l, `stack[${i}]`, lib, err));
220
+ }
221
+ if (!(isNum(score.durationSec) && score.durationSec >= 3 && score.durationSec <= 5)) err("durationSec", "must be a number from 3 to 5");
222
+ const fps = score.fps ?? tpl?.fx.X_AGENTVFX.fps ?? 30;
223
+ if (!FPS_ALLOWED.includes(fps)) err("fps", `must be one of ${FPS_ALLOWED.join(", ")}`);
224
+ if (!(isInt(score.seed) && score.seed >= 0 && score.seed <= 0xffffffff)) err("seed", "must be an integer 0..2^32-1");
225
+ if (score.input !== undefined) {
226
+ if (!isObj(score.input)) err("input", "must be { video?, audio?, startSec? }");
227
+ else {
228
+ for (const k of Object.keys(score.input)) if (!["video", "audio", "startSec"].includes(k)) err(`input.${k}`, "unknown key");
229
+ for (const k of ["video", "audio"]) if (score.input[k] !== undefined && (typeof score.input[k] !== "string" || !score.input[k])) err(`input.${k}`, "must be a file path");
230
+ if (score.input.startSec !== undefined && !(isNum(score.input.startSec) && score.input.startSec >= 0)) err("input.startSec", "must be seconds >= 0");
231
+ }
232
+ }
233
+ if (tpl && tpl.fx.X_AGENTVFX.input === "footage" && !score.input?.video) err("input.video", `${tpl.id} needs a video clip`);
234
+ if (score.bindings !== undefined) {
235
+ if (!Array.isArray(score.bindings)) err("bindings", "must be a list");
236
+ else score.bindings.forEach((b, i) => {
237
+ const p = `bindings[${i}]`;
238
+ if (!isObj(b)) return err(p, "must be an object");
239
+ const inputs = new Map((tpl?.fx.INPUTS ?? []).map((x) => [x.NAME, x]));
240
+ const input = inputs.get(b.param);
241
+ if (!input) err(`${p}.param`, `not an input of ${score.template}`);
242
+ else if (!["float", "long", "bool"].includes(input.TYPE)) err(`${p}.param`, "only float, long and bool inputs can be bound");
243
+ if (!(SCALAR_FEATURES.includes(b.feature) || BAND_FEATURE.test(b.feature ?? ""))) err(`${p}.feature`, `must be one of ${SCALAR_FEATURES.join(", ")} or bands.0..bands.31`);
244
+ for (const k of ["gain", "offset"]) if (!isNum(b[k])) err(`${p}.${k}`, "required number");
245
+ for (const k of ["attackMs", "releaseMs"]) if (b[k] !== undefined && !(isNum(b[k]) && b[k] >= 0)) err(`${p}.${k}`, "must be ms >= 0");
246
+ });
247
+ if (score.bindings.length && !score.input?.audio && !(tpl && score.input?.video)) err("input.audio", "bindings need an audio source (input.audio, or a video with sound)");
248
+ }
249
+ if (score.hits !== undefined) {
250
+ if (!Array.isArray(score.hits)) err("hits", "must be a list");
251
+ else score.hits.forEach((h, i) => {
252
+ const p = `hits[${i}]`;
253
+ if (!isObj(h)) return err(p, "must be an object");
254
+ checkWhen(h, p, err, ["t", "onset", "beat"]);
255
+ checkLayer({ template: h.template, preset: h.preset, params: h.params }, p, lib, err);
256
+ if (!(isInt(h.durationFrames) && h.durationFrames >= 1)) err(`${p}.durationFrames`, "must be an integer >= 1");
257
+ });
258
+ }
259
+ if (score.cuts !== undefined) {
260
+ if (!Array.isArray(score.cuts)) err("cuts", "must be a list");
261
+ else score.cuts.forEach((c, i) => {
262
+ const p = `cuts[${i}]`;
263
+ if (!isObj(c)) return err(p, "must be an object");
264
+ checkWhen(c, p, err, ["t", "beat"]);
265
+ if (!Array.isArray(c.layers) || c.layers.length < 1 || c.layers.length > 3) err(`${p}.layers`, "must be a list of 1 to 3 layers");
266
+ else c.layers.forEach((l, j) => checkLayer(l, `${p}.layers[${j}]`, lib, err));
267
+ });
268
+ }
269
+ if (score.palette !== undefined) {
270
+ const ok = typeof score.palette === "string" ? PALETTE_NAMES.includes(score.palette)
271
+ : Array.isArray(score.palette) && score.palette.length >= 2 && score.palette.length <= 8 && score.palette.every((h) => HEX.test(h));
272
+ if (!ok) err("palette", `must be one of ${PALETTE_NAMES.join(", ")} or a list of 2-8 #rrggbb colours`);
273
+ }
274
+ const safety = { mode: "stage", safeVariant: false };
275
+ if (score.safety !== undefined) {
276
+ if (!isObj(score.safety)) err("safety", "must be { mode?, safeVariant? }");
277
+ else {
278
+ for (const k of Object.keys(score.safety)) if (!["mode", "safeVariant"].includes(k)) err(`safety.${k}`, "unknown key");
279
+ if (score.safety.mode !== undefined) {
280
+ if (!SAFETY_MODES.includes(score.safety.mode)) err("safety.mode", `must be one of ${SAFETY_MODES.join(", ")}`);
281
+ else safety.mode = score.safety.mode;
282
+ }
283
+ if (score.safety.safeVariant !== undefined) {
284
+ if (typeof score.safety.safeVariant !== "boolean") err("safety.safeVariant", "must be boolean");
285
+ else safety.safeVariant = score.safety.safeVariant;
286
+ }
287
+ }
288
+ }
289
+ // On-screen credit: a small corner mark on by default; the score may opt out.
290
+ const credit = { overlay: true };
291
+ if (score.credit !== undefined) {
292
+ if (!isObj(score.credit)) err("credit", "must be { overlay? }");
293
+ else {
294
+ for (const k of Object.keys(score.credit)) if (k !== "overlay") err(`credit.${k}`, "unknown key");
295
+ if (score.credit.overlay !== undefined) {
296
+ if (typeof score.credit.overlay !== "boolean") err("credit.overlay", "must be boolean");
297
+ else credit.overlay = score.credit.overlay;
298
+ }
299
+ }
300
+ }
301
+ if (score.out !== undefined && (typeof score.out !== "string" || !score.out)) err("out", "must be a non-empty string");
302
+ if (score.labels !== undefined) {
303
+ if (!isObj(score.labels)) err("labels", "must be an object of text key -> string");
304
+ else {
305
+ const known = scoreTextKeys(score, lib);
306
+ for (const [k, v] of Object.entries(score.labels)) {
307
+ if (typeof v !== "string") err(`labels.${k}`, "must be a string");
308
+ else if (!known.has(k)) err(`labels.${k}`, `no template in this score declares a text item "${k}" (have ${[...known].join(", ") || "none"})`);
309
+ }
310
+ }
311
+ }
312
+ if (score.textStyle !== undefined) {
313
+ if (!isObj(score.textStyle)) err("textStyle", "must be an object of text key -> { font?, size?, x?, y?, tracking?, align? }");
314
+ else {
315
+ const known = scoreTextKeys(score, lib);
316
+ for (const [k, v] of Object.entries(score.textStyle)) {
317
+ if (!known.has(k)) err(`textStyle.${k}`, `no template in this score declares a text item "${k}" (have ${[...known].join(", ") || "none"})`);
318
+ else if (!isObj(v)) err(`textStyle.${k}`, "must be an object");
319
+ else {
320
+ for (const p of Object.keys(v)) if (!TEXT_STYLE_KEYS.includes(p)) err(`textStyle.${k}.${p}`, `unknown key (allowed: ${TEXT_STYLE_KEYS.join(", ")})`);
321
+ if (v.font !== undefined && !TEXT_FONTS.includes(v.font)) err(`textStyle.${k}.font`, `must be one of ${TEXT_FONTS.join(", ")}`);
322
+ if (v.family !== undefined && !(typeof v.family === "string" && FAMILY_PATTERN.test(v.family))) err(`textStyle.${k}.family`, "an installed font family name (letters, digits, spaces, . _ -; up to 64 characters)");
323
+ if (v.size !== undefined && !(isNum(v.size) && v.size > 0 && v.size <= 1)) err(`textStyle.${k}.size`, "must be 0..1 (fraction of frame height)");
324
+ for (const p of ["x", "y"]) if (v[p] !== undefined && !(isNum(v[p]) && v[p] >= -0.5 && v[p] <= 1.5)) err(`textStyle.${k}.${p}`, "must be a fraction of the frame, -0.5..1.5");
325
+ if (v.weight !== undefined && !(isNum(v.weight) && v.weight >= 100 && v.weight <= 900)) err(`textStyle.${k}.weight`, "must be 100..900");
326
+ if (v.tracking !== undefined && !(isNum(v.tracking) && v.tracking >= -0.2 && v.tracking <= 2)) err(`textStyle.${k}.tracking`, "must be -0.2..2 (em)");
327
+ if (v.align !== undefined && !["left", "center", "right"].includes(v.align)) err(`textStyle.${k}.align`, "left, center or right");
328
+ }
329
+ }
330
+ }
331
+ }
332
+ const value = errors.length ? null : { ...score, fps, safety, credit };
333
+ return { ok: errors.length === 0, errors, value };
334
+ }
@@ -0,0 +1,172 @@
1
+ // Per-frame analysis of the exact bytes that leave the renderer. The page writes the glyph-coverage
2
+ // mask into the alpha channel; this reads it (ink pixel count, optional 8-bit mask), then forces
3
+ // alpha to 255 so the bytes hashed (rgbaSha256, MO-C-09), audited (MO-C-03) and encoded are one
4
+ // and the same buffer. It also computes the 99.5th-percentile luminance for the empty-frame rule
5
+ // (MO-D-03) and the flash-audit cell grid.
6
+ import { createHash } from "node:crypto";
7
+ import { GATE } from "../core/constants.mjs";
8
+ import { LINEAR_LUT, gridFor } from "../core/flash.mjs";
9
+
10
+ const HIST = 2048;
11
+
12
+ /** Analyse and normalise one RGBA frame in place. Returns ink count, mask (optional), cells, p99.5 L. */
13
+ export function analyzeFrame(bytes, width, height, { keepMask = false } = {}) {
14
+ const { gw: GW, gh: GH } = gridFor(width, height);
15
+ const cells = GW * GH;
16
+ const R = new Float32Array(cells);
17
+ const G = new Float32Array(cells);
18
+ const B = new Float32Array(cells);
19
+ const count = new Float32Array(cells);
20
+ const hist = new Uint32Array(HIST);
21
+ const mask = keepMask ? Buffer.alloc(width * height) : null;
22
+ let inkPx = 0;
23
+ for (let y = 0; y < height; y++) {
24
+ const cy = Math.min(GH - 1, Math.floor((y * GH) / height)) * GW;
25
+ for (let x = 0; x < width; x++) {
26
+ const o = (y * width + x) * 4;
27
+ const a = bytes[o + 3];
28
+ if (a >= 128) inkPx += 1;
29
+ if (mask) mask[y * width + x] = a;
30
+ bytes[o + 3] = 255;
31
+ const r = LINEAR_LUT[bytes[o]];
32
+ const g = LINEAR_LUT[bytes[o + 1]];
33
+ const b = LINEAR_LUT[bytes[o + 2]];
34
+ const c = cy + Math.min(GW - 1, Math.floor((x * GW) / width));
35
+ R[c] += r;
36
+ G[c] += g;
37
+ B[c] += b;
38
+ count[c] += 1;
39
+ const l = 0.2126 * r + 0.7152 * g + 0.0722 * b;
40
+ hist[Math.min(HIST - 1, Math.floor(l * HIST))] += 1;
41
+ }
42
+ }
43
+ for (let i = 0; i < cells; i++) {
44
+ const n = count[i] || 1;
45
+ R[i] /= n;
46
+ G[i] /= n;
47
+ B[i] /= n;
48
+ }
49
+ const target = GATE.nearBlackPercentile * width * height;
50
+ const low = (1 - GATE.nearBlackPercentile) * width * height;
51
+ let seen = 0;
52
+ let p995 = 1;
53
+ let p005 = null;
54
+ for (let i = 0; i < HIST; i++) {
55
+ seen += hist[i];
56
+ if (p005 === null && seen >= low) p005 = i / HIST;
57
+ if (seen >= target) {
58
+ p995 = (i + 1) / HIST;
59
+ break;
60
+ }
61
+ }
62
+ const rgbaSha256 = createHash("sha256").update(bytes).digest("hex");
63
+ // The 0.5 % and 99.5 % luminance: their spread is what the empty-frame rules read (MO-D-03, FLAT-FRAME).
64
+ return { inkPx, mask, cells: { R, G, B }, luminanceP995: +p995.toFixed(5), luminanceP005: +(p005 ?? 0).toFixed(5), rgbaSha256 };
65
+ }
66
+
67
+ /** Area-average downscale of RGBA8 (display-referred, like ffmpeg's area filter). */
68
+ export function areaDownscale(src, sw, sh, dw, dh) {
69
+ const tmp = new Float32Array(dw * sh * 4);
70
+ const sx = sw / dw;
71
+ for (let y = 0; y < sh; y++) {
72
+ for (let x = 0; x < dw; x++) {
73
+ const x0 = x * sx;
74
+ const x1 = x0 + sx;
75
+ const acc = [0, 0, 0, 0];
76
+ for (let xi = Math.floor(x0); xi < Math.ceil(x1); xi++) {
77
+ const w = Math.min(x1, xi + 1) - Math.max(x0, xi);
78
+ const o = (y * sw + xi) * 4;
79
+ acc[0] += src[o] * w; acc[1] += src[o + 1] * w; acc[2] += src[o + 2] * w; acc[3] += src[o + 3] * w;
80
+ }
81
+ const t = (y * dw + x) * 4;
82
+ tmp[t] = acc[0] / sx; tmp[t + 1] = acc[1] / sx; tmp[t + 2] = acc[2] / sx; tmp[t + 3] = acc[3] / sx;
83
+ }
84
+ }
85
+ const out = Buffer.alloc(dw * dh * 4);
86
+ const sy = sh / dh;
87
+ for (let y = 0; y < dh; y++) {
88
+ const y0 = y * sy;
89
+ const y1 = y0 + sy;
90
+ for (let x = 0; x < dw; x++) {
91
+ const acc = [0, 0, 0, 0];
92
+ for (let yi = Math.floor(y0); yi < Math.ceil(y1); yi++) {
93
+ const w = Math.min(y1, yi + 1) - Math.max(y0, yi);
94
+ const o = (yi * dw + x) * 4;
95
+ acc[0] += tmp[o] * w; acc[1] += tmp[o + 1] * w; acc[2] += tmp[o + 2] * w; acc[3] += tmp[o + 3] * w;
96
+ }
97
+ const t = (y * dw + x) * 4;
98
+ out[t] = Math.round(acc[0] / sy); out[t + 1] = Math.round(acc[1] / sy); out[t + 2] = Math.round(acc[2] / sy); out[t + 3] = 255;
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
104
+ /** Nearest-neighbour resample of an 8-bit mask. */
105
+ export function resizeMask(mask, sw, sh, dw, dh) {
106
+ const out = Buffer.alloc(dw * dh);
107
+ for (let y = 0; y < dh; y++) {
108
+ const yy = Math.min(sh - 1, Math.floor(((y + 0.5) * sh) / dh));
109
+ for (let x = 0; x < dw; x++) out[y * dw + x] = mask[yy * sw + Math.min(sw - 1, Math.floor(((x + 0.5) * sw) / dw))];
110
+ }
111
+ return out;
112
+ }
113
+
114
+ // RICH-FILL: the frame cut into 4 x 3 regions. A dense reference frame (a full dashboard) measured
115
+ // 0.57 or more in every region; round-3 films with an empty right half measured 0.00-0.10 there.
116
+ export const FILL_GRID = Object.freeze({ cols: 4, rows: 3 });
117
+
118
+ /** The busyShare of each region of the FILL_GRID, row by row. */
119
+ export function regionFill(rgba, width, height) {
120
+ const out = [];
121
+ const rw = Math.floor(width / FILL_GRID.cols);
122
+ const rh = Math.floor(height / FILL_GRID.rows);
123
+ for (let r = 0; r < FILL_GRID.rows; r++) {
124
+ for (let c = 0; c < FILL_GRID.cols; c++) {
125
+ const sub = new Uint8Array(rw * rh * 4);
126
+ for (let y = 0; y < rh; y++) {
127
+ const from = ((r * rh + y) * width + c * rw) * 4;
128
+ sub.set(rgba.subarray(from, from + rw * 4), y * rw * 4);
129
+ }
130
+ out.push(busyShare(sub, rw, rh));
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+
136
+ /**
137
+ * Share of 48 px cells that carry something (DEAD-AIR): a cell is busy when its luma varies (sd > 6)
138
+ * or its mean sits more than 16 levels off the frame's ground, the most common luma smoothed over
139
+ * +-3 levels. Sampled on a 3 px lattice; an opaque RGBA frame in, a 0..1 share out.
140
+ */
141
+ export function busyShare(rgba, width, height, { cell = 48, step = 3 } = {}) {
142
+ const hist = new Uint32Array(256);
143
+ for (let y = 0; y < height; y += step) for (let x = 0; x < width; x += step) hist[luma8(rgba, (y * width + x) * 4)] += 1;
144
+ let ground = 0;
145
+ let best = -1;
146
+ for (let i = 0; i < 256; i++) {
147
+ let s = 0;
148
+ for (let k = Math.max(0, i - 3); k <= Math.min(255, i + 3); k++) s += hist[k];
149
+ if (s > best) [best, ground] = [s, i];
150
+ }
151
+ let busy = 0;
152
+ let cells = 0;
153
+ for (let cy = 0; cy + cell <= height; cy += cell) {
154
+ for (let cx = 0; cx + cell <= width; cx += cell) {
155
+ let s = 0;
156
+ let s2 = 0;
157
+ let n = 0;
158
+ for (let y = cy; y < cy + cell; y += step) for (let x = cx; x < cx + cell; x += step) {
159
+ const l = luma8(rgba, (y * width + x) * 4);
160
+ s += l;
161
+ s2 += l * l;
162
+ n += 1;
163
+ }
164
+ const mean = s / n;
165
+ if (Math.sqrt(Math.max(0, s2 / n - mean * mean)) > 6 || Math.abs(mean - ground) > 16) busy += 1;
166
+ cells += 1;
167
+ }
168
+ }
169
+ return cells ? busy / cells : 0;
170
+ }
171
+
172
+ const luma8 = (p, o) => Math.round(0.299 * p[o] + 0.587 * p[o + 1] + 0.114 * p[o + 2]);