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,48 @@
1
+ // waveform-line pass 0: persistent `wstate`.
2
+ // texel (0, 0) : clock (hit count, last hit frame, kick count, last kick frame)
3
+ // row 1, column x : the smoothed waveform value for screen column x (the live line)
4
+ // rows 2 .. 25 : a shift register, row 1 + d holds the line as it was d frames ago
5
+ // The live line is the 512-sample trigger-aligned waveform (audio texture row 0) low-passed with a
6
+ // 65-tap triangular window of +-smoothWin samples, compressed with tanh(gain * w) and added to a slow
7
+ // travelling sine pair scaled by the RMS envelope, so it keeps flowing between frames instead of
8
+ // showing a raw 10 ms slice. The history rows give delayed copies for the multi-layer look.
9
+ // clock texel: (hit count, last hit frame, kick count, last kick frame). A hit is an onset whose spectral
10
+ // flux reaches hitFlux; a kick is an onset whose bass ratio reaches kickBass.
11
+ vec4 clockStep(vec4 ev) {
12
+ if (AUDIO_ONSET > 0.5 && AUDIO_FLUX >= hitFlux) { ev.x += 1.0; ev.y = float(FRAMEINDEX); }
13
+ if (AUDIO_ONSET > 0.5 && AUDIO_BASS >= kickBass) { ev.z += 1.0; ev.w = float(FRAMEINDEX); }
14
+ return ev;
15
+ }
16
+ float liveLine(float u) {
17
+ float w = 0.0;
18
+ float wsum = 0.0;
19
+ for (int k = -32; k <= 32; k++) {
20
+ float wt = 33.0 - abs(float(k));
21
+ w += wt * (texture(audio, vec2(clamp(u + float(k) * smoothWin / 32.0 / 512.0, 0.0, 1.0), 0.25)).r * 2.0 - 1.0);
22
+ wsum += wt;
23
+ }
24
+ float v = tanh(gain * w / wsum);
25
+ float env = clamp(AUDIO_RMS * 2.5, 0.05, 1.0);
26
+ float s1 = sin(6.2832 * (u * 2.0 - TIME * 0.9));
27
+ float s2 = sin(6.2832 * (u * 3.3 + TIME * 0.6 + 0.3));
28
+ return v + flow * env * 0.5 * (s1 + 0.6 * s2);
29
+ }
30
+
31
+ void main() {
32
+ ivec2 px = ivec2(gl_FragCoord.xy);
33
+ vec4 prev = texelFetch(wstate, px, 0);
34
+ if (px.y == 0) {
35
+ fragColor = px.x == 0 ? clockStep(prev) : vec4(0.0);
36
+ return;
37
+ }
38
+ if (px.y == 1) {
39
+ float u = (float(px.x) + 0.5) / RENDERSIZE.x;
40
+ fragColor = vec4(liveLine(clamp((u - 0.08) / 0.84, 0.0, 1.0)), 0.0, 0.0, 1.0);
41
+ return;
42
+ }
43
+ if (px.y <= 25) {
44
+ fragColor = texelFetch(wstate, ivec2(px.x, px.y - 1), 0);
45
+ return;
46
+ }
47
+ fragColor = vec4(0.0);
48
+ }
@@ -0,0 +1,86 @@
1
+ // waveform-line pass 1: draw the 512-sample waveform (audio texture row 0, trigger aligned) into the
2
+ // persistent `trace` buffer. trace = max(previous * echo, this frame's line): the line leaves a fading
3
+ // echo of itself. rgb = colour * coverage, a = coverage. Display: 0 digital (LED columns filled from
4
+ // the centre line), 1 lines (thick polyline, exact distance to the 14 nearest segments), 2 dots.
5
+ float waveAt(float x, float x0, float span) {
6
+ float w = 0.0;
7
+ for (int k = -2; k <= 2; k++) {
8
+ w += texture(audio, vec2(clamp((x - x0) / span + float(k) * smoothWin / 512.0, 0.0, 1.0), 0.25)).r;
9
+ }
10
+ return amplitude * tanh(gain * (w * 0.4 - 1.0));
11
+ }
12
+
13
+ float segDist(vec2 p, vec2 a, vec2 b) {
14
+ vec2 pa = p - a;
15
+ vec2 ba = b - a;
16
+ return length(pa - ba * clamp(dot(pa, ba) / max(dot(ba, ba), 1.0e-6), 0.0, 1.0));
17
+ }
18
+
19
+ vec3 shade(float t, float u) {
20
+ vec3 c = colorMode == 1 ? paletteCycle(u + paletteShift + TIME * 0.05) : (colorMode == 2 ? paletteRamp(t) : mix(insideColor.rgb, outsideColor.rgb, t));
21
+ if (hueInterpolation != 0.0) {
22
+ vec3 h = rgb2hsv(c);
23
+ h.x = fract(h.x + hueInterpolation * u);
24
+ c = hsv2rgb(h);
25
+ }
26
+ return c;
27
+ }
28
+
29
+ void main() {
30
+ vec2 res = RENDERSIZE;
31
+ vec2 px = vUv * res;
32
+ float unit = res.y / 1080.0;
33
+ vec4 ev = texelFetch(clock, ivec2(0, 0), 0);
34
+ float since = ev.x > 0.5 ? (float(FRAMEINDEX) - ev.y) * TIMEDELTA : 99.0;
35
+ float hit = step(0.5, exp(-since / 0.1)) * hitBoost;
36
+ float x0 = res.x * 0.03;
37
+ float span = res.x * 0.94;
38
+ float mid = res.y * 0.5;
39
+ float th = max(thickness * unit * (1.0 + hit), 1.0);
40
+ float cov = 0.0;
41
+ float amp = 0.0;
42
+ float u = clamp((px.x - x0) / span, 0.0, 1.0);
43
+ for (int side = 0; side < 2; side++) {
44
+ float sgn = side == 0 ? 1.0 : -1.0;
45
+ if (side == 1 && !mirror) break;
46
+ if (displayMode == 1) {
47
+ float step1 = max(span / 512.0, 1.0);
48
+ float dmin = 1.0e9;
49
+ vec2 prevPt = vec2(px.x - 7.0 * step1, mid + sgn * waveAt(px.x - 7.0 * step1, x0, span) * res.y);
50
+ for (int k = -6; k <= 7; k++) {
51
+ float xk = px.x + float(k) * step1;
52
+ vec2 pt = vec2(xk, mid + sgn * waveAt(xk, x0, span) * res.y);
53
+ dmin = min(dmin, segDist(px, prevPt, pt));
54
+ prevPt = pt;
55
+ }
56
+ cov = max(cov, clamp(th * 0.5 - dmin + 0.5, 0.0, 1.0) * (px.x >= x0 && px.x <= x0 + span ? 1.0 : 0.0));
57
+ amp = max(amp, abs(waveAt(px.x, x0, span)) / max(amplitude, 0.001));
58
+ } else {
59
+ float cell = max(span / float(columns), 2.0);
60
+ float c = floor((px.x - x0) / cell);
61
+ float xc = x0 + (c + 0.5) * cell;
62
+ float w = sgn * waveAt(xc, x0, span) * res.y;
63
+ float dyv = px.y - mid;
64
+ vec2 lp = vec2(px.x - xc, dyv);
65
+ amp = max(amp, abs(w) / max(amplitude * res.y, 1.0));
66
+ if (px.x >= x0 && px.x <= x0 + span) {
67
+ if (displayMode == 0) {
68
+ float rowH = cell;
69
+ float rr = floor(abs(dyv) / rowH);
70
+ bool lit = dyv * w >= 0.0 && abs(dyv) <= abs(w) + rowH * 0.5;
71
+ float inCell = abs(dyv) - rr * rowH;
72
+ float blk = clamp(0.5 - max(abs(lp.x) - cell * (0.5 - 0.08), abs(inCell - rowH * 0.5) - rowH * 0.42), 0.0, 1.0);
73
+ cov = max(cov, lit ? blk : 0.0);
74
+ } else {
75
+ float rad = max(cell * 0.5 * (0.35 + 0.65 * min(abs(w) / (amplitude * res.y), 1.0)), th * 0.5);
76
+ cov = max(cov, clamp(rad - length(vec2(lp.x, dyv - w)) + 0.5, 0.0, 1.0));
77
+ }
78
+ }
79
+ }
80
+ }
81
+ vec3 line = shade(clamp(amp * amp, 0.0, 1.0) * 0.6, u);
82
+ line = mix(line, vec3(1.4), hit);
83
+ vec4 prev = texelFetch(trace, ivec2(gl_FragCoord.xy), 0);
84
+ vec4 cur = vec4(line * cov, cov);
85
+ fragColor = max(prev * echo, cur);
86
+ }
@@ -0,0 +1,38 @@
1
+ // usage: node test/_render_light.mjs [outDir] (default ./out/ under the current directory; renders the light-family demos)
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdirSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const outDir = path.resolve(process.argv[2] ?? "out");
9
+
10
+ const jobs = [
11
+ ["templates/light/bloom-glow/demo-neon-night.json", "bloom-glow-neon-night"],
12
+ ["templates/light/anamorphic-streak/demo-anamorphic-cinema.json", "anamorphic-streak-anamorphic-cinema"],
13
+ ["templates/light/god-rays/demo-golden-hour.json", "god-rays-golden-hour"],
14
+ ["templates/light/light-leak/demo-vintage-burn.json", "light-leak-vintage-burn"],
15
+ ["templates/light/neon-saber/demo-saber-laser.json", "neon-saber-saber-laser"],
16
+ ["templates/light/lens-flare/demo-sun-ghosts.json", "lens-flare-sun-ghosts"],
17
+ ["templates/light/light-sweep/demo-logo-sweep.json", "light-sweep-logo-sweep"],
18
+ ["templates/light/bloom-glow/demo-clip01-low.json", "bloom-glow-clip01-low"],
19
+ ["templates/light/light-sweep/demo-onset.json", "light-sweep-onset"],
20
+ ];
21
+
22
+ let failed = 0;
23
+ for (const [score, name] of jobs) {
24
+ const out = path.join(outDir, name);
25
+ mkdirSync(out, { recursive: true });
26
+ const started = Date.now();
27
+ console.log(`START ${out}`);
28
+ const r = spawnSync("node", ["scripts/vfx.mjs", "render", score, "--out", out], { stdio: "inherit", cwd: root });
29
+ const sec = ((Date.now() - started) / 1000).toFixed(1);
30
+ if (r.status !== 0) {
31
+ failed += 1;
32
+ console.log(`FAIL ${out} status=${r.status} sec=${sec}`);
33
+ } else {
34
+ console.log(`OK ${out} sec=${sec}`);
35
+ }
36
+ }
37
+ console.log(failed === 0 ? "RENDER_ALL_OK" : `RENDER_FAILED ${failed}`);
38
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,45 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawnSync } from "node:child_process";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import path from "node:path";
7
+ import { analyzeAudio } from "../engine/node/audio.mjs";
8
+ import { seedToUnit } from "../engine/web/prelude.mjs";
9
+
10
+ test("waveL and waveR carry the two channels (L = 440 Hz sine, R = silence)", async () => {
11
+ const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-stereo-"));
12
+ try {
13
+ const wav = path.join(dir, "lr.wav");
14
+ const r = spawnSync("ffmpeg", ["-v", "error", "-y", "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000:duration=2", "-f", "lavfi", "-i", "anullsrc=r=48000:cl=mono", "-filter_complex", "[0:a][1:a]join=inputs=2:channel_layout=stereo[a]", "-map", "[a]", "-t", "2", wav], { encoding: "utf8" });
15
+ assert.equal(r.status, 0, r.stderr);
16
+ const f = await analyzeAudio(wav, { fps: 30 });
17
+ const fr = f.frames[20];
18
+ const p2p = (a) => Math.max(...a) - Math.min(...a);
19
+ // ffmpeg's sine source has amplitude 1/8, so peak-to-peak is 0.25.
20
+ assert.ok(p2p(fr.waveL) > 0.2, `left p2p ${p2p(fr.waveL)}`);
21
+ assert.ok(p2p(fr.waveR) < 0.01, `right p2p ${p2p(fr.waveR)}`);
22
+ } finally {
23
+ rmSync(dir, { recursive: true, force: true });
24
+ }
25
+ });
26
+
27
+ test("no band is dead: a 40 Hz sine lights band 1", async () => {
28
+ const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-band-"));
29
+ try {
30
+ const wav = path.join(dir, "b.wav");
31
+ const r = spawnSync("ffmpeg", ["-v", "error", "-y", "-f", "lavfi", "-i", "sine=frequency=40:sample_rate=48000:duration=2", "-ac", "2", wav], { encoding: "utf8" });
32
+ assert.equal(r.status, 0, r.stderr);
33
+ const f = await analyzeAudio(wav, { fps: 30 });
34
+ assert.ok(f.frames[30].bands[1] > 0.3, `band 1 = ${f.frames[30].bands[1]}`);
35
+ } finally {
36
+ rmSync(dir, { recursive: true, force: true });
37
+ }
38
+ });
39
+
40
+ test("seedToUnit spreads small integer seeds", () => {
41
+ const v = [1, 2, 3, 7, 31].map(seedToUnit);
42
+ for (const x of v) assert.ok(x >= 0 && x < 1);
43
+ const spread = Math.max(...v) - Math.min(...v);
44
+ assert.ok(spread > 0.3, `spread ${spread}`);
45
+ });
@@ -0,0 +1,257 @@
1
+ // Lane audio — TDD tests for engine/node/audio.mjs
2
+ // Task 7 acceptance criteria (plans/agentvfx-v1.md). WAV fixtures are
3
+ // generated at runtime under os.tmpdir() and removed afterwards.
4
+ import { describe, it, before, after } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import {
10
+ analyzeAudio,
11
+ writeFeatures,
12
+ summarize,
13
+ } from '../engine/node/audio.mjs';
14
+
15
+ const SR = 48000;
16
+ const FPS = 30;
17
+ let dir;
18
+ let clickPath;
19
+ let click140;
20
+ let click174;
21
+ let sinePath;
22
+ let sineLoudPath;
23
+ let noisePath;
24
+ let leftPath;
25
+
26
+ function wavBytes({ seconds, channels, fn }) {
27
+ const n = Math.floor(seconds * SR);
28
+ const data = Buffer.alloc(n * channels * 2);
29
+ for (let i = 0; i < n; i++) {
30
+ const t = i / SR;
31
+ const s = fn(t, i);
32
+ const ch = Array.isArray(s) ? s : [s, s];
33
+ for (let c = 0; c < channels; c++) {
34
+ const v = Math.max(-1, Math.min(1, ch[c] ?? 0));
35
+ data.writeInt16LE(Math.round(v * 32767), (i * channels + c) * 2);
36
+ }
37
+ }
38
+ const header = Buffer.alloc(44);
39
+ header.write('RIFF', 0);
40
+ header.writeUInt32LE(36 + data.length, 4);
41
+ header.write('WAVE', 8);
42
+ header.write('fmt ', 12);
43
+ header.writeUInt32LE(16, 16);
44
+ header.writeUInt16LE(1, 20); // PCM
45
+ header.writeUInt16LE(channels, 22);
46
+ header.writeUInt32LE(SR, 24);
47
+ header.writeUInt32LE(SR * channels * 2, 28);
48
+ header.writeUInt16LE(channels * 2, 32);
49
+ header.writeUInt16LE(16, 34);
50
+ header.write('data', 36);
51
+ header.writeUInt32LE(data.length, 40);
52
+ return Buffer.concat([header, data]);
53
+ }
54
+
55
+ // 120 BPM click: 16 clicks at k*0.5 s, short decaying 2 kHz blip.
56
+ function clickFn(t) {
57
+ const phase = t % 0.5;
58
+ const k = Math.floor(phase * SR);
59
+ if (k >= 96) return 0;
60
+ return Math.sin(2 * Math.PI * 2000 * phase) * Math.exp(-k / 24) * 0.9;
61
+ }
62
+
63
+ // Click train at a given BPM; returns { bytes, times } with click times in seconds.
64
+ function clickTrack(bpm, seconds) {
65
+ const period = 60 / bpm;
66
+ const times = [];
67
+ for (let k = 0; ; k++) {
68
+ const t = k * period;
69
+ if (t > seconds - 0.15) break;
70
+ times.push(t);
71
+ }
72
+ const fn = (t) => {
73
+ let v = 0;
74
+ for (const c of times) {
75
+ const dt = t - c;
76
+ if (dt >= 0 && dt * SR < 96) {
77
+ const k = Math.floor(dt * SR);
78
+ v += Math.sin(2 * Math.PI * 2000 * dt) * Math.exp(-k / 24) * 0.9;
79
+ }
80
+ }
81
+ return v;
82
+ };
83
+ return { bytes: wavBytes({ seconds, channels: 2, fn }), times };
84
+ }
85
+
86
+ // Deterministic pseudo-random noise (LCG) at fixed amplitude.
87
+ function noiseFn(amp, seed) {
88
+ let s = seed;
89
+ return () => {
90
+ s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
91
+ return ((s / 0x100000000) * 2 - 1) * amp;
92
+ };
93
+ }
94
+
95
+ before(async () => {
96
+ dir = await mkdtemp(join(tmpdir(), 'lane-audio-'));
97
+ clickPath = join(dir, 'click-120.wav');
98
+ click140 = join(dir, 'click-140.wav');
99
+ click174 = join(dir, 'click-174.wav');
100
+ sinePath = join(dir, 'sine-440.wav');
101
+ sineLoudPath = join(dir, 'sine-440-loud.wav');
102
+ noisePath = join(dir, 'noise-mid.wav');
103
+ leftPath = join(dir, 'hard-left.wav');
104
+ await writeFile(clickPath, wavBytes({ seconds: 8, channels: 2, fn: clickFn }));
105
+ await writeFile(click140, clickTrack(140, 6).bytes);
106
+ await writeFile(click174, clickTrack(174, 6).bytes);
107
+ await writeFile(
108
+ sinePath,
109
+ wavBytes({ seconds: 8, channels: 2, fn: (t) => Math.sin(2 * Math.PI * 440 * t) * 0.5 }),
110
+ );
111
+ await writeFile(
112
+ sineLoudPath,
113
+ wavBytes({ seconds: 4, channels: 2, fn: (t) => Math.sin(2 * Math.PI * 440 * t) * 1.0 }),
114
+ );
115
+ await writeFile(noisePath, wavBytes({ seconds: 4, channels: 2, fn: noiseFn(0.8, 12345) }));
116
+ await writeFile(
117
+ leftPath,
118
+ wavBytes({
119
+ seconds: 4,
120
+ channels: 2,
121
+ fn: (t) => [Math.sin(2 * Math.PI * 440 * t) * 0.5, 0],
122
+ }),
123
+ );
124
+ });
125
+
126
+ after(async () => {
127
+ await rm(dir, { recursive: true, force: true });
128
+ });
129
+
130
+ describe('audio features — 120 BPM click track (8 s)', () => {
131
+ it('detects every click as an onset within ±1 frame at 30 fps', async () => {
132
+ const f = await analyzeAudio(clickPath, { fps: FPS });
133
+ assert.equal(f.frames.length, 240);
134
+ const expectedClicks = Array.from({ length: 16 }, (_, k) => k * 0.5);
135
+ for (const c of expectedClicks) {
136
+ const frame = Math.round(c * FPS);
137
+ const hit = f.meta.onsets.some((o) => Math.abs(o - frame) <= 1);
138
+ assert.ok(hit, `click at ${c}s (frame ${frame}) has no onset within ±1 frame`);
139
+ }
140
+ assert.equal(f.meta.onsets.length, 16);
141
+ });
142
+
143
+ it('estimates bpm 120 ± 1', async () => {
144
+ const f = await analyzeAudio(clickPath, { fps: FPS });
145
+ assert.ok(Math.abs(f.meta.bpm - 120) <= 1, `bpm was ${f.meta.bpm}`);
146
+ const s = summarize(f);
147
+ assert.ok(Math.abs(s.bpm - 120) <= 1);
148
+ assert.equal(s.onsetCount, 16);
149
+ assert.equal(s.frames, 240);
150
+ });
151
+ });
152
+
153
+ describe('audio features — 440 Hz sine', () => {
154
+ it('gives the 440 Hz band the maximum mean value', async () => {
155
+ const f = await analyzeAudio(sinePath, { fps: FPS });
156
+ // log-spaced band edges, 30 Hz–16 kHz, 32 bands (mirrors implementation)
157
+ const edges = Array.from({ length: 33 }, (_, k) => 30 * Math.pow(16000 / 30, k / 32));
158
+ let want = -1;
159
+ for (let k = 0; k < 32; k++) {
160
+ if (edges[k] <= 440 && 440 < edges[k + 1]) want = k;
161
+ }
162
+ assert.notEqual(want, -1);
163
+ const means = Array(32).fill(0);
164
+ for (const fr of f.frames) fr.bands.forEach((v, k) => { means[k] += v; });
165
+ for (let k = 0; k < 32; k++) means[k] /= f.frames.length;
166
+ let best = 0;
167
+ for (let k = 1; k < 32; k++) if (means[k] > means[best]) best = k;
168
+ assert.equal(best, want, `max band was ${best}, want ${want}`);
169
+ assert.ok(means[want] > 0.5, `440 Hz band mean was ${means[want]}`);
170
+ });
171
+ });
172
+
173
+ for (const [bpm, file] of [[140, 'click140'], [174, 'click174']]) {
174
+ const getPath = () => (file === 'click140' ? click140 : click174);
175
+ describe(`audio features — ${bpm} BPM click track (6 s)`, () => {
176
+ it('detects every click as an onset within ±1 frame at 30 fps', async () => {
177
+ const f = await analyzeAudio(getPath(), { fps: FPS });
178
+ assert.equal(f.frames.length, 180);
179
+ const { times } = clickTrack(bpm, 6);
180
+ for (const c of times) {
181
+ const frame = Math.round(c * FPS);
182
+ const hit = f.meta.onsets.some((o) => Math.abs(o - frame) <= 1);
183
+ assert.ok(hit, `click at ${c}s (frame ${frame}) has no onset within ±1 frame`);
184
+ }
185
+ assert.equal(f.meta.onsets.length, times.length);
186
+ });
187
+
188
+ it(`estimates bpm ${bpm} ± 1`, async () => {
189
+ const f = await analyzeAudio(getPath(), { fps: FPS });
190
+ assert.ok(Math.abs(f.meta.bpm - bpm) <= 1, `bpm was ${f.meta.bpm}`);
191
+ });
192
+ });
193
+ }
194
+
195
+ describe('audio features — windowing', () => {
196
+ it('startSec=1, durationSec=5 at 30 fps gives exactly 150 frames', async () => {
197
+ const f = await analyzeAudio(sinePath, { fps: FPS, startSec: 1, durationSec: 5 });
198
+ assert.equal(f.frames.length, 150);
199
+ assert.equal(f.meta.frames, 150);
200
+ assert.ok(Math.abs(f.frames[0].t - 0) < 1e-9, `first frame t was ${f.frames[0].t}`);
201
+ });
202
+
203
+ it('a 440 Hz sine yields non-flat wave on every frame after the first', async () => {
204
+ const f = await analyzeAudio(sineLoudPath, { fps: FPS });
205
+ for (let i = 1; i < f.frames.length; i++) {
206
+ const w = f.frames[i].wave;
207
+ let lo = Infinity;
208
+ let hi = -Infinity;
209
+ for (const v of w) {
210
+ if (v < lo) lo = v;
211
+ if (v > hi) hi = v;
212
+ }
213
+ assert.ok(hi - lo > 1.5, `frame ${i} wave peak-to-peak was ${hi - lo}`);
214
+ }
215
+ });
216
+
217
+ it('a track starting mid-loud-noise has no onset at frame 0', async () => {
218
+ const f = await analyzeAudio(noisePath, { fps: FPS });
219
+ assert.ok(!f.meta.onsets.includes(0), `onsets were ${f.meta.onsets.slice(0, 8)}`);
220
+ });
221
+
222
+ it('RMS of a full-scale sine is within 0.5 dB of -3.01 dBFS', async () => {
223
+ const f = await analyzeAudio(sineLoudPath, { fps: FPS });
224
+ const mean = f.frames.reduce((a, fr) => a + fr.rmsDb, 0) / f.frames.length;
225
+ assert.ok(Math.abs(mean - -3.01) <= 0.5, `mean rmsDb was ${mean}`);
226
+ });
227
+ });
228
+
229
+ describe('audio features — hard-left signal', () => {
230
+ it('reports mean balance < -0.9', async () => {
231
+ const f = await analyzeAudio(leftPath, { fps: FPS });
232
+ const mean = f.frames.reduce((a, fr) => a + fr.balance, 0) / f.frames.length;
233
+ assert.ok(mean < -0.9, `mean balance was ${mean}`);
234
+ });
235
+ });
236
+
237
+ describe('audio features — determinism and errors', () => {
238
+ it('writes byte-identical JSON for identical input twice', async () => {
239
+ const a = await analyzeAudio(sinePath, { fps: FPS });
240
+ const b = await analyzeAudio(sinePath, { fps: FPS });
241
+ const p1 = join(dir, 'a.json');
242
+ const p2 = join(dir, 'b.json');
243
+ await writeFeatures(a, p1);
244
+ await writeFeatures(b, p2);
245
+ const [b1, b2] = await Promise.all([readFile(p1), readFile(p2)]);
246
+ assert.ok(b1.equals(b2), 'writeFeatures output differed between runs');
247
+ });
248
+
249
+ it('rejects with an error naming the path when the file is missing', async () => {
250
+ const missing = join(dir, 'does-not-exist.wav');
251
+ await assert.rejects(analyzeAudio(missing, { fps: FPS }), (err) => {
252
+ assert.ok(err instanceof Error);
253
+ assert.ok(String(err.message).includes(missing), `message was: ${err.message}`);
254
+ return true;
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,190 @@
1
+ // Brutal family (plan task 14, first slice): hud-deck, halftone-field, boot-log. Validation of every preset and
2
+ // demo, 90-frame render + determinism + flash report per template, text ink, halftone dot structure,
3
+ // window shapes, audio-following panels and the beat flare.
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { loadLibrary } from "../engine/core/templates.mjs";
11
+ import { validateScore } from "../engine/core/validate.mjs";
12
+ import { runJob } from "../engine/node/job.mjs";
13
+ import { renderScore } from "../engine/node/render.mjs";
14
+ import { analyzeAudio } from "../engine/node/audio.mjs";
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+ const MUSIC = "test-media/beat-132.mp3";
18
+ const lib = loadLibrary();
19
+ const IDS = ["hud-deck", "halftone-field", "boot-log"];
20
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-brutal-"));
21
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
22
+ let runs = 0;
23
+
24
+ const score = (template, preset, extra = {}) => {
25
+ const v = validateScore({ version: 1, template, ...(preset ? { preset } : {}), durationSec: 3, seed: 7, out: "t", ...extra }, lib);
26
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
27
+ return v.value;
28
+ };
29
+
30
+ /** Render a score at w x h and keep the RGBA of the wanted frames (audio features from `audio` when given). */
31
+ async function frames(s, keep, [w, h] = [640, 360], audio = null) {
32
+ const features = audio ? await analyzeAudio(path.resolve(ROOT, audio), { fps: 30 }) : null;
33
+ const kept = new Map();
34
+ await renderScore(s, lib, { outDir: path.join(tmp, `r-${runs++}`), width: w, height: h, features, replay: false, onFrame: (f, a) => { if (keep.includes(f)) kept.set(f, Buffer.from(a.rgba)); } });
35
+ return { kept, features };
36
+ }
37
+ /** Pixels in the (fractional) region whose channel `ch` is above `min`. */
38
+ function count(rgba, [w, h], [x0, y0, x1, y1], ch, min) {
39
+ let n = 0;
40
+ for (let y = Math.round(y0 * h); y < Math.round(y1 * h); y++) for (let x = Math.round(x0 * w); x < Math.round(x1 * w); x++) if (rgba[(y * w + x) * 4 + ch] > min) n += 1;
41
+ return n;
42
+ }
43
+ const px = (rgba, w, x, y) => { const o = (y * w + x) * 4; return [rgba[o], rgba[o + 1], rgba[o + 2]]; };
44
+
45
+ test("every preset of every brutal template validates, with at least three presets, a flash class and demos", () => {
46
+ for (const id of IDS) {
47
+ const tpl = lib.get(id);
48
+ assert.ok(tpl, `${id} is in the library`);
49
+ assert.equal(tpl.family, "brutal");
50
+ assert.ok(Object.keys(tpl.fx.X_AGENTVFX.presets).length >= 3, `${id} has at least 3 presets`);
51
+ assert.ok(["none", "possible", "strobe"].includes(tpl.fx.X_AGENTVFX.flash));
52
+ for (const preset of Object.keys(tpl.fx.X_AGENTVFX.presets)) score(id, preset, tpl.fx.X_AGENTVFX.input === "footage" ? { input: { video: "test-media/clip-01.mp4" } } : {});
53
+ const demos = readdirSync(tpl.dir).filter((f) => f.startsWith("demo-"));
54
+ assert.ok(demos.length >= 3, `${id} has demo scores`);
55
+ for (const d of demos) {
56
+ const demo = JSON.parse(readFileSync(path.join(tpl.dir, d), "utf8"));
57
+ const v = validateScore(demo, lib);
58
+ assert.equal(v.ok, true, `${d}: ${JSON.stringify(v.errors)}`);
59
+ assert.equal(demo.durationSec, 4);
60
+ if (demo.input?.video) assert.ok(existsSync(path.join(ROOT, demo.input.video)));
61
+ assert.equal(demo.input.audio, MUSIC);
62
+ }
63
+ }
64
+ });
65
+
66
+ for (const id of IDS) {
67
+ test(`${id}: demo renders every frame, passes determinism, reports flashes, muxes audio`, async () => {
68
+ const tpl = lib.get(id);
69
+ const demo = JSON.parse(readFileSync(path.join(tpl.dir, readdirSync(tpl.dir).filter((f) => f.startsWith("demo-")).sort()[0]), "utf8"));
70
+ const s = validateScore({ ...demo, durationSec: 3, out: "t" }, lib).value;
71
+ const gate = await runJob(s, lib, { outDir: path.join(tmp, `job-${id}`), width: 320, height: 180, cwd: ROOT });
72
+ assert.equal(gate.render.frames, 90);
73
+ assert.equal(gate.determinism.pass, true, JSON.stringify(gate.determinism));
74
+ assert.equal(typeof gate.flash.exceeds, "boolean");
75
+ assert.equal(typeof gate.flash.flashesPerSecondMax, "number");
76
+ });
77
+ }
78
+
79
+ test("hud-deck: label text draws ink in the desk and the tall title, and labels override", async () => {
80
+ const size = [640, 360];
81
+ const { kept } = await frames(score("hud-deck", "night"), [75], size);
82
+ const f = kept.get(75);
83
+ assert.ok(count(f, size, [0.012, 0.745, 0.2, 0.82], 0, 160) > 250, "artist label in the desk");
84
+ assert.ok(count(f, size, [0.012, 0.84, 0.2, 0.915], 0, 160) > 250, "track label in the desk");
85
+ assert.ok(count(f, size, [0.7, 0.88, 0.99, 0.975], 0, 160) > 200, "tall title bottom right");
86
+ const wide = await frames(score("hud-deck", "night", { labels: { title: "WWWWWWWWWW" } }), [75], size);
87
+ assert.ok(count(wide.kept.get(75), size, [0.4, 0.88, 0.99, 0.975], 0, 160) > count(f, size, [0.4, 0.88, 0.99, 0.975], 0, 160), "longer title -> more ink");
88
+ });
89
+
90
+ test("hud-deck: spectrum graph and waveform follow the music and sit at the floor in silence", async () => {
91
+ const size = [960, 540];
92
+ const music = await frames(score("hud-deck", "night", { input: { audio: MUSIC } }), [60], size, MUSIC);
93
+ const quiet = await frames(score("hud-deck", "night"), [60], size);
94
+ const above = [0.3, 0.75, 0.68, 0.9];
95
+ const loud = count(music.kept.get(60), size, above, 0, 200);
96
+ assert.ok(loud > 30, `spectrum line above the floor with music: ${loud}`);
97
+ assert.ok(count(quiet.kept.get(60), size, above, 0, 200) < 8, "silent spectrum stays on the floor");
98
+ const waveBox = [0.02, 0.915, 0.26, 0.96];
99
+ assert.ok(count(music.kept.get(60), size, waveBox, 0, 150) > count(quiet.kept.get(60), size, waveBox, 0, 150) + 40, "waveform strip draws the wave");
100
+ });
101
+
102
+ test("hud-deck: the window is a chamfered octagon in the octagon preset, a rect otherwise", async () => {
103
+ const size = [640, 360];
104
+ const solid = { params: { mediaMode: 3, winBg: [1, 1, 1, 1], tone: [1, 1, 1, 1], flare: 0, tearAmount: 0 } };
105
+ const oct = await frames(score("hud-deck", "octagon", solid), [70], size);
106
+ const rect = await frames(score("hud-deck", "octagon", { params: { ...solid.params, winShape: 0 } }), [70], size);
107
+ const corner = [Math.round((0.55 - 0.29) * 640) + 6, Math.round((0.47 - 0.35) * 360) + 6];
108
+ const centre = [320, 190];
109
+ assert.ok(px(oct.kept.get(70), 640, ...corner)[0] < 40, "octagon corner is ground");
110
+ assert.ok(px(rect.kept.get(70), 640, ...corner)[0] > 200, "rect corner is the window");
111
+ assert.ok(px(oct.kept.get(70), 640, ...centre)[0] > 200, "octagon centre is the window");
112
+ });
113
+
114
+ test("hud-deck: the white-hot light bar breathes with the beat", async () => {
115
+ const size = [640, 360];
116
+ const base = await frames(score("hud-deck", "night", { input: { audio: MUSIC } }), Array.from({ length: 80 }, (_, i) => i), size, MUSIC);
117
+ const off = await frames(score("hud-deck", "night", { input: { audio: MUSIC }, params: { flare: 0 } }), Array.from({ length: 80 }, (_, i) => i), size, MUSIC);
118
+ const beat = base.features.meta.beats.find((b) => b >= 20 && b < 65);
119
+ assert.ok(beat !== undefined, "a beat inside the window");
120
+ const band = [0.3542 - 0.2, 0.3657 - 0.05, 0.3542 + 0.2, 0.3657 + 0.05];
121
+ const light = (f) => { let s = 0; const a = base.kept.get(f); const b = off.kept.get(f); for (let y = Math.round(band[1] * 360); y < Math.round(band[3] * 360); y++) for (let x = Math.round(band[0] * 640); x < Math.round(band[2] * 640); x++) s += Math.max(0, a[(y * 640 + x) * 4 + 1] - b[(y * 640 + x) * 4 + 1]); return s; };
122
+ assert.ok(light(beat + 1) > 1000, `flare adds light on the beat: ${light(beat + 1)}`);
123
+ assert.ok(light(beat + 1) > light(Math.min(beat + 10, 79)) * 1.1, "brighter on the beat than between beats");
124
+ });
125
+
126
+ test("halftone-field: single-tone dot lattice at the set pitch, binary dither and glyph screens stay on two colours", async () => {
127
+ const size = [960, 540];
128
+ const params = { cell: 24, dotAngle: 0, tearAmount: 0, kickGain: 0, textKnock: 0 };
129
+ const field = [20, 20, 255];
130
+ const dot = [203, 189, 255];
131
+ const region = [0.36, 0.56, 0.64, 0.9];
132
+ const lin = (v) => (v / 255 <= 0.04045 ? v / 255 / 12.92 : ((v / 255 + 0.055) / 1.055) ** 2.4);
133
+ const srgb = (c) => 255 * (c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055);
134
+ const twoTone = (rgba, lim) => {
135
+ let on = 0;
136
+ let all = 0;
137
+ for (let y = Math.round(region[1] * 540); y < Math.round(region[3] * 540); y++) {
138
+ for (let x = Math.round(region[0] * 960); x < Math.round(region[2] * 960); x++) {
139
+ const p = px(rgba, 960, x, y);
140
+ const t = Math.min(1, Math.max(0, (lin(p[0]) - lin(field[0])) / (lin(dot[0]) - lin(field[0]))));
141
+ const err = Math.max(...p.map((v, i) => Math.abs(v - srgb(lin(field[i]) + (lin(dot[i]) - lin(field[i])) * t))));
142
+ all += 1;
143
+ if (err <= lim) on += 1;
144
+ }
145
+ }
146
+ return on / all;
147
+ };
148
+ const a = await frames(score("halftone-field", "ultramarine", { params }), [40], size);
149
+ const f = a.kept.get(40);
150
+ assert.ok(twoTone(f, 12) > 0.99, `dot screen is one tone: ${twoTone(f, 12)}`);
151
+ const prof = [];
152
+ for (let x = Math.round(region[0] * 960); x < Math.round(region[2] * 960); x++) { let s = 0; for (let y = Math.round(region[1] * 540); y < Math.round(region[3] * 540); y++) s += px(f, 960, x, y)[0]; prof.push(s); }
153
+ const mean = prof.map((_, i) => { let s = 0; let n = 0; for (let k = Math.max(0, i - 12); k <= Math.min(prof.length - 1, i + 12); k++) { s += prof[k]; n += 1; } return s / n; });
154
+ const z = prof.map((v, i) => v - mean[i]);
155
+ const ac = (lag) => { let s = 0; for (let i = 0; i + lag < z.length; i++) s += z[i] * z[i + lag]; return s; };
156
+ assert.ok(ac(12) > 0.3 * ac(0), `pitch 24 design px = 12 px at 960x540: lag-12 autocorrelation ${(ac(12) / ac(0)).toFixed(2)}`);
157
+ assert.ok(ac(12) > ac(6) + 0.4 * ac(0), "half-pitch lag is anti-phase");
158
+ const lo = Math.min(...prof);
159
+ const hi = Math.max(...prof);
160
+ assert.ok(hi > lo * 1.01, "dot area varies with the image tone");
161
+ const dither = await frames(score("halftone-field", "dither", { params: { tearAmount: 0, kickGain: 0, textKnock: 0 } }), [40], size);
162
+ assert.ok(twoTone(dither.kept.get(40), 6) > 0.995, `ordered dither is binary: ${twoTone(dither.kept.get(40), 6)}`);
163
+ });
164
+
165
+ test("halftone-field: pink and cyan micro bars hang on the image edges and the tall headline draws ink", async () => {
166
+ const size = [640, 360];
167
+ const { kept } = await frames(score("halftone-field", "ultramarine", { input: { audio: MUSIC } }), [60], size, MUSIC);
168
+ const f = kept.get(60);
169
+ const pink = count(f, size, [0.03, 0.15, 0.1, 0.95], 0, 230);
170
+ assert.ok(pink > 40, `edge bars on the left edge: ${pink}`);
171
+ assert.ok(count(f, size, [0.04, 0.1, 0.45, 0.5], 0, 215) > 200, "headline ink");
172
+ });
173
+
174
+ test("boot-log: the log types out, the headline decodes only after the log, then holds still", async () => {
175
+ const size = [640, 360];
176
+ const quiet = { params: { flickerAmount: 0, scanAmount: 0, tearAmount: 0, streakAmount: 0, dustAmount: 0 } };
177
+ const { kept } = await frames(score("boot-log", "phosphor", { ...quiet, durationSec: 4 }), [8, 40, 85, 112, 119], size);
178
+ const logBox = [0.03, 0.24, 0.45, 0.62];
179
+ const headBox = [0.03, 0.68, 0.45, 0.84];
180
+ const lit = (f, box) => count(kept.get(f), size, box, 1, 140);
181
+ assert.ok(lit(8, logBox) < lit(40, logBox), "log grows while typing");
182
+ assert.ok(lit(40, logBox) < lit(85, logBox), "log keeps typing");
183
+ assert.ok(lit(40, headBox) < 12, "headline hidden before the decode");
184
+ assert.ok(lit(112, headBox) > 120, "headline drawn after the decode");
185
+ let diff = 0;
186
+ const a = kept.get(112);
187
+ const b = kept.get(119);
188
+ for (let y = Math.round(headBox[1] * 360); y < Math.round(headBox[3] * 360); y++) for (let x = Math.round(headBox[0] * 640); x < Math.round(headBox[2] * 640); x++) diff += Math.abs(a[(y * 640 + x) * 4 + 1] - b[(y * 640 + x) * 4 + 1]);
189
+ assert.ok(diff / ((headBox[2] - headBox[0]) * 640 * (headBox[3] - headBox[1]) * 360) < 2.5, "decoded headline is stable");
190
+ });