diff --git a/.github/workflows/droid-control-tests.yml b/.github/workflows/droid-control-tests.yml index fbddada..c9b6838 100644 --- a/.github/workflows/droid-control-tests.yml +++ b/.github/workflows/droid-control-tests.yml @@ -6,7 +6,7 @@ on: - 'plugins/droid-control/bin/tctl' - 'plugins/droid-control/scripts/render-showcase.sh' - 'plugins/droid-control/tests/**' - - 'plugins/droid-control/remotion/**' + - 'plugins/droid-control/fframes/**' - '.github/workflows/droid-control-tests.yml' push: branches: [master] @@ -14,62 +14,43 @@ on: - 'plugins/droid-control/bin/tctl' - 'plugins/droid-control/scripts/render-showcase.sh' - 'plugins/droid-control/tests/**' - - 'plugins/droid-control/remotion/**' + - 'plugins/droid-control/fframes/**' - '.github/workflows/droid-control-tests.yml' env: PYTHONDONTWRITEBYTECODE: 1 + CARGO_TERM_COLOR: always jobs: - render-helper: - name: render-showcase.sh behavior + showcase: + name: Showcase composition, render helper, real renders runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install ffmpeg and agg + - name: Install ffmpeg, agg, and the fframes build dependencies run: | set -euo pipefail sudo apt-get update - sudo apt-get install -y --no-install-recommends ffmpeg + sudo apt-get install -y --no-install-recommends ffmpeg clang libclang-dev libx264-dev curl -fsSL -o agg https://github.com/asciinema/agg/releases/download/v1.9.0/agg-x86_64-unknown-linux-gnu echo "f111e315cd71056b116302342553dd765b7297579ed511f111d0cedb442aeda6 agg" | sha256sum -c - sudo install -m 0755 agg /usr/local/bin/agg - - name: Run helper integration tests - run: python3 -m unittest discover -v -s plugins/droid-control/tests -p 'test_*.py' - - remotion: - name: Remotion typecheck, duration tests, real render - runs-on: ubuntu-latest - defaults: - run: - working-directory: plugins/droid-control/remotion - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 + - uses: dtolnay/rust-toolchain@stable with: - # node --test with built-in type stripping (22.18+); Remotion runtime itself needs >= 18 - node-version: 22 - cache: npm - cache-dependency-path: plugins/droid-control/remotion/package-lock.json + components: clippy, rustfmt - - run: npm ci - - - run: npx tsc --noEmit -p . - - - run: npm test + - uses: Swatinem/rust-cache@v2 + with: + workspaces: plugins/droid-control/fframes - - name: Install ffmpeg and the Remotion browser + - name: Format, lint, and unit tests + working-directory: plugins/droid-control/fframes run: | - set -euo pipefail - sudo apt-get update - sudo apt-get install -y --no-install-recommends ffmpeg - npx remotion browser ensure + cargo fmt --check + cargo clippy --all-targets --release -- -D warnings + cargo test --release - - name: Render a synthetic clip through render-showcase.sh and check the encoded file - working-directory: . - env: - RENDER_SHOWCASE_E2E: 1 - run: python3 -m unittest discover -v -s plugins/droid-control/tests -p 'test_*.py' -k RealRender + - name: Helper integration tests and real renders + run: python3 -m unittest discover -v -s plugins/droid-control/tests -p 'test_*.py' diff --git a/.gitignore b/.gitignore index 13af873..c2a48c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ .factory/ node_modules/ -# Per-render clip staging created by render-showcase.sh; only survives a SIGKILL -plugins/droid-control/remotion/public/render-*/ diff --git a/plugins/droid-control/ARCHITECTURE.md b/plugins/droid-control/ARCHITECTURE.md index ba3e9ca..7d6a5b1 100644 --- a/plugins/droid-control/ARCHITECTURE.md +++ b/plugins/droid-control/ARCHITECTURE.md @@ -10,7 +10,7 @@ Editable source: [`diagrams/architecture-routing.excalidraw`](diagrams/architect The plugin is designed to keep a droid focused while it operates real software: -- **Low context load:** load the Linux tuistory path without dragging in Windows KVM notes, macOS VM controls, browser automation, and Remotion internals. +- **Low context load:** load the Linux tuistory path without dragging in Windows KVM notes, macOS VM controls, browser automation, and renderer internals. - **Evidence-first workflows:** every command starts by making commitments, then ends by verifying the artifact against those commitments. - **Parallel execution:** independent capture environments and render jobs can run in workers. Shared desktop input stays serialized; session names do not isolate focus. - **Clear ownership:** commands decide *what* must be produced; atoms decide *how* to execute their slice. @@ -91,7 +91,7 @@ The parent droid keeps judgment. Workers get exact commands. | Interpret PR / claim / QA goal | Parent | Requires context and judgment. | | Write the interaction script | Parent | Defines the proof story. | | Capture baseline and candidate branches | Workers only for independent environments | A shared desktop must be captured serially. | -| Render Remotion video | Worker droid | Mechanical once props and clips are fixed. | +| Render showcase video | Worker droid | Mechanical once props and clips are fixed. | | Verify commitments | Parent | Requires the original contract and evidence judgment. | This boundary follows the stage handoffs. Capture workers need resolved `tctl` commands and worktree paths, not PR context. Render workers need a props JSON and clip paths, not a feature explanation. @@ -126,32 +126,32 @@ Browser/Electron and native-desktop workflows intentionally do **not** go throug ## Video composition -The compose stage uses the Remotion project in `remotion/` as a single video engine. The droid writes a `Showcase` props JSON; `scripts/render-showcase.sh` handles the mechanical rendering pipeline: +The compose stage uses the [fframes](https://github.com/dmtrKovalenko/fframes) project in `fframes/` as a single video engine: a Rust crate whose `droid-showcase` binary renders the `Showcase` composition from SVG frames. The droid writes a `Showcase` props JSON; `scripts/render-showcase.sh` builds the binary on first use (cargo) and runs it. The binary owns the mechanical rendering pipeline: -1. Accept `.cast`, `.mp4`, and `.webm` clips only; normalize props and resolve fidelity (omitted: side-by-side `inspect`, single `standard`). +1. Accept `.cast`, `.mp4`, and `.webm` clips only; validate props and resolve defaults and fidelity (omitted: side-by-side `inspect`, single `standard`). 2. Convert `.cast` recordings through `agg` and `ffmpeg` at 1x, keeping the recording's timeline. -3. Stage clips as `clip-` inside a directory created under Remotion `public/` for this render only. -4. Set `clipDuration` to the longest clip with `ffprobe`. -5. Render the `Showcase` composition as limited-range `yuv420p`/`bt709` H.264, or one frame with `--still`. -6. Remove that render's staged directory and conversion outputs on exit. +3. Stage clips as `clip-` inside a work directory created for this render only. +4. Probe every clip with `ffprobe`; the longest one sets the content length. +5. Print the resolved plan (`showcase plan: {...}` on stderr), then render every frame on all cores and encode limited-range `yuv420p`/`bt709` H.264 through `ffmpeg`, or one PNG frame with `--still`. +6. Remove that render's work directory on exit, failure, or SIGINT/SIGTERM. -`remotion/src/lib/duration.ts` owns the timeline: the composition applies `speed` once to every clip, the clips run for `clipDuration / speed`, and the content sequence is padded by one crossfade on each side so the clips start after the title crossfade and the final frame is held through the outro crossfade. Total length is `4s title + clipDuration / speed + 3.5s outro`; a shorter clip holds its final frame. This keeps droids out of the common failure modes: two `recording.mp4` inputs overwriting each other in `public/`, concurrent renders deleting each other's clips, mismatched `clipDuration`, casts sped up twice, wrong `agg` theme, invalid pixel formats, and hand-written Remotion commands with missing encode flags. +`fframes/src/timing.rs` owns the timeline: the composition applies `speed` once to every clip, the clips run for `longest clip / speed`, and the content sequence is padded by one crossfade on each side so the clips start after the title crossfade and the final frame is held through the outro crossfade. Total length is `4s title + longest clip / speed + 3.5s outro`; a shorter clip holds its final frame. This keeps droids out of the common failure modes: two `recording.mp4` inputs overwriting each other, concurrent renders deleting each other's clips, a mismatched clip duration, casts sped up twice, wrong `agg` theme, invalid pixel formats, and hand-written encode commands with missing flags. ### Composition surface -The `Showcase` composition in `remotion/src/compositions/Showcase.tsx` is the only video entry point. Everything else lives in `remotion/src/components/` and is composed by props: +`fframes/src/showcase.rs` is the only video entry point (`Showcase`, an fframes `Video`). It composes the layer modules by props: -| Layer | Purpose | Controlled by | -|---|---|---| -| Background + FloatingParticles | Preset-driven warmth or coolness | `preset` | -| TitleCard / DroidOutro | Opening and closing cards (outro plays fanning rotor → crossfade → DROID wordmark) | `title`, `subtitle`, `speedNote` | -| Window chrome + layouts | `SingleLayout` or `SideBySideLayout` | `layout`, `labels`, `objectFit` | -| ZoomEffect / SpotlightOverlay / KeystrokeOverlay / SectionHeader | Timed in-scene overlays | `effects`, `keys`, `sections` | -| CodeAnnotationOverlay | Timed syntax-highlighted code cards | `codeAnnotations` | -| Transition presentation | Title→content and content→outro crossfade | `transitionStyle` (default `motion-blur`) | -| NoiseOverlay + ColorGradeOverlay + Watermark | Topmost polish pass | `fidelity`, `preset` | +| Layer | Module | Purpose | Controlled by | +|---|---|---|---| +| Background + particles | `scenery.rs` | Preset-driven warmth or coolness | `preset` | +| Title card / Droid outro | `title.rs`, `outro.rs` | Opening and closing cards (outro plays fanning rotor → crossfade → DROID wordmark) | `title`, `subtitle`, `speedNote` | +| Window chrome + layouts | `content.rs` | Single or side-by-side windows, panel entrances, zooms, spotlights | `layout`, `labels`, `windowTitle`, `objectFit`, `effects` | +| Callouts / section sweeps and headers / keystrokes | `overlays.rs` | Timed in-scene overlays | `effects`, `sections`, `keys` | +| Code cards | `overlays.rs`, `code.rs` | Timed syntax-highlighted code cards | `codeAnnotations` | +| Transition presentation | `transition.rs` | Title→content and content→outro crossfade | `transitionStyle` (default `motion-blur`) | +| Noise + colour grade + watermark | `scenery.rs` | Topmost polish pass | `fidelity`, `preset` | -The key property is that the main composition is data-driven: the droid never writes Remotion JSX. Adding a new overlay or transition style is a new component plus a schema field, not a new composition. +The key property is that the main composition is data-driven: the droid never writes composition code. Adding a new overlay or transition style is a new layer function plus a props field, not a new composition. Fonts (Geist, Geist Mono) are embedded in the binary, so renders look the same on every machine. ## Platform isolation @@ -182,7 +182,7 @@ Use the same composition rules when adding capability: | New user workflow | Add a command that parses arguments into commitments, then routes through existing atoms. | | New target type | Add one target atom and one target-route row. | | New capture backend | Add a driver atom or extend `tctl` only if it belongs behind the same terminal boundary. | -| New visual treatment | Add Remotion props/schema support and document compose/showcase behavior. | +| New visual treatment | Add props support and a layer in `fframes/src/`, then document compose/showcase behavior. | | New platform mechanics | Add a `platforms/.md` file under the relevant atom. | If a change makes every droid read more global instructions, it is probably fighting the architecture. Prefer a new scoped surface over a larger shared surface. diff --git a/plugins/droid-control/NOTICES.md b/plugins/droid-control/NOTICES.md index 4249560..3616a89 100644 --- a/plugins/droid-control/NOTICES.md +++ b/plugins/droid-control/NOTICES.md @@ -8,10 +8,10 @@ This plugin depends on several third-party tools and libraries. Executables are ## Video rendering -- **[Remotion](https://www.remotion.dev/)** -- React-based video renderer used by the compose/showcase pipeline. Remotion is free for individuals, small teams (<=3 employees), and non-profits. Larger companies require a [company license](https://www.remotion.pro/). See the [full license terms](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md). -- **[React](https://react.dev/)** -- MIT License -- **[Zod](https://zod.dev/)** -- MIT License -- **[prism-react-renderer](https://github.com/FormidableLabs/prism-react-renderer)** -- MIT License. Powers the syntax highlighting in the `CodeAnnotationOverlay` component. +- **[fframes](https://github.com/dmtrKovalenko/fframes)** (MIT) -- Rust SVG-based video framework that renders the `fframes/` showcase composition. Its SVG stack, [svgr/usvgr](https://github.com/dmtrKovalenko/fframes), is MPL-2.0; its clip decoder statically links [FFmpeg](https://ffmpeg.org/) (LGPL-2.1+) through `ffmpeg-sys-fframes` (WTFPL). +- **[Geist and Geist Mono](https://github.com/vercel/geist-font)** -- SIL Open Font License 1.1, embedded in the renderer from `fframes/media/` (license text in `fframes/media/GEIST-OFL.txt`). +- **[syntect](https://github.com/trishume/syntect)** (MIT) and **[two-face](https://github.com/CosmicHorrorDev/two-face)** (MIT OR Apache-2.0) -- syntax highlighting for code annotations. +- **[clap](https://github.com/clap-rs/clap)**, **[serde](https://serde.rs/)**, **[signal-hook](https://github.com/vorner/signal-hook)**, **[tempfile](https://github.com/Stebalien/tempfile)** -- MIT OR Apache-2.0. ## Terminal automation @@ -31,4 +31,4 @@ This plugin depends on several third-party tools and libraries. Executables are ## Design influences -- **[@hyperframes/shader-transitions](https://github.com/heygen-com/hyperframes/tree/main/packages/shader-transitions)** (Apache-2.0) -- the `transitionStyle` prop's naming and taxonomy (`whip-pan`, `light-leak`, `flash`, `glitch-lite`) was shaped by Hyperframes' shader-transitions catalog. Implementations in `ShowcaseTransition.tsx` are original Remotion-native CSS/SVG overlays, not GLSL ports. +- **[@hyperframes/shader-transitions](https://github.com/heygen-com/hyperframes/tree/main/packages/shader-transitions)** (Apache-2.0) -- the `transitionStyle` prop's naming and taxonomy (`whip-pan`, `light-leak`, `flash`, `glitch-lite`) was shaped by Hyperframes' shader-transitions catalog. Implementations in `fframes/src/transition.rs` are original SVG filter and overlay effects, not GLSL ports. diff --git a/plugins/droid-control/README.md b/plugins/droid-control/README.md index f9c33f6..933aeec 100644 --- a/plugins/droid-control/README.md +++ b/plugins/droid-control/README.md @@ -39,9 +39,9 @@ droid plugin marketplace add https://github.com/Factory-AI/factory-plugins # Install the plugin droid plugin install droid-control@factory-plugins --scope user -# Install Remotion dependencies (one-time, only needed for video rendering) +# Build the video renderer (one-time, only needed for video rendering; needs Rust, clang, libx264) # Find the plugin install path with: droid plugin list --scope user -cd /remotion && npm install +cargo build --release --manifest-path /fframes/Cargo.toml ``` Or use the `/plugins` UI: Browse tab, select droid-control, install. @@ -80,9 +80,9 @@ For the full rationale and runtime pipeline, see [`ARCHITECTURE.md`](ARCHITECTUR ## Video rendering -The compose stage uses [Remotion](https://www.remotion.dev/) for video compositing. Presets provide window chrome, spacing, palettes, backgrounds, particles, noise, color grading, configurable transitions (`motion-blur`, `flash`, `whip-pan`, `light-leak`, `glitch-lite`), zooms, spotlights, callout annotations, keystroke overlays, section headers, and syntax-highlighted code annotations. +The compose stage uses [fframes](https://github.com/dmtrKovalenko/fframes), a Rust SVG video framework, for video compositing. Presets provide window chrome, spacing, palettes, backgrounds, particles, noise, color grading, configurable transitions (`motion-blur`, `flash`, `whip-pan`, `light-leak`, `glitch-lite`), zooms, spotlights, callout annotations, keystroke overlays, section headers, and syntax-highlighted code annotations. -The `render-showcase.sh` helper owns the full pipeline: `.cast` conversion via `agg`, per-render clip staging, longest-clip duration, Remotion rendering (or a `--still` preview), and cleanup. Playback `speed` is applied once by the composition to every clip. +The `render-showcase.sh` helper owns the full pipeline: `.cast` conversion via `agg`, per-render clip staging, longest-clip duration, rendering on every core (or a `--still` preview), and cleanup. It builds the `droid-showcase` binary from `fframes/` on first use. Playback `speed` is applied once by the composition to every clip. ## Prerequisites @@ -95,7 +95,7 @@ The `render-showcase.sh` helper owns the full pipeline: `.cast` conversion via ` | browser-use | All | `agent-browser` | | desktop-use | All | `cua-driver` | | compose | All | `ffmpeg`, `ffprobe`, `agg` | -| showcase | All | Node.js (>= 18), Chrome/Chromium | +| showcase | All | Rust (stable, via rustup), clang/libclang, libx264 | ```bash npm install -g tuistory # virtual PTY driver @@ -104,7 +104,8 @@ cargo install --git https://github.com/asciinema/agg # .cast -> .gif converter sudo apt-get install -y ffmpeg # video processing agent-browser install # browser automation (downloads Chromium) curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh | bash # native desktop GUI automation -cd plugins/droid-control/remotion && npm install # Remotion video rendering +sudo apt-get install -y clang libclang-dev libx264-dev # renderer build deps (macOS: xcode-select --install; brew install x264) +cargo build --release --manifest-path plugins/droid-control/fframes/Cargo.toml # video renderer ``` Only install what you need, with approval. Terminal demos need tuistory, asciinema, agg, and ffmpeg. Web/Electron automation defaults to browser-use; an explicit cua-only/native-input request uses desktop-use instead. Native desktop automation needs cua-driver plus the graphical session and OS permissions reported by its preflight. Recording and rendering have additional dependencies; they are not required for ordinary desktop tasks. diff --git a/plugins/droid-control/diagrams/architecture-routing.excalidraw b/plugins/droid-control/diagrams/architecture-routing.excalidraw index b62d5c5..11137c7 100644 --- a/plugins/droid-control/diagrams/architecture-routing.excalidraw +++ b/plugins/droid-control/diagrams/architecture-routing.excalidraw @@ -985,7 +985,7 @@ "textAlign": "center", "verticalAlign": "middle", "containerId": "compose", - "originalText": "Compose\nRemotion props\nrender-showcase", + "originalText": "Compose\nShowcase props\nrender-showcase", "autoResize": true, "lineHeight": 1.25, "id": "t_compose", @@ -993,7 +993,7 @@ "y": 758, "width": 194, "height": 104, - "text": "Compose\nRemotion props\nrender-showcase" + "text": "Compose\nShowcase props\nrender-showcase" }, { "angle": 0, diff --git a/plugins/droid-control/diagrams/architecture-routing.svg b/plugins/droid-control/diagrams/architecture-routing.svg index f388cb6..aed3f49 100644 --- a/plugins/droid-control/diagrams/architecture-routing.svg +++ b/plugins/droid-control/diagrams/architecture-routing.svg @@ -74,7 +74,7 @@ Compose -Remotion props +Showcase props render-showcase diff --git a/plugins/droid-control/diagrams/capture-compose-verify.excalidraw b/plugins/droid-control/diagrams/capture-compose-verify.excalidraw index 2324427..9e270dd 100644 --- a/plugins/droid-control/diagrams/capture-compose-verify.excalidraw +++ b/plugins/droid-control/diagrams/capture-compose-verify.excalidraw @@ -970,7 +970,7 @@ "isDeleted": false, "boundElements": [ { - "id": "t_remotion", + "id": "t_showcase", "type": "text" } ], @@ -978,7 +978,7 @@ "link": null, "locked": false, "type": "rectangle", - "id": "remotion", + "id": "showcase", "x": 820, "y": 620, "width": 300, @@ -1009,16 +1009,16 @@ "fontSize": 18, "textAlign": "center", "verticalAlign": "middle", - "containerId": "remotion", - "originalText": "Remotion Showcase\n\nlayout + presets\nwindow chrome\nparticles / zoom / spotlight", + "containerId": "showcase", + "originalText": "fframes Showcase\n\nlayout + presets\nwindow chrome\nparticles / zoom / spotlight", "autoResize": true, "lineHeight": 1.25, - "id": "t_remotion", + "id": "t_showcase", "x": 828, "y": 628, "width": 284, "height": 129, - "text": "Remotion Showcase\n\nlayout + presets\nwindow chrome\nparticles / zoom / spotlight" + "text": "fframes Showcase\n\nlayout + presets\nwindow chrome\nparticles / zoom / spotlight" }, { "angle": 0, @@ -1135,7 +1135,7 @@ "link": null, "locked": false, "type": "arrow", - "id": "a_render_remotion", + "id": "a_render_showcase", "x": 970, "y": 550, "width": 0, @@ -1159,7 +1159,7 @@ ] }, "endBinding": { - "elementId": "remotion", + "elementId": "showcase", "fixedPoint": [ 0.5, 0 @@ -1329,7 +1329,7 @@ "link": null, "locked": false, "type": "arrow", - "id": "a_remotion_verify", + "id": "a_showcase_verify", "x": 1120, "y": 692, "width": 125, @@ -1346,7 +1346,7 @@ ], "lastCommittedPoint": null, "startBinding": { - "elementId": "remotion", + "elementId": "showcase", "fixedPoint": [ 1, 0.5 diff --git a/plugins/droid-control/diagrams/capture-compose-verify.svg b/plugins/droid-control/diagrams/capture-compose-verify.svg index 0f664f9..c57940b 100644 --- a/plugins/droid-control/diagrams/capture-compose-verify.svg +++ b/plugins/droid-control/diagrams/capture-compose-verify.svg @@ -80,7 +80,7 @@ longest-clip duration -Remotion Showcase +fframes Showcase layout + presets window chrome diff --git a/plugins/droid-control/fframes/.gitignore b/plugins/droid-control/fframes/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/plugins/droid-control/fframes/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/plugins/droid-control/fframes/Cargo.lock b/plugins/droid-control/fframes/Cargo.lock new file mode 100644 index 0000000..e2ae47b --- /dev/null +++ b/plugins/droid-control/fframes/Cargo.lock @@ -0,0 +1,2781 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "console" +version = "0.16.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + +[[package]] +name = "const_panic" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3" +dependencies = [ + "typewit", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "droid-showcase" +version = "0.1.0" +dependencies = [ + "clap", + "fast_image_resize", + "fframes", + "multiversion", + "serde", + "serde_json", + "serde_path_to_error", + "signal-hook", + "svgr", + "syntect", + "tempfile", + "two-face", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "either_n" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c91ae510829160d5cfb19eb4ae7b6e01d44b767ca8f727c6cee936e53cc9ae5" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fast_image_resize" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c50201dc184ba6553da1695aac20a042efffbe2d84542cee31917c86c3ab1e" +dependencies = [ + "cfg-if", + "document-features", + "num-traits", + "thiserror", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ffmpeg-sys-fframes" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc8123240ea935f4d766323979843650afd6d00b475c561d3bb8a59a2e6e31f" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "fframes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004b1d0fec651ea8c5f62aa5be88696ffeeb6dcf37b194ce5c13b68b188f1016" +dependencies = [ + "colored", + "fframes-media", + "fframes-media-dir-macro", + "futures", + "image", + "indicatif", + "kamadak-exif", + "konst", + "lazy_static", + "libm", + "log", + "lru", + "memmap", + "microfft", + "num", + "once_cell", + "parking_lot", + "rangemap", + "rayon", + "serde", + "svgr", + "svgr-macro", + "ttf-parser 0.25.1", + "usvgr", + "uuid", + "wasm-bindgen", +] + +[[package]] +name = "fframes-media" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b7ac1e484226f05bfac8ee35de8949dd3c51425fec1cb84d5e17313a0c3177" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "ffmpeg-sys-fframes", + "image", + "itoa", + "libc", + "ttf-parser 0.25.1", + "usvgr", + "wasm-bindgen", + "webvtt-parser", +] + +[[package]] +name = "fframes-media-dir-macro" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0011bdfc7620d44fd128ea3f49b1d63767bac78f40d8b8db592e4119d9e166" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "fframes-media", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kamadak-exif" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "konst" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97feab15b395d1860944abe6a8dd8ed9f8eadfae01750fada8427abda531d887" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + +[[package]] +name = "kurbo" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "microfft" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b6673eb0cc536241d6734c2ca45abfdbf90e9e7791c66a36a7ba3c315b76cf" +dependencies = [ + "cfg-if", + "num-complex", + "static_assertions", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.7", +] + +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom_locate" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" +dependencies = [ + "bytecount", + "memchr", + "nom 7.1.3", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rust-fuzzy-search" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "smallvec", + "ttf-parser 0.20.0", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self-rust-tokenize" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3191133be4b3e5c19f3a9f23efb784e982060373c0f3c9f3362fcfcb5d1e6" +dependencies = [ + "proc-macro2", + "quote", + "self-rust-tokenize-derive", +] + +[[package]] +name = "self-rust-tokenize-derive" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0d9ce060439c2bc0821a7e8b3123ee1743bebd88101a9357c3405010ed7826" +dependencies = [ + "syn-helpers", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "serde_json" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "svgr" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7693be4358db6d2b20e039a8b9ff93b3736e481ad36ab435ccdb489da104214" +dependencies = [ + "gif 0.12.0", + "jpeg-decoder", + "log", + "pico-args", + "png 0.17.16", + "rgb", + "svgrtypes", + "tiny-skia", + "usvgr", +] + +[[package]] +name = "svgr-macro" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad73ddb90343b270413fcf4e84e2860f836b87ed1f2bfb1627fd149f8a80e5f" +dependencies = [ + "lazy_static", + "proc-macro-crate", + "proc-macro2", + "quote", + "rust-fuzzy-search", + "simplecss", + "syn 2.0.119", + "usvgr", +] + +[[package]] +name = "svgrtypes" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bbca5efc5d159da4ce7ef452d7a65dfd58ed6c71b5454304fd2d9ae9786c6c4" +dependencies = [ + "kurbo 0.8.3", + "proc-macro2", + "quote", + "siphasher 0.3.11", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-helpers" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c391dcee7e7a1efd7dc921922b97b4be54129a195b6baec95bb2a223203d9d" +dependencies = [ + "either_n", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "regex-syntax", + "serde", + "serde_derive", + "thiserror", + "walkdir", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "two-face" +version = "0.5.2+bat-0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915be7adc2ff6f4338acbf71f3eda0a146f46003b992a1669c674308610efdac" +dependencies = [ + "serde", + "serde_derive", + "syntect", +] + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "usvgr" +version = "0.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6aeccea458c5a0ca94be0f1a5eabf44d4b754316101569f3cae399c53debb5" +dependencies = [ + "ahash", + "base64 0.21.7", + "data-url", + "flate2", + "fontdb", + "kurbo 0.11.3", + "log", + "lru", + "pico-args", + "quote", + "roxmltree", + "rustybuzz", + "self-rust-tokenize", + "simplecss", + "siphasher 1.0.4", + "smallvec", + "strict-num", + "svgrtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.7", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webvtt-parser" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579514a240693530a03c73287e3cad2a9d4a984863b39cb64c0c65fd64096be0" +dependencies = [ + "nom 7.1.3", + "nom_locate", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/plugins/droid-control/fframes/Cargo.toml b/plugins/droid-control/fframes/Cargo.toml new file mode 100644 index 0000000..cc70265 --- /dev/null +++ b/plugins/droid-control/fframes/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "droid-showcase" +version = "0.1.0" +edition = "2024" +publish = false +description = "droid-control showcase composition, rendered with fframes" + +[dependencies] +clap = { version = "4", features = ["derive"] } +fast_image_resize = { version = "6.1", features = ["only_u8x4"] } +fframes = { version = "=1.2.0", features = ["compile-time-svgtree"] } +multiversion = "0.9" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_path_to_error = "0.1" +signal-hook = "0.4" +svgr = "=0.46.1" +tempfile = "3" +syntect = { version = "5.3", default-features = false, features = ["parsing", "default-syntaxes", "regex-fancy"] } +two-face = { version = "0.5", default-features = false, features = ["syntect-fancy"] } + +[lib] +name = "droid_showcase" +path = "src/lib.rs" +doctest = false + +[[bin]] +name = "droid-showcase" +path = "src/main.rs" + +# A standalone workspace: the plugin repository has no Cargo workspace of its own. +[workspace] + +[profile.dev] +opt-level = 1 + +[profile.dev.package."*"] +opt-level = 3 diff --git a/plugins/droid-control/fframes/media/GEIST-OFL.txt b/plugins/droid-control/fframes/media/GEIST-OFL.txt new file mode 100644 index 0000000..8d003fe --- /dev/null +++ b/plugins/droid-control/fframes/media/GEIST-OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2023 Vercel, in collaboration with basement.studio + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plugins/droid-control/fframes/media/Geist-Bold.ttf b/plugins/droid-control/fframes/media/Geist-Bold.ttf new file mode 100644 index 0000000..863b286 Binary files /dev/null and b/plugins/droid-control/fframes/media/Geist-Bold.ttf differ diff --git a/plugins/droid-control/fframes/media/Geist-Light.ttf b/plugins/droid-control/fframes/media/Geist-Light.ttf new file mode 100644 index 0000000..3f16cc7 Binary files /dev/null and b/plugins/droid-control/fframes/media/Geist-Light.ttf differ diff --git a/plugins/droid-control/fframes/media/Geist-Medium.ttf b/plugins/droid-control/fframes/media/Geist-Medium.ttf new file mode 100644 index 0000000..96cb22f Binary files /dev/null and b/plugins/droid-control/fframes/media/Geist-Medium.ttf differ diff --git a/plugins/droid-control/fframes/media/Geist-Regular.ttf b/plugins/droid-control/fframes/media/Geist-Regular.ttf new file mode 100644 index 0000000..4da1b3a Binary files /dev/null and b/plugins/droid-control/fframes/media/Geist-Regular.ttf differ diff --git a/plugins/droid-control/fframes/media/Geist-SemiBold.ttf b/plugins/droid-control/fframes/media/Geist-SemiBold.ttf new file mode 100644 index 0000000..b2b0618 Binary files /dev/null and b/plugins/droid-control/fframes/media/Geist-SemiBold.ttf differ diff --git a/plugins/droid-control/fframes/media/GeistMono-Medium.ttf b/plugins/droid-control/fframes/media/GeistMono-Medium.ttf new file mode 100644 index 0000000..ff49ece Binary files /dev/null and b/plugins/droid-control/fframes/media/GeistMono-Medium.ttf differ diff --git a/plugins/droid-control/fframes/media/GeistMono-Regular.ttf b/plugins/droid-control/fframes/media/GeistMono-Regular.ttf new file mode 100644 index 0000000..50c9d5a Binary files /dev/null and b/plugins/droid-control/fframes/media/GeistMono-Regular.ttf differ diff --git a/plugins/droid-control/remotion/public/bg-halftone-rotor.jpg b/plugins/droid-control/fframes/media/bg-halftone-rotor.png similarity index 100% rename from plugins/droid-control/remotion/public/bg-halftone-rotor.jpg rename to plugins/droid-control/fframes/media/bg-halftone-rotor.png diff --git a/plugins/droid-control/fframes/rustfmt.toml b/plugins/droid-control/fframes/rustfmt.toml new file mode 100644 index 0000000..c775577 --- /dev/null +++ b/plugins/droid-control/fframes/rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 120 +use_small_heuristics = "Max" diff --git a/plugins/droid-control/fframes/src/clips.rs b/plugins/droid-control/fframes/src/clips.rs new file mode 100644 index 0000000..122edf7 --- /dev/null +++ b/plugins/droid-control/fframes/src/clips.rs @@ -0,0 +1,145 @@ +//! Clip sources, probed once with ffprobe: the longest one sets the content length and +//! every clip holds its own last frame once it ends. + +use crate::timing::FPS; +use serde::Deserialize; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug, Clone, PartialEq)] +pub struct Clip { + /// The file name: fframes keys media and decoders on it, so it is unique per render. + pub name: String, + pub path: PathBuf, + /// Container duration in source seconds. + pub duration: f64, + /// Output-frame offset of the last frame; playback holds it after the clip ends. + pub last_frame: usize, +} + +#[derive(Deserialize)] +struct Probe { + format: ProbeFormat, + #[serde(default)] + packets: Vec, +} + +#[derive(Deserialize)] +struct ProbeFormat { + duration: Option, +} + +#[derive(Deserialize)] +struct ProbePacket { + pts_time: Option, +} + +impl Clip { + pub fn probe_all(paths: &[PathBuf]) -> Result, String> { + let mut names = HashSet::new(); + paths + .iter() + .map(|path| { + let clip = Clip::probe(path)?; + if !names.insert(clip.name.clone()) { + return Err(format!( + "clips must have distinct file names, {} repeats one (stage them as clip-.)", + path.display() + )); + } + Ok(clip) + }) + .collect() + } + + fn probe(path: &Path) -> Result { + let output = Command::new("ffprobe") + .args([ + "-v", + "error", + "-select_streams", + "v:0", + "-show_entries", + "format=duration:packet=pts_time", + "-of", + "json", + ]) + .arg(path) + .output() + .map_err(|err| format!("could not run ffprobe: {err}"))?; + if !output.status.success() { + return Err(format!( + "ffprobe could not read clip {}: {}", + path.display(), + String::from_utf8_lossy(&output.stderr).trim() + )); + } + let probe: Probe = serde_json::from_slice(&output.stdout) + .map_err(|err| format!("unexpected ffprobe output for {}: {err}", path.display()))?; + Clip::from_probe(path, &probe) + } + + fn from_probe(path: &Path, probe: &Probe) -> Result { + let seconds = |value: &Option| value.as_deref().and_then(|v| v.parse::().ok()); + let duration = seconds(&probe.format.duration) + .ok_or_else(|| format!("ffprobe could not read a duration from clip: {}", path.display()))?; + let last_pts = probe + .packets + .iter() + .filter_map(|packet| seconds(&packet.pts_time)) + .fold(None, |max: Option, pts| Some(max.map_or(pts, |m| m.max(pts)))) + .ok_or_else(|| format!("clip has no video frames: {}", path.display()))?; + let name = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| format!("clip path has no UTF-8 file name: {}", path.display()))? + .to_owned(); + Ok(Clip { + name, + path: path.to_owned(), + duration, + // The decoder shows the first frame at or after the requested time, so the last + // frame's own timestamp (not the container end) is the latest offset to ask for. + last_frame: (last_pts * FPS as f64 + 1e-6).floor().max(0.) as usize, + }) + } +} + +/// The longest clip in source seconds, rounded to centiseconds. +pub fn longest(clips: &[Clip]) -> Option { + clips.iter().map(|clip| (clip.duration * 100.).round() / 100.).reduce(f64::max) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn probe(json: &str) -> Result { + Clip::from_probe(Path::new("/tmp/clip-0.mp4"), &serde_json::from_str(json).unwrap()) + } + + #[test] + fn last_frame_comes_from_the_last_presented_packet() { + let clip = probe( + r#"{"format":{"duration":"5.000000"},"packets":[{"pts_time":"0.000000"},{"pts_time":"4.966667"},{"pts_time":"4.933333"}]}"#, + ) + .unwrap(); + assert_eq!(clip.name, "clip-0.mp4"); + assert_eq!(clip.duration, 5.); + assert_eq!(clip.last_frame, 149); + } + + #[test] + fn unreadable_probes_are_errors() { + assert!(probe(r#"{"format":{"duration":"N/A"},"packets":[{"pts_time":"0"}]}"#).is_err()); + assert!(probe(r#"{"format":{"duration":"1.0"},"packets":[]}"#).is_err()); + } + + #[test] + fn longest_rounds_to_centiseconds() { + let clip = |duration| Clip { name: String::new(), path: PathBuf::new(), duration, last_frame: 0 }; + assert_eq!(longest(&[clip(1.234), clip(5.006)]), Some(5.01)); + assert_eq!(longest(&[]), None); + } +} diff --git a/plugins/droid-control/fframes/src/code.rs b/plugins/droid-control/fframes/src/code.rs new file mode 100644 index 0000000..f5be9c9 --- /dev/null +++ b/plugins/droid-control/fframes/src/code.rs @@ -0,0 +1,103 @@ +//! Syntax highlighting for code annotations in the colours of prism's vsDark theme, with +//! the palette's text colour for unclassified tokens. + +use crate::svg::Rgba; +use std::str::FromStr; +use std::sync::LazyLock; +use syntect::easy::HighlightLines; +use syntect::highlighting::{Color, ScopeSelectors, StyleModifier, Theme, ThemeItem, ThemeSettings}; +use syntect::parsing::{SyntaxReference, SyntaxSet}; +use syntect::util::LinesWithEndings; + +static SYNTAXES: LazyLock = LazyLock::new(two_face::syntax::extra_newlines); + +/// prism-react-renderer's vsDark token colours, keyed by TextMate scope. +const VS_DARK: &[(&str, (u8, u8, u8))] = &[ + ("comment, punctuation.definition.comment", (106, 153, 85)), + ( + "keyword, storage, constant.language.null, constant.language.undefined, variable.language, support.function.builtin", + (86, 156, 214), + ), + ("keyword.operator, punctuation", (212, 212, 212)), + ("constant.numeric", (181, 206, 168)), + ("string, punctuation.definition.string", (206, 145, 120)), + ("entity.name.tag", (78, 201, 176)), + ("entity.other.attribute-name", (156, 220, 254)), + ("entity.name.function, support.function, meta.function-call entity.name", (220, 220, 170)), + ("entity.name.type, entity.name.class, support.class, support.type, entity.other.inherited-class", (78, 201, 176)), + ("constant.character", (209, 105, 105)), +]; + +fn theme(plain: Rgba) -> Theme { + let color = |(r, g, b): (u8, u8, u8)| Color { r, g, b, a: 255 }; + Theme { + settings: ThemeSettings { foreground: Some(color((plain.0, plain.1, plain.2))), ..ThemeSettings::default() }, + scopes: VS_DARK + .iter() + .map(|(scope, rgb)| ThemeItem { + scope: ScopeSelectors::from_str(scope).expect("valid scope selectors"), + style: StyleModifier { foreground: Some(color(*rgb)), ..StyleModifier::default() }, + }) + .collect(), + ..Theme::default() + } +} + +fn syntax(language: &str) -> &'static SyntaxReference { + let token = match language.to_ascii_lowercase().as_str() { + "shell" | "zsh" | "console" => "bash".to_owned(), + "markup" | "svg" | "xml" => "html".to_owned(), + other => other.to_owned(), + }; + SYNTAXES.find_syntax_by_token(&token).unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()) +} + +/// Coloured tokens per line of `code` (one trailing newline dropped, like the overlay). +pub fn highlight(code: &str, language: &str, plain: Rgba) -> Vec> { + let theme = theme(plain); + let mut highlighter = HighlightLines::new(syntax(language), &theme); + let code = code.strip_suffix('\n').unwrap_or(code); + LinesWithEndings::from(code) + .map(|line| { + let tokens = highlighter.highlight_line(line, &SYNTAXES).unwrap_or_default(); + tokens + .into_iter() + .map(|(style, text)| { + let c = style.foreground; + (Rgba(c.r, c.g, c.b, 1.), text.trim_end_matches(['\n', '\r']).to_owned()) + }) + .filter(|(_, text)| !text.is_empty()) + .collect() + }) + .chain(code.ends_with('\n').then(Vec::new)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEXT: Rgba = Rgba(240, 232, 224, 1.); + + fn color_of(lines: &[Vec<(Rgba, String)>], token: &str) -> Rgba { + lines.iter().flatten().find(|(_, text)| text.contains(token)).unwrap().0 + } + + #[test] + fn tsx_tokens_take_vs_dark_colours() { + let lines = highlight("const answer = \"42\"; // why\n", "tsx", TEXT); + assert_eq!(lines.len(), 1); + assert_eq!(color_of(&lines, "const"), Rgba(86, 156, 214, 1.)); + assert_eq!(color_of(&lines, "42"), Rgba(206, 145, 120, 1.)); + assert_eq!(color_of(&lines, "why"), Rgba(106, 153, 85, 1.)); + assert_eq!(color_of(&lines, "answer"), TEXT); + } + + #[test] + fn unknown_languages_render_plain_and_keep_blank_lines() { + let lines = highlight("a\n\nb", "klingon", TEXT); + assert_eq!(lines.len(), 3); + assert!(lines[1].is_empty()); + assert!(lines.iter().flatten().all(|(color, _)| *color == TEXT)); + } +} diff --git a/plugins/droid-control/fframes/src/composite.rs b/plugins/droid-control/fframes/src/composite.rs new file mode 100644 index 0000000..1e34c4b --- /dev/null +++ b/plugins/droid-control/fframes/src/composite.rs @@ -0,0 +1,396 @@ +//! Compositing in Rust on 8-bit premultiplied pixels: layers blended in at whole pixels, +//! resampling, blurs and the final colour grade. tiny-skia runs every image, faded group and +//! `mix-blend-mode` through its floating point pipeline, a full pass per layer per frame. + +use crate::svg::{Rect, Rgba}; +use fast_image_resize::images::{TypedImage, TypedImageRef}; +use fast_image_resize::pixels::U8x4; +use fast_image_resize::{FilterType, ResizeAlg, ResizeOptions, Resizer}; +use fframes::usvgr::PreloadedImageData; +use multiversion::multiversion; +use std::array; +use std::borrow::Cow; +use std::cell::RefCell; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// Premultiplied RGBA pixels being composited: a frame, or a layer of one. +#[derive(Clone)] +pub struct Pixels { + pub width: u32, + pub height: u32, + pub data: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Blend { + Over, + Screen, +} + +/// `v / 255`, rounded to nearest, for `v` up to `255 * 255`. +fn div255(v: u16) -> u16 { + let v = v + 128; + (v + (v >> 8)) >> 8 +} + +/// Loads a channel like tiny-skia. +fn unit(channel: u8) -> f32 { + f32::from(channel) * (1. / 255.) +} + +/// Stores a channel like tiny-skia, clamped to 0..1 (by the saturating cast) and rounded; +/// exact halves round up rather than to even, which no blend here lands on in practice. +fn store(value: f32) -> u8 { + (value * 255. + 0.5) as u8 +} + +/// Luminosity as the non-separable blend modes define it. +fn lum([r, g, b]: [f32; 3]) -> f32 { + r * 0.30 + g * 0.59 + b * 0.11 +} + +impl Pixels { + pub fn transparent(width: u32, height: u32) -> Self { + Self { width, height, data: vec![0; width as usize * height as usize * 4] } + } + + /// A copy of `image` to composite onto. + pub fn of(image: &PreloadedImageData) -> Self { + Self { width: image.width, height: image.height, data: image.data.to_vec() } + } + + /// Blends `layer`, placed at whole pixel `at` and faded by `opacity`, onto these pixels: + /// tiny-skia's blend modes on every channel alike, in 8-bit fixed point (within a level + /// of its floating point pipeline). Runs of pixels go through branch-free loops, which the + /// compiler vectorises; source-over skips transparent runs and copies opaque ones. + pub fn blend(&mut self, layer: &PreloadedImageData, at: (u32, u32), opacity: f32, mode: Blend) { + let o = (opacity.clamp(0., 1.) * 255. + 0.5) as u16; + if o > 0 { + blend(self, layer, at, o, mode); + } + } + + /// How far `blur(sigma)` spreads a pixel along each axis. + pub fn blur_reach(sigma: f32) -> usize { + box_sizes(sigma).iter().map(|size| (size - 1) / 2).sum() + } + + /// A Gaussian blur of `sigma` pixels, approximated like svgr's `feGaussianBlur`: five box + /// blurs per axis, with the pixels outside counting as transparent black. + pub fn blur(&mut self, sigma: f32) { + let width = self.width as usize; + let pixels = self.data.as_chunks_mut::<4>().0; + let mut back = vec![[0; 4]; pixels.len()]; + for size in box_sizes(sigma) { + let radius = (size - 1) / 2; + blur_columns(radius, width, pixels, &mut back); + for (src, dst) in back.chunks_exact(width).zip(pixels.chunks_exact_mut(width)) { + blur_row(radius, src, dst); + } + } + } + + /// These pixels as an `` source. + pub fn into_image(self) -> Arc { + // svgr keys resampled copies of an image by its id and address, and a freed image's + // address is soon reused: a fresh id keeps a new image from hitting a stale copy. + static NEXT: AtomicUsize = AtomicUsize::new(0); + let id = format!("pixels-{}", NEXT.fetch_add(1, Ordering::Relaxed)); + Arc::new(PreloadedImageData { data: Cow::Owned(self.data), width: self.width, height: self.height, id }) + } +} + +/// The body of [`Pixels::blend`], compiled for each SIMD level the CPU may have. +#[multiversion(targets = "simd")] +fn blend(pixels: &mut Pixels, layer: &PreloadedImageData, (x, y): (u32, u32), o: u16, mode: Blend) { + let columns = layer.width.min(pixels.width.saturating_sub(x)) as usize; + let rows = layer.height.min(pixels.height.saturating_sub(y)); + const RUN: usize = 64; + // Per channel of a run: what source-over keeps of the destination, `1 - sa`. + let mut keep = [0u8; RUN]; + for row in 0..rows { + let src = (row * layer.width) as usize * 4; + let dst = (((y + row) * pixels.width + x) as usize) * 4; + let src = &layer.data[src..src + columns * 4]; + let dst = &mut pixels.data[dst..dst + columns * 4]; + match mode { + Blend::Over => { + for (dst, src) in dst.chunks_mut(RUN).zip(src.chunks(RUN)) { + // Premultiplied: a transparent pixel is all zeros. + let words = src.as_chunks::<4>().0.iter().map(|s| u32::from_le_bytes(*s)); + let (any, all) = words.fold((0, !0), |(any, all), s| (any | s, all & s)); + if any == 0 { + continue; + } + if all >> 24 == 255 && o == 255 { + dst.copy_from_slice(src); + continue; + } + for (keep, s) in keep.as_chunks_mut::<4>().0.iter_mut().zip(src.as_chunks::<4>().0) { + *keep = [255 - div255(u16::from(s[3]) * o) as u8; 4]; + } + for ((d, s), keep) in dst.iter_mut().zip(src).zip(&keep) { + *d = (div255(u16::from(*s) * o) + div255(u16::from(*d) * u16::from(*keep))) as u8; + } + } + } + Blend::Screen => { + for (d, s) in dst.iter_mut().zip(src) { + let (s, d0) = (div255(u16::from(*s) * o), u16::from(*d)); + *d = (s + d0 - div255(s * d0)) as u8; + } + } + } + } +} + +/// `crop` (in pixels, not necessarily whole) of the `size` RGBA pixels `data`, resampled to +/// `width` by `height` pixels with the Mitchell filter of tiny-skia's bicubic sampling. +/// Resampled once here, an image is then drawn 1:1 instead of sampled with sixteen taps per +/// output pixel. +pub fn resample(data: &[u8], size: (u32, u32), crop: Rect, (width, height): (u32, u32)) -> Pixels { + thread_local! { + static RESIZER: RefCell = RefCell::new(Resizer::new()); + } + let source = TypedImageRef::::from_buffer(size.0, size.1, data).expect("images hold their size in pixels"); + let mut resampled = vec![0; width as usize * height as usize * 4]; + let mut target = TypedImage::::from_buffer(width, height, &mut resampled).expect("sized for the target"); + let options = ResizeOptions::new() + .resize_alg(ResizeAlg::Convolution(FilterType::Mitchell)) + .crop(f64::from(crop.x), f64::from(crop.y), f64::from(crop.w), f64::from(crop.h)) + // Premultiplied already. + .use_alpha(false); + RESIZER + .with_borrow_mut(|resizer| resizer.resize_typed(&source, &mut target, &options)) + .expect("source and target are both RGBA8"); + Pixels { width, height, data: resampled } +} + +/// Widths of the five box blurs that approximate a Gaussian of `sigma`. +fn box_sizes(sigma: f32) -> [usize; 5] { + const STEPS: f32 = 5.; + let ideal = (12. * sigma * sigma / STEPS).sqrt() + 1.; + let lower = ideal.floor() as usize; + let lower = if lower.is_multiple_of(2) { lower - 1 } else { lower }; + let l = lower as f32; + let narrow = + ((12. * sigma * sigma - STEPS * l * l - 4. * STEPS * l - 3. * STEPS) / (-4. * l - 4.)).round() as usize; + array::from_fn(|i| if i < narrow { lower } else { lower + 2 }) +} + +/// The mean of a box blur window, rounded like svgr: to nearest, ties to even, by adding and +/// taking away 1.5 * 2^23, which leaves no fraction bits (`round_ties_even` is a libm call +/// on baseline x86-64). +fn mean(sum: [i32; 4], scale: f32) -> [u8; 4] { + const SNAP: f32 = 12_582_912.; + sum.map(|s| (s as f32 * scale + SNAP - SNAP) as u8) +} + +fn accumulate(sum: &mut [i32; 4], px: [u8; 4], sign: i32) { + for c in 0..4 { + sum[c] += sign * i32::from(px[c]); + } +} + +/// A vertical box blur of `2 radius + 1` rows, with the window sums kept for a whole row so +/// memory is walked in order. +#[multiversion(targets = "simd")] +fn blur_columns(radius: usize, width: usize, src: &[[u8; 4]], dst: &mut [[u8; 4]]) { + let height = src.len() / width; + let scale = 1. / (2 * radius + 1) as f32; + let row = |y: usize| &src[y * width..(y + 1) * width]; + let add = |sums: &mut [[i32; 4]], y: usize, sign: i32| { + sums.iter_mut().zip(row(y)).for_each(|(sum, px)| accumulate(sum, *px, sign)); + }; + let mut sums = vec![[0; 4]; width]; + for y in 0..radius.min(height) { + add(&mut sums, y, 1); + } + for (y, out) in dst.chunks_exact_mut(width).enumerate() { + if y + radius < height { + add(&mut sums, y + radius, 1); + } + out.iter_mut().zip(&sums).for_each(|(px, sum)| *px = mean(*sum, scale)); + if y >= radius { + add(&mut sums, y - radius, -1); + } + } +} + +/// A horizontal box blur of `2 radius + 1` pixels along one row. +#[multiversion(targets = "simd")] +fn blur_row(radius: usize, src: &[[u8; 4]], dst: &mut [[u8; 4]]) { + let scale = 1. / (2 * radius + 1) as f32; + let mut sum = [0; 4]; + for px in &src[..radius.min(src.len())] { + accumulate(&mut sum, *px, 1); + } + for (x, out) in dst.iter_mut().enumerate() { + if let Some(px) = src.get(x + radius) { + accumulate(&mut sum, *px, 1); + } + *out = mean(sum, scale); + if x >= radius { + accumulate(&mut sum, src[x - radius], -1); + } + } +} + +/// Three static layers over opaque frames: overlay-blended grain, a full cover of one colour +/// with `mix-blend-mode: color`, and a black vignette, in fixed point a pixel at a time. +pub struct Grade { + /// Per channel of every pixel: the faded grain's overlay strength `k = 2 s - sa` (in + /// 2^-15) on the colour channels, what the vignette keeps (in 255ths) on alpha. + terms: Vec, + /// Per colour channel: the tint's hue term, its colour minus its luminosity (in 1/256 + /// levels). + hue: [i32; 3], + /// The tint's alpha per luminosity unit (in 2^-12 levels per unit of `30 r + 59 g + 11 b`). + lift: i32, + /// How fast the hue term is clipped above the luminosity where it would overflow (in + /// 2^-24 per luminosity unit), see `grade`. + clip: i32, + /// What the tint leaves of the destination, `1 - alpha` (in 2^-16). + keep: i32, +} + +impl Grade { + /// `grain` faded by `opacity`, then `tint`, then `vignette` (black at varying alpha); the + /// layers are frame-sized. + pub fn new(grain: &Pixels, opacity: f32, tint: Rgba, vignette: &Pixels) -> Self { + let terms = grain + .data + .as_chunks::<4>() + .0 + .iter() + .zip(vignette.data.as_chunks::<4>().0) + .flat_map(|(g, v)| { + let k = |c: usize| ((2. * unit(g[c]) - unit(g[3])) * opacity * 32768.).round() as i16; + [k(0), k(1), k(2), 255 - i16::from(v[3])] + }) + .collect(); + // The colour is filled into an 8-bit layer before it is blended. + let alpha = unit(store(tint.3)); + let s = [tint.0, tint.1, tint.2].map(|channel| unit(store(unit(channel) * tint.3))); + let hue = s.map(|channel| channel - lum(s)); + let widest = hue.iter().copied().fold(0., f32::max); + Self { + terms, + hue: hue.map(|h| (h * 255. * 256.).round() as i32), + lift: (alpha * 255. * 4096. / 25500.).round() as i32, + clip: (alpha / (widest * 25500.) * 16_777_216.).round().min(65536.) as i32, + keep: ((1. - alpha) * 65536.).round() as i32, + } + } + + pub fn apply(&self, frame: &mut Pixels) { + grade(self, frame); + } +} + +/// The body of [`Grade::apply`], compiled for each SIMD level the CPU may have. +#[multiversion(targets = "simd")] +fn grade(grade: &Grade, frame: &mut Pixels) { + let &Grade { hue, lift, clip, keep, .. } = grade; + for (px, terms) in frame.data.as_chunks_mut::<4>().0.iter_mut().zip(grade.terms.as_chunks::<4>().0) { + // Overlay onto an opaque `d`: `d (1 + k)` up to `d = 0.5`, `d (1 - k) + k` above. + let grained: [i32; 3] = array::from_fn(|c| { + let d = i32::from(px[c]); + d + ((i32::from(terms[c]) * d.min(255 - d) + (1 << 14)) >> 15) + }); + // The colour blend sets the tint's colour to the destination's luminosity `l`: the + // hue term over `l * alpha`, scaled down toward it where the brightest channel would + // pass `alpha` (tiny-skia's clip_color), and none of it below zero. + let l = 30 * grained[0] + 59 * grained[1] + 11 * grained[2]; + let clipped = (((25500 - l) * clip) >> 12).min(4096); + let lifted = (l * lift) >> 4; + for c in 0..3 { + let tinted = (lifted + ((hue[c] * clipped) >> 12)).max(0); + let level = (((grained[c] * keep) >> 8) + tinted + 128) >> 8; + px[c] = div255((level.min(255) * i32::from(terms[3])) as u16) as u8; + } + px[3] = 255; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn solid(width: u32, height: u32, px: [u8; 4]) -> Pixels { + Pixels { width, height, data: px.repeat((width * height) as usize) } + } + + #[test] + fn layers_blend_only_where_they_are_placed() { + let mut frame = solid(3, 2, [100, 100, 100, 255]); + frame.blend(&solid(1, 1, [128, 0, 0, 128]).into_image(), (2, 1), 1., Blend::Over); + assert_eq!(frame.data[..20], [100, 100, 100, 255].repeat(5)); + assert_eq!(frame.data[20..], [178, 50, 50, 255]); + } + + #[test] + fn blur_matches_svgr_gaussian_blur() { + let (width, height) = (23, 17); + let mut seed = 0x9e37_79b9u32; + let data = (0..width * height) + .flat_map(|_| { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + let a = (seed >> 24) as u8; + [seed as u8, (seed >> 8) as u8, (seed >> 16) as u8].map(|c| c.min(a)).into_iter().chain([a]) + }) + .collect(); + let mut blurred = Pixels { width, height, data }; + let area = Rect::new(0., 0., width as f32, height as f32); + let image = blurred.clone().into_image(); + let expected = crate::raster::rasterize( + fframes::svgr!( + + {crate::svg::blur("b".to_owned(), 2.5, area)} + + + + + ), + area, + 1., + ); + blurred.blur(2.5); + assert!(blurred.data == expected.data); + } + + #[test] + fn screen_follows_its_formula() { + let mut screened = solid(1, 1, [51, 204, 0, 255]); + screened.blend(&solid(1, 1, [102, 102, 102, 255]).into_image(), (0, 0), 1., Blend::Screen); + assert_eq!(screened.data, [133, 224, 102, 255]); + } + + fn graded(d: [u8; 4], grain: [u8; 4], tint: Rgba, vignette: u8) -> Vec { + let mut frame = solid(1, 1, d); + Grade::new(&solid(1, 1, grain), 1., tint, &solid(1, 1, [0, 0, 0, vignette])).apply(&mut frame); + frame.data + } + + #[test] + fn grain_is_overlay_blended() { + let none = Rgba::BLACK.alpha(0.); + assert_eq!(graded([51, 204, 255, 255], [102, 102, 102, 255], none, 0), [41, 194, 255, 255]); + } + + #[test] + fn the_tint_keeps_luminosity_and_takes_the_hue() { + let grey = graded([128, 128, 128, 255], [0, 0, 0, 0], Rgba(255, 0, 0, 1.), 0); + assert!(grey[0] > grey[1] && grey[1] == grey[2], "{grey:?}"); + let lum = 0.30 * f32::from(grey[0]) + 0.59 * f32::from(grey[1]) + 0.11 * f32::from(grey[2]); + assert!((lum - 128.).abs() <= 1., "{grey:?}"); + } + + #[test] + fn the_vignette_darkens_by_its_alpha() { + assert_eq!(graded([200, 100, 0, 255], [0, 0, 0, 0], Rgba::BLACK.alpha(0.), 51), [160, 80, 0, 255]); + } +} diff --git a/plugins/droid-control/fframes/src/content.rs b/plugins/droid-control/fframes/src/content.rs new file mode 100644 index 0000000..051086e --- /dev/null +++ b/plugins/droid-control/fframes/src/content.rs @@ -0,0 +1,365 @@ +//! The clip layer: window chrome around each clip, the panel entrances, directed zooms and +//! spotlights. Everything here runs on the clip clock (frames since the first clip frame). + +use crate::clips::Clip; +use crate::composite::{self, Blend, Pixels}; +use crate::fonts::Font; +use crate::motion::{Bezier, interpolate_eased}; +use crate::props::{Effect, Layout, ObjectFit, Region}; +use crate::raster; +use crate::svg::{Anchor, Card, Rect, Rgba, Shadow, Similarity, TextStyle, faded, rounded_rect_d}; +use crate::theme::{Bar, BarSide, Palette, PresetConfig}; +use fframes::usvgr::PreloadedImageData; +use fframes::{FFramesContext, FFramesSyncedVideoFrame, Frame, Svgr, SyncVideoFrameInput, svgr}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::Arc; + +/// An image and the whole screen pixels it is drawn 1:1 at. +type Layer = (Arc, Rect); + +/// What a window showed last, so that a window or clip holding still is drawn once. +#[derive(Default)] +struct Shown { + /// The chrome (see `Stage::chrome`) and the transform it was drawn by. + chrome: Option<(Similarity, Layer)>, + /// The clip frame: as decoded, and as drawn (the `crop` of it resampled to the screen). + clip: Option<(Arc, Rect, Layer)>, +} + +thread_local! { + /// By window id. + static SHOWN: RefCell> = RefCell::default(); +} + +const BAR_HEIGHT: f32 = 36.; +const SIDE_BY_SIDE_GAP: f32 = 16.; + +const WARM_SHADOWS: [Shadow; 3] = [ + Shadow::new(8., 60., Rgba(30, 12, 4, 0.6)), + Shadow::new(2., 20., Rgba(60, 24, 8, 0.3)), + Shadow::new(0., 120., Rgba(238, 96, 24, 0.07)), +]; +const COOL_SHADOWS: [Shadow; 2] = [Shadow::new(8., 40., Rgba(0, 0, 0, 0.45)), Shadow::new(2., 12., Rgba(0, 0, 0, 0.3))]; + +/// What the clip layer draws from: the composition's look and its clip sources. +pub struct Stage<'s> { + pub palette: &'s Palette, + pub config: &'s PresetConfig, + pub size: (f32, f32), + pub layout: Layout, + pub clips: &'s [Clip], + pub labels: &'s [String], + pub window_title: &'s str, + pub speed: f64, + pub object_fit: ObjectFit, + pub effects: &'s [Effect], +} + +impl Stage<'_> { + /// Composites the windows with their clips at `frame` on the clip clock, inside every + /// zoom, and the spotlights over them onto `canvas`. + pub fn compose(&self, canvas: &mut Pixels, frame: usize, video: &Frame, ctx: &FFramesContext<'_, '_>) { + let (w, h) = self.size; + let m = self.config.margin; + let f = frame as f32; + let entrance = |delay: f32| interpolate_eased(f, &[delay, delay + 15.], &[0., 1.], Bezier::EXPO_OUT.f()); + let zoom = self.effects.iter().fold(Similarity::IDENTITY, |zoom, effect| match effect { + Effect::Zoom { t, dur, to } => { + let (cx, cy) = ((to.x.0 + to.w.0 / 2.) * w, (to.y.0 + to.h.0 / 2.) * h); + zoom.then(Similarity::scale_about(zoom_scale(f, *t, *dur, to), cx, cy)) + } + _ => zoom, + }); + match self.layout { + Layout::Single => { + if let Some(clip) = self.clips.first() { + let inner = Rect::new(m, m, w - 2. * m, h - 2. * m); + let progress = entrance(0.); + let place = Similarity::scale_about(0.92 + 0.08 * progress, inner.cx(), inner.cy()); + let title = (!self.window_title.is_empty()).then_some(self.window_title); + let window = Window { id: "window0", inner, title, to_screen: place.then(zoom), opacity: progress }; + self.window(canvas, &window, clip, frame, video, ctx); + } + } + Layout::SideBySide => { + let shown = &self.clips[..self.clips.len().min(2)]; + let panel_w = ((w - 2. * m - SIDE_BY_SIDE_GAP) / 2.).floor(); + let panel_h = h - 2. * m; + // Panels are content-box: the 1px border sits outside the panel size. + let row = shown.len() as f32 * (panel_w + 2.) + (shown.len().max(1) - 1) as f32 * SIDE_BY_SIDE_GAP; + let left = (w - row) / 2.; + for (i, clip) in shown.iter().enumerate() { + let inner = Rect::new( + left + i as f32 * (panel_w + 2. + SIDE_BY_SIDE_GAP) + 1., + (h - panel_h) / 2., + panel_w, + panel_h, + ); + let progress = entrance(i as f32 * 6.); + let place = Similarity::translate(0., 12. * (1. - progress)).then(Similarity::scale_about( + 0.92 + 0.08 * progress, + inner.cx(), + inner.cy(), + )); + let label = self.labels.get(i).cloned().unwrap_or_else(|| format!("Clip {}", i + 1)); + let id = format!("window{i}"); + let window = + Window { id: &id, inner, title: Some(&label), to_screen: place.then(zoom), opacity: progress }; + self.window(canvas, &window, clip, frame, video, ctx); + } + } + } + canvas.draw(self.spotlights(frame)); + } + + /// Dimmed frame with a rounded cutout around each active spotlight region. + pub fn spotlights<'a>(&self, frame: usize) -> Svgr<'a> { + let (w, h) = self.size; + let f = frame as f32; + self.effects + .iter() + .filter_map(|effect| match effect { + Effect::Spotlight { t, dur, on, dim } => { + let (start, end) = (t * 30., (t + dur) * 30.); + if f < start || f > end { + return None; + } + let local = f - start; + let total = end - start; + let fade_in = interpolate_eased(local, &[0., 9.], &[0., 1.], Bezier::EXPO_OUT.f()); + let fade_out = crate::motion::interpolate(local, &[total - 9., total], &[1., 0.]); + let hole = Rect::new(on.x.0 * w, on.y.0 * h, on.w.0 * w, on.h.0 * h); + let d = format!("M0 0H{w}V{h}H0Z{}", rounded_rect_d(hole, 8.)); + Some(faded( + fade_in.min(fade_out), + svgr!(), + )) + } + _ => None, + }) + .collect() + } + + /// The clip frame shown `frame` frames after playback started (`speed` source frames per + /// output frame, holding the last frame once the clip ends), fitted into `window` as drawn + /// on screen: its visible part resampled to the whole screen pixels it + /// covers inside the content box, and those pixels. A partly covered edge pixel keeps the + /// window fill. A frame identical to the last one the window showed at the same place + /// keeps its image, so a screen recording holding still is resampled once. + fn clip_frame( + &self, + window: &Window<'_>, + clip: &Clip, + frame: usize, + video: &Frame, + ctx: &FFramesContext<'_, '_>, + ) -> Option { + let &Window { id, inner, to_screen, .. } = window; + let mut at = video.clone(); + at.index = ((frame as f64 * self.speed + 1e-9).floor() as usize).min(clip.last_frame); + let input = SyncVideoFrameInput { start_from: 0., looping: false, editor_fallback_image: None }; + let decoded = at.get_synced_video_frame(ctx, &clip.name, &input)?.into_image().href(); + let size = (decoded.width as f32, decoded.height as f32); + let content = self.content_box(inner); + let (w, h) = self.size; + let bounds = to_screen.apply(content).intersect(Rect::new(0., 0., w, h)); + let (at, crop) = raster::coverage(size, to_screen.apply(fitted(size, content, self.object_fit)), bounds)?; + SHOWN.with_borrow_mut(|shown| { + let last = &mut shown.entry(id.to_owned()).or_default().clip; + if let Some((last, last_crop, layer)) = last + && (layer.1, *last_crop) == (at, crop) + && (last.width, last.height, &last.data) == (decoded.width, decoded.height, &decoded.data) + { + return Some(layer.clone()); + } + let size = (decoded.width, decoded.height); + let image = composite::resample(&decoded.data, size, crop, (at.w as u32, at.h as u32)).into_image(); + Some(last.insert((decoded, crop, (image, at))).2.clone()) + }) + } + + /// A window's chrome, everything but its clip: the card's shadows and its body with `bar`, + /// composited into one layer over the screen pixels they cover. A window that has not + /// moved since it was last drawn keeps its chrome. + fn chrome<'a>(&self, card: &Card<'_>, bar: impl FnOnce() -> Svgr<'a>) -> Layer { + SHOWN.with_borrow_mut(|shown| { + let last = &mut shown.entry(card.id.to_owned()).or_default().chrome; + if let Some((to_screen, layer)) = last + && *to_screen == card.to_screen + { + return layer.clone(); + } + let (w, h) = self.size; + let shadows = card.shadows(); + // The shadows' pixels take in the whole card. + let at = shadows.as_ref().map_or_else( + || card.to_screen.apply(card.rect).whole_pixels().intersect(Rect::new(0., 0., w, h)), + |(_, at)| *at, + ); + let mut layer = Pixels::transparent(at.w as u32, at.h as u32); + if let Some((shadows, from)) = shadows { + layer.blend(&shadows, ((from.x - at.x) as u32, (from.y - at.y) as u32), 1., Blend::Over); + } + let place = card.to_screen.then(Similarity::translate(-at.x, -at.y)); + layer.draw(svgr!({card.body(bar())})); + last.insert((card.to_screen, (layer.into_image(), at))).1.clone() + }) + } + + /// Where a window's clip goes: `inner` below the title bar, inset by the padding. + fn content_box(&self, inner: Rect) -> Rect { + let PresetConfig { bar, padding, .. } = *self.config; + let bar_height = if bar == Bar::None { 0. } else { BAR_HEIGHT }; + Rect::new( + inner.x + padding, + inner.y + bar_height + padding, + inner.w - 2. * padding, + inner.h - bar_height - 2. * padding, + ) + } + + /// A window and its clip, composited onto `canvas` from their screen pixels. The clip lies + /// inside the content box, clear of the bar, the border and the rounded corners (every + /// preset pads it by more than the corners cut in), so it can go on last. + fn window( + &self, + canvas: &mut Pixels, + window: &Window<'_>, + clip: &Clip, + frame: usize, + video: &Frame, + ctx: &FFramesContext<'_, '_>, + ) { + let &Window { id, inner, title, to_screen, opacity } = window; + if opacity <= 0. { + return; + } + let PresetConfig { bar, bar_side, radius, shadow, .. } = *self.config; + let palette = self.palette; + let shadows: &[Shadow] = match (shadow, palette.warm) { + (false, _) => &[], + (true, true) => &WARM_SHADOWS, + (true, false) => &COOL_SHADOWS, + }; + + let window_bar = || { + if bar == Bar::None { + Svgr::empty() + } else { + let cy = inner.y + BAR_HEIGHT / 2.; + let (first, step) = match bar_side { + BarSide::Left => (inner.x + 8. + 14., 20.), + BarSide::Right => (inner.right() - 8. - 80. + 62., -20.), + }; + let dots: Svgr = ["#ff5f57", "#febc2e", "#28c840"] + .into_iter() + .enumerate() + .map(|(i, color)| { + let cx = first + i as f32 * step; + match bar { + Bar::Rings => { + svgr!() + } + _ => svgr!(), + } + }) + .collect(); + let label = title.map_or_else(Svgr::empty, |title| { + let style = TextStyle::new(Font::sans(400), 13.); + style.draw( + title.to_owned(), + inner.cx(), + style.baseline(inner.y, BAR_HEIGHT), + Rgba::hex(palette.muted), + Anchor::Middle, + ) + }); + svgr!({dots}{label}) + } + }; + + let card = Card { + id, + rect: inner.outset(1.), + radius, + fill: Rgba::hex(palette.surface), + border: Rgba::WHITE.alpha(0.06), + accent: None, + shadows, + frost: None, + to_screen, + screen: self.size, + }; + let chrome = self.chrome(&card, window_bar); + let video = self.clip_frame(window, clip, frame, video, ctx); + let paint = |layer: &mut Pixels| { + for (image, at) in [Some(&chrome), video.as_ref()].into_iter().flatten() { + layer.blend(image, (at.x as u32, at.y as u32), 1., Blend::Over); + } + }; + if opacity >= 1. { + paint(canvas); + } else { + // Faded as a whole, like a CSS opacity group. + let mut layer = Pixels::transparent(canvas.width, canvas.height); + paint(&mut layer); + canvas.blend(&layer.into_image(), (0, 0), opacity, Blend::Over); + } + } +} + +/// A window as laid out at a frame: its content rect (the 1px border lies outside it), bar +/// title, where it is drawn on screen and how faded in it is. +struct Window<'s> { + id: &'s str, + inner: Rect, + title: Option<&'s str>, + to_screen: Similarity, + opacity: f32, +} + +/// Where an image of `size` lands in `content` under `fit`, like `preserveAspectRatio`. +fn fitted((iw, ih): (f32, f32), content: Rect, fit: ObjectFit) -> Rect { + let (sx, sy) = (content.w / iw, content.h / ih); + let s = match fit { + ObjectFit::Contain => sx.min(sy), + ObjectFit::Cover => sx.max(sy), + ObjectFit::Fill => return content, + }; + Rect::new(content.cx() - iw * s / 2., content.cy() - ih * s / 2., iw * s, ih * s) +} + +/// Directed zoom into `to`: ease in over the first 30%, hold, ease out over the last 30%. +fn zoom_scale(f: f32, t: f32, dur: f32, to: &Region) -> f32 { + let (start, end) = (t * 30., (t + dur) * 30.); + let (in_end, hold_end) = (start + (end - start) * 0.3, start + (end - start) * 0.7); + let ease = Bezier(0.25, 0.1, 0.25, 1.); + let progress = if f < start || f > end { + 0. + } else if f <= in_end { + interpolate_eased(f, &[start, in_end], &[0., 1.], ease.f()) + } else if f <= hold_end { + 1. + } else { + interpolate_eased(f, &[hold_end, end], &[1., 0.], ease.f()) + }; + let target = (1. / to.w.0.max(0.01)).min(1. / to.h.0.max(0.01)); + 1. + (target - 1.) * progress +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::props::Pct; + + #[test] + fn zoom_fills_the_frame_with_the_region_during_the_hold() { + let to = Region { x: Pct(0.25), y: Pct(0.25), w: Pct(0.5), h: Pct(0.25) }; + assert_eq!(zoom_scale(0., 1., 2., &to), 1.); + assert!(zoom_scale(40., 1., 2., &to) > 1.); + assert_eq!(zoom_scale(48., 1., 2., &to), 2.); + assert_eq!(zoom_scale(60., 1., 2., &to), 2.); + assert_eq!(zoom_scale(91., 1., 2., &to), 1.); + } +} diff --git a/plugins/droid-control/fframes/src/fonts.rs b/plugins/droid-control/fframes/src/fonts.rs new file mode 100644 index 0000000..0ff9878 --- /dev/null +++ b/plugins/droid-control/fframes/src/fonts.rs @@ -0,0 +1,178 @@ +//! The embedded Geist families: registered with fframes for drawing and measured here for +//! layout, so boxes are sized from the same font files the renderer draws with. + +use fframes::RawFontData; +use fframes::ttf_parser::{Face, GlyphId}; +use std::sync::{Arc, LazyLock}; + +pub const SANS: &str = "Geist"; +pub const MONO: &str = "Geist Mono"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Font { + pub family: &'static str, + pub weight: u16, +} + +struct Embedded { + file: &'static str, + font: Font, + bytes: &'static [u8], +} + +macro_rules! embedded { + ($file:literal, $family:expr, $weight:literal) => { + Embedded { + file: $file, + font: Font { family: $family, weight: $weight }, + bytes: include_bytes!(concat!("../media/", $file)), + } + }; +} + +const EMBEDDED: [Embedded; 7] = [ + embedded!("Geist-Light.ttf", SANS, 300), + embedded!("Geist-Regular.ttf", SANS, 400), + embedded!("Geist-Medium.ttf", SANS, 500), + embedded!("Geist-SemiBold.ttf", SANS, 600), + embedded!("Geist-Bold.ttf", SANS, 700), + embedded!("GeistMono-Regular.ttf", MONO, 400), + embedded!("GeistMono-Medium.ttf", MONO, 500), +]; + +static FACES: LazyLock)>> = LazyLock::new(|| { + EMBEDDED.iter().map(|e| (e.font, Face::parse(e.bytes, 0).expect("embedded fonts parse"))).collect() +}); + +/// The font files for the fframes media provider. +pub fn font_data() -> Vec { + EMBEDDED.iter().map(|e| RawFontData { file_name: e.file.to_owned(), data: Arc::new(e.bytes) }).collect() +} + +impl Font { + pub const fn sans(weight: u16) -> Self { + Self { family: SANS, weight } + } + + pub const fn mono(weight: u16) -> Self { + Self { family: MONO, weight } + } + + fn face(self) -> &'static Face<'static> { + FACES + .iter() + .find(|(font, _)| *font == self) + .map(|(_, face)| face) + .unwrap_or_else(|| panic!("{self:?} is not an embedded font")) + } + + fn em(self, units: i16) -> f32 { + f32::from(units) / f32::from(self.face().units_per_em()) + } + + /// Advance width of `text` at `size` px with CSS `letter-spacing` (in em) after every + /// character. + pub fn width(self, text: &str, size: f32, letter_spacing: f32) -> f32 { + let face = self.face(); + let scale = size / f32::from(face.units_per_em()); + text.chars() + .map(|c| { + let glyph = face.glyph_index(c).unwrap_or(GlyphId(0)); + f32::from(face.glyph_hor_advance(glyph).unwrap_or(0)) * scale + letter_spacing * size + }) + .sum() + } + + /// CSS `line-height: normal`. + pub fn normal_line_height(self, size: f32) -> f32 { + let face = self.face(); + (self.em(face.ascender()) - self.em(face.descender()) + self.em(face.line_gap())) * size + } + + /// Distance from the top of a CSS line box `line_height` px tall to the text baseline. + pub fn baseline(self, size: f32, line_height: f32) -> f32 { + let face = self.face(); + let ascent = self.em(face.ascender()) * size; + let descent = -self.em(face.descender()) * size; + (line_height - ascent - descent) / 2. + ascent + } + + /// Greedy word wrap at `max_width`, the way a browser breaks `white-space: normal` text. + pub fn wrap(self, text: &str, size: f32, letter_spacing: f32, max_width: f32) -> Vec { + let space = self.width(" ", size, letter_spacing); + let mut lines: Vec<(String, f32)> = Vec::new(); + for word in text.split_whitespace() { + let width = self.width(word, size, letter_spacing); + match lines.last_mut() { + Some((line, line_width)) if *line_width + space + width <= max_width => { + line.push(' '); + line.push_str(word); + *line_width += space + width; + } + _ => lines.push((word.to_owned(), width)), + } + } + lines.into_iter().map(|(line, _)| line).collect() + } +} + +/// Runs of non-blank characters with their column, for monospaced text whose spacing must +/// survive SVG whitespace collapsing (code, the ASCII wordmark). +pub fn mono_runs(line: &str) -> Vec<(usize, String)> { + let mut runs: Vec<(usize, String)> = Vec::new(); + let mut column = 0; + for c in line.chars() { + match c { + '\t' => column = (column / 8 + 1) * 8, + c if c.is_whitespace() => column += 1, + c => { + match runs.last_mut() { + Some((start, run)) if *start + run.chars().count() == column => run.push(c), + _ => runs.push((column, c.to_string())), + } + column += 1; + } + } + } + runs +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn geist_metrics_give_the_css_normal_line_height() { + let font = Font::sans(400); + assert!((font.normal_line_height(100.) - 130.).abs() < 0.5); + assert!(font.baseline(100., 130.) > 95. && font.baseline(100., 130.) < 105.); + } + + #[test] + fn width_scales_with_size_and_letter_spacing() { + let font = Font::sans(700); + let base = font.width("Droid", 100., 0.); + assert!((font.width("Droid", 50., 0.) * 2. - base).abs() < 0.01); + assert!((font.width("Droid", 100., -0.04) - (base - 5. * 4.)).abs() < 0.01); + } + + #[test] + fn wrap_breaks_between_words_only() { + let font = Font::sans(500); + let one_word = font.width("autonomous", 24., 0.); + let lines = font.wrap("autonomous engineering droid", 24., 0., one_word + 1.); + assert_eq!(lines, ["autonomous", "engineering", "droid"]); + assert_eq!(font.wrap("short", 24., 0., 1.), ["short"]); + } + + #[test] + fn geist_mono_draws_the_wordmark_blocks() { + let face = Font::mono(400).face(); + assert!(face.glyph_index('█').is_some()); + } + + #[test] + fn mono_runs_keep_columns_through_spaces_and_tabs() { + assert_eq!(mono_runs(" ab c\td"), [(2, "ab".to_owned()), (6, "c".to_owned()), (8, "d".to_owned())]); + } +} diff --git a/plugins/droid-control/fframes/src/lib.rs b/plugins/droid-control/fframes/src/lib.rs new file mode 100644 index 0000000..5626ec4 --- /dev/null +++ b/plugins/droid-control/fframes/src/lib.rs @@ -0,0 +1,28 @@ +//! The droid-control showcase composition, rendered with fframes. +//! +//! A showcase is a title card, one or two screen recordings in window chrome with timed +//! overlays (keystrokes, sections, zooms, spotlights, callouts, code), and the Droid outro. +//! [`Showcase`] draws any frame as SVG; [`render`] rasterises stills and whole videos. + +pub mod clips; +pub mod code; +pub mod fonts; +pub mod motion; +pub mod props; +pub mod render; +pub mod stage; +pub mod svg; +pub mod theme; +pub mod timing; + +mod composite; +mod content; +mod outro; +mod overlays; +mod raster; +mod scenery; +mod showcase; +mod title; +mod transition; + +pub use showcase::Showcase; diff --git a/plugins/droid-control/fframes/src/main.rs b/plugins/droid-control/fframes/src/main.rs new file mode 100644 index 0000000..d865d92 --- /dev/null +++ b/plugins/droid-control/fframes/src/main.rs @@ -0,0 +1,115 @@ +//! `droid-showcase`: renders the droid-control showcase video (or one still) from props JSON +//! and clip files. + +use clap::Parser; +use droid_showcase::clips::Clip; +use droid_showcase::props::ShowcaseProps; +use droid_showcase::{Showcase, render, stage}; +use serde_json::json; +use signal_hook::consts::{SIGINT, SIGTERM}; +use signal_hook::iterator::Signals; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +/// Render a droid-control showcase: title card, clips with overlays, Droid outro. +#[derive(Debug, Parser)] +#[command(name = "droid-showcase", version)] +struct Cli { + /// Props JSON file. + #[arg(long, value_name = "FILE", required_unless_present = "props_inline", conflicts_with = "props_inline")] + props: Option, + /// Props JSON given inline. + #[arg(long, value_name = "JSON")] + props_inline: Option, + /// Fidelity profile (compact, standard, inspect); overrides the props. + #[arg(long, value_name = "PROFILE")] + fidelity: Option, + /// Render only this frame, as a PNG. + #[arg(long, value_name = "FRAME")] + still: Option, + /// Output file: an .mp4, or a .png with --still. + #[arg(long, short)] + output: PathBuf, + /// Clips (.cast, .mp4 or .webm); side-by-side shows the first two. + #[arg(required = true, value_name = "CLIP")] + clips: Vec, +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + let work_dir = match tempfile::Builder::new().prefix("droid-showcase-").tempdir() { + Ok(dir) => dir.keep(), + Err(err) => { + eprintln!("error: could not create a work directory: {err}"); + return ExitCode::FAILURE; + } + }; + remove_on_signal(work_dir.clone()); + let result = run(&cli, &work_dir); + let _ = fs::remove_dir_all(&work_dir); + match result { + Ok(()) => { + println!("{}", cli.output.display()); + ExitCode::SUCCESS + } + Err(err) => { + eprintln!("error: {err}"); + ExitCode::FAILURE + } + } +} + +/// Ctrl-C or a cancelled task stops the render without leaving staged clips behind; the +/// ffmpeg and agg children share the process group and stop with it. +fn remove_on_signal(work_dir: PathBuf) { + let mut signals = Signals::new([SIGINT, SIGTERM]).expect("SIGINT and SIGTERM handlers install"); + std::thread::spawn(move || { + if let Some(signal) = signals.forever().next() { + let _ = fs::remove_dir_all(&work_dir); + std::process::exit(128 + signal); + } + }); +} + +fn run(cli: &Cli, work_dir: &Path) -> Result<(), String> { + let json = match (&cli.props, &cli.props_inline) { + (Some(file), _) => { + fs::read_to_string(file).map_err(|err| format!("props file is not readable: {}: {err}", file.display()))? + } + (None, Some(inline)) => inline.clone(), + (None, None) => unreachable!("clap requires --props or --props-inline"), + }; + for clip in &cli.clips { + stage::kind(clip)?; + fs::File::open(clip).map_err(|err| format!("clip is not readable: {}: {err}", clip.display()))?; + } + let props = ShowcaseProps::from_json(&json, cli.fidelity.as_deref())?; + let encoding = props.fidelity.encoding(); + + let staged = stage::stage(&cli.clips, work_dir, &encoding)?; + let clips = Clip::probe_all(&staged)?; + let showcase = Showcase::new(props, clips)?; + + let props = showcase.props(); + let timeline = showcase.timeline(); + let plan = json!({ + "fidelity": props.fidelity, + "width": props.width, + "height": props.height, + "speed": props.speed, + "longestClip": droid_showcase::clips::longest(showcase.clips()), + "frames": timeline.total(), + "crf": encoding.crf, + "preset": encoding.preset, + "clips": showcase.clips().iter().map(|clip| clip.path.display().to_string()).collect::>(), + "workDir": work_dir.display().to_string(), + }); + eprintln!("showcase plan: {plan}"); + + let media = render::media(showcase.clips()); + match cli.still { + Some(frame) => render::still(&showcase, &media, frame, &cli.output), + None => render::video(&showcase, &media, &cli.output), + } +} diff --git a/plugins/droid-control/fframes/src/motion.rs b/plugins/droid-control/fframes/src/motion.rs new file mode 100644 index 0000000..182ccc8 --- /dev/null +++ b/plugins/droid-control/fframes/src/motion.rs @@ -0,0 +1,132 @@ +//! Keyframe interpolation and easing: inputs are clamped to the keyframe range, CSS +//! cubic-bezier easings shape each segment. + +/// Piecewise-linear interpolation over increasing `input` stops, clamped at both ends. +pub fn interpolate(x: f32, input: &[f32], output: &[f32]) -> f32 { + interpolate_eased(x, input, output, linear) +} + +/// Like [`interpolate`], with `easing` applied to the progress of every segment. +pub fn interpolate_eased(x: f32, input: &[f32], output: &[f32], easing: impl Fn(f32) -> f32) -> f32 { + debug_assert!(input.len() == output.len() && input.len() >= 2); + let last = input.len() - 1; + let segment = (1..last).take_while(|&i| x >= input[i]).last().unwrap_or(0); + let (a, b) = (input[segment], input[segment + 1]); + let t = if b > a { ((x - a) / (b - a)).clamp(0., 1.) } else { 1. }; + output[segment] + (output[segment + 1] - output[segment]) * easing(t) +} + +pub fn linear(t: f32) -> f32 { + t +} + +/// CSS `cubic-bezier(x1, y1, x2, y2)`. +#[derive(Debug, Clone, Copy)] +pub struct Bezier(pub f32, pub f32, pub f32, pub f32); + +impl Bezier { + pub const EXPO_OUT: Bezier = Bezier(0.16, 1., 0.3, 1.); + + pub fn ease(self, t: f32) -> f32 { + let Bezier(x1, y1, x2, y2) = self; + if t <= 0. || t >= 1. { + return t.clamp(0., 1.); + } + let curve = |p1: f64, p2: f64, s: f64| { + let u = 1. - s; + 3. * u * u * s * p1 + 3. * u * s * s * p2 + s * s * s + }; + let slope = |p1: f64, p2: f64, s: f64| { + let u = 1. - s; + 3. * u * u * p1 + 6. * u * s * (p2 - p1) + 3. * s * s * (1. - p2) + }; + let (x1, x2, target) = (f64::from(x1), f64::from(x2), f64::from(t)); + let mut s = target; + for _ in 0..8 { + let d = slope(x1, x2, s); + if d.abs() < 1e-7 { + break; + } + s -= (curve(x1, x2, s) - target) / d; + } + if !(0. ..=1.).contains(&s) || (curve(x1, x2, s) - target).abs() > 1e-6 { + let (mut lo, mut hi) = (0., 1.); + s = target; + for _ in 0..40 { + if curve(x1, x2, s) < target { + lo = s; + } else { + hi = s; + } + s = (lo + hi) / 2.; + } + } + curve(f64::from(y1), f64::from(y2), s) as f32 + } + + pub fn f(self) -> impl Fn(f32) -> f32 { + move |t| self.ease(t) + } +} + +/// `Easing.out(Easing.back(s))`: overshoots the end, then settles. +pub fn back_out(s: f32) -> impl Fn(f32) -> f32 { + move |t| { + let u = 1. - t; + 1. - u * u * ((s + 1.) * u - s) + } +} + +/// `Easing.out(Easing.cubic)`. +pub fn cubic_out(t: f32) -> f32 { + 1. - (1. - t).powi(3) +} + +/// Remotion's deterministic `random(seed)`: mulberry32 over a Java-style string hash. +pub fn seeded_random(seed: &str) -> f32 { + let hash = seed + .encode_utf16() + .fold(0i32, |hash, unit| hash.wrapping_shl(5).wrapping_sub(hash).wrapping_add(i32::from(unit))); + let mut t = (hash as u32).wrapping_add(0x6d2b_79f5); + t = (t ^ (t >> 15)).wrapping_mul(t | 1); + t ^= t.wrapping_add((t ^ (t >> 7)).wrapping_mul(t | 61)); + f64::from(t ^ (t >> 14)) as f32 / 4_294_967_296. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn interpolate_clamps_and_walks_segments() { + let stops = [0., 0.35, 0.55, 0.85, 1.]; + let values = [0., 0.25, 1., 0.2, 0.]; + assert_eq!(interpolate(-1., &stops, &values), 0.); + assert!((interpolate(0.45, &stops, &values) - 0.625).abs() < 1e-6); + assert_eq!(interpolate(2., &stops, &values), 0.); + } + + #[test] + fn bezier_matches_css_endpoints_and_midpoint() { + let ease = Bezier(0.25, 0.1, 0.25, 1.); + assert_eq!(ease.ease(0.), 0.); + assert_eq!(ease.ease(1.), 1.); + // CSS `ease` at 50% progress. + assert!((ease.ease(0.5) - 0.8024).abs() < 1e-3); + } + + #[test] + fn back_out_overshoots_then_settles() { + let ease = back_out(1.5); + assert!(ease(0.6) > 1.); + assert!((ease(1.) - 1.).abs() < 1e-6); + } + + #[test] + fn seeded_random_is_deterministic_and_in_unit_range() { + let value = seeded_random("glitch-x-1000"); + assert_eq!(value, seeded_random("glitch-x-1000")); + assert!((0. ..1.).contains(&value)); + assert_ne!(value, seeded_random("glitch-x-1001")); + } +} diff --git a/plugins/droid-control/fframes/src/outro.rs b/plugins/droid-control/fframes/src/outro.rs new file mode 100644 index 0000000..9a59f7e --- /dev/null +++ b/plugins/droid-control/fframes/src/outro.rs @@ -0,0 +1,113 @@ +//! The closing card: eight rotor wedges pop in as one blade and fan out into the mark, +//! then crossfade into the ASCII DROID wordmark. + +use crate::fonts::{Font, mono_runs}; +use crate::motion::{back_out, cubic_out, interpolate, interpolate_eased}; +use crate::svg::{Anchor, ROTOR_PATH, ROTOR_VIEWBOX, Rect, Rgba, TextStyle, blended, faded, glow}; +use crate::theme::Palette; +use fframes::{Svgr, svgr}; + +const DROID_ASCII: [&str; 7] = [ + "████████ █████████ ██████████ ███ ████████ ", + "███ ███ ███ ███ ███ ███ ███ ███ ███ ", + "███ ███ ███ ███ ███ ███ ███ ███ ███", + "███ ███ █████████ ███ ███ ███ ███ ███", + "███ ███ ███ ███ ███ ███ ███ ███ ███", + "███ ███ ███ ███ ███ ███ ███ ███ ███ ", + "████████ ███ ███ ██████████ ███ ████████ ", +]; +const TAGLINE: &str = "AUTONOMOUS ENGINEERING"; +/// The rotor's hub in its own viewBox; every wedge spins around it. +const HUB: (f32, f32) = (303.105, 319.528); +const FAN_END: f32 = 45.; +const CROSSFADE_END: f32 = 75.; + +pub fn draw<'a>(palette: &Palette, (w, h): (f32, f32), frame: usize) -> Svgr<'a> { + let f = frame as f32; + let wedges = interpolate(f, &[FAN_END, CROSSFADE_END], &[1., 0.]); + let wordmark = 1. - wedges; + svgr!( + + + {faded(wedges, fan(f, w / 2., h / 2.))} + {faded(wordmark, wordmark_layer(palette, w, h))} + + ) +} + +/// Eight copies of the mark clipped to one 45-degree slice: scaled in together, then each +/// rotated `i * 45` degrees so the slices tile the whole rotor. +fn fan<'a>(f: f32, cx: f32, cy: f32) -> Svgr<'a> { + let scale = interpolate_eased(f, &[0., 12.], &[0., 1.], back_out(1.5)); + let glitching = f > 12. && f < 24. && (f as usize).is_multiple_of(3); + let opacity = if glitching { 0.5 } else { interpolate(f, &[0., 12.], &[0., 1.]) }; + if scale <= 0. { + return Svgr::empty(); + } + let (hx, hy) = HUB; + let slice = format!("{hx},{hy} {vw},{hy} {vw},{top}", vw = ROTOR_VIEWBOX.0, top = hy - (ROTOR_VIEWBOX.0 - hx)); + let blades: Svgr = (0..8) + .map(|i| { + let angle = interpolate_eased(f, &[12., FAN_END], &[0., i as f32 * 45.], cubic_out); + let place = format!("translate({cx} {cy}) scale({scale}) rotate({angle}) translate({} {})", -hx, -hy); + blended( + "screen", + opacity, + svgr!( + + + + ), + ) + }) + .collect(); + svgr!( + + + + + {blades} + + ) +} + +fn wordmark_layer<'a>(palette: &Palette, w: f32, h: f32) -> Svgr<'a> { + let mono = TextStyle::new(Font::mono(400), 24.); + let line_height = 24. * 1.2; + let tagline = TextStyle::new(Font::sans(300), 32.).spaced(0.2); + let tagline_height = tagline.normal_line_height(); + let block_height = DROID_ASCII.len() as f32 * line_height; + let top = (h - block_height - 40. - tagline_height) / 2.; + let advance = mono.width("█"); + let columns = DROID_ASCII.iter().map(|line| line.chars().count()).max().unwrap_or(0); + let left = (w - columns as f32 * advance) / 2.; + let block_area = Rect::new(left, top, columns as f32 * advance, block_height); + + let ascii = |fill: Rgba| -> Svgr<'a> { + DROID_ASCII + .iter() + .enumerate() + .flat_map(|(row, line)| { + let baseline = mono.baseline(top + row as f32 * line_height, line_height); + mono_runs(line).into_iter().map(move |(column, run)| { + mono.draw(run, left + column as f32 * advance, baseline, fill, Anchor::Start) + }) + }) + .collect() + }; + + let accent = Rgba::hex(palette.accent); + let tagline_top = top + block_height + 40.; + let tagline_baseline = tagline.baseline(tagline_top, tagline_height); + let tagline_text = |fill: Rgba| tagline.draw(TAGLINE.to_owned(), w / 2., tagline_baseline, fill, Anchor::Middle); + let tagline_width = tagline.width(TAGLINE); + let tagline_area = Rect::new((w - tagline_width) / 2., tagline_top, tagline_width, tagline_height); + svgr!( + + {glow("outro-ascii-glow".to_owned(), 10., block_area, ascii(Rgba::WHITE.alpha(0.4)))} + {ascii(Rgba::WHITE)} + {glow("outro-tagline-glow".to_owned(), 7.5, tagline_area, tagline_text(accent.a8(0x66)))} + {tagline_text(accent)} + + ) +} diff --git a/plugins/droid-control/fframes/src/overlays.rs b/plugins/droid-control/fframes/src/overlays.rs new file mode 100644 index 0000000..fadb8e4 --- /dev/null +++ b/plugins/droid-control/fframes/src/overlays.rs @@ -0,0 +1,465 @@ +//! Timed overlays on the clip clock: callouts, section sweeps and headers, keystrokes and +//! code annotations. Every panel frosts the scene behind it. + +use crate::code::highlight; +use crate::fonts::Font; +use crate::motion::{Bezier, interpolate, interpolate_eased}; +use crate::props::{CodeAnnotation, CodePosition, Effect, Keystroke, Section}; +use crate::raster::Backdrop; +use crate::svg::{Anchor, Card, Edge, Frost, Rect, Rgba, Shadow, Similarity, TextStyle, blur, faded, stops}; +use crate::theme::{Palette, PresetConfig}; +use fframes::{Svgr, svgr}; + +/// Overshooting pop-in shared by callouts and keystrokes. +const POP: Bezier = Bezier(0.34, 1.56, 0.64, 1.); + +fn frame_of(seconds: f32) -> f32 { + (seconds * 30.).round() +} + +pub struct Overlays<'s> { + pub palette: &'s Palette, + pub config: &'s PresetConfig, + pub size: (f32, f32), + /// The scene behind the overlays, for their frosted backdrops. + pub backdrop: &'s Backdrop, +} + +impl<'s> Overlays<'s> { + fn frost(&self, sigma: f32) -> Option> { + Some(Frost { backdrop: self.backdrop, sigma }) + } + + /// Text pills centred on percentage positions. + pub fn callouts<'a>(&self, effects: &[Effect], frame: usize) -> Svgr<'a> { + let (w, h) = self.size; + let f = frame as f32; + let style = TextStyle::new(Font::sans(500), 24.); + let line_height = 24. * 1.4; + effects + .iter() + .enumerate() + .filter_map(|(i, effect)| { + let Effect::Callout { t, dur, text, at } = effect else { return None }; + let (enter, exit) = (frame_of(*t), frame_of(t + dur)); + if f < enter || f >= exit { + return None; + } + let local = f - enter; + let pop = interpolate_eased(local, &[0., 7.5], &[0., 1.], POP.f()); + let fade = interpolate(local, &[exit - enter - 6., exit - enter], &[1., 0.]); + + // Shrink-to-fit next to `left`, capped at 40% of the width. + let (left, top) = (at.x.0 * w, at.y.0 * h); + let (border_x, padding_x) = (1. + 3., 48.); + let max_content = style.width(text); + let min_content = text.split_whitespace().map(|word| style.width(word)).fold(0., f32::max); + let available = w - left - padding_x - border_x; + let content_w = max_content.min(min_content.max(available)).min(0.4 * w); + let lines = style.font.wrap(text, style.size, 0., content_w); + let rect_w = content_w + padding_x + border_x; + let rect_h = lines.len() as f32 * line_height + 24. + 2.; + let rect = Rect::new(left - rect_w / 2., top - rect_h / 2., rect_w, rect_h); + let place = Similarity::scale_about(0.85 + 0.15 * pop, left, top); + + let text_x = rect.x + 3. + 24. + content_w / 2.; + let fill = Rgba::hex(self.palette.text); + let text: Svgr = lines + .into_iter() + .enumerate() + .map(|(row, line)| { + let baseline = style.baseline(rect.y + 1. + 12. + row as f32 * line_height, line_height); + style.draw(line, text_x, baseline, fill, Anchor::Middle) + }) + .collect(); + let card = Card { + id: &format!("callout{i}"), + rect, + radius: 12., + fill: Rgba::hex(self.palette.surface).a8(0xE6), + border: Rgba::hex(self.palette.border), + accent: Some((Edge::Left, 3., Rgba::hex(self.palette.accent))), + shadows: &[Shadow::new(8., 32., Rgba(0, 0, 0, 0.35))], + frost: self.frost(12.), + to_screen: place, + screen: self.size, + }; + Some(faded(pop.min(fade), svgr!({card.draw(text)}))) + }) + .collect() + } + + /// A frosted band sweeping across the frame at every section boundary after the first. + pub fn section_sweeps<'a>(&self, sections: &[Section], frame: usize) -> Svgr<'a> { + let (w, h) = self.size; + let f = frame as f32; + let sweep = 15.; + sections + .iter() + .enumerate() + .skip(1) + .filter_map(|(i, section)| { + let start = frame_of(section.t); + if f < start || f > start + sweep { + return None; + } + let local = f - start; + let x = interpolate_eased(local, &[0., sweep], &[-0.1, 1.1], Bezier(0.4, 0., 0.2, 1.).f()); + let opacity = interpolate(local, &[0., sweep * 0.15, sweep * 0.85, sweep], &[0., 1., 1., 0.]); + let band = Rect::new(x * w - 0.1 * w, 0., 0.2 * w, h); + let id = format!("sweep{i}"); + let clip = format!("url(#{id}-clip)"); + let sheen = format!("url(#{id}-sheen)"); + let frost = Frost { backdrop: self.backdrop, sigma: 28. }.draw(band, Similarity::IDENTITY); + let white = Rgba::WHITE; + Some(faded( + opacity, + svgr!( + + + + + {frost} + + {stops(&[(0., white.alpha(0.)), (0.5, white.alpha(0.04)), (1., white.alpha(0.))])} + + + + ), + )) + }) + .collect() + } + + /// Section titles at the top: each slides in and stays until the next one (or the end of + /// the content, `end` frames on the clip clock). + pub fn section_headers<'a>(&self, sections: &[Section], frame: usize, end: usize) -> Svgr<'a> { + let (w, _) = self.size; + let f = frame as f32; + let end = end as f32; + let style = TextStyle::new(Font::sans(600), 28.).spaced(-0.02); + sections + .iter() + .enumerate() + .filter_map(|(i, section)| { + let enter = frame_of(section.t); + let next = sections.get(i + 1).map_or(end, |next| frame_of(next.t)); + let exit = next.min(end); + if f < enter || f >= exit { + return None; + } + let local = f - enter; + let total = exit - enter; + let entered = interpolate_eased(local, &[0., 12.], &[0., 1.], Bezier(0.22, 1., 0.36, 1.).f()); + let exited = interpolate_eased(local, &[total - 9., total], &[0., 1.], Bezier(0.32, 0., 0.67, 0.).f()); + let dy = -20. * (1. - entered) + 20. * exited; + + let line_height = style.normal_line_height(); + let rect_w = style.width(§ion.title) + 64. + 2.; + let rect = Rect::new( + (w - rect_w) / 2., + (self.config.margin / 2. - 20.).max(30.), + rect_w, + line_height + 24. + 3., + ); + let place = Similarity::translate(0., dy); + let baseline = style.baseline(rect.y + 1. + 12., line_height); + let text = + style.draw(section.title.clone(), w / 2., baseline, Rgba::hex(self.palette.text), Anchor::Middle); + let card = Card { + id: &format!("section{i}"), + rect, + radius: 16., + fill: Rgba::hex(self.palette.surface).a8(0xE6), + border: Rgba::hex(self.palette.border), + accent: Some((Edge::Bottom, 2., Rgba::hex(self.palette.accent).a8(0x80))), + shadows: &[Shadow::new(8., 32., Rgba(0, 0, 0, 0.4))], + frost: self.frost(12.), + to_screen: place, + screen: self.size, + }; + Some(faded(entered * (1. - exited), svgr!({card.draw(text)}))) + }) + .collect() + } + + /// Key pills above the bottom edge; a key gives way to the next one early. + pub fn keystrokes<'a>(&self, keys: &[Keystroke], frame: usize) -> Svgr<'a> { + let (w, h) = self.size; + let f = frame as f32; + let style = TextStyle::new(Font::mono(500), 22.); + keys.iter() + .enumerate() + .filter_map(|(i, key)| { + let enter = frame_of(key.t); + let natural_exit = frame_of(key.t + key.dur.unwrap_or(1.2)); + let exit = keys.get(i + 1).map_or(natural_exit, |next| natural_exit.min(frame_of(next.t))); + if f < enter || f >= exit { + return None; + } + let local = f - enter; + let pop = interpolate_eased(local, &[0., 6.], &[0., 1.], POP.f()); + let fade = interpolate(local, &[exit - enter - 4.5, exit - enter], &[1., 0.]); + + let line_height = style.normal_line_height(); + let rect_w = style.width(&key.label) + 40. + 2.; + let rect_h = line_height + 16. + 2.; + let bottom = (self.config.margin + 40.).max(50.); + let rect = Rect::new((w - rect_w) / 2., h - bottom - rect_h, rect_w, rect_h); + let place = Similarity::scale_about(0.8 + 0.2 * pop, rect.cx(), rect.cy()); + let baseline = style.baseline(rect.y + 1. + 8., line_height); + let text = + style.draw(key.label.clone(), w / 2., baseline, Rgba::hex(self.palette.text), Anchor::Middle); + let card = Card { + id: &format!("key{i}"), + rect, + radius: 8., + fill: Rgba::hex(self.palette.surface).a8(0xBF), + border: Rgba::hex(self.palette.border), + accent: None, + shadows: &[], + frost: self.frost(8.), + to_screen: place, + screen: self.size, + }; + Some(faded(pop.min(fade), svgr!({card.draw(text)}))) + }) + .collect() + } + + pub fn code_cards<'a>(&self, cards: &[CodeCard], frame: usize) -> Svgr<'a> { + cards.iter().enumerate().map(|(i, card)| self.code_card(i, card, frame as f32)).collect() + } + + fn code_card<'a>(&self, index: usize, card: &CodeCard, f: f32) -> Svgr<'a> { + if f < card.enter || f >= card.exit { + return Svgr::empty(); + } + let local = f - card.enter; + let total = card.exit - card.enter; + let entered = interpolate_eased(local, &[0., 10.5], &[0., 1.], Bezier::EXPO_OUT.f()); + let exited = interpolate(local, &[(total - 10.5).max(0.), total], &[0., 1.]); + let rect = card.rect; + let place = Similarity::scale_about(0.96 + 0.04 * entered, rect.cx(), rect.cy()); + let palette = self.palette; + let muted = Rgba::hex(palette.muted); + let accent = Rgba::hex(palette.accent); + let id = format!("code{index}"); + + let content_x = rect.x + 1. + 16.; + let content_y = rect.y + 1. + 16.; + let title: Svgr = card + .title + .iter() + .enumerate() + .map(|(row, line)| { + let line_height = CODE_TITLE.normal_line_height(); + let baseline = CODE_TITLE.baseline(content_y + row as f32 * line_height, line_height); + CODE_TITLE.draw(line.clone(), content_x, baseline, muted, Anchor::Start) + }) + .collect(); + + let code_y = content_y + card.title_height(); + let pre = Rect::new(content_x, code_y, card.content_w, card.lines.len() as f32 * CODE_LINE_HEIGHT); + let advance = CODE.width("0"); + let gutter_w = (card.lines.len().to_string().len() + 1) as f32 * advance; + let text_x = content_x + 2.; + let rows: Svgr = card + .lines + .iter() + .enumerate() + .map(|(row, line)| { + let top = code_y + row as f32 * CODE_LINE_HEIGHT; + let baseline = CODE.baseline(top, CODE_LINE_HEIGHT); + // The row's left border falls outside the clipped
; only its tint shows.
+                let tint = if line.highlighted {
+                    svgr!()
+                } else {
+                    Svgr::empty()
+                };
+                let number = CODE.draw((row + 1).to_string(), text_x + gutter_w, baseline, muted, Anchor::End);
+                let tokens: Svgr = line
+                    .runs
+                    .iter()
+                    .map(|(column, color, run)| {
+                        CODE.draw(run.clone(), text_x + gutter_w + 12. + *column as f32 * advance, baseline, *color, Anchor::Start)
+                    })
+                    .collect();
+                let text = svgr!({number}{tokens});
+                let text = if line.focused {
+                    text
+                } else {
+                    let filter = format!("{id}-unfocused{row}");
+                    let url = format!("url(#{filter})");
+                    let area = Rect::new(pre.x, top, pre.w, CODE_LINE_HEIGHT);
+                    svgr!({blur(filter, 1.2, area)}{text})
+                };
+                svgr!({tint}{text})
+            })
+            .collect();
+        let pre_clip = format!("{id}-pre");
+        let pre_url = format!("url(#{pre_clip})");
+        let ring = rect.outset(0.5);
+
+        let panel = Card {
+            id: &id,
+            rect,
+            radius: 12.,
+            fill: Rgba::hex(palette.surface).a8(0xF2),
+            border: Rgba::hex(palette.border),
+            accent: Some((Edge::Bottom, 2., accent.a8(0xAA))),
+            shadows: &[Shadow::new(18., 48., Rgba(0, 0, 0, 0.45))],
+            frost: self.frost(10.),
+            to_screen: place,
+            screen: self.size,
+        };
+        let drawn = svgr!(
+            
+                
+                {panel.draw(svgr!(
+                    
+                        {title}
+                        
+                            
+                        
+                        {rows}
+                    
+                ))}
+            
+        );
+        let sigma = 6. * (1. - entered);
+        let drawn = if sigma > 0.01 {
+            let filter = format!("{id}-enter");
+            let url = format!("url(#{filter})");
+            svgr!({blur(filter, sigma, rect.outset(60.))}{drawn})
+        } else {
+            drawn
+        };
+        faded(entered * (1. - exited), svgr!({drawn}))
+    }
+}
+
+const CODE: TextStyle = TextStyle::new(Font::mono(400), 14.);
+const CODE_LINE_HEIGHT: f32 = 14. * 1.5;
+const CODE_TITLE: TextStyle = TextStyle::new(Font::sans(500), 11.).spaced(0.1);
+
+pub struct CodeLine {
+    /// Non-blank runs: column, colour, text.
+    runs: Vec<(usize, Rgba, String)>,
+    highlighted: bool,
+    focused: bool,
+}
+
+/// A code annotation laid out once: wrapped title, highlighted lines and the panel rect.
+pub struct CodeCard {
+    enter: f32,
+    exit: f32,
+    title: Vec,
+    lines: Vec,
+    content_w: f32,
+    rect: Rect,
+}
+
+impl CodeCard {
+    pub fn new(annotation: &CodeAnnotation, palette: &Palette, config: &PresetConfig, (w, h): (f32, f32)) -> Self {
+        let tokens = highlight(&annotation.code, &annotation.language, Rgba::hex(palette.text));
+        let has_focus = !annotation.focus.is_empty();
+        let lines: Vec = tokens
+            .into_iter()
+            .enumerate()
+            .map(|(i, tokens)| {
+                let number = i as u32 + 1;
+                CodeLine {
+                    runs: columns(&tokens),
+                    highlighted: annotation.highlight.iter().any(|range| range.contains(number)),
+                    focused: !has_focus || annotation.focus.iter().any(|range| range.contains(number)),
+                }
+            })
+            .collect();
+
+        let advance = CODE.width("0");
+        let gutter_w = (lines.len().to_string().len() + 1) as f32 * advance + 12.;
+        let line_w = |line: &CodeLine| {
+            line.runs.last().map_or(0., |(column, _, run)| (column + run.chars().count()) as f32 * advance)
+        };
+        let code_w = 2. + gutter_w + lines.iter().map(line_w).fold(0., f32::max);
+        let title = annotation.title.to_uppercase();
+        let max_content = code_w.max(CODE_TITLE.width(&title));
+        let min_content = code_w.max(title.split_whitespace().map(|word| CODE_TITLE.width(word)).fold(0., f32::max));
+        let edge = (config.margin + 24.).max(40.);
+        let chrome = 32. + 2.;
+        let available = match annotation.position {
+            CodePosition::Center => w / 2. - chrome,
+            CodePosition::TopRight | CodePosition::BottomLeft => w - edge - chrome,
+        };
+        let content_w = max_content.min(min_content.max(available)).min(0.42 * w).max(320.);
+        let title = if title.is_empty() {
+            Vec::new()
+        } else {
+            CODE_TITLE.font.wrap(&title, CODE_TITLE.size, CODE_TITLE.letter_spacing, content_w)
+        };
+
+        let mut card = CodeCard {
+            enter: (annotation.t * 30.).round(),
+            exit: ((annotation.t + annotation.dur) * 30.).round(),
+            title,
+            lines,
+            content_w,
+            rect: Rect::new(0., 0., content_w + chrome, 0.),
+        };
+        card.rect.h = card.title_height() + card.lines.len() as f32 * CODE_LINE_HEIGHT + 32. + 1. + 2.;
+        (card.rect.x, card.rect.y) = match annotation.position {
+            CodePosition::Center => ((w - card.rect.w) / 2., (h - card.rect.h) / 2.),
+            CodePosition::TopRight => (w - edge - card.rect.w, edge),
+            CodePosition::BottomLeft => (edge, h - edge - card.rect.h),
+        };
+        card
+    }
+
+    fn title_height(&self) -> f32 {
+        if self.title.is_empty() { 0. } else { self.title.len() as f32 * CODE_TITLE.normal_line_height() + 10. }
+    }
+}
+
+/// Splits highlighted tokens into non-blank runs at their column (tabs stop every 8).
+fn columns(tokens: &[(Rgba, String)]) -> Vec<(usize, Rgba, String)> {
+    let mut runs: Vec<(usize, Rgba, String)> = Vec::new();
+    let mut column = 0;
+    for (color, text) in tokens {
+        let mut open = false;
+        for c in text.chars() {
+            match c {
+                '\t' => {
+                    column = (column / 8 + 1) * 8;
+                    open = false;
+                }
+                c if c.is_whitespace() => {
+                    column += 1;
+                    open = false;
+                }
+                c => {
+                    match runs.last_mut() {
+                        Some((_, _, run)) if open => run.push(c),
+                        _ => runs.push((column, *color, c.to_string())),
+                    }
+                    open = true;
+                    column += 1;
+                }
+            }
+        }
+    }
+    runs
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn columns_split_on_blanks_and_colour_changes() {
+        let red = Rgba(255, 0, 0, 1.);
+        let blue = Rgba(0, 0, 255, 1.);
+        let runs = columns(&[(red, "\tlet x".to_owned()), (blue, "=1".to_owned())]);
+        assert_eq!(runs, [(8, red, "let".to_owned()), (12, red, "x".to_owned()), (13, blue, "=1".to_owned())]);
+    }
+}
diff --git a/plugins/droid-control/fframes/src/props.rs b/plugins/droid-control/fframes/src/props.rs
new file mode 100644
index 0000000..336fce8
--- /dev/null
+++ b/plugins/droid-control/fframes/src/props.rs
@@ -0,0 +1,366 @@
+//! The Showcase props contract. Missing fields take the composition defaults and unknown
+//! fields are ignored. Clips are not props: they are passed to the binary and probed.
+
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::{Map, Value};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum Preset {
+    Macos,
+    Minimal,
+    Hero,
+    Presentation,
+    Factory,
+    FactoryHero,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum Layout {
+    Single,
+    SideBySide,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum Fidelity {
+    Compact,
+    Standard,
+    Inspect,
+}
+
+/// How clip video is sized inside its panel.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ObjectFit {
+    /// Preserve the aspect ratio and letterbox.
+    #[default]
+    Contain,
+    /// Fill the panel and crop the overflow.
+    Cover,
+    /// Stretch to the panel (distorts the aspect ratio).
+    Fill,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum TransitionStyle {
+    #[default]
+    MotionBlur,
+    Flash,
+    WhipPan,
+    LightLeak,
+    GlitchLite,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Keystroke {
+    pub t: f32,
+    pub label: String,
+    pub dur: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Section {
+    pub t: f32,
+    pub title: String,
+}
+
+/// A CSS-style percentage such as `"25%"`, stored as a fraction (0.25).
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Pct(pub f32);
+
+impl<'de> Deserialize<'de> for Pct {
+    fn deserialize>(deserializer: D) -> Result {
+        let raw = String::deserialize(deserializer)?;
+        let number = raw.trim().trim_end_matches('%').trim();
+        number
+            .parse::()
+            .ok()
+            .filter(|value| value.is_finite())
+            .map(|value| Pct(value / 100.))
+            .ok_or_else(|| serde::de::Error::custom(format!("expected a percentage like \"25%\", got {raw:?}")))
+    }
+}
+
+#[derive(Debug, Clone, Copy, Deserialize)]
+pub struct Region {
+    pub x: Pct,
+    pub y: Pct,
+    pub w: Pct,
+    pub h: Pct,
+}
+
+#[derive(Debug, Clone, Copy, Deserialize)]
+pub struct Point {
+    pub x: Pct,
+    pub y: Pct,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(tag = "fx", rename_all = "kebab-case")]
+pub enum Effect {
+    FadeIn {
+        t: f32,
+        dur: f32,
+    },
+    FadeOut {
+        t: f32,
+        dur: f32,
+    },
+    Zoom {
+        t: f32,
+        dur: f32,
+        to: Region,
+    },
+    Spotlight {
+        t: f32,
+        dur: f32,
+        on: Region,
+        #[serde(default = "default_dim")]
+        dim: f32,
+    },
+    Callout {
+        t: f32,
+        dur: f32,
+        text: String,
+        at: Point,
+    },
+}
+
+fn default_dim() -> f32 {
+    0.6
+}
+
+impl Effect {
+    /// Schema-valid effects this composition does not draw.
+    pub fn unrendered_name(&self) -> Option<&'static str> {
+        match self {
+            Effect::FadeIn { .. } => Some("fade-in"),
+            Effect::FadeOut { .. } => Some("fade-out"),
+            _ => None,
+        }
+    }
+}
+
+/// Inclusive 1-based line range.
+#[derive(Debug, Clone, Copy, Deserialize)]
+pub struct CodeRange {
+    pub start: u32,
+    pub end: u32,
+}
+
+impl CodeRange {
+    pub fn contains(&self, line: u32) -> bool {
+        (self.start..=self.end).contains(&line)
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum CodePosition {
+    Center,
+    #[default]
+    TopRight,
+    BottomLeft,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CodeAnnotation {
+    pub t: f32,
+    pub dur: f32,
+    pub code: String,
+    #[serde(default = "default_language")]
+    pub language: String,
+    #[serde(default)]
+    pub title: String,
+    #[serde(default)]
+    pub highlight: Vec,
+    #[serde(default)]
+    pub focus: Vec,
+    #[serde(default)]
+    pub position: CodePosition,
+}
+
+fn default_language() -> String {
+    "tsx".to_owned()
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct ShowcaseProps {
+    pub layout: Layout,
+    pub labels: Vec,
+    pub title: String,
+    pub subtitle: String,
+    pub preset: Preset,
+    pub keys: Vec,
+    pub effects: Vec,
+    pub sections: Vec
, + /// Output size; omitted => 2560x1440 for `inspect`, 1920x1080 otherwise. + pub width: u32, + pub height: u32, + pub speed_note: String, + pub window_title: String, + /// Playback multiplier applied once, to every clip. Overlay times (keys, sections, + /// effects, codeAnnotations) are already output seconds. + pub speed: f64, + /// Omitted => `inspect` for side-by-side, `standard` otherwise. + pub fidelity: Fidelity, + pub object_fit: ObjectFit, + /// Timed syntax-highlighted code overlays, relative to the first clip frame. + pub code_annotations: Vec, + /// Presentation of the title->content and content->outro transitions. + pub transition_style: TransitionStyle, +} + +impl Default for ShowcaseProps { + fn default() -> Self { + Self { + layout: Layout::Single, + labels: Vec::new(), + title: "Demo".to_owned(), + subtitle: String::new(), + preset: Preset::Factory, + keys: Vec::new(), + effects: Vec::new(), + sections: Vec::new(), + width: 1920, + height: 1080, + speed_note: String::new(), + window_title: String::new(), + speed: 1., + fidelity: Fidelity::Standard, + object_fit: ObjectFit::Contain, + code_annotations: Vec::new(), + transition_style: TransitionStyle::MotionBlur, + } + } +} + +impl ShowcaseProps { + /// Parses props JSON. `fidelity` (a profile name) overrides the one in the JSON; the + /// fidelity and output size defaults depend on other fields, so they are resolved here. + pub fn from_json(json: &str, fidelity: Option<&str>) -> Result { + if json.trim().is_empty() { + return Err("props JSON is empty".to_owned()); + } + let mut value: Value = serde_json::from_str(json).map_err(|err| format!("props JSON is invalid: {err}"))?; + let object = value.as_object_mut().ok_or("props JSON must be an object")?; + resolve_defaults(object, fidelity); + let props: Self = serde_path_to_error::deserialize(value).map_err(|err| format!("invalid props: {err}"))?; + props.validate()?; + Ok(props) + } + + fn validate(&self) -> Result<(), String> { + if !(self.speed.is_finite() && self.speed > 0.) { + return Err(format!("invalid props: speed must be a positive finite number, got {}", self.speed)); + } + if self.width == 0 || self.height == 0 || self.width % 2 == 1 || self.height % 2 == 1 { + return Err(format!( + "invalid props: width and height must be positive even numbers (yuv420p), got {}x{}", + self.width, self.height + )); + } + let ranges = self.code_annotations.iter().flat_map(|a| a.highlight.iter().chain(&a.focus)); + if ranges.clone().any(|range| range.start == 0 || range.end == 0) { + return Err("invalid props: code line ranges are 1-based (start and end must be positive)".to_owned()); + } + Ok(()) + } +} + +fn resolve_defaults(props: &mut Map, fidelity: Option<&str>) { + fn default(props: &mut Map, key: &str, value: impl Into) { + if props.get(key).is_none_or(Value::is_null) { + props.insert(key.to_owned(), value.into()); + } + } + if let Some(fidelity) = fidelity { + props.insert("fidelity".to_owned(), Value::from(fidelity)); + } + let side_by_side = props.get("layout").and_then(Value::as_str) == Some("side-by-side"); + default(props, "fidelity", if side_by_side { "inspect" } else { "standard" }); + let inspect = props.get("fidelity").and_then(Value::as_str) == Some("inspect"); + default(props, "width", if inspect { 2560 } else { 1920 }); + default(props, "height", if inspect { 1440 } else { 1080 }); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(json: &str) -> ShowcaseProps { + ShowcaseProps::from_json(json, None).unwrap() + } + + #[test] + fn missing_fields_take_composition_defaults() { + let props = parse(r#"{"clips":["ignored.mp4"],"unknown":1}"#); + assert_eq!(props.preset, Preset::Factory); + assert_eq!(props.fidelity, Fidelity::Standard); + assert_eq!((props.width, props.height), (1920, 1080)); + assert_eq!(props.speed, 1.); + assert_eq!(props.transition_style, TransitionStyle::MotionBlur); + } + + #[test] + fn fidelity_defaults_by_layout_and_sizes_by_fidelity() { + let side = parse(r#"{"layout":"side-by-side"}"#); + assert_eq!(side.fidelity, Fidelity::Inspect); + assert_eq!((side.width, side.height), (2560, 1440)); + + let explicit = parse(r#"{"layout":"side-by-side","fidelity":"compact","height":720}"#); + assert_eq!(explicit.fidelity, Fidelity::Compact); + assert_eq!((explicit.width, explicit.height), (1920, 720)); + + let overridden = ShowcaseProps::from_json(r#"{"fidelity":"compact"}"#, Some("inspect")).unwrap(); + assert_eq!(overridden.fidelity, Fidelity::Inspect); + assert_eq!(overridden.width, 2560); + } + + #[test] + fn effects_parse_by_fx_tag_with_percent_regions() { + let props = parse( + r#"{"effects":[ + {"fx":"zoom","t":1,"dur":2,"to":{"x":"10%","y":"20%","w":"50%","h":"40%"},"ease":"x"}, + {"fx":"spotlight","t":1,"dur":2,"on":{"x":"0%","y":"0%","w":"50%","h":"50%"}}, + {"fx":"fade-out","t":1,"dur":2} + ]}"#, + ); + let Effect::Zoom { to, .. } = &props.effects[0] else { panic!("zoom") }; + assert_eq!((to.x, to.w), (Pct(0.1), Pct(0.5))); + let Effect::Spotlight { dim, .. } = &props.effects[1] else { panic!("spotlight") }; + assert_eq!(*dim, 0.6); + assert_eq!(props.effects[2].unrendered_name(), Some("fade-out")); + } + + #[test] + fn code_annotations_take_schema_defaults() { + let props = parse(r#"{"codeAnnotations":[{"t":0,"dur":1,"code":"x"}]}"#); + let annotation = &props.code_annotations[0]; + assert_eq!(annotation.language, "tsx"); + assert_eq!(annotation.position, CodePosition::TopRight); + assert!(annotation.highlight.is_empty() && annotation.focus.is_empty()); + } + + #[test] + fn invalid_values_are_rejected() { + for json in [ + "", + "[]", + r#"{"speed":0}"#, + r#"{"speed":-2}"#, + r#"{"speed":true}"#, + r#"{"width":641}"#, + r#"{"preset":"neon"}"#, + r#"{"fidelity":"ultra"}"#, + r#"{"effects":[{"fx":"zoom","t":0,"dur":1,"to":{"x":"left","y":"0%","w":"1%","h":"1%"}}]}"#, + r#"{"codeAnnotations":[{"t":0,"dur":1,"code":"x","highlight":[{"start":0,"end":1}]}]}"#, + ] { + assert!(ShowcaseProps::from_json(json, None).is_err(), "{json} must be rejected"); + } + } +} diff --git a/plugins/droid-control/fframes/src/raster.rs b/plugins/droid-control/fframes/src/raster.rs new file mode 100644 index 0000000..5196274 --- /dev/null +++ b/plugins/droid-control/fframes/src/raster.rs @@ -0,0 +1,234 @@ +//! Rasterising markup in Rust, outside fframes' single tree per frame: frames composited in +//! layers, static layers baked once and reused as images, images resampled to the screen +//! pixels they cover, and the scene blurred at reduced resolution behind frosted panels. +//! Each stands in for work a CPU rasteriser would otherwise redo on every frame: large +//! Gaussian blurs, sixteen-tap bicubic sampling, and images run through its floating point +//! pipeline even where they land pixel for pixel. + +use crate::composite::{Pixels, resample}; +use crate::fonts; +use crate::svg::{Rect, Similarity}; +use fframes::usvgr::{self, Group, ImageKind, Node, PreloadedImageData, fontdb}; +use fframes::{Svgr, svgr}; +use std::cell::RefCell; +use std::collections::{HashMap, VecDeque}; +use std::mem; +use std::sync::{Arc, LazyLock, Mutex}; +use svgr::tiny_skia::{IntSize, Pixmap, Transform}; + +static FONTS: LazyLock = LazyLock::new(|| { + let mut fonts_db = fontdb::Database::new(); + for font in fonts::font_data() { + fonts_db.load_font_source(fontdb::Source::Binary(font.data)); + } + fonts_db.load_system_fonts(); + fonts_db +}); + +struct Rasterizer { + converter: usvgr::Cache, + cache: svgr::SvgrCache, + pool: svgr::PixmapPool, +} + +thread_local! { + static RASTERIZER: RefCell = RefCell::new(Rasterizer { + converter: usvgr::Cache::new_with_text_cache(10), + cache: svgr::SvgrCache::new(20), + pool: svgr::PixmapPool::new(), + }); +} + +/// `content` over `area` (user units) at `scale` pixels per unit. +pub fn rasterize(content: Svgr<'_>, area: Rect, scale: f32) -> Pixels { + let (width, height) = ((area.w * scale).round().max(1.) as u32, (area.h * scale).round().max(1.) as u32); + let mut pixels = Pixels::transparent(width, height); + render(content, area, &mut pixels); + pixels +} + +/// `content` over `area` (user units) drawn onto `pixels`, which cover `area`. +fn render(content: Svgr<'_>, area: Rect, pixels: &mut Pixels) { + let view_box = format!("{} {} {} {}", area.x, area.y, area.w, area.h); + let document = svgr!( + {content} + ); + let size = IntSize::from_wh(pixels.width, pixels.height).expect("pixels are never empty"); + let mut pixmap = Pixmap::from_vec(mem::take(&mut pixels.data), size).expect("pixels hold their size"); + RASTERIZER.with_borrow_mut(|Rasterizer { converter, cache, pool }| { + let tree = document + .into_svg_tree(&usvgr::Options::default(), converter, &FONTS) + .expect("rasterised markup is valid SVG"); + let context = svgr::Context::new_from_pixmap_unsafe(&pixmap); + let fit = Transform::from_scale(pixmap.width() as f32 / area.w, pixmap.height() as f32 / area.h); + svgr::render(&tree, fit, &mut pixmap.as_mut(), cache, pool, &context); + }); + pixels.data = pixmap.take(); +} + +impl Pixels { + /// Rasterises `markup` onto these pixels, one per user unit. + pub fn draw(&mut self, markup: Svgr<'_>) { + render(markup, Rect::new(0., 0., self.width as f32, self.height as f32), self); + } +} + +/// `image` drawn pixel for pixel from the origin. +pub fn image<'a>(image: Arc) -> Svgr<'a> { + let (width, height) = (image.width, image.height); + svgr!() +} + +/// The pixels of a tree that is just one `image`: a frame composited before it reached the tree. +pub fn sole_image(tree: usvgr::Tree) -> Option { + fn find(group: &Group) -> Option<&Arc> { + match group.children() { + [Node::Group(group)] => find(group), + [Node::Image(image)] => match image.kind() { + ImageKind::DATA(data) => Some(data), + ImageKind::SVG { .. } => None, + }, + _ => None, + } + } + let image = Arc::clone(find(tree.root())?); + drop(tree); + Some(match Arc::try_unwrap(image) { + Ok(image) => Pixels { width: image.width, height: image.height, data: image.data.into_owned() }, + Err(shared) => Pixels::of(&shared), + }) +} + +/// `image`, whole screen pixels at `at`, drawn 1:1 from inside a box placed by `to_screen`. +pub fn on_pixels<'a>(image: Arc, at: Rect, to_screen: Similarity) -> Svgr<'a> { + svgr!( + + + + ) +} + +static BAKED: LazyLock>>> = LazyLock::new(Default::default); + +/// A static layer over `area`, drawn once per `key` over the whole pixels `area` touches +/// (in its units, passed to `draw`): the image, and that rect. `key` identifies everything +/// `draw` depends on except where `area` sits. +pub fn baked(key: String, area: Rect, draw: impl FnOnce(Rect) -> Pixels) -> (Arc, Rect) { + let pixels = area.whole_pixels(); + let key = format!("{key} at +{}+{}", area.x - pixels.x, area.y - pixels.y); + // Held while drawing, so render workers needing the same layer at once draw it once. + let mut baked = BAKED.lock().expect("bake cache lock"); + let image = baked.entry(key).or_insert_with(|| draw(pixels).into_image()); + (Arc::clone(image), pixels) +} + +/// `content` through an SVG Gaussian blur of `sigma` over `region` (see `Pixels::blur`): +/// rasterised over the whole pixels `region` touches and blurred there, and that rect. +pub fn blurred(content: Svgr<'_>, region: Rect, sigma: f32) -> (Pixels, Rect) { + let pixels = region.whole_pixels(); + let mut layer = rasterize(content, pixels, 1.); + layer.blur(sigma); + (layer, pixels) +} + +/// Where an image stretched over `drawn` (screen units) shows inside `bounds`: the whole +/// screen pixels it covers there, and the part of the image (in its `size` pixels) they show. +/// A partly covered pixel at the edges is left out. +pub fn coverage((iw, ih): (f32, f32), drawn: Rect, bounds: Rect) -> Option<(Rect, Rect)> { + const SLACK: f32 = 1e-3; + let visible = drawn.intersect(bounds); + let (x, y) = ((visible.x - SLACK).ceil(), (visible.y - SLACK).ceil()); + let (right, bottom) = ((visible.right() + SLACK).floor(), (visible.bottom() + SLACK).floor()); + if x >= right || y >= bottom { + return None; + } + let (sx, sy) = (iw / drawn.w, ih / drawn.h); + let (cx, cy) = (((x - drawn.x) * sx).max(0.), ((y - drawn.y) * sy).max(0.)); + let crop = Rect::new(cx, cy, ((right - drawn.x) * sx).min(iw) - cx, ((bottom - drawn.y) * sy).min(ih) - cy); + Some((Rect::new(x, y, right - x, bottom - y), crop)) +} + +type Resampled = (Arc, Rect, Rect, Arc); + +thread_local! { + /// Recent `on_screen` results: source, screen pixels, crop and the resampled image. The + /// source is held so that its address, which identifies it, is not reused meanwhile. + static RESAMPLED: RefCell> = RefCell::default(); +} + +/// `image` stretched over `area` (local units) placed on screen by `to_screen`, resampled to +/// the whole screen pixels it covers inside `bounds` (see `coverage`), and those pixels: one +/// separable resample instead of tiny-skia's sixteen taps per output pixel, and none at all +/// for an image that holds still, whose last results are kept. +pub fn on_screen( + image: &Arc, + area: Rect, + to_screen: Similarity, + bounds: Rect, +) -> Option<(Arc, Rect)> { + let size = (image.width as f32, image.height as f32); + let (at, crop) = coverage(size, to_screen.apply(area), bounds)?; + let resampled = RESAMPLED.with_borrow_mut(|recent| { + let hit = recent.iter().find(|(source, a, c, _)| Arc::ptr_eq(source, image) && (*a, *c) == (at, crop)); + if let Some((.., resampled)) = hit { + return Arc::clone(resampled); + } + let resampled = if crop == Rect::new(0., 0., size.0, size.1) && (at.w, at.h) == size { + Arc::clone(image) + } else { + resample(&image.data, (image.width, image.height), crop, (at.w as u32, at.h as u32)).into_image() + }; + if recent.len() == 16 { + recent.pop_front(); + } + recent.push_back((Arc::clone(image), at, crop, Arc::clone(&resampled))); + resampled + }); + Some((resampled, at)) +} + +/// The scene behind the overlays, blurred where a frosted panel shows it. Each blur runs on +/// a downscaled copy (still at least 3 px of blur wide) of just the part of the scene it +/// needs, and each patch is scaled back up to screen pixels, like browsers do for large +/// `backdrop-filter` radii. +pub struct Backdrop { + scene: Pixels, +} + +impl Backdrop { + pub fn new(scene: Pixels) -> Self { + Self { scene } + } + + /// The scene under `area` (screen units) blurred by `sigma`, and the screen rect it covers. + pub fn frosted(&self, sigma: f32, area: Rect) -> Option<(Arc, Rect)> { + let scene = &self.scene; + let (w, h) = (scene.width as f32, scene.height as f32); + // The whole screen pixels `area` touches, so the patch is drawn 1:1. + let at = area.whole_pixels().intersect(Rect::new(0., 0., w, h)); + if at.w <= 0. || at.h <= 0. { + return None; + } + let factor = (sigma / 3.).floor().clamp(1., 4.); + let (small_w, small_h) = ((w / factor).round(), (h / factor).round()); + let (sx, sy) = (small_w / w, small_h / h); + let sigma = sigma * sx; + // Only the part the patch is resampled from is downscaled and blurred, with room for + // the blur's reach and the resampling filter around it: there it comes out as if all + // of the scene were. + let reach = (Pixels::blur_reach(sigma) + 3) as f32; + let (rx, ry) = ((at.x * sx - reach).floor().max(0.), (at.y * sy - reach).floor().max(0.)); + let (region_w, region_h) = + ((at.right() * sx + reach).ceil().min(small_w) - rx, (at.bottom() * sy + reach).ceil().min(small_h) - ry); + let source = Rect::new(rx / sx, ry / sy, region_w / sx, region_h / sy); + let mut region = resample(&scene.data, (scene.width, scene.height), source, (region_w as u32, region_h as u32)); + region.blur(sigma); + let crop = Rect::new(at.x * sx - rx, at.y * sy - ry, at.w * sx, at.h * sy); + let patch = resample(®ion.data, (region.width, region.height), crop, (at.w as u32, at.h as u32)); + Some((patch.into_image(), at)) + } + + pub fn into_scene(self) -> Pixels { + self.scene + } +} diff --git a/plugins/droid-control/fframes/src/render.rs b/plugins/droid-control/fframes/src/render.rs new file mode 100644 index 0000000..28c9786 --- /dev/null +++ b/plugins/droid-control/fframes/src/render.rs @@ -0,0 +1,183 @@ +//! Rasterising the composition at the props size: single PNG stills, and whole videos +//! rendered on every core, converted to yuv420p there and piped to ffmpeg for the H.264 +//! encode. + +use crate::Showcase; +use crate::clips::Clip; +use crate::fonts; +use crate::raster; +use fframes::media::VideoMedia; +use fframes::{DynamicMediaProvider, Previewer, RenderOptions, RgbaFrame}; +use multiversion::multiversion; +use std::collections::HashMap; +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::sync::mpsc; +use std::thread; + +/// Consecutive frames per worker turn: long enough runs keep each worker's decoders reading +/// forward, short enough that the in-order writer never waits long. +const BLOCK: usize = 8; + +/// The clip files and the embedded fonts, as fframes media. +pub fn media(clips: &[Clip]) -> DynamicMediaProvider<'static> { + let videos = + clips.iter().map(|clip| (clip.name.clone(), VideoMedia { path: clip.path.clone(), metadata: None })).collect(); + DynamicMediaProvider::new(HashMap::new(), HashMap::new(), HashMap::new(), videos, fonts::font_data()) +} + +struct FrameSource<'a> { + previewer: Previewer<'a, 'a, Showcase>, +} + +impl<'a> FrameSource<'a> { + fn new(showcase: &'a Showcase, media: &'a DynamicMediaProvider<'static>) -> Result { + // System fonts back the embedded Geist families for glyphs they lack (CJK, symbols). + let options = RenderOptions { media: Some(media), load_system_fonts: true, ..Default::default() }; + let previewer = + Previewer::new(showcase, &options).map_err(|err| format!("could not prepare the renderer: {err}"))?; + Ok(Self { previewer }) + } + + /// The showcase composites each frame itself, so its tree is that one image. + fn render(&mut self, frame: usize) -> Result { + let tree = self.previewer.svg_tree(frame).map_err(|err| format!("frame {frame}: {err}"))?; + let pixels = raster::sole_image(tree).expect("showcase frames are one image"); + // Opaque, so the premultiplied pixels are the straight ones. + Ok(RgbaFrame { width: pixels.width, height: pixels.height, pixels: pixels.data }) + } +} + +/// Renders one frame to a PNG. +pub fn still( + showcase: &Showcase, + media: &DynamicMediaProvider<'static>, + frame: usize, + output: &Path, +) -> Result<(), String> { + let total = showcase.timeline().total(); + if frame >= total { + return Err(format!("--still {frame} is past the last frame ({})", total - 1)); + } + FrameSource::new(showcase, media)? + .render(frame)? + .save_png(output) + .map_err(|err| format!("could not write {}: {err}", output.display())) +} + +/// `frame` in yuv420p with BT.709 limited range, what the encoder takes, with each chroma +/// sample the mean of its 2x2 pixels. Coefficients are in 2^-15. +#[multiversion(targets = "simd")] +fn yuv420(frame: &RgbaFrame) -> Vec { + let (width, height) = (frame.width as usize, frame.height as usize); + let mut yuv = vec![0; width * height * 3 / 2]; + let (luma, chroma) = yuv.split_at_mut(width * height); + let (blue, red) = chroma.split_at_mut(width * height / 4); + for (y, px) in luma.iter_mut().zip(frame.pixels.as_chunks::<4>().0) { + let [r, g, b, _] = px.map(i32::from); + *y = ((5983 * r + 20127 * g + 2032 * b + (16 << 15) + (1 << 14)) >> 15) as u8; + } + let rows = + frame.pixels.chunks_exact(width * 8).zip(blue.chunks_exact_mut(width / 2).zip(red.chunks_exact_mut(width / 2))); + for (pair, (blue, red)) in rows { + let (top, bottom) = pair.split_at(width * 4); + let blocks = top.as_chunks::<8>().0.iter().zip(bottom.as_chunks::<8>().0); + for ((cb, cr), (top, bottom)) in blue.iter_mut().zip(red.iter_mut()).zip(blocks) { + let sum = + |c: usize| i32::from(top[c]) + i32::from(top[c + 4]) + i32::from(bottom[c]) + i32::from(bottom[c + 4]); + let (r, g, b) = (sum(0), sum(1), sum(2)); + *cb = ((-3298 * r - 11094 * g + 14392 * b + (128 << 17) + (1 << 16)) >> 17) as u8; + *cr = ((14392 * r - 13072 * g - 1320 * b + (128 << 17) + (1 << 16)) >> 17) as u8; + } + } + yuv +} + +/// Renders every frame and encodes them to H.264 (yuv420p, BT.709 limited range). +pub fn video(showcase: &Showcase, media: &DynamicMediaProvider<'static>, output: &Path) -> Result<(), String> { + let total = showcase.timeline().total(); + let props = showcase.props(); + let encoding = props.fidelity.encoding(); + let mut ffmpeg = Command::new("ffmpeg") + .args(["-hide_banner", "-loglevel", "error", "-y", "-f", "rawvideo", "-pix_fmt", "yuv420p"]) + .args(["-s", &format!("{}x{}", props.width, props.height), "-framerate", "30"]) + .args([ + "-colorspace", + "bt709", + "-color_primaries", + "bt709", + "-color_trc", + "bt709", + "-color_range", + "tv", + "-i", + "-", + ]) + .args(["-c:v", "libx264", "-preset", encoding.preset, "-crf", &encoding.crf.to_string(), "-pix_fmt", "yuv420p"]) + .args(["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv"]) + .args(["-movflags", "+faststart"]) + .arg(output) + .stdin(Stdio::piped()) + .spawn() + .map_err(|err| format!("could not run ffmpeg: {err}"))?; + let mut encoder = ffmpeg.stdin.take().expect("ffmpeg stdin is piped"); + let workers = thread::available_parallelism().map_or(1, usize::from).min(total.div_ceil(BLOCK)).max(1); + + let rendered = thread::scope(|scope| { + // Worker `w` renders blocks w, w + workers, ...; the writer drains them in frame order. + let queues: Vec, String>>> = (0..workers) + .map(|worker| { + let (frames, queue) = mpsc::sync_channel(BLOCK); + scope.spawn(move || { + let mut source = match FrameSource::new(showcase, media) { + Ok(source) => source, + Err(err) => return drop(frames.send(Err(err))), + }; + for frame in (0..total).filter(|frame| (frame / BLOCK) % workers == worker) { + let result = source.render(frame).map(|rgba| yuv420(&rgba)); + let failed = result.is_err(); + if frames.send(result).is_err() || failed { + return; + } + } + }); + queue + }) + .collect(); + for frame in 0..total { + let yuv = queues[(frame / BLOCK) % workers].recv().map_err(|_| "a render worker stopped".to_owned())??; + encoder.write_all(&yuv).map_err(|err| format!("ffmpeg stopped accepting frames: {err}"))?; + if (frame + 1) % 30 == 0 || frame + 1 == total { + eprint!("\rrendered {}/{total} frames", frame + 1); + } + } + eprintln!(); + Ok::<(), String>(()) + }); + drop(encoder); + if rendered.is_err() { + let _ = ffmpeg.kill(); + } + let status = ffmpeg.wait().map_err(|err| format!("ffmpeg did not finish: {err}"))?; + rendered?; + if !status.success() { + return Err(format!("ffmpeg failed to encode {} ({status})", output.display())); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn yuv420_is_bt709_limited_range_with_averaged_chroma() { + // A 2x2 block: white, black, and pure red twice. + let pixels = [[255, 255, 255, 255], [0, 0, 0, 255], [255, 0, 0, 255], [255, 0, 0, 255]].concat(); + let yuv = yuv420(&RgbaFrame { width: 2, height: 2, pixels }); + assert_eq!(yuv[..4], [235, 16, 63, 63]); + // Red's (102, 240) and black and white's neutral 128, averaged. + assert_eq!(yuv[4..], [115, 184]); + } +} diff --git a/plugins/droid-control/fframes/src/scenery.rs b/plugins/droid-control/fframes/src/scenery.rs new file mode 100644 index 0000000..d927da8 --- /dev/null +++ b/plugins/droid-control/fframes/src/scenery.rs @@ -0,0 +1,205 @@ +//! Layers that frame every segment: the warm background, drifting particles, the rotor +//! watermark, film grain and the final colour grade. The full-frame ones are rasterised +//! once and composited per frame (see `composite`). + +use crate::composite::{Blend, Grade, Pixels}; +use crate::raster; +use crate::svg::{Rect, Rgba, faded, rotor, stops}; +use crate::theme::{BgStyle, Palette, PresetConfig, Treatment}; +use fframes::media::ImageData; +use fframes::usvgr::PreloadedImageData; +use fframes::{Svgr, svgr}; +use std::collections::VecDeque; +use std::f32::consts::FRAC_1_SQRT_2; +use std::sync::{Arc, Mutex}; + +const HALFTONE: &[u8] = include_bytes!("../media/bg-halftone-rotor.png"); + +/// The warm glow level is rounded to this many steps per unit: a step moves no channel by +/// more than about a quarter of an 8-bit level, and runs of frames share one painting (and +/// svgr's resampled copy of it). +const GLOW_STEPS: f32 = 512.; + +/// A CSS `radial-gradient(ellipse at center, ...)` (farthest-corner) filling `r`. +fn radial<'a>(r: Rect, css: &[(f32, Rgba)]) -> Svgr<'a> { + svgr!( + + {stops(css)} + + + ) +} + +/// A warm corner glow at full strength, rasterised over the whole pixels it touches. +struct Glow { + at: (u32, u32), + image: Arc, + /// Its opacity relative to the shared glow level. + strength: f32, +} + +/// The warm background: a vignette gradient, four corner glows that intensify from a mild +/// wash to a rich bloom over a segment, and the halftone rotor texture screened over them. +struct Warm { + base: Pixels, + glows: Vec, + halftone: Arc, +} + +enum Painting { + Warm(Warm), + /// A background that never changes. + Still(Arc), +} + +pub struct Scenery { + painting: Painting, + /// Recent warm paintings by glow step, shared by the render workers. + painted: Mutex)>>, + /// Fractal noise grain, a colour-blended temperature tint and a soft corner vignette. + grade: Grade, +} + +impl Scenery { + pub fn new( + palette: &'static Palette, + config: &'static PresetConfig, + treatment: Treatment, + size: (f32, f32), + ) -> Result { + let (w, h) = size; + let full = Rect::new(0., 0., w, h); + let painting = if palette.warm { + let halftone = ImageData::new_from_bytes("bg-halftone-rotor.png", HALFTONE) + .map_err(|err| format!("could not decode the embedded halftone texture: {err:?}"))?; + let side = w.max(h); + let glow = |(x, y, bw, bh): (f32, f32, f32, f32), color: Rgba, strength: f32, edge: f32| { + let area = Rect::new(x * w, y * h, bw * w, bh * h); + let (x0, y0) = (area.x.floor().max(0.), area.y.floor().max(0.)); + let (x1, y1) = (area.right().ceil().min(w), area.bottom().ceil().min(h)); + let touched = Rect::new(x0, y0, x1 - x0, y1 - y0); + let image = raster::rasterize(radial(area, &[(0., color), (edge, color.alpha(0.))]), touched, 1.); + Glow { at: (x0 as u32, y0 as u32), image: image.into_image(), strength } + }; + Painting::Warm(Warm { + base: raster::rasterize( + radial( + full, + &[ + (0., Rgba::hex(palette.bg)), + (0.55, Rgba::hex("#1a0e08")), + (0.85, Rgba::hex("#2a1510")), + (1., Rgba::hex("#351a12")), + ], + ), + full, + 1., + ), + glows: vec![ + glow((-0.1, 0.5, 0.6, 0.6), Rgba(200, 80, 20, 1.), 1., 0.7), + glow((0.55, -0.15, 0.55, 0.55), Rgba(180, 60, 15, 1.), 0.8, 0.7), + glow((0.6, 0.55, 0.45, 0.5), Rgba(160, 40, 10, 1.), 0.6, 0.65), + glow((-0.05, -0.08, 0.4, 0.4), Rgba(238, 96, 24, 1.), 0.4, 0.6), + ], + halftone: raster::rasterize( + svgr!(), + full, + 1., + ) + .into_image(), + }) + } else { + let still = match config.bg_style { + BgStyle::Gradient => radial( + full, + &[ + (0., Rgba::hex(palette.bg)), + (0.5, Rgba::hex("#0f0f1a")), + (0.8, Rgba::hex("#121220")), + (1., Rgba::hex("#1a1028")), + ], + ), + BgStyle::Solid => svgr!(), + }; + Painting::Still(raster::rasterize(still, full, 1.).into_image()) + }; + let intensity = treatment.grade_intensity; + let grade = Grade::new( + &raster::rasterize( + svgr!( + + + + + + + ), + full, + 1., + ), + treatment.noise_opacity, + if palette.warm { Rgba(200, 120, 40, intensity) } else { Rgba(80, 100, 200, intensity * 0.75) }, + &raster::rasterize(radial(full, &[(0.4, Rgba::BLACK.alpha(0.)), (1., Rgba::BLACK.alpha(0.15))]), full, 1.), + ); + Ok(Self { painting, painted: Mutex::default(), grade }) + } + + /// The opaque full-frame background at `frame` of a `total`-frame segment. + pub fn background(&self, frame: usize, total: usize) -> Arc { + let (base, glows, halftone) = match &self.painting { + Painting::Warm(Warm { base, glows, halftone }) => (base, glows, halftone), + Painting::Still(still) => return Arc::clone(still), + }; + let step = ((0.05 + 0.15 * (frame as f32 / total as f32).clamp(0., 1.)) * GLOW_STEPS).round() as u32; + let cached = self + .painted + .lock() + .expect("paintings lock") + .iter() + .find(|(s, _)| *s == step) + .map(|(_, image)| Arc::clone(image)); + cached.unwrap_or_else(|| { + let level = step as f32 / GLOW_STEPS; + let mut painted = base.clone(); + for Glow { at, image, strength } in glows { + painted.blend(image, *at, level * strength, Blend::Over); + } + painted.blend(halftone, (0, 0), 0.15, Blend::Screen); + let image = painted.into_image(); + let mut recent = self.painted.lock().expect("paintings lock"); + // Enough for every worker's current frame and the next. + if recent.len() == 8 { + recent.pop_front(); + } + recent.push_back((step, Arc::clone(&image))); + image + }) + } + + /// Film grain and the colour grade over a finished, opaque frame: the topmost layers. + pub fn grade(&self, frame: &mut Pixels) { + self.grade.apply(frame); + } +} + +/// 30 accent dots on slow Lissajous paths. +pub fn particles<'a>(accent: &'a str, (w, h): (f32, f32), frame: usize) -> Svgr<'a> { + let f = frame as f32; + let dots: Svgr = (0..30) + .map(|i| { + let base_x = ((i * 73 + 17) % 100) as f32 / 100. * w; + let base_y = ((i * 47 + 31) % 100) as f32 / 100. * h; + let size = (2 + (i % 3) * 2) as f32; + let dx = (f * 0.008 + (i * 13 + 7) as f32 * 0.1).sin() * 60.; + let dy = (f * 0.006 + (i * 19 + 11) as f32 * 0.1).cos() * 40.; + svgr!() + }) + .collect(); + faded(0.07, dots) +} + +/// The rotor in the bottom-right corner, fading in to 20% over the first second. +pub fn watermark<'a>((w, h): (f32, f32), frame: usize) -> Svgr<'a> { + let size = 48.; + faded(0.2 * (frame as f32 / 30.).min(1.), rotor(w - 32. - size, h - 24. - size, size, "white")) +} diff --git a/plugins/droid-control/fframes/src/showcase.rs b/plugins/droid-control/fframes/src/showcase.rs new file mode 100644 index 0000000..26c0bcd --- /dev/null +++ b/plugins/droid-control/fframes/src/showcase.rs @@ -0,0 +1,200 @@ +//! The Showcase composition: background and particles under a title card, the clip content +//! with its overlays and the Droid outro (joined by two transitions), then the watermark, +//! and the film grain and colour grade. Each frame is composited in layers (see `raster`). + +use crate::clips::{self, Clip}; +use crate::composite::Pixels; +use crate::content::Stage; +use crate::outro; +use crate::overlays::{CodeCard, Overlays}; +use crate::props::{Effect, ShowcaseProps}; +use crate::raster::{self, Backdrop}; +use crate::scenery::{self, Scenery}; +use crate::theme::{Palette, PresetConfig}; +use crate::timing::{self, TITLE_FRAMES, TRANSITION_FRAMES, Timeline}; +use crate::title::{TitleCard, TitleLayout}; +use crate::transition::{Direction, Transition}; +use fframes::{AudioMap, Duration, FFramesContext, Frame, Svgr, Video, svgr}; +use std::collections::HashSet; + +pub struct Showcase { + props: ShowcaseProps, + clips: Vec, + timeline: Timeline, + palette: &'static Palette, + config: &'static PresetConfig, + size: (f32, f32), + scenery: Scenery, + title: TitleLayout, + code_cards: Vec, +} + +impl Showcase { + /// `clips` are probed sources; the longest one sets the content length. + pub fn new(props: ShowcaseProps, clips: Vec) -> Result { + let mut warned = HashSet::new(); + for name in props.effects.iter().filter_map(Effect::unrendered_name) { + if warned.insert(name) { + eprintln!("Showcase: effect fx='{name}' is schema-valid but not rendered by this composition"); + } + } + let palette = props.preset.palette(); + let config = props.preset.config(); + let size = (props.width as f32, props.height as f32); + let scenery = Scenery::new(palette, config, props.fidelity.treatment(), size)?; + let code_cards = props.code_annotations.iter().map(|a| CodeCard::new(a, palette, config, size)).collect(); + Ok(Self { + timeline: Timeline { content: timing::content_frames(clips::longest(&clips), props.speed) }, + title: TitleLayout::new(&props.title, &props.subtitle, size.0), + props, + clips, + palette, + config, + size, + scenery, + code_cards, + }) + } + + pub fn props(&self) -> &ShowcaseProps { + &self.props + } + + pub fn clips(&self) -> &[Clip] { + &self.clips + } + + pub fn timeline(&self) -> Timeline { + self.timeline + } + + /// The finished frame at `frame.index`, opaque. Between its transitions the content + /// paints its own background over the whole frame; around them the background and + /// particles show. + fn frame(&self, frame: &Frame, ctx: &FFramesContext<'_, '_>) -> Pixels { + let f = frame.index; + let timeline = self.timeline; + let mut canvas = if (timeline.clips_start()..timeline.outro_start()).contains(&f) { + self.content(f - timeline.content_start(), frame, ctx) + } else { + let mut canvas = Pixels::of(&self.scenery.background(f, timeline.total())); + canvas.draw(svgr!({scenery::particles(self.palette.accent, self.size, f)}{self.series(frame, ctx)})); + canvas + }; + if self.props.preset.is_factory() { + canvas.draw(scenery::watermark(self.size, f)); + } + self.scenery.grade(&mut canvas); + canvas + } + + /// Title, content and outro with their transitions at global `frame.index`. + fn series<'a>(&'a self, frame: &Frame, ctx: &FFramesContext<'_, '_>) -> Svgr<'a> { + let f = frame.index; + let (content_start, outro_start) = (self.timeline.content_start(), self.timeline.outro_start()); + let transition = Transition { style: self.props.transition_style, warm: self.palette.warm, size: self.size }; + let progress = |since: usize| since as f32 / TRANSITION_FRAMES as f32; + let mut scenes = Vec::with_capacity(2); + if f < TITLE_FRAMES { + let props = &self.props; + let card = TitleCard { + layout: &self.title, + title: &props.title, + subtitle: &props.subtitle, + speed_note: &props.speed_note, + palette: self.palette, + size: self.size, + }; + let title = card.draw(f); + scenes.push(match f.checked_sub(content_start) { + Some(since) => transition.present(Direction::Exiting, progress(since), title), + None => title, + }); + } + if (content_start..outro_start + TRANSITION_FRAMES).contains(&f) { + let local = f - content_start; + let content = raster::image(self.content(local, frame, ctx).into_image()); + scenes.push(if local < TRANSITION_FRAMES { + transition.present(Direction::Entering, progress(local), content) + } else if f >= outro_start { + transition.present(Direction::Exiting, progress(f - outro_start), content) + } else { + content + }); + } + if let Some(local) = f.checked_sub(outro_start) { + let outro = outro::draw(self.palette, self.size, local); + scenes.push(if local < TRANSITION_FRAMES { + transition.present(Direction::Entering, progress(local), outro) + } else { + outro + }); + } + scenes.into_iter().collect() + } + + /// The content segment at `local` frames into it. The clips and every overlay start one + /// transition in, so overlay times are seconds from the first clip frame. + fn content(&self, local: usize, frame: &Frame, ctx: &FFramesContext<'_, '_>) -> Pixels { + let sequence = self.timeline.content_sequence(); + let mut scene = Pixels::of(&self.scenery.background(local, sequence)); + let Some(clock) = local.checked_sub(TRANSITION_FRAMES) else { + return scene; + }; + let props = &self.props; + let stage = Stage { + palette: self.palette, + config: self.config, + size: self.size, + layout: props.layout, + clips: &self.clips, + labels: &props.labels, + window_title: &props.window_title, + speed: props.speed, + object_fit: props.object_fit, + effects: &props.effects, + }; + stage.compose(&mut scene, clock, frame, ctx); + let backdrop = Backdrop::new(scene); + let overlays = Overlays { palette: self.palette, config: self.config, size: self.size, backdrop: &backdrop }; + let sweeps = + if props.sections.len() > 1 { overlays.section_sweeps(&props.sections, clock) } else { Svgr::empty() }; + let drawn = svgr!( + + {overlays.callouts(&props.effects, clock)} + {sweeps} + {overlays.section_headers(&props.sections, clock, sequence - TRANSITION_FRAMES)} + {overlays.keystrokes(&props.keys, clock)} + {overlays.code_cards(&self.code_cards, clock)} + + ); + let mut content = backdrop.into_scene(); + content.draw(drawn); + content + } +} + +impl Video for Showcase { + const FPS: usize = timing::FPS; + // Nominal: `render` rasterises every frame at the props size, whatever these say. + const WIDTH: usize = 1920; + const HEIGHT: usize = 1080; + + fn duration(&self) -> Duration<'_> { + Duration::Frames(self.timeline.total()) + } + + fn audio(&self) -> AudioMap<'_> { + AudioMap::none() + } + + /// The whole frame, composited and graded: one image, which `render` takes the pixels of. + fn render_frame<'a>(&'a self, frame: Frame, ctx: &FFramesContext<'a, '_>) -> Svgr<'a> { + let (w, h) = self.size; + svgr!( + + {raster::image(self.frame(&frame, ctx).into_image())} + + ) + } +} diff --git a/plugins/droid-control/fframes/src/stage.rs b/plugins/droid-control/fframes/src/stage.rs new file mode 100644 index 0000000..36cee8d --- /dev/null +++ b/plugins/droid-control/fframes/src/stage.rs @@ -0,0 +1,124 @@ +//! Clip staging: every input gets a distinct `clip-` name inside the render's work +//! directory (fframes keys decoders by file name), and asciinema casts become MP4s on their +//! own timeline. + +use crate::theme::Encoding; +use serde::Deserialize; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// The Droid CLI terminal palette for agg. +const DROID_CLI_THEME: &str = "181818,e0d0c0,15161e,f7768e,9ece6a,e0af68,7aa2f7,bb9af7,7dcfff,a9b1d6,414868,f7768e,9ece6a,e0af68,7aa2f7,bb9af7,7dcfff,c0caf5"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + Cast, + Video, +} + +/// The clip kind by extension; anything else (stills included) is refused. +pub fn kind(clip: &Path) -> Result { + match clip.extension().and_then(|ext| ext.to_str()) { + Some("cast") => Ok(Kind::Cast), + Some("mp4" | "webm") => Ok(Kind::Video), + _ => Err(format!( + "unsupported clip type: {} (clips are .cast, .mp4, or .webm; stills belong in the screenshot artifact path)", + clip.display() + )), + } +} + +/// Links (or converts) every clip into `work_dir` and returns the staged paths in order. +pub fn stage(clips: &[PathBuf], work_dir: &Path, encoding: &Encoding) -> Result, String> { + clips + .iter() + .enumerate() + .map(|(index, clip)| match kind(clip)? { + Kind::Cast => { + let staged = work_dir.join(format!("clip-{index}.mp4")); + convert_cast(clip, &staged, encoding)?; + Ok(staged) + } + Kind::Video => { + let ext = clip.extension().and_then(|ext| ext.to_str()).unwrap_or_default(); + let staged = work_dir.join(format!("clip-{index}.{ext}")); + let source = + fs::canonicalize(clip).map_err(|err| format!("clip is not readable: {}: {err}", clip.display()))?; + std::os::unix::fs::symlink(&source, &staged) + .map_err(|err| format!("could not stage {}: {err}", clip.display()))?; + Ok(staged) + } + }) + .collect() +} + +#[derive(Deserialize)] +struct CastHeader { + #[serde(default = "default_cols")] + width: u32, + #[serde(default = "default_rows")] + height: u32, +} + +fn default_cols() -> u32 { + 120 +} + +fn default_rows() -> u32 { + 36 +} + +fn run(command: &mut Command, what: &str) -> Result<(), String> { + let output = command.output().map_err(|err| format!("could not run {what}: {err}"))?; + if output.status.success() { + return Ok(()); + } + Err(format!("{what} failed ({}): {}", output.status, String::from_utf8_lossy(&output.stderr).trim())) +} + +/// agg renders the cast at its own pace (no idle compression: the composition applies +/// `speed` to every clip alike), then ffmpeg encodes the GIF losslessly: the composition +/// decodes it again, so it loses nothing to a second lossy encode, and the fastest preset +/// costs only file size. +fn convert_cast(cast: &Path, output: &Path, encoding: &Encoding) -> Result<(), String> { + let file = fs::File::open(cast).map_err(|err| format!("clip is not readable: {}: {err}", cast.display()))?; + let mut header = String::new(); + BufReader::new(file).read_line(&mut header).map_err(|err| format!("could not read {}: {err}", cast.display()))?; + let header: CastHeader = + serde_json::from_str(&header).map_err(|err| format!("not an asciinema cast: {}: {err}", cast.display()))?; + let gif = output.with_extension("gif"); + run( + Command::new("agg") + .args(["--speed", "1", "--idle-time-limit", "1000000000", "--renderer", "fontdue"]) + .args(["--cols", &header.width.to_string(), "--rows", &header.height.to_string()]) + .args(["--fps-cap", &encoding.cast_fps_cap.to_string(), "--theme", DROID_CLI_THEME]) + .arg(cast) + .arg(&gif), + "agg", + )?; + run( + Command::new("ffmpeg") + .args(["-v", "error", "-y", "-i"]) + .arg(&gif) + .args(["-movflags", "+faststart", "-pix_fmt", "yuv420p", "-c:v", "libx264", "-preset", "ultrafast"]) + .args(["-qp", "0", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2"]) + .arg(output), + "ffmpeg", + )?; + fs::remove_file(&gif).map_err(|err| format!("could not remove {}: {err}", gif.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_casts_and_videos_are_clips() { + assert_eq!(kind(Path::new("a/demo.cast")), Ok(Kind::Cast)); + assert_eq!(kind(Path::new("demo.webm")), Ok(Kind::Video)); + let refused = kind(Path::new("proof.png")).unwrap_err(); + assert!(refused.starts_with("unsupported clip type: proof.png"), "{refused}"); + } +} diff --git a/plugins/droid-control/fframes/src/svg.rs b/plugins/droid-control/fframes/src/svg.rs new file mode 100644 index 0000000..33bdbd8 --- /dev/null +++ b/plugins/droid-control/fframes/src/svg.rs @@ -0,0 +1,468 @@ +//! SVG building blocks shared by the layers: geometry, colours, blur filters and the +//! rounded "card" every overlay is drawn as. + +use crate::composite::{Blend, Pixels}; +use crate::fonts::Font; +use crate::raster::{self, Backdrop}; +use fframes::usvgr::PreloadedImageData; +use fframes::{Svgr, svgr}; +use std::fmt; +use std::sync::Arc; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rect { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, +} + +impl Rect { + pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self { + Self { x, y, w, h } + } + + pub fn cx(&self) -> f32 { + self.x + self.w / 2. + } + + pub fn cy(&self) -> f32 { + self.y + self.h / 2. + } + + pub fn right(&self) -> f32 { + self.x + self.w + } + + pub fn bottom(&self) -> f32 { + self.y + self.h + } + + pub fn outset(&self, by: f32) -> Rect { + Rect::new(self.x - by, self.y - by, self.w + 2. * by, self.h + 2. * by) + } + + pub fn offset(&self, dx: f32, dy: f32) -> Rect { + Rect::new(self.x + dx, self.y + dy, self.w, self.h) + } + + /// The rect of whole pixels this one touches. + pub fn whole_pixels(&self) -> Rect { + let (x, y) = (self.x.floor(), self.y.floor()); + Rect::new(x, y, self.right().ceil() - x, self.bottom().ceil() - y) + } + + /// The overlap of two rects; empty ones come out with a negative size. + pub fn intersect(&self, other: Rect) -> Rect { + let (x, y) = (self.x.max(other.x), self.y.max(other.y)); + Rect::new(x, y, self.right().min(other.right()) - x, self.bottom().min(other.bottom()) - y) + } +} + +/// A CSS colour with straight alpha. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rgba(pub u8, pub u8, pub u8, pub f32); + +impl Rgba { + pub const WHITE: Rgba = Rgba(255, 255, 255, 1.); + pub const BLACK: Rgba = Rgba(0, 0, 0, 1.); + + /// `#rrggbb`. + pub fn hex(hex: &str) -> Rgba { + let channel = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).expect("palette colours are #rrggbb"); + Rgba(channel(1), channel(3), channel(5), 1.) + } + + pub fn alpha(self, alpha: f32) -> Rgba { + Rgba(self.0, self.1, self.2, alpha) + } + + /// The alpha of an 8-digit CSS hex suffix, e.g. `#181818E6` is `hex("#181818").a8(0xE6)`. + pub fn a8(self, alpha: u8) -> Rgba { + self.alpha(f32::from(alpha) / 255.) + } + + pub fn rgb(&self) -> String { + format!("rgb({},{},{})", self.0, self.1, self.2) + } +} + +impl fmt::Display for Rgba { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "rgba({},{},{},{})", self.0, self.1, self.2, self.3) + } +} + +/// A uniform scale followed by a translation, `p -> s * p + (dx, dy)`: every overlay +/// transform (CSS `translate` and `scale` around a `transform-origin`) has this shape. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Similarity { + pub s: f32, + pub dx: f32, + pub dy: f32, +} + +impl Similarity { + pub const IDENTITY: Similarity = Similarity { s: 1., dx: 0., dy: 0. }; + + pub fn translate(dx: f32, dy: f32) -> Self { + Self { s: 1., dx, dy } + } + + /// CSS `scale(s)` around (`cx`, `cy`). + pub fn scale_about(s: f32, cx: f32, cy: f32) -> Self { + Self { s, dx: cx - s * cx, dy: cy - s * cy } + } + + /// `self` followed by `outer`. + pub fn then(self, outer: Similarity) -> Self { + Self { s: self.s * outer.s, dx: self.dx * outer.s + outer.dx, dy: self.dy * outer.s + outer.dy } + } + + pub fn inverse(self) -> Self { + Self { s: 1. / self.s, dx: -self.dx / self.s, dy: -self.dy / self.s } + } + + pub fn apply(self, r: Rect) -> Rect { + Rect::new(r.x * self.s + self.dx, r.y * self.s + self.dy, r.w * self.s, r.h * self.s) + } + + pub fn attr(self) -> String { + format!("matrix({} 0 0 {} {} {})", self.s, self.s, self.dx, self.dy) + } +} + +/// `` with `opacity`, skipping the group entirely once it is invisible. +pub fn faded<'a>(opacity: f32, content: Svgr<'a>) -> Svgr<'a> { + if opacity <= 0. { + return Svgr::empty(); + } + svgr!({content}) +} + +/// `` composited with a CSS `mix-blend-mode` (only accepted through `style`). +pub fn blended<'a>(mode: &str, opacity: f32, content: Svgr<'a>) -> Svgr<'a> { + if opacity <= 0. { + return Svgr::empty(); + } + svgr!({content}) +} + +/// The Factory rotor mark, `ROTOR_VIEWBOX` units wide and tall. +pub const ROTOR_PATH: &str = "M396.406 180.565C395.606 180.367 394.858 179.997 394.213 179.483C393.568 178.968 393.041 178.321 392.671 177.585C392.299 176.848 392.092 176.04 392.061 175.216C392.031 174.391 392.179 173.571 392.496 172.809C403.427 146.207 408.25 124.922 400.466 116.012C379.851 92.371 297.179 139.382 270.819 155.303C270.113 155.728 269.324 155.995 268.505 156.086C267.687 156.178 266.858 156.091 266.076 155.833C265.294 155.575 264.576 155.152 263.973 154.591C263.369 154.031 262.894 153.346 262.579 152.586C251.499 126.04 239.85 107.576 228.044 106.775C196.75 104.634 171.526 196.337 164.142 226.226C163.945 227.026 163.577 227.775 163.064 228.419C162.55 229.065 161.903 229.591 161.167 229.962C160.432 230.334 159.625 230.541 158.801 230.571C157.977 230.602 157.157 230.454 156.396 230.137C129.794 219.206 108.499 214.383 99.5981 222.167C75.9574 242.782 122.96 325.454 138.881 351.814C139.307 352.519 139.575 353.308 139.667 354.128C139.759 354.946 139.674 355.776 139.416 356.558C139.158 357.341 138.733 358.059 138.172 358.662C137.61 359.266 136.925 359.74 136.163 360.053C109.626 371.133 91.1623 382.782 90.3522 394.588C88.22 425.882 179.915 451.107 209.813 458.491C210.612 458.69 211.358 459.059 212.001 459.573C212.644 460.087 213.169 460.735 213.538 461.47C213.909 462.205 214.117 463.011 214.146 463.834C214.177 464.656 214.03 465.477 213.714 466.236C202.783 492.838 197.96 514.132 205.744 523.034C226.359 546.675 309.041 499.672 335.4 483.751C336.106 483.325 336.896 483.057 337.715 482.965C338.534 482.872 339.363 482.958 340.145 483.217C340.928 483.475 341.645 483.899 342.249 484.461C342.852 485.022 343.327 485.708 343.641 486.469C354.721 513.006 366.36 531.471 378.175 532.281C409.47 534.413 434.693 442.718 442.068 412.82C442.267 412.02 442.637 411.272 443.152 410.629C443.666 409.985 444.314 409.46 445.051 409.089C445.787 408.719 446.595 408.512 447.419 408.483C448.243 408.453 449.063 408.601 449.824 408.919C476.425 419.85 497.711 424.663 506.621 416.888C530.262 396.273 483.25 313.591 467.329 287.232C466.906 286.526 466.64 285.736 466.55 284.919C466.459 284.1 466.546 283.272 466.804 282.49C467.062 281.708 467.485 280.991 468.045 280.388C468.604 279.785 469.288 279.308 470.047 278.992C496.593 267.912 515.057 256.263 515.858 244.457C517.999 213.162 426.295 187.94 396.406 180.565ZM360.503 150.564C366.518 161.346 335.521 233.191 312.467 283.443C312.082 284.283 311.449 284.985 310.652 285.454C309.856 285.923 308.934 286.138 308.012 286.068C307.09 285.998 306.211 285.647 305.495 285.063C304.778 284.478 304.258 283.689 304.003 282.8C294.692 250.127 284.05 211.739 272.663 179.15C272.216 177.871 272.238 176.474 272.726 175.21C273.214 173.946 274.136 172.897 275.325 172.25C303.761 156.719 352.421 136.095 360.503 150.564ZM224.226 159.456C236.098 162.827 264.981 235.547 284.208 287.382C284.529 288.247 284.577 289.191 284.346 290.085C284.114 290.979 283.615 291.781 282.915 292.383C282.214 292.986 281.346 293.359 280.427 293.453C279.508 293.548 278.583 293.359 277.774 292.912C248.063 276.422 213.416 256.776 182.317 241.785C181.1 241.194 180.131 240.191 179.584 238.953C179.036 237.715 178.946 236.323 179.329 235.025C188.481 203.964 208.277 154.94 224.226 159.456ZM134.151 262.11C144.924 256.095 216.778 287.093 267.02 310.147C267.861 310.532 268.563 311.166 269.032 311.963C269.501 312.759 269.716 313.681 269.646 314.602C269.575 315.524 269.225 316.402 268.64 317.119C268.056 317.835 267.267 318.357 266.378 318.611C233.714 327.922 195.316 338.564 162.727 349.952C161.45 350.396 160.055 350.373 158.793 349.885C157.531 349.397 156.483 348.477 155.837 347.288C140.334 318.852 119.673 270.192 134.151 262.11ZM143.043 398.388C146.405 386.516 219.133 357.632 270.968 338.405C271.834 338.085 272.778 338.037 273.672 338.268C274.567 338.5 275.368 338.999 275.971 339.699C276.572 340.4 276.947 341.268 277.041 342.186C277.135 343.105 276.946 344.031 276.499 344.84C260 374.552 240.353 409.198 225.362 440.287C224.777 441.509 223.774 442.482 222.535 443.031C221.296 443.581 219.902 443.671 218.603 443.286C187.541 434.189 138.518 414.338 143.043 398.388ZM245.698 488.463C239.673 477.69 270.679 405.837 293.733 355.594C294.119 354.753 294.753 354.051 295.549 353.582C296.346 353.113 297.267 352.899 298.189 352.969C299.111 353.038 299.99 353.389 300.706 353.973C301.422 354.558 301.943 355.348 302.197 356.237C311.508 388.9 322.151 427.299 333.538 459.887C333.982 461.166 333.957 462.56 333.468 463.823C332.979 465.085 332.056 466.132 330.866 466.777C302.439 482.28 253.77 502.941 245.726 488.463H245.698ZM381.974 479.57C370.093 476.209 341.21 403.481 321.983 351.646C321.661 350.779 321.612 349.833 321.843 348.937C322.074 348.041 322.575 347.238 323.277 346.635C323.979 346.032 324.85 345.659 325.77 345.566C326.691 345.473 327.618 345.665 328.426 346.115C358.129 362.605 392.784 382.261 423.874 397.252C425.095 397.839 426.065 398.842 426.613 400.081C427.161 401.32 427.249 402.713 426.863 404.012C417.719 435.12 397.924 484.095 381.974 479.57ZM472.049 376.916C461.267 382.94 389.423 351.934 339.171 328.88C338.331 328.494 337.628 327.861 337.159 327.064C336.69 326.268 336.477 325.346 336.547 324.425C336.616 323.503 336.966 322.625 337.551 321.908C338.135 321.191 338.925 320.671 339.814 320.417C372.486 311.106 410.876 300.463 443.464 289.075C444.745 288.631 446.141 288.655 447.405 289.145C448.668 289.635 449.717 290.558 450.364 291.748C465.857 320.175 486.519 368.843 472.049 376.916ZM463.157 240.639C459.787 252.52 387.067 281.404 335.233 300.631C334.365 300.953 333.42 301.002 332.524 300.771C331.628 300.539 330.825 300.039 330.222 299.337C329.619 298.635 329.247 297.764 329.154 296.843C329.06 295.923 329.251 294.996 329.702 294.187C346.192 264.485 365.838 229.829 380.829 198.739C381.418 197.521 382.421 196.551 383.66 196.004C384.898 195.456 386.291 195.366 387.589 195.751C418.65 204.894 467.673 224.689 463.157 240.639Z"; +pub const ROTOR_VIEWBOX: (f32, f32) = (613., 650.); + +/// The rotor mark fitted into a `size`-px square at (`x`, `y`), like `` with the +/// default `xMidYMid meet`. +pub fn rotor<'a>(x: f32, y: f32, size: f32, fill: &'a str) -> Svgr<'a> { + let (vw, vh) = ROTOR_VIEWBOX; + let s = size / vw.max(vh); + let place = Similarity { s, dx: x + (size - vw * s) / 2., dy: y + (size - vh * s) / 2. }; + svgr!() +} + +/// Path data of a rounded rectangle, for even-odd cutouts. +pub fn rounded_rect_d(r: Rect, radius: f32) -> String { + let k = radius.min(r.w / 2.).min(r.h / 2.); + format!( + "M{} {}H{}A{k} {k} 0 0 1 {} {}V{}A{k} {k} 0 0 1 {} {}H{}A{k} {k} 0 0 1 {} {}V{}A{k} {k} 0 0 1 {} {}Z", + r.x + k, + r.y, + r.right() - k, + r.right(), + r.y + k, + r.bottom() - k, + r.right() - k, + r.bottom(), + r.x + k, + r.x, + r.bottom() - k, + r.y + k, + r.x + k, + r.y, + ) +} + +/// A Gaussian blur filter over `region` (user space), padded so the blur is not cut off. +/// `sigma` is the CSS `blur()` value; CSS shadows blur by half their radius. +pub fn blur<'a>(id: String, sigma: f32, region: Rect) -> Svgr<'a> { + let region = region.outset(3. * sigma); + svgr!( + + + + ) +} + +/// A CSS shadow without offset (`text-shadow: 0 0 <2 * sigma>px`): `shape`, already drawn in +/// the shadow colour, blurred over `area`. +pub fn glow<'a>(id: String, sigma: f32, area: Rect, shape: Svgr<'a>) -> Svgr<'a> { + let url = format!("url(#{id})"); + svgr!({blur(id, sigma, area)}{shape}) +} + +/// Gradient stops in CSS terms (offsets may fall outside 0..1); SVG clamps offsets, so the +/// list is resampled at both edges to keep the colours a browser would show. +pub fn stops<'a>(css: &[(f32, Rgba)]) -> Svgr<'a> { + // CSS moves a stop placed before an earlier one up to that earlier position. + let css: Vec<(f32, Rgba)> = css + .iter() + .scan(f32::NEG_INFINITY, |max, &(offset, color)| { + *max = max.max(offset); + Some((*max, color)) + }) + .collect(); + let css = css.as_slice(); + let lerp = |a: Rgba, b: Rgba, t: f32| { + let ch = |x: u8, y: u8| (f32::from(x) + (f32::from(y) - f32::from(x)) * t).round() as u8; + Rgba(ch(a.0, b.0), ch(a.1, b.1), ch(a.2, b.2), a.3 + (b.3 - a.3) * t) + }; + let at = |offset: f32| { + let next = css.iter().position(|(o, _)| *o >= offset).unwrap_or(css.len() - 1); + if next == 0 { + return css[0].1; + } + let ((o0, c0), (o1, c1)) = (css[next - 1], css[next]); + if o1 <= o0 { c1 } else { lerp(c0, c1, ((offset - o0) / (o1 - o0)).clamp(0., 1.)) } + }; + let mut resolved = vec![(0., at(0.))]; + resolved.extend(css.iter().copied().filter(|(o, _)| *o > 0. && *o < 1.)); + resolved.push((1., at(1.))); + resolved + .into_iter() + .map(|(offset, color)| svgr!()) + .collect() +} + +/// A CSS `box-shadow` (no spread): offset, blur radius and colour. +#[derive(Debug, Clone, Copy)] +pub struct Shadow { + pub dy: f32, + pub blur: f32, + pub color: Rgba, +} + +impl Shadow { + pub const fn new(dy: f32, blur: f32, color: Rgba) -> Self { + Self { dy, blur, color } + } + + /// The shadow of a box at `rect` with corners of `radius`, inside and out, over the whole + /// pixels it reaches, and those pixels. + pub fn blurred(&self, rect: Rect, radius: f32) -> (Pixels, Rect) { + let shape = rect.offset(0., self.dy); + let sigma = self.blur / 2.; + let drawn = svgr!(); + raster::blurred(drawn, shape.outset(3. * sigma), sigma) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Edge { + Left, + Bottom, +} + +/// A CSS `backdrop-filter: blur()`: the scene behind an element, blurred and shown through it. +#[derive(Clone, Copy)] +pub struct Frost<'s> { + pub backdrop: &'s Backdrop, + pub sigma: f32, +} + +impl Frost<'_> { + /// The blurred scene over `area`, in the coordinates of a box placed by `to_screen`. + pub fn draw<'a>(&self, area: Rect, to_screen: Similarity) -> Svgr<'a> { + self.backdrop + .frosted(self.sigma, to_screen.apply(area)) + .map_or_else(Svgr::empty, |(image, at)| raster::on_pixels(image, at, to_screen)) + } +} + +/// A rounded box with a 1px border, optionally one thicker accent edge, shadows and a frosted +/// backdrop: the shape of the window and of every overlay pill and panel. +pub struct Card<'s> { + pub id: &'s str, + pub rect: Rect, + pub radius: f32, + pub fill: Rgba, + pub border: Rgba, + pub accent: Option<(Edge, f32, Rgba)>, + /// CSS order: the first shadow is drawn on top. + pub shadows: &'s [Shadow], + pub frost: Option>, + /// Where the card is drawn on screen, for the frosted backdrop and the shadows. + pub to_screen: Similarity, + /// The screen's size: nothing is drawn outside it. + pub screen: (f32, f32), +} + +impl Card<'_> { + pub fn draw<'a>(&self, content: Svgr<'a>) -> Svgr<'a> { + let shadows = + self.shadows().map_or_else(Svgr::empty, |(image, at)| raster::on_pixels(image, at, self.to_screen)); + svgr!({shadows}{self.body(content)}) + } + + /// The shadows, resampled to the screen pixels they cover, and those pixels. + pub fn shadows(&self) -> Option<(Arc, Rect)> { + let Card { rect: r, radius, shadows, to_screen, screen, .. } = self; + if shadows.is_empty() { + return None; + } + // Shadows only depend on the card's size, so each set is baked once. + let reach = shadows.iter().map(|s| s.dy.abs() + 1.5 * s.blur).fold(0., f32::max); + let outer = r.outset(reach); + let (image, pixels) = raster::baked(format!("shadows {} {} {radius} {shadows:?}", r.w, r.h), outer, |pixels| { + let mut layer = Pixels::transparent(pixels.w as u32, pixels.h as u32); + for shadow in shadows.iter().rev() { + let (blurred, at) = shadow.blurred(*r, *radius); + let to = ((at.x - pixels.x) as u32, (at.y - pixels.y) as u32); + layer.blend(&blurred.into_image(), to, 1., Blend::Over); + } + // A CSS box-shadow is only painted outside the box. + let d = format!( + "M{} {}H{}V{}H{}Z{}", + outer.x, + outer.y, + outer.right(), + outer.bottom(), + outer.x, + rounded_rect_d(*r, *radius) + ); + let drawn = raster::on_pixels(layer.into_image(), pixels, Similarity::IDENTITY); + raster::rasterize( + svgr!( + + + + + {drawn} + + ), + pixels, + 1., + ) + }); + raster::on_screen(&image, pixels, *to_screen, Rect::new(0., 0., screen.0, screen.1)) + } + + /// The card without its shadows. + pub fn body<'a>(&self, content: Svgr<'a>) -> Svgr<'a> { + let Card { id, rect: r, radius, fill, border, accent, frost, to_screen, .. } = self; + let clip_id = format!("{id}-clip"); + let clip = format!("url(#{clip_id})"); + let frost = frost.map_or_else(Svgr::empty, |frost| frost.draw(*r, *to_screen)); + let accent = match accent { + Some((Edge::Left, width, color)) => { + svgr!() + } + Some((Edge::Bottom, width, color)) => { + svgr!() + } + None => Svgr::empty(), + }; + svgr!( + + + + + + {frost} + + {content} + + {accent} + + + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Anchor { + Start, + Middle, + End, +} + +impl Anchor { + fn attr(self) -> &'static str { + match self { + Anchor::Start => "start", + Anchor::Middle => "middle", + Anchor::End => "end", + } + } +} + +/// A font at a size with CSS `letter-spacing` in em. +#[derive(Debug, Clone, Copy)] +pub struct TextStyle { + pub font: Font, + pub size: f32, + pub letter_spacing: f32, +} + +impl TextStyle { + pub const fn new(font: Font, size: f32) -> Self { + Self { font, size, letter_spacing: 0. } + } + + pub const fn spaced(self, letter_spacing: f32) -> Self { + Self { letter_spacing, ..self } + } + + pub fn width(&self, text: &str) -> f32 { + self.font.width(text, self.size, self.letter_spacing) + } + + pub fn normal_line_height(&self) -> f32 { + self.font.normal_line_height(self.size) + } + + /// Baseline of a line box starting at `top`. + pub fn baseline(&self, top: f32, line_height: f32) -> f32 { + top + self.font.baseline(self.size, line_height) + } + + pub fn draw<'a>(&self, content: String, x: f32, baseline: f32, fill: Rgba, anchor: Anchor) -> Svgr<'a> { + // CSS letter-spacing trails the last glyph too, which shifts centred text left. + let x = match anchor { + Anchor::Start => x, + Anchor::Middle => x - self.letter_spacing * self.size / 2., + Anchor::End => x - self.letter_spacing * self.size, + }; + svgr!( + + {content} + + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_colours_take_css_alpha_suffixes() { + assert_eq!(Rgba::hex("#EE6018").a8(0x80), Rgba(238, 96, 24, 128. / 255.)); + assert_eq!(Rgba::hex("#181818").alpha(0.5).to_string(), "rgba(24,24,24,0.5)"); + } + + #[test] + fn similarity_composes_and_inverts() { + let place = Similarity::scale_about(2., 10., 10.).then(Similarity::translate(5., 0.)); + let r = place.apply(Rect::new(10., 10., 4., 4.)); + assert_eq!(r, Rect::new(15., 10., 8., 8.)); + assert_eq!(place.inverse().apply(r), Rect::new(10., 10., 4., 4.)); + } + + #[test] + fn rounded_rect_radius_is_capped_by_the_short_side() { + let d = rounded_rect_d(Rect::new(0., 0., 10., 4.), 8.); + assert!(d.starts_with("M2 0H8A2 2"), "{d}"); + } +} diff --git a/plugins/droid-control/fframes/src/theme.rs b/plugins/droid-control/fframes/src/theme.rs new file mode 100644 index 0000000..d44efa8 --- /dev/null +++ b/plugins/droid-control/fframes/src/theme.rs @@ -0,0 +1,133 @@ +//! Palettes, window presets and fidelity treatments. + +use crate::props::{Fidelity, Preset}; + +#[derive(Debug)] +pub struct Palette { + pub bg: &'static str, + pub surface: &'static str, + pub accent: &'static str, + pub border: &'static str, + pub text: &'static str, + pub muted: &'static str, + /// The Factory palette carries a warm treatment throughout (glows, grade, shadows). + pub warm: bool, +} + +const FACTORY: Palette = Palette { + bg: "#0a0804", + surface: "#181818", + accent: "#EE6018", + border: "#342F2D", + text: "#f0e8e0", + muted: "#948781", + warm: true, +}; + +const CATPPUCCIN: Palette = Palette { + bg: "#0d1117", + surface: "#181818", + accent: "#89b4fa", + border: "#313244", + text: "#cdd6f4", + muted: "#6c7086", + warm: false, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Bar { + Colorful, + Rings, + None, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BarSide { + Left, + Right, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BgStyle { + Solid, + Gradient, +} + +#[derive(Debug)] +pub struct PresetConfig { + pub bar: Bar, + pub bar_side: BarSide, + pub radius: f32, + pub padding: f32, + pub margin: f32, + pub shadow: bool, + pub bg_style: BgStyle, +} + +const fn preset(radius: f32, padding: f32, margin: f32, shadow: bool, bg_style: BgStyle) -> PresetConfig { + PresetConfig { bar: Bar::Colorful, bar_side: BarSide::Left, radius, padding, margin, shadow, bg_style } +} + +const MACOS: PresetConfig = preset(12., 20., 60., true, BgStyle::Solid); +const MINIMAL: PresetConfig = PresetConfig { bar: Bar::None, ..preset(8., 16., 32., false, BgStyle::Solid) }; +const HERO: PresetConfig = preset(16., 24., 80., true, BgStyle::Gradient); +const PRESENTATION: PresetConfig = preset(12., 24., 48., true, BgStyle::Solid); +const FACTORY_PRESET: PresetConfig = preset(12., 20., 80., true, BgStyle::Solid); +const FACTORY_HERO: PresetConfig = preset(12., 24., 80., true, BgStyle::Gradient); + +impl Preset { + pub fn palette(self) -> &'static Palette { + match self { + Preset::Factory | Preset::FactoryHero => &FACTORY, + _ => &CATPPUCCIN, + } + } + + pub fn config(self) -> &'static PresetConfig { + match self { + Preset::Macos => &MACOS, + Preset::Minimal => &MINIMAL, + Preset::Hero => &HERO, + Preset::Presentation => &PRESENTATION, + Preset::Factory => &FACTORY_PRESET, + Preset::FactoryHero => &FACTORY_HERO, + } + } + + pub fn is_factory(self) -> bool { + matches!(self, Preset::Factory | Preset::FactoryHero) + } +} + +/// Film grain and colour grade strength: lighter where detail must stay readable. +pub struct Treatment { + pub noise_opacity: f32, + pub grade_intensity: f32, +} + +impl Fidelity { + pub fn treatment(self) -> Treatment { + match self { + Fidelity::Compact => Treatment { noise_opacity: 0.03, grade_intensity: 0.04 }, + Fidelity::Standard => Treatment { noise_opacity: 0.02, grade_intensity: 0.025 }, + Fidelity::Inspect => Treatment { noise_opacity: 0.008, grade_intensity: 0.012 }, + } + } +} + +/// x264 settings of the final encode and of `.cast` conversions, and agg's frame-rate cap. +pub struct Encoding { + pub crf: u8, + pub preset: &'static str, + pub cast_fps_cap: u8, +} + +impl Fidelity { + pub fn encoding(self) -> Encoding { + match self { + Fidelity::Compact => Encoding { crf: 21, preset: "medium", cast_fps_cap: 24 }, + Fidelity::Standard => Encoding { crf: 18, preset: "slow", cast_fps_cap: 30 }, + Fidelity::Inspect => Encoding { crf: 14, preset: "slow", cast_fps_cap: 30 }, + } + } +} diff --git a/plugins/droid-control/fframes/src/timing.rs b/plugins/droid-control/fframes/src/timing.rs new file mode 100644 index 0000000..4245905 --- /dev/null +++ b/plugins/droid-control/fframes/src/timing.rs @@ -0,0 +1,89 @@ +//! The showcase timeline: title card, clip content, outro, joined by two transitions. + +pub const FPS: usize = 30; +pub const TITLE_FRAMES: usize = 4 * FPS; +/// 3.5 s. +pub const OUTRO_FRAMES: usize = 7 * FPS / 2; +pub const TRANSITION_FRAMES: usize = 15; + +/// Frames during which the clips actually play: the longest source clip (in source seconds) +/// at `speed`. A video without clips holds its empty content for 10 s. +pub fn content_frames(longest_clip: Option, speed: f64) -> usize { + ((longest_clip.unwrap_or(10.) / speed) * FPS as f64).ceil() as usize +} + +/// Frame ranges of the three segments. The content segment pads the clips with one +/// transition on each side: the title crossfade precedes playback, and the outro crossfade +/// begins after the frame-rounded playback interval and shows held frames. Both transitions +/// overlap that padding, so the total is simply title + clips + outro. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Timeline { + pub content: usize, +} + +impl Timeline { + pub fn content_sequence(&self) -> usize { + self.content + 2 * TRANSITION_FRAMES + } + + pub fn content_start(&self) -> usize { + TITLE_FRAMES - TRANSITION_FRAMES + } + + /// The first clip frame: playback starts once the title crossfade is over. + pub fn clips_start(&self) -> usize { + TITLE_FRAMES + } + + pub fn outro_start(&self) -> usize { + TITLE_FRAMES + self.content + } + + pub fn total(&self) -> usize { + TITLE_FRAMES + self.content + OUTRO_FRAMES + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn content_frames_are_the_longest_source_divided_by_speed() { + assert_eq!(content_frames(Some(5.), 2.), 75); + } + + #[test] + fn content_frames_round_up_partial_frames() { + assert_eq!(content_frames(Some(5.01), 3.), 51); + } + + #[test] + fn videos_without_clips_hold_10s_of_content() { + assert_eq!(content_frames(None, 1.), 300); + } + + #[test] + fn content_sequence_pads_the_clips_by_one_transition_on_each_side() { + let timeline = Timeline { content: content_frames(Some(5.), 2.) }; + assert_eq!(timeline.content_sequence(), 75 + 2 * TRANSITION_FRAMES); + } + + #[test] + fn content_shorter_than_a_transition_still_outlasts_both_transitions() { + let timeline = Timeline { content: content_frames(Some(5.), 20.) }; + assert_eq!(timeline.content, 8); + assert!(timeline.content_sequence() > 2 * TRANSITION_FRAMES); + assert_eq!(timeline.total(), 120 + 8 + 105); + } + + #[test] + fn segments_overlap_by_one_transition_and_total_title_clips_outro() { + let timeline = Timeline { content: content_frames(Some(5.), 2.) }; + let sequence_total = TITLE_FRAMES + timeline.content_sequence() + OUTRO_FRAMES - 2 * TRANSITION_FRAMES; + assert_eq!(timeline.total(), sequence_total); + assert_eq!(timeline.total(), 300); + assert_eq!(timeline.content_start() + TRANSITION_FRAMES, timeline.clips_start()); + assert_eq!(timeline.content_start() + timeline.content_sequence() - TRANSITION_FRAMES, timeline.outro_start()); + } +} diff --git a/plugins/droid-control/fframes/src/title.rs b/plugins/droid-control/fframes/src/title.rs new file mode 100644 index 0000000..3319e48 --- /dev/null +++ b/plugins/droid-control/fframes/src/title.rs @@ -0,0 +1,189 @@ +//! The opening title card: tagline, title and subtitle rise in while the rotor sweeps an +//! accent line under the title (a gradient sweep for cool palettes), all inside a slow +//! breathing zoom. + +use crate::fonts::Font; +use crate::motion::{Bezier, interpolate, interpolate_eased}; +use crate::svg::{Anchor, Rect, Rgba, Similarity, TextStyle, faded, glow, rotor, stops}; +use crate::theme::Palette; +use crate::timing::TITLE_FRAMES; +use fframes::{Svgr, svgr}; + +const TITLE_MAX_SIZE: f32 = 64.; +const SUBTITLE_MAX_SIZE: f32 = 24.; +const SWEEP_WIDTH: f32 = 280.; +const ROTOR_SIZE: f32 = 28.; + +/// Title and subtitle sizes, fitted once to 80% and 70% of the width. +#[derive(Debug, Clone, Copy)] +pub struct TitleLayout { + title: TextStyle, + subtitle: TextStyle, +} + +fn fit(text: &str, style: TextStyle, within: f32, max: f32) -> TextStyle { + let at_100 = TextStyle { size: 100., ..style }.width(text); + let size = if at_100 > 0. { (within * 100. / at_100).min(max) } else { max }; + TextStyle { size, ..style } +} + +impl TitleLayout { + pub fn new(title: &str, subtitle: &str, width: f32) -> Self { + let subtitle = if subtitle.is_empty() { " " } else { subtitle }; + Self { + title: fit(title, TextStyle::new(Font::sans(700), 0.).spaced(-0.04), width * 0.8, TITLE_MAX_SIZE), + subtitle: fit(subtitle, TextStyle::new(Font::sans(400), 0.), width * 0.7, SUBTITLE_MAX_SIZE), + } + } +} + +pub struct TitleCard<'s> { + pub layout: &'s TitleLayout, + pub title: &'s str, + pub subtitle: &'s str, + pub speed_note: &'s str, + pub palette: &'s Palette, + pub size: (f32, f32), +} + +impl TitleCard<'_> { + pub fn draw<'a>(&self, frame: usize) -> Svgr<'a> { + let TitleCard { layout, title, subtitle, speed_note, palette, size: (w, h) } = *self; + let f = frame as f32; + let expo = || Bezier::EXPO_OUT.f(); + let tagline = interpolate_eased(f, &[0., 12.], &[0., 1.], expo()); + let entrance = interpolate_eased(f, &[0., 18.], &[0., 1.], expo()); + let rise = interpolate_eased(f, &[4.5, 22.5], &[0., 1.], expo()); + + let note_style = TextStyle::new(Font::mono(400), 14.).spaced(0.08); + let note = speed_note.to_uppercase(); + let note_height = if note.is_empty() { 0. } else { note_style.normal_line_height() + 16. }; + let title_height = layout.title.normal_line_height(); + let accent_height = if palette.warm { 16. + ROTOR_SIZE + 8. + 16. } else { 20. + 2. + 20. }; + let subtitle_height = if subtitle.is_empty() { 0. } else { layout.subtitle.normal_line_height() }; + let top = (h - note_height - title_height - accent_height - subtitle_height) / 2.; + let title_top = top + note_height; + let accent_top = title_top + title_height; + let subtitle_top = accent_top + accent_height; + let cx = w / 2.; + + let note = if note.is_empty() { + Svgr::empty() + } else { + let text = note_style.draw( + note, + cx, + note_style.baseline(top, note_style.normal_line_height()), + Rgba::hex(palette.muted), + Anchor::Middle, + ); + faded(tagline, svgr!({text})) + }; + + let accent = Rgba::hex(palette.accent); + let title_baseline = layout.title.baseline(title_top, title_height); + let title_text = |fill: Rgba| layout.title.draw(title.to_owned(), cx, title_baseline, fill, Anchor::Middle); + let title_area = + Rect::new(cx - layout.title.width(title) / 2., title_top, layout.title.width(title), title_height); + let place_title = Similarity::scale_about(1.08 - 0.08 * entrance, cx, title_top + title_height / 2.) + .then(Similarity::translate(0., 30. * (1. - entrance))); + let title = faded( + entrance, + svgr!( + + {glow("title-glow-far".to_owned(), 20., title_area, title_text(accent.a8(0x1a)))} + {glow("title-glow-near".to_owned(), 10., title_area, title_text(accent.a8(0x4d)))} + {title_text(Rgba::hex(palette.text))} + + ), + ); + + let accent_line = if palette.warm { + self.rotor_sweep(f, cx - SWEEP_WIDTH / 2., accent_top + 16. + (ROTOR_SIZE + 8.) / 2.) + } else { + Self::gradient_sweep(f, palette, cx, accent_top + 20.) + }; + + let subtitle = if subtitle.is_empty() { + Svgr::empty() + } else { + let baseline = layout.subtitle.baseline(subtitle_top, subtitle_height); + let text = + layout.subtitle.draw(subtitle.to_owned(), cx, baseline, Rgba::hex(palette.muted), Anchor::Middle); + faded(rise, svgr!({text})) + }; + + let breathing = 1. + 0.04 * Bezier(0.45, 0., 0.55, 1.).ease(f / TITLE_FRAMES as f32); + svgr!( + + {note} + {title} + {accent_line} + {subtitle} + + ) + } + + /// The spinning rotor rolls right while a gradient trail grows behind it. + fn rotor_sweep<'a>(&self, f: f32, left: f32, cy: f32) -> Svgr<'a> { + let (start, end) = (4.5, 21.); + let progress = interpolate_eased(f, &[start, end], &[0., 1.], Bezier::EXPO_OUT.f()); + let x = progress * SWEEP_WIDTH; + let rotor_opacity = interpolate(f, &[start, start + 2.4, end, end + 7.5], &[0., 1., 1., 0.]); + let trail_opacity = interpolate(f, &[start, start + 3.], &[0., 1.]); + let accent = Rgba::hex(self.palette.accent); + + let trail = if x > 0. { + faded( + trail_opacity, + svgr!( + + + {stops(&[(0., accent.alpha(0.)), (0.3, accent.a8(0x66)), (1., accent)])} + + + + ), + ) + } else { + Svgr::empty() + }; + + let (rx, ry) = (left + x - ROTOR_SIZE / 2., cy - ROTOR_SIZE / 2.); + let region = Rect::new(rx, ry, ROTOR_SIZE, ROTOR_SIZE).outset(12.); + let spin = format!("rotate({} {} {})", 720. * progress, left + x, cy); + let rotor = faded( + rotor_opacity, + svgr!( + + + + + {rotor(rx, ry, ROTOR_SIZE, "white")} + + ), + ); + svgr!({trail}{rotor}) + } + + /// A short accent line widens while a highlight sweeps through it. + fn gradient_sweep<'a>(f: f32, palette: &Palette, cx: f32, top: f32) -> Svgr<'a> { + let width = interpolate_eased(f, &[6., 21.], &[0., 120.], Bezier::EXPO_OUT.f()); + if width <= 0. { + return Svgr::empty(); + } + let position = interpolate_eased(f, &[6., 30.], &[-200., 100.], Bezier::EXPO_OUT.f()); + let accent = Rgba::hex(palette.accent); + let left = cx - width / 2.; + // `background-size: 200%` positioned at `position`%, repeating. + let tile = left - width * position / 100.; + svgr!( + + + {stops(&[(0., accent.alpha(0.)), (0.25, accent), (0.5, Rgba::WHITE), (0.75, accent), (1., accent.alpha(0.))])} + + + + ) + } +} diff --git a/plugins/droid-control/fframes/src/transition.rs b/plugins/droid-control/fframes/src/transition.rs new file mode 100644 index 0000000..050c5fb --- /dev/null +++ b/plugins/droid-control/fframes/src/transition.rs @@ -0,0 +1,249 @@ +//! The two scene transitions (title -> content, content -> outro). Each style restyles both +//! the exiting and the entering scene from the same 0..1 progress; the entering scene is +//! drawn on top. +//! +//! Style names follow @hyperframes/shader-transitions (Apache-2.0); the looks are drawn +//! here with SVG filters and blend modes. + +use crate::motion::{Bezier, interpolate, seeded_random}; +use crate::props::TransitionStyle; +use crate::svg::{Rect, Rgba, Similarity, blended, blur, faded, stops}; +use fframes::{Svgr, svgr}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Entering, + Exiting, +} + +pub struct Transition { + pub style: TransitionStyle, + pub warm: bool, + pub size: (f32, f32), +} + +impl Transition { + /// `scene` restyled at `progress` (0..1) of the transition. + pub fn present<'a>(&self, direction: Direction, progress: f32, scene: Svgr<'a>) -> Svgr<'a> { + let p = progress; + let entering = direction == Direction::Entering; + let id = if entering { "enter" } else { "exit" }; + match self.style { + TransitionStyle::MotionBlur => { + if entering { + let (w, h) = self.size; + let place = Similarity::scale_about(1.03 - 0.03 * p, w / 2., h / 2.); + faded(p, svgr!({self.blurred(id, 6. * (1. - p), scene)})) + } else { + faded(1. - p, self.blurred(id, 6. * p, scene)) + } + } + TransitionStyle::Flash => { + if entering { + let flash = interpolate(p, &[0., 0.35, 0.55, 0.85, 1.], &[0., 0.25, 1., 0.2, 0.]); + let color = if self.warm { Rgba(255, 220, 180, 1.) } else { Rgba(235, 240, 255, 1.) }; + let (w, h) = self.size; + svgr!( + + {faded(((p - 0.45) / 0.35).clamp(0., 1.), scene)} + {blended("screen", flash, svgr!())} + + ) + } else { + faded((1. - (p - 0.2) / 0.35).clamp(0., 1.), scene) + } + } + TransitionStyle::WhipPan => { + let eased = Bezier(0.6, 0., 0.1, 1.).ease(p); + let (shift, sigma, opacity) = if entering { + ( + 0.3 * (1. - eased), + interpolate(p, &[0., 0.6, 1.], &[14., 4., 0.]), + interpolate(p, &[0., 0.2, 1.], &[0., 1., 1.]), + ) + } else { + ( + -0.3 * eased, + interpolate(p, &[0., 0.4, 1.], &[0., 10., 14.]), + interpolate(p, &[0., 0.8, 1.], &[1., 1., 0.]), + ) + }; + let place = Similarity::translate(shift * self.size.0, 0.); + faded(opacity, svgr!({self.blurred(id, sigma, scene)})) + } + TransitionStyle::LightLeak => { + if entering { + let scene_opacity = interpolate(p, &[0., 0.5, 1.], &[0., 0.4, 1.]); + let sweep = interpolate(p, &[0., 0.3, 0.6, 1.], &[0., 1., 0.6, 0.]); + svgr!({faded(scene_opacity, scene)}{blended("screen", sweep, self.light_leak(p))}) + } else { + faded(interpolate(p, &[0., 0.5, 1.], &[1., 0.6, 0.]), scene) + } + } + TransitionStyle::GlitchLite => self.glitch(entering, p, scene), + } + } + + /// CSS `filter: blur(sigma)` over the whole frame. + fn blurred<'a>(&self, id: &str, sigma: f32, scene: Svgr<'a>) -> Svgr<'a> { + if sigma <= 0.01 { + return scene; + } + let (w, h) = self.size; + let filter = format!("transition-{id}-blur"); + let url = format!("url(#{filter})"); + svgr!({blur(filter, sigma, Rect::new(0., 0., w, h))}{scene}) + } + + /// A warm (or cool) highlight band at 105 degrees, sweeping left to right. + fn light_leak<'a>(&self, p: f32) -> Svgr<'a> { + let (w, h) = self.size; + let leak = if self.warm { Rgba(238, 96, 24, 0.) } else { Rgba(137, 180, 250, 0.) }; + let at = -0.4 + 1.8 * p; + // CSS gradient line for 105deg: through the centre, long enough to reach the corners. + let angle = 105f32.to_radians(); + let (dx, dy) = (angle.sin(), -angle.cos()); + let half = (w * dx.abs() + h * dy.abs()) / 2.; + let (cx, cy) = (w / 2., h / 2.); + svgr!( + + + {stops(&[ + (0., leak), + (at - 0.3, leak), + (at, leak.alpha(0.45)), + (at + 0.12, Rgba(255, 240, 200, 0.35)), + (at + 0.4, leak), + (1., leak), + ])} + + + + ) + } + + /// RGB-split ghosts and a displaced horizontal band around the midpoint. + fn glitch<'a>(&self, entering: bool, p: f32, scene: Svgr<'a>) -> Svgr<'a> { + let (w, h) = self.size; + let scene_opacity = if entering { + interpolate(p, &[0., 0.4, 1.], &[0., 1., 1.]) + } else { + interpolate(p, &[0., 0.6, 1.], &[1., 1., 0.]) + }; + let intensity = interpolate(p, &[0., 0.35, 0.55, 0.75, 1.], &[0., 1., 1., 0.3, 0.]); + let seed = if entering { 1000 } else { 2000 }; + let random = + |name: &str, steps: f32| seeded_random(&format!("glitch-{name}-{}", (p * steps).round() as i32 + seed)); + let jitter = (random("x", 60.) - 0.5) * 16. * intensity; + let split = 6. * intensity; + let band_top = 20. + 40. * random("band-top", 30.); + let band_height = 6. + 14. * random("band-h", 30.); + let id = if entering { "enter" } else { "exit" }; + let shifted = + |dx: f32, content: Svgr<'a>| svgr!({content}); + + let ghost = |name: &str, hue: f32, dx: f32| { + if split <= 0. { + return Svgr::empty(); + } + let filter = format!("glitch-{id}-{name}"); + let url = format!("url(#{filter})"); + let tint = tint_matrix(hue); + blended( + "screen", + 0.45, + shifted( + dx, + svgr!( + + + + + + + + {scene.clone()} + + ), + ), + ) + }; + let band = if intensity > 0.6 { + let clip = format!("glitch-{id}-band"); + let url = format!("url(#{clip})"); + faded( + 0.85, + shifted( + jitter * 2.2, + svgr!( + + + + + {scene.clone()} + + ), + ), + ) + } else { + Svgr::empty() + }; + faded( + scene_opacity, + svgr!( + + {ghost("red", -50., jitter - split)} + {ghost("blue", 180., jitter + split)} + {shifted(jitter, scene.clone())} + {band} + + ), + ) + } +} + +/// `sepia(1) saturate(6) hue-rotate(hue) contrast(1.1)` as four colour matrices, applied in +/// turn so every step clamps like the CSS filter chain. +fn tint_matrix(hue: f32) -> [String; 4] { + let (sin, cos) = hue.to_radians().sin_cos(); + let s = 6.; + let rows = |m: [[f32; 3]; 3], offset: f32| { + m.iter() + .map(|row| format!("{} {} {} 0 {offset}", row[0], row[1], row[2])) + .chain(["0 0 0 1 0".to_owned()]) + .collect::>() + .join(" ") + }; + [ + rows([[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]], 0.), + rows( + [ + [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s], + [0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s], + [0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s], + ], + 0., + ), + rows( + [ + [ + 0.213 + cos * 0.787 - sin * 0.213, + 0.715 - cos * 0.715 - sin * 0.715, + 0.072 - cos * 0.072 + sin * 0.928, + ], + [ + 0.213 - cos * 0.213 + sin * 0.143, + 0.715 + cos * 0.285 + sin * 0.140, + 0.072 - cos * 0.072 - sin * 0.283, + ], + [ + 0.213 - cos * 0.213 - sin * 0.787, + 0.715 - cos * 0.715 + sin * 0.715, + 0.072 + cos * 0.928 + sin * 0.072, + ], + ], + 0., + ), + rows([[1.1, 0., 0.], [0., 1.1, 0.], [0., 0., 1.1]], -0.05), + ] +} diff --git a/plugins/droid-control/remotion/package-lock.json b/plugins/droid-control/remotion/package-lock.json deleted file mode 100644 index acb1704..0000000 --- a/plugins/droid-control/remotion/package-lock.json +++ /dev/null @@ -1,5526 +0,0 @@ -{ - "name": "droid-control-remotion", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "droid-control-remotion", - "version": "1.0.0", - "dependencies": { - "@remotion/cli": "4.0.445", - "@remotion/layout-utils": "4.0.445", - "@remotion/light-leaks": "4.0.445", - "@remotion/media": "4.0.445", - "@remotion/tailwind": "4.0.445", - "@remotion/transitions": "4.0.445", - "prism-react-renderer": "^2.4.1", - "react": "^19", - "react-dom": "^19", - "remotion": "4.0.445", - "zod": "4.3.6" - }, - "devDependencies": { - "@types/react": "^19", - "tailwindcss": "^4", - "typescript": "^5.7" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", - "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", - "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.2.1.tgz", - "integrity": "sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.2.4.tgz", - "integrity": "sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.6.3.tgz", - "integrity": "sha512-pQPUPo32HW3/NuZxrwr3VJHE+vGqSTVI5gK4jGbuJ7eOFUrsTmZikXcVdInCVWOvuxK5xbCzwDWoTlZUCAKN+A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^4.1.0", - "@csstools/css-calc": "^1.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.6.1", - "@csstools/css-tokenizer": "^2.2.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", - "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^2.4.1" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", - "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - } - }, - "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", - "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1" - } - }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-3.0.1.tgz", - "integrity": "sha512-dD8W98dOYNOH/yX4V4HXOhfCOnvVAg8TtsL+qCGNoKXuq5z2C/d026wGWgySgC8cajXXo/wNezS31Glj5GcqrA==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.2", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-2.2.3.tgz", - "integrity": "sha512-b1ptNkr1UWP96EEHqKBWWaV5m/0hgYGctgA/RVZhONeP1L3T/8hwoqDm9bB23yVCfOgE9U93KI9j06+pEkJTvw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-mix-function": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-1.0.3.tgz", - "integrity": "sha512-QGXjGugTluqFZWzVf+S3wCiRiI0ukXlYqCi7OnpDotP/zaVTyl/aqZujLFzTOXy24BoWnu89frGMc79ohY5eog==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-2.0.2.tgz", - "integrity": "sha512-iKYZlIs6JsNT7NKyRjyIyezTCHLh4L4BBB3F5Nx7Dc4Z/QmBgX+YJFuUSar8IM6KclGiAUFGomXFdYxAwJydlA==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-3.0.6.tgz", - "integrity": "sha512-rBOBTat/YMmB0G8VHwKqDEx+RZ4KCU9j42K8LwS0IpZnyThalZZF7BCSsZ6TFlZhcRZKlZy3LLFI2pLqjNVGGA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-2.2.2.tgz", - "integrity": "sha512-W5Y5oaJ382HSlbdGfPf60d7dAK6Hqf10+Be1yZbd/TNNrQ/3dDdV1c07YwOXPQ3PZ6dvFMhxbIbn8EC3ki3nEg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-2.0.4.tgz", - "integrity": "sha512-9W2ZbV7whWnr1Gt4qYgxMWzbevZMOvclUczT5vk4yR6vS53W/njiiUhtm/jh/BKYwQ1W3PECZjgAd2dH4ebJig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^2.3.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-3.2.1.tgz", - "integrity": "sha512-AtANdV34kJl04Al62is3eQRk/BfOfyAvEmRJvbt+nx5REqImLC+2XhuE6skgkcPli1l8ONS67wS+l1sBzySc3Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-float-and-clear": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-1.0.1.tgz", - "integrity": "sha512-eO9z2sMLddvlfFEW5Fxbjyd03zaO7cJafDurK4rCqyRt9P7aaWwha0LcSzoROlcZrw1NBV2JAp2vMKfPMQO1xw==", - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-resize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-1.0.1.tgz", - "integrity": "sha512-x1ge74eCSvpBkDDWppl+7FuD2dL68WP+wwP2qvdUcKY17vJksz+XoE1ZRV38uJgS6FNUwC0AxrPW5gy3MxsDHQ==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-1.0.3.tgz", - "integrity": "sha512-6zqcyRg9HSqIHIPMYdt6THWhRmE5/tyHKJQLysn2TeDf/ftq7Em9qwMTx98t2C/7UxIsYS8lOiHHxAVjWn2WUg==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-tokenizer": "^2.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-media-minmax": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.8.tgz", - "integrity": "sha512-KYQCal2i7XPNtHAUxCECdrC7tuxIWQCW+s8eMYs5r5PaAiVTeKwlrkRS096PFgojdNCmHeG0Cb7njtuNswNf+w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^1.2.4", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/media-query-list-parser": "^2.1.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-1.0.4.tgz", - "integrity": "sha512-IwyTbyR8E2y3kh6Fhrs251KjKBJeUPV5GlnUKnpU70PRFEN2DolWbf2V4+o/B9+Oj77P/DullLTulWEQ8uFtAA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-parser-algorithms": "^2.2.0", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/media-query-list-parser": "^2.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-2.0.2.tgz", - "integrity": "sha512-jbwrP8rN4e7LNaRcpx3xpMUjhtt34I9OV+zgbcsYAAk6k1+3kODXJBf95/JMYWhu9g1oif7r06QVUgfWsKxCFw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-2.0.1.tgz", - "integrity": "sha512-TQT5g3JQ5gPXC239YuRK8jFceXF9d25ZvBkyjzBGGoW5st5sPXFVQS8OjYb9IJ/K3CdfK4528y483cgS2DJR/w==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-2.2.3.tgz", - "integrity": "sha512-AgJ2rWMnLCDcbSMTHSqBYn66DNLBym6JpBpCaqmwZ9huGdljjDRuH3DzOYzkgQ7Pm2K92IYIq54IvFHloUOdvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-2.3.0.tgz", - "integrity": "sha512-Zd8ojyMlsL919TBExQ1I0CTpBDdyCpH/yOdqatZpuC3sd22K4SwC7+Yez3Q/vmXMWSAl+shjNeFZ7JMyxMjK+Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-1.0.2.tgz", - "integrity": "sha512-juCoVInkgH2TZPfOhyx6tIal7jW37L/0Tt+Vcl1LoxqQA9sxcg3JWYZ98pl1BonDnki6s/M7nXzFQHWsWMeHgw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-scope-pseudo-class": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-2.0.2.tgz", - "integrity": "sha512-6Pvo4uexUCXt+Hz5iUtemQAcIuCYnL+ePs1khFR6/xPgC92aQLJ0zGHonWoewiBE+I++4gXK3pr+R1rlOFHe5w==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-2.1.1.tgz", - "integrity": "sha512-YCvdF0GCZK35nhLgs7ippcxDlRVe5QsSht3+EghqTjnYnyl3BbWIN6fYQ1dKWYTJ+7Bgi41TgqQFfJDcp9Xy/w==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-calc": "^1.1.1", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-2.2.4.tgz", - "integrity": "sha512-zPN56sQkS/7YTCVZhOBVCWf7AiNge8fXDl7JVaHLz2RyT4pnyK2gFjckWRLpO0A2xkm1lCgZ0bepYZTwAVd/5A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/color-helpers": "^2.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand/node_modules/@csstools/color-helpers": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-2.1.0.tgz", - "integrity": "sha512-OWkqBa7PDzZuJ3Ha7T5bxdSVfSCfTq6K1mbAhbO1MD+GSULGjrp45i5RudyJOedstSarN/3mdwu9upJE7gDXfw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-2.1.1.tgz", - "integrity": "sha512-XcXmHEFfHXhvYz40FtDlA4Fp4NQln2bWTsCwthd2c+MCnYArUYU3YaMqzR5CrKP3pMoGYTBnp5fMqf1HxItNyw==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-calc": "^1.1.1", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-2.0.1.tgz", - "integrity": "sha512-oJ9Xl29/yU8U7/pnMJRqAZd4YXNCfGEdcP4ywREuqm/xMqcgDNDppYRoCGDt40aaZQIEKBS79LytUDN/DHf0Ew==", - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.10" - } - }, - "node_modules/@csstools/utilities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-1.0.0.tgz", - "integrity": "sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mediabunny/aac-encoder": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.39.2.tgz", - "integrity": "sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA==", - "license": "MPL-2.0", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - }, - "peerDependencies": { - "mediabunny": "^1.0.0" - } - }, - "node_modules/@mediabunny/flac-encoder": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.39.2.tgz", - "integrity": "sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ==", - "license": "MPL-2.0", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - }, - "peerDependencies": { - "mediabunny": "^1.0.0" - } - }, - "node_modules/@mediabunny/mp3-encoder": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.39.2.tgz", - "integrity": "sha512-3rrodrGnUpUP8F2d1aRUl8IvjqK3jegkupbOzvOokooSAO5rXk2Lr5jZe7TnPeiVGiXfmnoJ7s9uyUOHlCd8qw==", - "license": "MPL-2.0", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - }, - "peerDependencies": { - "mediabunny": "^1.0.0" - } - }, - "node_modules/@module-federation/error-codes": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", - "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", - "license": "MIT" - }, - "node_modules/@module-federation/runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", - "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", - "license": "MIT", - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/runtime-core": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@module-federation/runtime-core": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", - "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", - "license": "MIT", - "dependencies": { - "@module-federation/error-codes": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@module-federation/runtime-tools": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", - "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/webpack-bundler-runtime": "0.22.0" - } - }, - "node_modules/@module-federation/sdk": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", - "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", - "license": "MIT" - }, - "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", - "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", - "license": "MIT", - "dependencies": { - "@module-federation/runtime": "0.22.0", - "@module-federation/sdk": "0.22.0" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@remotion/bundler": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.445.tgz", - "integrity": "sha512-jA5VZfFeHv1gVfwdQAWUtKDDc7axVoB/wgCcmnlVh8jCKUZMJnUWewH5ac7FDEbZ/7zKie71naR8Ad0EgD4Hew==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@remotion/media-parser": "4.0.445", - "@remotion/studio": "4.0.445", - "@remotion/studio-shared": "4.0.445", - "@rspack/core": "1.7.6", - "@rspack/plugin-react-refresh": "1.6.1", - "esbuild": "0.25.0", - "loader-utils": "2.0.4", - "postcss": "8.5.1", - "postcss-value-parser": "4.2.0", - "react-refresh": "0.18.0", - "remotion": "4.0.445", - "source-map": "0.7.3", - "style-loader": "4.0.0", - "webpack": "5.105.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/cli": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.445.tgz", - "integrity": "sha512-lfAcT6tLSFmvdR1I4Re40Ym6JGMh/1muApJsn9blOkqJevAZr4lqK/Fjqf4pbcm2B4QslX6Uu5jWGoVJgli+fg==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@remotion/bundler": "4.0.445", - "@remotion/media-utils": "4.0.445", - "@remotion/player": "4.0.445", - "@remotion/renderer": "4.0.445", - "@remotion/studio": "4.0.445", - "@remotion/studio-server": "4.0.445", - "@remotion/studio-shared": "4.0.445", - "dotenv": "17.3.1", - "minimist": "1.2.6", - "prompts": "2.4.2", - "remotion": "4.0.445" - }, - "bin": { - "remotion": "remotion-cli.js", - "remotionb": "remotionb-cli.js", - "remotiond": "remotiond-cli.js" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/compositor-darwin-arm64": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.445.tgz", - "integrity": "sha512-yB2UNMb56l3grn1SpkHu4RMrmfXvC0j1hebgxUQYnp180AdhUq2oBHRArlm8O7HxctS7q0z2i4AYPh9oEkZkOw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@remotion/compositor-darwin-x64": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.445.tgz", - "integrity": "sha512-YFyuhS1wcKaIRptizZRjdcIHHg+MguHJGG4dqeSTSdxhYXky0+df9IuF/FFCN4ScWTbcn1KoaG/WdwglSId6/A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@remotion/compositor-linux-arm64-gnu": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.445.tgz", - "integrity": "sha512-OoZXonYdLI9t+LJAo3z3HlZ6ujrHKrS5sFw4snBg8mNIcjfblplDnydLXhV1PemNlVtAkU2ZUE0H5abqAQKoaQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-linux-arm64-musl": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.445.tgz", - "integrity": "sha512-fkqdErDMVbJdLlcfDh8LmEMZMfJAavxEOoVelLWkmDhNJnGvbPbDlwkITxg5H4U1ZBCjEF20lUAbLsIs7AD79Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-linux-x64-gnu": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.445.tgz", - "integrity": "sha512-JgcEWYsrIdVNkHYQsrBamrooXFPZRung7zPqMWIWmfM8TVXz2Ncid5JW8qMK+eRl/dQaBXw4V9WYaBfIj17JDA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-linux-x64-musl": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.445.tgz", - "integrity": "sha512-AAmWGt9TP2CrGRossBNBSerKMW8KIOpxSKTBjpaza5Yjjd+3S4XVhNlBnsTD/1cxw9HaQJ1um9detKsVev0Weg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@remotion/compositor-win32-x64-msvc": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.445.tgz", - "integrity": "sha512-qNa86D0j8kjFhU+t21/2IpyEsy1YIVHs9HkscxDOzN+SoW+9m3esWc4VSXnBZsu2PMq8zplYb79roQZGKk8BFw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@remotion/layout-utils": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/layout-utils/-/layout-utils-4.0.445.tgz", - "integrity": "sha512-VneNiJy6LkqO1VYyya+p+rt1lUfl88TpKz/lS84je6mUw/7++gzfhrVdWycFoQSralJxoft9885cU+hR9huwTw==", - "license": "MIT" - }, - "node_modules/@remotion/licensing": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.445.tgz", - "integrity": "sha512-IbRTYGNzw0CCqJpxpdMVYPDKU15gJKK3DEjxE9nU4UXJHCEaGglxU6KvnDK3ykjHawuMmND+gSSHpj++89l+Sg==", - "license": "MIT" - }, - "node_modules/@remotion/light-leaks": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/light-leaks/-/light-leaks-4.0.445.tgz", - "integrity": "sha512-9UIYrNP2uV9rtU/LeKIdXw5QIz7YzCf64cG6WH5PaKOHnMzHw8HfGlYwVS9s3+ruUSj2wnQPd2ZU8dZPYHiPVw==", - "license": "Remotion License", - "dependencies": { - "remotion": "4.0.445", - "zod": "4.3.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/media": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/media/-/media-4.0.445.tgz", - "integrity": "sha512-c/G6LP20nECovxzcuMb+80n6+Qea1APvC2Ewp8Mpr07IjAKVv9+FC+H4/omxhC/wXRXchwigEUV/Y964z0QCQg==", - "dependencies": { - "mediabunny": "1.39.2", - "remotion": "4.0.445", - "zod": "4.3.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/media-parser": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.445.tgz", - "integrity": "sha512-lKQnttWt2XCMQfr8eralZihnPAYDiJDG3uhhBfI13f4OHmKDSxjijP11w1k6JTbhIlER1Ijm/ZfSbQ3UjWQYgA==", - "license": "Remotion License https://remotion.dev/license" - }, - "node_modules/@remotion/media-utils": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.445.tgz", - "integrity": "sha512-pH7bM3Uq19I7zm0TR3kpRUfNh/jtBZjjLfV395kzO+ccdN1VQhfe5C/btQ/G0GIgwhP0mdm9jm8TZw2CTGQuSg==", - "license": "MIT", - "dependencies": { - "mediabunny": "1.39.2", - "remotion": "4.0.445" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/paths": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/paths/-/paths-4.0.445.tgz", - "integrity": "sha512-NRDlT0yJ4bgOLHjGFTENK3AOzAEhj06Tla3b6c0F9KtyrCGTHehmu2EE+FfA4tJl3VzH6j0xfQEhUZc/A62+JQ==", - "license": "MIT" - }, - "node_modules/@remotion/player": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.445.tgz", - "integrity": "sha512-AYhu6LXrL66FNOg5Md5DNF/BH0TQg7C26lESZozukVnSH6dmgJQgwtEcCH7l3hwDz+IJhZPn6Lbwo+2rpr2M+A==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "remotion": "4.0.445" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/renderer": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.445.tgz", - "integrity": "sha512-RejUK9zQxNt1h6b5gNHRGDIjFnun3ZQ6yyGE8PBnSXyU8ONMKLDq+2iDsE3CRftWdld3uNtWrVAhSabW9QRmqA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@remotion/licensing": "4.0.445", - "@remotion/streaming": "4.0.445", - "execa": "5.1.1", - "extract-zip": "2.0.1", - "remotion": "4.0.445", - "source-map": "^0.8.0-beta.0", - "ws": "8.17.1" - }, - "optionalDependencies": { - "@remotion/compositor-darwin-arm64": "4.0.445", - "@remotion/compositor-darwin-x64": "4.0.445", - "@remotion/compositor-linux-arm64-gnu": "4.0.445", - "@remotion/compositor-linux-arm64-musl": "4.0.445", - "@remotion/compositor-linux-x64-gnu": "4.0.445", - "@remotion/compositor-linux-x64-musl": "4.0.445", - "@remotion/compositor-win32-x64-msvc": "4.0.445" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/renderer/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "deprecated": "The work that was done in this beta branch won't be included in future versions", - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@remotion/shapes": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/shapes/-/shapes-4.0.445.tgz", - "integrity": "sha512-tPBHyoy8Fl3fHRDekeFJ7l6KG8H34KOAVexLx42AdhxNnMMKq2dqP1Mosqjw9mTdicKvZvrIbojZPQIbUhkb7g==", - "license": "MIT", - "dependencies": { - "@remotion/paths": "4.0.445" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/streaming": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.445.tgz", - "integrity": "sha512-NCy1XYSEGbPb/xkyt6WdFIlEawnYCFcprKHFCrZfAY7Pu7jgNuMDfgXDgmmQe3gvyuS6NxCEaJphZ6VToLoKKA==", - "license": "MIT" - }, - "node_modules/@remotion/studio": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.445.tgz", - "integrity": "sha512-wiGNDZHcvVbqyhlMCVypWqsd1l6JSiyc+7kcfBtSAABz1X4bWCmSuIC4K3s6CsOlhFzUjZJFtz3b3fi1a1Gzaw==", - "license": "MIT", - "dependencies": { - "@remotion/media-utils": "4.0.445", - "@remotion/player": "4.0.445", - "@remotion/renderer": "4.0.445", - "@remotion/studio-shared": "4.0.445", - "@remotion/web-renderer": "4.0.445", - "@remotion/zod-types": "4.0.445", - "mediabunny": "1.39.2", - "memfs": "3.4.3", - "open": "^8.4.2", - "remotion": "4.0.445", - "semver": "7.5.3", - "source-map": "0.7.3", - "zod": "4.3.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/studio-server": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.445.tgz", - "integrity": "sha512-fiSNJf0MroDILWLL611HAOiEXxAISe0g1Wk/c+guFixcPCFcAH4+BFGJrBPor+IO8GbO4MgWnt88CAMPf2ifTw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "7.24.1", - "@remotion/bundler": "4.0.445", - "@remotion/renderer": "4.0.445", - "@remotion/studio-shared": "4.0.445", - "memfs": "3.4.3", - "open": "^8.4.2", - "prettier": "3.8.1", - "recast": "0.23.11", - "remotion": "4.0.445", - "semver": "7.5.3", - "source-map": "0.7.3" - } - }, - "node_modules/@remotion/studio-shared": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.445.tgz", - "integrity": "sha512-OqGPFSklJ0E1p2P4sWwnh23xsnxWwhqGkz8payR49a+nYyueXaX+MvKLJrhiXgAMqt4L7507pqvYjRe2Ca96jw==", - "license": "MIT", - "dependencies": { - "remotion": "4.0.445" - } - }, - "node_modules/@remotion/studio/node_modules/@remotion/zod-types": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.445.tgz", - "integrity": "sha512-LSxbdQZY/ZA3gaQ7A+j18kcrEjgTJBQ8mqSl52VJl4X4gDcUUWiYp9k4hFBUOhAEMXHF8KInvf+RPAzHfpSLWg==", - "license": "MIT", - "dependencies": { - "remotion": "4.0.445" - }, - "peerDependencies": { - "zod": "4.3.6" - } - }, - "node_modules/@remotion/tailwind": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/tailwind/-/tailwind-4.0.445.tgz", - "integrity": "sha512-5xt/LjuKyObI8jcvMNcIi4YYaUZSWXHoIOlRdVumX7N0bvn+3qPrQCIzbGk3QuFJL/JvDEzC9jx5mlkA6uu8xg==", - "license": "MIT", - "dependencies": { - "autoprefixer": "10.4.20", - "postcss": "8.4.47", - "postcss-loader": "^7.3.0", - "postcss-preset-env": "^8.3.2", - "style-loader": "4.0.0", - "tailwindcss": "3.4.13" - }, - "peerDependencies": { - "@remotion/bundler": "4.0.445" - } - }, - "node_modules/@remotion/tailwind/node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/@remotion/tailwind/node_modules/tailwindcss": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", - "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@remotion/transitions": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/transitions/-/transitions-4.0.445.tgz", - "integrity": "sha512-f82c17oAaAMAYVnEEhqEmnotRVItg/52G47jRGN6wNBDGaqvnZ4kTFHyXxTTO3EVZXUau9l+hyGad9y4PufnUg==", - "license": "UNLICENSED", - "dependencies": { - "@remotion/paths": "4.0.445", - "@remotion/shapes": "4.0.445", - "remotion": "4.0.445" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remotion/web-renderer": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.445.tgz", - "integrity": "sha512-EHOFdrqe/T3gUS7W1XbmcjAAeCSNJ5CDAcqy6EZWIVOf8nXJbPuh820SohOHdyPBFs5weola/Dt/XiYCunisCg==", - "license": "UNLICENSED", - "dependencies": { - "@mediabunny/aac-encoder": "1.39.2", - "@mediabunny/flac-encoder": "1.39.2", - "@mediabunny/mp3-encoder": "1.39.2", - "@remotion/licensing": "4.0.445", - "mediabunny": "1.39.2", - "remotion": "4.0.445" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@rspack/binding": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.6.tgz", - "integrity": "sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ==", - "license": "MIT", - "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.7.6", - "@rspack/binding-darwin-x64": "1.7.6", - "@rspack/binding-linux-arm64-gnu": "1.7.6", - "@rspack/binding-linux-arm64-musl": "1.7.6", - "@rspack/binding-linux-x64-gnu": "1.7.6", - "@rspack/binding-linux-x64-musl": "1.7.6", - "@rspack/binding-wasm32-wasi": "1.7.6", - "@rspack/binding-win32-arm64-msvc": "1.7.6", - "@rspack/binding-win32-ia32-msvc": "1.7.6", - "@rspack/binding-win32-x64-msvc": "1.7.6" - } - }, - "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.6.tgz", - "integrity": "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rspack/binding-darwin-x64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.6.tgz", - "integrity": "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.6.tgz", - "integrity": "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.6.tgz", - "integrity": "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.6.tgz", - "integrity": "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.6.tgz", - "integrity": "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.6.tgz", - "integrity": "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "1.0.7" - } - }, - "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.6.tgz", - "integrity": "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.6.tgz", - "integrity": "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.6.tgz", - "integrity": "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rspack/core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.6.tgz", - "integrity": "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==", - "license": "MIT", - "dependencies": { - "@module-federation/runtime-tools": "0.22.0", - "@rspack/binding": "1.7.6", - "@rspack/lite-tapable": "1.1.0" - }, - "engines": { - "node": ">=18.12.0" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.1" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@rspack/lite-tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", - "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", - "license": "MIT" - }, - "node_modules/@rspack/plugin-react-refresh": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.1.tgz", - "integrity": "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==", - "license": "MIT", - "dependencies": { - "error-stack-parser": "^2.1.4", - "html-entities": "^2.6.0" - }, - "peerDependencies": { - "react-refresh": ">=0.10.0 <1.0.0", - "webpack-hot-middleware": "2.x" - }, - "peerDependenciesMeta": { - "webpack-hot-middleware": { - "optional": true - } - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/dom-mediacapture-transform": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", - "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", - "license": "MIT", - "dependencies": { - "@types/dom-webcodecs": "*" - } - }, - "node_modules/@types/dom-webcodecs": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", - "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/prismjs": { - "version": "1.26.6", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", - "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", - "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001785", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", - "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-blank-pseudo": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-5.0.2.tgz", - "integrity": "sha512-aCU4AZ7uEcVSUzagTlA9pHciz7aWPKA/YzrEkpdSopJ2pvhIxiQ5sYeMz1/KByxlIo4XBdvMNJAVKMg/GRnhfw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-has-pseudo": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-5.0.2.tgz", - "integrity": "sha512-q+U+4QdwwB7T9VEW/LyO6CFrLAeLqOykC5mDqJXc7aKZAhDbq7BvGT13VGJe+IwBfdN2o3Xdw2kJ5IxwV1Sc9Q==", - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.1", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-8.0.2.tgz", - "integrity": "sha512-OvFghizHJ45x7nsJJUSYLyQNTzsCU8yWjxAc/nhPQg1pbs18LMoET8N3kOweFDPy0JV0OSXN2iqRFhPBHYOeMA==", - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/cssdb": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", - "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "CC0-1.0" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.331", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", - "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", - "license": "ISC" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "license": "MIT", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "license": "Unlicense" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mediabunny": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.39.2.tgz", - "integrity": "sha512-VcrisGRt+OI7tTPrziucJoCIPYIS/DEWY37TqzQVLWSUUHiyvsiRizEypQ3FOlhfIZ4ytAG/Mw4zxfetCTyKUg==", - "license": "MPL-2.0", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@types/dom-mediacapture-transform": "^0.1.11", - "@types/dom-webcodecs": "0.1.13" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/Vanilagy" - } - }, - "node_modules/memfs": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", - "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", - "license": "Unlicense", - "dependencies": { - "fs-monkey": "1.0.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.3.tgz", - "integrity": "sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-5.1.0.tgz", - "integrity": "sha512-w2R4py6zrVE1U7FwNaAc76tNQlG9GLkrBbcFw+VhUjyDDiV28vfZG+l4LyPmpoQpeSJVtu8VgNjE8Jv5SpC7dQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^2.3.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-hex-alpha": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.4.tgz", - "integrity": "sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-8.0.2.tgz", - "integrity": "sha512-xWf/JmAxVoB5bltHpXk+uGRoGFwu4WDAR7210el+iyvTdqiKpDhtcT8N3edXMoVJY0WHFMrKMUieql/wRNiXkw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-media": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz", - "integrity": "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.2", - "@csstools/css-parser-algorithms": "^2.2.0", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/media-query-list-parser": "^2.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-properties": { - "version": "13.3.12", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.12.tgz", - "integrity": "sha512-oPn/OVqONB2ZLNqN185LDyaVByELAA/u3l2CS2TS16x2j2XsmV4kd8U49+TMxmUsEU9d8fB/I10E6U7kB0L1BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.13", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "@csstools/utilities": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.12.tgz", - "integrity": "sha512-ctIoprBMJwByYMGjXG0F7IT2iMF2hnamQ+aWZETyBM0aAlyaYdVZTeUkk8RB+9h9wP+NdN3f01lfvKl2ZSqC0g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.13", - "@csstools/css-parser-algorithms": "^2.7.1", - "@csstools/css-tokenizer": "^2.4.1", - "postcss-selector-parser": "^6.1.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-7.0.2.tgz", - "integrity": "sha512-cMnslilYxBf9k3qejnovrUONZx1rXeUZJw06fgIUBzABJe3D2LiLL5WAER7Imt3nrkaIgG05XZBztueLEf5P8w==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-4.0.4.tgz", - "integrity": "sha512-nUAbUXURemLXIrl4Xoia2tiu5z/n8sY+BVDZApoeT9BlpByyrp02P/lFCRrRvZ/zrGRE+MOGLhk8o7VcMCtPtQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^2.3.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-8.0.2.tgz", - "integrity": "sha512-f/Vd+EC/GaKElknU59esVcRYr/Y3t1ZAQyL4u2xSOgkDy4bMCmG7VP5cGvj3+BTLNE9ETfEuz2nnt4qkZwTTeA==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-7.0.2.tgz", - "integrity": "sha512-AHAJ89UQBcqBvFgQJE9XasGuwMNkKsGj4D/f9Uk60jFmEBHpAL14DrnSk3Rj+SwZTr/WUG+mh+Rvf8fid/346w==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-4.0.1.tgz", - "integrity": "sha512-V5OuQGw4lBumPlwHWk/PRfMKjaq/LTGR4WDTemIMCaMevArVfCCA9wBJiL1VjDAd+rzuCIlkRoRvDsSiAaZ4Fg==", - "license": "CC0-1.0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-image-set-function": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-5.0.2.tgz", - "integrity": "sha512-Sszjwo0ubETX0Fi5MvpYzsONwrsjeabjMoc5YqHvURFItXgIu3HdCjcVuVKGMPGzKRhgaknmdM5uVWInWPJmeg==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-lab-function": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-5.2.3.tgz", - "integrity": "sha512-fi32AYKzji5/rvgxo5zXHFvAYBw0u0OzELbeCNjEZVLUir18Oj+9RmNphtM8QdLUaUnrfx8zy8vVYLmFLkdmrQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/css-color-parser": "^1.2.0", - "@csstools/css-parser-algorithms": "^2.1.1", - "@csstools/css-tokenizer": "^2.1.1", - "@csstools/postcss-progressive-custom-properties": "^2.3.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss-logical": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-6.2.0.tgz", - "integrity": "sha512-aqlfKGaY0nnbgI9jwUikp4gJKBqcH5noU/EdnIVceghaaDPYhZuyJVxlvWNy55tlTG5tunRKCTAX9yljLiFgmw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-11.3.0.tgz", - "integrity": "sha512-JlS10AQm/RzyrUGgl5irVkAlZYTJ99mNueUl+Qab+TcHhVedLiylWVkKBhRale+rS9yWIJK48JVzQlq3LcSdeA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz", - "integrity": "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-4.0.1.tgz", - "integrity": "sha512-HQZ0qi/9iSYHW4w3ogNqVNr2J49DHJAl7r8O2p0Meip38jsdnRPgiDW7r/LlLrrMBMe3KHkvNtAV2UmRVxzLIg==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-8.0.1.tgz", - "integrity": "sha512-Ow2LedN8sL4pq8ubukO77phSVt4QyCm35ZGCYXKvRFayAwcpgB0sjNJglDoTuRdUL32q/ZC1VkPBo0AOEr4Uiw==", - "license": "CC0-1.0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-preset-env": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.5.1.tgz", - "integrity": "sha512-qhWnJJjP6ArLUINWJ38t6Aftxnv9NW6cXK0NuwcLCcRilbuw72dSFLkCVUJeCfHGgJiKzX+pnhkGiki0PEynWg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "CC0-1.0", - "dependencies": { - "@csstools/postcss-cascade-layers": "^3.0.1", - "@csstools/postcss-color-function": "^2.2.3", - "@csstools/postcss-color-mix-function": "^1.0.3", - "@csstools/postcss-font-format-keywords": "^2.0.2", - "@csstools/postcss-gradients-interpolation-method": "^3.0.6", - "@csstools/postcss-hwb-function": "^2.2.2", - "@csstools/postcss-ic-unit": "^2.0.4", - "@csstools/postcss-is-pseudo-class": "^3.2.1", - "@csstools/postcss-logical-float-and-clear": "^1.0.1", - "@csstools/postcss-logical-resize": "^1.0.1", - "@csstools/postcss-logical-viewport-units": "^1.0.3", - "@csstools/postcss-media-minmax": "^1.0.4", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^1.0.4", - "@csstools/postcss-nested-calc": "^2.0.2", - "@csstools/postcss-normalize-display-values": "^2.0.1", - "@csstools/postcss-oklab-function": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^2.3.0", - "@csstools/postcss-relative-color-syntax": "^1.0.2", - "@csstools/postcss-scope-pseudo-class": "^2.0.2", - "@csstools/postcss-stepped-value-functions": "^2.1.1", - "@csstools/postcss-text-decoration-shorthand": "^2.2.4", - "@csstools/postcss-trigonometric-functions": "^2.1.1", - "@csstools/postcss-unset-value": "^2.0.1", - "autoprefixer": "^10.4.14", - "browserslist": "^4.21.9", - "css-blank-pseudo": "^5.0.2", - "css-has-pseudo": "^5.0.2", - "css-prefers-color-scheme": "^8.0.2", - "cssdb": "^7.6.0", - "postcss-attribute-case-insensitive": "^6.0.2", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^5.1.0", - "postcss-color-hex-alpha": "^9.0.2", - "postcss-color-rebeccapurple": "^8.0.2", - "postcss-custom-media": "^9.1.5", - "postcss-custom-properties": "^13.2.0", - "postcss-custom-selectors": "^7.1.3", - "postcss-dir-pseudo-class": "^7.0.2", - "postcss-double-position-gradients": "^4.0.4", - "postcss-focus-visible": "^8.0.2", - "postcss-focus-within": "^7.0.2", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^4.0.1", - "postcss-image-set-function": "^5.0.2", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^5.2.3", - "postcss-logical": "^6.2.0", - "postcss-nesting": "^11.3.0", - "postcss-opacity-percentage": "^2.0.0", - "postcss-overflow-shorthand": "^4.0.1", - "postcss-page-break": "^3.0.4", - "postcss-place": "^8.0.1", - "postcss-pseudo-class-any-link": "^8.0.2", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^7.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-8.0.2.tgz", - "integrity": "sha512-FYTIuRE07jZ2CW8POvctRgArQJ43yxhr5vLmImdKUvjFCkR09kh8pIdlCwdx/jbFm7MiW4QP58L4oOUv3grQYA==", - "license": "CC0-1.0", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.2.tgz", - "integrity": "sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.13" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/recast/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remotion": { - "version": "4.0.445", - "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.445.tgz", - "integrity": "sha512-SiEueQVsc93G8BAC1U1Ein+9vAKU7/17AgyXmGCADWyywObnRSPC6j7uJmvEDtwsxGWycP3vDV4e+fNd+41rUw==", - "license": "SEE LICENSE IN LICENSE.md", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "license": "MIT" - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/style-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", - "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", - "license": "MIT", - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.27.0" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.46.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", - "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", - "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "license": "BSD-2-Clause" - }, - "node_modules/webpack": { - "version": "5.105.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", - "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/plugins/droid-control/remotion/package.json b/plugins/droid-control/remotion/package.json deleted file mode 100644 index 893c1d3..0000000 --- a/plugins/droid-control/remotion/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "droid-control-remotion", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "npx remotion studio", - "render": "npx remotion render", - "still": "npx remotion still", - "test": "node --test 'tests/**/*.test.mts'" - }, - "dependencies": { - "@remotion/cli": "4.0.445", - "@remotion/layout-utils": "4.0.445", - "@remotion/light-leaks": "4.0.445", - "@remotion/media": "4.0.445", - "@remotion/tailwind": "4.0.445", - "@remotion/transitions": "4.0.445", - "prism-react-renderer": "^2.4.1", - "react": "^19", - "react-dom": "^19", - "remotion": "4.0.445", - "zod": "4.3.6" - }, - "devDependencies": { - "@types/react": "^19", - "tailwindcss": "^4", - "typescript": "^5.7" - } -} diff --git a/plugins/droid-control/remotion/src/Root.tsx b/plugins/droid-control/remotion/src/Root.tsx deleted file mode 100644 index f2a4d71..0000000 --- a/plugins/droid-control/remotion/src/Root.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Composition } from 'remotion'; -import { ShowcaseComposition, showcaseSchema } from './compositions/Showcase'; -import { calculateShowcaseDuration } from './lib/duration'; - -export const RemotionRoot: React.FC = () => { - return ( - <> - { - const fps = 30; - return { - durationInFrames: calculateShowcaseDuration(props, fps), - fps, - width: props.width ?? 1920, - height: props.height ?? 1080, - }; - }} - defaultProps={{ - clips: [], - layout: 'single' as const, - labels: [], - title: 'Demo', - subtitle: '', - preset: 'factory' as const, - fidelity: 'standard' as const, - keys: [], - effects: [], - width: 1920, - height: 1080, - }} - /> - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/ActiveInputPulse.tsx b/plugins/droid-control/remotion/src/components/ActiveInputPulse.tsx deleted file mode 100644 index 3493bfd..0000000 --- a/plugins/droid-control/remotion/src/components/ActiveInputPulse.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion'; - -const TARGET_HZ = 1.5; - -export const ActiveInputPulse: React.FC<{ - y?: string; - color?: string; - active?: boolean; -}> = ({ y = '50%', color = '#EE6018', active = true }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - if (!active) { - return null; - } - - // Oscillate opacity between 0.08 and 0.22 at ~1.5 Hz - const omega = (2 * Math.PI * TARGET_HZ) / fps; - const sinValue = Math.sin(frame * omega); - const opacity = 0.15 + sinValue * 0.07; // range: 0.08 .. 0.22 - - return ( - -
- - ); -}; diff --git a/plugins/droid-control/remotion/src/components/Background.tsx b/plugins/droid-control/remotion/src/components/Background.tsx deleted file mode 100644 index c2f0f3d..0000000 --- a/plugins/droid-control/remotion/src/components/Background.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - AbsoluteFill, - useCurrentFrame, - useVideoConfig, - interpolate, - staticFile, -} from 'remotion'; -import type { Palette } from '../lib/palettes'; -import type { PresetConfig } from '../lib/presets'; - -/** - * Full-bleed background layer. - * - * For warm (Factory) palettes: a radial vignette (dark center, warm - * amber-brown edges) with 4 asymmetric corner glow blobs whose opacity - * intensifies over the video duration — from a mild warm wash (~0.05) to - * a rich reddish-amber bloom (~0.2). - * - * For cool palettes (Catppuccin, etc.): a cool-toned radial or flat fill. - */ -export const Background: React.FC<{ - palette: Palette; - config: PresetConfig; - totalFrames?: number; -}> = ({ palette, config, totalFrames }) => { - const frame = useCurrentFrame(); - const { durationInFrames } = useVideoConfig(); - const total = totalFrames ?? durationInFrames; - - const isWarm = palette.accent === '#EE6018'; - - if (isWarm) { - // Progressive warmth: 0 → 1 over full duration - const warmth = interpolate(frame, [0, total], [0, 1], { - extrapolateRight: 'clamp', - }); - - // Glow opacity ramps from subtle to prominent - const glowBase = interpolate(warmth, [0, 1], [0.05, 0.2]); - - return ( - - {/* Bottom-left: strongest warm amber glow */} -
- - {/* Top-right: slightly cooler amber */} -
- - {/* Bottom-right: deep red accent */} -
- - {/* Top-left: faint Factory-orange kiss */} -
- - {/* Halftone texture overlay */} - - - ); - } - - // Cool palettes — radial gradient or flat fill - if (config.bgStyle === 'gradient') { - return ( - - ); - } - - return ; -}; diff --git a/plugins/droid-control/remotion/src/components/CalloutOverlay.tsx b/plugins/droid-control/remotion/src/components/CalloutOverlay.tsx deleted file mode 100644 index 478b7ae..0000000 --- a/plugins/droid-control/remotion/src/components/CalloutOverlay.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; -import type { Effect } from '../lib/schema'; -import type { Palette } from '../lib/palettes'; - -type Callout = Extract; - -const CalloutPill: React.FC<{ - callout: Callout; - palette: Palette; - enterFrame: number; - exitFrame: number; -}> = ({ callout, palette, enterFrame, exitFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const isVisible = frame >= enterFrame && frame < exitFrame; - if (!isVisible) return null; - - const localFrame = frame - enterFrame; - - // Pop-in animation over 0.25s (same overshoot bezier as KeystrokePill) - const enterProgress = interpolate(localFrame, [0, 0.25 * fps], [0, 1], { - easing: Easing.bezier(0.34, 1.56, 0.64, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - // Fade out over last 0.2s - const totalFrames = exitFrame - enterFrame; - const fadeOutStart = totalFrames - 0.2 * fps; - const exitOpacity = interpolate( - localFrame, - [fadeOutStart, totalFrames], - [1, 0], - { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - - const scale = interpolate(enterProgress, [0, 1], [0.85, 1]); - - return ( -
- {callout.text} -
- ); -}; - -export const CalloutOverlay: React.FC<{ - callouts: Callout[]; - palette: Palette; -}> = ({ callouts, palette }) => { - const { fps } = useVideoConfig(); - - return ( - <> - {callouts.map((callout) => ( - - ))} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx b/plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx deleted file mode 100644 index 327a2a0..0000000 --- a/plugins/droid-control/remotion/src/components/CodeAnnotationOverlay.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React, { useMemo } from 'react'; -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; -import { Highlight, themes, type PrismTheme } from 'prism-react-renderer'; -import type { CodeAnnotation, CodeRange } from '../lib/schema'; -import type { Palette } from '../lib/palettes'; -import type { PresetConfig } from '../lib/presets'; - -const MONO = - "'Geist Mono', 'Berkeley Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace"; - -const inRange = (line: number, ranges: CodeRange[]): boolean => - ranges.some((r) => line >= r.start && line <= r.end); - -const buildPrismTheme = (palette: Palette): PrismTheme => { - const base = themes.vsDark; - return { - ...base, - plain: { - ...base.plain, - color: palette.text, - backgroundColor: 'transparent', - }, - }; -}; - -const positionToAnchor = ( - position: CodeAnnotation['position'], - margin: number, -): React.CSSProperties => { - const edge = Math.max(40, margin + 24); - switch (position) { - case 'center': - return { - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)', - }; - case 'bottom-left': - return { left: edge, bottom: edge }; - case 'top-right': - default: - return { right: edge, top: edge }; - } -}; - -const CodeCard: React.FC<{ - annotation: CodeAnnotation; - palette: Palette; - config: PresetConfig; - enterFrame: number; - exitFrame: number; -}> = ({ annotation, palette, config, enterFrame, exitFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - const lines = useMemo( - () => annotation.code.replace(/\n$/, '').split('\n'), - [annotation.code], - ); - const prismTheme = useMemo(() => buildPrismTheme(palette), [palette]); - - if (frame < enterFrame || frame >= exitFrame) return null; - - const localFrame = frame - enterFrame; - const totalFrames = exitFrame - enterFrame; - - const enterProgress = interpolate(localFrame, [0, 0.35 * fps], [0, 1], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - const exitStart = Math.max(0, totalFrames - 0.35 * fps); - const exitProgress = interpolate( - localFrame, - [exitStart, totalFrames], - [0, 1], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, - ); - - const opacity = enterProgress * (1 - exitProgress); - const scale = interpolate(enterProgress, [0, 1], [0.96, 1]); - const blur = interpolate(enterProgress, [0, 1], [6, 0]); - - const highlightRanges = annotation.highlight ?? []; - const focusRanges = annotation.focus ?? []; - const hasFocus = focusRanges.length > 0; - const anchor = positionToAnchor( - annotation.position ?? 'top-right', - config.margin, - ); - const maxLine = lines.length; - const gutterWidth = `${String(maxLine).length + 1}ch`; - - return ( -
- {annotation.title && ( -
- {annotation.title} -
- )} - - {({ tokens, getLineProps, getTokenProps }) => ( -
-            {tokens.map((line, i) => {
-              const lineNumber = i + 1;
-              const isHighlighted = inRange(lineNumber, highlightRanges);
-              const isFocused = !hasFocus || inRange(lineNumber, focusRanges);
-              const lineOpacity = isFocused ? 1 : 0.3;
-              const lineFilter = isFocused ? 'none' : 'blur(1.2px)';
-              const bg = isHighlighted ? `${palette.accent}22` : 'transparent';
-              const lineBorder = isHighlighted
-                ? `2px solid ${palette.accent}`
-                : `2px solid transparent`;
-              const { key: _lineKey, ...lineRest } = getLineProps({
-                line,
-                key: i,
-              });
-
-              return (
-                
- - {lineNumber} - - - {line.map((token, tokenIndex) => { - const { key: _tokenKey, ...tokenRest } = getTokenProps({ - token, - key: tokenIndex, - }); - return ( - - ); - })} - -
- ); - })} -
- )} -
-
- ); -}; - -export const CodeAnnotationOverlay: React.FC<{ - annotations: CodeAnnotation[]; - palette: Palette; - config: PresetConfig; -}> = ({ annotations, palette, config }) => { - const { fps } = useVideoConfig(); - - if (!annotations.length) return null; - - return ( - <> - {annotations.map((annotation, i) => { - const enterFrame = Math.round(annotation.t * fps); - const exitFrame = Math.round((annotation.t + annotation.dur) * fps); - return ( - - ); - })} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/ColorGradeOverlay.tsx b/plugins/droid-control/remotion/src/components/ColorGradeOverlay.tsx deleted file mode 100644 index 83c870f..0000000 --- a/plugins/droid-control/remotion/src/components/ColorGradeOverlay.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { AbsoluteFill } from 'remotion'; -import type { Palette } from '../lib/palettes'; - -/** - * Global colour-grade post-processing layer — analogous to a DaVinci Resolve - * colour grade applied over the final composite. - * - * - Factory (warm): warm amber tint via `mix-blend-mode: color` - * - Catppuccin (cool): cool blue tint at 75 % of the given intensity - * - A nested radial vignette darkens the corners for a cinematic frame. - * - * Place this as the **last** child in the z-stack so it sits above all - * content, including any noise overlays. - */ -export const ColorGradeOverlay: React.FC<{ - palette: Palette; - intensity?: number; -}> = ({ palette, intensity = 0.04 }) => { - const isWarm = palette.accent === '#EE6018'; - - const gradeColor = isWarm - ? `rgba(200, 120, 40, ${intensity})` - : `rgba(80, 100, 200, ${intensity * 0.75})`; - - return ( - - {/* Colour tint — shifts the entire frame's temperature */} - - - {/* Radial vignette — subtle darkening at the corners */} - - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/DroidOutro.tsx b/plugins/droid-control/remotion/src/components/DroidOutro.tsx deleted file mode 100644 index 716ea38..0000000 --- a/plugins/droid-control/remotion/src/components/DroidOutro.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; -import type { Palette } from '../lib/palettes'; -import { RotorMark } from './RotorMark'; -import { DroidWordmark } from './DroidWordmark'; - -export const DroidOutro: React.FC<{ - palette: Palette; -}> = ({ palette }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - const rotorCenterX = 303.105; - const rotorCenterY = 319.528; - const rotorCenterXPercent = (rotorCenterX / 613) * 100; - const rotorCenterYPercent = (rotorCenterY / 650) * 100; - const topRightSliceYPercent = ((rotorCenterY - (613 - rotorCenterX)) / 650) * 100; - - // Phase 1: Fan out the 8 triangles (0s to 1.5s) - const fanDuration = 1.5 * fps; - // Phase 2: Crossfade to ASCII Droid (1.5s to 2.5s) - const fadeDuration = 1.0 * fps; - - // 8 overlapping wedges that form the RotorMark logo - const wedges = Array.from({ length: 8 }).map((_, i) => { - // 1. Scale up as a single stacked blade (frames 0 to 12) - const scaleProgress = interpolate(frame, [0, 12], [0, 1], { - easing: Easing.out(Easing.back(1.5)), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - // 2. Fan out from the single blade (frames 12 to fanDuration) - const rotationProgress = interpolate(frame, [12, fanDuration], [0, i * 45], { - easing: Easing.out(Easing.cubic), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - // Glitch effect: flash opacity slightly during the fanning phase - const isGlitching = frame > 12 && frame < 24 && frame % 3 === 0; - const baseOpacity = interpolate(frame, [0, 12], [0, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const opacity = isGlitching ? 0.5 : baseOpacity; - - return ( -
- -
- ); - }); - - // Crossfade between the rotor triangles and the ASCII logo - const wedgesOpacity = interpolate(frame, [fanDuration, fanDuration + fadeDuration], [1, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - const asciiOpacity = interpolate(frame, [fanDuration, fanDuration + fadeDuration], [0, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - return ( - - {/* The fanning rotor layer */} -
- {wedges} -
- - {/* The ASCII Droid wordmark layer */} -
- -
-
- ); -}; diff --git a/plugins/droid-control/remotion/src/components/DroidWordmark.tsx b/plugins/droid-control/remotion/src/components/DroidWordmark.tsx deleted file mode 100644 index 558f7f8..0000000 --- a/plugins/droid-control/remotion/src/components/DroidWordmark.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import type { Palette } from '../lib/palettes'; - -const DROID_ASCII = ` - ██████████ ██████████ ██████████ ███ ██████████ - ███ ███ ███ ███ ███ ███ ███ ███ ███ - ███ ███ ███ ███ ███ ███ ███ ███ ███ - ███ ███ ██████████ ███ ███ ███ ███ ███ - ███ ███ ███ ███ ███ ███ ███ ███ ███ - ███ ███ ███ ███ ███ ███ ███ ███ ███ - ██████████ ███ ███ ██████████ ███ ██████████ -`.replace(/^\n/, '').trimEnd(); - -export const DroidWordmark: React.FC<{ - palette: Palette; - logoColor?: string; - taglineColor?: string; -}> = ({ palette, logoColor, taglineColor }) => ( -
-
- {DROID_ASCII} -
-
- AUTONOMOUS ENGINEERING -
-
-); diff --git a/plugins/droid-control/remotion/src/components/DynamicShadowParallax.tsx b/plugins/droid-control/remotion/src/components/DynamicShadowParallax.tsx deleted file mode 100644 index c88e353..0000000 --- a/plugins/droid-control/remotion/src/components/DynamicShadowParallax.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useCurrentFrame, useVideoConfig, interpolate } from 'remotion'; - -/** - * Provides subtle shadow-offset parallax that shifts over the video duration, - * creating a subliminal sense of depth as if the light source drifts with a - * slow Ken Burns camera move. - * - * offsetX: 0 → 2 px (horizontal drift) - * offsetY: 8 → 10 px (vertical drift deepens) - */ -export function useShadowParallax(): { offsetX: number; offsetY: number } { - const frame = useCurrentFrame(); - const { durationInFrames } = useVideoConfig(); - - const offsetX = interpolate(frame, [0, durationInFrames], [0, 2], { - extrapolateRight: 'clamp', - }); - const offsetY = interpolate(frame, [0, durationInFrames], [8, 10], { - extrapolateRight: 'clamp', - }); - - return { offsetX, offsetY }; -} diff --git a/plugins/droid-control/remotion/src/components/FloatingParticles.tsx b/plugins/droid-control/remotion/src/components/FloatingParticles.tsx deleted file mode 100644 index 89ab1d7..0000000 --- a/plugins/droid-control/remotion/src/components/FloatingParticles.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useMemo } from 'react'; -import { AbsoluteFill, useCurrentFrame } from 'remotion'; - -interface ParticleSeed { - baseX: number; - baseY: number; - offsetX: number; - offsetY: number; - size: number; -} - -export const FloatingParticles: React.FC<{ - count?: number; - color?: string; - opacity?: number; -}> = ({ count = 30, color = '#EE6018', opacity = 0.07 }) => { - const frame = useCurrentFrame(); - - // Deterministic particle seeds — stable across frames, varies only by count - const seeds = useMemo( - () => - Array.from({ length: count }, (_, i) => ({ - baseX: (i * 73 + 17) % 100, - baseY: (i * 47 + 31) % 100, - offsetX: (i * 13 + 7) * 0.1, - offsetY: (i * 19 + 11) * 0.1, - size: 2 + (i % 3) * 2, // 2, 4, or 6px - })), - [count] - ); - - return ( - - {seeds.map((p, i) => { - // Slow Lissajous paths driven by frame count - const dx = Math.sin(frame * 0.008 + p.offsetX) * 60; - const dy = Math.cos(frame * 0.006 + p.offsetY) * 40; - - return ( -
- ); - })} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/GlitchTitle.tsx b/plugins/droid-control/remotion/src/components/GlitchTitle.tsx deleted file mode 100644 index 9376167..0000000 --- a/plugins/droid-control/remotion/src/components/GlitchTitle.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import React, { useMemo } from 'react'; -import { - AbsoluteFill, - useCurrentFrame, - useVideoConfig, - interpolate, - Easing, -} from 'remotion'; -import type { Palette } from '../lib/palettes'; - -const BLOCK_SIZE = 12; -const GRID_WIDTH = 800; -const GRID_HEIGHT = 120; - -interface PixelBlock { - /** Grid-assembled position (top-left corner) */ - x: number; - y: number; - /** Scattered offset from assembled position */ - scatterDx: number; - scatterDy: number; - /** true = accent color, false = text color */ - isAccent: boolean; -} - -/** - * Deterministic "garbled" subtitle: replace ~30% of characters with random ASCII, - * seeded by character index. - */ -function garbleText(text: string): string { - const chars: string[] = []; - for (let i = 0; i < text.length; i++) { - const shouldGarble = (i * 17 + 3) % 10 < 3; // ~30% - if (shouldGarble && text[i] !== ' ') { - const code = 33 + ((i * 17) % 94); - chars.push(String.fromCharCode(code)); - } else { - chars.push(text[i]); - } - } - return chars.join(''); -} - -/** - * Build a deterministic grid of pixel blocks that represent the title area. - * Each block gets a fixed scatter target computed from its index. - */ -function buildBlockGrid(): PixelBlock[] { - const cols = Math.floor(GRID_WIDTH / BLOCK_SIZE); - const rows = Math.floor(GRID_HEIGHT / BLOCK_SIZE); - const blocks: PixelBlock[] = []; - - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const i = row * cols + col; - blocks.push({ - x: col * BLOCK_SIZE, - y: row * BLOCK_SIZE, - scatterDx: Math.sin(i * 7.3 + 3.1) * 200, - scatterDy: Math.cos(i * 5.7 + 1.4) * 150, - isAccent: (i * 13 + 7) % 5 === 0, // ~20% accent - }); - } - } - - return blocks; -} - -export const GlitchTitle: React.FC<{ - title: string; - subtitle?: string; - palette: Palette; -}> = ({ title, subtitle, palette }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const blocks = useMemo(buildBlockGrid, []); - const garbled = useMemo( - () => (subtitle ? garbleText(subtitle) : undefined), - [subtitle] - ); - - // Phase boundaries in frames - const scatterEnd = Math.round(0.5 * fps); // ~15 frames at 30fps - const assembleEnd = Math.round(1.5 * fps); // ~45 frames at 30fps - - // --- Garbled subtitle fade-in (during scatter phase) --- - const garbledOpacity = interpolate(frame, [0, scatterEnd], [0, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - // --- Title text fade-in (after assembly completes) --- - const titleOpacity = interpolate( - frame, - [assembleEnd - Math.round(0.2 * fps), assembleEnd], - [0, 1], - { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - - return ( - - {/* Garbled subtitle line above the pixel grid */} - {garbled && ( -
- {garbled} -
- )} - - {/* Pixel block grid container */} -
- {/* Rendered pixel blocks */} - {/* Hoisted outside .map() — same value for every block */} - {(() => { - const scatterOpacity = interpolate(frame, [0, scatterEnd], [0, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const assembleProgress = interpolate( - frame, - [scatterEnd, assembleEnd], - [0, 1], - { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - return blocks.map((block, i) => { - // Phase (c): subtle jitter when assembled - const jitterX = - frame >= assembleEnd ? Math.sin(frame * 0.1 + i) * 1.5 : 0; - const jitterY = - frame >= assembleEnd ? Math.cos(frame * 0.13 + i * 0.7) * 1.5 : 0; - - // Interpolate from scatter position to grid position - const dx = interpolate( - assembleProgress, - [0, 1], - [block.scatterDx, 0] - ); - const dy = interpolate( - assembleProgress, - [0, 1], - [block.scatterDy, 0] - ); - - const opacity = frame < scatterEnd ? scatterOpacity : 1; - const x = block.x + dx + jitterX; - const y = block.y + dy + jitterY; - - return ( -
- ); - }); - })()} - - {/* Title text rendered on top of assembled blocks */} -
- {title} -
-
- - {/* Scanline overlay */} -
- - ); -}; diff --git a/plugins/droid-control/remotion/src/components/GlowLines.tsx b/plugins/droid-control/remotion/src/components/GlowLines.tsx deleted file mode 100644 index 516ce02..0000000 --- a/plugins/droid-control/remotion/src/components/GlowLines.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { - AbsoluteFill, - useCurrentFrame, - useVideoConfig, - interpolate, -} from 'remotion'; - -interface GlowEvent { - t: number; - y: string; // percentage within terminal content area - dur?: number; -} - -const DEFAULT_DURATION = 0.95; // seconds - -/** - * Renders a single glow bar for a content event. - * Fades in over 0.15s, holds for 0.3s, fades out over 0.5s. - */ -const GlowBar: React.FC<{ - event: GlowEvent; - color: string; - fps: number; - frame: number; -}> = ({ event, color, fps, frame }) => { - const totalDur = event.dur ?? DEFAULT_DURATION; - const startFrame = Math.round(event.t * fps); - - // Phase durations in frames - const fadeInFrames = Math.round(0.15 * fps); - const holdFrames = Math.round(0.3 * fps); - const fadeOutFrames = Math.max(0, Math.round((totalDur - 0.15 - 0.3) * fps)); - const endFrame = startFrame + fadeInFrames + holdFrames + fadeOutFrames; - - // Outside active range → don't render - if (frame < startFrame || frame > endFrame) { - return null; - } - - const fadeInEnd = startFrame + fadeInFrames; - const holdEnd = fadeInEnd + holdFrames; - - let opacity: number; - if (frame <= fadeInEnd) { - // Fade in - opacity = interpolate(frame, [startFrame, fadeInEnd], [0, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - } else if (frame <= holdEnd) { - // Hold at full - opacity = 1; - } else { - // Fade out - opacity = interpolate(frame, [holdEnd, endFrame], [1, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - } - - return ( -
- ); -}; - -export const GlowLines: React.FC<{ - events: GlowEvent[]; - color?: string; -}> = ({ events, color = '#EE6018' }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - return ( - - {events.map((event, i) => ( - - ))} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/KeystrokeOverlay.tsx b/plugins/droid-control/remotion/src/components/KeystrokeOverlay.tsx deleted file mode 100644 index ee776e8..0000000 --- a/plugins/droid-control/remotion/src/components/KeystrokeOverlay.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; -import type { Keystroke } from '../lib/schema'; -import type { Palette } from '../lib/palettes'; -import type { PresetConfig } from '../lib/presets'; - -const KeystrokePill: React.FC<{ - label: string; - palette: Palette; - config: PresetConfig; - enterFrame: number; - exitFrame: number; -}> = ({ label, palette, config, enterFrame, exitFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const isVisible = frame >= enterFrame && frame < exitFrame; - if (!isVisible) return null; - - const localFrame = frame - enterFrame; - - // Pop-in animation over 0.2s - const enterProgress = interpolate(localFrame, [0, 0.2 * fps], [0, 1], { - easing: Easing.bezier(0.34, 1.56, 0.64, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - // Fade out over last 0.15s - const totalFrames = exitFrame - enterFrame; - const fadeOutStart = totalFrames - 0.15 * fps; - const exitOpacity = interpolate( - localFrame, - [fadeOutStart, totalFrames], - [1, 0], - { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - - const scale = interpolate(enterProgress, [0, 1], [0.8, 1]); - const enterOpacity = enterProgress; - - return ( -
- {label} -
- ); -}; - -export const KeystrokeOverlay: React.FC<{ - keys: Keystroke[]; - palette: Palette; - config: PresetConfig; -}> = ({ keys, palette, config }) => { - const { fps } = useVideoConfig(); - const defaultDur = 1.2; - - return ( - <> - {keys.map((key, i) => { - const enterFrame = Math.round(key.t * fps); - const naturalExit = Math.round((key.t + (key.dur ?? defaultDur)) * fps); - const nextStart = - i + 1 < keys.length ? Math.round(keys[i + 1].t * fps) : Infinity; - const exitFrame = Math.min(naturalExit, nextStart); - - return ( - - ); - })} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/NoiseOverlay.tsx b/plugins/droid-control/remotion/src/components/NoiseOverlay.tsx deleted file mode 100644 index 5cb5608..0000000 --- a/plugins/droid-control/remotion/src/components/NoiseOverlay.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { AbsoluteFill } from 'remotion'; - -export const NoiseOverlay: React.FC<{ - opacity?: number; -}> = ({ opacity = 0.03 }) => { - return ( - - - - - - - - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/RotorMark.tsx b/plugins/droid-control/remotion/src/components/RotorMark.tsx deleted file mode 100644 index 6c30e64..0000000 --- a/plugins/droid-control/remotion/src/components/RotorMark.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -const ROTOR_PATH = - 'M396.406 180.565C395.606 180.367 394.858 179.997 394.213 179.483C393.568 178.968 393.041 178.321 392.671 177.585C392.299 176.848 392.092 176.04 392.061 175.216C392.031 174.391 392.179 173.571 392.496 172.809C403.427 146.207 408.25 124.922 400.466 116.012C379.851 92.371 297.179 139.382 270.819 155.303C270.113 155.728 269.324 155.995 268.505 156.086C267.687 156.178 266.858 156.091 266.076 155.833C265.294 155.575 264.576 155.152 263.973 154.591C263.369 154.031 262.894 153.346 262.579 152.586C251.499 126.04 239.85 107.576 228.044 106.775C196.75 104.634 171.526 196.337 164.142 226.226C163.945 227.026 163.577 227.775 163.064 228.419C162.55 229.065 161.903 229.591 161.167 229.962C160.432 230.334 159.625 230.541 158.801 230.571C157.977 230.602 157.157 230.454 156.396 230.137C129.794 219.206 108.499 214.383 99.5981 222.167C75.9574 242.782 122.96 325.454 138.881 351.814C139.307 352.519 139.575 353.308 139.667 354.128C139.759 354.946 139.674 355.776 139.416 356.558C139.158 357.341 138.733 358.059 138.172 358.662C137.61 359.266 136.925 359.74 136.163 360.053C109.626 371.133 91.1623 382.782 90.3522 394.588C88.22 425.882 179.915 451.107 209.813 458.491C210.612 458.69 211.358 459.059 212.001 459.573C212.644 460.087 213.169 460.735 213.538 461.47C213.909 462.205 214.117 463.011 214.146 463.834C214.177 464.656 214.03 465.477 213.714 466.236C202.783 492.838 197.96 514.132 205.744 523.034C226.359 546.675 309.041 499.672 335.4 483.751C336.106 483.325 336.896 483.057 337.715 482.965C338.534 482.872 339.363 482.958 340.145 483.217C340.928 483.475 341.645 483.899 342.249 484.461C342.852 485.022 343.327 485.708 343.641 486.469C354.721 513.006 366.36 531.471 378.175 532.281C409.47 534.413 434.693 442.718 442.068 412.82C442.267 412.02 442.637 411.272 443.152 410.629C443.666 409.985 444.314 409.46 445.051 409.089C445.787 408.719 446.595 408.512 447.419 408.483C448.243 408.453 449.063 408.601 449.824 408.919C476.425 419.85 497.711 424.663 506.621 416.888C530.262 396.273 483.25 313.591 467.329 287.232C466.906 286.526 466.64 285.736 466.55 284.919C466.459 284.1 466.546 283.272 466.804 282.49C467.062 281.708 467.485 280.991 468.045 280.388C468.604 279.785 469.288 279.308 470.047 278.992C496.593 267.912 515.057 256.263 515.858 244.457C517.999 213.162 426.295 187.94 396.406 180.565ZM360.503 150.564C366.518 161.346 335.521 233.191 312.467 283.443C312.082 284.283 311.449 284.985 310.652 285.454C309.856 285.923 308.934 286.138 308.012 286.068C307.09 285.998 306.211 285.647 305.495 285.063C304.778 284.478 304.258 283.689 304.003 282.8C294.692 250.127 284.05 211.739 272.663 179.15C272.216 177.871 272.238 176.474 272.726 175.21C273.214 173.946 274.136 172.897 275.325 172.25C303.761 156.719 352.421 136.095 360.503 150.564ZM224.226 159.456C236.098 162.827 264.981 235.547 284.208 287.382C284.529 288.247 284.577 289.191 284.346 290.085C284.114 290.979 283.615 291.781 282.915 292.383C282.214 292.986 281.346 293.359 280.427 293.453C279.508 293.548 278.583 293.359 277.774 292.912C248.063 276.422 213.416 256.776 182.317 241.785C181.1 241.194 180.131 240.191 179.584 238.953C179.036 237.715 178.946 236.323 179.329 235.025C188.481 203.964 208.277 154.94 224.226 159.456ZM134.151 262.11C144.924 256.095 216.778 287.093 267.02 310.147C267.861 310.532 268.563 311.166 269.032 311.963C269.501 312.759 269.716 313.681 269.646 314.602C269.575 315.524 269.225 316.402 268.64 317.119C268.056 317.835 267.267 318.357 266.378 318.611C233.714 327.922 195.316 338.564 162.727 349.952C161.45 350.396 160.055 350.373 158.793 349.885C157.531 349.397 156.483 348.477 155.837 347.288C140.334 318.852 119.673 270.192 134.151 262.11ZM143.043 398.388C146.405 386.516 219.133 357.632 270.968 338.405C271.834 338.085 272.778 338.037 273.672 338.268C274.567 338.5 275.368 338.999 275.971 339.699C276.572 340.4 276.947 341.268 277.041 342.186C277.135 343.105 276.946 344.031 276.499 344.84C260 374.552 240.353 409.198 225.362 440.287C224.777 441.509 223.774 442.482 222.535 443.031C221.296 443.581 219.902 443.671 218.603 443.286C187.541 434.189 138.518 414.338 143.043 398.388ZM245.698 488.463C239.673 477.69 270.679 405.837 293.733 355.594C294.119 354.753 294.753 354.051 295.549 353.582C296.346 353.113 297.267 352.899 298.189 352.969C299.111 353.038 299.99 353.389 300.706 353.973C301.422 354.558 301.943 355.348 302.197 356.237C311.508 388.9 322.151 427.299 333.538 459.887C333.982 461.166 333.957 462.56 333.468 463.823C332.979 465.085 332.056 466.132 330.866 466.777C302.439 482.28 253.77 502.941 245.726 488.463H245.698ZM381.974 479.57C370.093 476.209 341.21 403.481 321.983 351.646C321.661 350.779 321.612 349.833 321.843 348.937C322.074 348.041 322.575 347.238 323.277 346.635C323.979 346.032 324.85 345.659 325.77 345.566C326.691 345.473 327.618 345.665 328.426 346.115C358.129 362.605 392.784 382.261 423.874 397.252C425.095 397.839 426.065 398.842 426.613 400.081C427.161 401.32 427.249 402.713 426.863 404.012C417.719 435.12 397.924 484.095 381.974 479.57ZM472.049 376.916C461.267 382.94 389.423 351.934 339.171 328.88C338.331 328.494 337.628 327.861 337.159 327.064C336.69 326.268 336.477 325.346 336.547 324.425C336.616 323.503 336.966 322.625 337.551 321.908C338.135 321.191 338.925 320.671 339.814 320.417C372.486 311.106 410.876 300.463 443.464 289.075C444.745 288.631 446.141 288.655 447.405 289.145C448.668 289.635 449.717 290.558 450.364 291.748C465.857 320.175 486.519 368.843 472.049 376.916ZM463.157 240.639C459.787 252.52 387.067 281.404 335.233 300.631C334.365 300.953 333.42 301.002 332.524 300.771C331.628 300.539 330.825 300.039 330.222 299.337C329.619 298.635 329.247 297.764 329.154 296.843C329.06 295.923 329.251 294.996 329.702 294.187C346.192 264.485 365.838 229.829 380.829 198.739C381.418 197.521 382.421 196.551 383.66 196.004C384.898 195.456 386.291 195.366 387.589 195.751C418.65 204.894 467.673 224.689 463.157 240.639Z'; - -export const RotorMark: React.FC<{ - size?: number; - width?: number; - height?: number; - color?: string; - style?: React.CSSProperties; -}> = ({ size = 24, width, height, color = 'white', style }) => ( - - - -); diff --git a/plugins/droid-control/remotion/src/components/SectionHeader.tsx b/plugins/droid-control/remotion/src/components/SectionHeader.tsx deleted file mode 100644 index 689c9c0..0000000 --- a/plugins/droid-control/remotion/src/components/SectionHeader.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; -import type { Section } from '../lib/schema'; -import type { Palette } from '../lib/palettes'; -import type { PresetConfig } from '../lib/presets'; - -const SectionTitle: React.FC<{ - title: string; - palette: Palette; - config: PresetConfig; - enterFrame: number; - exitFrame: number; -}> = ({ title, palette, config, enterFrame, exitFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const isVisible = frame >= enterFrame && frame < exitFrame; - if (!isVisible) return null; - - const localFrame = frame - enterFrame; - - // Slide down & fade in - const enterProgress = interpolate(localFrame, [0, 0.4 * fps], [0, 1], { - easing: Easing.bezier(0.22, 1, 0.36, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - const totalFrames = exitFrame - enterFrame; - const fadeOutStart = totalFrames - 0.3 * fps; - const exitProgress = interpolate( - localFrame, - [fadeOutStart, totalFrames], - [0, 1], - { - easing: Easing.bezier(0.32, 0, 0.67, 0), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - - const opacity = - interpolate(enterProgress, [0, 1], [0, 1]) * - interpolate(exitProgress, [0, 1], [1, 0]); - const translateY = - interpolate(enterProgress, [0, 1], [-20, 0]) - - interpolate(exitProgress, [0, 1], [0, -20]); - - return ( -
- {title} -
- ); -}; - -export const SectionHeaderOverlay: React.FC<{ - sections: Section[]; - palette: Palette; - config: PresetConfig; -}> = ({ sections, palette, config }) => { - const { fps, durationInFrames } = useVideoConfig(); - - return ( - <> - {sections.map((sec, i) => { - const enterFrame = Math.round(sec.t * fps); - const nextStart = - i + 1 < sections.length - ? Math.round(sections[i + 1].t * fps) - : durationInFrames; - const exitFrame = Math.min(nextStart, durationInFrames); - - return ( - - ); - })} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/SectionTransition.tsx b/plugins/droid-control/remotion/src/components/SectionTransition.tsx deleted file mode 100644 index 4bd1fcc..0000000 --- a/plugins/droid-control/remotion/src/components/SectionTransition.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import { - AbsoluteFill, - useCurrentFrame, - useVideoConfig, - interpolate, - Easing, -} from 'remotion'; -import type { Section } from '../lib/schema'; - -const SWEEP_DURATION_S = 0.5; -const BLUR_PX = 28; - -const FrostedSweep: React.FC<{ - triggerFrame: number; -}> = ({ triggerFrame }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const sweepFrames = Math.round(SWEEP_DURATION_S * fps); - const endFrame = triggerFrame + sweepFrames; - - if (frame < triggerFrame || frame > endFrame) return null; - - const localFrame = frame - triggerFrame; - - // Band sweeps from left (-10%) to right (110%) - const bandX = interpolate(localFrame, [0, sweepFrames], [-10, 110], { - easing: Easing.bezier(0.4, 0, 0.2, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - // Opacity: quick fade in, hold, quick fade out - const opacity = interpolate( - localFrame, - [0, sweepFrames * 0.15, sweepFrames * 0.85, sweepFrames], - [0, 1, 1, 0], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, - ); - - return ( - -
- - ); -}; - -export const SectionTransitionOverlay: React.FC<{ - sections: Section[]; -}> = ({ sections }) => { - const { fps } = useVideoConfig(); - - // Skip the first section -- it marks the start, not a transition point - return ( - <> - {sections.slice(1).map((sec) => { - const triggerFrame = Math.round(sec.t * fps); - return ( - - ); - })} - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx b/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx deleted file mode 100644 index 303091e..0000000 --- a/plugins/droid-control/remotion/src/components/ShowcaseTransition.tsx +++ /dev/null @@ -1,335 +0,0 @@ -// Transition style names (`whip-pan`, `light-leak`, `flash`, `glitch-lite`) and -// the "selectable enum of named transitions" API shape are inspired by -// @hyperframes/shader-transitions (Apache-2.0). Implementations below are -// original Remotion-native CSS/SVG overlays — no GLSL/WebGL involved. -// https://github.com/heygen-com/hyperframes/tree/main/packages/shader-transitions - -import React from 'react'; -import { AbsoluteFill, interpolate, Easing, random } from 'remotion'; -import type { - TransitionPresentation, - TransitionPresentationComponentProps, -} from '@remotion/transitions'; -import type { TransitionStyle } from '../lib/schema'; -import type { Palette } from '../lib/palettes'; - -type BaseProps = { - palette: Palette; -}; - -const clamp01 = (v: number): number => Math.min(1, Math.max(0, v)); - -// --------------------------------------------------------------------------- -// Motion blur: blur() + scale + opacity crossfade. (Existing aesthetic.) -// --------------------------------------------------------------------------- -const MotionBlurPresentation: React.FC< - TransitionPresentationComponentProps -> = ({ children, presentationDirection, presentationProgress }) => { - const maxBlur = 6; - const enterScale = 1.03; - const isEntering = presentationDirection === 'entering'; - - if (isEntering) { - const blur = interpolate(presentationProgress, [0, 1], [maxBlur, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const scale = interpolate(presentationProgress, [0, 1], [enterScale, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - return ( - - {children} - - ); - } - - const blur = interpolate(presentationProgress, [0, 1], [0, maxBlur], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - return ( - - {children} - - ); -}; - -// --------------------------------------------------------------------------- -// Flash: hard cut emphasised by a warm/white flash overlay at the midpoint. -// --------------------------------------------------------------------------- -const FlashPresentation: React.FC< - TransitionPresentationComponentProps -> = ({ children, presentationDirection, presentationProgress, passedProps }) => { - const isEntering = presentationDirection === 'entering'; - const palette = passedProps.palette; - const isWarm = palette.accent === '#EE6018'; - - const sceneOpacity = isEntering - ? clamp01((presentationProgress - 0.45) / 0.35) - : clamp01(1 - (presentationProgress - 0.2) / 0.35); - - // Flash is rendered only on the entering side so the peak is single-source. - const flashOpacity = isEntering - ? interpolate( - presentationProgress, - [0, 0.35, 0.55, 0.85, 1], - [0, 0.25, 1, 0.2, 0], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, - ) - : 0; - - const flashColor = isWarm - ? `rgba(255, 220, 180, ${flashOpacity})` - : `rgba(235, 240, 255, ${flashOpacity})`; - - return ( - - {children} - {flashOpacity > 0 && ( - - )} - - ); -}; - -// --------------------------------------------------------------------------- -// Whip-pan: horizontal pan + motion blur for energetic scene changes. -// --------------------------------------------------------------------------- -const WhipPanPresentation: React.FC< - TransitionPresentationComponentProps -> = ({ children, presentationDirection, presentationProgress }) => { - const isEntering = presentationDirection === 'entering'; - const easing = Easing.bezier(0.6, 0, 0.1, 1); - const eased = easing(presentationProgress); - const maxBlur = 14; - - if (isEntering) { - const translateX = interpolate(eased, [0, 1], [30, 0]); - const blur = interpolate(presentationProgress, [0, 0.6, 1], [maxBlur, 4, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const opacity = interpolate(presentationProgress, [0, 0.2, 1], [0, 1, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - return ( - - {children} - - ); - } - - const translateX = interpolate(eased, [0, 1], [0, -30]); - const blur = interpolate(presentationProgress, [0, 0.4, 1], [0, 10, maxBlur], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const opacity = interpolate(presentationProgress, [0, 0.8, 1], [1, 1, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - return ( - - {children} - - ); -}; - -// --------------------------------------------------------------------------- -// Light leak: warm moving gradient sweeps across while scenes crossfade. -// --------------------------------------------------------------------------- -const LightLeakPresentation: React.FC< - TransitionPresentationComponentProps -> = ({ children, presentationDirection, presentationProgress, passedProps }) => { - const isEntering = presentationDirection === 'entering'; - const palette = passedProps.palette; - const isWarm = palette.accent === '#EE6018'; - - const sceneOpacity = isEntering - ? interpolate(presentationProgress, [0, 0.5, 1], [0, 0.4, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }) - : interpolate(presentationProgress, [0, 0.5, 1], [1, 0.6, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - const sweepOpacity = isEntering - ? interpolate( - presentationProgress, - [0, 0.3, 0.6, 1], - [0, 1, 0.6, 0], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, - ) - : 0; - - const sweepPosition = interpolate(presentationProgress, [0, 1], [-40, 140]); - - const leakColor = isWarm ? '238, 96, 24' : '137, 180, 250'; - - return ( - - {children} - {sweepOpacity > 0 && ( - - )} - - ); -}; - -// --------------------------------------------------------------------------- -// Glitch-lite: brief RGB offset + clipped horizontal bands near midpoint. -// --------------------------------------------------------------------------- -const GlitchLitePresentation: React.FC< - TransitionPresentationComponentProps -> = ({ children, presentationDirection, presentationProgress }) => { - const isEntering = presentationDirection === 'entering'; - - const sceneOpacity = isEntering - ? interpolate(presentationProgress, [0, 0.4, 1], [0, 1, 1], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }) - : interpolate(presentationProgress, [0, 0.6, 1], [1, 1, 0], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - const glitchIntensity = interpolate( - presentationProgress, - [0, 0.35, 0.55, 0.75, 1], - [0, 1, 1, 0.3, 0], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, - ); - - const seedBase = isEntering ? 1000 : 2000; - const jitterX = (random(`glitch-x-${Math.round(presentationProgress * 60) + seedBase}`) - 0.5) * 16 * glitchIntensity; - const rgbSplit = 6 * glitchIntensity; - - const bandTop = 20 + 40 * random(`glitch-band-top-${Math.round(presentationProgress * 30) + seedBase}`); - const bandHeight = 6 + 14 * random(`glitch-band-h-${Math.round(presentationProgress * 30) + seedBase}`); - const showBand = glitchIntensity > 0.6; - - return ( - - {/* Red ghost */} - 0 ? 0.45 : 0, - }} - > - - {children} - - - {/* Blue ghost */} - 0 ? 0.45 : 0, - }} - > - - {children} - - - {/* Main scene */} - - {children} - - {/* Displaced band */} - {showBand && ( - - {children} - - )} - - ); -}; - -const presentationMap: Record< - TransitionStyle, - React.FC> -> = { - 'motion-blur': MotionBlurPresentation, - flash: FlashPresentation, - 'whip-pan': WhipPanPresentation, - 'light-leak': LightLeakPresentation, - 'glitch-lite': GlitchLitePresentation, -}; - -export const getTransitionPresentation = ( - style: TransitionStyle, - palette: Palette, -): TransitionPresentation => { - const component = presentationMap[style]; - return { - component, - props: { palette }, - }; -}; diff --git a/plugins/droid-control/remotion/src/components/SpotlightOverlay.tsx b/plugins/droid-control/remotion/src/components/SpotlightOverlay.tsx deleted file mode 100644 index b54ee55..0000000 --- a/plugins/droid-control/remotion/src/components/SpotlightOverlay.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; - -interface Region { - x: string; - y: string; - w: string; - h: string; -} - -function pctToNum(val: string): number { - return parseFloat(val.replace('%', '')) / 100; -} - -export const SpotlightOverlay: React.FC<{ - startTime: number; - duration: number; - region: Region; - dim?: number; -}> = ({ startTime, duration, region, dim = 0.6 }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const startFrame = startTime * fps; - const endFrame = (startTime + duration) * fps; - - if (frame < startFrame || frame > endFrame) return null; - - const localFrame = frame - startFrame; - const totalFrames = endFrame - startFrame; - - // Fade in over 0.3s, fade out over 0.3s - const fadeIn = interpolate(localFrame, [0, 0.3 * fps], [0, 1], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const fadeOut = interpolate( - localFrame, - [totalFrames - 0.3 * fps, totalFrames], - [1, 0], - { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - const opacity = Math.min(fadeIn, fadeOut); - - const rx = pctToNum(region.x); - const ry = pctToNum(region.y); - const rw = pctToNum(region.w); - const rh = pctToNum(region.h); - - // SVG mask: white everywhere except a black (transparent) cutout for the spotlight region. - // Use only the standard `maskImage` property (Chrome uses it in headless rendering). - const svgMask = `url("data:image/svg+xml,${encodeURIComponent( - `` + - `` + - `` + - `` - )}")`; - - return ( -
- ); -}; diff --git a/plugins/droid-control/remotion/src/components/StaggeredPanelEntrance.tsx b/plugins/droid-control/remotion/src/components/StaggeredPanelEntrance.tsx deleted file mode 100644 index 3701207..0000000 --- a/plugins/droid-control/remotion/src/components/StaggeredPanelEntrance.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; - -export const StaggeredPanelEntrance: React.FC<{ - delay?: number; - children: React.ReactNode; -}> = ({ delay = 0, children }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const startFrame = delay * fps; - const progress = interpolate( - frame, - [startFrame, startFrame + 0.5 * fps], - [0, 1], - { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - - const scale = interpolate(progress, [0, 1], [0.92, 1]); - const opacity = interpolate(progress, [0, 1], [0, 1]); - const translateY = interpolate(progress, [0, 1], [12, 0]); - - return ( -
- {children} -
- ); -}; diff --git a/plugins/droid-control/remotion/src/components/TitleCard.tsx b/plugins/droid-control/remotion/src/components/TitleCard.tsx deleted file mode 100644 index cac9bce..0000000 --- a/plugins/droid-control/remotion/src/components/TitleCard.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { - AbsoluteFill, - useCurrentFrame, - useVideoConfig, - interpolate, - Easing, -} from 'remotion'; -import { fitText } from '@remotion/layout-utils'; -import type { Palette } from '../lib/palettes'; -import { RotorMark } from './RotorMark'; -import { BreathingZoom } from './ZoomEffect'; - -const TITLE_MAX_FONT = 64; -const SUBTITLE_MAX_FONT = 24; -const TITLE_FONT_FAMILY = "'Geist', system-ui, sans-serif"; - -export const TitleCard: React.FC<{ - title: string; - subtitle: string; - palette: Palette; - speedNote?: string; -}> = ({ title, subtitle, palette, speedNote }) => { - const frame = useCurrentFrame(); - const { fps, width } = useVideoConfig(); - - const titleFontSize = Math.min( - TITLE_MAX_FONT, - fitText({ - text: title, - withinWidth: width * 0.8, - fontFamily: TITLE_FONT_FAMILY, - fontWeight: 700, - letterSpacing: '-0.04em', - }).fontSize, - ); - - const subtitleFontSize = Math.min( - SUBTITLE_MAX_FONT, - fitText({ - text: subtitle || ' ', - withinWidth: width * 0.7, - fontFamily: TITLE_FONT_FAMILY, - fontWeight: 400, - }).fontSize, - ); - - // Tagline fades in first (monospace label above title) - const taglineProgress = interpolate(frame, [0, 0.4 * fps], [0, 1], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const taglineY = interpolate(taglineProgress, [0, 1], [15, 0]); - const taglineOpacity = taglineProgress; - - // Title: scale 1.08→1.0 + translateY 30→0 + fade in - const titleProgress = interpolate(frame, [0, 0.6 * fps], [0, 1], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const titleY = interpolate(titleProgress, [0, 1], [30, 0]); - const titleScale = interpolate(titleProgress, [0, 1], [1.08, 1.0]); - const titleOpacity = titleProgress; - - // Subtitle follows with a slight delay - const subtitleProgress = interpolate( - frame, - [0.15 * fps, 0.75 * fps], - [0, 1], - { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - const subtitleY = interpolate(subtitleProgress, [0, 1], [20, 0]); - const subtitleOpacity = subtitleProgress; - - const isFactory = palette.accent === '#EE6018'; - - // --- Factory preset: spinning rotor leads the accent line --- - const SWEEP_WIDTH = 280; - const ROTOR_SIZE = 28; - const sweepStart = 0.15 * fps; - const sweepEnd = 0.7 * fps; - - // Rotor horizontal progress: 0 → SWEEP_WIDTH - const rotorProgress = interpolate(frame, [sweepStart, sweepEnd], [0, 1], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const rotorX = rotorProgress * SWEEP_WIDTH; - - // Continuous spin: ties to progress so it rolls naturally - const rotorRotation = interpolate(rotorProgress, [0, 1], [0, 720]); - - // Rotor fades in at the start, fades out after reaching the end - const rotorOpacity = interpolate( - frame, - [sweepStart, sweepStart + 0.08 * fps, sweepEnd, sweepEnd + 0.25 * fps], - [0, 1, 1, 0], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } - ); - - // Trail line grows to follow the rotor - const trailWidth = rotorX; - const trailOpacity = interpolate( - frame, - [sweepStart, sweepStart + 0.1 * fps], - [0, 1], - { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } - ); - - // --- Non-factory: original simple line animation --- - const lineWidth = interpolate(frame, [0.2 * fps, 0.7 * fps], [0, 120], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - const sweepPosition = interpolate( - frame, - [0.2 * fps, 0.2 * fps + 0.8 * fps], - [-200, 100], - { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - } - ); - - return ( - - - {/* Monospace tagline above title (shows speedNote if present) */} - {speedNote && ( -
- {speedNote} -
- )} - - {/* Title with glow, scale entrance, and accent text-shadow */} -
- {title} -
- - {/* Accent line — factory: spinning rotor with trailing line; other: gradient sweep */} - {isFactory ? ( -
- {/* Trailing line: grows from left edge toward the rotor */} -
- - {/* Spinning rotor at the leading edge */} -
- -
-
- ) : ( -
- )} - -
- {subtitle} -
- - - ); -}; diff --git a/plugins/droid-control/remotion/src/components/Watermark.tsx b/plugins/droid-control/remotion/src/components/Watermark.tsx deleted file mode 100644 index 051bdc3..0000000 --- a/plugins/droid-control/remotion/src/components/Watermark.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { - AbsoluteFill, - useCurrentFrame, - useVideoConfig, - interpolate, -} from 'remotion'; -import { RotorMark } from './RotorMark'; - -export const Watermark: React.FC<{ - opacity?: number; - size?: number; -}> = ({ opacity = 0.2, size = 48 }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const fadeIn = interpolate(frame, [0, fps], [0, opacity], { - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - - return ( - -
- -
-
- ); -}; diff --git a/plugins/droid-control/remotion/src/components/WindowChrome.tsx b/plugins/droid-control/remotion/src/components/WindowChrome.tsx deleted file mode 100644 index 597a4e5..0000000 --- a/plugins/droid-control/remotion/src/components/WindowChrome.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; -import type { Palette } from '../lib/palettes'; -import type { PresetConfig } from '../lib/presets'; - -const TrafficLight: React.FC<{ - color: string; - cx: number; -}> = ({ color, cx }) => ; - -const TrafficLightRing: React.FC<{ - color: string; - cx: number; -}> = ({ color, cx }) => ( - -); - -const WindowBar: React.FC<{ - config: PresetConfig; - palette: Palette; - width: number; - title?: string; -}> = ({ config, palette, width, title }) => { - if (config.bar === 'none') return null; - - const Dot = config.bar === 'rings' ? TrafficLightRing : TrafficLight; - const isRight = config.barSide === 'right'; - - const dots = ( - - - - - - ); - - return ( -
- {dots} - {title && ( - - {title} - - )} -
- ); -}; - -export const WindowChrome: React.FC<{ - config: PresetConfig; - palette: Palette; - width: number; - height: number; - title?: string; - /** Set to false when an ancestor already handles the entrance animation. */ - animate?: boolean; - children: React.ReactNode; -}> = ({ config, palette, width, height, title, animate = true, children }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - // Animate the window scaling in over 0.5s with a crisp ease-out. - // Skipped when `animate` is false (e.g. StaggeredPanelEntrance handles it). - const enterProgress = animate - ? interpolate(frame, [0, 0.5 * fps], [0, 1], { - easing: Easing.bezier(0.16, 1, 0.3, 1), - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }) - : 1; - - const scale = interpolate(enterProgress, [0, 1], [0.92, 1]); - const opacity = enterProgress; - - const barHeight = config.bar !== 'none' ? 36 : 0; - const contentHeight = height - barHeight; - - return ( -
- -
- {children} -
-
- ); -}; diff --git a/plugins/droid-control/remotion/src/components/WindowReflection.tsx b/plugins/droid-control/remotion/src/components/WindowReflection.tsx deleted file mode 100644 index 6517d03..0000000 --- a/plugins/droid-control/remotion/src/components/WindowReflection.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -// Uses -webkit-box-reflect to avoid rendering children twice (which would -// double Video decoding and hurt render performance). -export const WindowReflection: React.FC<{ - opacity?: number; - children: React.ReactNode; -}> = ({ opacity = 0.06, children }) => { - return ( -
- {children} -
- ); -}; diff --git a/plugins/droid-control/remotion/src/components/ZoomEffect.tsx b/plugins/droid-control/remotion/src/components/ZoomEffect.tsx deleted file mode 100644 index bda28d1..0000000 --- a/plugins/droid-control/remotion/src/components/ZoomEffect.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react'; -import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; - -interface ZoomRegion { - x: string; - y: string; - w: string; - h: string; -} - -/** Parse a percentage string (e.g. "25%") to a 0–1 fraction. */ -function pctToFraction(val: string): number { - return parseFloat(val.replace('%', '')) / 100; -} - -/** Cinematic ease-in-out for directed zoom motion. */ -const ZOOM_EASING = Easing.bezier(0.25, 0.1, 0.25, 1); - -/** - * Directed zoom to a target region. - * - * Animates from the full view (scale=1) into a zoomed view where the - * target region fills the frame, then back out. - * - * Phases (relative to duration): - * 0–30% IN — scale up to target - * 30–70% HOLD — maintain zoom - * 70–100% OUT — scale back to 1 - */ -export const ZoomEffect: React.FC<{ - startTime: number; - duration: number; - to: ZoomRegion; - children: React.ReactNode; -}> = ({ startTime, duration, to, children }) => { - const frame = useCurrentFrame(); - const { fps } = useVideoConfig(); - - const startFrame = startTime * fps; - const endFrame = (startTime + duration) * fps; - const totalFrames = endFrame - startFrame; - - // Phase boundaries (absolute frame numbers) - const inEnd = startFrame + totalFrames * 0.3; - const holdEnd = startFrame + totalFrames * 0.7; - - // Convert percentage-based region to 0–1 fractions - const xFrac = pctToFraction(to.x); - const yFrac = pctToFraction(to.y); - const wFrac = pctToFraction(to.w); - const hFrac = pctToFraction(to.h); - - const targetScale = Math.min( - 1 / Math.max(0.01, wFrac), - 1 / Math.max(0.01, hFrac) - ); - - // Anchor scaling at the center of the target region so the camera - // "pushes in" toward that point. - const originX = (xFrac + wFrac / 2) * 100; - const originY = (yFrac + hFrac / 2) * 100; - - // Determine zoom progress (0 = full view, 1 = fully zoomed) based - // on the current phase. - let zoomProgress: number; - if (frame < startFrame || frame > endFrame) { - zoomProgress = 0; - } else if (frame <= inEnd) { - // IN — ease from 0 → 1 - zoomProgress = interpolate(frame, [startFrame, inEnd], [0, 1], { - easing: ZOOM_EASING, - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - } else if (frame <= holdEnd) { - // HOLD - zoomProgress = 1; - } else { - // OUT — ease from 1 → 0 - zoomProgress = interpolate(frame, [holdEnd, endFrame], [1, 0], { - easing: ZOOM_EASING, - extrapolateLeft: 'clamp', - extrapolateRight: 'clamp', - }); - } - - const currentScale = interpolate(zoomProgress, [0, 1], [1, targetScale]); - - return ( -
- {children} -
- ); -}; - -/** - * Ambient "breathing" Ken Burns zoom. - * - * Wraps children in a slowly scaling container (1.0 → 1.04) over the - * full composition duration, giving a subtle cinematic drift. - */ -export const BreathingZoom: React.FC<{ - children: React.ReactNode; -}> = ({ children }) => { - const frame = useCurrentFrame(); - const { durationInFrames } = useVideoConfig(); - - const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.04], { - easing: Easing.bezier(0.45, 0, 0.55, 1), - extrapolateRight: 'clamp', - }); - - return ( -
- {children} -
- ); -}; diff --git a/plugins/droid-control/remotion/src/compositions/Showcase.tsx b/plugins/droid-control/remotion/src/compositions/Showcase.tsx deleted file mode 100644 index b373886..0000000 --- a/plugins/droid-control/remotion/src/compositions/Showcase.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import React, { useMemo } from 'react'; -import { z } from 'zod'; -import { AbsoluteFill, Sequence, staticFile, useVideoConfig } from 'remotion'; -import { Video } from '@remotion/media'; -import { TransitionSeries, linearTiming } from '@remotion/transitions'; -import { getTransitionPresentation } from '../components/ShowcaseTransition'; -import { - fidelitySchema, - presetSchema, - layoutSchema, - keystrokeSchema, - effectSchema, - sectionSchema, - codeAnnotationSchema, - transitionStyleSchema, -} from '../lib/schema'; -import { getPalette } from '../lib/palettes'; -import { getPresetConfig } from '../lib/presets'; -import { - TITLE_DURATION_S, - OUTRO_DURATION_S, - TRANSITION_FRAMES, - contentSequenceFrames, - playbackSpeed, -} from '../lib/duration'; -import { Background } from '../components/Background'; -import { WindowChrome } from '../components/WindowChrome'; -import { TitleCard } from '../components/TitleCard'; -import { SectionHeaderOverlay } from '../components/SectionHeader'; -import { KeystrokeOverlay } from '../components/KeystrokeOverlay'; -import { SpotlightOverlay } from '../components/SpotlightOverlay'; -import { StaggeredPanelEntrance } from '../components/StaggeredPanelEntrance'; -import { NoiseOverlay } from '../components/NoiseOverlay'; -import { FloatingParticles } from '../components/FloatingParticles'; -import { ColorGradeOverlay } from '../components/ColorGradeOverlay'; -import { Watermark } from '../components/Watermark'; -import { ZoomEffect } from '../components/ZoomEffect'; -import { SectionTransitionOverlay } from '../components/SectionTransition'; -import { DroidOutro } from '../components/DroidOutro'; -import { CodeAnnotationOverlay } from '../components/CodeAnnotationOverlay'; -import { CalloutOverlay } from '../components/CalloutOverlay'; - -export const showcaseSchema = z.object({ - clips: z.array(z.string()), - layout: layoutSchema, - labels: z.array(z.string()), - title: z.string(), - subtitle: z.string(), - preset: presetSchema, - keys: z.array(keystrokeSchema), - effects: z.array(effectSchema), - sections: z.array(sectionSchema).optional(), - width: z.number().optional(), - height: z.number().optional(), - speedNote: z.string().optional(), - windowTitle: z.string().optional(), - // Longest source clip in source seconds; render-showcase.sh probes it. - clipDuration: z.number().optional(), - // Playback multiplier applied once, here, to every clip. Overlay times - // (keys, sections, effects, codeAnnotations) are already output seconds. - speed: z.number().positive().optional(), - // Resolved by render-showcase.sh (omitted there => chosen by layout). - fidelity: fidelitySchema, - // How clip video is sized inside its panel. - // - "contain" (default): preserve aspect ratio, letterbox if needed. Safe default. - // - "cover": fill the panel, crop overflow. Use when the clip aspect doesn't match the - // panel aspect (e.g. 16:9 landscape browser capture in a near-square side-by-side panel) - // and you'd rather crop edges than see giant black bars. - // - "fill": stretch to fill the panel (distorts aspect). Rarely what you want. - objectFit: z.enum(['contain', 'cover', 'fill']).optional(), - // Timed syntax-highlighted code overlays rendered during the main content - // sequence (above content, below noise/color grade). Timings are relative - // to the start of the main content clip. - codeAnnotations: z.array(codeAnnotationSchema).optional(), - // Presentation used for title->content and content->outro transitions. - // Defaults to 'motion-blur', which preserves existing aesthetic. - transitionStyle: transitionStyleSchema.optional(), -}); - -// Effect types this composition actually renders. Anything schema-valid but -// absent from this set is a silent no-op — warn instead of dropping quietly. -const RENDERED_FX = new Set(['zoom', 'spotlight', 'callout']); -const warnedUnrenderedFx = new Set(); - -const warnUnrenderedEffects = (effects: z.infer[]) => { - for (const effect of effects) { - if (RENDERED_FX.has(effect.fx) || warnedUnrenderedFx.has(effect.fx)) { - continue; - } - warnedUnrenderedFx.add(effect.fx); - console.warn( - `Showcase: effect fx='${effect.fx}' is schema-valid but not rendered by this composition` - ); - } -}; - -const visualTreatmentByFidelity = { - compact: { noiseOpacity: 0.03, gradeIntensity: 0.04 }, - standard: { noiseOpacity: 0.02, gradeIntensity: 0.025 }, - inspect: { noiseOpacity: 0.008, gradeIntensity: 0.012 }, -} as const; - -type ObjectFit = 'contain' | 'cover' | 'fill'; - -// One playback contract for every clip: `speed` is applied here and nowhere -// else, and a clip that ends before the content sequence does not loop -- it -// holds its last decoded frame, so a shorter comparison panel stays on its -// final state. -const ClipVideo: React.FC<{ - clip: string; - speed: number; - objectFit: ObjectFit; - surface: string; -}> = ({ clip, speed, objectFit, surface }) => ( -