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,236 @@
1
+ // Collage layouts (editorial model): layout data validation and packing, the three editorial presets, the text sheet,
2
+ // the layout file shipped for the layout editor, and measured behaviour on rendered frames (type holds its place
3
+ // while it reacts, instruments move, the old tile layouts still render outlines that hold).
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { existsSync, 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 { MAX_CELLS, ROLES, checkLayout, layoutCells, layoutToParams, packCell, packSrc, packStyle } 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 EDITORIAL = ["poster", "spread", "index"];
22
+ 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"];
23
+ const SCENES = ["specimen", "wiring", "field-cut", "pattern-sheet", "anatomy", "deck", "bootlog", "tunnel", "runner", "ascii", "cube-wall", "vortex-count", "sticker-slap", "ribbon-rays", "slab-shear", "frame-loop-dial", "tilt-tiles", "marquee-stack", "lens-wrap", "liquid-type", "panel-grid", "mirror-track"];
24
+ const TILES = ["wide-hero", "split", "mosaic"];
25
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-collage-layout-"));
26
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
27
+ let runs = 0;
28
+ const W = 640;
29
+ const H = 360;
30
+
31
+ const cell = (x, y, w, h, role, more = {}) => ({ x, y, w, h, role, ...more });
32
+ const defaults = () => Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i.DEFAULT]));
33
+
34
+ test("thirty-six layouts: editorial (poster, spread, index), eleven experimental type layouts and twenty-two scenes listed first, three old tile layouts kept; each matches its preset", () => {
35
+ assert.deepEqual(Object.keys(LAYOUTS), [...EDITORIAL, ...EXPERIMENTAL, ...SCENES, ...TILES]);
36
+ assert.deepEqual(Object.keys(tpl.fx.X_AGENTVFX.presets).slice(0, EDITORIAL.length + EXPERIMENTAL.length + SCENES.length), [...EDITORIAL, ...EXPERIMENTAL, ...SCENES]);
37
+ for (const n of [...EDITORIAL, ...EXPERIMENTAL, ...SCENES, ...TILES]) {
38
+ assert.deepEqual(checkLayout(LAYOUTS[n]), [], n);
39
+ const expected = { ...defaults(), ...(LAYOUTS[n].params ?? {}), ...layoutToParams(LAYOUTS[n]) };
40
+ assert.deepEqual({ ...defaults(), ...tpl.fx.X_AGENTVFX.presets[n] }, expected, `${n} preset equals its layout`);
41
+ assert.equal(validateScore({ version: 1, template: "collage", preset: n, input: { audio: MUSIC }, durationSec: 3, seed: 5, out: "t" }, lib).ok, true, n);
42
+ }
43
+ assert.equal(defaults().layoutMode, 0, "fixed is the default mode");
44
+ });
45
+
46
+ test("editorial layouts are not tiled partitions: no boxed tiles, overlap and edge bleed, type and live elements, vermilion accent", () => {
47
+ const key = (c) => `${c.x},${c.y},${c.w},${c.h},${c.role}`;
48
+ for (const n of EDITORIAL) {
49
+ const cells = layoutCells(LAYOUTS[n]);
50
+ assert.ok(cells.every((c) => c.frame !== undefined && c.frame !== "tile"), `${n}: every cell sets a frame other than tile`);
51
+ assert.ok(cells.some((c) => c.role === "footage"), `${n} has footage`);
52
+ assert.ok(cells.filter((c) => c.role === "type").length >= 2, `${n} has type cells`);
53
+ assert.ok(cells.some((c) => ["spectrum", "scope", "meter", "crosshair", "rule", "readout"].includes(c.role)), `${n} has an instrument`);
54
+ const area = cells.reduce((s, c) => s + c.w * c.h, 0);
55
+ const mirrored = new Set(cells.map((c) => key({ ...c, x: 32 - c.x - c.w })));
56
+ assert.ok(cells.some((c) => !mirrored.has(key(c))), `${n} is not mirror-symmetric`);
57
+ const overlaps = cells.some((a, i) => cells.some((b, j) => i < j && a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h));
58
+ assert.ok(overlaps, `${n} layers cells over each other (${area} cell units of 576)`);
59
+ const [r, g, b] = LAYOUTS[n].params.accentColor;
60
+ assert.ok(r > 0.8 && g > 0.15 && g < 0.3 && b < 0.15, `${n} accent is vermilion`);
61
+ assert.equal(LAYOUTS[n].params.margin, 0, `${n} runs to the frame edge`);
62
+ }
63
+ assert.ok(layoutCells(LAYOUTS.poster).some((c) => c.role === "footage" && c.w === 32 && c.h === 18), "poster footage is full bleed");
64
+ const idx = layoutCells(LAYOUTS.index);
65
+ assert.ok(idx.find((c) => c.role === "footage").w < 32, "index footage is a small plate");
66
+ });
67
+
68
+ test("checkLayout: tile cells may not overlap, editorial cells may; bad frame, fill, tone, variant, rot, src, bounds and count are rejected", () => {
69
+ const msgs = (l) => checkLayout(l).map((e) => `${e.path} ${e.message}`).join("|");
70
+ assert.deepEqual(checkLayout([cell(0, 0, 16, 18, "footage"), cell(16, 0, 16, 18, "scope")]), []);
71
+ assert.match(msgs([cell(0, 0, 17, 18, "footage"), cell(16, 0, 16, 18, "scope")]), /overlaps/u);
72
+ assert.deepEqual(checkLayout([cell(0, 0, 32, 18, "footage", { frame: "none" }), cell(4, 4, 20, 6, "spectrum", { frame: "none", variant: 1 })]), []);
73
+ assert.deepEqual(checkLayout([cell(0, 0, 32, 18, "footage", { frame: "none" }), cell(4, 4, 20, 6, "scope")]), [], "a tile panel may sit on an editorial cell");
74
+ assert.match(msgs([cell(30, 0, 3, 4, "footage")]), /out of bounds: x \+ w = 33/u);
75
+ assert.match(msgs([cell(0, 17, 4, 2, "footage")]), /out of bounds: y \+ h = 19/u);
76
+ assert.match(msgs([cell(0, 0, 4, 2, "banner")]), /role must be one of footage, spectrum/u);
77
+ assert.match(msgs([cell(0.5, 0, 4, 2, "footage")]), /\[0\]\.x must be an integer/u);
78
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { frame: "dotted" })]), /frame/u);
79
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { fill: "red" })]), /fill/u);
80
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { tone: "red" })]), /tone/u);
81
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { variant: 10 })]), /variant/u);
82
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { rot: 3 })]), /rot/u);
83
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { src: { x: 0.3, y: 0, scale: 1 } })]), /src\.x/u);
84
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { src: { x: 0, y: 0, scale: 1.55 } })]), /src\.scale/u);
85
+ assert.match(msgs([cell(0, 0, 4, 2, "type", { src: { x: 0, y: 0, zoom: 2 } })]), /unknown key/u);
86
+ assert.match(msgs([{ ...cell(0, 0, 4, 2, "footage"), z: 1 }]), /unknown key/u);
87
+ assert.match(msgs([]), /1 to 16 cells/u);
88
+ assert.match(msgs(Array.from({ length: MAX_CELLS + 1 }, (_, i) => cell(i, 0, 1, 1, "hatch", { frame: "none" }))), /1 to 16 cells/u);
89
+ assert.throws(() => layoutToParams([cell(0, 0, 40, 2, "footage")]), /invalid collage layout/u);
90
+ assert.equal(Object.keys(ROLES).length, 14);
91
+ assert.equal(ROLES.typefx, 13);
92
+ assert.equal(ROLES.scene, 14);
93
+ });
94
+
95
+ test("packing: cellN is RRXXYYWWHH, styleN is variant tone fill frame rot, srcN is scale x y; unused slots are 0", () => {
96
+ const c = cell(3, 8, 29, 10, "type", { frame: "none", variant: 4, tone: "ground", fill: "ink", rot: 1, src: { x: 19, y: -1.5, scale: 1.6 } });
97
+ assert.equal(packCell(c), 1103082910);
98
+ assert.equal(packStyle(c), 41210 + 1);
99
+ assert.equal(packSrc(c), 16 * 10000 + 58 * 100 + 17);
100
+ assert.equal(packStyle(cell(0, 0, 1, 1, "footage")), 0, "old cells keep style 0 = tile");
101
+ assert.equal(packSrc(cell(0, 0, 1, 1, "footage")), 0);
102
+ const p = layoutToParams([c]);
103
+ assert.equal(Object.keys(p).length, 48);
104
+ assert.equal(p.cell1, 0);
105
+ assert.equal(p.style15, 0);
106
+ assert.equal(p.src0, packSrc(c));
107
+ const input = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i]));
108
+ for (const [k, v] of Object.entries(p)) assert.ok(v >= input[k].MIN && v <= input[k].MAX, `${k} ${v} inside its input range`);
109
+ });
110
+
111
+ test("text sheet: declared slots, fonts from the engine list, atlas row matches the shader constants, every src window lands on the sheet", () => {
112
+ const text = tpl.fx.X_AGENTVFX.text;
113
+ const keys = text.map((t) => t.key);
114
+ for (const k of ["word", "numeral", "headline", "body", "fields", "spine", "capA", "capB", "atlas"]) assert.ok(keys.includes(k), `slot ${k}`);
115
+ const atlas = text.find((t) => t.key === "atlas");
116
+ const frag = readFileSync(path.join(tpl.dir, "main.frag"), "utf8");
117
+ const konst = (n) => Number(new RegExp(`const float ${n} = ([0-9.]+);`, "u").exec(frag)?.[1]);
118
+ assert.ok(Math.abs(konst("ATLAS_X") - atlas.x) < 1e-4 && Math.abs(konst("ATLAS_Y") - atlas.y) < 1e-4 && konst("ATLAS_PX") === atlas.size, "ATLAS_X/Y/PX equal the atlas text item");
119
+ assert.match(atlas.default, /^0123456789:A.*Z$/u);
120
+ for (const n of EDITORIAL) for (const c of layoutCells(LAYOUTS[n]).filter((q) => q.role === "type")) {
121
+ const s = { x: 0, y: 0, scale: 1, ...c.src };
122
+ assert.ok(s.x >= 0 && s.x < 32 && s.y > -10 && s.y < 18, `${n} type window starts on the sheet`);
123
+ }
124
+ });
125
+
126
+ test("the shipped r7 layout file lists every layout with its packed params and the text sheet", () => {
127
+ const file = path.join(ROOT, "test/fixtures/r7-collage-LAYOUTS.json");
128
+ assert.ok(existsSync(file), "r7-collage-LAYOUTS.json exists");
129
+ const shipped = JSON.parse(readFileSync(file, "utf8"));
130
+ assert.equal(shipped.format, "collage layout v3");
131
+ assert.equal(shipped.maxCells, 16);
132
+ assert.deepEqual(Object.keys(shipped.layouts), Object.keys(LAYOUTS));
133
+ for (const [n, e] of Object.entries(LAYOUTS)) {
134
+ assert.deepEqual(shipped.layouts[n].cells, layoutCells(e), `${n} cells`);
135
+ assert.deepEqual(shipped.layouts[n].params, { ...(e.params ?? {}), ...layoutToParams(e) }, `${n} params`);
136
+ }
137
+ assert.deepEqual(Object.keys(shipped.textSheet), tpl.fx.X_AGENTVFX.text.map((t) => t.key));
138
+ });
139
+
140
+ async function frames(params, keep, preset) {
141
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
142
+ const v = validateScore({ version: 1, template: "collage", ...(preset ? { preset } : {}), input: { audio: MUSIC }, durationSec: 3, seed: 5, out: "t", params: { construction: false, grain: 0, gridDots: 0, ...params } }, lib);
143
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
144
+ const kept = new Map();
145
+ await renderScore(v.value, lib, { outDir: path.join(tmp, `l-${runs++}`), width: W, height: H, features, replay: false, onFrame: (f, a) => { if (keep === "all" || keep.includes(f)) kept.set(f, Buffer.from(a.rgba)); } });
146
+ return { kept, features };
147
+ }
148
+
149
+ const px = (rgba, x, y) => { const i = (y * W + x) * 4; return [rgba[i], rgba[i + 1], rgba[i + 2]]; };
150
+ const isRed = ([r, g, b]) => r > 150 && g < 110 && b < 90;
151
+ const isInk = ([r, g, b]) => r + g + b < 200;
152
+
153
+ /** Numeral window of the index layout in pixels (grid cell 1,2,9,13 on a 20 px unit), clear of the level hairline rows at the ends. */
154
+ function numeralStats(rgba) {
155
+ let red = 0;
156
+ const glyph = new Set();
157
+ for (let y = 50; y < 290; y++) for (let x = 24; x < 196; x++) {
158
+ const p = px(rgba, x, y);
159
+ if (isRed(p)) red += 1;
160
+ if (isRed(p) || isInk(p)) glyph.add(y * W + x);
161
+ }
162
+ return { red, glyph };
163
+ }
164
+
165
+ test("index: the numeral keeps its shape across all frames while its vermilion fill follows the rms (type reacts, never moves)", async () => {
166
+ const { kept } = await frames({ footageSat: 1 }, "all", "index");
167
+ const stats = [...kept.keys()].filter((f) => f >= 3).map((f) => numeralStats(kept.get(f)));
168
+ const reds = stats.map((s) => s.red);
169
+ const area = Math.max(...stats.map((s) => s.glyph.size));
170
+ assert.ok(area > 3000, `numeral covers pixels: ${area}`);
171
+ assert.ok(Math.max(...reds) > 3 * Math.min(...reds) + 400, `fill range across frames: ${Math.min(...reds)}..${Math.max(...reds)}`);
172
+ const base = stats[0].glyph;
173
+ for (const s of stats) {
174
+ let both = 0;
175
+ for (const k of s.glyph) if (base.has(k)) both += 1;
176
+ assert.ok(both / Math.max(s.glyph.size, base.size) > 0.8, `glyph shape overlap ${(both / Math.max(s.glyph.size, base.size)).toFixed(2)}`);
177
+ }
178
+ });
179
+
180
+ test("index numeral fill on the bass attacks on the onset frame and falls by half within 4 frames", async () => {
181
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
182
+ 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);
183
+ assert.ok(onsets.length >= 1, "a clean strong onset exists");
184
+ const keep = onsets.flatMap((o) => [o - 1, o, o + 4]);
185
+ const { kept } = await frames({ style0: 10010 }, keep, "index");
186
+ for (const o of onsets) {
187
+ const [a, b, c] = [o - 1, o, o + 4].map((f) => numeralStats(kept.get(f)).red);
188
+ assert.ok(b > a * 1.3 + 50, `onset ${o}: fill ${a} -> ${b}`);
189
+ assert.ok(c <= b * 0.5 + 50, `onset ${o}: fill ${b} -> ${c} after 4 frames`);
190
+ }
191
+ });
192
+
193
+ test("spread: the scope line crossing both pages changes every frame while the page edge stays", async () => {
194
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
195
+ const beat = features.meta.beats.find((b) => b >= 6 && b < 80);
196
+ const { kept } = await frames({}, [beat - 1, beat], "spread");
197
+ const a = kept.get(beat - 1);
198
+ const b = kept.get(beat);
199
+ let diff = 0;
200
+ let n = 0;
201
+ for (let y = 300; y < 358; y++) for (let x = 0; x < W; x++) { n += 1; diff += Math.abs(px(a, x, y)[0] - px(b, x, y)[0]) > 40 ? 1 : 0; }
202
+ assert.ok(diff / n > 0.004, `scope strip changed across a beat: ${(diff / n).toFixed(4)}`);
203
+ for (const y of [4, 8, 12]) assert.ok(px(a, 318, y)[0] < 80 && px(a, 330, y)[0] > 190 && px(b, 318, y)[0] < 80 && px(b, 330, y)[0] > 190, `page edge at x=320 holds (y ${y})`);
204
+ });
205
+
206
+ const brightAt = (rgba, x, y) => (x < 0 || y < 0 || x >= W || y >= H ? 0 : (rgba[(y * W + x) * 4] + rgba[(y * W + x) * 4 + 1] + rgba[(y * W + x) * 4 + 2]) / 3);
207
+
208
+ /** Pixel rectangle of a tile cell inside the frame line (gutter and margin as the shader sets them). */
209
+ function box(c) {
210
+ const U = H / 1080;
211
+ const m = 24 * U;
212
+ const g = 10 * U * 0.5;
213
+ const un = [(W - 2 * m) / 32, (H - 2 * m) / 18];
214
+ return { x0: m + c.x * un[0] + g, y0: m + c.y * un[1] + g, x1: m + (c.x + c.w) * un[0] - g, y1: m + (c.y + c.h) * un[1] - g };
215
+ }
216
+
217
+ function outlineInk(rgba, c) {
218
+ const b = box(c);
219
+ let on = 0;
220
+ let n = 0;
221
+ const hit = (x, y) => Math.max(brightAt(rgba, x, y), brightAt(rgba, x, y - 1), brightAt(rgba, x - 1, y)) > 90;
222
+ for (let x = Math.ceil(b.x0) + 6; x < b.x1 - 6; x++) for (const y of [b.y0, b.y1]) { n += 1; on += hit(x, Math.round(y)) ? 1 : 0; }
223
+ for (let y = Math.ceil(b.y0) + 6; y < b.y1 - 6; y++) for (const x of [b.x0, b.x1]) { n += 1; on += hit(Math.round(x), y) ? 1 : 0; }
224
+ return on / n;
225
+ }
226
+
227
+ test("old tile layouts still render: every panel outline holds across a beat (fixed), and reshuffle moves them (control)", async () => {
228
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
229
+ const beat = features.meta.beats.find((b) => b >= 4 && b < 85);
230
+ const cells = layoutCells(LAYOUTS["wide-hero"]);
231
+ const fixed = (await frames({ cutMode: 2 }, [beat - 1, beat], "wide-hero")).kept;
232
+ for (const f of [beat - 1, beat]) assert.ok(Math.min(...cells.map((c) => outlineInk(fixed.get(f), c))) > 0.9, `wide-hero outlines hold at frame ${f}`);
233
+ const shuffled = (await frames({ layoutMode: 1, cutMode: 2 }, [beat - 1, beat])).kept;
234
+ const worst = Math.min(...[beat - 1, beat].flatMap((f) => cells.map((c) => outlineInk(shuffled.get(f), c))));
235
+ assert.ok(worst < 0.6, `some outline of the wide-hero grid is missing after a reshuffle: ${worst.toFixed(2)}`);
236
+ });
@@ -0,0 +1,305 @@
1
+ // Collage scenes (role 14): four mixed-material compositions (specimen, wiring, field-cut, pattern-sheet), each with its own
2
+ // text set (text item `set` + template input textSet), palette, fonts, image treatment and beat/onset behaviour. Layout data,
3
+ // the text-set filter and measured behaviour on rendered frames: the music event frame is the attack, 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, validateTemplate } from "../engine/core/validate.mjs";
12
+ import { createFlashAuditor } from "../engine/core/flash.mjs";
13
+ import { renderScore } from "../engine/node/render.mjs";
14
+ import { analyzeAudio } from "../engine/node/audio.mjs";
15
+ import { checkLayout, layoutCells, layoutToParams, packCell, ROLES } from "../templates/brutal/collage/layout.mjs";
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const MUSIC = "test-media/beat-132.mp3";
19
+ const lib = loadLibrary();
20
+ const tpl = lib.get("collage");
21
+ const LAYOUTS = JSON.parse(readFileSync(path.join(tpl.dir, "layouts.json"), "utf8"));
22
+ const SCENES = { specimen: 1, wiring: 2, "field-cut": 3, "pattern-sheet": 4, anatomy: 5, deck: 6, bootlog: 7, tunnel: 8, runner: 9, ascii: 0, "cube-wall": 10, "vortex-count": 11, "sticker-slap": 12, "ribbon-rays": 13, "slab-shear": 14, "frame-loop-dial": 15, "tilt-tiles": 16, "marquee-stack": 17, "lens-wrap": 18, "liquid-type": 19, "panel-grid": 20, "mirror-track": 21 };
23
+ const by_ = (text, key) => text.find((t) => t.key === key);
24
+ const tmp = mkdtempSync(path.join(tmpdir(), "agentvfx-collage-scenes-"));
25
+ process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
26
+ const W = 640;
27
+ const H = 360;
28
+ let runs = 0;
29
+
30
+ test("scene role: role 14 packs into the cell int range and each preset is one full-frame scene cell equal to its fx.json preset", () => {
31
+ assert.equal(ROLES.scene, 14);
32
+ const cell = { x: 0, y: 0, w: 32, h: 18, role: "scene", frame: "none", variant: 3 };
33
+ assert.deepEqual(checkLayout([cell]), []);
34
+ assert.equal(packCell(cell), 1400003218);
35
+ const defaults = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i.DEFAULT]));
36
+ const input = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i]));
37
+ for (const [name, variant] of Object.entries(SCENES)) {
38
+ const e = LAYOUTS[name];
39
+ assert.deepEqual(checkLayout(e), [], name);
40
+ const cells = layoutCells(e);
41
+ assert.equal(cells.length, 1, `${name} is a single scene cell`);
42
+ assert.deepEqual([cells[0].role, cells[0].variant, cells[0].rot ?? 0, cells[0].w, cells[0].h], ["scene", variant % 10, Math.floor(variant / 10), 32, 18], `${name}: scene id = variant + 10 * rot`);
43
+ const p = layoutToParams(e);
44
+ for (const [k, v] of Object.entries(p)) assert.ok(v >= input[k].MIN && v <= input[k].MAX, `${name} ${k} ${v} inside its input range`);
45
+ assert.deepEqual({ ...defaults, ...tpl.fx.X_AGENTVFX.presets[name] }, { ...defaults, ...e.params, ...p }, `${name} preset equals its layout`);
46
+ assert.ok(e.description.length > 120, `${name} describes itself`);
47
+ }
48
+ });
49
+
50
+ test("diversity: four different palettes (ground colours), four text sets, no vermilion in more than one, beats-only cuts", () => {
51
+ const defaults = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i.DEFAULT]));
52
+ const palettes = Object.keys(SCENES).map((n) => ["groundColor", "inkColor", "accentColor", "tintA", "tintB"].flatMap((k) => ({ ...defaults, ...LAYOUTS[n].params })[k].slice(0, 3)));
53
+ const dist = (a, b) => Math.hypot(...a.map((v, i) => v - b[i]));
54
+ for (let a = 0; a < palettes.length; a++) for (let b = a + 1; b < palettes.length; b++) assert.ok(dist(palettes[a], palettes[b]) > 0.8, `palette ${a} vs ${b}`);
55
+ assert.deepEqual(Object.keys(SCENES).map((n) => LAYOUTS[n].params.textSet).sort((a, b) => a - b), Array.from({ length: 22 }, (_, i) => i + 1));
56
+ assert.equal(new Set(Object.values(SCENES)).size, 22, "twenty-two different scenes");
57
+ const vermilion = ([r, g, b]) => r > 0.8 && g > 0.15 && g < 0.3 && b < 0.15;
58
+ const colours = Object.keys(SCENES).flatMap((n) => Object.entries(LAYOUTS[n].params).filter(([k]) => /Color$|^tint/u.test(k)).map(([, v]) => v));
59
+ assert.ok(colours.filter(vermilion).length <= 1, "vermilion appears in at most one preset");
60
+ for (const n of Object.keys(SCENES)) assert.equal(LAYOUTS[n].params.cutMode, 0, `${n} cuts on beats only`);
61
+ });
62
+
63
+ test("text sets: items with `set` are only in 1..16, every preset's textSet has items, base items keep set 0, bad set values are rejected", () => {
64
+ const text = tpl.fx.X_AGENTVFX.text;
65
+ const base = text.filter((t) => t.set === undefined);
66
+ assert.ok(base.length >= 20, "the older sheet is untouched");
67
+ for (let s = 1; s <= 22; s++) assert.ok(text.some((t) => t.set === s), `set ${s} has items`);
68
+ assert.ok(text.every((t) => t.set === undefined || (t.set >= 1 && t.set <= 22)));
69
+ assert.equal(by_(text, "tlF").family, "Rockwell");
70
+ assert.equal(by_(text, "lnWord").family, "Didot");
71
+ assert.equal(by_(text, "lqWord").family, "Bodoni 72");
72
+ assert.equal(by_(text, "mrWord").family, "Phosphate");
73
+ assert.equal(by_(text, "cwL0").family, "Big Shoulders Display");
74
+ assert.equal(by_(text, "vxLine").family, "Bebas Neue");
75
+ assert.equal(by_(text, "stA").family, "Impact");
76
+ assert.equal(by_(text, "fdLoop").family, "Georgia");
77
+ assert.equal(by_(text, "blLog").reveal, "type", "the boot log is typed by the engine");
78
+ assert.equal(by_(text, "rwA").family, "Arial Black");
79
+ assert.equal(by_(text, "tnWord").family, "Monument Extended");
80
+ assert.equal(by_(text, "dkA").box, "tab");
81
+ const by = Object.fromEntries(text.map((t) => [t.key, t]));
82
+ assert.equal(by.spWord.family, "Pretendard");
83
+ assert.equal(by.spWord.weight, 200);
84
+ assert.equal(by.spCap.family, "MesloLGS NF");
85
+ assert.equal(by.wL0.family, "Monument Extended");
86
+ assert.equal(by.fcWord.weight, 900);
87
+ assert.match(by.fcKo.default, /[가-힣]/u);
88
+ assert.equal(by.ptWord.family, undefined, "pattern-sheet label uses the generic heavy grotesk");
89
+ assert.equal(by.ptQL.font, "serif");
90
+ const input = Object.fromEntries(tpl.fx.INPUTS.map((i) => [i.NAME, i]));
91
+ assert.deepEqual([input.textSet.MIN, input.textSet.MAX, input.textSet.DEFAULT], [0, 31, 0]);
92
+ const ok = validateScore({ version: 1, template: "collage", preset: "wiring", input: { audio: MUSIC }, durationSec: 3, seed: 5, out: "t", labels: { wTitle: "HALL" } }, lib);
93
+ assert.equal(ok.ok, true, JSON.stringify(ok.errors));
94
+ const withSet = (set) => ({ ...tpl.fx, X_AGENTVFX: { ...tpl.fx.X_AGENTVFX, text: [{ key: "a", default: "A", x: 0, y: 0, size: 0.1, set }] } });
95
+ assert.deepEqual(validateTemplate(withSet(2), "collage").errors, []);
96
+ for (const bad of [32, 1.5, -1, "1"]) assert.ok(validateTemplate(withSet(bad), "collage").errors.some((e) => e.path === "X_AGENTVFX.text[0].set"), String(bad));
97
+ });
98
+
99
+ async function frames(preset, keep, { audit = false, dur = 3, audio = MUSIC } = {}) {
100
+ const features = await analyzeAudio(path.resolve(ROOT, audio), { fps: 30 });
101
+ const v = validateScore({ version: 1, template: "collage", preset, input: { audio }, durationSec: dur, seed: 5, out: "t", params: { grain: 0 } }, lib);
102
+ assert.equal(v.ok, true, JSON.stringify(v.errors));
103
+ const kept = new Map();
104
+ const auditor = audit ? createFlashAuditor({ fps: 30, width: W, height: H }) : null;
105
+ await renderScore(v.value, lib, { outDir: path.join(tmp, `t-${runs++}`), width: W, height: H, features, replay: false, onFrame: (f, a) => {
106
+ if (keep.includes(f)) kept.set(f, Buffer.from(a.rgba));
107
+ auditor?.push({ data: a.rgba, width: W, height: H });
108
+ } });
109
+ return { kept, features, flash: auditor?.finish() };
110
+ }
111
+ const clean = (events, lo, hi) => events.filter((e) => e >= lo && e < hi && !events.some((p) => p !== e && p > e - 5 && p < e + 6));
112
+ const px = (rgba, x, y) => { const i = (y * W + x) * 4; return [rgba[i], rgba[i + 1], rgba[i + 2]]; };
113
+ 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; };
114
+ const centroid = (rgba, test) => { let n = 0; let sx = 0; let sy = 0; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) if (test(px(rgba, x, y))) { n += 1; sx += x; sy += y; } return { n, x: sx / Math.max(n, 1), y: sy / Math.max(n, 1) }; };
115
+
116
+ test("specimen: the black square and semicircle snap to a new place on the beat frame and then hold", async () => {
117
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
118
+ const beats = clean(features.meta.beats, 8, 85).slice(0, 2);
119
+ assert.ok(beats.length >= 2, `clean beats exist: ${beats}`);
120
+ const { kept } = await frames("specimen", beats.flatMap((b) => [b - 1, b, b + 4]));
121
+ const black = ([r, g, b]) => r < 30 && g < 30 && b < 40;
122
+ for (const b of beats) {
123
+ const [before, at, later] = [b - 1, b, b + 4].map((f) => centroid(kept.get(f), black));
124
+ assert.ok(Math.hypot(at.x - before.x, at.y - before.y) > 30, `beat ${b}: the pair moved ${Math.hypot(at.x - before.x, at.y - before.y).toFixed(1)} px on the beat frame`);
125
+ assert.ok(Math.hypot(later.x - at.x, later.y - at.y) < 3, `beat ${b}: the pair holds its place until the next beat`);
126
+ }
127
+ });
128
+
129
+ test("wiring: the crimson glyph is at full brightness on the onset frame and dim 5 frames later", async () => {
130
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
131
+ const onsets = clean(features.meta.onsets, 8, 85).slice(0, 3);
132
+ assert.ok(onsets.length >= 2, `clean onsets exist: ${onsets}`);
133
+ const { kept } = await frames("wiring", onsets.flatMap((o) => [o, o + 5]));
134
+ const bright = ([r, g, b]) => r > 200 && g < 60 && b < 90;
135
+ for (const o of onsets) {
136
+ const [at, after] = [o, o + 5].map((f) => count(kept.get(f), bright, 140, 14, 185, 56));
137
+ assert.ok(at > 120, `onset ${o}: bright glyph pixels ${at}`);
138
+ assert.ok(after < at * 0.1, `onset ${o}: ${at} -> ${after} after 5 frames`);
139
+ }
140
+ });
141
+
142
+ test("field-cut: the field cuts on a beat frame (lime, amber, ultramarine in turn) and stays under the flash limit", async () => {
143
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
144
+ const [b0, b1, , b3] = features.meta.beats;
145
+ assert.ok(b3 < 100 && b0 > 3, `four beats early in the clip: ${features.meta.beats.slice(0, 4)}`);
146
+ const { kept, flash } = await frames("field-cut", [3, b1 - 1, b1, b3 + 1], { audit: true, dur: 4, audio: MUSIC });
147
+ const lime = ([r, g, b]) => g > 220 && r > 150 && b < 90;
148
+ const amber = ([r, g, b]) => r > 230 && g > 110 && g < 175 && b < 70;
149
+ const blue = ([r, g, b]) => b > 200 && r < 70 && g < 70;
150
+ const share = (f, t) => count(kept.get(f), t) / (W * H);
151
+ assert.ok(share(3, lime) > 0.4, `before the first cut the lime field shows (${share(3, lime).toFixed(2)})`);
152
+ assert.ok(share(b1 - 1, lime) > 0.4 && share(b1 - 1, amber) < 0.05, "the frame before the second beat is still lime");
153
+ assert.ok(share(b1, amber) > 0.15 && share(b1, lime) < 0.05, `the beat frame is the amber field (${share(b1, amber).toFixed(2)})`);
154
+ assert.ok(share(b3 + 1, blue) > 0.3, `two beats later the ultramarine field shows (${share(b3 + 1, blue).toFixed(2)})`);
155
+ assert.equal(flash.exceeds, false, `flash gate: ${JSON.stringify(flash.worstWindow)} red ${flash.redFlashes}`);
156
+ });
157
+
158
+ test("pattern-sheet: the QR stamps re-seed on the beat frame and hold between beats; the green border stays", async () => {
159
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
160
+ const beats = clean(features.meta.beats, 8, 85).slice(0, 2);
161
+ assert.ok(beats.length >= 2, `clean beats exist: ${beats}`);
162
+ const { kept } = await frames("pattern-sheet", beats.flatMap((b) => [b - 1, b, b + 3]));
163
+ const region = (rgba) => { const out = []; for (let y = 30; y < 92; y += 2) for (let x = 90; x < 152; x += 2) out.push(px(rgba, x, y)[0] < 100 ? 1 : 0); return out; };
164
+ const diff = (a, b) => region(a).filter((v, i) => v !== region(b)[i]).length;
165
+ for (const b of beats) {
166
+ assert.ok(diff(kept.get(b - 1), kept.get(b)) > 80, `beat ${b}: the stamp changed on the beat frame (${diff(kept.get(b - 1), kept.get(b))} cells)`);
167
+ assert.ok(diff(kept.get(b), kept.get(b + 3)) < 6, `beat ${b}: the stamp holds after the beat`);
168
+ const [r, g, bl] = px(kept.get(b), 8, 180);
169
+ assert.ok(g > 150 && r < 40 && bl < 120, "the signal-green border is there");
170
+ }
171
+ });
172
+
173
+ /** Pixels of the region whose colour changed by more than 40 in any channel between two frames. */
174
+ const changed = (a, b, x0, y0, x1, y1) => { let n = 0; for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) { const p = px(a, x, y); const q = px(b, x, y); if (Math.max(Math.abs(p[0] - q[0]), Math.abs(p[1] - q[1]), Math.abs(p[2] - q[2])) > 40) n += 1; } return n; };
175
+ const cleanBeats = async (n = 2) => {
176
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
177
+ const beats = clean(features.meta.beats, 8, 85).slice(0, n);
178
+ assert.ok(beats.length >= n, `clean beats exist: ${beats}`);
179
+ return beats;
180
+ };
181
+
182
+ test("bootlog: the progress bar fills three more segments on the beat frame", async () => {
183
+ const beats = await cleanBeats();
184
+ const { kept } = await frames("bootlog", beats.flatMap((b) => [b - 1, b]));
185
+ const lit = ([, g]) => g > 150;
186
+ for (const b of beats) {
187
+ const [before, at] = [b - 1, b].map((f) => count(kept.get(f), lit, 86, 311, 345, 329));
188
+ assert.ok(at > before + 100, `beat ${b}: bright progress pixels ${before} -> ${at}`);
189
+ }
190
+ });
191
+
192
+ test("tunnel: the rings jump a step deeper on the beat frame and hold until the next beat", async () => {
193
+ const beats = await cleanBeats();
194
+ const { kept } = await frames("tunnel", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
195
+ for (const b of beats) {
196
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 0, W, 60) > 0.15 * W * 60, `beat ${b}: the checker above the window changed on the beat frame`);
197
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 0, W, 60) < 0.02 * W * 60, `beat ${b}: the tunnel holds between beats`);
198
+ }
199
+ });
200
+
201
+ test("runner: the bar word swaps on the beat frame and holds", async () => {
202
+ const beats = await cleanBeats();
203
+ const { kept } = await frames("runner", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
204
+ for (const b of beats) {
205
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 217, W, 285) > 2500, `beat ${b}: the bar changed on the beat frame`);
206
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 217, W, 285) < 150, `beat ${b}: the bar holds between beats`);
207
+ }
208
+ });
209
+
210
+ /** Beats that start a new even beat count (the scenes that act every second beat), clean of other events. */
211
+ const evenBeats = async () => {
212
+ const features = await analyzeAudio(path.resolve(ROOT, MUSIC), { fps: 30 });
213
+ const cleanSet = new Set(clean(features.meta.beats, 8, 85));
214
+ const out = features.meta.beats.filter((b, i) => i % 2 === 1 && cleanSet.has(b));
215
+ assert.ok(out.length >= 1, `an even clean beat exists: ${features.meta.beats}`);
216
+ return out.slice(0, 2);
217
+ };
218
+
219
+ test("cube-wall: the cubes turn mid-way on the beat that starts a new pair of beats and are at rest 4 frames later", async () => {
220
+ const beats = await evenBeats();
221
+ const { kept } = await frames("cube-wall", beats.flatMap((b) => [b - 1, b + 1, b + 5, b + 6]));
222
+ for (const b of beats) {
223
+ const turn = changed(kept.get(b - 1), kept.get(b + 1), 180, 20, 460, 340);
224
+ const hold = changed(kept.get(b + 5), kept.get(b + 6), 180, 20, 460, 340);
225
+ assert.ok(turn > 4000, `beat ${b}: the wall changed while the cubes turn (${turn})`);
226
+ assert.ok(hold < turn * 0.3, `beat ${b}: the wall is nearly still once the turn is done (${hold} vs ${turn})`);
227
+ }
228
+ });
229
+
230
+ test("sticker-slap: a new sticker lands on the beat that starts a new pair of beats", async () => {
231
+ const beats = await evenBeats();
232
+ const { kept } = await frames("sticker-slap", beats.flatMap((b) => [b - 1, b + 1, b + 5, b + 6]));
233
+ for (const b of beats) {
234
+ const slap = changed(kept.get(b - 1), kept.get(b + 1), 0, 0, W, H);
235
+ const hold = changed(kept.get(b + 5), kept.get(b + 6), 0, 0, W, H);
236
+ assert.ok(slap > 3000, `beat ${b}: the frame changed when the sticker slapped on (${slap})`);
237
+ assert.ok(hold < slap * 0.5, `beat ${b}: the pile is calmer after the slap (${hold} vs ${slap})`);
238
+ }
239
+ });
240
+
241
+ test("slab-shear: the slabs shift on the beat frame and hold between beats", async () => {
242
+ const beats = await cleanBeats();
243
+ const { kept } = await frames("slab-shear", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
244
+ for (const b of beats) {
245
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 0, W, H) > 3000, `beat ${b}: the slabs changed on the beat frame`);
246
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 0, W, H) < 600, `beat ${b}: the slabs hold between beats`);
247
+ }
248
+ });
249
+
250
+ test("frame-loop-dial: the split-flap digits change on the beat and the border text jumps ahead on an onset", async () => {
251
+ const beats = await cleanBeats();
252
+ const { kept } = await frames("frame-loop-dial", beats.flatMap((b) => [b - 1, b + 4]));
253
+ for (const b of beats) assert.ok(changed(kept.get(b - 1), kept.get(b + 4), 400, 130, 536, 230) > 150, `beat ${b}: the flap digits changed`);
254
+ });
255
+
256
+ test("marquee-stack: every row jumps on the beat frame and holds between beats", async () => {
257
+ const beats = await cleanBeats();
258
+ const { kept } = await frames("marquee-stack", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
259
+ for (const b of beats) {
260
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 20, 380, 280) > 5000, `beat ${b}: the rows jumped on the beat frame`);
261
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 20, 380, 280) < 800, `beat ${b}: the rows hold between beats`);
262
+ }
263
+ });
264
+
265
+ test("lens-wrap: the floor steps toward the viewer on the beat frame and holds", async () => {
266
+ const beats = await cleanBeats();
267
+ const { kept } = await frames("lens-wrap", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
268
+ for (const b of beats) {
269
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 280, W, 330) > 150, `beat ${b}: the floor lines moved on the beat frame`);
270
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 280, W, 330) < 100, `beat ${b}: the floor holds between beats`);
271
+ }
272
+ });
273
+
274
+ test("liquid-type: the ripple phase steps on the beat frame and holds", async () => {
275
+ const beats = await cleanBeats();
276
+ const { kept } = await frames("liquid-type", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
277
+ for (const b of beats) {
278
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 0, W, 240) > 1500, `beat ${b}: the word moved on the beat frame`);
279
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 0, W, 240) < 500, `beat ${b}: the word holds between beats`);
280
+ }
281
+ });
282
+
283
+ test("tilt-tiles and mirror-track: the rows or targets step on a beat and hold between beats", async () => {
284
+ const beats = await evenBeats();
285
+ const { kept } = await frames("tilt-tiles", beats.flatMap((b) => [b - 1, b, b + 3, b + 4]));
286
+ for (const b of beats) {
287
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 0, 0, W, H) > 8000, `tilt-tiles beat ${b}: the rows jumped`);
288
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 4), 0, 0, W, H) < 2500, `tilt-tiles beat ${b}: the rows hold`);
289
+ }
290
+ const m = await cleanBeats();
291
+ const mk = (await frames("mirror-track", m.flatMap((b) => [b - 1, b, b + 3, b + 4]))).kept;
292
+ for (const b of m) {
293
+ assert.ok(changed(mk.get(b - 1), mk.get(b), 100, 20, 560, 280) > 300, `mirror-track beat ${b}: the targets jumped`);
294
+ assert.ok(changed(mk.get(b + 3), mk.get(b + 4), 100, 20, 560, 280) < 150, `mirror-track beat ${b}: the targets hold`);
295
+ }
296
+ });
297
+
298
+ test("anatomy: the red scan band jumps on the beat frame and the wireframe settles within 3 frames", async () => {
299
+ const beats = await cleanBeats();
300
+ const { kept } = await frames("anatomy", beats.flatMap((b) => [b - 1, b, b + 3, b + 5]));
301
+ for (const b of beats) {
302
+ assert.ok(changed(kept.get(b - 1), kept.get(b), 190, 20, 470, 340) > 1500, `beat ${b}: the panel changed on the beat frame`);
303
+ assert.ok(changed(kept.get(b + 3), kept.get(b + 5), 190, 20, 470, 340) < 300, `beat ${b}: the panel is settled after 3 frames`);
304
+ }
305
+ });