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