@koda-sl/baker-cli 0.74.0 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -8
- package/canvas/end-card-composition/index.html +66 -0
- package/canvas/end-card-composition/meta.json +19 -0
- package/canvas/feature-reveal-composition/index.html +83 -0
- package/canvas/feature-reveal-composition/meta.json +18 -0
- package/canvas/lower-third-composition/index.html +75 -0
- package/canvas/lower-third-composition/meta.json +18 -0
- package/canvas/stat-counter-composition/index.html +73 -0
- package/canvas/stat-counter-composition/meta.json +20 -0
- package/canvas/title-card-composition/index.html +90 -0
- package/canvas/title-card-composition/meta.json +20 -0
- package/canvas/video-call-composition/index.html +136 -0
- package/canvas/video-call-composition/meta.json +26 -0
- package/canvas/video-overlay-composition/index.html +39 -2
- package/dist/{chunk-JIDZ37KG.js → chunk-NBNUNCY7.js} +552 -313
- package/dist/chunk-NBNUNCY7.js.map +1 -0
- package/dist/cli.js +640 -114
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-JIDZ37KG.js.map +0 -1
package/README.md
CHANGED
|
@@ -1661,7 +1661,7 @@ baker images find "office" --sources library,magnific --fallback --threshold 0.4
|
|
|
1661
1661
|
baker images find "celebration" --sources library,giphy --auto-ingest 3
|
|
1662
1662
|
```
|
|
1663
1663
|
|
|
1664
|
-
Providers: `library`, `magnific`, `google`, `iconify`, `giphy`. Brandfetch is not part of the fanout — it takes a domain, not a query, so it lives at `baker images logo <domain>`. Response shape: `{ groups: { library, external }, ingested, meta: { counts, errors } }`. When `--auto-ingest` is used, `ingested[]` preserves successful external ingest order and matching external hits are enriched with the Baker-owned URL plus source provenance. Partial failures (one provider throws) return the successful providers plus a `meta.errors` array; the whole call never fails on a single provider error.
|
|
1664
|
+
Providers: `library`, `magnific`, `google`, `iconify`, `giphy`, `pinterest`. Brandfetch is not part of the fanout — it takes a domain, not a query, so it lives at `baker images logo <domain>`. (`pinterest` is photo-real/candid imagery — best for sourcing fresh people and sets, e.g. a recast creator or a pinned location in a video reproduction.) Response shape: `{ groups: { library, external }, ingested, meta: { counts, errors } }`. When `--auto-ingest` is used, `ingested[]` preserves successful external ingest order and matching external hits are enriched with the Baker-owned URL plus source provenance. Partial failures (one provider throws) return the successful providers plus a `meta.errors` array; the whole call never fails on a single provider error.
|
|
1665
1665
|
|
|
1666
1666
|
**Flags:**
|
|
1667
1667
|
|
|
@@ -2401,7 +2401,7 @@ A re-run with no changes hits the cache for every node — total runtime drops t
|
|
|
2401
2401
|
|
|
2402
2402
|
1. **You write a JSON file** describing nodes and how they wire together (`$ref:other_node.output`).
|
|
2403
2403
|
2. **The validator** checks the schema, every node's params against the model registry, and that every `{{slot}}` in a prompt has a wired input.
|
|
2404
|
-
3. **The engine** runs nodes in topological order. Local nodes (text, ffmpeg, imagemagick, hyperframe_*, font_specimen) execute in-process. Remote nodes (text_generate, image_generate, image_describe, image_search, video_generate, tts, music…) POST to the Convex backend.
|
|
2404
|
+
3. **The engine** runs nodes in topological order. Local nodes (text, ffmpeg, imagemagick, hyperframe_*, font_specimen) execute in-process. Remote nodes (text_generate, image_generate, image_describe, image_search, video_generate, tts, audio_voice_convert, music…) POST to the Convex backend. (`audio_voice_convert` is ElevenLabs Voice Changer / speech-to-speech — re-voices a clip's audio in one target voice while preserving timing; used to give Seedance native-audio talking heads a consistent brand voice.)
|
|
2405
2405
|
4. **Outputs land on disk** the moment each node finishes — assets are downloaded from S3 and named `<node_id>__<slot>[__<index>].<ext>` inside the run dir.
|
|
2406
2406
|
5. **The cache** is content-addressed by node params + input hashes. Edit a prompt, only the dependent nodes re-run.
|
|
2407
2407
|
|
|
@@ -2500,7 +2500,9 @@ Local ImageMagick passthrough. Identical schema and safety contract to `ffmpeg`;
|
|
|
2500
2500
|
|
|
2501
2501
|
##### `hyperframe_render`
|
|
2502
2502
|
|
|
2503
|
-
Render an HTML/CSS/GSAP composition to **mp4
|
|
2503
|
+
Render an HTML/CSS/GSAP composition to **mp4** (default), or to a transparent **webm**/**mov** alpha overlay. Quality and worker count are fixed by the engine for ad-creative delivery; the canvas owns the composition path, the output format, and the composition vars.
|
|
2504
|
+
|
|
2505
|
+
Before each render the engine runs a **pre-render quality gate** on the *substituted* composition — Hyperframes' own `lint` + `inspect`. Layout **overflow** (clipped text) and structural lint **errors block** the render; **low-contrast** text and missing brand fonts/assets only **warn** (logged as `hyperframe check warning …`). When the composition embeds another via a live `data-composition-src` (a nested catalog block), the gate additionally runs `snapshot` as a render-path smoke test — the only check that catches a broken embed (id mismatch, `<style>`/`<script>` outside the sub-comp's `<template>`, or a class-styled sub-comp root) that `lint`/`inspect`/preview all miss. If the `hyperframes` binary or its subcommands are unavailable the gate degrades gracefully and the render proceeds.
|
|
2504
2506
|
|
|
2505
2507
|
**Inputs**
|
|
2506
2508
|
|
|
@@ -2511,6 +2513,7 @@ Open record keyed by the composition's `meta.json` inputs (declared per composit
|
|
|
2511
2513
|
| Name | Type | Required | Default | Notes |
|
|
2512
2514
|
|---|---|---|---|---|
|
|
2513
2515
|
| `composition` | string | yes | — | path to composition dir |
|
|
2516
|
+
| `format` | `mp4 \| webm \| mov` | no | `mp4` | `webm`/`mov` render WITH transparency (alpha) when the composition background is transparent — for overlays you composite elsewhere |
|
|
2514
2517
|
| `timeout_ms` | number | no | 600000 | positive |
|
|
2515
2518
|
| **composition vars** | per `meta.json` | per `meta.json` | per `meta.json` | substituted into `{{var}}` |
|
|
2516
2519
|
|
|
@@ -2518,7 +2521,7 @@ Open record keyed by the composition's `meta.json` inputs (declared per composit
|
|
|
2518
2521
|
|
|
2519
2522
|
| Slot | Kind | MIME |
|
|
2520
2523
|
|---|---|---|
|
|
2521
|
-
| `video` | video | `video/mp4` |
|
|
2524
|
+
| `video` | video | `video/mp4`, `video/webm`, or `video/quicktime` (by `format`) |
|
|
2522
2525
|
|
|
2523
2526
|
See [authoring compositions](#authoring-compositions) for the composition file format.
|
|
2524
2527
|
|
|
@@ -2526,7 +2529,7 @@ See [authoring compositions](#authoring-compositions) for the composition file f
|
|
|
2526
2529
|
|
|
2527
2530
|
##### `hyperframe_snapshot`
|
|
2528
2531
|
|
|
2529
|
-
Render the same composition format to a **PNG** still at **2× device-scale** (retina). Output size = composition `meta.json` `width × height × 2`.
|
|
2532
|
+
Render the same composition format to a **PNG** still at **2× device-scale** (retina). Output size = composition `meta.json` `width × height × 2`. Runs the same pre-render `lint`/`inspect` gate as `hyperframe_render` (overflow/structural errors block; contrast/asset advisories warn).
|
|
2530
2533
|
|
|
2531
2534
|
**Inputs**
|
|
2532
2535
|
|
|
@@ -3614,15 +3617,21 @@ Turn a reference video into a **runnable, self-validated reproduction canvas** i
|
|
|
3614
3617
|
|
|
3615
3618
|
It then scaffolds the full pipeline: per scene, two **static-ad-grade frames** (`image_generate` with its **own self-contained `params.prompt`** — edit a frame node to change only that frame; `prompt.json` is wired as a demoted shared-style `target_blueprint`, a per-element reference legend, the real extracted frame as a composition anchor) → `video_generate` (Seedance first/last-frame, fed an ultra-detailed motion brief composed from the scene's action, camera, dialogue, and transcript; duration snapped to the nearest allowed clip length).
|
|
3616
3619
|
|
|
3617
|
-
**Sequenced audio.** Dialogue is a back-and-forth on one absolute timeline, so each **contiguous same-speaker turn** becomes its own `tts` placed at its real `start_s` — turns alternate and never stack (the earlier design concatenated each speaker's whole monologue at their earliest timestamp, so two voices played in parallel for the entire video). Each speaker is locked to one shared `voice_select` voice; a `sound_effect` per SFX and a `music` bed (styled after the AudD-identified track when available, ducked under the voices) round out the mix (`audio_timeline`). The final mux normalizes the soundtrack to **−14 LUFS (stereo)** so the output plays loud in every player — the raw mix is quiet mono, which reads as "no sound."
|
|
3620
|
+
**Sequenced audio.** Dialogue is a back-and-forth on one absolute timeline, so each **contiguous same-speaker turn** becomes its own `tts` placed at its real `start_s` — turns alternate and never stack (the earlier design concatenated each speaker's whole monologue at their earliest timestamp, so two voices played in parallel for the entire video). Each speaker is locked to one shared `voice_select` voice; a `sound_effect` per SFX and a `music` bed (styled after the AudD-identified track when available, ducked under the voices, and started at the reference's `music.starts_at_s` rather than always at 0) round out the mix (`audio_timeline`). The final mux normalizes the soundtrack to **−14 LUFS (stereo)** so the output plays loud in every player — the raw mix is quiet mono, which reads as "no sound."
|
|
3621
|
+
|
|
3622
|
+
**Native talking heads (no post-hoc lip-sync).** Seedance 2.0 generates lip-synced speech **natively** — a scene with a **single on-camera speaker** (a `dialogue.speaker` that maps to a `global.cast` member, when `global.voiceover.mode` isn't pure `voiceover`/`none`) puts the line in the clip's prompt with `generate_audio`, so lips and voice are generated together (no `video_lipsync`/veed). Each native clip's audio is then re-voiced through `audio_voice_convert` (ElevenLabs Voice Changer) to **one brand voice** so every scene sounds like the same person, timing preserved so the lips stay matched. Off-camera narration keeps a sequenced `tts` per turn; scenes with two on-camera speakers stay native-per-clip and are flagged for you to split or pick a primary.
|
|
3618
3623
|
|
|
3619
|
-
**
|
|
3624
|
+
**Native-audio drift guard.** Seedance paces a spoken line to fill the clip, so each native clip is generated **long enough for the estimated speech** (≈150 wpm) — not just the visual scene length — and its audio is extracted at the **full line length** rather than hard-trimmed to the scene, so a line that runs a beat long isn't cut mid-word (it continues over the next scene as natural VO). `metadata.video.talking_scenes` records each scene's `scene_s` vs `est_speech_s` so you can spot a line that overruns its cut.
|
|
3620
3625
|
|
|
3621
3626
|
**Timeline-accurate picture.** Seedance can't render under 4s, so each clip is generated at the smallest allowed duration ≥ the scene length and then **trimmed back to the exact scene duration** before concat. This keeps the concatenated picture on the same timeline as the absolute-timed audio — without it, short scenes balloon to 4s, the spine runs far longer than the soundtrack, and every line plays over the wrong (slowed) scene so the lips never match. Frames are also prompted as **clean text-free plates** (no baked captions/lower-thirds/tickers/logos-as-text) so the overlay layer is the single source of on-screen text.
|
|
3622
3627
|
|
|
3628
|
+
**Scene transitions.** When the deconstruct flags a boundary as `fade`/`whip`/`zoom`/`dissolve`/`swipe` (`scene.transition_out`), the spine reproduces it as an ffmpeg **`xfade`** instead of a hard cut; plain `cut`/`match_cut` stay hard cuts. The overlap is consumed from **extra generated footage** (each transitioning clip is trimmed to `scene_s + transition` and the xfade `offset` lands on the cumulative scene start), so the total length still equals the sum of the scene lengths — the picture stays exactly on the audio timeline.
|
|
3629
|
+
|
|
3630
|
+
**One person, multiple looks.** If a single individual plays multiple personas/wardrobes (e.g. a creator as a skeptic then a believer), the selection pass emits **one element per look** linked via `same_as` — each outfit gets its own reference slot, but the frame legend and the `el_*` TODO tell the generator they are the **same person** (keep the face identical, change only wardrobe). The always-on `metadata.todo.completeness_check` reminds you to split a person collapsed into a single slot.
|
|
3631
|
+
|
|
3623
3632
|
**Overlays are agent-painted HTML, not props.** The clips are concatenated, then the `video-overlay` composition (copied next to the canvas) composites the overlay layer. The scaffold **bakes the reference's overlays into that composition's `index.html` as real, editable HTML** (each overlay is a plain element with its text, a `.pos-*` position class, and `data-start`/`data-dur` timing); a tiny generic runtime only shows/hides each element at its timestamp (with an optional `data-anim` entrance). It makes **no styling decisions** — bars, tickers, colors, fonts, and a real logo `<img>` you drop into the dir all live in the HTML/CSS you edit. Floating elements (logo bugs) are seeded as commented `<img>` stubs so an un-edited render stays clean. Drop `brand-bold.otf`/`brand-regular.otf` for on-brand type.
|
|
3624
3633
|
|
|
3625
|
-
The emitted canvas is validated (`validateCanvasDeep`) before it's written, so it always runs. It also carries a **`metadata.video`** timing plan that `baker canvas validate` proves **statically, before any billed render**: no two voiceover turns overlap, the audio length ≈ the video length, and every single-on-camera-speaker scene
|
|
3634
|
+
The emitted canvas is validated (`validateCanvasDeep`) before it's written, so it always runs. It also carries a **`metadata.video`** timing plan that `baker canvas validate` proves **statically, before any billed render**: no two voiceover turns overlap, the audio length ≈ the video length, and every single-on-camera-speaker scene is a native talking head (its clip carries `generate_audio` and is wired to an `audio_voice_convert` node). The full editable checklist is embedded as **`metadata.todo`** (with a step-by-step guide in `metadata.description`). stdout returns `{ ok, canvas_path, prompt_path, models, stats, checklist }`.
|
|
3626
3635
|
|
|
3627
3636
|
```bash
|
|
3628
3637
|
baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
|
|
@@ -3636,6 +3645,8 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
3636
3645
|
|---|---|---|
|
|
3637
3646
|
| `--out <path>` | `<video-dir>/<name>.video.canvas.json` | Where to write the canvas (composition is copied alongside). |
|
|
3638
3647
|
| `--frames <mode>` | `generate` | `generate` regenerates frames anchored on the originals; `reuse` wires the real extracted frames straight into the clips (faithful, cheaper). |
|
|
3648
|
+
| `--ambient` | off | Give silent **b-roll** scenes native diegetic ambient (Seedance `generate_audio`), mixed deep under the music bed. Talking scenes already carry voice; check levels don't muddy the mix before keeping it. |
|
|
3649
|
+
| `--actor-sheets` | off | Lock a recast **person/animal that recurs across ≥2 scenes** to ONE multi-view turnaround (`image_reference_sheet`) that every frame grounds on — the strongest cross-scene identity lock. Costs extra credits per sheet; a fused sheet can over-polish, so eyeball it. |
|
|
3639
3650
|
| `--max-scenes <n>` | all source scenes | **Cost lever that reduces fidelity** — caps the deconstruct, MERGING away every scene beyond the cap (fewer cuts, lost beats). Prints a warning when set; omit it to reproduce every scene. |
|
|
3640
3651
|
| `--language <code>` | auto | Transcript/dialogue language hint (e.g. `fr`, `en`). |
|
|
3641
3652
|
| `--focus <text>` | — | Known provenance/emphasis to ground the deconstruct. |
|
|
@@ -3644,6 +3655,8 @@ baker canvas run ./reference-ad.video.canvas.json
|
|
|
3644
3655
|
| `--image-model <id>` | `openai/gpt-5.4-image-2` | Override the per-frame `image_generate` model (defaults to the strongest, matching `scaffold-static-ad`). |
|
|
3645
3656
|
| `--video-model <id>` | `bytedance/seedance-2.0` | Override the `video_generate` model. |
|
|
3646
3657
|
|
|
3658
|
+
Each scene is captured in a **shoot mode** — `ugc_selfie` (talking heads, the default look), `ugc_broll`, `studio_product` (pack shot), `lifestyle_cinematic`, or `screen_ui`. The scaffold derives one per scene (UGC by default; the cinematic and screen lanes are opt-in) and bakes its capture block into the frame and a camera default into the clip; override per scene with a `shoot_mode` field in `prompt.json`. Capture aesthetic + depth-of-field follow the mode (UGC stays flat; studio/lifestyle allow shallow DoF). Clips also carry **diegetic native audio** — the scene's own ambience described in the Seedance prompt, never music (the music bed is a separate, ducked track); set a scene's `ambient` field to steer it.
|
|
3659
|
+
|
|
3647
3660
|
The two scaffold passes are billed (the full `video_deconstruct` is the heavy one); **running** the result then generates many image/video/audio assets and is not free. Defaults to vertical 1080×1920 overlays — copy + edit the composition for other aspect ratios. For on-brand overlay type, drop `brand-bold.otf`/`brand-regular.otf` into the copied `video-overlay-composition/` dir (wired via `@font-face`, with a system fallback). Richer transcription (punctuated words + paragraphs) is available via the deconstruct's `transcriber: "deepgram"` param when `DEEPGRAM_API_KEY` is set.
|
|
3648
3661
|
|
|
3649
3662
|
#### `baker canvas scaffold-static-ad <image> [flags]`
|
|
@@ -4080,6 +4093,7 @@ For video rendering, use `hyperframe_render` instead — same composition format
|
|
|
4080
4093
|
- **GSAP animations.** Use `gsap.set()` for visibility transitions — `fromTo(opacity)` silently fails under Hyperframes' screenshot capture.
|
|
4081
4094
|
- **Single root timeline.** `window.__timelines["main"]` only. Sub-composition timelines aren't scrubbed.
|
|
4082
4095
|
- **Cache invalidation.** The composition directory is hashed recursively; editing any file invalidates the cache automatically.
|
|
4096
|
+
- **Pre-render gate.** Both nodes run `hyperframes lint` + `inspect` on the substituted composition before rendering. Keep content inside its container (text/container **overflow blocks** the render — shrink the font, widen the container, or mark intentional bleed with `data-layout-allow-overflow`). Use auto-resolved fonts (generic families, `Arial`) or declare `@font-face` for named/brand fonts — an undeclared named font (`Impact`, `system-ui`, `Roboto`, …) raises a font warning. Low contrast and missing assets only warn.
|
|
4083
4097
|
|
|
4084
4098
|
---
|
|
4085
4099
|
|
|
@@ -4099,6 +4113,18 @@ Non-linear vertical scroll of a tall app screenshot, driven by a GSAP timeline.
|
|
|
4099
4113
|
|
|
4100
4114
|
Output is a 720×1280 MP4 (modern phone aspect) intended to feed straight into an `ffmpeg` chromakey filter graph as the `app` overlay.
|
|
4101
4115
|
|
|
4116
|
+
#### Reusable motion-graphic "moves"
|
|
4117
|
+
|
|
4118
|
+
A library of proven, parameterized motion graphics ships under `packages/cli/canvas/` so agents reuse them instead of authoring HTML/GSAP from scratch. Each is 1080×1920, `lint`/`inspect`-clean, and exposes a `@font-face` hook for `brand-bold.otf` / `brand-regular.otf`:
|
|
4119
|
+
|
|
4120
|
+
- **`title-card-composition`** — kinetic kicker + headline (word-by-word rise) + subhead on a gradient.
|
|
4121
|
+
- **`lower-third-composition`** — animated name/title bar that wipes in, holds, slides out (transparent bg).
|
|
4122
|
+
- **`stat-counter-composition`** — a number that counts up to a value with prefix/suffix + label (transparent bg).
|
|
4123
|
+
- **`feature-reveal-composition`** — a check-marked feature list that staggers in (transparent bg).
|
|
4124
|
+
- **`end-card-composition`** — logo (inline SVG) + closing line + CTA pill.
|
|
4125
|
+
|
|
4126
|
+
Render a still with `hyperframe_snapshot` or an animated clip with `hyperframe_render`; the transparent-bg moves render to an alpha overlay with `format: "webm"`.
|
|
4127
|
+
|
|
4102
4128
|
---
|
|
4103
4129
|
|
|
4104
4130
|
### Programmatic use
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="./gsap.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: 'BrandFont';
|
|
10
|
+
src: url('./brand-bold.otf') format('opentype'), url('./brand-bold.ttf') format('truetype');
|
|
11
|
+
font-weight: 700 900;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
}
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: 'BrandFont';
|
|
16
|
+
src: url('./brand-regular.otf') format('opentype'), url('./brand-regular.ttf') format('truetype');
|
|
17
|
+
font-weight: 100 600;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; }
|
|
22
|
+
#root {
|
|
23
|
+
position: absolute; inset: 0; background: {{bg_color}};
|
|
24
|
+
font-family: 'BrandFont', Arial, sans-serif;
|
|
25
|
+
color: {{text_color}};
|
|
26
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 56px; padding: 120px;
|
|
27
|
+
text-align: center;
|
|
28
|
+
}
|
|
29
|
+
.logo { width: 360px; max-height: 200px; opacity: 0; transform: translateY(20px); }
|
|
30
|
+
.logo svg { width: 100%; height: auto; display: block; }
|
|
31
|
+
.headline { font-size: 84px; font-weight: 900; line-height: 1.08; letter-spacing: -1px; max-width: 860px; text-wrap: balance; opacity: 0; transform: translateY(24px); }
|
|
32
|
+
.cta {
|
|
33
|
+
font-size: 46px; font-weight: 700; padding: 32px 64px; border-radius: 999px;
|
|
34
|
+
background: {{cta_bg}}; color: {{cta_text_color}}; opacity: 0; transform: scale(0.8);
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
40
|
+
<div class="logo" id="logo">{{logo_svg}}</div>
|
|
41
|
+
<h1 class="headline" id="headline">{{headline}}</h1>
|
|
42
|
+
<div class="cta" id="cta">{{cta_text}}</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
(() => {
|
|
47
|
+
var DURATION = parseFloat('{{duration}}') || 3;
|
|
48
|
+
var logoEl = document.getElementById('logo');
|
|
49
|
+
var ctaEl = document.getElementById('cta');
|
|
50
|
+
var hasLogo = (logoEl.innerHTML || '').trim().length > 0;
|
|
51
|
+
var hasCta = ((ctaEl.textContent) || '').trim().length > 0;
|
|
52
|
+
if (!hasLogo) logoEl.style.display = 'none';
|
|
53
|
+
if (!hasCta) ctaEl.style.display = 'none';
|
|
54
|
+
|
|
55
|
+
var tl = gsap.timeline({ paused: true });
|
|
56
|
+
if (hasLogo) tl.to('#logo', { opacity: 1, y: 0, duration: 0.5, ease: 'power3.out' }, 0.1);
|
|
57
|
+
tl.to('#headline', { opacity: 1, y: 0, duration: 0.5, ease: 'power3.out' }, 0.3);
|
|
58
|
+
if (hasCta) tl.to('#cta', { opacity: 1, scale: 1, duration: 0.45, ease: 'back.out(1.7)' }, 0.6);
|
|
59
|
+
tl.to({}, { duration: 0.01 }, Math.max(0.02, DURATION - 0.01));
|
|
60
|
+
|
|
61
|
+
window.__timelines = window.__timelines || {};
|
|
62
|
+
window.__timelines.main = tl;
|
|
63
|
+
})();
|
|
64
|
+
</script>
|
|
65
|
+
</body>
|
|
66
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "end-card",
|
|
3
|
+
"title": "End card — logo, closing line, CTA pill",
|
|
4
|
+
"description": "A closing/pack-shot card: optional inline-SVG wordmark, a closing headline, and a call-to-action pill that pops in. Renders a solid background, so it works as a final clip (mp4) or, with format=webm/mov, a transparent end band over a product shot. Drop brand-bold.otf / brand-regular.otf in this dir for on-brand type.",
|
|
5
|
+
"width": 1080,
|
|
6
|
+
"height": 1920,
|
|
7
|
+
"fps": 30,
|
|
8
|
+
"default_duration": 3,
|
|
9
|
+
"inputs": {},
|
|
10
|
+
"params": {
|
|
11
|
+
"logo_svg": { "kind": "string", "default": "", "description": "Raw inline <svg>…</svg> wordmark with fill set to text_color (optional)." },
|
|
12
|
+
"headline": { "kind": "string", "required": true, "description": "Closing line." },
|
|
13
|
+
"cta_text": { "kind": "string", "default": "", "description": "Call-to-action pill label (optional)." },
|
|
14
|
+
"bg_color": { "kind": "color", "default": "#0b1020" },
|
|
15
|
+
"text_color": { "kind": "color", "default": "#ffffff" },
|
|
16
|
+
"cta_bg": { "kind": "color", "default": "#5b8cff" },
|
|
17
|
+
"cta_text_color": { "kind": "color", "default": "#0b1020" }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="./gsap.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: 'BrandFont';
|
|
10
|
+
src: url('./brand-bold.otf') format('opentype'), url('./brand-bold.ttf') format('truetype');
|
|
11
|
+
font-weight: 700 900;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
}
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: 'BrandFont';
|
|
16
|
+
src: url('./brand-regular.otf') format('opentype'), url('./brand-regular.ttf') format('truetype');
|
|
17
|
+
font-weight: 100 600;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: transparent; }
|
|
22
|
+
#root {
|
|
23
|
+
position: absolute; inset: 0; background: transparent;
|
|
24
|
+
font-family: 'BrandFont', Arial, sans-serif;
|
|
25
|
+
color: {{text_color}};
|
|
26
|
+
display: flex; flex-direction: column; justify-content: center; padding: 130px 110px;
|
|
27
|
+
}
|
|
28
|
+
.title { font-size: 72px; font-weight: 900; line-height: 1.08; margin-bottom: 56px; text-wrap: balance; opacity: 0; transform: translateY(24px); }
|
|
29
|
+
.list { display: flex; flex-direction: column; gap: 36px; }
|
|
30
|
+
.item { display: flex; align-items: center; gap: 32px; opacity: 0; transform: translateX(-40px); }
|
|
31
|
+
.circle {
|
|
32
|
+
flex: 0 0 auto; width: 72px; height: 72px; border-radius: 50%;
|
|
33
|
+
background: {{accent_color}}; display: flex; align-items: center; justify-content: center;
|
|
34
|
+
}
|
|
35
|
+
.circle svg { width: 38px; height: 38px; }
|
|
36
|
+
.item .text { font-size: 52px; font-weight: 600; line-height: 1.15; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
41
|
+
<h2 class="title" id="title">{{title}}</h2>
|
|
42
|
+
<div class="list" id="list"></div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
(() => {
|
|
47
|
+
var DURATION = parseFloat('{{duration}}') || 5;
|
|
48
|
+
var STAGGER = parseFloat('{{stagger_s}}') || 0.18;
|
|
49
|
+
var features = {{features}};
|
|
50
|
+
var CHECK = '{{check_color}}';
|
|
51
|
+
|
|
52
|
+
var hasTitle = ((document.getElementById('title').textContent) || '').trim().length > 0;
|
|
53
|
+
if (!hasTitle) document.getElementById('title').style.display = 'none';
|
|
54
|
+
|
|
55
|
+
var list = document.getElementById('list');
|
|
56
|
+
(features || []).forEach(function (label) {
|
|
57
|
+
var item = document.createElement('div');
|
|
58
|
+
item.className = 'item';
|
|
59
|
+
var circle = document.createElement('div');
|
|
60
|
+
circle.className = 'circle';
|
|
61
|
+
circle.innerHTML = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 12.5l4.5 4.5L19 7" stroke="' + CHECK + '" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
62
|
+
var text = document.createElement('div');
|
|
63
|
+
text.className = 'text';
|
|
64
|
+
text.textContent = String(label);
|
|
65
|
+
item.appendChild(circle);
|
|
66
|
+
item.appendChild(text);
|
|
67
|
+
list.appendChild(item);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
var items = Array.prototype.slice.call(list.querySelectorAll('.item'));
|
|
71
|
+
var tl = gsap.timeline({ paused: true });
|
|
72
|
+
if (hasTitle) tl.to('#title', { opacity: 1, y: 0, duration: 0.45, ease: 'power3.out' }, 0.1);
|
|
73
|
+
if (items.length) {
|
|
74
|
+
tl.to(items, { opacity: 1, x: 0, duration: 0.45, stagger: STAGGER, ease: 'power3.out' }, hasTitle ? 0.4 : 0.15);
|
|
75
|
+
}
|
|
76
|
+
tl.to({}, { duration: 0.01 }, Math.max(0.02, DURATION - 0.01));
|
|
77
|
+
|
|
78
|
+
window.__timelines = window.__timelines || {};
|
|
79
|
+
window.__timelines.main = tl;
|
|
80
|
+
})();
|
|
81
|
+
</script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "feature-reveal",
|
|
3
|
+
"title": "Feature reveal — checklist items animate in staggered",
|
|
4
|
+
"description": "An optional title plus a list of check-marked feature lines that stagger in one by one, then hold. Transparent background — render with format=webm/mov for an alpha overlay over footage, or format=mp4 over a color. Drop brand-bold.otf / brand-regular.otf in this dir for on-brand type.",
|
|
5
|
+
"width": 1080,
|
|
6
|
+
"height": 1920,
|
|
7
|
+
"fps": 30,
|
|
8
|
+
"default_duration": 5,
|
|
9
|
+
"inputs": {},
|
|
10
|
+
"params": {
|
|
11
|
+
"title": { "kind": "string", "default": "", "description": "Optional heading above the list." },
|
|
12
|
+
"features": { "kind": "json", "required": true, "description": "Array of 1–6 short labels, e.g. [\"No setup fees\", \"Cancel anytime\"]." },
|
|
13
|
+
"text_color": { "kind": "color", "default": "#0b1020" },
|
|
14
|
+
"accent_color": { "kind": "color", "default": "#1f9d57", "description": "Check-circle fill." },
|
|
15
|
+
"check_color": { "kind": "color", "default": "#ffffff", "description": "The check mark inside the circle." },
|
|
16
|
+
"stagger_s": { "kind": "number", "default": 0.18, "min": 0.02, "max": 1, "description": "Seconds between each item." }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="./gsap.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: 'BrandFont';
|
|
10
|
+
src: url('./brand-bold.otf') format('opentype'), url('./brand-bold.ttf') format('truetype');
|
|
11
|
+
font-weight: 700 900;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
}
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: 'BrandFont';
|
|
16
|
+
src: url('./brand-regular.otf') format('opentype'), url('./brand-regular.ttf') format('truetype');
|
|
17
|
+
font-weight: 100 600;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: transparent; }
|
|
22
|
+
#root {
|
|
23
|
+
position: absolute; inset: 0; background: transparent;
|
|
24
|
+
font-family: 'BrandFont', Arial, sans-serif;
|
|
25
|
+
}
|
|
26
|
+
.l3 {
|
|
27
|
+
position: absolute; left: 64px; bottom: {{from_bottom_px}}px;
|
|
28
|
+
display: flex; align-items: stretch; max-width: 900px;
|
|
29
|
+
filter: drop-shadow(0 12px 30px rgba(0,0,0,0.35));
|
|
30
|
+
}
|
|
31
|
+
.accent { width: 16px; background: {{accent_color}}; border-radius: 4px 0 0 4px; transform: scaleY(0); transform-origin: bottom; }
|
|
32
|
+
.panel {
|
|
33
|
+
background: {{bar_color}}; color: {{text_color}};
|
|
34
|
+
padding: 30px 44px; border-radius: 0 8px 8px 0; overflow: hidden;
|
|
35
|
+
clip-path: inset(0 100% 0 0);
|
|
36
|
+
}
|
|
37
|
+
.name { font-size: 60px; font-weight: 800; line-height: 1.05; white-space: nowrap; }
|
|
38
|
+
.title { font-size: 34px; font-weight: 500; opacity: 0.85; margin-top: 8px; white-space: nowrap; }
|
|
39
|
+
.name, .title { transform: translateY(20px); opacity: 0; }
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
44
|
+
<div class="l3" id="l3">
|
|
45
|
+
<div class="accent" id="accent"></div>
|
|
46
|
+
<div class="panel" id="panel">
|
|
47
|
+
<div class="name" id="name">{{name}}</div>
|
|
48
|
+
<div class="title" id="title">{{title}}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
(() => {
|
|
55
|
+
var DURATION = parseFloat('{{duration}}') || 4;
|
|
56
|
+
var hasTitle = ((document.getElementById('title').textContent) || '').trim().length > 0;
|
|
57
|
+
if (!hasTitle) document.getElementById('title').style.display = 'none';
|
|
58
|
+
|
|
59
|
+
var tl = gsap.timeline({ paused: true });
|
|
60
|
+
// IN
|
|
61
|
+
tl.to('#accent', { scaleY: 1, duration: 0.35, ease: 'power3.out' }, 0.1);
|
|
62
|
+
tl.to('#panel', { clipPath: 'inset(0 0% 0 0)', duration: 0.45, ease: 'power3.out' }, 0.25);
|
|
63
|
+
tl.to('#name', { y: 0, opacity: 1, duration: 0.4, ease: 'power2.out' }, 0.45);
|
|
64
|
+
if (hasTitle) tl.to('#title', { y: 0, opacity: 0.85, duration: 0.4, ease: 'power2.out' }, 0.55);
|
|
65
|
+
// OUT — slide down + fade in the last 0.5s
|
|
66
|
+
var outAt = Math.max(1.0, DURATION - 0.5);
|
|
67
|
+
tl.to('#l3', { y: 60, opacity: 0, duration: 0.4, ease: 'power2.in' }, outAt);
|
|
68
|
+
tl.to({}, { duration: 0.01 }, Math.max(0.02, DURATION - 0.01));
|
|
69
|
+
|
|
70
|
+
window.__timelines = window.__timelines || {};
|
|
71
|
+
window.__timelines.main = tl;
|
|
72
|
+
})();
|
|
73
|
+
</script>
|
|
74
|
+
</body>
|
|
75
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "lower-third",
|
|
3
|
+
"title": "Lower-third name bar — animated name + title",
|
|
4
|
+
"description": "A transparent name/title bar that wipes in, holds, and slides out. Renders on a transparent background, so render with format=webm (or mov) to get an alpha overlay you composite over footage in the ffmpeg node or an editor, or paste it into another composition. Drop brand-bold.otf / brand-regular.otf in this dir for on-brand type.",
|
|
5
|
+
"width": 1080,
|
|
6
|
+
"height": 1920,
|
|
7
|
+
"fps": 30,
|
|
8
|
+
"default_duration": 4,
|
|
9
|
+
"inputs": {},
|
|
10
|
+
"params": {
|
|
11
|
+
"name": { "kind": "string", "required": true, "description": "Primary line (person/brand name)." },
|
|
12
|
+
"title": { "kind": "string", "default": "", "description": "Secondary line (role/handle)." },
|
|
13
|
+
"bar_color": { "kind": "color", "default": "#0b1020" },
|
|
14
|
+
"text_color": { "kind": "color", "default": "#ffffff" },
|
|
15
|
+
"accent_color": { "kind": "color", "default": "#5b8cff", "description": "The leading accent block." },
|
|
16
|
+
"from_bottom_px": { "kind": "integer", "default": 220, "min": 0, "max": 1600, "description": "Distance of the bar from the bottom edge." }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="./gsap.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: 'BrandFont';
|
|
10
|
+
src: url('./brand-bold.otf') format('opentype'), url('./brand-bold.ttf') format('truetype');
|
|
11
|
+
font-weight: 700 900;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
}
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: 'BrandFont';
|
|
16
|
+
src: url('./brand-regular.otf') format('opentype'), url('./brand-regular.ttf') format('truetype');
|
|
17
|
+
font-weight: 100 600;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
}
|
|
20
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; background: transparent; }
|
|
22
|
+
#root {
|
|
23
|
+
position: absolute; inset: 0; background: transparent;
|
|
24
|
+
font-family: 'BrandFont', Arial, sans-serif;
|
|
25
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80px;
|
|
26
|
+
}
|
|
27
|
+
.value {
|
|
28
|
+
font-size: 240px; font-weight: 900; line-height: 1; letter-spacing: -4px; color: {{value_color}};
|
|
29
|
+
font-variant-numeric: tabular-nums; text-align: center; transform: scale(0.9); opacity: 0;
|
|
30
|
+
}
|
|
31
|
+
.label { font-size: 48px; font-weight: 500; color: {{label_color}}; margin-top: 36px; text-align: center; max-width: 820px; text-wrap: balance; opacity: 0; }
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div id="root" data-composition-id="main" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
36
|
+
<div class="value" id="value">{{prefix}}0{{suffix}}</div>
|
|
37
|
+
<div class="label" id="label">{{label}}</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
(() => {
|
|
42
|
+
var DURATION = parseFloat('{{duration}}') || 3;
|
|
43
|
+
var TARGET = parseFloat('{{value}}') || 0;
|
|
44
|
+
var DECIMALS = parseInt('{{decimals}}', 10) || 0;
|
|
45
|
+
var COUNT_S = parseFloat('{{count_s}}') || 1.2;
|
|
46
|
+
var PREFIX = '{{prefix}}';
|
|
47
|
+
var SUFFIX = '{{suffix}}';
|
|
48
|
+
var valueEl = document.getElementById('value');
|
|
49
|
+
var hasLabel = ((document.getElementById('label').textContent) || '').trim().length > 0;
|
|
50
|
+
if (!hasLabel) document.getElementById('label').style.display = 'none';
|
|
51
|
+
|
|
52
|
+
function render(n) {
|
|
53
|
+
var fixed = Number(n).toFixed(DECIMALS);
|
|
54
|
+
// group thousands for readability
|
|
55
|
+
var parts = fixed.split('.');
|
|
56
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
57
|
+
valueEl.textContent = PREFIX + parts.join('.') + SUFFIX;
|
|
58
|
+
}
|
|
59
|
+
render(0);
|
|
60
|
+
|
|
61
|
+
var counter = { n: 0 };
|
|
62
|
+
var tl = gsap.timeline({ paused: true });
|
|
63
|
+
tl.to('#value', { scale: 1, opacity: 1, duration: 0.4, ease: 'back.out(1.6)' }, 0.1);
|
|
64
|
+
tl.to(counter, { n: TARGET, duration: COUNT_S, ease: 'power1.out', onUpdate: function () { render(counter.n); } }, 0.15);
|
|
65
|
+
if (hasLabel) tl.to('#label', { opacity: 1, duration: 0.4, ease: 'power2.out' }, 0.2 + COUNT_S * 0.5);
|
|
66
|
+
tl.to({}, { duration: 0.01 }, Math.max(0.02, DURATION - 0.01));
|
|
67
|
+
|
|
68
|
+
window.__timelines = window.__timelines || {};
|
|
69
|
+
window.__timelines.main = tl;
|
|
70
|
+
})();
|
|
71
|
+
</script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "stat-counter",
|
|
3
|
+
"title": "Animated stat counter — number counts up to a value",
|
|
4
|
+
"description": "A big number that counts up from zero to a target, with a prefix/suffix and a label underneath. Transparent background — render with format=webm/mov for an alpha overlay, or format=mp4 over a color. Good for data beats (\"$2.4M raised\", \"98% retention\"). Drop brand-bold.otf / brand-regular.otf in this dir for on-brand type.",
|
|
5
|
+
"width": 1080,
|
|
6
|
+
"height": 1920,
|
|
7
|
+
"fps": 30,
|
|
8
|
+
"default_duration": 3,
|
|
9
|
+
"inputs": {},
|
|
10
|
+
"params": {
|
|
11
|
+
"value": { "kind": "number", "required": true, "description": "Target number to count up to." },
|
|
12
|
+
"decimals": { "kind": "integer", "default": 0, "min": 0, "max": 4, "description": "Decimal places to display." },
|
|
13
|
+
"prefix": { "kind": "string", "default": "", "description": "e.g. \"$\"." },
|
|
14
|
+
"suffix": { "kind": "string", "default": "", "description": "e.g. \"%\", \"M\", \"x\"." },
|
|
15
|
+
"label": { "kind": "string", "default": "", "description": "Caption under the number." },
|
|
16
|
+
"value_color": { "kind": "color", "default": "#ffffff" },
|
|
17
|
+
"label_color": { "kind": "color", "default": "#9fb3d1" },
|
|
18
|
+
"count_s": { "kind": "number", "default": 1.2, "min": 0.2, "max": 6, "description": "Seconds spent counting up." }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=1080, height=1920" />
|
|
6
|
+
<script src="./gsap.min.js"></script>
|
|
7
|
+
<style>
|
|
8
|
+
/* Drop brand-bold.otf / brand-regular.otf in this dir for on-brand type;
|
|
9
|
+
falls back cleanly to a system stack if absent. */
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: 'BrandFont';
|
|
12
|
+
src: url('./brand-bold.otf') format('opentype'), url('./brand-bold.ttf') format('truetype');
|
|
13
|
+
font-weight: 700 900;
|
|
14
|
+
font-display: swap;
|
|
15
|
+
}
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: 'BrandFont';
|
|
18
|
+
src: url('./brand-regular.otf') format('opentype'), url('./brand-regular.ttf') format('truetype');
|
|
19
|
+
font-weight: 100 600;
|
|
20
|
+
font-display: swap;
|
|
21
|
+
}
|
|
22
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
23
|
+
html, body { width: 1080px; height: 1920px; overflow: hidden; }
|
|
24
|
+
#root {
|
|
25
|
+
position: absolute; inset: 0;
|
|
26
|
+
background: linear-gradient(160deg, {{bg_from}} 0%, {{bg_to}} 100%);
|
|
27
|
+
font-family: 'BrandFont', Arial, sans-serif;
|
|
28
|
+
color: {{text_color}};
|
|
29
|
+
display: flex; flex-direction: column; justify-content: center;
|
|
30
|
+
padding: 140px 110px;
|
|
31
|
+
text-align: {{align}};
|
|
32
|
+
}
|
|
33
|
+
#root.center { align-items: center; }
|
|
34
|
+
#root.left { align-items: flex-start; }
|
|
35
|
+
.kicker {
|
|
36
|
+
font-size: 38px; font-weight: 600; letter-spacing: 6px; text-transform: uppercase;
|
|
37
|
+
color: {{accent_color}}; opacity: 0; margin-bottom: 28px;
|
|
38
|
+
}
|
|
39
|
+
.rule { height: 6px; width: 160px; background: {{accent_color}}; border-radius: 3px; transform: scaleX(0); transform-origin: left center; margin-bottom: 40px; }
|
|
40
|
+
#root.center .rule { transform-origin: center; }
|
|
41
|
+
.headline { font-size: 132px; font-weight: 900; line-height: 1.1; letter-spacing: -1px; text-wrap: balance; }
|
|
42
|
+
.headline .word { display: inline-block; }
|
|
43
|
+
.headline .word > span { display: inline-block; transform: translateY(40px); opacity: 0; }
|
|
44
|
+
.subhead { font-size: 44px; font-weight: 500; line-height: 1.3; opacity: 0.92; margin-top: 40px; max-width: 820px; text-wrap: balance; opacity: 0; }
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<div id="root" class="{{align}}" data-composition-id="main" data-start="0" data-duration="{{duration}}" data-width="1080" data-height="1920">
|
|
49
|
+
<div class="kicker" id="kicker">{{kicker}}</div>
|
|
50
|
+
<div class="rule" id="rule"></div>
|
|
51
|
+
<h1 class="headline" id="headline">{{headline}}</h1>
|
|
52
|
+
<div class="subhead" id="subhead">{{subhead}}</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<script>
|
|
56
|
+
(() => {
|
|
57
|
+
var DURATION = parseFloat('{{duration}}') || 4;
|
|
58
|
+
|
|
59
|
+
// Wrap each headline word so it can rise/fade in independently.
|
|
60
|
+
var h = document.getElementById('headline');
|
|
61
|
+
var words = (h.textContent || '').trim().split(/\s+/).filter(Boolean);
|
|
62
|
+
h.textContent = '';
|
|
63
|
+
var spans = words.map(function (w, i) {
|
|
64
|
+
var word = document.createElement('span');
|
|
65
|
+
word.className = 'word';
|
|
66
|
+
var inner = document.createElement('span');
|
|
67
|
+
inner.textContent = w;
|
|
68
|
+
word.appendChild(inner);
|
|
69
|
+
h.appendChild(word);
|
|
70
|
+
if (i < words.length - 1) h.appendChild(document.createTextNode(' '));
|
|
71
|
+
return inner;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
var hasKicker = ((document.getElementById('kicker').textContent) || '').trim().length > 0;
|
|
75
|
+
var hasSub = ((document.getElementById('subhead').textContent) || '').trim().length > 0;
|
|
76
|
+
|
|
77
|
+
var tl = gsap.timeline({ paused: true });
|
|
78
|
+
if (hasKicker) tl.to('#kicker', { opacity: 1, duration: 0.4, ease: 'power2.out' }, 0.1);
|
|
79
|
+
tl.to('#rule', { scaleX: 1, duration: 0.5, ease: 'power3.out' }, 0.25);
|
|
80
|
+
tl.to(spans, { y: 0, opacity: 1, duration: 0.6, stagger: 0.08, ease: 'power3.out' }, 0.35);
|
|
81
|
+
if (hasSub) tl.to('#subhead', { opacity: 0.92, duration: 0.5, ease: 'power2.out' }, '>-0.1');
|
|
82
|
+
// Pad the timeline to the full duration so the card holds on screen.
|
|
83
|
+
tl.to({}, { duration: 0.01 }, Math.max(0.02, DURATION - 0.01));
|
|
84
|
+
|
|
85
|
+
window.__timelines = window.__timelines || {};
|
|
86
|
+
window.__timelines.main = tl;
|
|
87
|
+
})();
|
|
88
|
+
</script>
|
|
89
|
+
</body>
|
|
90
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "title-card",
|
|
3
|
+
"title": "Kinetic title card — kicker + headline + subhead on a gradient",
|
|
4
|
+
"description": "A standalone opening/section card. Headline masks up word-by-word, kicker and accent rule wipe in, subhead fades. No inputs — renders a solid gradient background, so it works as an intro clip (mp4) or, with format=webm/mov, a transparent lower band you composite over footage. Drop brand-bold.otf / brand-regular.otf in this dir for on-brand type.",
|
|
5
|
+
"width": 1080,
|
|
6
|
+
"height": 1920,
|
|
7
|
+
"fps": 30,
|
|
8
|
+
"default_duration": 4,
|
|
9
|
+
"inputs": {},
|
|
10
|
+
"params": {
|
|
11
|
+
"kicker": { "kind": "string", "default": "", "description": "Small label above the headline (optional)." },
|
|
12
|
+
"headline": { "kind": "string", "required": true, "description": "The main line. Word-by-word mask reveal." },
|
|
13
|
+
"subhead": { "kind": "string", "default": "", "description": "Supporting line below (optional)." },
|
|
14
|
+
"bg_from": { "kind": "color", "default": "#0b1020", "description": "Gradient start." },
|
|
15
|
+
"bg_to": { "kind": "color", "default": "#1b2a4a", "description": "Gradient end." },
|
|
16
|
+
"text_color": { "kind": "color", "default": "#ffffff" },
|
|
17
|
+
"accent_color": { "kind": "color", "default": "#5b8cff", "description": "Kicker + accent rule." },
|
|
18
|
+
"align": { "kind": "string", "default": "center", "enum": ["left", "center"], "description": "Text alignment." }
|
|
19
|
+
}
|
|
20
|
+
}
|