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,397 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { loadLibrary } from "../engine/core/templates.mjs";
|
|
9
|
+
import { validateScore } from "../engine/core/validate.mjs";
|
|
10
|
+
import { APP_ORIGIN, ChromeSession, findChrome, stageFlags } from "../engine/node/chrome.mjs";
|
|
11
|
+
import { decodePng } from "../engine/node/png.mjs";
|
|
12
|
+
import { renderScore } from "../engine/node/render.mjs";
|
|
13
|
+
|
|
14
|
+
const root = fileURLToPath(new URL("../", import.meta.url));
|
|
15
|
+
const exportScript = path.join(root, "scripts/export-web.mjs");
|
|
16
|
+
const URL_RE = /https?:\/\//iu;
|
|
17
|
+
|
|
18
|
+
function runExport(args) {
|
|
19
|
+
return spawnSync(process.execPath, [exportScript, ...args], { encoding: "utf8" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertNoExternalUrl(dir) {
|
|
23
|
+
for (const name of ["agentvfx.js", "templates.js", "demo.html"]) {
|
|
24
|
+
const text = readFileSync(path.join(dir, name), "utf8");
|
|
25
|
+
assert.equal(URL_RE.test(text), false, `${name} contains an external URL`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function meanAbs(a, b) {
|
|
30
|
+
const width = a.width ?? b.width;
|
|
31
|
+
const height = a.height ?? b.height;
|
|
32
|
+
const ap = a.pixels ?? a;
|
|
33
|
+
const bp = b.pixels ?? b;
|
|
34
|
+
const ac = a.channels ?? 4;
|
|
35
|
+
const bc = b.channels ?? 4;
|
|
36
|
+
let sum = 0;
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (let y = 0; y < height; y++) {
|
|
39
|
+
for (let x = 0; x < width; x++) {
|
|
40
|
+
const ia = (y * width + x) * ac;
|
|
41
|
+
const ib = (y * width + x) * bc;
|
|
42
|
+
sum += Math.abs(ap[ia] - bp[ib]) + Math.abs(ap[ia + 1] - bp[ib + 1]) + Math.abs(ap[ia + 2] - bp[ib + 2]);
|
|
43
|
+
count += 3;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return sum / count;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function meanRgb(image) {
|
|
50
|
+
const pixels = image.pixels ?? image;
|
|
51
|
+
const channels = image.channels ?? 4;
|
|
52
|
+
const n = image.width && image.height ? image.width * image.height : pixels.length / channels;
|
|
53
|
+
let sum = 0;
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
const o = i * channels;
|
|
56
|
+
sum += pixels[o] + pixels[o + 1] + pixels[o + 2];
|
|
57
|
+
}
|
|
58
|
+
return sum / (n * 3);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function shot(session, width, height) {
|
|
62
|
+
const png = await session.send("Page.captureScreenshot", {
|
|
63
|
+
format: "png",
|
|
64
|
+
fromSurface: true,
|
|
65
|
+
captureBeyondViewport: false,
|
|
66
|
+
clip: { x: 0, y: 0, width, height, scale: 1 },
|
|
67
|
+
});
|
|
68
|
+
const image = decodePng(Buffer.from(png.data, "base64"));
|
|
69
|
+
assert.equal(image.width, width, `screenshot width ${image.width}`);
|
|
70
|
+
assert.equal(image.height, height, `screenshot height ${image.height}`);
|
|
71
|
+
return image;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function withChrome(dir, { width, height, reduced = false }, fn) {
|
|
75
|
+
const chrome = findChrome();
|
|
76
|
+
assert.ok(chrome, "Chrome/Chromium not found");
|
|
77
|
+
const profileDir = mkdtempSync(path.join(tmpdir(), "agentvfx-web-"));
|
|
78
|
+
const routes = (pathname) => {
|
|
79
|
+
const rel = decodeURIComponent(pathname).replace(/^\/+/u, "");
|
|
80
|
+
if (!rel || rel.includes("..")) return null;
|
|
81
|
+
const file = path.join(dir, rel);
|
|
82
|
+
if (!existsSync(file) || !statSync(file).isFile()) return null;
|
|
83
|
+
return file;
|
|
84
|
+
};
|
|
85
|
+
const session = await ChromeSession.launch({
|
|
86
|
+
chrome,
|
|
87
|
+
flags: stageFlags({ width, height, rung: "gpu" }),
|
|
88
|
+
profileDir,
|
|
89
|
+
routes,
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
await session.openPage();
|
|
93
|
+
await session.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 1, mobile: false });
|
|
94
|
+
if (reduced) {
|
|
95
|
+
await session.send("Emulation.setEmulatedMedia", { features: [{ name: "prefers-reduced-motion", value: "reduce" }] });
|
|
96
|
+
}
|
|
97
|
+
return await fn(session);
|
|
98
|
+
} finally {
|
|
99
|
+
await session.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function open(session, page, readyExpr) {
|
|
104
|
+
await session.navigate(`${APP_ORIGIN}/${page}`);
|
|
105
|
+
const deadline = Date.now() + 20000;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const ready = await session.evaluate(readyExpr).catch(() => false);
|
|
108
|
+
if (ready) return;
|
|
109
|
+
if (Date.now() > deadline) {
|
|
110
|
+
const logs = session.logs.join("\n");
|
|
111
|
+
throw new Error(`page did not become ready: ${page}\n${logs}`);
|
|
112
|
+
}
|
|
113
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PROBE = `<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:#fff}canvas{display:block;width:320px;height:180px}</style></head>
|
|
118
|
+
<body><canvas id="c"></canvas><canvas id="bad"></canvas>
|
|
119
|
+
<script type="module">
|
|
120
|
+
import { AgentVfx } from "./agentvfx.js";
|
|
121
|
+
import templates from "./templates.js";
|
|
122
|
+
window.__fx = await AgentVfx.mount(document.getElementById("c"), { templates, template: "test-gradient", params: { speed: 0 }, motion: "always", dpr: 1 });
|
|
123
|
+
const broken = { ...templates, broken: { fx: { DESCRIPTION: "broken", INPUTS: [], X_AGENTVFX: { id: "broken", family: "_test", input: "generative", spdx: "MIT", flash: "none", presets: {} } }, passes: [{ target: null, persistent: false, src: "void main() { this is not glsl }" }] } };
|
|
124
|
+
try { await AgentVfx.mount(document.getElementById("bad"), { templates: broken, template: "broken", motion: "always", dpr: 1 }); window.__badThrew = false; } catch (e) { window.__badThrew = true; }
|
|
125
|
+
window.__litReady = true;
|
|
126
|
+
</script></body></html>`;
|
|
127
|
+
|
|
128
|
+
test("trigger() hits a still hero, a broken shader stays contained, and the context recovers after loss", async () => {
|
|
129
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-probe-"));
|
|
130
|
+
try {
|
|
131
|
+
const exported = runExport(["test-gradient", "test-strobe", "--out", dir]);
|
|
132
|
+
assert.equal(exported.status, 0, exported.stderr || exported.stdout);
|
|
133
|
+
writeFileSync(path.join(dir, "probe.html"), PROBE);
|
|
134
|
+
await withChrome(dir, { width: 320, height: 360 }, async (session) => {
|
|
135
|
+
await open(session, "probe.html", "window.__litReady === true");
|
|
136
|
+
const a = await shot(session, 320, 180);
|
|
137
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
138
|
+
const b = await shot(session, 320, 180);
|
|
139
|
+
assert.ok(meanAbs(a, b) < 1, "the still hero (speed 0) should not change");
|
|
140
|
+
await session.evaluate("window.__fx.trigger('test-strobe', { preset: 'hold', durationMs: 600 })");
|
|
141
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
142
|
+
const c = await shot(session, 320, 180);
|
|
143
|
+
assert.ok(meanAbs(b, c) > 20, `trigger() did not change the next capture (${meanAbs(b, c).toFixed(2)})`);
|
|
144
|
+
assert.equal(await session.evaluate("window.__badThrew"), false, "a compile error must not throw into the page");
|
|
145
|
+
assert.ok(session.logs.some((l) => /broken/u.test(l)), `expected a console error naming the broken template\n${session.logs.join("\n")}`);
|
|
146
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
147
|
+
const before = await shot(session, 320, 180);
|
|
148
|
+
await session.evaluate("(() => { const gl = document.getElementById('c').getContext('webgl2'); window.__lose = gl.getExtension('WEBGL_lose_context'); window.__lose.loseContext(); return true; })()");
|
|
149
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
150
|
+
await session.evaluate("window.__lose.restoreContext(), true");
|
|
151
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
152
|
+
const after = await shot(session, 320, 180);
|
|
153
|
+
assert.ok(meanRgb(after) > 8, "canvas is blank after context restore");
|
|
154
|
+
assert.ok(meanAbs(before, after) < 3, `restored frame differs (${meanAbs(before, after).toFixed(2)})`);
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
rmSync(dir, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const AUDIO_PROBE = `<!doctype html><html><head><meta charset="utf-8"></head>
|
|
162
|
+
<body><canvas id="c" style="width:160px;height:90px"></canvas>
|
|
163
|
+
<script type="module">
|
|
164
|
+
import { AgentVfx } from "./agentvfx.js";
|
|
165
|
+
import templates from "./templates.js";
|
|
166
|
+
const Base = window.AudioContext;
|
|
167
|
+
window.__ctxs = [];
|
|
168
|
+
window.AudioContext = class extends Base { constructor(...a) { super(...a); window.__ctxs.push(this); } };
|
|
169
|
+
const fx = await AgentVfx.mount(document.getElementById("c"), { templates, template: "test-gradient", motion: "always", dpr: 1 });
|
|
170
|
+
fx.audio(new Audio());
|
|
171
|
+
fx.destroy();
|
|
172
|
+
window.__litReady = true;
|
|
173
|
+
</script></body></html>`;
|
|
174
|
+
|
|
175
|
+
test("destroy() closes the AudioContext that audio() opened", async () => {
|
|
176
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-audio-"));
|
|
177
|
+
try {
|
|
178
|
+
const exported = runExport(["test-gradient", "--out", dir]);
|
|
179
|
+
assert.equal(exported.status, 0, exported.stderr || exported.stdout);
|
|
180
|
+
writeFileSync(path.join(dir, "audio.html"), AUDIO_PROBE);
|
|
181
|
+
await withChrome(dir, { width: 320, height: 180 }, async (session) => {
|
|
182
|
+
await open(session, "audio.html", "window.__litReady === true");
|
|
183
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
184
|
+
assert.equal(await session.evaluate("window.__ctxs.length"), 1);
|
|
185
|
+
assert.equal(await session.evaluate("window.__ctxs[0].state"), "closed");
|
|
186
|
+
});
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(dir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const GUARD_PROBE = `<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:#fff}canvas{display:block;width:320px;height:180px}</style></head>
|
|
193
|
+
<body><canvas id="c"></canvas>
|
|
194
|
+
<script type="module">
|
|
195
|
+
import { AgentVfx } from "./agentvfx.js";
|
|
196
|
+
import templates from "./templates.js";
|
|
197
|
+
const q = new URLSearchParams(location.search);
|
|
198
|
+
window.__trips = [];
|
|
199
|
+
window.__fx = await AgentVfx.mount(document.getElementById("c"), {
|
|
200
|
+
templates, template: q.get("t"), params: JSON.parse(q.get("p") || "{}"), motion: "always", dpr: 1,
|
|
201
|
+
onFlashGuard: (info) => window.__trips.push(info),
|
|
202
|
+
});
|
|
203
|
+
window.__litReady = true;
|
|
204
|
+
</script></body></html>`;
|
|
205
|
+
|
|
206
|
+
async function guardRun(dir, query, ms) {
|
|
207
|
+
return withChrome(dir, { width: 320, height: 180 }, async (session) => {
|
|
208
|
+
await open(session, `guard.html?${query}`, "window.__litReady === true");
|
|
209
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
210
|
+
const trips = await session.evaluate("JSON.stringify(window.__trips)").then(JSON.parse);
|
|
211
|
+
const a = await shot(session, 320, 180);
|
|
212
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
213
|
+
const b = await shot(session, 320, 180);
|
|
214
|
+
let resumed = null;
|
|
215
|
+
if (trips.length) {
|
|
216
|
+
await session.evaluate("window.__fx.resume(), true");
|
|
217
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
218
|
+
resumed = await session.evaluate("window.__trips.length");
|
|
219
|
+
}
|
|
220
|
+
return { trips, still: meanAbs(a, b), resumed };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
test("the live flash guard stops a strobe before a fourth flash in one second and shows no tripping frame", async () => {
|
|
225
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-guard-"));
|
|
226
|
+
try {
|
|
227
|
+
const exported = runExport(["test-strobe", "test-gradient", "--out", dir]);
|
|
228
|
+
assert.equal(exported.status, 0, exported.stderr || exported.stdout);
|
|
229
|
+
writeFileSync(path.join(dir, "guard.html"), GUARD_PROBE);
|
|
230
|
+
const strobe = await guardRun(dir, "t=test-strobe&p=%7B%22rate%22%3A12%7D", 1500);
|
|
231
|
+
assert.equal(strobe.trips.length, 1, "a 12 Hz strobe must trip the guard once");
|
|
232
|
+
assert.equal(strobe.trips[0].reason, "general");
|
|
233
|
+
assert.ok(strobe.trips[0].shownTransitions <= 6, `shown transitions ${strobe.trips[0].shownTransitions} must stay at three flashes or fewer`);
|
|
234
|
+
assert.ok(strobe.still < 0.5, "after the trip the canvas must hold still");
|
|
235
|
+
assert.equal(strobe.resumed, 2, "resume() must run the effect again under a re-armed guard, which stops it again");
|
|
236
|
+
const red = await guardRun(dir, "t=test-strobe&p=%7B%22rate%22%3A2%2C%22red%22%3Atrue%7D", 2500);
|
|
237
|
+
assert.equal(red.trips.length, 1, "a slow saturated-red flash must trip the guard");
|
|
238
|
+
assert.equal(red.trips[0].reason, "red");
|
|
239
|
+
assert.ok(red.trips[0].shownRedTransitions <= 1, "no complete red flash may be shown");
|
|
240
|
+
const calm = await guardRun(dir, "t=test-gradient", 1500);
|
|
241
|
+
assert.equal(calm.trips.length, 0, "a calm gradient must not trip the guard");
|
|
242
|
+
} finally {
|
|
243
|
+
rmSync(dir, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("export-web bundles templates whose shader credits carry links, keeping the credit without the scheme", () => {
|
|
248
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-export-"));
|
|
249
|
+
try {
|
|
250
|
+
const result = runExport(["boot-log", "halftone-field", "hud-deck", "vhs", "--out", dir]);
|
|
251
|
+
assert.equal(result.status, 0, result.stderr);
|
|
252
|
+
assertNoExternalUrl(dir);
|
|
253
|
+
const bundled = readFileSync(path.join(dir, "templates.js"), "utf8");
|
|
254
|
+
assert.match(bundled, /Inigo Quilez, iquilezles\.org\/articles\/distfunctions2d/u);
|
|
255
|
+
assert.match(bundled, /en\.wikipedia\.org\/wiki\/YIQ/u);
|
|
256
|
+
} finally {
|
|
257
|
+
rmSync(dir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("export-web writes the LICENSE next to the bundle and keeps the licence notice in the runtime", () => {
|
|
262
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-export-"));
|
|
263
|
+
try {
|
|
264
|
+
const result = runExport(["test-gradient", "--out", dir]);
|
|
265
|
+
assert.equal(result.status, 0, result.stderr);
|
|
266
|
+
assertNoExternalUrl(dir);
|
|
267
|
+
const license = readFileSync(path.join(dir, "LICENSE"), "utf8");
|
|
268
|
+
assert.equal(license, readFileSync(path.join(root, "LICENSE"), "utf8"), "LICENSE is copied verbatim");
|
|
269
|
+
assert.match(license, /^Required Notice: Copyright \(c\) 2026 Woojin Go \(https:\/\/github\.com\/wjgoarxiv\/agentvfx\)$/mu);
|
|
270
|
+
const runtime = readFileSync(path.join(dir, "agentvfx.js"), "utf8");
|
|
271
|
+
assert.match(runtime, /^\/\/ agentvfx licence: the full terms are in the LICENSE file in this folder/mu);
|
|
272
|
+
assert.match(runtime, /^\/\/ Required Notice: Copyright \(c\) 2026 Woojin Go \(github\.com\/wjgoarxiv\/agentvfx\)$/mu);
|
|
273
|
+
assert.match(readFileSync(path.join(dir, "demo.html"), "utf8"), /Required Notice: Copyright \(c\) 2026 Woojin Go/u);
|
|
274
|
+
} finally {
|
|
275
|
+
rmSync(dir, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("export-web rejects an unknown template id", () => {
|
|
280
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-export-"));
|
|
281
|
+
try {
|
|
282
|
+
const result = runExport(["no-such-template", "--out", dir]);
|
|
283
|
+
assert.notEqual(result.status, 0);
|
|
284
|
+
assert.match(`${result.stderr}\n${result.stdout}`, /no-such-template/u);
|
|
285
|
+
} finally {
|
|
286
|
+
rmSync(dir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("exported demo animates, freezes under reduced motion, and matches the offline frame", async () => {
|
|
291
|
+
const dir = mkdtempSync(path.join(tmpdir(), "agentvfx-export-"));
|
|
292
|
+
const offlineDir = mkdtempSync(path.join(tmpdir(), "agentvfx-offline-"));
|
|
293
|
+
try {
|
|
294
|
+
const exported = runExport(["test-gradient", "sacred", "test-strobe", "--out", dir]);
|
|
295
|
+
assert.equal(exported.status, 0, exported.stderr || exported.stdout);
|
|
296
|
+
for (const name of ["agentvfx.js", "templates.js", "demo.html"]) {
|
|
297
|
+
assert.equal(existsSync(path.join(dir, name)), true, name);
|
|
298
|
+
}
|
|
299
|
+
assertNoExternalUrl(dir);
|
|
300
|
+
const demo = readFileSync(path.join(dir, "demo.html"), "utf8");
|
|
301
|
+
assert.match(demo, /test-gradient/u);
|
|
302
|
+
assert.match(demo, /textSource/u);
|
|
303
|
+
assert.match(demo, /type="file"/u);
|
|
304
|
+
assert.match(demo, /trigger\(/u);
|
|
305
|
+
|
|
306
|
+
const W = 640;
|
|
307
|
+
const H = 360;
|
|
308
|
+
await withChrome(dir, { width: W, height: H }, async (session) => {
|
|
309
|
+
await open(session, "demo.html", "window.__litReady === true");
|
|
310
|
+
const a = await shot(session, W, H);
|
|
311
|
+
assert.ok(meanRgb(a) > 8, `demo canvas looks black (${meanRgb(a)})`);
|
|
312
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
313
|
+
const b = await shot(session, W, H);
|
|
314
|
+
assert.ok(meanAbs(a, b) > 1, "demo did not change across 500 ms");
|
|
315
|
+
await session.evaluate("window.__hit()");
|
|
316
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
317
|
+
const c = await shot(session, W, H);
|
|
318
|
+
assert.ok(meanAbs(b, c) > 1, "trigger() did not change the next capture");
|
|
319
|
+
const warned = session.logs.some((line) => /WCAG 2\.3\.1/u.test(line) && /test-strobe/u.test(line));
|
|
320
|
+
assert.equal(warned, true, `expected a WCAG 2.3.1 warning for test-strobe\n${session.logs.join("\n")}`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await withChrome(dir, { width: W, height: H, reduced: true }, async (session) => {
|
|
324
|
+
await open(session, "demo.html", "window.__litReady === true");
|
|
325
|
+
const a = await shot(session, W, H);
|
|
326
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
327
|
+
const b = await shot(session, W, H);
|
|
328
|
+
assert.equal(meanAbs(a, b), 0, "reduced motion captures differ");
|
|
329
|
+
await session.evaluate("window.__hit()");
|
|
330
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
331
|
+
const c = await shot(session, W, H);
|
|
332
|
+
assert.equal(meanAbs(b, c), 0, "reduced motion did not suppress trigger()");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const lib = loadLibrary({ includeTest: true });
|
|
336
|
+
const checked = validateScore({
|
|
337
|
+
version: 1,
|
|
338
|
+
template: "test-gradient",
|
|
339
|
+
preset: "default",
|
|
340
|
+
palette: "rainbow",
|
|
341
|
+
durationSec: 3,
|
|
342
|
+
fps: 30,
|
|
343
|
+
seed: 1,
|
|
344
|
+
out: "parity",
|
|
345
|
+
}, lib);
|
|
346
|
+
assert.equal(checked.ok, true, JSON.stringify(checked.errors));
|
|
347
|
+
const frame = 15;
|
|
348
|
+
let offline = null;
|
|
349
|
+
await renderScore(checked.value, lib, {
|
|
350
|
+
outDir: offlineDir,
|
|
351
|
+
width: 256,
|
|
352
|
+
height: 144,
|
|
353
|
+
replay: false,
|
|
354
|
+
onFrame: (f, analysed) => {
|
|
355
|
+
if (f === frame) offline = analysed.rgba;
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
assert.ok(offline, "offline frame missing");
|
|
359
|
+
|
|
360
|
+
const harness = `<!DOCTYPE html>
|
|
361
|
+
<html><head><meta charset="utf-8"></head>
|
|
362
|
+
<body style="margin:0;background:#000">
|
|
363
|
+
<canvas id="c" width="256" height="144" style="width:256px;height:144px;display:block"></canvas>
|
|
364
|
+
<script type="module">
|
|
365
|
+
import { AgentVfx } from "./agentvfx.js";
|
|
366
|
+
import templates from "./templates.js";
|
|
367
|
+
const canvas = document.getElementById("c");
|
|
368
|
+
const fx = await AgentVfx.mount(canvas, {
|
|
369
|
+
templates, template: "test-gradient", preset: "default", palette: "rainbow",
|
|
370
|
+
seed: 1, dpr: 1, fps: 30, motion: "always"
|
|
371
|
+
});
|
|
372
|
+
window.__fx = fx;
|
|
373
|
+
window.__litReady = true;
|
|
374
|
+
</script>
|
|
375
|
+
</body></html>`;
|
|
376
|
+
writeFileSync(path.join(dir, "harness.html"), harness);
|
|
377
|
+
await withChrome(dir, { width: 256, height: 144 }, async (session) => {
|
|
378
|
+
await open(session, "harness.html", "window.__litReady === true");
|
|
379
|
+
await session.evaluate(`window.__fx.renderAt(${frame}/30)`);
|
|
380
|
+
const live = await shot(session, 256, 144);
|
|
381
|
+
const diff = meanAbs(live, { pixels: offline, channels: 4, width: 256, height: 144 });
|
|
382
|
+
assert.ok(diff <= 2, `live vs offline mean abs ${diff} > 2`);
|
|
383
|
+
});
|
|
384
|
+
} finally {
|
|
385
|
+
rmSync(dir, { recursive: true, force: true });
|
|
386
|
+
rmSync(offlineDir, { recursive: true, force: true });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("live analyser reads the same level window as the render, so loud masters do not pin every bar", () => {
|
|
391
|
+
const runtime = readFileSync(path.join(root, "runtime/agentvfx.js"), "utf8");
|
|
392
|
+
// Render: dB of power vs a full-scale sine, over 70 dB (engine/node/audio.mjs). AnalyserNode dB sit about 7 dB
|
|
393
|
+
// higher on the same audio, so the live window is -63..+7 dB, unsmoothed (templates do their own attack/release).
|
|
394
|
+
assert.match(runtime, /analyser\.minDecibels = -63;/u);
|
|
395
|
+
assert.match(runtime, /analyser\.maxDecibels = 7;/u);
|
|
396
|
+
assert.match(runtime, /analyser\.smoothingTimeConstant = 0;/u);
|
|
397
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Test media sources
|
|
2
|
+
|
|
3
|
+
Access date for every file: 2026-11-01.
|
|
4
|
+
|
|
5
|
+
Licence text below is the visible Usage wording on the archive.org item page (the `rel="license"` link), not the API `licenseurl` alone.
|
|
6
|
+
|
|
7
|
+
`beat-132.mp3` is not sourced from anywhere: `scripts/make-beat.mjs` synthesises it (132 BPM, 10 s) and it ships under the same licence as the code (see LICENSE).
|
|
8
|
+
|
|
9
|
+
## clip-01.mp4
|
|
10
|
+
|
|
11
|
+
- Page URL: https://archive.org/details/JuniorPr1946
|
|
12
|
+
- Direct file URL: https://archive.org/download/JuniorPr1946/JuniorPr1946.mp4
|
|
13
|
+
- Title: Junior Prom
|
|
14
|
+
- Author: Simmel-Meservey
|
|
15
|
+
- Licence: Public Domain
|
|
16
|
+
- Licence link on the page: http://creativecommons.org/licenses/publicdomain/ (link title "Public Domain")
|
|
17
|
+
- Trim window: 849.0 s–859.0 s (source time). Colour prom dance: faces, a light dress against dark suits, strong luminance change.
|
|
18
|
+
- ffmpeg command:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
ffmpeg -y -ss 849 -t 10.0 -i JuniorPr1946.mp4 -vf "fps=30,scale=1920:1440:flags=lanczos" -frames:v 300 -af "atrim=end=10,asetpts=PTS-STARTPTS" -c:v libx264 -pix_fmt yuv420p -r 30 -fps_mode cfr -c:a aac -b:a 160k -ar 48000 -ac 2 -movflags +faststart test-media/clip-01.mp4
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## clip-02.mp4
|
|
25
|
+
|
|
26
|
+
- Page URL: https://archive.org/details/DuckandC1951
|
|
27
|
+
- Direct file URL: https://archive.org/download/DuckandC1951/DuckandC1951.mp4
|
|
28
|
+
- Title: Duck and Cover
|
|
29
|
+
- Author: Archer Productions, Inc.
|
|
30
|
+
- Licence: Public Domain
|
|
31
|
+
- Licence link on the page: http://creativecommons.org/licenses/publicdomain/ (link title "Public Domain")
|
|
32
|
+
- Trim window: 118.0 s–128.0 s (source time). Different subject from clip-01: a street, cars, and a dark figure against a bright road.
|
|
33
|
+
- ffmpeg command:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
ffmpeg -y -ss 118 -t 10.0 -i DuckandC1951.mp4 -vf "fps=30,scale=1472:1920:flags=lanczos" -frames:v 300 -af "atrim=end=10,asetpts=PTS-STARTPTS" -c:v libx264 -pix_fmt yuv420p -r 30 -fps_mode cfr -c:a aac -b:a 160k -ar 48000 -ac 2 -movflags +faststart test-media/clip-02.mp4
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The archive file is 368×480 with no rotate tag, so the long edge of 1920 is the height (1472×1920).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|