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