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,153 @@
1
+ // Collage experimental type (role typefx): layout data and packing for the new role and the sheet channel, the three presets
2
+ // glyph-flood / slit-scan / echo-stack and the G / B text sheets they read, and measured behaviour on rendered frames:
3
+ // every effect attacks on the music event frame and relaxes within a few frames (snappy), while the cells hold their place.
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdtempSync, 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 { renderScore } from "../engine/node/render.mjs";
13
+ import { analyzeAudio } from "../engine/node/audio.mjs";
14
+ import { checkLayout, layoutCells, layoutToParams, packSrc } from "../templates/brutal/collage/layout.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 tpl = lib.get("collage");
20
+ const LAYOUTS = JSON.parse(readFileSync(path.join(tpl.dir, "layouts.json"), "utf8"));
21
+ const EXPERIMENTAL = ["glyph-flood", "glyph-flood-ko", "glyph-flood-solid", "slit-scan", "slit-scan-ko", "slit-scan-bone", "echo-stack", "echo-stack-ko", "echo-ripple", "type-eq", "type-eq-ko"];
22
+ const WORD_VARIANT = { "glyph-flood": 2, "glyph-flood-ko": 2, "glyph-flood-solid": 2, "slit-scan": 3, "slit-scan-ko": 3, "slit-scan-bone": 3, "echo-stack": 4, "echo-stack-ko": 4, "echo-ripple": 5, "type-eq": 6, "type-eq-ko": 6 };
23
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-collage-typefx-"));
24
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
25
+ const W = 640;
26
+ const H = 360;
27
+ let runs = 0;
28
+
29
+ const cell = (x, y, w, h, role, more = {}) => ({ x, y, w, h, role, ...more });
30
+
31
+ test("typefx role and src.sheet: packing puts the sheet in the millions digit, bad sheets and out-of-range values are rejected", () => {
32
+ const c = cell(0, 0, 32, 18, "typefx", { frame: "none", variant: 3, src: { x: -2, y: -0.5, scale: 1.2, sheet: "b" } });
33
+ assert.deepEqual(checkLayout([c]), []);
34
+ assert.equal(packSrc(c), 2000000 + 12 * 10000 + 16 * 100 + 19);
35
+ assert.equal(packSrc(cell(0, 0, 4, 4, "type", { src: { x: 1, y: 1, scale: 1, sheet: "g" } })), 1000000 + 10 * 10000 + 22 * 100 + 22, "type cells take a sheet too");
36
+ assert.equal(packSrc(cell(0, 0, 4, 4, "type", { src: { x: 1, y: 1, scale: 1 } })), 10 * 10000 + 22 * 100 + 22, "default sheet is r (digit 0)");
37
+ assert.equal(packSrc(cell(0, 0, 4, 4, "typefx")), 10 * 10000 + 20 * 100 + 20, "a typefx cell without src still reads the sheet origin");
38
+ assert.match(checkLayout([cell(0, 0, 4, 4, "typefx", { src: { x: 0, y: 0, scale: 1, sheet: "x" } })]).map((e) => e.message).join("|"), /one of r, g, b/u);
39
+ assert.equal(checkLayout([cell(0, 0, 4, 4, "typefx", { variant: 6 })]).length, 0, "variants up to 9 are valid for typefx");
40
+ const p = layoutToParams([c, cell(0, 0, 32, 18, "typefx", { frame: "none", variant: 1 })]);
41
+ const input = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i]));
42
+ for (const [k, v] of Object.entries(p)) assert.ok(v >= input[k].MIN && v <= input[k].MAX, `${k} ${v} inside its input range`);
43
+ assert.equal(p.cell1, 1300003218, "typefx is role 13");
44
+ });
45
+
46
+ test("experimental presets: valid layouts that equal their preset, vermilion family, full-bleed, typefx cells, G / B sheet windows", () => {
47
+ const defaults = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i.DEFAULT]));
48
+ for (const n of EXPERIMENTAL) {
49
+ const e = LAYOUTS[n];
50
+ assert.deepEqual(checkLayout(e), [], n);
51
+ assert.deepEqual({ ...defaults, ...tpl.fx.X_AGENTVFX.presets[n] }, { ...defaults, ...e.params, ...layoutToParams(e) }, `${n} preset equals its layout`);
52
+ const cells = layoutCells(e);
53
+ assert.ok(cells.every((c) => c.frame === "none"), `${n}: no boxed tiles`);
54
+ assert.ok(cells.filter((c) => c.role === "typefx").length >= 2, `${n} has typefx cells`);
55
+ assert.equal(e.params.margin, 0, `${n} runs to the frame edge`);
56
+ assert.ok(typeof e.description === "string" && e.description.length > 80, `${n} describes itself`);
57
+ const vermilion = ([r, g, b]) => r > 0.8 && g > 0.15 && g < 0.3 && b < 0.15;
58
+ assert.ok(vermilion(e.params.accentColor) || vermilion(e.params.groundColor), `${n} uses vermilion as accent or ground`);
59
+ for (const c of cells.filter((q) => q.role === "typefx")) assert.ok(["g", "b"].includes(c.src?.sheet), `${n}: every typefx cell reads the G or B sheet`);
60
+ assert.ok(cells.some((c) => c.role === "typefx" && c.variant === WORD_VARIANT[n]), `${n} has its word cell (typefx variant ${WORD_VARIANT[n]})`);
61
+ assert.ok(cells.some((c) => c.role === "typefx" && c.variant === 1), `${n} has a micro-type field`);
62
+ }
63
+ const layoutOf = (n) => JSON.stringify(layoutCells(LAYOUTS[n]));
64
+ assert.equal(new Set(EXPERIMENTAL.map(layoutOf)).size, EXPERIMENTAL.length, "every preset is its own composition");
65
+ const warp = (n) => ({ ...defaults, ...tpl.fx.X_AGENTVFX.presets[n] }).typeWarp;
66
+ assert.ok(warp("slit-scan-ko") < 0.6 && warp("slit-scan-bone") < 0.6 && warp("slit-scan") === 1, "the calmer slit-scan variants lower typeWarp");
67
+ const word = (n) => layoutCells(LAYOUTS[n]).find((c) => c.role === "typefx" && c.variant === WORD_VARIANT[n]);
68
+ assert.deepEqual(["glyph-flood", "slit-scan", "echo-stack"].map((n) => word(n).src.sheet), ["g", "b", "g"]);
69
+ assert.equal(word("glyph-flood-solid").tone, "accent", "the solid glyph variant fills with the accent");
70
+ });
71
+
72
+ test("text sheet: the experimental words are on G and B, in Monument Extended / Pretendard, the micro-type lines in MesloLGS NF / Pretendard, the editorial sheet stays on R", () => {
73
+ const text = tpl.fx.X_AGENTVFX.text.filter((t) => t.set === undefined);
74
+ const by =Object.fromEntries(text.map((t) => [t.key, t]));
75
+ const channels = { mega: "g", mega2: "g", megaK: "g", echo: "g", echoK: "g", microK: "g", slit: "b", slitK: "b", eq: "b", eqK: "b", microM: "b" };
76
+ for (const [k, ch] of Object.entries(channels)) assert.equal(by[k].channel, ch, `${k} channel`);
77
+ assert.ok(text.filter((t) => !(t.key in channels)).every((t) => t.channel === undefined || t.channel === "r"), "the editorial sheet stays on R");
78
+ for (const k of ["mega", "mega2", "echo", "eq"]) assert.equal(by[k].family, "Monument Extended", `${k} is Monument Extended`);
79
+ for (const k of ["megaK", "echoK", "microK", "slit", "slitK", "eqK"]) assert.equal(by[k].family, "Pretendard", `${k} is Pretendard`);
80
+ assert.equal(by.microM.family, "MesloLGS NF");
81
+ for (const k of ["capA", "capB", "body", "fields", "atlas"]) assert.equal(by[k].family, "MesloLGS NF", `${k} caption/counter text is MesloLGS NF`);
82
+ for (const k of ["megaK", "echoK", "slitK", "eqK"]) assert.match(by[k].default, /[가-힣]/u, `${k} is Hangul`);
83
+ for (const t of Object.values(by)) assert.ok(t.default.length >= 1 && t.size <= 1, `${t.key} has text`);
84
+ const v = validateScore({ version: 1, template: "collage", preset: "glyph-flood", input: { audio: MUSIC }, durationSec: 3, seed: 5, out: "t", labels: { mega: "R", mega2: "Q", echo: "NODE", slit: "HALL", eqK: "빛" }, textStyle: { mega: { family: "Pretendard", weight: 700 } } }, lib);
85
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
86
+ });
87
+
88
+ async function frames(preset, keep, params = {}) {
89
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
90
+ const v = validateScore({ version: 1, template: "collage", preset, input: { audio: MUSIC }, durationSec: 3, seed: 5, out: "t", params: { grain: 0, ...params } }, lib);
91
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
92
+ const kept = new Map();
93
+ await renderScore(v.value, lib, { outDir: path.join(tmp, `t-${runs++}`), width: W, height: H, features, replay: false, onFrame: (f, a) => { if (keep.includes(f)) kept.set(f, Buffer.from(a.rgba)); } });
94
+ return { kept, features };
95
+ }
96
+
97
+ /** Events (onsets or beats) with no other event in the 4 frames before and the 5 after, so the pulse attacks clean and has fully fallen by e + 5. */
98
+ const clean = (events, lo, hi) => events.filter((e) => e >= lo && e < hi && !events.some((p) => p !== e && p > e - 5 && p < e + 6));
99
+ const px = (rgba, x, y) => { const i = (y * W + x) * 4; return [rgba[i], rgba[i + 1], rgba[i + 2]]; };
100
+ const count = (rgba, test, x0 = 0, y0 = 0, x1 = W, y1 = H) => { let n = 0; for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) if (test(px(rgba, x, y))) n += 1; return n; };
101
+ const isVermilion = ([r, g, b]) => r > 170 && g < 110 && b < 80;
102
+
103
+ test("echo-stack: the stack is at its fullest on the beat frame (more vermilion copies) and has fallen back by 5 frames later", async () => {
104
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
105
+ const beats = clean(features.meta.beats, 8, 85).slice(0, 3);
106
+ assert.ok(beats.length >= 2, `clean beats exist: ${beats}`);
107
+ const { kept } = await frames("echo-stack", beats.flatMap((b) => [b - 1, b, b + 5]));
108
+ for (const b of beats) {
109
+ const [before, at, after] = [b - 1, b, b + 5].map((f) => count(kept.get(f), isVermilion));
110
+ assert.ok(at >= before * 0.98 && at > 18000, `beat ${b}: vermilion ${before} -> ${at}`);
111
+ assert.ok(after < at * 0.6, `beat ${b}: vermilion ${at} -> ${after} after 5 frames`);
112
+ }
113
+ });
114
+
115
+ test("typeWarp scales the warp: declared 0..1.5 with default 1, and 0 holds the echo stack still across a beat", async () => {
116
+ const input = tpl.fx.INPUTS.find((i) => i.NAME === "typeWarp");
117
+ assert.deepEqual([input.MIN, input.MAX, input.DEFAULT], [0, 1.5, 1]);
118
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
119
+ const b = clean(features.meta.beats, 8, 85)[0];
120
+ const { kept } = await frames("echo-stack", [b, b + 5], { typeWarp: 0 });
121
+ const [at, after] = [b, b + 5].map((f) => count(kept.get(f), isVermilion));
122
+ assert.ok(Math.abs(at - after) < at * 0.03, `beat ${b}: vermilion ${at} vs ${after} with the warp off`);
123
+ assert.ok(at < 12000, `only the front pair of copies is drawn: ${at}`);
124
+ });
125
+
126
+ test("glyph-flood: the hot extrusion jumps out on the onset frame and is back within 5 frames; the cut-out letter keeps its place", async () => {
127
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
128
+ const onsets = clean(features.meta.onsets, 8, 85).slice(0, 3);
129
+ const { kept } = await frames("glyph-flood", onsets.flatMap((o) => [o - 1, o, o + 5]));
130
+ for (const o of onsets) {
131
+ const [before, at, after] = [o - 1, o, o + 5].map((f) => count(kept.get(f), isVermilion, 48, 0, W, H));
132
+ assert.ok(at > before * 1.1 + 1500, `onset ${o}: vermilion ${before} -> ${at}`);
133
+ assert.ok(after < at * 0.9, `onset ${o}: vermilion ${at} -> ${after} after 5 frames`);
134
+ const a = kept.get(o - 1);
135
+ const b = kept.get(o + 5);
136
+ let same = 0;
137
+ for (let y = 120; y < 240; y++) same += Math.abs(px(a, 320, y)[0] - px(b, 320, y)[0]) < 40 ? 1 : 0;
138
+ assert.ok(same > 100, `the glyph interior column holds its place between onsets (${same} of 120 rows unchanged)`);
139
+ }
140
+ });
141
+
142
+ test("slit-scan: a beat smears the word into a pale streak on its frame and the streak is gone 5 frames later", async () => {
143
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
144
+ const beats = clean(features.meta.beats, 8, 85).slice(0, 3);
145
+ assert.ok(beats.length >= 2, `clean beats exist: ${beats}`);
146
+ const { kept } = await frames("slit-scan", beats.flatMap((b) => [b - 1, b, b + 5]));
147
+ const pale = ([r, g, b]) => r > 200 && g > 110 && g < 236 && b > 90 && r - g > 8;
148
+ for (const b of beats) {
149
+ const [before, at, after] = [b - 1, b, b + 5].map((f) => count(kept.get(f), pale, 0, 24, 320, H));
150
+ assert.ok(at > before && at > 15000, `beat ${b}: pale smear ${before} -> ${at}`);
151
+ assert.ok(after < 500, `beat ${b}: pale smear ${at} -> ${after} after 5 frames`);
152
+ }
153
+ });
@@ -0,0 +1,84 @@
1
+ // Concept C "collage": validation, snappy-motion declarations, measured responsiveness on rendered
2
+ // frames, and layout regeneration on beats.
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { loadLibrary } from "../engine/core/templates.mjs";
10
+ import { validateScore } from "../engine/core/validate.mjs";
11
+ import { renderScore } from "../engine/node/render.mjs";
12
+ import { analyzeAudio } from "../engine/node/audio.mjs";
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const MUSIC = "test-media/beat-132.mp3";
16
+ const lib = loadLibrary();
17
+ const tpl = lib.get("collage");
18
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-collage-"));
19
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
20
+ let runs = 0;
21
+
22
+ function score(preset, params = {}) {
23
+ const v = validateScore({ version: 1, template: "collage", ...(preset ? { preset } : {}), input: { audio: MUSIC }, durationSec: 3, seed: 5, out: "t", params }, lib);
24
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
25
+ return v.value;
26
+ }
27
+
28
+ async function frames(preset, params, keep) {
29
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
30
+ const kept = new Map();
31
+ await renderScore(score(preset, params), lib, { outDir: path.join(tmp, `c-${runs++}`), width: 320, height: 180, features, replay: false, onFrame: (f, a) => { if (keep.includes(f)) kept.set(f, Buffer.from(a.rgba)); } });
32
+ return { kept, features };
33
+ }
34
+
35
+ /** Bright pixels in the top channel row (bass) of the solo level meter, rows 12..40 of 180. */
36
+ const lit = (rgba) => { let c = 0; for (let y = 12; y < 40; y++) for (let x = 0; x < 320; x++) { const i = (y * 320 + x) * 4; if (rgba[i] + rgba[i + 1] + rgba[i + 2] > 380) c += 1; } return c; };
37
+ const diff = (a, b) => { let c = 0; for (let i = 0; i < a.length; i += 4) if (Math.abs(a[i + 1] - b[i + 1]) > 40) c += 1; return c / (a.length / 4); };
38
+
39
+ test("collage validates, ships 4 presets incl. release 0.06 and 0.12, and a demo score each", () => {
40
+ assert.ok(tpl, "collage is in the library");
41
+ const presets = tpl.fx.X_AGENTVFX.presets;
42
+ assert.ok(Object.keys(presets).length >= 3);
43
+ for (const p of Object.keys(presets)) score(p);
44
+ const rel = (p) => ({ ...Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i.DEFAULT])), ...presets[p] }).release;
45
+ const values = Object.keys(presets).map(rel);
46
+ assert.ok(values.includes(0.06) && values.includes(0.12), `release values ${values}`);
47
+ assert.equal(tpl.fx.X_AGENTVFX.spdx, "Apache-2.0");
48
+ assert.ok(tpl.fx.X_AGENTVFX.safeSubstitute);
49
+ for (const d of readdirSync(tpl.dir).filter((f) => f.startsWith("demo-"))) assert.equal(validateScore(JSON.parse(readFileSync(path.join(tpl.dir, d), "utf8")), lib).ok, true);
50
+ });
51
+
52
+ test("snappy declarations: release default <= 0.07 s, no attack or settle style parameter", () => {
53
+ const names = tpl.fx.INPUTS.map((i) => i.NAME);
54
+ assert.ok(tpl.fx.INPUTS.find((i) => i.NAME === "release").DEFAULT <= 0.07);
55
+ assert.deepEqual(names.filter((n) => /attack|settle|smooth|easing|lag/iu.test(n)), []);
56
+ });
57
+
58
+ test("level meter: lights within 1 frame of a strong bass onset and drops by half within 4 frames", async () => {
59
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
60
+ const onsets = features.meta.onsets.filter((o) => o >= 4 && o < 85 && features.frames[o].bass > 3 && features.frames[o - 1].bass < 1.3 && features.frames[o + 4].bass < 1);
61
+ assert.ok(onsets.length >= 1, "a clean strong onset exists");
62
+ const keep = onsets.flatMap((o) => [o - 1, o, o + 4]);
63
+ const { kept } = await frames("bone-lime", { cutMode: 3, soloPanel: 4, grain: 0, gridDots: 0, construction: false }, keep);
64
+ for (const o of onsets) {
65
+ const before = lit(kept.get(o - 1));
66
+ const at = lit(kept.get(o));
67
+ const after = lit(kept.get(o + 4));
68
+ assert.ok(at > before * 1.3, `onset ${o}: lit ${before} -> ${at}`);
69
+ assert.ok(after <= at * 0.5, `onset ${o}: lit ${at} -> ${after} after 4 frames`);
70
+ }
71
+ });
72
+
73
+ test("layoutMode reshuffle (opt-in): the layout is rebuilt on a beat and holds between cuts", async () => {
74
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
75
+ const beat = features.meta.beats.find((b) => b >= 4 && b < 85);
76
+ const keep = [beat - 1, beat, beat + 1, beat + 2];
77
+ const live = (await frames("bone-lime", { layoutMode: 1, construction: false, grain: 0 }, keep)).kept;
78
+ const frozen = (await frames("bone-lime", { layoutMode: 1, construction: false, grain: 0, cutMode: 3 }, keep)).kept;
79
+ const cut = diff(live.get(beat - 1), live.get(beat));
80
+ const still = diff(frozen.get(beat - 1), frozen.get(beat));
81
+ assert.ok(cut > 0.2, `pixels changed on the beat: ${cut}`);
82
+ assert.ok(cut > still * 2, `cut ${cut} vs frozen layout ${still}`);
83
+ assert.ok(diff(live.get(beat), live.get(beat + 1)) < cut, "layout holds after the cut");
84
+ });
@@ -0,0 +1,133 @@
1
+ // Concept B "lattice": validation, snappy-motion declarations, and a rendered responsiveness probe
2
+ // (the lattice deforms on the frame of a kick and is mostly back to order within four frames).
3
+ import { test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { loadLibrary } from "../engine/core/templates.mjs";
10
+ import { validateScore } from "../engine/core/validate.mjs";
11
+ import { renderScore } from "../engine/node/render.mjs";
12
+ import { analyzeAudio } from "../engine/node/audio.mjs";
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const MUSIC = "test-media/beat-132.mp3";
16
+ const lib = loadLibrary();
17
+ const tpl = lib.get("lattice");
18
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-lattice-"));
19
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
20
+ let runs = 0;
21
+
22
+ const score = (preset, extra = {}) => {
23
+ const v = validateScore({ version: 1, template: "lattice", preset, durationSec: 3, seed: 5, out: "t", ...extra }, lib);
24
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
25
+ return v.value;
26
+ };
27
+ const releaseOf = (preset) => tpl.fx.X_AGENTVFX.presets[preset].release ?? tpl.fx.INPUTS.find((i) => i.NAME === "release").DEFAULT;
28
+
29
+ test("lattice validates: every preset and demo, 4 s demos with the shared music", () => {
30
+ assert.ok(tpl, "lattice is in the library");
31
+ const presets = Object.keys(tpl.fx.X_AGENTVFX.presets);
32
+ assert.ok(presets.length >= 3);
33
+ for (const p of presets) score(p);
34
+ const demos = readdirSync(tpl.dir).filter((f) => f.startsWith("demo-"));
35
+ assert.ok(demos.length >= 3);
36
+ for (const d of demos) {
37
+ const demo = JSON.parse(readFileSync(path.join(tpl.dir, d), "utf8"));
38
+ const v = validateScore(demo, lib);
39
+ assert.equal(v.ok, true, `${d}: ${JSON.stringify(v.errors)}`);
40
+ assert.equal(demo.durationSec, 4);
41
+ assert.equal(demo.input.audio, MUSIC);
42
+ assert.ok(existsSync(path.join(ROOT, demo.input.video)));
43
+ }
44
+ assert.equal(tpl.fx.X_AGENTVFX.spdx, "Apache-2.0");
45
+ assert.ok(tpl.fx.X_AGENTVFX.safeSubstitute);
46
+ });
47
+
48
+ test("lattice knobs: colours, grid size and headline font/text are settable from a score", () => {
49
+ const palettes = ["graphite-ice", "phosphor", "bone-ink"];
50
+ for (const p of palettes) assert.ok(tpl.fx.X_AGENTVFX.presets[p]?.dotColor, `palette preset ${p}`);
51
+ assert.equal(tpl.fx.X_AGENTVFX.presets.vermilion && Object.keys(tpl.fx.X_AGENTVFX.presets.vermilion).length, 0, "vermilion stays the default");
52
+ const colours = { groundColor: [0.1, 0.1, 0.1, 1], dotColor: [0.2, 0.9, 0.3, 1], hotColor: [1, 1, 1, 1] };
53
+ assert.deepEqual(score("vermilion", { params: colours }).params, colours);
54
+ assert.equal(validateScore({ version: 1, template: "lattice", durationSec: 3, seed: 5, params: { dotColor: [2, 0, 0, 1] } }, lib).ok, false, "out-of-range colour rejected");
55
+ for (const pitch of [8, 80]) score("vermilion", { params: { pitch } });
56
+ assert.equal(validateScore({ version: 1, template: "lattice", durationSec: 3, seed: 5, params: { pitch: 81 } }, lib).ok, false);
57
+ score("vermilion", { labels: { headline: "ZONE" }, textStyle: { headline: { font: "mono", size: 0.12, x: 0.1, y: 0.4 } } });
58
+ });
59
+
60
+ test("lattice grid size is resolution independent: the same pitch gives the same number of columns at 640x360 and 960x540", async () => {
61
+ const quiet = { mediaAmount: 0, headline: 0, jitter: 0, sparkle: 0, tearShear: 0, ghost: 0, links: 0, dotBase: 0.3 };
62
+ const columns = async (w, h) => {
63
+ const kept = new Map();
64
+ await renderScore(score("vermilion", { params: quiet, fps: 24 }), lib, { outDir: path.join(tmp, `r-${runs++}`), width: w, height: h, replay: false, onFrame: (f, a) => { if (f === 2) kept.set(f, Buffer.from(a.rgba)); } });
65
+ const px = kept.get(2);
66
+ const prof = Array.from({ length: w }, (_, x) => { let s = 0; for (let y = 0; y < h; y++) s += px[(y * w + x) * 4 + 0]; return s / h; });
67
+ const mid = (Math.min(...prof) + Math.max(...prof)) / 2;
68
+ let n = 0;
69
+ for (let x = 1; x < w; x++) if (prof[x - 1] < mid && prof[x] >= mid) n++;
70
+ return n;
71
+ };
72
+ const small = await columns(640, 360);
73
+ const big = await columns(960, 540);
74
+ const expected = 1920 / 22;
75
+ assert.ok(Math.abs(small - expected) <= 3, `640x360 shows ${small} columns, expected about ${expected.toFixed(1)}`);
76
+ assert.ok(Math.abs(big - expected) <= 3, `960x540 shows ${big} columns, expected about ${expected.toFixed(1)}`);
77
+ });
78
+
79
+ test("lattice declares snappy motion: release default <= 0.07 s, one preset at 0.12, no attack smoothing anywhere", () => {
80
+ const rel = tpl.fx.INPUTS.find((i) => i.NAME === "release");
81
+ assert.ok(rel.DEFAULT <= 0.07, `release default ${rel.DEFAULT}`);
82
+ const rels = Object.keys(tpl.fx.X_AGENTVFX.presets).map(releaseOf);
83
+ assert.ok(rels.includes(0.12), "a 0.12 s preset to compare");
84
+ assert.ok(rels.every((r) => r <= 0.12 && r >= rel.MIN));
85
+ for (const i of tpl.fx.INPUTS) assert.ok(!/attack|settle|lag|inertia|smooth|easing|glide/iu.test(i.NAME), `no smoothing-style param: ${i.NAME}`);
86
+ const src = tpl.passes.map((f) => readFileSync(path.join(tpl.dir, f), "utf8").replace(/\/\/.*$/gmu, "")).join("\n");
87
+ assert.ok(!/attack|settle|smoothstep\s*\(\s*0\.0\s*,\s*1\.0\s*,\s*AUDIO/iu.test(src), "no attack/settle in shader code");
88
+ assert.ok(/max\(\s*clamp\(\(AUDIO_TREB/u.test(src), "running envelope is max(raw, decayed previous): instant attack");
89
+ });
90
+
91
+ /** Render frames 0..n of a score at 320x180 and return the RGBA of each wanted frame. */
92
+ async function frames(s, keep, features) {
93
+ const kept = new Map();
94
+ await renderScore(s, lib, { outDir: path.join(tmp, `r-${runs++}`), width: 320, height: 180, features, replay: false, onFrame: (f, a) => { if (keep.includes(f)) kept.set(f, Buffer.from(a.rgba)); } });
95
+ return kept;
96
+ }
97
+ /** Peak disturbance: the largest mean |difference| over any 16x16 block (amplitude, not the area the wave has reached). */
98
+ const dev = (a, b) => {
99
+ const B = 16;
100
+ let best = 0;
101
+ for (let by = 0; by < Math.floor(180 / B); by++) {
102
+ for (let bx = 0; bx < Math.floor(320 / B); bx++) {
103
+ let s = 0;
104
+ for (let y = 0; y < B; y++) for (let x = 0; x < B; x++) { const i = ((by * B + y) * 320 + bx * B + x) * 4; s += Math.abs(a[i] - b[i]) + Math.abs(a[i + 1] - b[i + 1]) + Math.abs(a[i + 2] - b[i + 2]); }
105
+ best = Math.max(best, s / (B * B));
106
+ }
107
+ }
108
+ return best;
109
+ };
110
+
111
+ test("lattice: silence is a perfect static grid; a kick deforms it on the same frame and it is mostly back within 4 frames", async () => {
112
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
113
+ const fr = features.frames;
114
+ const kickBass = tpl.fx.INPUTS.find((i) => i.NAME === "kickBass").DEFAULT;
115
+ const f = fr.findIndex((x, i) => i >= 8 && x.bass >= kickBass && x.bass > 1.5 * fr[i - 1].bass
116
+ && fr.slice(i - 6, i).every((y) => !y.onset) && fr.slice(i + 1, i + 6).every((y) => !y.onset));
117
+ assert.ok(f > 0, "an isolated kick exists in the test music");
118
+ const quiet = { mediaAmount: 0, headline: 0, jitter: 0, sparkle: 0, tearShear: 0 };
119
+ const keep = [f - 1, f, f + 1, f + 2, f + 4];
120
+ const rest = await frames(score("vermilion", { params: quiet }), keep, null);
121
+ const floor = dev(rest.get(f - 1), rest.get(f + 4));
122
+ assert.ok(floor < 2, `silent lattice only differs by the output dither: ${floor.toFixed(3)}`);
123
+ const run = async (preset) => {
124
+ const k = await frames(score(preset, { params: quiet, input: { audio: MUSIC } }), keep, features);
125
+ const ref = preset === "vermilion" ? rest : await frames(score(preset, { params: quiet }), keep, null);
126
+ return keep.map((n) => Math.max(dev(k.get(n), ref.get(n)) - floor, 0));
127
+ };
128
+ const [before, onset, , , after] = await run("vermilion");
129
+ assert.ok(onset > 3 * before + 1,`deformed on the onset frame: ${onset.toFixed(2)} vs ${before.toFixed(2)} before`);
130
+ assert.ok(after <= 0.5 * onset, `drops by half within 4 frames: ${after.toFixed(2)} vs ${onset.toFixed(2)}`);
131
+ const slow = await run("violet-slow");
132
+ assert.ok(slow[4] / slow[1] > after / onset, "release 0.12 lingers longer than release 0.06");
133
+ });
@@ -0,0 +1,172 @@
1
+ // signal-cut concept (design A): validation of every preset and demo, snappy-response declarations
2
+ // (release default <= 0.07 s, one preset at 0.12, no attack smoothing parameter) and a render probe on the
3
+ // real clip and music: the field cut lands on the beat frame and is gone within four frames.
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { spawnSync } from "node:child_process";
7
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { loadLibrary } from "../engine/core/templates.mjs";
12
+ import { validateScore } from "../engine/core/validate.mjs";
13
+ import { renderScore } from "../engine/node/render.mjs";
14
+ import { analyzeAudio } from "../engine/node/audio.mjs";
15
+ import { openFootage } from "../engine/node/footage.mjs";
16
+ import { auditFlash, LINEAR_LUT } from "../engine/core/flash.mjs";
17
+ import { FLASH } from "../engine/core/flash-constants.mjs";
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const CLIP = "test-media/clip-01.mp4";
21
+ const MUSIC = "test-media/beat-132.mp3";
22
+ const lib = loadLibrary();
23
+ const tpl = lib.get("signal-cut");
24
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-signal-cut-"));
25
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
26
+ const W = 320;
27
+ const H = 180;
28
+
29
+ const score = (preset, extra = {}) => {
30
+ const v = validateScore({ version: 1, template: "signal-cut", preset, input: { video: CLIP, audio: MUSIC }, durationSec: 3, seed: 7, out: "t", ...extra }, lib);
31
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
32
+ return v.value;
33
+ };
34
+
35
+ async function frames(s, keep) {
36
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30, durationSec: s.durationSec });
37
+ const footage = openFootage(path.resolve(ROOT, CLIP), { fps: 30, durationSec: s.durationSec, longEdge: W });
38
+ const kept = new Map();
39
+ try {
40
+ await renderScore(s, lib, { outDir: mkdtempSync(path.join(tmp, "r-")), width: W, height: H, footage, features, replay: false, onFrame: (f, a) => { if (keep.includes(f)) kept.set(f, Buffer.from(a.rgba)); } });
41
+ } finally {
42
+ footage.close();
43
+ }
44
+ return { kept, features };
45
+ }
46
+ /** Share of pixels that are the acid field colour (200, 255, 0). */
47
+ const fieldShare = (rgba) => {
48
+ let n = 0;
49
+ for (let i = 0; i < W * H; i++) if (rgba[i * 4] > 150 && rgba[i * 4 + 1] > 200 && rgba[i * 4 + 2] < 60) n += 1;
50
+ return n / (W * H);
51
+ };
52
+
53
+ test("signal-cut is declared: family, 3+ presets, strobe class with a safe substitute, demos validate", () => {
54
+ assert.ok(tpl, "signal-cut is in the library");
55
+ const x = tpl.fx.X_AGENTVFX;
56
+ assert.equal(x.input, "footage");
57
+ assert.equal(x.spdx, "Apache-2.0");
58
+ assert.ok(Object.keys(x.presets).length >= 3);
59
+ assert.equal(x.flash, "strobe");
60
+ assert.equal(x.safeSubstitute.params.fieldCut, 0);
61
+ for (const preset of Object.keys(x.presets)) score(preset);
62
+ const demos = readdirSync(tpl.dir).filter((f) => f.startsWith("demo-"));
63
+ assert.ok(demos.length >= 3);
64
+ for (const d of demos) {
65
+ const demo = JSON.parse(readFileSync(path.join(tpl.dir, d), "utf8"));
66
+ assert.equal(validateScore(demo, lib).ok, true, d);
67
+ assert.equal(demo.durationSec, 4);
68
+ assert.ok(existsSync(path.join(ROOT, demo.input.video)));
69
+ assert.equal(demo.input.audio, MUSIC);
70
+ }
71
+ });
72
+
73
+ test("signal-cut is snappy by declaration: release default <= 0.07 s, a 0.12 s preset, no attack smoothing parameter", () => {
74
+ const inputs = tpl.fx.INPUTS;
75
+ const release = inputs.find((i) => i.NAME === "release");
76
+ assert.ok(release.DEFAULT <= 0.07, `release default ${release.DEFAULT}`);
77
+ const x = tpl.fx.X_AGENTVFX;
78
+ const resolved = (p) => score(p).params?.release ?? release.DEFAULT;
79
+ assert.ok(Object.keys(x.presets).some((p) => (x.presets[p].release ?? release.DEFAULT) === 0.12), "one preset releases at 0.12 s");
80
+ assert.ok(resolved("lime") <= 0.07);
81
+ assert.equal(inputs.some((i) => /attack|smooth|settle|lag|damp|inertia/iu.test(i.NAME)), false, "no attack or smoothing parameter");
82
+ const pass0 = readFileSync(path.join(tpl.dir, "pass0.frag"), "utf8");
83
+ assert.ok(/max\(target, held\)/u.test(pass0), "follower is max(target, held): instant attack");
84
+ });
85
+
86
+ test("signal-cut: the field cut lands on the beat frame and is gone within four frames", async () => {
87
+ const { kept, features } = await frames(score("lime"), Array.from({ length: 70 }, (_, i) => i));
88
+ const beat = features.meta.beats.find((b) => b >= 10 && b < 60);
89
+ assert.ok(beat !== undefined, "a beat inside the window");
90
+ const before = fieldShare(kept.get(beat - 1));
91
+ const on = fieldShare(kept.get(beat));
92
+ const later = fieldShare(kept.get(beat + 4));
93
+ assert.ok(on > 0.25, `field colour covers the frame on the beat frame: ${on}`);
94
+ assert.ok(before < on * 0.5, `no field colour the frame before: ${before}`);
95
+ assert.ok(later < on * 0.5, `field cut dropped by half within four frames: ${on} -> ${later}`);
96
+ });
97
+
98
+ test("signal-cut: fieldCut 0 (the safe substitute) removes the cut but the slabs still move", async () => {
99
+ const { kept, features } = await frames(score("lime", { params: { fieldCut: 0, stampFrames: 0 } }), Array.from({ length: 70 }, (_, i) => i));
100
+ const beat = features.meta.beats.find((b) => b >= 10 && b < 60);
101
+ assert.ok(fieldShare(kept.get(beat)) < 0.3, `no solid field without the cut: ${fieldShare(kept.get(beat)).toFixed(3)} at beat ${beat}`);
102
+ let d = 0;
103
+ for (let i = 0; i < W * H * 4; i++) d += Math.abs(kept.get(beat)[i] - kept.get(beat - 1)[i]);
104
+ assert.ok(d / (W * H * 4) > 3, "frame still changes on the beat");
105
+ });
106
+
107
+ // slabContrast crushes green and blue on warm footage, so a hard scene cut in warm footage used to read as a
108
+ // saturated-red transition with the safe values on (the plate behind the live preview does exactly this).
109
+ test("signal-cut: hard cuts between warm and neutral footage never make a saturated-red transition", async () => {
110
+ const clip = path.join(tmp, "warm-cuts.mp4");
111
+ const lavfi = `color=c=0x8c453f:size=${W}x${H}:rate=30:d=1,format=yuv420p[a];color=c=0x303030:size=${W}x${H}:rate=30:d=1,format=yuv420p[b];[a][b]concat=n=2:v=1,loop=loop=3:size=60`;
112
+ const made = spawnSync("ffmpeg", ["-v", "error", "-y", "-filter_complex", lavfi, "-t", "4", "-c:v", "libx264", "-crf", "12", "-pix_fmt", "yuv420p", clip], { encoding: "utf8" });
113
+ assert.equal(made.status, 0, made.stderr);
114
+ const v = validateScore({ version: 1, template: "signal-cut", preset: "lime", params: tpl.fx.X_AGENTVFX.safeSubstitute.params, input: { video: CLIP }, durationSec: 4, seed: 7, out: "t" }, lib);
115
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
116
+ const footage = openFootage(clip, { fps: 30, durationSec: 4, longEdge: W });
117
+ const seq = [];
118
+ try {
119
+ await renderScore(v.value, lib, { outDir: mkdtempSync(path.join(tmp, "r-")), width: W, height: H, footage, replay: false, onFrame: (f, a) => { seq[f] = { width: W, height: H, data: Buffer.from(a.rgba) }; } });
120
+ } finally {
121
+ footage.close();
122
+ }
123
+ assert.equal(auditFlash(seq, { fps: 30 }).redFlashes, 0);
124
+ });
125
+
126
+ // The declared safe substitute is what the engine renders as film.safe.mp4. Before it also held the stamp, the
127
+ // idle/live dimming and the beat shifts, every preset still flashed 8+ times a second (orange also in red) and the
128
+ // engine withheld the safe film.
129
+ test("signal-cut: every preset passes the flash audit with the safe substitute (real clip and music)", async () => {
130
+ const features = await analyzeAudio(path.join(ROOT, MUSIC), { fps: 30, durationSec: 5 });
131
+ for (const preset of Object.keys(tpl.fx.X_AGENTVFX.presets)) {
132
+ const v = validateScore({ version: 1, template: "signal-cut", preset, params: tpl.fx.X_AGENTVFX.safeSubstitute.params, input: { video: CLIP, audio: MUSIC }, durationSec: 5, seed: 7, out: "t" }, lib);
133
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
134
+ const footage = openFootage(path.join(ROOT, CLIP), { fps: 30, durationSec: 5, longEdge: W });
135
+ const seq = [];
136
+ try {
137
+ await renderScore(v.value, lib, { outDir: mkdtempSync(path.join(tmp, "s-")), width: W, height: H, footage, features, replay: false, onFrame: (f, a) => { seq[f] = { width: W, height: H, data: Buffer.from(a.rgba) }; } });
138
+ } finally {
139
+ footage.close();
140
+ }
141
+ const r = auditFlash(seq, { fps: 30 });
142
+ assert.equal(r.exceeds, false, `${preset}: ${JSON.stringify(r.worstWindow)}`);
143
+ assert.equal(r.redFlashes, 0, preset);
144
+ }
145
+ });
146
+
147
+ // The field colour fills the frame on every beat in the stage film, so a saturated-red field colour makes every cut a red
148
+ // flash (orange at (1, 0.235, 0) gave 20 in 5 s). Each preset's field colour must sit below the auditor's red rule.
149
+ test("signal-cut: every preset's field colour is below the red-flash rule in linear light", () => {
150
+ const def = tpl.fx.INPUTS.find((i) => i.NAME === "fieldColor").DEFAULT;
151
+ for (const [preset, p] of Object.entries(tpl.fx.X_AGENTVFX.presets)) {
152
+ const [r, g, b] = (p.fieldColor ?? def).slice(0, 3).map((c) => LINEAR_LUT[Math.round(c * 255)]);
153
+ assert.ok(r / (r + g + b) < FLASH.redRatio, `${preset}: red share ${(r / (r + g + b)).toFixed(3)}`);
154
+ }
155
+ });
156
+
157
+ // The stage film may strobe, but never in saturated red: the cut shows the raw footage for a frame or two between field
158
+ // states, and deep-red footage (a lit red dress in the test clip) then read as red flashes in the orange demo.
159
+ test("signal-cut: the orange stage demo on the real clip has no red flashes", async () => {
160
+ const demo = JSON.parse(readFileSync(path.join(tpl.dir, "demo-orange.json"), "utf8"));
161
+ const v = validateScore({ ...demo, durationSec: 4 }, lib);
162
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
163
+ const features = await analyzeAudio(path.join(ROOT, MUSIC), { fps: 30, durationSec: 4 });
164
+ const footage = openFootage(path.join(ROOT, CLIP), { fps: 30, durationSec: 4, longEdge: W });
165
+ const seq = [];
166
+ try {
167
+ await renderScore(v.value, lib, { outDir: mkdtempSync(path.join(tmp, "o-")), width: W, height: H, footage, features, replay: false, onFrame: (f, a) => { seq[f] = { width: W, height: H, data: Buffer.from(a.rgba) }; } });
168
+ } finally {
169
+ footage.close();
170
+ }
171
+ assert.equal(auditFlash(seq, { fps: 30 }).redFlashes, 0);
172
+ });
@@ -0,0 +1,58 @@
1
+ // The on-screen credit: platforms strip MP4 metadata, so every render burns a small "made with agentvfx" mark into the
2
+ // bottom-right corner unless the score opts out with "credit": { "overlay": false }. The mark is static, so it cannot add
3
+ // a flash, and it touches nothing outside its corner.
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdtempSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+ import { loadLibrary } from "../engine/core/templates.mjs";
10
+ import { validateScore } from "../engine/core/validate.mjs";
11
+ import { renderScore } from "../engine/node/render.mjs";
12
+
13
+ const lib = loadLibrary({ includeTest: true });
14
+ const W = 640;
15
+ const H = 360;
16
+ const base = { version: 1, template: "test-gradient", durationSec: 3, seed: 3, out: "t" };
17
+
18
+ test("credit defaults to an on-screen overlay and only accepts { overlay: boolean }", () => {
19
+ const v = validateScore(base, lib);
20
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
21
+ assert.deepEqual(v.value.credit, { overlay: true });
22
+ assert.deepEqual(validateScore({ ...base, credit: { overlay: false } }, lib).value.credit, { overlay: false });
23
+ const bad = validateScore({ ...base, credit: { overlay: "no" } }, lib);
24
+ assert.ok(bad.errors.some((e) => e.path === "credit.overlay"), JSON.stringify(bad.errors));
25
+ const unknown = validateScore({ ...base, credit: { size: 3 } }, lib);
26
+ assert.ok(unknown.errors.some((e) => e.path === "credit.size"), JSON.stringify(unknown.errors));
27
+ });
28
+
29
+ async function frame(score, f = 30) {
30
+ const v = validateScore(score, lib);
31
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
32
+ const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-overlay-"));
33
+ let kept = null;
34
+ try {
35
+ await renderScore(v.value, lib, { outDir: dir, width: W, height: H, replay: false, onFrame: (i, a) => { if (i === f) kept = Buffer.from(a.rgba); } });
36
+ } finally {
37
+ rmSync(dir, { recursive: true, force: true });
38
+ }
39
+ return kept;
40
+ }
41
+
42
+ test("the overlay marks the bottom-right corner and nothing else; overlay false leaves the frame untouched", async () => {
43
+ const on = await frame(base);
44
+ const off = await frame({ ...base, credit: { overlay: false } });
45
+ let inside = 0;
46
+ let outside = 0;
47
+ for (let y = 0; y < H; y++) {
48
+ for (let x = 0; x < W; x++) {
49
+ const i = (y * W + x) * 4;
50
+ const d = Math.abs(on[i] - off[i]) + Math.abs(on[i + 1] - off[i + 1]) + Math.abs(on[i + 2] - off[i + 2]);
51
+ if (d <= 6) continue;
52
+ if (x > W * 0.55 && y > H * 0.85) inside += 1;
53
+ else outside += 1;
54
+ }
55
+ }
56
+ assert.ok(inside > 150, `credit pixels in the corner: ${inside}`);
57
+ assert.equal(outside, 0, "nothing changes outside the corner");
58
+ });