Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aecd0471fa | ||
|
|
48f0af927a | ||
|
|
b73b057862 | ||
|
|
1b2c8439f0 |
No files matched your search
@@ -1,24 +0,0 @@
|
||||
# Caption Animation
|
||||
|
||||
Turn a transcript or voiceover into word-timed, scroll-stopping captions: each word pops in on the syllable, the active word highlights, and the text stays glued to the narration. Build the kind of captions that drive watch-time on TikTok, Reels, and Shorts — readable, on-beat, and inside the safe area. For short-form creators burning open captions onto vertical video.
|
||||
|
||||
## When it activates
|
||||
|
||||
- "Add animated captions," "make TikTok captions," or "karaoke captions."
|
||||
- "Word-by-word subtitles," "auto subtitles," or "highlight the active word."
|
||||
- "Burn in captions to a video" or "sync captions to a voiceover/narration."
|
||||
- "Turn an SRT/transcript into animated text."
|
||||
|
||||
## Example prompts
|
||||
|
||||
- "Turn this Whisper transcript into karaoke captions with a per-word pop."
|
||||
- "Restyle these auto subtitles into a branded, Hormozi-style reveal."
|
||||
- "Burn word-level captions onto my voiceover and export a sidecar SRT."
|
||||
|
||||
## What's inside
|
||||
|
||||
- `SKILL.md` — the transcribe → normalize → page → animate → place → export pipeline.
|
||||
- `references/word-timed-captions.md` — deeper guidance on word-level timing and animation.
|
||||
|
||||
---
|
||||
Part of **[Short-Form Video Skills](../../)** · Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=tiktok-video-skills&utm_content=skill_footer&utm_term=caption-animation)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
name: caption-animation
|
||||
description: This skill should be used when the user asks to "add animated captions", "make TikTok captions", "karaoke captions", "word-by-word subtitles", "auto subtitles", "highlight the active word", "burn in captions to a video", "sync captions to a voiceover/narration", or "turn an SRT/transcript into animated text". Covers word-level timing from Whisper, per-word pop/scale-in, active-word highlight, 9:16 safe-area placement, readable type, and burn-in vs sidecar SRT/VTT.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Caption Animation
|
||||
|
||||
Turn a transcript or voiceover into word-timed, scroll-stopping captions: each word pops in on the syllable, the active word highlights, and the text stays glued to the narration. Build the kind of captions that drive watch-time on TikTok, Reels, and Shorts — readable, on-beat, and inside the safe area.
|
||||
|
||||
## When to use
|
||||
|
||||
- Word-by-word or karaoke captions on short-form vertical video (9:16).
|
||||
- Burning open captions onto a voiceover/narration track from a transcript.
|
||||
- Turning a Whisper/SRT transcript into animated text in Remotion or on the web.
|
||||
- Restyling auto-generated subtitles into a branded, "Hormozi-style" reveal.
|
||||
|
||||
## The pipeline
|
||||
|
||||
| Stage | Job | Tool |
|
||||
|---|---|---|
|
||||
| Transcribe | Audio → word-level timestamps | Whisper (`@remotion/install-whisper-cpp`), AssemblyAI |
|
||||
| Normalize | → `{ text, startMs, endMs }[]` tokens | `@remotion/captions` `Caption` type |
|
||||
| Page | Group words into 1–4 word "pages" | `createTikTokStyleCaptions()` |
|
||||
| Animate | Per-word pop + active-word highlight | Remotion `spring()` / CSS |
|
||||
| Place | Safe-area, readable type | layout rules below |
|
||||
| Export | Burn-in (MP4) and/or sidecar SRT/VTT | Remotion render / file emit |
|
||||
|
||||
## Two non-negotiable rules
|
||||
|
||||
1. **Word-level timing, not line-level.** Karaoke reads as magic only when each word lands on the syllable. Always transcribe to word timestamps; never fake them by splitting a line evenly over its duration — drift is instantly visible.
|
||||
2. **Readability beats style.** Captions are read on a phone, in sunlight, muted. Bold sans-serif, heavy stroke or shadow, high contrast first; decoration second. A caption nobody can read is decoration, not a caption.
|
||||
|
||||
## Word timing from a transcript
|
||||
|
||||
Whisper emits word-level timestamps directly. Normalize every source into one flat token shape so the renderer never cares where the words came from:
|
||||
|
||||
```ts
|
||||
// Caption token — the one shape everything downstream consumes
|
||||
type Token = { text: string; startMs: number; endMs: number };
|
||||
|
||||
// Parse an SRT cue block "00:00:01,200 --> 00:00:01,640" into ms
|
||||
const toMs = (t: string) => {
|
||||
const [h, m, rest] = t.split(":");
|
||||
const [s, ms] = rest.split(",");
|
||||
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
|
||||
};
|
||||
```
|
||||
|
||||
If only sentence-level cues exist, re-transcribe for word timing — do not interpolate. Whisper's `medium.en` is ~2x faster than `large` and accurate enough for clean voice audio.
|
||||
|
||||
## Per-word pop-in (Remotion)
|
||||
|
||||
Drive each word's scale and opacity off a spring anchored to its own `startMs`. The micro-overshoot is what makes it feel "alive".
|
||||
|
||||
```tsx
|
||||
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
|
||||
|
||||
const Word: React.FC<{ token: Token; active: boolean }> = ({ token, active }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const enter = (token.startMs / 1000) * fps; // word's own entrance frame
|
||||
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
|
||||
const scale = interpolate(p, [0, 1], [0.6, 1]); // pop from 60% → 100% (overshoots)
|
||||
return (
|
||||
<span style={{
|
||||
display: "inline-block",
|
||||
transform: `scale(${scale})`,
|
||||
opacity: interpolate(p, [0, 1], [0, 1]),
|
||||
color: active ? "#FFE45E" : "#FFFFFF", // active-word highlight
|
||||
transition: "color 80ms linear",
|
||||
}}>{token.text} </span>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Compute `active` by testing `frame` against each token's `[startMs, endMs]`. Keep the highlight a single accent color or a filled "box" behind the live word — never animate every word's color at once.
|
||||
|
||||
## Pages, not walls of text
|
||||
|
||||
Show 1–4 words at a time. Use Remotion's `createTikTokStyleCaptions()` (or group manually) and tune `combineTokensWithinMilliseconds`: ~200–500ms for true word-by-word, ~1000–1500ms for short readable phrases.
|
||||
|
||||
```ts
|
||||
import { createTikTokStyleCaptions } from "@remotion/captions";
|
||||
const { pages } = createTikTokStyleCaptions({
|
||||
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 words per page
|
||||
});
|
||||
```
|
||||
|
||||
## Web/CSS pop (no Remotion)
|
||||
|
||||
For a DOM/live player, give each word its own `animation-delay` equal to `startMs`:
|
||||
|
||||
```css
|
||||
.word { display:inline-block; opacity:0; animation: pop .26s cubic-bezier(.34,1.56,.64,1) forwards; }
|
||||
@keyframes pop { from { opacity:0; transform:translateY(.18em) scale(.7) } to { opacity:1; transform:none } }
|
||||
.word.active { color:#FFE45E; }
|
||||
```
|
||||
```js
|
||||
words.forEach(w => { const el = mk(w.text); el.style.animationDelay = `${w.startMs}ms`; track.append(el); });
|
||||
```
|
||||
|
||||
## Readable type and safe-area placement (9:16, 1080×1920)
|
||||
|
||||
| Property | Value | Why |
|
||||
|---|---|---|
|
||||
| Font | Bold/ExtraBold sans (Montserrat, Inter, Helvetica) | Reads on small, busy screens |
|
||||
| Size | 56–80px (≈8% of frame height), min 45px | Legible muted on a phone |
|
||||
| Stroke | 2–6px solid black outline | Survives any background |
|
||||
| Shadow | Soft drop shadow as backup to stroke | Separation on bright frames |
|
||||
| Case | Uppercase or sentence; high contrast fill | Punch + scannability |
|
||||
| Vertical pos | Center band, ~62–70% down | Above the UI, below the action |
|
||||
| Bottom safe | Keep clear of bottom ~280px / 15% | Avoids caption/CTA/audio UI |
|
||||
| Side safe | Keep within center 80% width | Avoids right-rail icons |
|
||||
|
||||
Place captions in the center band — not pinned to the very bottom, where the platform UI (username, audio, buttons) lives. Pad text to a max of ~2 lines.
|
||||
|
||||
## Sync to a voiceover / narration
|
||||
|
||||
The transcript already carries the timing of the exact audio. Lay that same audio under the composition and keep timestamps in the audio's timebase — captions and voice stay locked with zero manual nudging. If the VO is re-recorded, re-transcribe; never hand-shift offsets.
|
||||
|
||||
## Burn-in vs sidecar SRT/VTT
|
||||
|
||||
| | Burn-in (open) | Sidecar SRT/VTT (closed) |
|
||||
|---|---|---|
|
||||
| Where | Pixels in the MP4 | Separate `.srt`/`.vtt` file |
|
||||
| Social (TikTok/Reels) | Required — guaranteed, styleable | Often ignored by the platform |
|
||||
| Accessibility (ADA/WCAG) | Does not satisfy alone | Required (toggleable) |
|
||||
| Best practice | Burn animated captions for social | Also ship a sidecar for the web/SEO |
|
||||
|
||||
Emit both: burn the animated captions for social, and write a plain SRT/VTT from the same tokens for accessible/SEO playback.
|
||||
|
||||
## Output checklist
|
||||
|
||||
- Word-level timestamps (real, from transcription) — no even-split fakery.
|
||||
- Per-word pop with micro-overshoot; one accent color for the active word.
|
||||
- 1–4 words per page; never a wall of text.
|
||||
- Bold sans, 56–80px, 2–6px stroke + shadow, high contrast.
|
||||
- Center band, clear of bottom ~280px and side rails.
|
||||
- Audio laid under composition; timestamps in the audio timebase.
|
||||
- Ship burn-in for social and a sidecar SRT/VTT for accessibility.
|
||||
|
||||
## Deliver & verify (rendered stills → MP4)
|
||||
|
||||
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
|
||||
|
||||
Captions ship as a Remotion composition (`<Composition>` + zod `schema` + `defaultProps`) — all word motion frame-driven off `useCurrentFrame()`, never `Date.now()` / `Math.random()` / timers. Deliverable = `out/*.mp4` (burned-in) + the project + the sidecar SRT/VTT. 9:16 vertical (1080×1920) is the default.
|
||||
|
||||
**Verify loop — render stills → inspect → encode.** Word timing is the thing that breaks; check it at exact frames before you spend an encode.
|
||||
|
||||
```bash
|
||||
# Stills at start / a sampled active-word frame / end — WITH SHIPPED PROPS (real tokens + audio)
|
||||
npx remotion still Captions out/f-start.png --frame=0 --props='{"captionsSrc":"vo.json"}'
|
||||
npx remotion still Captions out/f-mid.png --frame=90 --props='{"captionsSrc":"vo.json"}'
|
||||
npx remotion still Captions out/f-end.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = durationInFrames-1
|
||||
|
||||
# Inspect each PNG:
|
||||
# - the word highlighted at frame 90 is the word whose [startMs,endMs] contains 90/fps (no drift)
|
||||
# - burn-in legible: bold sans, stroke+shadow holds, no clipping
|
||||
# - 9:16 safe area: caption sits in the center band, clear of top ~12% and bottom ~20-35% (captions/CTA/audio UI) and the right action rail
|
||||
|
||||
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # encode once stills are right
|
||||
npx remotion render Captions out/demo.gif --codec=gif # README proof clip
|
||||
```
|
||||
|
||||
Use `npx remotion compositions` to read `durationInFrames`/`fps` and pick the active-word + end frames.
|
||||
|
||||
**Before you finish:**
|
||||
1. Stills render cleanly at frame 0, a mid active-word frame, and last — no missing font/audio.
|
||||
2. The correct word is highlighted at the sampled frame (frame/fps lands inside its token); no even-split fakery.
|
||||
3. Burn-in is legible (stroke+shadow) and the caption is fully inside the 9:16 safe area at every checked frame.
|
||||
4. Frame-driven only — no `Date.now()` / `Math.random()` / timers.
|
||||
5. Shipped props (real tokens, not just `defaultProps`) render correctly; MP4 + sidecar SRT/VTT emitted, GIF optional.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/word-timed-captions.md` — end-to-end build: Whisper transcription and the `Caption` type, a full SRT parser, Remotion word-timed component with active-highlight, manual paging, an SRT/VTT emitter, per-platform safe-area maps, and a readable-type spec sheet.
|
||||
@@ -1,254 +0,0 @@
|
||||
# Word-Timed Captions — End-to-End Build
|
||||
|
||||
A complete, runnable path from audio/transcript to animated burn-in captions plus a sidecar SRT/VTT. Code targets Remotion for burn-in and plain JS for the web; the data shapes are framework-agnostic.
|
||||
|
||||
## 1. The one data shape
|
||||
|
||||
Everything normalizes to a flat array of word tokens. Get to this shape and the renderer never cares about the source.
|
||||
|
||||
```ts
|
||||
export type Token = { text: string; startMs: number; endMs: number };
|
||||
export type Page = { startMs: number; endMs: number; tokens: Token[] };
|
||||
```
|
||||
|
||||
Remotion's `@remotion/captions` uses a compatible `Caption` type:
|
||||
|
||||
```ts
|
||||
type Caption = { text: string; startMs: number; endMs: number; timestampMs: number | null; confidence: number | null };
|
||||
```
|
||||
|
||||
## 2. Transcribe to word-level timing (Whisper)
|
||||
|
||||
Word timestamps are non-negotiable for karaoke. Whisper produces them natively.
|
||||
|
||||
```bash
|
||||
npx @remotion/install-whisper-cpp # one-time: installs whisper.cpp + a model
|
||||
```
|
||||
|
||||
```ts
|
||||
import { transcribe, toCaptions } from "@remotion/install-whisper-cpp";
|
||||
|
||||
const { transcription } = await transcribe({
|
||||
inputPath: "voiceover.wav", // 16kHz mono WAV is ideal for whisper.cpp
|
||||
model: "medium.en", // medium.en ≈ 2x faster than large, accurate on clean voice
|
||||
tokenLevelTimestamps: true, // <-- the key flag: per-word, not per-line
|
||||
whisperPath: "./whisper.cpp",
|
||||
});
|
||||
const { captions } = toCaptions({ transcription }); // → Caption[]
|
||||
```
|
||||
|
||||
Tips:
|
||||
- Feed clean voice audio (denoise/normalize first). Background music wrecks word boundaries.
|
||||
- `large` is most accurate but 2–3x real-time on a laptop. Use `medium.en` unless accuracy is critical.
|
||||
- Cloud alternatives (AssemblyAI, Deepgram, OpenAI Whisper API) also return word timing — map their `words[]` into `Token` the same way.
|
||||
|
||||
Map any token shape into `Token`:
|
||||
|
||||
```ts
|
||||
const toTokens = (caps: Caption[]): Token[] =>
|
||||
caps.map(c => ({ text: c.text.trim(), startMs: c.startMs, endMs: c.endMs }));
|
||||
```
|
||||
|
||||
## 3. Parse an existing SRT into tokens
|
||||
|
||||
When handed an SRT, parse cues; if cues are sentence-level, re-transcribe for word timing rather than interpolating.
|
||||
|
||||
```ts
|
||||
const toMs = (t: string) => {
|
||||
const [h, m, rest] = t.trim().split(":");
|
||||
const [s, ms] = rest.split(",");
|
||||
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
|
||||
};
|
||||
|
||||
export function parseSrt(src: string): Token[] {
|
||||
return src.trim().split(/\n\s*\n/).map(block => {
|
||||
const lines = block.split("\n");
|
||||
const time = lines.find(l => l.includes("-->"))!;
|
||||
const [a, b] = time.split("-->");
|
||||
const text = lines.slice(lines.indexOf(time) + 1).join(" ").trim();
|
||||
return { text, startMs: toMs(a), endMs: toMs(b) };
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Page the tokens (1–4 words at a time)
|
||||
|
||||
Either use Remotion's helper or group manually. Manual grouping by gap + max-words keeps full control:
|
||||
|
||||
```ts
|
||||
export function paginate(tokens: Token[], maxWords = 3, maxGapMs = 1200): Page[] {
|
||||
const pages: Page[] = [];
|
||||
let cur: Token[] = [];
|
||||
for (const t of tokens) {
|
||||
const span = cur.length ? t.endMs - cur[0].startMs : 0;
|
||||
if (cur.length >= maxWords || span > maxGapMs) {
|
||||
if (cur.length) pages.push(pageOf(cur)); cur = [];
|
||||
}
|
||||
cur.push(t);
|
||||
}
|
||||
if (cur.length) pages.push(pageOf(cur));
|
||||
return pages;
|
||||
}
|
||||
const pageOf = (ts: Token[]): Page => ({ startMs: ts[0].startMs, endMs: ts.at(-1)!.endMs, tokens: ts });
|
||||
```
|
||||
|
||||
Remotion equivalent:
|
||||
|
||||
```ts
|
||||
import { createTikTokStyleCaptions } from "@remotion/captions";
|
||||
const { pages } = createTikTokStyleCaptions({ captions, combineTokensWithinMilliseconds: 1200 });
|
||||
// 200–500ms → word-by-word; 1000–1500ms → short readable phrases
|
||||
```
|
||||
|
||||
## 5. Remotion component: word-timed with active highlight
|
||||
|
||||
```tsx
|
||||
import { AbsoluteFill, Audio, Sequence, useCurrentFrame, useVideoConfig,
|
||||
spring, interpolate, staticFile } from "remotion";
|
||||
|
||||
const CAPTION_STYLE: React.CSSProperties = {
|
||||
fontFamily: "Montserrat, Inter, sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: 72, // ~8% of 1920px height
|
||||
color: "#fff",
|
||||
textAlign: "center",
|
||||
textTransform: "uppercase",
|
||||
WebkitTextStroke: "5px #000", // heavy outline survives any background
|
||||
paintOrder: "stroke fill", // stroke behind fill, not clipping glyphs
|
||||
textShadow: "0 4px 16px rgba(0,0,0,.55)",
|
||||
lineHeight: 1.1,
|
||||
padding: "0 80px", // keep inside center 80% width
|
||||
};
|
||||
|
||||
const Word: React.FC<{ token: Token; active: boolean }> = ({ token, active }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const enter = (token.startMs / 1000) * fps;
|
||||
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
|
||||
return (
|
||||
<span style={{
|
||||
display: "inline-block",
|
||||
transform: `scale(${interpolate(p, [0, 1], [0.6, 1])})`,
|
||||
opacity: interpolate(p, [0, 1], [0, 1]),
|
||||
color: active ? "#FFE45E" : undefined,
|
||||
transition: "color 80ms linear",
|
||||
}}>{token.text} </span>
|
||||
);
|
||||
};
|
||||
|
||||
export const Captions: React.FC<{ pages: Page[]; audio: string }> = ({ pages, audio }) => {
|
||||
const { fps } = useVideoConfig();
|
||||
const f = (ms: number) => Math.round((ms / 1000) * fps);
|
||||
const frame = useCurrentFrame();
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
<Audio src={staticFile(audio)} /> {/* same audio the transcript came from */}
|
||||
{pages.map((pg, i) => (
|
||||
<Sequence key={i} from={f(pg.startMs)} durationInFrames={Math.max(1, f(pg.endMs - pg.startMs))}>
|
||||
<AbsoluteFill style={{ justifyContent: "flex-start", alignItems: "center", paddingTop: "62%" }}>
|
||||
<div style={CAPTION_STYLE}>
|
||||
{pg.tokens.map((t, j) => {
|
||||
const now = (frame / fps) * 1000;
|
||||
return <Word key={j} token={t} active={now >= t.startMs && now < t.endMs} />;
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
))}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Because the `<Audio>` and the timestamps share one timebase, voice and captions stay locked with no manual nudging. Re-record the VO → re-transcribe → done.
|
||||
|
||||
## 6. Web/CSS path (live DOM player)
|
||||
|
||||
```js
|
||||
function renderTrack(tokens, mountEl) {
|
||||
tokens.forEach(t => {
|
||||
const el = document.createElement("span");
|
||||
el.className = "word";
|
||||
el.textContent = t.text + " ";
|
||||
el.style.animationDelay = `${t.startMs}ms`; // each word pops on its own start
|
||||
mountEl.append(el);
|
||||
});
|
||||
}
|
||||
// drive .active off the media element's currentTime
|
||||
audio.addEventListener("timeupdate", () => {
|
||||
const ms = audio.currentTime * 1000;
|
||||
[...track.children].forEach((el, i) =>
|
||||
el.classList.toggle("active", ms >= tokens[i].startMs && ms < tokens[i].endMs));
|
||||
});
|
||||
```
|
||||
|
||||
```css
|
||||
.cap { font: 800 72px/1.1 Montserrat, sans-serif; text-transform: uppercase;
|
||||
color:#fff; -webkit-text-stroke:5px #000; paint-order:stroke fill;
|
||||
text-shadow:0 4px 16px rgba(0,0,0,.55); text-align:center; }
|
||||
.word { display:inline-block; opacity:0; animation: pop .26s cubic-bezier(.34,1.56,.64,1) forwards; }
|
||||
.word.active { color:#FFE45E; }
|
||||
@keyframes pop { from { opacity:0; transform:translateY(.18em) scale(.7) } to { opacity:1; transform:none } }
|
||||
```
|
||||
|
||||
## 7. Emit a sidecar SRT / VTT from the same tokens
|
||||
|
||||
Burn-in alone does not satisfy ADA/WCAG and many platforms ignore uploaded tracks — so ship both. Group tokens into readable cues and serialize:
|
||||
|
||||
```ts
|
||||
const pad = (n: number, w = 2) => String(n).padStart(w, "0");
|
||||
const stamp = (ms: number, sep: string) => {
|
||||
const h = Math.floor(ms / 3.6e6), m = Math.floor(ms % 3.6e6 / 6e4),
|
||||
s = Math.floor(ms % 6e4 / 1000), x = ms % 1000;
|
||||
return `${pad(h)}:${pad(m)}:${pad(s)}${sep}${pad(x, 3)}`;
|
||||
};
|
||||
|
||||
export const toSrt = (pages: Page[]) => pages.map((p, i) =>
|
||||
`${i + 1}\n${stamp(p.startMs, ",")} --> ${stamp(p.endMs, ",")}\n${p.tokens.map(t => t.text).join(" ")}`
|
||||
).join("\n\n") + "\n";
|
||||
|
||||
export const toVtt = (pages: Page[]) => "WEBVTT\n\n" + pages.map(p =>
|
||||
`${stamp(p.startMs, ".")} --> ${stamp(p.endMs, ".")}\n${p.tokens.map(t => t.text).join(" ")}`
|
||||
).join("\n\n") + "\n";
|
||||
```
|
||||
|
||||
Use SRT for the broadest platform support; VTT when styling/positioning cues or serving on the web.
|
||||
|
||||
## 8. Per-platform safe areas (1080×1920, 9:16)
|
||||
|
||||
| Platform | Bottom keep-clear | Top keep-clear | Side keep-clear | Notes |
|
||||
|---|---|---|---|---|
|
||||
| TikTok | ~340px | ~120px | ~110px right rail | Captions, audio title, action buttons crowd the bottom-right |
|
||||
| Instagram Reels | ~280px | ~130px | ~60px | Username/CTA bar at bottom |
|
||||
| YouTube Shorts | ~240px | ~100px | ~120px right rail | Title + channel at bottom-left |
|
||||
| Facebook Reels | ~300px | ~130px | ~80px | Similar to Reels |
|
||||
|
||||
Safe rule across all: place caption baseline at ~62–70% of frame height, keep text within the center 80% width, and never let it enter the bottom ~15%.
|
||||
|
||||
## 9. Readable-type spec sheet
|
||||
|
||||
| Property | Recommended | Floor |
|
||||
|---|---|---|
|
||||
| Weight | 800 (ExtraBold) | 700 |
|
||||
| Size @1080p | 56–80px | 45px |
|
||||
| Stroke | 4–6px solid #000 | 2px |
|
||||
| Shadow | `0 4px 16px rgba(0,0,0,.55)` | soft 1px |
|
||||
| Active accent | one color (e.g. #FFE45E) or filled box | — |
|
||||
| Lines on screen | 1–2 | 2 max |
|
||||
| Words per page | 1–4 | 5 max |
|
||||
|
||||
Set `paint-order: stroke fill` so a thick stroke sits behind the glyph instead of eating it. Test on a bright frame and a busy frame; if it survives both, it ships.
|
||||
|
||||
## 10. Render
|
||||
|
||||
```bash
|
||||
npx remotion render src/index.ts Captions out/captions.mp4 \
|
||||
--props='{"pages": ..., "audio": "voiceover.wav"}'
|
||||
```
|
||||
|
||||
A 10-minute video with animated captions can take 30–60 min to render locally; render long-form in segments, or render on Remotion Lambda for parallel speed.
|
||||
|
||||
---
|
||||
## Built by the team behind iart.ai
|
||||
|
||||
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can auto-time captions to a narration track and one-click burn them in from a template — change the text/audio and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=caption-animation)
|
||||
@@ -1,25 +0,0 @@
|
||||
# Countdown Video
|
||||
|
||||
Make a countdown that ends exactly when it should. The whole craft is one idea: derive the remaining time from the frame (or a monotonic timestamp), then format and animate that number — never count down with a `setInterval`. Covers livestream "starting soon" screens, launch and flash-sale countdowns, and looping backgrounds. For creators who need a timer that lands on zero on the exact frame.
|
||||
|
||||
## When it activates
|
||||
|
||||
- "Make a countdown timer video" or "create a countdown animation."
|
||||
- "Build a stream starting soon countdown" or "do an event/launch/sale countdown."
|
||||
- "Animate a mm:ss or dd:hh:mm:ss timer."
|
||||
- "Add digit roll/flip transitions to a counter" or "loop a countdown background."
|
||||
|
||||
## Example prompts
|
||||
|
||||
- "Build a 10-minute 'be right back' livestream countdown."
|
||||
- "Make a dd:hh:mm:ss launch countdown to a fixed target date."
|
||||
- "Add flip transitions to an mm:ss timer that hits zero on the exact frame."
|
||||
|
||||
## What's inside
|
||||
|
||||
- `SKILL.md` — frame-accurate (drift-free) timers, time formatting, and a configurable duration prop.
|
||||
- `references/countdown-cookbook.md` — recipes for common countdown scenarios.
|
||||
- `references/digit-flip.md` — digit flip/roll transition techniques.
|
||||
|
||||
---
|
||||
Part of **[Short-Form Video Skills](../../)** · Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=tiktok-video-skills&utm_content=skill_footer&utm_term=countdown-video)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -1,152 +0,0 @@
|
||||
---
|
||||
name: countdown-video
|
||||
description: This skill should be used when the user asks to "make a countdown timer video", "create a countdown animation", "build a stream starting soon countdown", "do an event/launch/sale countdown", "animate a mm:ss or dd:hh:mm:ss timer", "add digit roll/flip transitions to a counter", or "loop a countdown background". Covers frame-accurate (drift-free) timers, digit flip/roll motion, time formatting, and a configurable duration prop.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Countdown Video
|
||||
|
||||
Make a countdown that ends exactly when it should. The whole craft is one idea: derive the remaining time from the frame (or a monotonic timestamp), then format and animate that number — never count down with a `setInterval`. Covers livestream "starting soon" screens, launch/sale countdowns, and looping backgrounds.
|
||||
|
||||
## When to use
|
||||
|
||||
- "Starting soon" / "be right back" livestream screens with a 5–10 min timer.
|
||||
- Launch and flash-sale countdowns (dd:hh:mm:ss down to a target date; mm:ss for urgency).
|
||||
- Any animated counter where the number must land on zero on the exact frame.
|
||||
|
||||
## The one rule: drive the number from the clock, never a tick
|
||||
|
||||
A `setInterval(fn, 1000)` countdown **drifts** — the callback fires *at least* 1000 ms later, never exactly, and stacks up when the tab is throttled or a render frame is slow. Reports of ~1 second lost per minute are common. The fix is the same for rendered video and live web: compute remaining time as a pure function of an authoritative clock, so a late frame self-corrects on the next frame instead of accumulating error.
|
||||
|
||||
| Context | Authoritative clock | Remaining time |
|
||||
|---|---|---|
|
||||
| Remotion render | `useCurrentFrame()` | `total − frame/fps` |
|
||||
| Web (live page) | `performance.now()` or `Date.now()` | `targetMs − now` |
|
||||
| To a fixed date | `Date.now()` | `targetTimestamp − Date.now()` |
|
||||
|
||||
Never store remaining time in state and decrement it. Recompute it every frame.
|
||||
|
||||
## Frame-accurate countdown (Remotion → MP4)
|
||||
|
||||
`useCurrentFrame()` is deterministic: frame N is always rendered at exactly `N/fps` seconds. Subtract from the total and the timer is drift-free by construction.
|
||||
|
||||
```tsx
|
||||
import { useCurrentFrame, useVideoConfig } from "remotion";
|
||||
|
||||
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
|
||||
durationInSeconds,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// remaining seconds, clamped at 0 — ceil so "1" shows for its full second
|
||||
const elapsed = frame / fps;
|
||||
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
|
||||
|
||||
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
`durationInSeconds` is a **prop**, not a constant — the same composition renders a 5-minute stream timer or a 30-second sale timer by changing one input. Set the composition's `durationInFrames` to `durationInSeconds * fps` so the video is exactly as long as the countdown.
|
||||
|
||||
## Web timer (live page, requestAnimationFrame)
|
||||
|
||||
For a live page, drive an rAF loop from `performance.now()` and only repaint when the displayed second changes (no work 60×/sec for a 1 Hz number):
|
||||
|
||||
```js
|
||||
const start = performance.now();
|
||||
let lastShown = -1;
|
||||
function tick(now) {
|
||||
const remaining = Math.max(0, Math.ceil(duration - (now - start) / 1000));
|
||||
if (remaining !== lastShown) { render(remaining); lastShown = remaining; }
|
||||
if (remaining > 0) requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
```
|
||||
|
||||
Counting down to a fixed date is the same idea with `Date.now()`: `Math.max(0, target - Date.now())`. See `references/countdown-cookbook.md` for the full date-target version with timezone handling.
|
||||
|
||||
## Time formatting
|
||||
|
||||
Break a remaining-seconds integer into fields with division + modulo, then zero-pad. Pick the format to fit the duration — never show `00d 00h 05m` when minutes is all that matters.
|
||||
|
||||
```js
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
const formatMMSS = (s) => `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;
|
||||
const formatDHMS = (s) => {
|
||||
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60), sec = s % 60;
|
||||
return `${pad(d)}:${pad(h)}:${pad(m)}:${pad(sec)}`;
|
||||
};
|
||||
```
|
||||
|
||||
| Format | Use | Drop fields when |
|
||||
|---|---|---|
|
||||
| `mm:ss` | stream "starting soon", short timers (<1h) | — |
|
||||
| `hh:mm:ss` | flash sale, same-day event | hide `dd` while 0 |
|
||||
| `dd:hh:mm:ss` | launch / multi-day countdown | hide leading 0 units for cleaner read |
|
||||
|
||||
Always set `font-variant-numeric: tabular-nums` (or a monospace digit font) so digits keep equal width and the layout does not jitter as numbers change.
|
||||
|
||||
## Digit roll / flip transitions
|
||||
|
||||
A flat number swap reads as cheap; the rolling "odometer" look sells the countdown. Render each digit as a vertical strip 0–9 and translate it by `-digit × digitHeight`. Animate the *position*, not the text content, so the change is a glide, not a pop.
|
||||
|
||||
```css
|
||||
.reel { height: 1em; overflow: hidden; }
|
||||
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
|
||||
/* col holds stacked 0..9; shift by -value em */
|
||||
```
|
||||
|
||||
In Remotion, drive the strip offset off the frame instead of a CSS transition (so it is deterministic). The flip-card style (top half folds down over the bottom) and the per-digit reel are both detailed, with runnable components, in `references/digit-flip.md`.
|
||||
|
||||
## Looping background
|
||||
|
||||
Stream screens hold for minutes, so the background must loop seamlessly. Build motion on a cycle that divides evenly into the timeline (e.g. a gradient or particle drift whose period is `loopFrames`), and read it as `frame % loopFrames` so there is no visible seam at the wrap. Keep it low-contrast and slow — the number is the subject; the background is ambience. Recipes in `references/countdown-cookbook.md`.
|
||||
|
||||
## Output checklist
|
||||
|
||||
- Remaining time recomputed from `useCurrentFrame()` / `performance.now()` every frame — no `setInterval` decrement.
|
||||
- Timer hits `00:00` on the exact intended frame; `durationInFrames === durationInSeconds × fps`.
|
||||
- Duration is an input prop, not a hardcoded constant.
|
||||
- Digits use `tabular-nums`; layout does not shift as numbers change.
|
||||
- Format matches the span (mm:ss vs dd:hh:mm:ss); leading zero units dropped when distracting.
|
||||
- Background loop period divides the timeline evenly; no seam at the wrap.
|
||||
|
||||
## Deliver & verify (rendered stills → MP4)
|
||||
|
||||
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
|
||||
|
||||
The countdown ships as a Remotion composition (`<Composition>` + zod `schema` + `defaultProps`, `durationInSeconds` a prop) — the timer is a pure function of `useCurrentFrame()`, never `setInterval` / `Date.now()` / `Math.random()`. Set `durationInFrames = durationInSeconds * fps`. Deliverable = `out/*.mp4` + the project. 9:16 vertical (1080×1920) is the default.
|
||||
|
||||
**Verify loop — render stills → inspect → encode.** The whole point is frame-accuracy, so check the *digit value* at exact frames before encoding.
|
||||
|
||||
```bash
|
||||
# Stills at start / mid / end — WITH SHIPPED PROPS (the real duration you'll render)
|
||||
npx remotion still Countdown out/f-start.png --frame=0 --props='{"durationInSeconds":300}'
|
||||
npx remotion still Countdown out/f-mid.png --frame=N --props='{"durationInSeconds":300}'
|
||||
npx remotion still Countdown out/f-end.png --frame=L --props='{"durationInSeconds":300}' # L = durationInFrames-1
|
||||
|
||||
# Inspect each PNG — the digits must read EXACTLY the frame/fps math:
|
||||
# - at frame N, displayed = ceil(durationInSeconds - N/fps) (e.g. 300s @ 30fps, frame 4500 → 150s → "02:30")
|
||||
# - last frame reads 00:00 (or the end-state hold); no drift, no off-by-one second
|
||||
# - tabular-nums: layout does not jitter between frames
|
||||
# - 9:16 safe area: the timer + brand block sit clear of top ~12% and bottom ~20-35% UI and the right action rail
|
||||
|
||||
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # encode once digits verify
|
||||
npx remotion render Countdown out/demo.gif --codec=gif # README proof clip
|
||||
```
|
||||
|
||||
Use `npx remotion compositions` to confirm `durationInFrames`/`fps`, then compute the frame for any second you want to check.
|
||||
|
||||
**Before you finish:**
|
||||
1. Stills render cleanly at frame 0, mid, and last — no errors.
|
||||
2. The digit value at frame N equals `ceil(durationInSeconds - N/fps)` exactly; last frame hits 00:00 on the intended frame (no drift / off-by-one).
|
||||
3. Timer + brand stay inside the 9:16 safe area at every checked frame.
|
||||
4. Frame-driven only — no `setInterval` / `Date.now()` / timers; `tabular-nums` so layout holds.
|
||||
5. Shipped `durationInSeconds` (not just `defaultProps`) renders correctly; MP4 encoded, GIF optional.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/countdown-cookbook.md` — complete runnable Remotion `<Countdown>` (configurable duration, mm:ss / dd:hh:mm:ss, end-state hold), the count-down-to-a-fixed-date web component with timezone handling, a "starting soon" stream-screen layout (brand block + social handles + looping gradient background), and the seamless-loop background pattern.
|
||||
- `references/digit-flip.md` — two deterministic digit-animation components: the odometer reel (vertical 0–9 strip driven by frame) and the flip-card (top-half fold) effect, plus when each suits launch vs sale vs stream.
|
||||
@@ -1,205 +0,0 @@
|
||||
# Countdown Cookbook
|
||||
|
||||
Complete, runnable recipes. Every value is derived from an authoritative clock (frame or timestamp) so nothing drifts.
|
||||
|
||||
## 1. Configurable Remotion countdown (mm:ss + dd:hh:mm:ss)
|
||||
|
||||
A single composition that handles any duration and either format. Drop into `src/Countdown.tsx`.
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useCurrentFrame, useVideoConfig, AbsoluteFill } from "remotion";
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
const formatTime = (totalSeconds: number, mode: "mm:ss" | "dd:hh:mm:ss") => {
|
||||
const s = Math.max(0, totalSeconds);
|
||||
if (mode === "mm:ss") {
|
||||
return `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;
|
||||
}
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
// drop leading day field once it is zero, for a cleaner read
|
||||
return d > 0
|
||||
? `${pad(d)}d ${pad(h)}:${pad(m)}:${pad(sec)}`
|
||||
: `${pad(h)}:${pad(m)}:${pad(sec)}`;
|
||||
};
|
||||
|
||||
export type CountdownProps = {
|
||||
durationInSeconds: number;
|
||||
format?: "mm:ss" | "dd:hh:mm:ss";
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const Countdown: React.FC<CountdownProps> = ({
|
||||
durationInSeconds,
|
||||
format = "mm:ss",
|
||||
label = "",
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// ceil so the last whole second ("1") is shown for its full duration,
|
||||
// and the display reaches "0" exactly on the final frame.
|
||||
const remaining = Math.max(0, Math.ceil(durationInSeconds - frame / fps));
|
||||
const ended = remaining === 0;
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
background: "#0b0d17",
|
||||
color: "#fff",
|
||||
fontFamily: "Inter, system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
{label && (
|
||||
<div style={{ fontSize: 36, opacity: 0.7, marginBottom: 16, letterSpacing: 4 }}>
|
||||
{label.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 180,
|
||||
fontWeight: 800,
|
||||
fontVariantNumeric: "tabular-nums", // equal-width digits → no jitter
|
||||
lineHeight: 1,
|
||||
color: ended ? "#ff4d4d" : "#fff",
|
||||
}}
|
||||
>
|
||||
{ended ? formatTime(0, format) : formatTime(remaining, format)}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Register it so `durationInFrames` matches the countdown exactly — the video ends when the timer does:
|
||||
|
||||
```tsx
|
||||
import { Composition } from "remotion";
|
||||
import { Countdown } from "./Countdown";
|
||||
|
||||
const FPS = 30;
|
||||
const DURATION_SECONDS = 300; // 5-minute "starting soon" timer
|
||||
|
||||
export const RemotionRoot = () => (
|
||||
<Composition
|
||||
id="Countdown"
|
||||
component={Countdown}
|
||||
durationInFrames={DURATION_SECONDS * FPS}
|
||||
fps={FPS}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{ durationInSeconds: DURATION_SECONDS, format: "mm:ss", label: "Starting soon" }}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
Render any duration/format without touching code — duration is a prop:
|
||||
|
||||
```bash
|
||||
npx remotion render Countdown out/sale.mp4 \
|
||||
--props='{"durationInSeconds":86400,"format":"dd:hh:mm:ss","label":"Sale ends in"}'
|
||||
```
|
||||
|
||||
To hold a final state (e.g. "WE'RE LIVE") instead of ending on `00:00`, extend `durationInFrames` past the countdown and branch on `ended`.
|
||||
|
||||
## 2. Count down to a fixed date (live web component)
|
||||
|
||||
For a real-time page that targets a calendar moment. Drift-free because remaining time is `target − Date.now()` recomputed each frame, never a decrement.
|
||||
|
||||
```js
|
||||
// targetISO must include an offset, e.g. "2026-07-01T09:00:00-07:00",
|
||||
// so the deadline is unambiguous regardless of the viewer's timezone.
|
||||
function startCountdown(targetISO, el) {
|
||||
const target = new Date(targetISO).getTime();
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
let lastShown = -1;
|
||||
|
||||
function tick() {
|
||||
const remainingMs = Math.max(0, target - Date.now());
|
||||
const s = Math.floor(remainingMs / 1000);
|
||||
if (s !== lastShown) {
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
el.textContent = `${pad(d)}:${pad(h)}:${pad(m)}:${pad(s % 60)}`;
|
||||
lastShown = s;
|
||||
}
|
||||
if (remainingMs > 0) requestAnimationFrame(tick);
|
||||
else el.textContent = "00:00:00:00"; // or swap to an "It's live" state
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
```
|
||||
|
||||
Timezone notes:
|
||||
- Bake the offset into the target string (`-07:00`, `Z`). A bare `"2026-07-01T09:00:00"` is parsed as the viewer's *local* time and every viewer sees a different deadline.
|
||||
- To show "ends 9am in *your* timezone everywhere," instead pick the wall-clock time per locale and let each viewer's `Date` resolve it — but that is a different deadline per region. Decide which you want before building.
|
||||
|
||||
## 3. "Starting soon" stream screen
|
||||
|
||||
Livestream lead-in screens hold 5–10 minutes: a clean brand block, social handles, and a calm looping background, with the timer as the focal point. Keep it low-clutter — chat and music carry the wait; the screen just signals "we're about to start."
|
||||
|
||||
```tsx
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from "remotion";
|
||||
|
||||
export const StartingSoon: React.FC<{ durationInSeconds: number; handle: string; brand: string }> = ({
|
||||
durationInSeconds,
|
||||
handle,
|
||||
brand,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
const remaining = Math.max(0, Math.ceil(durationInSeconds - frame / fps));
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
// seamless looping gradient: angle cycles over loopFrames with no seam
|
||||
const loopFrames = fps * 8;
|
||||
const angle = interpolate(frame % loopFrames, [0, loopFrames], [0, 360]);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: `linear-gradient(${angle}deg, #141a3a, #0b0d17 60%)`,
|
||||
color: "#fff",
|
||||
fontFamily: "Inter, system-ui, sans-serif",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 64, fontWeight: 800, marginBottom: 8 }}>{brand}</div>
|
||||
<div style={{ fontSize: 28, opacity: 0.6, letterSpacing: 6, marginBottom: 40 }}>
|
||||
STARTING SOON
|
||||
</div>
|
||||
<div style={{ fontSize: 200, fontWeight: 800, fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>
|
||||
{pad(Math.floor(remaining / 60))}:{pad(remaining % 60)}
|
||||
</div>
|
||||
<div style={{ position: "absolute", bottom: 60, fontSize: 30, opacity: 0.7 }}>{handle}</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Export as MP4 and add it as a Media/Video source in OBS, or use a looping playout for a browser source. Common durations: 300s (5 min) or 600s (10 min) at 1920×1080.
|
||||
|
||||
## 4. Seamless looping background
|
||||
|
||||
Any motion whose period divides the timeline evenly wraps without a seam. Read animation phase as `frame % loopFrames` so the value at the wrap equals the value at frame 0.
|
||||
|
||||
```tsx
|
||||
const loopFrames = fps * 6; // one full cycle every 6 seconds
|
||||
const phase = (frame % loopFrames) / loopFrames; // 0 → 1, repeats
|
||||
const driftY = Math.sin(phase * Math.PI * 2) * 40; // returns to start each cycle
|
||||
```
|
||||
|
||||
Use sine/cosine (inherently periodic) or `interpolate(frame % loopFrames, [0, loopFrames], [a, a])` with matching start/end so there is no jump. Keep contrast low and motion slow — the number is the subject.
|
||||
|
||||
---
|
||||
## Built by the team behind iart.ai
|
||||
|
||||
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can render countdown videos from a template with a set time and brand — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=countdown-video)
|
||||
@@ -1,146 +0,0 @@
|
||||
# Digit Flip & Roll Transitions
|
||||
|
||||
Two ways to animate a digit changing. Both are driven by the frame (deterministic) so they render identically every time — no CSS transition clock that the renderer can disagree with.
|
||||
|
||||
Pick by tone:
|
||||
|
||||
| Effect | Feel | Best for |
|
||||
|---|---|---|
|
||||
| Odometer reel | smooth, continuous glide | launch countdowns, premium/minimal timers |
|
||||
| Flip card | mechanical "snap", split-flap board | sale urgency, retro/airport-board style |
|
||||
|
||||
## Shared idea
|
||||
|
||||
A single displayed digit is a stack of glyphs `0..9` laid out vertically. Showing digit `d` means translating the stack so glyph `d` sits in the window: `translateY(-d × digitHeight)`. Animate the *offset*, not the text, and you get a roll. The trick for a countdown is that the offset is continuous: between whole seconds the digit eases from its old value toward its new one.
|
||||
|
||||
## Odometer reel (Remotion, frame-driven)
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import { useCurrentFrame, useVideoConfig, interpolate, Easing } from "remotion";
|
||||
|
||||
const DIGIT_H = 160; // px per glyph
|
||||
const ROLL_FRAMES = 12; // how long a single digit roll takes
|
||||
|
||||
// Animated single digit: rolls from `prev` to `value` over the last ROLL_FRAMES
|
||||
// of the current second.
|
||||
const Reel: React.FC<{ value: number; prev: number; secondProgress: number }> = ({
|
||||
value,
|
||||
prev,
|
||||
secondProgress,
|
||||
}) => {
|
||||
// secondProgress: 0→1 across the current whole second.
|
||||
// Start the roll near the end of the second so the number "settles" on change.
|
||||
const t = interpolate(secondProgress, [0.7, 1], [0, 1], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
easing: Easing.bezier(0.22, 1, 0.36, 1),
|
||||
});
|
||||
// continuous position between prev and value
|
||||
const shown = prev + (value - prev) * t;
|
||||
const offset = -shown * DIGIT_H;
|
||||
|
||||
return (
|
||||
<div style={{ height: DIGIT_H, width: 110, overflow: "hidden", position: "relative" }}>
|
||||
<div style={{ transform: `translateY(${offset}px)` }}>
|
||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => (
|
||||
<div
|
||||
key={n}
|
||||
style={{
|
||||
height: DIGIT_H,
|
||||
lineHeight: `${DIGIT_H}px`,
|
||||
textAlign: "center",
|
||||
fontSize: 140,
|
||||
fontWeight: 800,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders an mm:ss reel where each digit rolls.
|
||||
export const ReelClock: React.FC<{ durationInSeconds: number }> = ({ durationInSeconds }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
const elapsed = frame / fps;
|
||||
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
|
||||
const prevRemaining = Math.max(0, remaining + 1); // value one second ago
|
||||
const secondProgress = elapsed % 1; // 0→1 within the current second
|
||||
|
||||
const digits = (s: number) =>
|
||||
[Math.floor(s / 600) % 10, Math.floor(s / 60) % 10, Math.floor(s / 10) % 10, s % 10];
|
||||
|
||||
const cur = digits(remaining);
|
||||
const prev = digits(prevRemaining);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 8, color: "#fff", background: "#0b0d17", padding: 40 }}>
|
||||
{cur.map((d, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i === 2 && <div style={{ fontSize: 140, fontWeight: 800 }}>:</div>}
|
||||
<Reel value={d} prev={prev[i]} secondProgress={secondProgress} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Each digit only rolls when it actually changes; a digit whose `prev === value` sits still because `shown` stays constant.
|
||||
- A `9 → 0` rollover rolls *backward* through the strip (the cheap-looking part). To make tens/units roll the natural way on a carry, render an extra `0` glyph below the `9` and clamp — covered as an exercise; for most timers the backward roll at the tens boundary is unnoticeable at speed.
|
||||
- `ROLL_FRAMES` is unused above because the roll is tied to `secondProgress`; expose it instead if a fixed-length roll is preferred.
|
||||
|
||||
## Flip card (split-flap)
|
||||
|
||||
The top half of the old digit folds down over a static bottom half, revealing the new digit. Pure CSS keyframe version — fine for a live web page; for Remotion, drive the `rotateX` off `secondProgress` the same way as the reel so it stays deterministic.
|
||||
|
||||
```css
|
||||
.flip { position: relative; width: 110px; height: 160px; perspective: 600px; }
|
||||
.flip .card {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 140px; font-weight: 800; font-variant-numeric: tabular-nums;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.flip .top { clip-path: inset(0 0 50% 0); transform-origin: bottom; }
|
||||
.flip .bottom { clip-path: inset(50% 0 0 0); }
|
||||
/* on change, animate the leaf folding down */
|
||||
.flip .leaf {
|
||||
position: absolute; inset: 0; transform-origin: bottom;
|
||||
animation: fold .45s cubic-bezier(.36,0,.66,-0.2) forwards;
|
||||
}
|
||||
@keyframes fold {
|
||||
0% { transform: rotateX(0deg); }
|
||||
100% { transform: rotateX(-90deg); }
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// swap the digit and replay the fold whenever the value changes
|
||||
function setFlip(el, newDigit) {
|
||||
if (el.dataset.v === String(newDigit)) return; // no change → no flip
|
||||
el.dataset.v = String(newDigit);
|
||||
el.querySelector(".top").textContent = newDigit;
|
||||
el.querySelector(".bottom").textContent = newDigit;
|
||||
const leaf = el.querySelector(".leaf");
|
||||
leaf.textContent = newDigit;
|
||||
leaf.style.animation = "none";
|
||||
void leaf.offsetWidth; // reflow → restart the animation
|
||||
leaf.style.animation = "";
|
||||
}
|
||||
```
|
||||
|
||||
Drive `setFlip` from the same `requestAnimationFrame` + `performance.now()` loop in the cookbook (only call it when the displayed second changes), so the flips fire on real time and never drift.
|
||||
|
||||
## Choosing
|
||||
|
||||
- **Launch / premium:** odometer reel, slow ease, large tabular digits, muted palette.
|
||||
- **Flash sale:** flip card for the mechanical urgency, or reel with a faster roll and a hot accent color on `mm:ss`.
|
||||
- **Stream "starting soon":** either works, but keep it calm — a single reel on `mm:ss` reads better than a busy split-flap for a 5–10 minute hold.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Lower Thirds
|
||||
|
||||
Build a clean, branded lower third — the name/role caption that lives in the bottom band of the frame — that animates in, holds long enough to read, and exits without stealing the shot. The craft is restraint: a crisp reveal, a strong type hierarchy, correct safe-area placement, and one template that drives a whole roster of speakers. For creators producing interviews, panels, podcasts, and broadcast-style titles.
|
||||
|
||||
## When it activates
|
||||
|
||||
- "Make an animated lower third" or "add lower thirds."
|
||||
- "Create a name tag/name lower third" or "do a speaker name overlay."
|
||||
- "Build broadcast titles" or "animate a title/name/role caption."
|
||||
- "Template lower thirds across a list of names."
|
||||
|
||||
## Example prompts
|
||||
|
||||
- "Add a lower third with the speaker's name and role, branded to my colors."
|
||||
- "Build a staggered slide-in name plate for a podcast guest."
|
||||
- "Template one lower-third design across a roster of 12 panelists."
|
||||
|
||||
## What's inside
|
||||
|
||||
- `SKILL.md` — enter/exit animation, in/out timing and dwell, safe-area conventions, and brand styling.
|
||||
- `references/lower-third-component.md` — a reusable lower-third component.
|
||||
|
||||
---
|
||||
Part of **[Short-Form Video Skills](../../)** · Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=tiktok-video-skills&utm_content=skill_footer&utm_term=lower-thirds)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
name: lower-thirds
|
||||
description: This skill should be used when the user asks to "make an animated lower third", "add lower thirds", "create a name tag/name lower third", "build broadcast titles", "animate a title/name/role caption", "do a speaker name overlay", or "template lower thirds across a list of names". Covers enter/exit animation (slide/wipe/mask reveal), in/out timing & dwell, safe-area & broadcast conventions, name-vs-role type hierarchy, brand styling, and rendering one design across many names.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Lower Thirds
|
||||
|
||||
Build a clean, branded lower third — the name/role caption that lives in the bottom band of the frame — that animates in, holds long enough to read, and exits without stealing the shot. The craft is restraint: a crisp reveal, a strong type hierarchy, correct safe-area placement, and one template that drives a whole roster of speakers.
|
||||
|
||||
## When to use
|
||||
|
||||
- Speaker/interview name tags, panel rosters, talking-head captions.
|
||||
- Broadcast-style titles, news straps, podcast/webinar name plates.
|
||||
- Templating one design across a list of names/titles → many clips (the payoff).
|
||||
|
||||
## Anatomy
|
||||
|
||||
A lower third is two text lines plus a graphic bed, placed in the lower band:
|
||||
|
||||
| Element | Role | Type treatment |
|
||||
|---|---|---|
|
||||
| **Name** (primary) | Who | Largest, heaviest, tightest tracking |
|
||||
| **Role/title** (secondary) | What/where | ~45–60% of name size, lighter weight, often a brand accent color |
|
||||
| **Bar / panel / accent** | Reads against any background | Solid or 70–85% opacity; an accent rule ties to brand |
|
||||
|
||||
Keep it to two lines. A name line + role line is the whole job — extra lines erode the hierarchy and crowd the safe area.
|
||||
|
||||
## Three rules that prevent cheap-looking titles
|
||||
|
||||
1. **Never hard-cut a title on or off.** A bare appear/disappear reads as a mistake. Even a 12-frame fade or slide makes it feel produced.
|
||||
2. **Stagger, don't slam.** Animate the bar first, then the name, then the role, each offset by 3–6 frames. Everything arriving at once feels flat; a short cascade feels designed.
|
||||
3. **Hold long enough to read twice.** Roughly 3–5s on screen (read it aloud twice). Too short and it's missed; too long and it nags. Animate in/out, never mid-segment.
|
||||
|
||||
## Enter & exit animations
|
||||
|
||||
Pick one motion language and reuse it. The most reliable, premium-reading reveals:
|
||||
|
||||
| Reveal | Feel | How |
|
||||
|---|---|---|
|
||||
| **Slide + fade** | Clean, default | Translate in ~24–40px while fading; ease-out on enter |
|
||||
| **Wipe bar** | Broadcast | A bar scales/sweeps in (`scaleX`/clip), text fades behind it |
|
||||
| **Mask reveal** | Premium | Clip text to a moving rectangle so letters "wipe on" |
|
||||
|
||||
Use asymmetric easing: a snappy `ease-out` (`cubic-bezier(.22,1,.36,1)`) on enter, a quicker `ease-in` on exit. Exit faster than you entered (~⅔ the duration) so the title gets out of the way.
|
||||
|
||||
```css
|
||||
/* Mask reveal: text wipes on left→right, no plugin needed */
|
||||
.lt-name { clip-path: inset(0 100% 0 0); animation: wipe .5s cubic-bezier(.22,1,.36,1) forwards; }
|
||||
@keyframes wipe { to { clip-path: inset(0 0 0 0); } }
|
||||
```
|
||||
|
||||
## In/out timing & dwell
|
||||
|
||||
Think in three phases. Budget frames, not vibes (at 30fps):
|
||||
|
||||
| Phase | Budget | Note |
|
||||
|---|---|---|
|
||||
| Enter | 12–18f (0.4–0.6s) | Staggered: bar → name → role |
|
||||
| Hold | 90–150f (3–5s) | The readable dwell; scale with line length |
|
||||
| Exit | 8–12f (0.27–0.4s) | Faster than enter |
|
||||
|
||||
In code, drive every state from the current frame (or a paused timeline), not wall-clock time — a video renderer demands deterministic frames. Express enter/hold/exit as a single progress curve so the same component works live in a browser and frame-by-frame in a renderer.
|
||||
|
||||
## Safe area & broadcast conventions
|
||||
|
||||
The lower third must survive every screen and platform UI. Compose inside the **title-safe area** (inner ~90% / margin 5%) and respect platform chrome:
|
||||
|
||||
| Frame | Anchor | Avoid |
|
||||
|---|---|---|
|
||||
| 16:9 (1920×1080) | Left edge at ~5% margin; baseline ~80–85% down | Outer 5% (title-safe), action-safe outer 10% |
|
||||
| 9:16 (1080×1920) | Same left margin; **lift the band up** | Bottom ~18% (captions/UI), top ~12% |
|
||||
| 1:1 (1080×1080) | Lower band, center-left | Outer 5% |
|
||||
|
||||
Anchor to the **left** and grow rightward so names of different lengths stay pinned and never re-center awkwardly. Keep the role line within the same left margin as the name. For 9:16, do not park the title at the very bottom — platform captions and buttons sit there.
|
||||
|
||||
## Type hierarchy & brand styling
|
||||
|
||||
Hierarchy is the whole design. Make the name unmistakably dominant:
|
||||
|
||||
- **Size contrast** ≥ 1.6× (name vs role). If unsure, push the name bigger, not the role.
|
||||
- **Weight + color**: name in bold/near-white; role in a lighter weight and a **brand accent** color (one accent, used consistently).
|
||||
- **Contrast for legibility**: text rides over unknown footage. Use a solid/semi-opaque bar, a subtle scrim, or a soft text shadow so it reads over both bright and dark plates.
|
||||
- **Brand lock**: pin font, accent color, bar style, and corner radius in one theme object so every title across a series matches.
|
||||
|
||||
## Templating one design across a list
|
||||
|
||||
The reason to build this in code: change the text/data, re-export — no re-animating. Make name/role/theme **props**, never hardcoded, then map a roster to instances.
|
||||
|
||||
```jsx
|
||||
const ROSTER = [
|
||||
{ name: "Ada Lovelace", role: "Founder, Analytical Engines" },
|
||||
{ name: "Grace Hopper", role: "Rear Admiral, US Navy" },
|
||||
{ name: "Alan Turing", role: "Mathematician" },
|
||||
];
|
||||
// One <LowerThird> design; map the list → many instances (sequence them in a renderer)
|
||||
{ROSTER.map((p, i) => <LowerThird key={p.name} {...p} theme={BRAND} startFrame={i * 150} />)}
|
||||
```
|
||||
|
||||
In a renderer, make the roster an input prop and render once per row (`--props`) to emit one branded clip per person — same animation, same brand, only the text changes. See `references/lower-third-component.md` for the full runnable component and batch script.
|
||||
|
||||
## Output checklist
|
||||
|
||||
- Two lines only: dominant name, secondary role (≥1.6× size contrast).
|
||||
- Animated in and out (no hard cut); enter staggered bar→name→role; exit faster.
|
||||
- Hold 3–5s (readable twice); motion only at the ends, never mid-segment.
|
||||
- Left-anchored inside title-safe; 9:16 band lifted above the bottom-18% UI zone.
|
||||
- Text legible over any plate (bar/scrim/shadow); one brand accent throughout.
|
||||
- Name/role/theme are props — one template renders the whole roster.
|
||||
|
||||
## Deliver & verify (rendered stills → MP4)
|
||||
|
||||
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
|
||||
|
||||
The lower third ships as a Remotion composition (`<Composition>` + zod `schema` + `defaultProps`, name/role/theme as props) — enter/dwell/exit all driven by `useCurrentFrame()`, never `Date.now()` / `Math.random()` / timers. Deliverable = `out/*.mp4` + the project (re-render per roster row). 9:16 vertical (1080×1920) is the default.
|
||||
|
||||
**Verify loop — render stills → inspect → encode.** Sample the three phases so you catch a stagger or safe-area bug before encoding (or batching the whole roster).
|
||||
|
||||
```bash
|
||||
# Stills at enter / dwell / exit — WITH SHIPPED PROPS (a real roster row, not defaults)
|
||||
npx remotion still LowerThird out/f-enter.png --frame=10 --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
npx remotion still LowerThird out/f-dwell.png --frame=75 --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
npx remotion still LowerThird out/f-exit.png --frame=N --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
|
||||
# Inspect each PNG — focus the DWELL frame (fully on screen):
|
||||
# - hierarchy holds: name dominant (>=1.6x role), role in brand accent; both fully revealed, not mid-wipe
|
||||
# - brand lock: font, accent color, bar/scrim correct; legible over the plate
|
||||
# - 9:16 safe area: band LIFTED above the bottom ~20-35% UI zone, clear of top ~12% and the right action rail; left-anchored inside title-safe
|
||||
# - enter/exit frames show partial reveal (animation actually runs), not a hard cut
|
||||
|
||||
npx remotion render LowerThird out/lower-third.mp4 --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
npx remotion render LowerThird out/demo.gif --codec=gif # README proof clip
|
||||
```
|
||||
|
||||
**Batch roster**: verify ONE row via stills *before* rendering all names — catch a layout/overflow bug once, not N times. Use `npx remotion compositions` for `durationInFrames`/`fps` to pick the dwell + exit frames.
|
||||
|
||||
**Before you finish:**
|
||||
1. Stills render cleanly at enter, dwell, and exit — no missing font/brand assets.
|
||||
2. At the dwell frame: name dominant over role, brand accent + bar correct, fully (not partly) revealed.
|
||||
3. The band is inside the 9:16 safe area — lifted above the bottom UI zone, clear of top and right rail — at every checked frame.
|
||||
4. Frame-driven only — no `Date.now()` / `Math.random()` / timers; enter/exit show motion, never a hard cut.
|
||||
5. A real roster row (not `defaultProps`) renders correctly; MP4 encoded, GIF optional.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/lower-third-component.md` — a complete runnable lower-third: a Remotion component with staggered spring enter, readable hold, and faster exit driven purely by frame; a typed theme object for brand lock; the list→many-instances sequencing pattern; a per-row batch render script; plus a dependency-free CSS/HTML version of the same design with mask-reveal and wipe-bar variants.
|
||||
@@ -1,268 +0,0 @@
|
||||
# Lower-Third Component — runnable code
|
||||
|
||||
A complete, production-grade lower third you can drop in. Two paths:
|
||||
|
||||
1. **Remotion** (frame-deterministic, renders to MP4, batches over a roster).
|
||||
2. **CSS/HTML** (no dependencies, for the web or a quick preview) with mask-reveal and wipe-bar variants.
|
||||
|
||||
Both share the same design language: a left-anchored bar, a dominant name, a brand-accent role line, a staggered enter, a readable hold, and a faster exit.
|
||||
|
||||
---
|
||||
|
||||
## 1. Remotion component (props-driven, frame-deterministic)
|
||||
|
||||
The animation is a pure function of `frame`, so it renders identically every time and stays in sync with the renderer. Enter staggers the bar, name, then role; exit is computed from the end of the clip so the title leaves faster than it arrived.
|
||||
|
||||
```tsx
|
||||
// LowerThird.tsx
|
||||
import {
|
||||
AbsoluteFill, Sequence, interpolate, spring,
|
||||
useCurrentFrame, useVideoConfig,
|
||||
} from "remotion";
|
||||
|
||||
// --- Brand lock: change once, every title matches -------------------------
|
||||
export type Theme = {
|
||||
font: string;
|
||||
bar: string; // panel/bar color
|
||||
name: string; // primary text color
|
||||
role: string; // accent color for the role line
|
||||
radius: number;
|
||||
marginPct: number; // left/safe margin as % of width
|
||||
};
|
||||
export const BRAND: Theme = {
|
||||
font: "Inter, system-ui, sans-serif",
|
||||
bar: "rgba(12,14,22,0.86)",
|
||||
name: "#FFFFFF",
|
||||
role: "#6EE7F0", // single brand accent
|
||||
radius: 10,
|
||||
marginPct: 5,
|
||||
};
|
||||
|
||||
export type LowerThirdProps = {
|
||||
name: string;
|
||||
role: string;
|
||||
theme?: Theme;
|
||||
holdSeconds?: number; // readable dwell, default 4s
|
||||
};
|
||||
|
||||
// Staggered enter via spring; exit via a quick fade/slide near the end.
|
||||
const useEnterExit = (delayFrames: number, holdSeconds: number) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
|
||||
// ENTER: spring 0→1, offset by delayFrames (the cascade)
|
||||
const enter = spring({
|
||||
frame: frame - delayFrames,
|
||||
fps,
|
||||
config: { damping: 200, stiffness: 180, mass: 0.6 },
|
||||
});
|
||||
|
||||
// EXIT: faster than enter — last 10 frames of the clip
|
||||
const exitStart = durationInFrames - 10;
|
||||
const exit = interpolate(frame, [exitStart, durationInFrames], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const opacity = enter * exit;
|
||||
const x = interpolate(enter, [0, 1], [-32, 0]); // slide in from left
|
||||
return { opacity, x };
|
||||
};
|
||||
|
||||
const Line: React.FC<{
|
||||
delay: number; holdSeconds: number; children: React.ReactNode;
|
||||
size: number; weight: number; color: string; tracking?: number;
|
||||
}> = ({ delay, holdSeconds, children, size, weight, color, tracking = 0 }) => {
|
||||
const { opacity, x } = useEnterExit(delay, holdSeconds);
|
||||
return (
|
||||
<div style={{
|
||||
opacity,
|
||||
transform: `translateX(${x}px)`,
|
||||
fontSize: size, fontWeight: weight, color,
|
||||
letterSpacing: tracking, lineHeight: 1.15, whiteSpace: "nowrap",
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LowerThird: React.FC<LowerThirdProps> = ({
|
||||
name, role, theme = BRAND, holdSeconds = 4,
|
||||
}) => {
|
||||
const { width, height } = useVideoConfig();
|
||||
const bar = useEnterExit(0, holdSeconds); // bar leads the cascade
|
||||
|
||||
const left = (width * theme.marginPct) / 100;
|
||||
const baseline = height * 0.80; // sit ~80% down, inside title-safe
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ fontFamily: theme.font }}>
|
||||
<div style={{
|
||||
position: "absolute", left, top: baseline,
|
||||
opacity: bar.opacity,
|
||||
transform: `translateX(${bar.x}px)`,
|
||||
}}>
|
||||
{/* panel behind text guarantees legibility over any plate */}
|
||||
<div style={{
|
||||
background: theme.bar, borderRadius: theme.radius,
|
||||
padding: "14px 22px 16px",
|
||||
borderLeft: `4px solid ${theme.role}`, // accent rule ties to brand
|
||||
backdropFilter: "blur(6px)",
|
||||
}}>
|
||||
<Line delay={4} holdSeconds={holdSeconds} size={46} weight={800} color={theme.name} tracking={-0.5}>
|
||||
{name}
|
||||
</Line>
|
||||
<Line delay={8} holdSeconds={holdSeconds} size={26} weight={500} color={theme.role}>
|
||||
{role}
|
||||
</Line>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Why each choice matters:
|
||||
- **`spring` per line, offset by `delay`** gives the bar→name→role cascade. Same offsets everywhere keep titles consistent.
|
||||
- **Exit derived from `durationInFrames`** means any clip length exits cleanly without per-instance tuning, and it's quicker than the enter (10f vs the spring's settle).
|
||||
- **`whiteSpace: nowrap` + left anchor** keeps long and short names pinned to the same edge — no awkward re-centering across a roster.
|
||||
- **`borderLeft` accent + semi-opaque panel** is the brand tie plus the legibility guarantee over unknown footage.
|
||||
|
||||
### Roster → many instances (one design, a whole panel)
|
||||
|
||||
Sequence the same component down a list. Each person gets a slot; only the text changes.
|
||||
|
||||
```tsx
|
||||
// Roster.tsx
|
||||
import { Series } from "remotion";
|
||||
import { LowerThird, BRAND } from "./LowerThird";
|
||||
|
||||
const ROSTER = [
|
||||
{ name: "Ada Lovelace", role: "Founder, Analytical Engines" },
|
||||
{ name: "Grace Hopper", role: "Rear Admiral, US Navy" },
|
||||
{ name: "Alan Turing", role: "Mathematician" },
|
||||
];
|
||||
|
||||
export const Roster: React.FC = () => (
|
||||
<Series>
|
||||
{ROSTER.map((p) => (
|
||||
<Series.Sequence key={p.name} durationInFrames={150 /* ~5s @30fps */}>
|
||||
<LowerThird name={p.name} role={p.role} theme={BRAND} />
|
||||
</Series.Sequence>
|
||||
))}
|
||||
</Series>
|
||||
);
|
||||
```
|
||||
|
||||
### Batch: one branded clip per person
|
||||
|
||||
Make the roster an input prop and render once per row, so each speaker becomes a standalone overlay clip — identical animation and brand, only the text differs.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# render one transparent-friendly lower-third clip per person in roster.json
|
||||
# roster.json: [{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}, ...]
|
||||
set -euo pipefail
|
||||
mkdir -p out
|
||||
jq -c '.[]' roster.json | while read -r row; do
|
||||
slug=$(echo "$row" | jq -r '.name' | tr '[:upper:] ' '[:lower:]-')
|
||||
npx remotion render LowerThird "out/${slug}.mp4" --props="$row"
|
||||
done
|
||||
```
|
||||
|
||||
For overlay-ready output with transparency, render the `LowerThird` composition (transparent background) to a codec that keeps alpha, e.g. `--codec=prores --prores-profile=4444`, or `--image-format=png` for a PNG sequence.
|
||||
|
||||
---
|
||||
|
||||
## 2. CSS / HTML version (no dependencies)
|
||||
|
||||
The same design without a framework — for the web, OBS browser sources, or a fast look-test. Includes both a **slide+fade** default and a **mask-reveal** name, plus a **wipe-bar** variant.
|
||||
|
||||
```html
|
||||
<div class="lt" style="--accent:#6EE7F0">
|
||||
<div class="lt-bar">
|
||||
<div class="lt-name">Ada Lovelace</div>
|
||||
<div class="lt-role">Founder, Analytical Engines</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:root { font-family: Inter, system-ui, sans-serif; }
|
||||
|
||||
.lt {
|
||||
position: absolute; left: 5%; top: 80%; /* title-safe, left-anchored */
|
||||
}
|
||||
.lt-bar {
|
||||
display: inline-block;
|
||||
background: rgba(12,14,22,0.86);
|
||||
backdrop-filter: blur(6px);
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 14px 22px 16px;
|
||||
/* bar leads the cascade: slide + fade in, then a faster fade out */
|
||||
animation: lt-in .5s cubic-bezier(.22,1,.36,1) both,
|
||||
lt-out .3s ease-in 4.5s forwards; /* hold ~4s, exit faster */
|
||||
}
|
||||
.lt-name {
|
||||
font-size: 46px; font-weight: 800; color: #fff; letter-spacing: -.5px;
|
||||
line-height: 1.15; white-space: nowrap;
|
||||
/* MASK REVEAL: letters wipe on left→right, staggered after the bar */
|
||||
clip-path: inset(0 100% 0 0);
|
||||
animation: lt-wipe .5s cubic-bezier(.22,1,.36,1) .12s forwards;
|
||||
}
|
||||
.lt-role {
|
||||
font-size: 26px; font-weight: 500; color: var(--accent);
|
||||
line-height: 1.15; white-space: nowrap;
|
||||
opacity: 0;
|
||||
animation: lt-fade .4s ease-out .22s forwards; /* role arrives last */
|
||||
}
|
||||
|
||||
@keyframes lt-in { from { opacity: 0; transform: translateX(-32px); }
|
||||
to { opacity: 1; transform: none; } }
|
||||
@keyframes lt-out { to { opacity: 0; transform: translateX(-16px); } }
|
||||
@keyframes lt-wipe { to { clip-path: inset(0 0 0 0); } }
|
||||
@keyframes lt-fade { to { opacity: 1; } }
|
||||
|
||||
/* Mobile/9:16: lift the band above the bottom UI/caption zone */
|
||||
@media (max-aspect-ratio: 1/1) { .lt { top: 70%; } }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Wipe-bar variant (broadcast feel)
|
||||
|
||||
Swap the bar animation so a colored bar sweeps in and the text rides behind it:
|
||||
|
||||
```css
|
||||
.lt-bar.wipe {
|
||||
position: relative; overflow: hidden;
|
||||
animation: lt-in .45s cubic-bezier(.22,1,.36,1) both,
|
||||
lt-out .3s ease-in 4.5s forwards;
|
||||
}
|
||||
.lt-bar.wipe::before {
|
||||
content: ""; position: absolute; inset: 0;
|
||||
background: var(--accent);
|
||||
transform-origin: left;
|
||||
animation: lt-sweep .55s cubic-bezier(.76,0,.24,1) both; /* sweep across, then reveal */
|
||||
}
|
||||
@keyframes lt-sweep { 0% { transform: scaleX(0); } 60% { transform: scaleX(1); }
|
||||
100% { transform: scaleX(0); transform-origin: right; } }
|
||||
```
|
||||
|
||||
The CSS clock (`animation`) is fine for live/web playback. For a deterministic MP4, prefer the Remotion path above — a frame renderer and CSS timers disagree, which causes flicker or desync at export.
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Knob |
|
||||
|---|---|
|
||||
| Slower/faster reveal | enter duration 12–18f / `.5s` |
|
||||
| Longer dwell | `holdSeconds` / the `4.5s` exit delay |
|
||||
| Bigger name dominance | name `fontSize` (keep ≥1.6× the role) |
|
||||
| Rebrand everything | the `BRAND` theme object / `--accent` |
|
||||
| 9:16 placement | `baseline 0.80→0.70` / the media query |
|
||||
|
||||
---
|
||||
## Built by the team behind iart.ai
|
||||
|
||||
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can generate branded lower-thirds from a list of names/titles, one-click — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=lower-thirds)
|
||||
@@ -1,24 +0,0 @@
|
||||
# Short-Form Video
|
||||
|
||||
Engineer a 9:16 video that survives the swipe. Short-form is a retention machine, not a small launch film: a hook that earns the first second, pattern interrupts that reset attention every few seconds, and often a loop that buys watch-time past 100%. The craft is timing, not polish. For creators making Reels, TikToks, and YouTube Shorts (7–60s, vertical).
|
||||
|
||||
## When it activates
|
||||
|
||||
- "Make a Reels/TikTok/YouTube Shorts video" or "build a short-form video template."
|
||||
- "Write a hook for a short" or "fix the first 3 seconds."
|
||||
- "Edit for retention," "add pattern interrupts," or "pace cuts for a vertical video."
|
||||
- "Make a video loop" or "set up 9:16 safe areas."
|
||||
|
||||
## Example prompts
|
||||
|
||||
- "Build a reusable 9:16 short-form template — one design, many topics."
|
||||
- "Fix the drop-off in this Reel: tighten pacing and add pattern interrupts."
|
||||
- "Write three hook options for a 30-second educational short."
|
||||
|
||||
## What's inside
|
||||
|
||||
- `SKILL.md` — the retention arc, hook grammar, and pacing rules.
|
||||
- `references/retention-pacing.md` — deeper guidance on retention editing and pacing.
|
||||
|
||||
---
|
||||
Part of **[Short-Form Video Skills](../../)** · Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=tiktok-video-skills&utm_content=skill_footer&utm_term=short-form-video)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -1,155 +0,0 @@
|
||||
---
|
||||
name: short-form-video
|
||||
description: This skill should be used when the user asks to "make a Reels/TikTok/YouTube Shorts video", "build a short-form video template", "write a hook for a short", "edit for retention", "add pattern interrupts", "make a video loop", "fix the first 3 seconds", "pace cuts for a vertical video", or "set up 9:16 safe areas". Covers the hook→retention→loop grammar of vertical short-form (distinct from a launch hype film).
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Short-Form Video
|
||||
|
||||
Engineer a 9:16 video that survives the swipe. Short-form is not a small launch film — it is a retention machine: a hook that earns the first second, pattern interrupts that reset attention every few seconds, and often a loop that buys watch-time past 100%. The craft is timing, not polish.
|
||||
|
||||
## When to use
|
||||
|
||||
- Reels / TikTok / YouTube Shorts (7–60s, vertical 9:16).
|
||||
- Retention editing: fixing drop-off, adding pattern interrupts, tightening pacing.
|
||||
- Hooks, seamless loops, repeatable short-form templates (one design, many topics).
|
||||
|
||||
## Story structure (the retention contract)
|
||||
|
||||
Short-form storytelling is a contract with the viewer's thumb: every second must re-earn the next one. The narrative is shaped *backwards from the retention curve*, not forwards from an intro. Lock these before touching the timeline.
|
||||
|
||||
- **One idea per video.** A short carries exactly one payoff. A second idea splits attention and the curve sags in the middle — make it a second video instead. If you can't name the single takeaway in a sentence, you don't have a short yet.
|
||||
- **Open a loop in the first ~3s, close it last.** The hook poses a question or gap (the open loop); the payoff answers it. The viewer stays because the loop is open — so never answer it early, and never bury the payoff under setup.
|
||||
- **No dead air.** Every beat either advances the idea or resets attention. Cut anything that does neither. Silence, slow ramps, throat-clearing intros ("hey guys, in this video…") are where viewers swipe.
|
||||
- **Why the curve, not the cut, is the unit of work.** The algorithm distributes whatever holds the per-second retention curve flat; a "good" edit that flattens the curve loses to an ugly one that keeps it up. You are editing the *curve*, and the hook gates everything after it — fix it first.
|
||||
|
||||
The narrative spine (the `Hook → Setup → Body → Payoff/loop` arc is budgeted in the table below):
|
||||
|
||||
| Move | Story job | Failure if missed |
|
||||
|---|---|---|
|
||||
| Hook | Open the loop; promise the payoff | Swipe in first 3s |
|
||||
| Tension hold | Keep the gap open through the body | Flat middle, mid-drop |
|
||||
| Payoff | Close the loop — the one idea, delivered | "Wasted my time," no share |
|
||||
| Loop / CTA | Feed back to frame 0, or one clear ask | No re-watch, no action |
|
||||
|
||||
(`The retention arc` table below assigns seconds to each move; this section is the narrative logic and ordering behind it. `Hook grammar` lists the hook archetypes.)
|
||||
|
||||
## The one metric that drives everything
|
||||
|
||||
**Retention, not production value.** The algorithm distributes whatever holds viewers; ~87% decide to stay or swipe inside 3 seconds, and the threshold for distribution sits near **70%+ average view-through** (Shorts ~73%, TikTok ~78%, Reels ~65%). Every decision below trades against that curve. A flawless shot that bores at 0:04 loses to an ugly one that holds.
|
||||
|
||||
## The retention arc
|
||||
|
||||
| Beat | Job | Budget (of a 20s short) |
|
||||
|---|---|---|
|
||||
| Hook (0–1s) | One frame + one line that creates an open loop | 0–1s |
|
||||
| Setup | Pay off *why watch* — promise, stakes, or the gap | 1–4s |
|
||||
| Body | Deliver value in beats; one idea per beat | 4–17s |
|
||||
| Payoff / loop | Close the loop — or feed it back to frame 0 | 17–20s |
|
||||
|
||||
Scale the *body*, never the hook. A 60s short has a longer body and more beats — the same 1-second hook.
|
||||
|
||||
## Hook grammar (first ~1s)
|
||||
|
||||
The hook is a sentence + a visual that opens a curiosity gap the brain needs closed. Pick one pattern and commit:
|
||||
|
||||
| Hook type | Shape | Best for |
|
||||
|---|---|---|
|
||||
| Open loop | "The reason your X keeps Y…" (answer delayed) | most content |
|
||||
| Direct promise | "3 ways to ___ in 30 seconds" | educational / listicle |
|
||||
| Pattern interrupt | snap-zoom / whip-pan / visual mismatch in frame 1 | entertainment |
|
||||
| Negation | "Stop doing ___" / "You're ___ wrong" | strong opinions |
|
||||
|
||||
Two rules: the hook line is **on-screen as text by frame 1** (85% watch muted), and the most striking *visual* is also the first frame — never a slow ramp-in. Hold a strong opening frame; do not fade up from black.
|
||||
|
||||
## Pattern interrupts & cut rhythm
|
||||
|
||||
A pattern interrupt is any change that resets attention: a hard cut, zoom punch, b-roll insert, sound effect, caption pop, or angle change. In short-form, density is high — a *visual change every ~2–4 seconds*; videos with an interrupt roughly every 4s hold ~58% vs ~41% for a static talking head. The killer is a flat middle: TikTok throttles distribution when the watch-through curve flattens, so never stitch the body from low-energy takes.
|
||||
|
||||
```js
|
||||
// Drive every interrupt off the FRAME, not wall-clock — renders deterministically.
|
||||
import { useCurrentFrame } from "remotion";
|
||||
const fps = 30;
|
||||
const cuts = [0, 1.2, 3.0, 5.4, 8.0, 11.2, 14.0]; // seconds — uneven on purpose
|
||||
const frame = useCurrentFrame();
|
||||
const shot = cuts.findIndex((t, i) => frame < (cuts[i + 1] ?? Infinity) * fps);
|
||||
```
|
||||
|
||||
Vary the interval — a metronome reads as boredom; uneven cuts read as momentum. A punch-in zoom is the cheapest interrupt that needs no new footage:
|
||||
|
||||
```jsx
|
||||
import { useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";
|
||||
const PunchIn = ({ at, children }) => {
|
||||
const frame = useCurrentFrame(); const { fps } = useVideoConfig();
|
||||
const s = spring({ frame: frame - at * fps, fps, config: { damping: 12, stiffness: 200 } });
|
||||
const scale = interpolate(s, [0, 1], [1, 1.12]); // snap to 112% on the beat
|
||||
return <div style={{ transform: `scale(${scale})`, transformOrigin: "50% 45%" }}>{children}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Captions
|
||||
|
||||
Most viewers watch muted, so on-screen captions are not optional — they carry the content and double as a pattern interrupt when each phrase pops in. Keep 4–7 words per line, high contrast, inside the safe area. **Do not re-implement caption animation here** — use the `caption-animation` skill for word-by-word timing, karaoke highlight, and styling; this skill only places captions on the retention timeline and inside the 9:16 safe zone.
|
||||
|
||||
## Loops & re-watch
|
||||
|
||||
A seamless loop counts each re-watch as fresh watch-time — a clean 8s loop watched 3× is 300% view-through, the strongest possible signal. Two techniques: match the **last frame to the first** (same composition, position, color) so the cut is invisible, and write a **sentence loop** — phrase the hook so the payoff feeds back into frame 0 ("…and that's why I never — " → loops to the start). Best length for re-watch is ~7–15s.
|
||||
|
||||
## 9:16 safe areas
|
||||
|
||||
Compose inside the universal safe zone so platform UI never covers text or faces.
|
||||
|
||||
| Zone | Pixels (1080×1920) | Avoid |
|
||||
|---|---|---|
|
||||
| Universal safe (all platforms) | center **900×1400** | anything critical outside it |
|
||||
| Top | keep clear ~120px | profile / sound UI |
|
||||
| Bottom | keep clear ~320px | captions, CTA, hashtags, audio bar |
|
||||
| Right | keep clear ~120px | like / comment / share rail |
|
||||
|
||||
Put the hook text and key subject in the center 900×1400 box. Reserve the bottom ~320px even if it looks empty in the editor — that is where the platform paints its own captions and buttons.
|
||||
|
||||
## Output checklist
|
||||
|
||||
- Hook line is on-screen text by frame 1; strongest visual is the first frame (no fade-up).
|
||||
- One open loop / promise set in the first ~4s.
|
||||
- A visual pattern interrupt every ~2–4s; intervals uneven; no flat middle.
|
||||
- Captions present (via `caption-animation`), 4–7 words, inside safe area.
|
||||
- Loop closed — matched first/last frame or a sentence that feeds frame 0.
|
||||
- Composed inside the 900×1400 center box; bottom 320px / top 120px / right 120px kept clear.
|
||||
- Every animated value derived from `useCurrentFrame()` — no CSS/library timers.
|
||||
|
||||
## Deliver & verify (rendered stills → MP4)
|
||||
|
||||
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
|
||||
|
||||
The short ships as a Remotion composition (`<Composition>` + zod `schema` + `defaultProps`) — every cut, punch-in, and interrupt driven by `useCurrentFrame()`, never `Date.now()` / `Math.random()` / timers. Deliverable = `out/*.mp4` + the project (re-render per topic). 9:16 vertical (1080×1920) is the default.
|
||||
|
||||
**Verify loop — render stills → inspect → encode.** Three frames test the three things that make or break retention: the hook, the pacing, and the loop seam.
|
||||
|
||||
```bash
|
||||
# Stills at hook / mid-body / last frame — WITH SHIPPED PROPS (the real topic, not defaults)
|
||||
npx remotion still Short out/f-hook.png --frame=10 --props='{"topic":"...","hook":"..."}' # ~first 3s reads instantly
|
||||
npx remotion still Short out/f-mid.png --frame=N --props='{"topic":"...","hook":"..."}'
|
||||
npx remotion still Short out/f-end.png --frame=L --props='{"topic":"...","hook":"..."}' # L = durationInFrames-1
|
||||
|
||||
# Inspect each PNG:
|
||||
# - HOOK frame (~frame 0-ish / first 3s): hook line is on-screen text and reads instantly; strongest visual already up (no fade-up)
|
||||
# - LOOP seam: compare f-hook(frame 0) vs f-end(last frame) — same composition/position/color for an invisible loop cut
|
||||
# - 9:16 safe area: hook + key subject inside the center 900x1400 box; clear of top ~12%, bottom ~20-35% (captions/CTA/audio), right action rail
|
||||
|
||||
npx remotion render Short out/short.mp4 --props='{"topic":"...","hook":"..."}' # encode once stills verify
|
||||
npx remotion render Short out/demo.gif --codec=gif # README proof clip
|
||||
```
|
||||
|
||||
**Per topic / batch**: verify ONE topic via stills *before* rendering all of them. Use `npx remotion compositions` for `durationInFrames`/`fps` to pick the mid + last frames.
|
||||
|
||||
**Before you finish:**
|
||||
1. Stills render cleanly at the hook, mid, and last frame — no errors.
|
||||
2. The hook frame reads instantly (on-screen text + strongest visual up front, no fade-up); first vs last frame match for a clean loop seam.
|
||||
3. Hook + subject + captions sit inside the 9:16 center 900×1400 safe box at every checked frame.
|
||||
4. Frame-driven only — no `Date.now()` / `Math.random()` / timers; cuts uneven, no flat middle.
|
||||
5. A real topic's shipped props (not `defaultProps`) render correctly; MP4 encoded, GIF optional.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/retention-pacing.md` — a complete runnable Remotion `<Short>` composition: a frame-driven shot scheduler with uneven cuts, the PunchIn pattern interrupt, an on-screen hook, a 9:16 safe-area overlay, and a seamless first/last-frame loop. Plus a hook-pattern library, a retention-curve debugging method, per-platform safe-zone maps, and the template×topic batch-render pattern.
|
||||
@@ -1,209 +0,0 @@
|
||||
# Retention Pacing — Short-Form Video Reference
|
||||
|
||||
Deep reference for the `short-form-video` skill: a full runnable Remotion short, a hook library, a retention-debugging method, per-platform safe zones, and batch output. All timing is frame-driven so renders are deterministic.
|
||||
|
||||
## 1. A complete `<Short>` composition
|
||||
|
||||
One self-contained vertical short: frame-driven shot scheduler with uneven cuts, a punch-in pattern interrupt, an on-screen hook, a 9:16 safe-area overlay (dev only), and a seamless loop. Topic content is a prop, so the same file renders many shorts.
|
||||
|
||||
```tsx
|
||||
// Short.tsx — 1080×1920, 30fps. Register at 600 frames (20s) for a clean loop.
|
||||
import {
|
||||
AbsoluteFill, useCurrentFrame, useVideoConfig,
|
||||
interpolate, spring, Sequence, Img,
|
||||
} from "remotion";
|
||||
|
||||
type Beat = { at: number; text: string; src?: string }; // at = seconds
|
||||
export type ShortProps = {
|
||||
hook: string; // on-screen by frame 1
|
||||
beats: Beat[]; // body, one idea each
|
||||
loopText: string; // sentence that feeds back to frame 0
|
||||
};
|
||||
|
||||
// --- frame-driven shot scheduler: uneven cuts read as momentum ---
|
||||
const useShot = (cuts: number[]) => {
|
||||
const { fps } = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
let i = 0;
|
||||
for (let k = 0; k < cuts.length; k++) if (frame >= cuts[k] * fps) i = k;
|
||||
const startF = cuts[i] * fps;
|
||||
const endF = (cuts[i + 1] ?? cuts[i] + 2) * fps;
|
||||
const local = (frame - startF) / (endF - startF); // 0→1 within shot
|
||||
return { index: i, local, startF };
|
||||
};
|
||||
|
||||
// --- punch-in: the cheapest pattern interrupt, snaps on the cut ---
|
||||
const PunchIn: React.FC<{ startF: number; to?: number; children: React.ReactNode }> = ({
|
||||
startF, to = 1.1, children,
|
||||
}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const s = spring({ frame: frame - startF, fps, config: { damping: 13, stiffness: 220 } });
|
||||
const scale = interpolate(s, [0, 1], [1, to]);
|
||||
return <AbsoluteFill style={{ transform: `scale(${scale})`, transformOrigin: "50% 45%" }}>{children}</AbsoluteFill>;
|
||||
};
|
||||
|
||||
// --- hook: text present from frame 0, micro-overshoot, no fade-up ---
|
||||
const Hook: React.FC<{ text: string }> = ({ text }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const pop = spring({ frame, fps, config: { damping: 9, stiffness: 260 } });
|
||||
const scale = interpolate(pop, [0, 1], [0.9, 1]);
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute", top: 360, left: 90, right: 90, // inside 900-wide safe box
|
||||
transform: `scale(${scale})`, transformOrigin: "0 0",
|
||||
fontFamily: "Inter, sans-serif", fontWeight: 800, fontSize: 76, lineHeight: 1.05,
|
||||
color: "#fff", textShadow: "0 4px 24px rgba(0,0,0,.55)",
|
||||
}}>{text}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Short: React.FC<ShortProps> = ({ hook, beats, loopText }) => {
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
const frame = useCurrentFrame();
|
||||
const cuts = [0, ...beats.map((b) => b.at)]; // hook shot + one per beat
|
||||
const { index, startF } = useShot(cuts);
|
||||
const current = index === 0 ? null : beats[index - 1];
|
||||
|
||||
// seamless loop: match the LAST 0.5s back toward the opening frame
|
||||
const tailF = durationInFrames - frame;
|
||||
const loopFade = interpolate(tailF, [0, fps * 0.5], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0b0b0f" }}>
|
||||
<PunchIn startF={startF}>
|
||||
{current?.src && <Img src={current.src} style={{ width: "100%", height: "100%", objectFit: "cover" }} />}
|
||||
</PunchIn>
|
||||
|
||||
{index === 0 && <Hook text={hook} />}
|
||||
|
||||
{/* body caption — placement only; styling/word-timing belongs to caption-animation */}
|
||||
{current && (
|
||||
<div style={{
|
||||
position: "absolute", left: 90, right: 90, bottom: 380, // above the 320px bottom UI band
|
||||
fontFamily: "Inter, sans-serif", fontWeight: 700, fontSize: 60, color: "#fff",
|
||||
textAlign: "center", textShadow: "0 3px 18px rgba(0,0,0,.6)",
|
||||
}}>{current.text}</div>
|
||||
)}
|
||||
|
||||
{/* loop sentence rises as the clip ends, easing the eye back to frame 0 */}
|
||||
<div style={{
|
||||
position: "absolute", left: 90, right: 90, bottom: 380, opacity: 1 - loopFade,
|
||||
fontFamily: "Inter, sans-serif", fontWeight: 800, fontSize: 64, color: "#fff", textAlign: "center",
|
||||
}}>{loopText}</div>
|
||||
|
||||
<SafeZone />{/* remove for final render */}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Register it with a duration that *is* the loop length:
|
||||
|
||||
```ts
|
||||
// Root.tsx
|
||||
import { Composition } from "remotion";
|
||||
import { Short } from "./Short";
|
||||
export const Root = () => (
|
||||
<Composition
|
||||
id="Short" component={Short}
|
||||
durationInFrames={600} fps={30} width={1080} height={1920}
|
||||
defaultProps={{
|
||||
hook: "You're editing Shorts wrong —",
|
||||
beats: [
|
||||
{ at: 1.2, text: "Cut every 2–4 seconds", src: "b1.jpg" },
|
||||
{ at: 4.0, text: "Never on a fixed grid", src: "b2.jpg" },
|
||||
{ at: 7.5, text: "Kill the flat middle", src: "b3.jpg" },
|
||||
{ at: 11.0, text: "Match last frame to first", src: "b4.jpg" },
|
||||
],
|
||||
loopText: "…and that's why mine loop forever",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
## 2. The 9:16 safe-area overlay (dev-only)
|
||||
|
||||
Render this on top while editing; delete it before the final export.
|
||||
|
||||
```tsx
|
||||
import { AbsoluteFill } from "remotion";
|
||||
export const SafeZone: React.FC = () => (
|
||||
<AbsoluteFill style={{ pointerEvents: "none" }}>
|
||||
{/* universal safe box: center 900×1400 in 1080×1920 */}
|
||||
<div style={{ position: "absolute", left: 90, top: 260, width: 900, height: 1400, outline: "2px dashed #39d", opacity: .6 }} />
|
||||
{/* platform UI bands to keep clear */}
|
||||
<div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 120, background: "rgba(255,0,0,.12)" }} />
|
||||
<div style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 320, background: "rgba(255,0,0,.12)" }} />
|
||||
<div style={{ position: "absolute", bottom: 320, top: 120, right: 0, width: 120, background: "rgba(255,128,0,.10)" }} />
|
||||
</AbsoluteFill>
|
||||
);
|
||||
```
|
||||
|
||||
## 3. Hook pattern library
|
||||
|
||||
The hook is a sentence that opens a gap the brain must close, plus the most striking frame as frame 0. Copyable shapes:
|
||||
|
||||
| Pattern | Template | Note |
|
||||
|---|---|---|
|
||||
| Open loop | "The real reason your ___ keeps ___ …" | answer withheld until payoff |
|
||||
| Direct promise | "___ in 30 seconds, no ___ needed" | name the payoff + the timebox |
|
||||
| Negation | "Stop ___. You're doing it backwards." | strong-opinion content |
|
||||
| Pattern interrupt | snap-zoom / whip / hard visual mismatch | entertainment, no text needed |
|
||||
| Stakes | "This ___ cost me $___ — don't repeat it" | story / case-study |
|
||||
| Listicle | "3 ___ that ___ (the 3rd one ___ )" | tees up a forward reference |
|
||||
|
||||
Rules that matter more than the wording: (1) the hook text is on-screen by frame 1 — most viewers are muted; (2) frame 0 is the best frame, never a fade-up from black; (3) one gap only — stacking two hooks closes neither.
|
||||
|
||||
## 4. Retention-curve debugging
|
||||
|
||||
Treat the per-second retention graph (TikTok Analytics, YT Studio "Audience retention", Reels) as a profiler.
|
||||
|
||||
| Symptom on the curve | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| Cliff at 0:00–0:02 | weak hook / slow first frame | new hook line; cut the ramp-in; lead with the best frame |
|
||||
| Steady gentle decline | normal — leave it | — |
|
||||
| Flat-then-drop in the middle | low-energy body, cuts too sparse | add interrupts; tighten cut spacing to 2–4s |
|
||||
| Bump back up near the end | viewers are looping | lean in — tighten the loop, shorten total length |
|
||||
| Spike at one timestamp | a re-watched moment | front-load that beat or make it the hook |
|
||||
|
||||
Target ≥70% average view-through. Iterate the hook first — it moves the curve more than any other edit because it gates everything after it.
|
||||
|
||||
## 5. Per-platform safe zones
|
||||
|
||||
All on a 1080×1920 canvas. The universal box (center 900×1400) is the intersection; design inside it and one master fits every platform.
|
||||
|
||||
| Platform | Top clear | Bottom clear | Right clear | Notes |
|
||||
|---|---|---|---|---|
|
||||
| Universal (use this) | ~120px | ~320px | ~120px | center 900×1400 holds on all |
|
||||
| TikTok | ~108px | ~320px | ~120px | caption + button rail at bottom-right |
|
||||
| Instagram Reels | ~120px | ~310px | ~84px | audio bar + caption bottom |
|
||||
| YouTube Shorts | ~120px | ~300px | ~60px | channel + subscribe bottom band |
|
||||
| TikTok Ads | ~108px | ~370px | ~120px | extra room for the CTA button |
|
||||
|
||||
When unsure, design to the *largest* margin (TikTok Ads bottom 370px) and nothing ever clips.
|
||||
|
||||
## 6. Template × topic — batch output
|
||||
|
||||
The payoff of code-driven shorts: lock the design once, swap only the props, render N videos. Make topic content a prop (already done above), keep one theme object, and loop over JSON files.
|
||||
|
||||
```jsx
|
||||
// theme.ts — one source of truth so 50 shorts stay on-brand
|
||||
export const theme = { font: "Inter", hookSize: 76, bodySize: 60, bg: "#0b0b0f", fg: "#fff", accent: "#39d" };
|
||||
```
|
||||
|
||||
```bash
|
||||
# render the same Short template for every topic file in /topics
|
||||
for f in topics/*.json; do
|
||||
name=$(basename "$f" .json)
|
||||
npx remotion render Short "out/$name.mp4" --props="$f"
|
||||
done
|
||||
```
|
||||
|
||||
Each `topics/*.json` is one `ShortProps` object (`hook`, `beats`, `loopText`). Keep the hook line, cut points, and loop sentence per-topic; everything else stays fixed so the series reads as one brand.
|
||||
|
||||
---
|
||||
## Built by the team behind iart.ai
|
||||
|
||||
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can batch-produce short-form videos from one template × data — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=short-form-video)
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/caption-animation
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/countdown-video
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/lower-thirds
|
||||
@@ -1,102 +0,0 @@
|
||||
---
|
||||
name: remotion-video
|
||||
description: Create videos from React code with Remotion. Use when the user asks to make, animate, preview, or render a video, motion graphic, intro, caption, or social clip using code.
|
||||
---
|
||||
|
||||
# Remotion Video
|
||||
|
||||
Remotion makes videos out of React components. Each frame is a web page. Render them in order and you get an MP4.
|
||||
|
||||
## Core ideas
|
||||
- **Composition**: one video. Has `width`, `height`, `fps`, `durationInFrames`.
|
||||
- **Frame**: `useCurrentFrame()` returns the current frame number. All animation is a function of the frame.
|
||||
- **`interpolate`**: maps a frame range to a value range (fade, slide).
|
||||
- **`spring`**: natural bouncy motion.
|
||||
- **`Sequence`**: shows children only during a frame range.
|
||||
- **`AbsoluteFill`**: full-screen layer; stack them for backgrounds.
|
||||
- **`Audio` / `OffthreadVideo` / `Img`**: media. Put files in `public/` and load with `staticFile("name.mp3")`.
|
||||
|
||||
## Workflow
|
||||
1. **Plan first.** Ask or decide: length (seconds), size (1920x1080, or 1080x1920 for vertical), fps (30), and the scenes. Keep scenes to a short list.
|
||||
2. **Set up** (skip if the project exists):
|
||||
```
|
||||
npx create-video@latest
|
||||
cd <project> && npm install
|
||||
```
|
||||
3. **Write** compositions in `src/`, register them in `src/Root.tsx`.
|
||||
4. **Preview**: `npm run dev` (opens Remotion Studio).
|
||||
5. **Render**: `npx remotion render <CompositionId> out/video.mp4`
|
||||
Still frame: `npx remotion still <CompositionId> out/frame.png --frame=30`
|
||||
6. **Verify**: render one still first, check it looks right, then render the full video.
|
||||
|
||||
## Starter example
|
||||
`src/Root.tsx`
|
||||
```tsx
|
||||
import { Composition } from "remotion";
|
||||
import { Hello } from "./Hello";
|
||||
|
||||
export const RemotionRoot = () => (
|
||||
<Composition
|
||||
id="Hello"
|
||||
component={Hello}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
width={1920}
|
||||
height={1080}
|
||||
defaultProps={{ title: "Hello, world" }}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
`src/Hello.tsx`
|
||||
```tsx
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
|
||||
export const Hello = ({ title }: { title: string }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
|
||||
const scale = spring({ frame, fps, config: { damping: 12 } });
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0b1020", justifyContent: "center", alignItems: "center" }}>
|
||||
<h1 style={{ color: "white", fontSize: 120, opacity, transform: `scale(${scale})` }}>{title}</h1>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Rules of thumb
|
||||
- Never animate with CSS transitions, `setTimeout`, or CSS animations. They don't render per frame. Drive everything from `useCurrentFrame()`.
|
||||
- Always pass `extrapolateRight: "clamp"` to `interpolate` unless you want values to keep growing.
|
||||
- Seconds to frames: `seconds * fps`.
|
||||
- Use `<Sequence from={..} durationInFrames={..}>` to order scenes; `<Series>` to chain them.
|
||||
- Load fonts with `@remotion/google-fonts` so they are ready before rendering.
|
||||
- Keep props in `defaultProps` so the user can edit text in the Studio.
|
||||
- Rendering needs Chromium and ffmpeg (Remotion installs/handles these); first render can be slow.
|
||||
|
||||
## Fonts
|
||||
Google Fonts: `npm i @remotion/google-fonts`, then
|
||||
```tsx
|
||||
import { loadFont } from "@remotion/google-fonts/Inter";
|
||||
const { fontFamily } = loadFont(); // use as style={{ fontFamily }}
|
||||
```
|
||||
It downloads the font while rendering, so the machine needs internet access to fonts.googleapis.com. If the render fails with "Failed to load font", the network is blocking it: use a local font file in `public/` with `@remotion/fonts` (`loadFont({ family, url: staticFile("x.woff2") })`) or a system font. (Not verified in the sandbox where this skill was tested, which had no access to Google.)
|
||||
|
||||
## Common problems
|
||||
- **Can't download the browser (no internet / locked-down machine)**: point Remotion at an installed Chromium:
|
||||
`npx remotion render <Id> out/video.mp4 --browser-executable=/path/to/chromium --chrome-mode=chrome-for-testing`
|
||||
- **Blank/white frames**: an asset path is wrong; use `staticFile()`.
|
||||
- **Flicker**: an image or font not loaded; use `<Img>` and `@remotion/google-fonts`.
|
||||
- **Slow render**: lower resolution/fps while testing, or use `--concurrency`.
|
||||
|
||||
## Templates
|
||||
Copy-paste starting points. Read the one that fits:
|
||||
- `templates/captions.md` - words highlighted in time with speech
|
||||
- `templates/charts.md` - animated bar chart
|
||||
- `templates/intro-outro.md` - title card + chaining scenes with `Series`
|
||||
- `templates/audio-voiceover.md` - background music, voiceover, auto video length
|
||||
- `templates/vertical.md` - 1080x1920 clips for Reels/TikTok/Shorts
|
||||
|
||||
## Docs
|
||||
https://www.remotion.dev/docs
|
||||
@@ -1,62 +0,0 @@
|
||||
# Template: Audio, music and voiceover
|
||||
|
||||
Put sound files in `public/` (mp3 or wav). Load them with `staticFile()`.
|
||||
|
||||
`src/Narrated.tsx`
|
||||
```tsx
|
||||
import { AbsoluteFill, Audio, Sequence, interpolate, staticFile, useVideoConfig } from "remotion";
|
||||
import { getAudioDurationInSeconds } from "@remotion/media-utils";
|
||||
import type { CalculateMetadataFunction } from "remotion";
|
||||
|
||||
type Props = { title: string; voice: string; music: string };
|
||||
|
||||
// Make the video exactly as long as the voiceover (+1 second of padding).
|
||||
export const calcNarratedMetadata: CalculateMetadataFunction<Props> = async ({ props }) => {
|
||||
const seconds = await getAudioDurationInSeconds(staticFile(props.voice));
|
||||
return { durationInFrames: Math.ceil((seconds + 1) * 30) };
|
||||
};
|
||||
|
||||
export const Narrated = ({ title, voice, music }: Props) => {
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
return (
|
||||
<AbsoluteFill style={{ background: "#0b1020", justifyContent: "center", alignItems: "center" }}>
|
||||
<h1 style={{ color: "white", fontSize: 110 }}>{title}</h1>
|
||||
|
||||
{/* Background music: quiet, fades out over the last second. */}
|
||||
<Audio
|
||||
src={staticFile(music)}
|
||||
volume={(f) => interpolate(f, [durationInFrames - fps, durationInFrames], [0.15, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })}
|
||||
/>
|
||||
|
||||
{/* Voiceover starts half a second in, full volume. */}
|
||||
<Sequence from={Math.round(fps * 0.5)}>
|
||||
<Audio src={staticFile(voice)} volume={1} />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Register it (note `calculateMetadata`):
|
||||
```tsx
|
||||
<Composition
|
||||
id="Narrated"
|
||||
component={Narrated}
|
||||
calculateMetadata={calcNarratedMetadata}
|
||||
durationInFrames={300} // placeholder, replaced by calculateMetadata
|
||||
fps={30} width={1920} height={1080}
|
||||
defaultProps={{ title: "My story", voice: "voice.mp3", music: "music.mp3" }}
|
||||
/>
|
||||
```
|
||||
|
||||
Install: `npm i @remotion/media-utils`
|
||||
|
||||
Getting a voiceover
|
||||
- Record yourself, or generate speech with a text-to-speech service, and save as `public/voice.mp3`. Never commit API keys; keep them in environment variables.
|
||||
- Pair with `templates/captions.md`: feed it word timings from a transcription of the same file.
|
||||
|
||||
Rules
|
||||
- Music volume 0.1-0.2 so the voice stays clear.
|
||||
- `<Sequence from={...}>` delays when a sound starts.
|
||||
- Audio is only in the final render (mp4). Stills (`remotion still`) have no sound.
|
||||
- Check the sound made it in: `ffprobe out/video.mp4` should list an `Audio:` stream.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Template: Captions
|
||||
|
||||
Words appear in time with speech. Give each word a start time in seconds.
|
||||
|
||||
`src/Captions.tsx`
|
||||
```tsx
|
||||
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
|
||||
type Word = { text: string; start: number; end: number }; // seconds
|
||||
|
||||
export const Captions = ({ words, background = "#111" }: { words: Word[]; background?: string }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const t = frame / fps;
|
||||
|
||||
// Show a window of up to 5 words around the current one.
|
||||
const current = words.findIndex((w) => t >= w.start && t < w.end);
|
||||
const i = current === -1 ? 0 : current;
|
||||
const visible = words.slice(Math.floor(i / 5) * 5, Math.floor(i / 5) * 5 + 5);
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: background, justifyContent: "flex-end", alignItems: "center", paddingBottom: 200 }}>
|
||||
<div style={{ fontSize: 72, fontWeight: 800, color: "white", textShadow: "0 4px 12px black", textAlign: "center", maxWidth: "80%" }}>
|
||||
{visible.map((w, k) => (
|
||||
<span key={k} style={{ color: t >= w.start && t < w.end ? "#ffd400" : "white", marginRight: 16 }}>
|
||||
{w.text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Tips
|
||||
- Get word times from a transcription tool (e.g. `@remotion/captions` or Whisper) and save them as JSON in `public/`.
|
||||
- `background` is the backdrop when there is no video. Pass `"transparent"` when layering over a video.
|
||||
- Layer it over the video: `<AbsoluteFill><OffthreadVideo src={staticFile("clip.mp4")} /><Captions words={words} background="transparent" /></AbsoluteFill>`.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Template: Animated bar chart
|
||||
|
||||
Bars grow one after another.
|
||||
|
||||
`src/BarChart.tsx`
|
||||
```tsx
|
||||
import { AbsoluteFill, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
|
||||
type Bar = { label: string; value: number };
|
||||
|
||||
export const BarChart = ({ data, color = "#4f8cff" }: { data: Bar[]; color?: string }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const max = Math.max(...data.map((d) => d.value));
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#0b1020", padding: 120, justifyContent: "flex-end" }}>
|
||||
<div style={{ display: "flex", alignItems: "flex-end", gap: 40, height: 700 }}>
|
||||
{data.map((d, i) => {
|
||||
const grow = spring({ frame: frame - i * 6, fps, config: { damping: 15 } });
|
||||
return (
|
||||
<div key={d.label} style={{ flex: 1, textAlign: "center", color: "white", fontSize: 36 }}>
|
||||
<div style={{ height: (d.value / max) * 600 * grow, background: color, borderRadius: 8 }} />
|
||||
<div style={{ marginTop: 16 }}>{d.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Tips
|
||||
- Delay each bar with `frame - i * 6` (6 frames apart).
|
||||
- For lines, animate `strokeDashoffset` on an SVG path using `interpolate`.
|
||||
- Keep numbers readable: show the value as text, rounded.
|
||||
@@ -1,34 +0,0 @@
|
||||
# Template: Title intro / outro card
|
||||
|
||||
A short card (2-3 seconds) with a title and subtitle. Use at the start or end.
|
||||
|
||||
`src/TitleCard.tsx`
|
||||
```tsx
|
||||
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
||||
|
||||
export const TitleCard = ({ title, subtitle }: { title: string; subtitle?: string }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
const rise = spring({ frame, fps, config: { damping: 14 } });
|
||||
const fadeOut = interpolate(frame, [durationInFrames - 15, durationInFrames], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ background: "linear-gradient(135deg,#1b2a6b,#0b1020)", justifyContent: "center", alignItems: "center", opacity: fadeOut }}>
|
||||
<h1 style={{ color: "white", fontSize: 130, margin: 0, transform: `translateY(${(1 - rise) * 60}px)`, opacity: rise }}>{title}</h1>
|
||||
{subtitle && <p style={{ color: "#9fb3ff", fontSize: 48, opacity: rise }}>{subtitle}</p>}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Chain with the main video using `<Series>`:
|
||||
```tsx
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={90}><TitleCard title="My Video" /></Series.Sequence>
|
||||
<Series.Sequence durationInFrames={600}><MainScene /></Series.Sequence>
|
||||
<Series.Sequence durationInFrames={90}><TitleCard title="Thanks for watching" /></Series.Sequence>
|
||||
</Series>
|
||||
```
|
||||
@@ -1,23 +0,0 @@
|
||||
# Template: Vertical clips (Reels / TikTok / Shorts)
|
||||
|
||||
Same code, different size. Register a vertical composition:
|
||||
|
||||
```tsx
|
||||
<Composition
|
||||
id="Vertical"
|
||||
component={MyScene}
|
||||
width={1080}
|
||||
height={1920}
|
||||
fps={30}
|
||||
durationInFrames={30 * 30} // 30 seconds
|
||||
/>
|
||||
```
|
||||
|
||||
Render: `npx remotion render Vertical out/vertical.mp4`
|
||||
|
||||
Rules
|
||||
- Keep text inside the **safe zone**: about 150px from the top, 350px from the bottom (apps cover those parts with buttons and captions), and 60px from the sides.
|
||||
- Use big text (60px+). Phone screens are small.
|
||||
- Keep clips under 60 seconds (Shorts limit); 15-30 seconds does best.
|
||||
- Use `useVideoConfig()` for width/height instead of hard-coding, so the same scene works wide and tall.
|
||||
- Put captions in the lower-middle, above the bottom safe zone (see captions.md).
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/short-form-video
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"caption-animation": {
|
||||
"source": "iart-ai/tiktok-video-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caption-animation/SKILL.md",
|
||||
"computedHash": "e42c4828d3c6115dfb4d8fd3c3fd485b8e6a89c35a8835eee6dae39fb83210a4"
|
||||
},
|
||||
"countdown-video": {
|
||||
"source": "iart-ai/tiktok-video-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/countdown-video/SKILL.md",
|
||||
"computedHash": "dafb9127ededa76d0df08be73adc46e18d6ec3955153f6ecfd0f928984ce95b2"
|
||||
},
|
||||
"lower-thirds": {
|
||||
"source": "iart-ai/tiktok-video-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/lower-thirds/SKILL.md",
|
||||
"computedHash": "502c1a8a491e477dd855eb1a45469c09f6ff46a72ea841838d358d0a6a9fd790"
|
||||
},
|
||||
"short-form-video": {
|
||||
"source": "iart-ai/tiktok-video-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/short-form-video/SKILL.md",
|
||||
"computedHash": "893ea19615f3199159b9fe94f05e37a5e538e7e0c3af7f77f9f9bed7ff933423"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
.hyperframes/artifacts/
|
||||
node_modules/
|
||||
@@ -0,0 +1,233 @@
|
||||
<!--
|
||||
Daisy Days — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look. product-launch-video's
|
||||
Step 2 copies it into the project as caption-skin.html; captions.mjs then fills the
|
||||
three reserved holes, wraps the fragment in a <template>, and writes
|
||||
compositions/captions.html:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (captions.mjs injects it from
|
||||
frame.md, so the Step-2 brand overlay flows through; each var has a daisy-days literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a cheerful picture-book card — a cream surface inside a 3px charcoal outline
|
||||
with a hard 6px charcoal offset shadow (no blur) and a generous 24px radius, the
|
||||
sticker-on-paper signature. Fredoka One glyphs, sentence case, charcoal ink. Upcoming
|
||||
words sit in muted charcoal; the current word pops into a rounded sunny-pastel candy
|
||||
block (coral fill, 3px charcoal outline, hard 3px offset shadow, 12px radius) with a
|
||||
bouncy scale pop; spoken words settle to solid charcoal, the candy block cleared.
|
||||
-->
|
||||
<script src="assets/vendor/gsap.min.js"></script>
|
||||
|
||||
<!-- captions.mjs injects the project's :root tokens here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Fredoka One"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE picture-book card — cream surface, 3px charcoal outline, generous 24px radius,
|
||||
a hard 6px charcoal offset shadow (no blur): the sticker-on-paper signature */
|
||||
.caption-pill {
|
||||
max-width: 80%;
|
||||
padding: 18px 40px 22px;
|
||||
background: var(--cap-canvas, #f5f0e6);
|
||||
border: 3px solid var(--cap-ink, #2d2d2d);
|
||||
border-radius: 24px;
|
||||
box-shadow: 6px 6px 0 var(--cap-ink, #2d2d2d);
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.18em 0.32em;
|
||||
font-family: var(--font-display, "Fredoka"), sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: clamp(38px, 4.2vw, 56px);
|
||||
line-height: 1.22;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* upcoming — muted charcoal, no candy block */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0.02em 0.16em;
|
||||
border-radius: 12px;
|
||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 52%, var(--cap-canvas, #f5f0e6));
|
||||
}
|
||||
/* current word — THE sunny-pastel candy block: a rounded coral chip with a 3px
|
||||
charcoal outline + a hard 3px charcoal offset shadow (no blur), charcoal on top */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #2d2d2d);
|
||||
background: var(--cap-accent, #f8635f);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 0 0 3px var(--cap-ink, #2d2d2d),
|
||||
3px 5px 0 0 var(--cap-ink, #2d2d2d);
|
||||
}
|
||||
/* already spoken — solid charcoal, candy block cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #2d2d2d);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — a butter candy chip, the cheerful second pastel */
|
||||
.caption-word.cap-brand {
|
||||
color: var(--cap-ink, #2d2d2d);
|
||||
background: var(--cap-accent-2, #fde68a);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 0 3px var(--cap-ink, #2d2d2d);
|
||||
}
|
||||
/* numerals / data — Quicksand, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "Quicksand"), system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 78%, var(--cap-canvas, #f5f0e6));
|
||||
}
|
||||
/* emphasis — a butter underline rule (the second sunny pastel) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 3px;
|
||||
text-decoration-color: var(--cap-accent-2, #fde68a);
|
||||
text-underline-offset: 0.12em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(32px, 5.2vw, 46px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// captions.mjs injects the engine's scene-aware groups + real duration here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.82 }, { scale: 1, duration: 0.2, ease: "back.out(2.6)" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
635d4927-f2e8-44dd-9cbf-1b5d4f285f53
|
||||
@@ -0,0 +1 @@
|
||||
{"version":1,"paths":["renders/video.mp4"]}
|
||||
@@ -0,0 +1,939 @@
|
||||
{
|
||||
"version": 1,
|
||||
"historyId": "635d4927-f2e8-44dd-9cbf-1b5d4f285f53",
|
||||
"currentRevision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"pendingChanges": false,
|
||||
"files": {
|
||||
".media/manifest.jsonl": {
|
||||
"hash": "0619ff69adc91e800fdbfe4cd508b67d21791a1ef4eb8b043518158c0ce3483e",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/06/0619ff69adc91e800fdbfe4cd508b67d21791a1ef4eb8b043518158c0ce3483e"
|
||||
},
|
||||
"AGENTS.md": {
|
||||
"hash": "1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/1d/1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5"
|
||||
},
|
||||
"BRIEF.md": {
|
||||
"hash": "14d649b0556246e716324f80fc2b65b27aaa4a25d5f508c9e6fceb702f29ebb4",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/14/14d649b0556246e716324f80fc2b65b27aaa4a25d5f508c9e6fceb702f29ebb4"
|
||||
},
|
||||
"CLAUDE.md": {
|
||||
"hash": "1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/1d/1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5"
|
||||
},
|
||||
"SCRIPT.md": {
|
||||
"hash": "5fbae06748238e1717948353b077a8776b7f88221790923652363834c49fad9a",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/5f/5fbae06748238e1717948353b077a8776b7f88221790923652363834c49fad9a"
|
||||
},
|
||||
"STORYBOARD.md": {
|
||||
"hash": "34f5c1242e964e76cf330da278bb80d9174ba52197439863a9c32f6a2bf23383",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/34/34f5c1242e964e76cf330da278bb80d9174ba52197439863a9c32f6a2bf23383"
|
||||
},
|
||||
"assets/fonts/fredoka-600.woff2": {
|
||||
"hash": "b62f2898de150f019961f4884a49f83dcd2ce74375be5e0992c594ebb8f30294",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/b6/b62f2898de150f019961f4884a49f83dcd2ce74375be5e0992c594ebb8f30294"
|
||||
},
|
||||
"assets/fonts/quicksand-var.woff2": {
|
||||
"hash": "2add7d60b1cd2ab84c9967e23d5ec08eb3fc9635c46855b17d59404dec6b410e",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/2a/2add7d60b1cd2ab84c9967e23d5ec08eb3fc9635c46855b17d59404dec6b410e"
|
||||
},
|
||||
"assets/voice/01.wav": {
|
||||
"hash": "ef67616aef5fe0312c5a5a833eaba38438d7a1248057dcdd00b4d7053b064e98",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/ef/ef67616aef5fe0312c5a5a833eaba38438d7a1248057dcdd00b4d7053b064e98"
|
||||
},
|
||||
"assets/voice/02.wav": {
|
||||
"hash": "c4b74e279af592152edbe412bc477e2fe8da05cb5add99bdbd55ed536d80cc1b",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c4/c4b74e279af592152edbe412bc477e2fe8da05cb5add99bdbd55ed536d80cc1b"
|
||||
},
|
||||
"assets/voice/03.wav": {
|
||||
"hash": "e8537998055079e160467161206dcc3c51d8cc98411f89f42e478f3672fe9630",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/e8/e8537998055079e160467161206dcc3c51d8cc98411f89f42e478f3672fe9630"
|
||||
},
|
||||
"assets/voice/04.wav": {
|
||||
"hash": "e9e118731fb136fab2751e5392c47a1b89598acac4bee763962ba0e6f3ede377",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/e9/e9e118731fb136fab2751e5392c47a1b89598acac4bee763962ba0e6f3ede377"
|
||||
},
|
||||
"assets/voice/05.wav": {
|
||||
"hash": "70c9e8aad9f6f0167e2d9e31d830a27e0aa313e1307085b73e858c8108dc83e8",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/70/70c9e8aad9f6f0167e2d9e31d830a27e0aa313e1307085b73e858c8108dc83e8"
|
||||
},
|
||||
"assets/voice/06.wav": {
|
||||
"hash": "9441a5b1472967c809efb6ff246e25800e74fd9e6372a748c73568e4450108c3",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/94/9441a5b1472967c809efb6ff246e25800e74fd9e6372a748c73568e4450108c3"
|
||||
},
|
||||
"assets/voice/07.wav": {
|
||||
"hash": "4fbc575c406ead86d77a06c79dcd680303c7a935147208936c83969d0b4121b3",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/4f/4fbc575c406ead86d77a06c79dcd680303c7a935147208936c83969d0b4121b3"
|
||||
},
|
||||
"assets/voice/08.wav": {
|
||||
"hash": "7859a7ded9d7d2fe821b93162b97790c70b6929ff07211f230f9cc0f75764e95",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/78/7859a7ded9d7d2fe821b93162b97790c70b6929ff07211f230f9cc0f75764e95"
|
||||
},
|
||||
"assets/voice/09.wav": {
|
||||
"hash": "0864df842c3d65cfb43aafd936de8bc963cd91aca56248475a8eb2c968fd4a55",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/08/0864df842c3d65cfb43aafd936de8bc963cd91aca56248475a8eb2c968fd4a55"
|
||||
},
|
||||
"audio_engine_meta.json": {
|
||||
"hash": "4f2da6bf20a3982fb9ed326e7a457cac7e7128ef7e5e08d585f58bbf47a8c397",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/4f/4f2da6bf20a3982fb9ed326e7a457cac7e7128ef7e5e08d585f58bbf47a8c397"
|
||||
},
|
||||
"audio_meta.json": {
|
||||
"hash": "ecc4980e89d34d9873cdc8a92f954b38dcf094112a5bab89fde7fe98a321b624",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/ec/ecc4980e89d34d9873cdc8a92f954b38dcf094112a5bab89fde7fe98a321b624"
|
||||
},
|
||||
"audio_request.json": {
|
||||
"hash": "76765154cffa684843af5b4bc6492f4e22d7a6aa9d2b6a716351eceab241a464",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/76/76765154cffa684843af5b4bc6492f4e22d7a6aa9d2b6a716351eceab241a464"
|
||||
},
|
||||
"caption-overrides.json": {
|
||||
"hash": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/37/37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570"
|
||||
},
|
||||
"caption_groups.json": {
|
||||
"hash": "8b53722d14f329a000bcd109192eaedf8f098e16f4e553381c1a139843f8fb94",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/8b/8b53722d14f329a000bcd109192eaedf8f098e16f4e553381c1a139843f8fb94"
|
||||
},
|
||||
"capture/extracted/asset-descriptions.md": {
|
||||
"hash": "92c2cfe647902a93e595e21cebab23e9af1a2fefbec14cf22d0a5af437dc6964",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/92/92c2cfe647902a93e595e21cebab23e9af1a2fefbec14cf22d0a5af437dc6964"
|
||||
},
|
||||
"capture/extracted/tokens.json": {
|
||||
"hash": "2f533a1d991bdf84ad653ceb0061fbefd42a19f7b4dc22fe9b37f7156ce79c32",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/2f/2f533a1d991bdf84ad653ceb0061fbefd42a19f7b4dc22fe9b37f7156ce79c32"
|
||||
},
|
||||
"capture/extracted/visible-text.txt": {
|
||||
"hash": "39c044a9ee527c4a857cb65b5f0dc3f855fca4a7d17beb8ffb2cf24089f5d54f",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/39/39c044a9ee527c4a857cb65b5f0dc3f855fca4a7d17beb8ffb2cf24089f5d54f"
|
||||
},
|
||||
"compositions/captions.html": {
|
||||
"hash": "c7a724d68db626674aa77f2e4b9967b63cb606d3dd1156796d0a319e23f1e0e8",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c7/c7a724d68db626674aa77f2e4b9967b63cb606d3dd1156796d0a319e23f1e0e8"
|
||||
},
|
||||
"compositions/frames/01-hook.html": {
|
||||
"hash": "48852887a870cc3daa0c98e65504ee207475b7add2b4eecb79fe304b0539441b",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/48/48852887a870cc3daa0c98e65504ee207475b7add2b4eecb79fe304b0539441b"
|
||||
},
|
||||
"compositions/frames/02-worry.html": {
|
||||
"hash": "36c53f5f5bdcbf9ebb6a7c73dc22017e80dfc64bd98470d78af8ef7846bb1d94",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/36/36c53f5f5bdcbf9ebb6a7c73dc22017e80dfc64bd98470d78af8ef7846bb1d94"
|
||||
},
|
||||
"compositions/frames/03-meet.html": {
|
||||
"hash": "33f5f3cc5d827610300ff1198268b19f675d3adc26898e76ed11f7a89be5ef67",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/33/33f5f3cc5d827610300ff1198268b19f675d3adc26898e76ed11f7a89be5ef67"
|
||||
},
|
||||
"compositions/frames/04-limits.html": {
|
||||
"hash": "fd4173fd65eff6f6ca43680ba4824545ae6bb57eb8710780a035c9dd120ebc37",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/fd/fd4173fd65eff6f6ca43680ba4824545ae6bb57eb8710780a035c9dd120ebc37"
|
||||
},
|
||||
"compositions/frames/05-apps.html": {
|
||||
"hash": "5da142d1be64b6d51d5c707ac913fdf819a0419288696974c2775b1f35e3654e",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/5d/5da142d1be64b6d51d5c707ac913fdf819a0419288696974c2775b1f35e3654e"
|
||||
},
|
||||
"compositions/frames/06-location.html": {
|
||||
"hash": "199c8b0642ea141b105b1eb7f62ff545b4b3f77dcbf1767f6581cdceac9ac019",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/19/199c8b0642ea141b105b1eb7f62ff545b4b3f77dcbf1767f6581cdceac9ac019"
|
||||
},
|
||||
"compositions/frames/07-balance.html": {
|
||||
"hash": "6ed5fc2fb15ca1573c5ff92fc4d6c73aa89808776f0236d1934f0537b3e42a8e",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/6e/6ed5fc2fb15ca1573c5ff92fc4d6c73aa89808776f0236d1934f0537b3e42a8e"
|
||||
},
|
||||
"compositions/frames/08-platforms.html": {
|
||||
"hash": "b0a5b3c9b1d4bd94c0b3f9f7b71e7137632d8e400add3f5e93958acb7036d99a",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/b0/b0a5b3c9b1d4bd94c0b3f9f7b71e7137632d8e400add3f5e93958acb7036d99a"
|
||||
},
|
||||
"compositions/frames/09-cta.html": {
|
||||
"hash": "d9e539932cec2431c43f6125a26bb452abc66a2f7c616b1d1e26da5911e80e07",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/d9/d9e539932cec2431c43f6125a26bb452abc66a2f7c616b1d1e26da5911e80e07"
|
||||
},
|
||||
"frame.md": {
|
||||
"hash": "42d827dbd279990052e171d04380b5e6bbc111ae391dc283deb7a452e70b9b97",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/42/42d827dbd279990052e171d04380b5e6bbc111ae391dc283deb7a452e70b9b97"
|
||||
},
|
||||
"hyperframes.json": {
|
||||
"hash": "44d4e8dcefebce0a13f5afa82aa270b0b19a1d3f82e434f92616010cee775dc4",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/44/44d4e8dcefebce0a13f5afa82aa270b0b19a1d3f82e434f92616010cee775dc4"
|
||||
},
|
||||
"index.html": {
|
||||
"hash": "7f9f086ba7fb700d1308e5f9efee068c11034983a04dfc6baed7b8096034ea32",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/7f/7f9f086ba7fb700d1308e5f9efee068c11034983a04dfc6baed7b8096034ea32"
|
||||
},
|
||||
"meta.json": {
|
||||
"hash": "1497903acd6ced7f19d4cf6c76f104f5656593c484427d89aea883c8246ad541",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/14/1497903acd6ced7f19d4cf6c76f104f5656593c484427d89aea883c8246ad541"
|
||||
},
|
||||
"package.json": {
|
||||
"hash": "6873ac1737fc27fd1046b92a493604be2ebc328b232d134f04f3b65c07200b4f",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/68/6873ac1737fc27fd1046b92a493604be2ebc328b232d134f04f3b65c07200b4f"
|
||||
},
|
||||
"assets/vendor/gsap.min.js": {
|
||||
"hash": "c174bfce53a729418d57a8ad8625e7247c793a22fef8e2851e3cfa3de9cd8280",
|
||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c1/c174bfce53a729418d57a8ad8625e7247c793a22fef8e2851e3cfa3de9cd8280"
|
||||
}
|
||||
},
|
||||
"revisions": {
|
||||
"ad80a38d-f38a-4cc1-9042-d179e2b0e6f3": {
|
||||
"actor": {
|
||||
"kind": "outside",
|
||||
"name": "Outside"
|
||||
},
|
||||
"label": "Changed outside the app",
|
||||
"startedAt": 1792016103184,
|
||||
"endedAt": 1792016103225,
|
||||
"files": {
|
||||
"compositions/frames/03-meet.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/96/96f8c3849528fd883b0c70f66c0fd291aa3030adaaa346a8ebdf3c95aeb38d7e",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/33/33f5f3cc5d827610300ff1198268b19f675d3adc26898e76ed11f7a89be5ef67"
|
||||
},
|
||||
"compositions/frames/09-cta.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/a9/a924e8f0e68f92423d70d4d8e086d81f0596cec3d6ad1120d1c7e930fd086fad",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/35/3566b01fb9bb64e4d22e86877179c7b7cbee0a98f4bb3c354c97db2b85b13b15"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2748be21-fde2-40ba-a2c0-fddfb7e47431": {
|
||||
"actor": {
|
||||
"kind": "outside",
|
||||
"name": "Outside"
|
||||
},
|
||||
"label": "Changed outside the app",
|
||||
"startedAt": 1792016129829,
|
||||
"endedAt": 1792016129859,
|
||||
"files": {
|
||||
"assets/vendor/gsap.min.js": {
|
||||
"before": null,
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c1/c174bfce53a729418d57a8ad8625e7247c793a22fef8e2851e3cfa3de9cd8280"
|
||||
},
|
||||
"index.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/cb/cb8df64b81d4e2ad46e13002a5697224c5e21c3c04e925d3302f8c4ceb0a58c3",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/7f/7f9f086ba7fb700d1308e5f9efee068c11034983a04dfc6baed7b8096034ea32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"f155d8de-7fb6-4e12-9291-60b3874a47cc": {
|
||||
"actor": {
|
||||
"kind": "outside",
|
||||
"name": "Outside"
|
||||
},
|
||||
"label": "Changed outside the app",
|
||||
"startedAt": 1792016156924,
|
||||
"endedAt": 1792016156944,
|
||||
"files": {
|
||||
"compositions/captions.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/d9/d9ed837d5d7a9893cf38400b360adc8d5636489ede0eaffb3f4e341faab51c04",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c7/c7a724d68db626674aa77f2e4b9967b63cb606d3dd1156796d0a319e23f1e0e8"
|
||||
},
|
||||
"compositions/frames/01-hook.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/1f/1f8e5283549f95e82cfa7324067517b68a5c852ee0400e39a4b8eeadb8d2afdb",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/f4/f4e6a9d0f2414e3f78e0b7ec499f3adf397e480ad3e3cbfc13110bd9ee6553f3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e": {
|
||||
"actor": {
|
||||
"kind": "outside",
|
||||
"name": "Outside"
|
||||
},
|
||||
"label": "Changed outside the app",
|
||||
"startedAt": 1792016208793,
|
||||
"endedAt": 1792016208815,
|
||||
"files": {
|
||||
"compositions/frames/01-hook.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/f4/f4e6a9d0f2414e3f78e0b7ec499f3adf397e480ad3e3cbfc13110bd9ee6553f3",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/48/48852887a870cc3daa0c98e65504ee207475b7add2b4eecb79fe304b0539441b"
|
||||
},
|
||||
"compositions/frames/02-worry.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/4d/4df905219fb45cf58401646096425b66025ec56d32b12fca340e88e5a07ac264",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/36/36c53f5f5bdcbf9ebb6a7c73dc22017e80dfc64bd98470d78af8ef7846bb1d94"
|
||||
},
|
||||
"compositions/frames/04-limits.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/0b/0b852cb30390684e6002b762165405fabb56cad4955fc8287e84d595bd1891d3",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/fd/fd4173fd65eff6f6ca43680ba4824545ae6bb57eb8710780a035c9dd120ebc37"
|
||||
},
|
||||
"compositions/frames/09-cta.html": {
|
||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/35/3566b01fb9bb64e4d22e86877179c7b7cbee0a98f4bb3c354c97db2b85b13b15",
|
||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/d9/d9e539932cec2431c43f6125a26bb452abc66a2f7c616b1d1e26da5911e80e07"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"latest": {
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"id": "31e798e4-ad68-444f-b4c3-170cece3aeb1"
|
||||
},
|
||||
"revisions": {
|
||||
"ad80a38d-f38a-4cc1-9042-d179e2b0e6f3": [
|
||||
{
|
||||
"id": "bdef997d-ad31-4453-8fb8-c432364aadaf",
|
||||
"scope": "project",
|
||||
"metadata": {
|
||||
"ok": false,
|
||||
"browserSkipped": false,
|
||||
"options": {
|
||||
"samples": 9,
|
||||
"atTransitions": false,
|
||||
"maxIssues": 80,
|
||||
"collapseStatic": true,
|
||||
"tolerance": 2,
|
||||
"timeout": 3000,
|
||||
"contrast": true,
|
||||
"strict": false,
|
||||
"snapshots": false,
|
||||
"browserGpuMode": "auto"
|
||||
}
|
||||
},
|
||||
"createdAt": 1792016113510,
|
||||
"contentHash": "cbaddad6d2047ae01a30682fc6f678b633adb8e61cc6d7b2cd135829f2f6a67d",
|
||||
"revision": "ad80a38d-f38a-4cc1-9042-d179e2b0e6f3",
|
||||
"inputFingerprint": "69a931e0c93bee3efd6531a54ac48f71e88a2fc05106c9891ccb8713064b92ee",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
}
|
||||
],
|
||||
"2748be21-fde2-40ba-a2c0-fddfb7e47431": [
|
||||
{
|
||||
"id": "0e229a06-9c97-4ec5-badd-59e38d09bb91",
|
||||
"scope": "project",
|
||||
"metadata": {
|
||||
"ok": false,
|
||||
"browserSkipped": false,
|
||||
"options": {
|
||||
"samples": 9,
|
||||
"atTransitions": false,
|
||||
"maxIssues": 80,
|
||||
"collapseStatic": true,
|
||||
"tolerance": 2,
|
||||
"timeout": 3000,
|
||||
"contrast": true,
|
||||
"strict": false,
|
||||
"snapshots": false,
|
||||
"browserGpuMode": "auto"
|
||||
}
|
||||
},
|
||||
"createdAt": 1792016142676,
|
||||
"contentHash": "cd9b82c350369ca166d5210b7192e5c1f0394570ec246f30f0a3134ed89d3e9a",
|
||||
"revision": "2748be21-fde2-40ba-a2c0-fddfb7e47431",
|
||||
"inputFingerprint": "86db889a4aed182e0d1dc77ef05552d96c77a15f49c3c15e3fb33051e7f719e6",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
}
|
||||
],
|
||||
"f155d8de-7fb6-4e12-9291-60b3874a47cc": [
|
||||
{
|
||||
"id": "2e9e2e50-ea4c-4ce0-ae6d-feb42f2db77c",
|
||||
"scope": "project",
|
||||
"metadata": {
|
||||
"ok": true,
|
||||
"browserSkipped": false,
|
||||
"options": {
|
||||
"samples": 9,
|
||||
"atTransitions": false,
|
||||
"maxIssues": 80,
|
||||
"collapseStatic": true,
|
||||
"tolerance": 2,
|
||||
"timeout": 3000,
|
||||
"contrast": true,
|
||||
"strict": false,
|
||||
"snapshots": false,
|
||||
"browserGpuMode": "auto"
|
||||
}
|
||||
},
|
||||
"createdAt": 1792016169247,
|
||||
"contentHash": "3c336e043480b32bbb2ecf2a89aa5ad157bf0b6a1243806c47aeb6fba4bb5858",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
}
|
||||
],
|
||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e": [
|
||||
{
|
||||
"id": "31e798e4-ad68-444f-b4c3-170cece3aeb1",
|
||||
"scope": "project",
|
||||
"report": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/checks/31e798e4-ad68-444f-b4c3-170cece3aeb1.json",
|
||||
"metadata": {
|
||||
"ok": true,
|
||||
"browserSkipped": false,
|
||||
"options": {
|
||||
"samples": 9,
|
||||
"atTransitions": false,
|
||||
"maxIssues": 80,
|
||||
"collapseStatic": true,
|
||||
"tolerance": 2,
|
||||
"timeout": 3000,
|
||||
"contrast": true,
|
||||
"strict": false,
|
||||
"snapshots": false,
|
||||
"browserGpuMode": "auto"
|
||||
}
|
||||
},
|
||||
"createdAt": 1792016221309,
|
||||
"contentHash": "e4b290d5513ee41c22f25665bfab73ab61f26f0c137a9fddd861279b15f805d4",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision"
|
||||
}
|
||||
]
|
||||
},
|
||||
"unversioned": [
|
||||
{
|
||||
"id": "b81768cb-66dc-4b7d-8159-85d988ebc321",
|
||||
"scope": "project",
|
||||
"report": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/checks/b81768cb-66dc-4b7d-8159-85d988ebc321.json",
|
||||
"metadata": {
|
||||
"ok": false,
|
||||
"browserSkipped": false,
|
||||
"options": {
|
||||
"samples": 9,
|
||||
"atTransitions": false,
|
||||
"maxIssues": 80,
|
||||
"collapseStatic": true,
|
||||
"tolerance": 2,
|
||||
"timeout": 3000,
|
||||
"contrast": true,
|
||||
"strict": false,
|
||||
"snapshots": false,
|
||||
"browserGpuMode": "auto"
|
||||
}
|
||||
},
|
||||
"createdAt": 1792016095244,
|
||||
"contentHash": "aab572c254c1e2e2a0d41cb3e69f19c969c52ea74d52d8a7b70fa33ca1a3d2e6",
|
||||
"revision": null,
|
||||
"inputFingerprint": "3b3a22e794c378dd99ccd728175edebfae98c1e343c6ca06027a0679d088604a",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "unversioned"
|
||||
}
|
||||
]
|
||||
},
|
||||
"snapshots": {
|
||||
"latest": {
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"id": "a82e082b-e292-496f-a873-daced29d123f-4"
|
||||
},
|
||||
"revisions": {
|
||||
"f155d8de-7fb6-4e12-9291-60b3874a47cc": [
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-0",
|
||||
"time": 1.5,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "b9d7d37046d37a8418b0e35047fa3c528818b9f28f321fd8c163b7d821b7b2f8",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-1",
|
||||
"time": 4.8,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "8bfcfe4356e344034a588fb8eaa48d8c6a17fd1f00921172ac0452a80b1b71e4",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-2",
|
||||
"time": 9,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "d7c0b4142c09d36f84c8a5b82705bd6cf46522dc372d88cef51f43192b37e42d",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-3",
|
||||
"time": 12.3,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "90ca4efc8f9645e29abd5e9c1c6900404f7a53ac80294f7c6069b847fa1bccdd",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-4",
|
||||
"time": 16,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "bc7a6a0347049f51793185e2867b34f9200ff93d40a06f0bffa6965696338d8c",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-5",
|
||||
"time": 20.2,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "c86c03f4c948e4b51a67e7012bf1eb5898e381c173080a13261b212b5c8ce25a",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-6",
|
||||
"time": 24,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "e2b216d57db98ddf973fd0134008417ff00f646888ace8fc5d3e7ce2ddef9bd0",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-7",
|
||||
"time": 27.3,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "48b1ae5f592678b077b5b5ea2c166090dfc9e84c6baaabb2b1bba5f41642a735",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-8",
|
||||
"time": 31,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "6b3edca25c8d8c0e7be74e31690404694ecb679d0d4348a05823c96bf891e492",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-9",
|
||||
"time": 33,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182293,
|
||||
"contentHash": "228695183d4db7398381a0d6d7f502ad5755dfe8601c9c68d7c73b45b44a210c",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-10",
|
||||
"time": 36,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "d8a1ec155678c44ce8e68cde3085859a1ff031b78febf7ea5b6f4d5e45c4d0d6",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-11",
|
||||
"time": 39.5,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "d814f09e5d907213260a03a8b6b0129ed31848abbd29de93417b6deaa5ff8871",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-12",
|
||||
"time": 42,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "acb9108624fc79f1dcc4ecb6ddf74ee6f2c46be2c53604d8a8243b886779f9e7",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-13",
|
||||
"time": 45.3,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "73edeff4a0be0e45c4d82b20caafd7eb154d496c5252af5c351d9349871a2dcd",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-14",
|
||||
"time": 47.5,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "5f425b67e37447223fb533746a74d4994827e00945958068d3c6b6213f7a4938",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-15",
|
||||
"time": 49,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "73b125a618712fb0d3e5daecffb66f3704e0b160f5844e4759971551a98c1c76",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-16",
|
||||
"time": 51,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "83be74004821d6907afaaa93dd798169144c976855840ebaa8082069d1d7ce34",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-17",
|
||||
"time": 54.2,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "a4293fac697e7669476d2d8668abf92a9c4aec2ed6f11672ad53f15074b43cf6",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-18",
|
||||
"time": 53.091,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016182294,
|
||||
"contentHash": "882bf50bb17bf4324156c5719de60e94835d1af72402f7af984a845c7684fd3a",
|
||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision",
|
||||
"status": "cleaned",
|
||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
||||
}
|
||||
],
|
||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e": [
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "a82e082b-e292-496f-a873-daced29d123f-0",
|
||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-00-at-1.5s.png",
|
||||
"time": 1.5,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016226995,
|
||||
"contentHash": "79be7032c63e7a191b47eaceb152a02bf809936a7e76071b68c388a49038f28a",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "a82e082b-e292-496f-a873-daced29d123f-1",
|
||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-01-at-9s.png",
|
||||
"time": 9,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016226996,
|
||||
"contentHash": "1a9105cec34e2127ce91c60aab69a567895822f45267710285d03b2b99a1bf84",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "a82e082b-e292-496f-a873-daced29d123f-2",
|
||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-02-at-26.5s.png",
|
||||
"time": 26.5,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016226996,
|
||||
"contentHash": "9132f797582916f355f507d87da8843d0fdd23f1b9c5d7f6473eb859b3fd3392",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "a82e082b-e292-496f-a873-daced29d123f-3",
|
||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-03-at-53s.png",
|
||||
"time": 53,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016226996,
|
||||
"contentHash": "c8714c376f4e7f375d0c2c11766a2717127224c0927372e4ff67ee41c4d52a72",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision"
|
||||
},
|
||||
{
|
||||
"scope": "full",
|
||||
"id": "a82e082b-e292-496f-a873-daced29d123f-4",
|
||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-04-at-53.091s.png",
|
||||
"time": 53.091,
|
||||
"metadata": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"zoomScale": 1,
|
||||
"browserGpuMode": "auto",
|
||||
"warnings": [],
|
||||
"partial": false
|
||||
},
|
||||
"createdAt": 1792016226996,
|
||||
"contentHash": "5605b5c311601f318545227a90787eb6f2950613dea019cf8e0daf81353d7946",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
||||
"inputScope": "local-project-files",
|
||||
"attribution": "recorded-revision"
|
||||
}
|
||||
]
|
||||
},
|
||||
"unversioned": []
|
||||
},
|
||||
"renders": {
|
||||
"latest": {
|
||||
"id": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3",
|
||||
"completedAt": 1792016445275,
|
||||
"output": "/home/user/cycle-atlas/videos/lookatu-promo/renders/video.mp4",
|
||||
"status": "completed",
|
||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e"
|
||||
},
|
||||
"retainedRevisions": [
|
||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e"
|
||||
]
|
||||
},
|
||||
"pendingArtifactCleanup": []
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# .media · 9 assets
|
||||
|
||||
id type dur dims path description
|
||||
voice_010 voice 3.8s — assets/voice/01.wav Where are they right now? And what are they really doing on that phone?
|
||||
voice_011 voice 5.6s — assets/voice/02.wav Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.
|
||||
voice_012 voice 6.1s — assets/voice/03.wav There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.
|
||||
voice_013 voice 5.1s — assets/voice/04.wav Set screen time limits that actually stick. When the time is up, the phone knows.
|
||||
voice_014 voice 4.2s — assets/voice/05.wav Choose which apps are okay. Allow the good ones. Block the rest.
|
||||
voice_015 voice 5s — assets/voice/06.wav And when it really matters, see where they are. Location, only when you need it.
|
||||
voice_016 voice 4s — assets/voice/07.wav Because kids deserve some freedom, and parents deserve some calm.
|
||||
voice_017 voice 2.3s — assets/voice/08.wav Works on Android and on iPhone.
|
||||
voice_018 voice 3.1s — assets/voice/09.wav lookatu. Look out for them. Download it today.
|
||||
@@ -0,0 +1,18 @@
|
||||
{"id":"voice_001","type":"voice","path":"assets/voice/01.wav","source":"generated","description":"Where are they right now? And what are they really doing on that phone?","duration":3.3,"provenance":{"provider":"kokoro","prompt":"Where are they right now? And what are they really doing on that phone?"}}
|
||||
{"id":"voice_002","type":"voice","path":"assets/voice/02.wav","source":"generated","description":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.","duration":4.9,"provenance":{"provider":"kokoro","prompt":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."}}
|
||||
{"id":"voice_003","type":"voice","path":"assets/voice/03.wav","source":"generated","description":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.","duration":5.4,"provenance":{"provider":"kokoro","prompt":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."}}
|
||||
{"id":"voice_004","type":"voice","path":"assets/voice/04.wav","source":"generated","description":"Set screen time limits that actually stick. When the time is up, the phone knows.","duration":4.5,"provenance":{"provider":"kokoro","prompt":"Set screen time limits that actually stick. When the time is up, the phone knows."}}
|
||||
{"id":"voice_005","type":"voice","path":"assets/voice/05.wav","source":"generated","description":"Choose which apps are okay. Allow the good ones. Block the rest.","duration":3.7,"provenance":{"provider":"kokoro","prompt":"Choose which apps are okay. Allow the good ones. Block the rest."}}
|
||||
{"id":"voice_006","type":"voice","path":"assets/voice/06.wav","source":"generated","description":"And when it really matters, see where they are. Location, only when you need it.","duration":4.4,"provenance":{"provider":"kokoro","prompt":"And when it really matters, see where they are. Location, only when you need it."}}
|
||||
{"id":"voice_007","type":"voice","path":"assets/voice/07.wav","source":"generated","description":"Because kids deserve some freedom, and parents deserve some calm.","duration":3.6,"provenance":{"provider":"kokoro","prompt":"Because kids deserve some freedom, and parents deserve some calm."}}
|
||||
{"id":"voice_008","type":"voice","path":"assets/voice/08.wav","source":"generated","description":"Works on Android and on iPhone.","duration":2,"provenance":{"provider":"kokoro","prompt":"Works on Android and on iPhone."}}
|
||||
{"id":"voice_009","type":"voice","path":"assets/voice/09.wav","source":"generated","description":"lookatu. Look out for them. Download it today.","duration":2.7,"provenance":{"provider":"kokoro","prompt":"lookatu. Look out for them. Download it today."}}
|
||||
{"id":"voice_010","type":"voice","path":"assets/voice/01.wav","source":"generated","description":"Where are they right now? And what are they really doing on that phone?","duration":3.8,"provenance":{"provider":"kokoro","prompt":"Where are they right now? And what are they really doing on that phone?"}}
|
||||
{"id":"voice_011","type":"voice","path":"assets/voice/02.wav","source":"generated","description":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.","duration":5.6,"provenance":{"provider":"kokoro","prompt":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."}}
|
||||
{"id":"voice_012","type":"voice","path":"assets/voice/03.wav","source":"generated","description":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.","duration":6.1,"provenance":{"provider":"kokoro","prompt":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."}}
|
||||
{"id":"voice_013","type":"voice","path":"assets/voice/04.wav","source":"generated","description":"Set screen time limits that actually stick. When the time is up, the phone knows.","duration":5.1,"provenance":{"provider":"kokoro","prompt":"Set screen time limits that actually stick. When the time is up, the phone knows."}}
|
||||
{"id":"voice_014","type":"voice","path":"assets/voice/05.wav","source":"generated","description":"Choose which apps are okay. Allow the good ones. Block the rest.","duration":4.2,"provenance":{"provider":"kokoro","prompt":"Choose which apps are okay. Allow the good ones. Block the rest."}}
|
||||
{"id":"voice_015","type":"voice","path":"assets/voice/06.wav","source":"generated","description":"And when it really matters, see where they are. Location, only when you need it.","duration":5,"provenance":{"provider":"kokoro","prompt":"And when it really matters, see where they are. Location, only when you need it."}}
|
||||
{"id":"voice_016","type":"voice","path":"assets/voice/07.wav","source":"generated","description":"Because kids deserve some freedom, and parents deserve some calm.","duration":4,"provenance":{"provider":"kokoro","prompt":"Because kids deserve some freedom, and parents deserve some calm."}}
|
||||
{"id":"voice_017","type":"voice","path":"assets/voice/08.wav","source":"generated","description":"Works on Android and on iPhone.","duration":2.3,"provenance":{"provider":"kokoro","prompt":"Works on Android and on iPhone."}}
|
||||
{"id":"voice_018","type":"voice","path":"assets/voice/09.wav","source":"generated","description":"lookatu. Look out for them. Download it today.","duration":3.1,"provenance":{"provider":"kokoro","prompt":"lookatu. Look out for them. Download it today."}}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"style_preset.product-launch-video": {
|
||||
"value": "daisy-days",
|
||||
"confirmed_in": [
|
||||
"lookatu-promo"
|
||||
],
|
||||
"updated_at": "2026-10-14T22:05:31.928Z"
|
||||
}
|
||||
},
|
||||
"sightings": {}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
# HyperFrames Composition Project
|
||||
|
||||
## Skills — USE THESE FIRST
|
||||
|
||||
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
|
||||
|
||||
**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
|
||||
|
||||
- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
|
||||
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
|
||||
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
|
||||
- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
|
||||
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
|
||||
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
|
||||
- `/music-to-video` — a **music track** (audio file, video to pull audio from, or one generated from a mood brief) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
|
||||
- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
|
||||
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
|
||||
|
||||
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
|
||||
|
||||
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-audio`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
|
||||
|
||||
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
|
||||
|
||||
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
||||
|
||||
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
|
||||
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
|
||||
> the plugin manager, not the standalone commands below.
|
||||
>
|
||||
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
||||
> the specific skill you need (the `/hyperframes` router does this automatically before
|
||||
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
|
||||
> everything already installed — neither pulls the full set. Restart the agent session so
|
||||
> newly installed skills load.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # human-operated foreground preview (blocks until stopped)
|
||||
npx hyperframes preview --background # agent-safe persistent Studio preview
|
||||
npx hyperframes preview --status # verify the persistent preview is listening
|
||||
npx hyperframes preview --stop # stop it when review is finished
|
||||
npm run check # lint + runtime + layout + motion + contrast (one command)
|
||||
npm run render # render to MP4
|
||||
npm run publish # publish and get a shareable link
|
||||
npx hyperframes lint --verbose # include info-level findings
|
||||
npx hyperframes lint --json # machine-readable output for CI
|
||||
npx hyperframes docs <topic> # reference docs in terminal
|
||||
```
|
||||
|
||||
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
|
||||
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
|
||||
> remains owned by the invoking session and can disappear while the browser stays open,
|
||||
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
|
||||
> alive through review, and stop it explicitly with `preview --stop` afterward.
|
||||
|
||||
> **Back from the desktop app.** Once this project was opened in the HyperFrames desktop app, run `npx hyperframes catch-up` before your next change here: it lists what the person asked Framey in the app and which files changed since.
|
||||
|
||||
> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
|
||||
|
||||
## Documentation
|
||||
|
||||
**For quick reference**, use the local CLI docs command (no network required):
|
||||
|
||||
```bash
|
||||
npx hyperframes docs <topic>
|
||||
```
|
||||
|
||||
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
|
||||
|
||||
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
|
||||
|
||||
```
|
||||
https://hyperframes.heygen.com/llms.txt
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `index.html` — main composition (root timeline)
|
||||
- `compositions/` — sub-compositions referenced via `data-composition-src`
|
||||
- `meta.json` — project metadata (id, name)
|
||||
- `transcript.json` — whisper word-level transcript (if generated)
|
||||
|
||||
## Linting — ALWAYS RUN AFTER CHANGES
|
||||
|
||||
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
|
||||
Fix all errors before presenting the result. Warnings should be reviewed before rendering.
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
|
||||
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
|
||||
3. Register one paused root timeline per composition on `window.__timelines`:
|
||||
```js
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
|
||||
```
|
||||
Scene timelines manually added to this root must not be paused. A paused
|
||||
child does not advance when the root is seeked. The runtime activates
|
||||
registered composition siblings, not arbitrary nested scene timelines.
|
||||
4. A video with sound keeps it on the `<video>` (`data-has-audio="true"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.
|
||||
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
|
||||
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
workflow: product-launch-video
|
||||
flow: automation
|
||||
storyboard: no
|
||||
message: "Keep your kids safe on their phones, with peace of mind for parents"
|
||||
destination: tiktok-reels-shorts
|
||||
aspect: 1080x1920
|
||||
language: en
|
||||
audience: parents of children with smartphones
|
||||
length: 60s
|
||||
angle: parent peace of mind
|
||||
---
|
||||
|
||||
## Intent
|
||||
|
||||
Promo for "lookatu", an Android and iOS child-safety app. Parents control how
|
||||
much their kids use the phone (time limits, app control) and can track location
|
||||
only when needed. Story shape: start with a worried parent, then lookatu brings
|
||||
calm and safety. Warm, reassuring, trustworthy. Not scary, not spying.
|
||||
|
||||
## Assets
|
||||
|
||||
- None supplied. No website, logo, or screenshots yet. Use invented phone UI
|
||||
mock-ups and a typographic "lookatu" wordmark. Real screens come in Phase 2.
|
||||
|
||||
## Customizations
|
||||
|
||||
- No-capture mode (nothing to scrape). A shipped preset supplies the design.
|
||||
|
||||
## Notes
|
||||
|
||||
- User is not a coder: explain steps in plain language.
|
||||
- Only claim features the user described: usage control, location tracking
|
||||
when needed, Android and iOS. No invented stats, awards, or user counts.
|
||||
- Frame tracking as parent-led and transparent (no secret spying language).
|
||||
- Phase 2: swap in real logo and screenshots, add real voice. Phase 3: other sizes.
|
||||
@@ -0,0 +1,109 @@
|
||||
# HyperFrames Composition Project
|
||||
|
||||
## Skills — USE THESE FIRST
|
||||
|
||||
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
|
||||
|
||||
**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
|
||||
|
||||
- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
|
||||
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
|
||||
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
|
||||
- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
|
||||
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
|
||||
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
|
||||
- `/music-to-video` — a **music track** (audio file, video to pull audio from, or one generated from a mood brief) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
|
||||
- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
|
||||
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
|
||||
|
||||
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
|
||||
|
||||
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-audio`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
|
||||
|
||||
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
|
||||
|
||||
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
||||
|
||||
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
|
||||
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
|
||||
> the plugin manager, not the standalone commands below.
|
||||
>
|
||||
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
||||
> the specific skill you need (the `/hyperframes` router does this automatically before
|
||||
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
|
||||
> everything already installed — neither pulls the full set. Restart the agent session so
|
||||
> newly installed skills load.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # human-operated foreground preview (blocks until stopped)
|
||||
npx hyperframes preview --background # agent-safe persistent Studio preview
|
||||
npx hyperframes preview --status # verify the persistent preview is listening
|
||||
npx hyperframes preview --stop # stop it when review is finished
|
||||
npm run check # lint + runtime + layout + motion + contrast (one command)
|
||||
npm run render # render to MP4
|
||||
npm run publish # publish and get a shareable link
|
||||
npx hyperframes lint --verbose # include info-level findings
|
||||
npx hyperframes lint --json # machine-readable output for CI
|
||||
npx hyperframes docs <topic> # reference docs in terminal
|
||||
```
|
||||
|
||||
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
|
||||
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
|
||||
> remains owned by the invoking session and can disappear while the browser stays open,
|
||||
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
|
||||
> alive through review, and stop it explicitly with `preview --stop` afterward.
|
||||
|
||||
> **Back from the desktop app.** Once this project was opened in the HyperFrames desktop app, run `npx hyperframes catch-up` before your next change here: it lists what the person asked Framey in the app and which files changed since.
|
||||
|
||||
> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
|
||||
|
||||
## Documentation
|
||||
|
||||
**For quick reference**, use the local CLI docs command (no network required):
|
||||
|
||||
```bash
|
||||
npx hyperframes docs <topic>
|
||||
```
|
||||
|
||||
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
|
||||
|
||||
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
|
||||
|
||||
```
|
||||
https://hyperframes.heygen.com/llms.txt
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `index.html` — main composition (root timeline)
|
||||
- `compositions/` — sub-compositions referenced via `data-composition-src`
|
||||
- `meta.json` — project metadata (id, name)
|
||||
- `transcript.json` — whisper word-level transcript (if generated)
|
||||
|
||||
## Linting — ALWAYS RUN AFTER CHANGES
|
||||
|
||||
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
|
||||
Fix all errors before presenting the result. Warnings should be reviewed before rendering.
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
|
||||
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
|
||||
3. Register one paused root timeline per composition on `window.__timelines`:
|
||||
```js
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
|
||||
```
|
||||
Scene timelines manually added to this root must not be paused. A paused
|
||||
child does not advance when the root is seeked. The runtime activates
|
||||
registered composition siblings, not arbitrary nested scene timelines.
|
||||
4. A video with sound keeps it on the `<video>` (`data-has-audio="true"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.
|
||||
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
|
||||
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
|
||||
@@ -0,0 +1,70 @@
|
||||
# SCRIPT — lookatu-promo
|
||||
|
||||
**Voice:** af_heart (Kokoro, offline)
|
||||
**Voice settings:** default
|
||||
**Voice direction:** Warm, calm, reassuring. Like a friend who is also a parent.
|
||||
|
||||
---
|
||||
|
||||
## Line 1 — Where are they (Frame 1)
|
||||
|
||||
**Time:** 0.0 – 6.0s
|
||||
**Delivery:** Gentle worry, then curious.
|
||||
|
||||
Where are they right now? And what are they really doing on that phone?
|
||||
|
||||
## Line 2 — The quiet worry (Frame 2)
|
||||
|
||||
**Time:** 6.0 – 13.0s
|
||||
**Delivery:** Slow, a little tired, honest.
|
||||
|
||||
Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.
|
||||
|
||||
## Line 3 — Meet lookatu (Frame 3)
|
||||
|
||||
**Time:** 13.0 – 22.0s
|
||||
**Delivery:** A breath, then relief and warmth on the name.
|
||||
|
||||
There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.
|
||||
|
||||
## Line 4 — Screen time limits (Frame 4)
|
||||
|
||||
**Time:** 22.0 – 30.0s
|
||||
**Delivery:** Confident and clear.
|
||||
|
||||
Set screen time limits that actually stick. When the time is up, the phone knows.
|
||||
|
||||
## Line 5 — App control (Frame 5)
|
||||
|
||||
**Time:** 30.0 – 37.0s
|
||||
**Delivery:** Crisp, three short beats.
|
||||
|
||||
Choose which apps are okay. Allow the good ones. Block the rest.
|
||||
|
||||
## Line 6 — Location when needed (Frame 6)
|
||||
|
||||
**Time:** 37.0 – 45.0s
|
||||
**Delivery:** Soft and reassuring.
|
||||
|
||||
And when it really matters, see where they are. Location, only when you need it.
|
||||
|
||||
## Line 7 — Freedom and calm (Frame 7)
|
||||
|
||||
**Time:** 45.0 – 51.0s
|
||||
**Delivery:** Warm, almost a smile.
|
||||
|
||||
Because kids deserve some freedom, and parents deserve some calm.
|
||||
|
||||
## Line 8 — Android and iPhone (Frame 8)
|
||||
|
||||
**Time:** 51.0 – 55.0s
|
||||
**Delivery:** Simple and friendly.
|
||||
|
||||
Works on Android and on iPhone.
|
||||
|
||||
## Line 9 — Look out for them (Frame 9)
|
||||
|
||||
**Time:** 55.0 – 61.0s
|
||||
**Delivery:** Warm sign-off, land on the name.
|
||||
|
||||
lookatu. Look out for them. Download it today.
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
format: 1080x1920
|
||||
duration: 60s
|
||||
message: "Keep your kids safe on their phones, with peace of mind for parents"
|
||||
arc: Hook → Pain → Product → Time limits → App control → Location → Balance → Platforms → CTA
|
||||
audience: parents of children with smartphones
|
||||
mode: autonomous
|
||||
music: none
|
||||
---
|
||||
|
||||
## Video direction
|
||||
|
||||
Daisy Days picture-book look: cream and pastel grounds, 3px charcoal outlines, hard offset shadows,
|
||||
hand-drawn ornaments (daisy, star, sun, cloud) filling the corners. Warm, reassuring, never scary.
|
||||
Motion is soft and bouncy (back.out, power3.out), each piece lands when the voice names it. Phone
|
||||
mock-ups are invented generic screens with no real brand logos. Captions sit in the bottom band.
|
||||
|
||||
## Frame 1 — Where are they
|
||||
|
||||
- scene: Two big question bubbles pop in over a sky-blue ground, a small phone silhouette wobbles
|
||||
- voiceover: "Where are they right now? And what are they really doing on that phone?"
|
||||
- duration: 5.64s
|
||||
- transition_in: crossfade
|
||||
- status: animated
|
||||
- src: compositions/frames/01-hook.html
|
||||
- type: hook
|
||||
- persuasion: Pain validation
|
||||
- beat: worry
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Opens on the parent's silent question in outcome language.
|
||||
keyMessage: Parents worry about where their kids are and what the phone is doing.
|
||||
|
||||
## Frame 2 — The quiet worry
|
||||
|
||||
- scene: Three sticker words stack in one by one: SCROLLING, ARGUING, WORRYING
|
||||
- voiceover: "Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."
|
||||
- duration: 7.111s
|
||||
- transition_in: crossfade
|
||||
- status: animated
|
||||
- src: compositions/frames/02-worry.html
|
||||
- type: pain_point
|
||||
- persuasion: Pain agitation
|
||||
- beat: tension
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Names the everyday pain so the product feels like relief.
|
||||
keyMessage: The phone brings daily friction and worry.
|
||||
|
||||
## Frame 3 — Meet lookatu
|
||||
|
||||
- scene: The lookatu wordmark builds on a turquoise ground with a shield badge and a calm sun
|
||||
- voiceover: "There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."
|
||||
- duration: 8.144s
|
||||
- transition_in: zoom-through
|
||||
- status: animated
|
||||
- src: compositions/frames/03-meet.html
|
||||
- type: product_intro
|
||||
- persuasion: Future pacing
|
||||
- beat: relief
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Lands the promise (the message) by beat 3 and names the product.
|
||||
keyMessage: lookatu gives parents peace of mind.
|
||||
|
||||
## Frame 4 — Screen time limits
|
||||
|
||||
- scene: A phone mock-up with a timer ring that counts down and locks with a gentle lock icon
|
||||
- voiceover: "Set screen time limits that actually stick. When the time is up, the phone knows."
|
||||
- duration: 6.599s
|
||||
- transition_in: push-slide LEFT
|
||||
- status: animated
|
||||
- src: compositions/frames/04-limits.html
|
||||
- type: feature_showcase
|
||||
- persuasion: Feature-to-benefit translation
|
||||
- beat: control
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Feature 1 shown working, with the benefit of limits that hold.
|
||||
keyMessage: Parents set the phone usage limits.
|
||||
|
||||
## Frame 5 — App control
|
||||
|
||||
- scene: A phone mock-up with a list of generic app tiles and toggles; good apps switch on, others switch off
|
||||
- voiceover: "Choose which apps are okay. Allow the good ones. Block the rest."
|
||||
- duration: 5.703s
|
||||
- transition_in: push-slide LEFT
|
||||
- status: animated
|
||||
- src: compositions/frames/05-apps.html
|
||||
- type: feature_showcase
|
||||
- persuasion: Show-don't-tell proof
|
||||
- beat: control
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Feature 2, parents decide which apps are allowed.
|
||||
keyMessage: Parents allow or block apps.
|
||||
|
||||
## Frame 6 — Location when needed
|
||||
|
||||
- scene: A soft map card with a pin that drops and pulses once, sitting inside a phone mock-up
|
||||
- voiceover: "And when it really matters, see where they are. Location, only when you need it."
|
||||
- duration: 6.513s
|
||||
- transition_in: push-slide LEFT
|
||||
- status: animated
|
||||
- src: compositions/frames/06-location.html
|
||||
- type: feature_showcase
|
||||
- persuasion: Risk reversal
|
||||
- beat: reassurance
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Feature 3, location tracking available when needed.
|
||||
keyMessage: Parents can check location when needed.
|
||||
|
||||
## Frame 7 — Freedom and calm
|
||||
|
||||
- scene: Two big words on a pink ground, FREEDOM and CALM, each in a sticker card joined by a heart
|
||||
- voiceover: "Because kids deserve some freedom, and parents deserve some calm."
|
||||
- duration: 5.468s
|
||||
- transition_in: crossfade
|
||||
- status: animated
|
||||
- src: compositions/frames/07-balance.html
|
||||
- type: benefit_highlight
|
||||
- persuasion: Rule of two
|
||||
- beat: peace of mind
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: The emotional payoff that ties the features together.
|
||||
keyMessage: Safety without a fight.
|
||||
|
||||
## Frame 8 — Android and iPhone
|
||||
|
||||
- scene: Two pill badges, ANDROID and iOS, pop in side by side on a butter ground
|
||||
- voiceover: "Works on Android and on iPhone."
|
||||
- duration: 3.783s
|
||||
- transition_in: crossfade
|
||||
- status: animated
|
||||
- src: compositions/frames/08-platforms.html
|
||||
- type: benefit_highlight
|
||||
- persuasion: Friction reduction
|
||||
- beat: ease
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Removes the "will it work on my phone" question.
|
||||
keyMessage: Available for both Android and iOS.
|
||||
|
||||
## Frame 9 — Look out for them
|
||||
|
||||
- scene: Lavender ground, lookatu wordmark with the tagline, ornament wreath, gentle settle
|
||||
- voiceover: "lookatu. Look out for them. Download it today."
|
||||
- duration: 5.772s
|
||||
- transition_in: zoom-through
|
||||
- status: animated
|
||||
- src: compositions/frames/09-cta.html
|
||||
- type: cta
|
||||
- persuasion: Clear call to action
|
||||
- beat: confidence
|
||||
- blueprint: compose
|
||||
- asset_candidates:
|
||||
|
||||
narrativeRole: Closing lockup and call to action.
|
||||
keyMessage: Download lookatu.
|
||||
Binary file not shown.
Binary file not shown.
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* GSAP 3.14.2
|
||||
* https://gsap.com
|
||||
*
|
||||
* @license Copyright 2025, GreenSock. All rights reserved.
|
||||
* Subject to the terms at https://gsap.com/standard-license.
|
||||
* @author: Jack Doyle, jack@greensock.com
|
||||
*/
|
||||
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function R(t){return(i=bt(t,ht))&&Fe}function S(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function T(t,e){return!e&&console.warn(t)}function U(t,e){return t&&(ht[t]=e)&&i&&(i[t]=e)||ht}function V(){return 0}function ga(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=yt.length;r--&&!yt[r].targetTest(i););e=yt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Xt(t[r],e)))||t.splice(r,1);return t}function ha(t){return t._gsap||ga(Pt(t))[0]._gsap}function ia(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ja(t,e){return(t=t.split(",")).forEach(e)||t}function ka(t){return Math.round(1e5*t)/1e5||0}function la(t){return Math.round(1e7*t)/1e7||0}function ma(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function na(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r}function oa(){var t,e,r=pt.length,i=pt.slice(0);for(_t={},t=pt.length=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)}function pa(t){return!!(t._initted||t._startAt||t.add)}function qa(t,e,r,i){pt.length&&!I&&oa(),t.render(e,r,i||!!(I&&e<0&&pa(t))),pt.length&&!I&&oa()}function ra(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(ot).length<2?e:r(t)?t.trim():t}function sa(t){return t}function ta(t,e){for(var r in e)r in t||(t[r]=e[r]);return t}function wa(t,e){for(var r in e)"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(t[r]=v(e[r])?wa(t[r]||(t[r]={}),e[r]):e[r]);return t}function xa(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i}function ya(t){var e=t.parent||L,r=t.keyframes?function _setKeyframeDefaults(i){return function(t,e){for(var r in e)r in t||"duration"===r&&i||"ease"===r||(t[r]=e[r])}}($(t.keyframes)):ta;if(w(t.inherit))for(;e;)r(t,e.vars.defaults),e=e.parent||e._dp;return t}function Aa(t,e,r,i,n){void 0===r&&(r="_first"),void 0===i&&(i="_last");var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function Ba(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Ca(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Da(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Fa(t,e,r,i){return t._startAt&&(I?t._startAt.revert(ft):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ha(t){return t._repeat?wt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ja(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ka(t){return t._end=la(t._start+(t._tDur/Math.abs(t._ts||t._rts||q)||0))}function La(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=la(r._time-(0<t._ts?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ka(t),r._dirty||Da(r,t)),t}function Ma(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ja(t.rawTime(),e),(!e._dur||Mt(0,e.totalDuration(),r)-e._tTime>q)&&e.render(r,!0)),Da(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)0<=r.rawTime()&&r.totalTime(r._tTime),r=r._dp;t._zTime=-q}}function Na(e,r,i,n){return r.parent&&Ca(r),r._start=la((t(i)?i:i||e!==L?Ot(e,i,r):e._time)+r._delay),r._end=la(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),Aa(e,r,"_first","_last",e._sort?"_start":0),xt(r)||(e._recent=r),n||Ma(e,r),e._ts<0&&La(e,e._tTime),e}function Oa(t,e){return(ht.ScrollTrigger||S("scrollTrigger",e))&&ht.ScrollTrigger.create(e,t)}function Pa(t,e,r,i,n){return Qt(t,e,n),t._initted?!r&&t._pt&&!I&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==It.frame?(pt.push(t),t._lazy=[n,i],1):void 0:1}function Ua(t,e,r,i){var n=t._repeat,a=la(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:la(a*(n+1)+t._rDelay*n):a,0<s&&!i&&La(t,t._tTime=t._tDur*s),t.parent&&Ka(t),r||Da(t.parent,t),t}function Va(t){return t instanceof Zt?Da(t)Line truncated
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"tts_provider": "kokoro",
|
||||
"voice_id": "af_heart",
|
||||
"bgm": null,
|
||||
"bgm_pending": false,
|
||||
"bgm_provider": null,
|
||||
"bgm_pid": null,
|
||||
"bgm_log": null,
|
||||
"bgm_mode": null,
|
||||
"bgm_target_duration_s": null,
|
||||
"bgm_seed_duration_s": null,
|
||||
"bgm_loop_count": null,
|
||||
"voices": [
|
||||
{
|
||||
"id": "01",
|
||||
"path": "assets/voice/01.wav",
|
||||
"duration_s": 3.84,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "02",
|
||||
"path": "assets/voice/02.wav",
|
||||
"duration_s": 5.611,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "03",
|
||||
"path": "assets/voice/03.wav",
|
||||
"duration_s": 6.144,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "04",
|
||||
"path": "assets/voice/04.wav",
|
||||
"duration_s": 5.099,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "05",
|
||||
"path": "assets/voice/05.wav",
|
||||
"duration_s": 4.203,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "06",
|
||||
"path": "assets/voice/06.wav",
|
||||
"duration_s": 5.013,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "07",
|
||||
"path": "assets/voice/07.wav",
|
||||
"duration_s": 3.968,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "08",
|
||||
"path": "assets/voice/08.wav",
|
||||
"duration_s": 2.283,
|
||||
"words": []
|
||||
},
|
||||
{
|
||||
"id": "09",
|
||||
"path": "assets/voice/09.wav",
|
||||
"duration_s": 3.072,
|
||||
"words": []
|
||||
}
|
||||
],
|
||||
"sfx": [],
|
||||
"total_duration_s": 39.233
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
{
|
||||
"bgm": null,
|
||||
"bgm_pending": false,
|
||||
"voices": [
|
||||
{
|
||||
"frame": 1,
|
||||
"path": "assets/voice/01.wav",
|
||||
"duration_s": 5.64,
|
||||
"words": [
|
||||
{
|
||||
"text": "Where",
|
||||
"start": 0.8,
|
||||
"end": 1.058
|
||||
},
|
||||
{
|
||||
"text": "are",
|
||||
"start": 1.08,
|
||||
"end": 1.264
|
||||
},
|
||||
{
|
||||
"text": "they",
|
||||
"start": 1.28,
|
||||
"end": 1.501
|
||||
},
|
||||
{
|
||||
"text": "right",
|
||||
"start": 1.52,
|
||||
"end": 1.778
|
||||
},
|
||||
{
|
||||
"text": "now",
|
||||
"start": 1.8,
|
||||
"end": 2.205
|
||||
},
|
||||
{
|
||||
"text": "And",
|
||||
"start": 2.24,
|
||||
"end": 2.424
|
||||
},
|
||||
{
|
||||
"text": "what",
|
||||
"start": 2.44,
|
||||
"end": 2.661
|
||||
},
|
||||
{
|
||||
"text": "are",
|
||||
"start": 2.68,
|
||||
"end": 2.864
|
||||
},
|
||||
{
|
||||
"text": "they",
|
||||
"start": 2.88,
|
||||
"end": 3.101
|
||||
},
|
||||
{
|
||||
"text": "really",
|
||||
"start": 3.12,
|
||||
"end": 3.414
|
||||
},
|
||||
{
|
||||
"text": "doing",
|
||||
"start": 3.44,
|
||||
"end": 3.698
|
||||
},
|
||||
{
|
||||
"text": "on",
|
||||
"start": 3.72,
|
||||
"end": 3.867
|
||||
},
|
||||
{
|
||||
"text": "that",
|
||||
"start": 3.88,
|
||||
"end": 4.101
|
||||
},
|
||||
{
|
||||
"text": "phone",
|
||||
"start": 4.12,
|
||||
"end": 4.598
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2,
|
||||
"path": "assets/voice/02.wav",
|
||||
"duration_s": 7.111,
|
||||
"words": [
|
||||
{
|
||||
"text": "Endless",
|
||||
"start": 0.5,
|
||||
"end": 0.908
|
||||
},
|
||||
{
|
||||
"text": "scrolling",
|
||||
"start": 0.943,
|
||||
"end": 1.713
|
||||
},
|
||||
{
|
||||
"text": "Arguments",
|
||||
"start": 1.78,
|
||||
"end": 2.278
|
||||
},
|
||||
{
|
||||
"text": "at",
|
||||
"start": 2.321,
|
||||
"end": 2.502
|
||||
},
|
||||
{
|
||||
"text": "dinner",
|
||||
"start": 2.518,
|
||||
"end": 3.152
|
||||
},
|
||||
{
|
||||
"text": "And",
|
||||
"start": 3.207,
|
||||
"end": 3.433
|
||||
},
|
||||
{
|
||||
"text": "that",
|
||||
"start": 3.453,
|
||||
"end": 3.725
|
||||
},
|
||||
{
|
||||
"text": "quiet",
|
||||
"start": 3.748,
|
||||
"end": 4.065
|
||||
},
|
||||
{
|
||||
"text": "worry",
|
||||
"start": 4.093,
|
||||
"end": 4.41
|
||||
},
|
||||
{
|
||||
"text": "you",
|
||||
"start": 4.438,
|
||||
"end": 4.664
|
||||
},
|
||||
{
|
||||
"text": "never",
|
||||
"start": 4.684,
|
||||
"end": 5.001
|
||||
},
|
||||
{
|
||||
"text": "say",
|
||||
"start": 5.028,
|
||||
"end": 5.255
|
||||
},
|
||||
{
|
||||
"text": "out",
|
||||
"start": 5.274,
|
||||
"end": 5.501
|
||||
},
|
||||
{
|
||||
"text": "loud",
|
||||
"start": 5.52,
|
||||
"end": 6.064
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 3,
|
||||
"path": "assets/voice/03.wav",
|
||||
"duration_s": 8.144,
|
||||
"words": [
|
||||
{
|
||||
"text": "There",
|
||||
"start": 0.5,
|
||||
"end": 0.807
|
||||
},
|
||||
{
|
||||
"text": "has",
|
||||
"start": 0.833,
|
||||
"end": 1.052
|
||||
},
|
||||
{
|
||||
"text": "to",
|
||||
"start": 1.072,
|
||||
"end": 1.247
|
||||
},
|
||||
{
|
||||
"text": "be",
|
||||
"start": 1.262,
|
||||
"end": 1.437
|
||||
},
|
||||
{
|
||||
"text": "a",
|
||||
"start": 1.453,
|
||||
"end": 1.584
|
||||
},
|
||||
{
|
||||
"text": "better",
|
||||
"start": 1.595,
|
||||
"end": 1.946
|
||||
},
|
||||
{
|
||||
"text": "way",
|
||||
"start": 1.976,
|
||||
"end": 2.458
|
||||
},
|
||||
{
|
||||
"text": "Meet",
|
||||
"start": 2.5,
|
||||
"end": 2.763
|
||||
},
|
||||
{
|
||||
"text": "lookatu",
|
||||
"start": 2.786,
|
||||
"end": 3.443
|
||||
},
|
||||
{
|
||||
"text": "The",
|
||||
"start": 3.501,
|
||||
"end": 3.72
|
||||
},
|
||||
{
|
||||
"text": "child",
|
||||
"start": 3.739,
|
||||
"end": 4.045
|
||||
},
|
||||
{
|
||||
"text": "safety",
|
||||
"start": 4.072,
|
||||
"end": 4.423
|
||||
},
|
||||
{
|
||||
"text": "app",
|
||||
"start": 4.453,
|
||||
"end": 4.672
|
||||
},
|
||||
{
|
||||
"text": "that",
|
||||
"start": 4.691,
|
||||
"end": 4.954
|
||||
},
|
||||
{
|
||||
"text": "gives",
|
||||
"start": 4.977,
|
||||
"end": 5.284
|
||||
},
|
||||
{
|
||||
"text": "you",
|
||||
"start": 5.31,
|
||||
"end": 5.53
|
||||
},
|
||||
{
|
||||
"text": "peace",
|
||||
"start": 5.549,
|
||||
"end": 5.855
|
||||
},
|
||||
{
|
||||
"text": "of",
|
||||
"start": 5.882,
|
||||
"end": 6.057
|
||||
},
|
||||
{
|
||||
"text": "mind",
|
||||
"start": 6.072,
|
||||
"end": 6.598
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 4,
|
||||
"path": "assets/voice/04.wav",
|
||||
"duration_s": 6.599,
|
||||
"words": [
|
||||
{
|
||||
"text": "Set",
|
||||
"start": 0.5,
|
||||
"end": 0.715
|
||||
},
|
||||
{
|
||||
"text": "screen",
|
||||
"start": 0.734,
|
||||
"end": 1.078
|
||||
},
|
||||
{
|
||||
"text": "time",
|
||||
"start": 1.108,
|
||||
"end": 1.366
|
||||
},
|
||||
{
|
||||
"text": "limits",
|
||||
"start": 1.389,
|
||||
"end": 1.733
|
||||
},
|
||||
{
|
||||
"text": "that",
|
||||
"start": 1.763,
|
||||
"end": 2.021
|
||||
},
|
||||
{
|
||||
"text": "actually",
|
||||
"start": 2.044,
|
||||
"end": 2.474
|
||||
},
|
||||
{
|
||||
"text": "stick",
|
||||
"start": 2.512,
|
||||
"end": 3.071
|
||||
},
|
||||
{
|
||||
"text": "When",
|
||||
"start": 3.12,
|
||||
"end": 3.378
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 3.4,
|
||||
"end": 3.616
|
||||
},
|
||||
{
|
||||
"text": "time",
|
||||
"start": 3.634,
|
||||
"end": 3.892
|
||||
},
|
||||
{
|
||||
"text": "is",
|
||||
"start": 3.915,
|
||||
"end": 4.087
|
||||
},
|
||||
{
|
||||
"text": "up",
|
||||
"start": 4.102,
|
||||
"end": 4.403
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 4.43,
|
||||
"end": 4.645
|
||||
},
|
||||
{
|
||||
"text": "phone",
|
||||
"start": 4.663,
|
||||
"end": 4.965
|
||||
},
|
||||
{
|
||||
"text": "knows",
|
||||
"start": 4.991,
|
||||
"end": 5.55
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 5,
|
||||
"path": "assets/voice/05.wav",
|
||||
"duration_s": 5.703,
|
||||
"words": [
|
||||
{
|
||||
"text": "Choose",
|
||||
"start": 0.5,
|
||||
"end": 0.836
|
||||
},
|
||||
{
|
||||
"text": "which",
|
||||
"start": 0.865,
|
||||
"end": 1.16
|
||||
},
|
||||
{
|
||||
"text": "apps",
|
||||
"start": 1.185,
|
||||
"end": 1.437
|
||||
},
|
||||
{
|
||||
"text": "are",
|
||||
"start": 1.459,
|
||||
"end": 1.67
|
||||
},
|
||||
{
|
||||
"text": "okay",
|
||||
"start": 1.688,
|
||||
"end": 2.192
|
||||
},
|
||||
{
|
||||
"text": "Allow",
|
||||
"start": 2.236,
|
||||
"end": 2.53
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 2.556,
|
||||
"end": 2.766
|
||||
},
|
||||
{
|
||||
"text": "good",
|
||||
"start": 2.784,
|
||||
"end": 3.036
|
||||
},
|
||||
{
|
||||
"text": "ones",
|
||||
"start": 3.058,
|
||||
"end": 3.563
|
||||
},
|
||||
{
|
||||
"text": "Block",
|
||||
"start": 3.607,
|
||||
"end": 3.901
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 3.926,
|
||||
"end": 4.137
|
||||
},
|
||||
{
|
||||
"text": "rest",
|
||||
"start": 4.155,
|
||||
"end": 4.659
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 6,
|
||||
"path": "assets/voice/06.wav",
|
||||
"duration_s": 6.513,
|
||||
"words": [
|
||||
{
|
||||
"text": "And",
|
||||
"start": 0.5,
|
||||
"end": 0.71
|
||||
},
|
||||
{
|
||||
"text": "when",
|
||||
"start": 0.728,
|
||||
"end": 0.979
|
||||
},
|
||||
{
|
||||
"text": "it",
|
||||
"start": 1.001,
|
||||
"end": 1.169
|
||||
},
|
||||
{
|
||||
"text": "really",
|
||||
"start": 1.184,
|
||||
"end": 1.519
|
||||
},
|
||||
{
|
||||
"text": "matters",
|
||||
"start": 1.548,
|
||||
"end": 2.051
|
||||
},
|
||||
{
|
||||
"text": "see",
|
||||
"start": 2.095,
|
||||
"end": 2.305
|
||||
},
|
||||
{
|
||||
"text": "where",
|
||||
"start": 2.323,
|
||||
"end": 2.616
|
||||
},
|
||||
{
|
||||
"text": "they",
|
||||
"start": 2.642,
|
||||
"end": 2.893
|
||||
},
|
||||
{
|
||||
"text": "are",
|
||||
"start": 2.915,
|
||||
"end": 3.377
|
||||
},
|
||||
{
|
||||
"text": "Location",
|
||||
"start": 3.417,
|
||||
"end": 3.962
|
||||
},
|
||||
{
|
||||
"text": "only",
|
||||
"start": 4.009,
|
||||
"end": 4.261
|
||||
},
|
||||
{
|
||||
"text": "when",
|
||||
"start": 4.283,
|
||||
"end": 4.534
|
||||
},
|
||||
{
|
||||
"text": "you",
|
||||
"start": 4.556,
|
||||
"end": 4.766
|
||||
},
|
||||
{
|
||||
"text": "need",
|
||||
"start": 4.784,
|
||||
"end": 5.035
|
||||
},
|
||||
{
|
||||
"text": "it",
|
||||
"start": 5.057,
|
||||
"end": 5.477
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 7,
|
||||
"path": "assets/voice/07.wav",
|
||||
"duration_s": 5.468,
|
||||
"words": [
|
||||
{
|
||||
"text": "Because",
|
||||
"start": 0.5,
|
||||
"end": 0.896
|
||||
},
|
||||
{
|
||||
"text": "kids",
|
||||
"start": 0.93,
|
||||
"end": 1.194
|
||||
},
|
||||
{
|
||||
"text": "deserve",
|
||||
"start": 1.217,
|
||||
"end": 1.613
|
||||
},
|
||||
{
|
||||
"text": "some",
|
||||
"start": 1.647,
|
||||
"end": 1.911
|
||||
},
|
||||
{
|
||||
"text": "freedom",
|
||||
"start": 1.934,
|
||||
"end": 2.462
|
||||
},
|
||||
{
|
||||
"text": "and",
|
||||
"start": 2.508,
|
||||
"end": 2.728
|
||||
},
|
||||
{
|
||||
"text": "parents",
|
||||
"start": 2.747,
|
||||
"end": 3.143
|
||||
},
|
||||
{
|
||||
"text": "deserve",
|
||||
"start": 3.177,
|
||||
"end": 3.573
|
||||
},
|
||||
{
|
||||
"text": "some",
|
||||
"start": 3.607,
|
||||
"end": 3.871
|
||||
},
|
||||
{
|
||||
"text": "calm",
|
||||
"start": 3.894,
|
||||
"end": 4.422
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 8,
|
||||
"path": "assets/voice/08.wav",
|
||||
"duration_s": 3.783,
|
||||
"words": [
|
||||
{
|
||||
"text": "Works",
|
||||
"start": 0.5,
|
||||
"end": 0.842
|
||||
},
|
||||
{
|
||||
"text": "on",
|
||||
"start": 0.872,
|
||||
"end": 1.067
|
||||
},
|
||||
{
|
||||
"text": "Android",
|
||||
"start": 1.084,
|
||||
"end": 1.524
|
||||
},
|
||||
{
|
||||
"text": "and",
|
||||
"start": 1.562,
|
||||
"end": 1.806
|
||||
},
|
||||
{
|
||||
"text": "on",
|
||||
"start": 1.827,
|
||||
"end": 2.023
|
||||
},
|
||||
{
|
||||
"text": "iPhone",
|
||||
"start": 2.04,
|
||||
"end": 2.724
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 9,
|
||||
"path": "assets/voice/09.wav",
|
||||
"duration_s": 5.772,
|
||||
"words": [
|
||||
{
|
||||
"text": "lookatu",
|
||||
"start": 0.5,
|
||||
"end": 1.106
|
||||
},
|
||||
{
|
||||
"text": "Look",
|
||||
"start": 1.158,
|
||||
"end": 1.401
|
||||
},
|
||||
{
|
||||
"text": "out",
|
||||
"start": 1.422,
|
||||
"end": 1.623
|
||||
},
|
||||
{
|
||||
"text": "for",
|
||||
"start": 1.641,
|
||||
"end": 1.843
|
||||
},
|
||||
{
|
||||
"text": "them",
|
||||
"start": 1.86,
|
||||
"end": 2.345
|
||||
},
|
||||
{
|
||||
"text": "Download",
|
||||
"start": 2.387,
|
||||
"end": 2.791
|
||||
},
|
||||
{
|
||||
"text": "it",
|
||||
"start": 2.826,
|
||||
"end": 2.987
|
||||
},
|
||||
{
|
||||
"text": "today",
|
||||
"start": 3.001,
|
||||
"end": 3.526
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sfx": []
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"provider": "kokoro",
|
||||
"speed": "0.85",
|
||||
"lines": [
|
||||
{
|
||||
"id": "01",
|
||||
"text": "Where are they right now? And what are they really doing on that phone?"
|
||||
},
|
||||
{
|
||||
"id": "02",
|
||||
"text": "Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."
|
||||
},
|
||||
{
|
||||
"id": "03",
|
||||
"text": "There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."
|
||||
},
|
||||
{
|
||||
"id": "04",
|
||||
"text": "Set screen time limits that actually stick. When the time is up, the phone knows."
|
||||
},
|
||||
{
|
||||
"id": "05",
|
||||
"text": "Choose which apps are okay. Allow the good ones. Block the rest."
|
||||
},
|
||||
{
|
||||
"id": "06",
|
||||
"text": "And when it really matters, see where they are. Location, only when you need it."
|
||||
},
|
||||
{
|
||||
"id": "07",
|
||||
"text": "Because kids deserve some freedom, and parents deserve some calm."
|
||||
},
|
||||
{
|
||||
"id": "08",
|
||||
"text": "Works on Android and on iPhone."
|
||||
},
|
||||
{
|
||||
"id": "09",
|
||||
"text": "lookatu. Look out for them. Download it today."
|
||||
}
|
||||
],
|
||||
"bgm": {
|
||||
"mode": "none"
|
||||
},
|
||||
"voice": "af_heart"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
No assets were captured. No website, logo or screenshots supplied. Use invented phone UI mock-ups and a typographic lookatu wordmark.
|
||||
@@ -0,0 +1 @@
|
||||
{ "title": "lookatu", "description": "Child-safety app (Android and iOS) that lets parents control kids' phone usage and track location when needed.", "colors": [], "fonts": [] }
|
||||
@@ -0,0 +1,4 @@
|
||||
lookatu - child safety app for Android and iOS.
|
||||
Parents control how their kids use the phone (time limits, app control).
|
||||
Parents can track location when needed.
|
||||
Message: keep your kids safe on their phones, with peace of mind for parents.
|
||||
@@ -0,0 +1,235 @@
|
||||
<template id="captions-template" data-composition-id="captions" data-width="1080" data-height="1920">
|
||||
<script src="assets/vendor/gsap.min.js"></script>
|
||||
|
||||
|
||||
<style data-brand-tokens>
|
||||
@font-face { font-family: 'Quicksand'; src: url('assets/fonts/quicksand-var.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: block; }
|
||||
:root {
|
||||
--cream: #F5F0E6;
|
||||
--turquoise: #7ECDC0;
|
||||
--soft-pink: #F7C8D4;
|
||||
--butter: #FDE68A;
|
||||
--mint: #A8E6CF;
|
||||
--lavender: #D4A5E8;
|
||||
--peach: #FFCBA4;
|
||||
--sky: #A8D8F0;
|
||||
--coral: #F8635F;
|
||||
--text-dark: #2D2D2D;
|
||||
--text-muted: #6B6B6B;
|
||||
--white: #FFFFFF;
|
||||
--cap-ink: #2D2D2D;
|
||||
--cap-canvas: #F5F0E6;
|
||||
--cap-accent: #F8635F;
|
||||
--cap-accent-2: #FDE68A;
|
||||
--font-display: "Fredoka One", system-ui, serif;
|
||||
--font-body: "Quicksand", system-ui, sans-serif;
|
||||
--cap-band-top: 1600px;
|
||||
--cap-band-height: 320px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Fredoka One"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE picture-book card — cream surface, 3px charcoal outline, generous 24px radius,
|
||||
a hard 6px charcoal offset shadow (no blur): the sticker-on-paper signature */
|
||||
.caption-pill {
|
||||
max-width: 80%;
|
||||
padding: 18px 40px 22px;
|
||||
background: var(--cap-canvas, #f5f0e6);
|
||||
border: 3px solid var(--cap-ink, #2d2d2d);
|
||||
border-radius: 24px;
|
||||
box-shadow: 6px 6px 0 var(--cap-ink, #2d2d2d);
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.18em 0.32em;
|
||||
font-family: "Fredoka One", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: clamp(38px, 4.2vw, 56px);
|
||||
line-height: 1.22;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* upcoming — muted charcoal, no candy block */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0.02em 0.16em;
|
||||
border-radius: 12px;
|
||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 52%, var(--cap-canvas, #f5f0e6));
|
||||
}
|
||||
/* current word — THE sunny-pastel candy block: a rounded coral chip with a 3px
|
||||
charcoal outline + a hard 3px charcoal offset shadow (no blur), charcoal on top */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #2d2d2d);
|
||||
background: var(--cap-accent, #f8635f);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 0 0 3px var(--cap-ink, #2d2d2d),
|
||||
3px 5px 0 0 var(--cap-ink, #2d2d2d);
|
||||
}
|
||||
/* already spoken — solid charcoal, candy block cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #2d2d2d);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — a butter candy chip, the cheerful second pastel */
|
||||
.caption-word.cap-brand {
|
||||
color: var(--cap-ink, #2d2d2d);
|
||||
background: var(--cap-accent-2, #fde68a);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 0 3px var(--cap-ink, #2d2d2d);
|
||||
}
|
||||
/* numerals / data — Quicksand, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: "Quicksand", system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 78%, var(--cap-canvas, #f5f0e6));
|
||||
}
|
||||
/* emphasis — a butter underline rule (the second sunny pastel) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 3px;
|
||||
text-decoration-color: var(--cap-accent-2, #fde68a);
|
||||
text-underline-offset: 0.12em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(32px, 5.2vw, 46px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="54.733"
|
||||
data-fps="30"
|
||||
data-width="1080"
|
||||
data-height="1920"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// captions.mjs injects the engine's scene-aware groups + real duration here.
|
||||
var GROUPS = [{"id":"caption-group-0","frame":1,"start":0.8,"end":1.28,"text":"Where are","words":[{"id":"caption-word-0-0","text":"Where","start":0.8,"end":1.058},{"id":"caption-word-0-1","text":"are","start":1.08,"end":1.264}]},{"id":"caption-group-1","frame":1,"start":1.28,"end":1.8,"text":"they right","words":[{"id":"caption-word-1-0","text":"they","start":1.28,"end":1.501},{"id":"caption-word-1-1","text":"right","start":1.52,"end":1.778}]},{"id":"caption-group-2","frame":1,"start":1.8,"end":2.44,"text":"now And","words":[{"id":"caption-word-2-0","text":"now","start":1.8,"end":2.205},{"id":"caption-word-2-1","text":"And","start":2.24,"end":2.424}]},{"id":"caption-group-3","frame":1,"start":2.44,"end":2.88,"text":"what are","words":[{"id":"caption-word-3-0","text":"what","start":2.44,"end":2.661},{"id":"caption-word-3-1","text":"are","start":2.68,"end":2.864}]},{"id":"caption-group-4","frame":1,"start":2.88,"end":3.44,"text":"they really","words":[{"id":"caption-word-4-0","text":"they","start":2.88,"end":3.101},{"id":"caption-word-4-1","text":"really","start":3.12,"end":3.414}]},{"id":"caption-group-5","frame":1,"start":3.44,"end":3.88,"text":"doing on","words":[{"id":"caption-word-5-0","text":"doing","start":3.44,"end":3.698},{"id":"caption-word-5-1","text":"on","start":3.72,"end":3.867}]},{"id":"caption-group-6","frame":1,"start":3.88,"end":4.718,"text":"that phone","words":[{"id":"caption-word-6-0","text":"that","start":3.88,"end":4.101},{"id":"caption-word-6-1","text":"phone","start":4.12,"end":4.598}]},{"id":"caption-group-7","frame":2,"start":6.14,"end":8.158,"text":"Endless scrolling Arguments at","words":[{"id":"caption-word-7-0","text":"Endless","start":6.14,"end":6.548},{"id":"caption-word-7-1","text":"scrolling","start":6.583,"end":7.353},{"id":"caption-word-7-2","text":"Arguments","start":7.42,"end":7.918},{"id":"caption-word-7-3","text":"at","start":7.961,"end":8.142}]},{"id":"caption-group-8","frame":2,"start":8.158,"end":9.388,"text":"dinner And that","words":[{"id":"caption-word-8-0","text":"dinner","start":8.158,"end":8.792},{"id":"caption-word-8-1","text":"And","start":8.847,"end":9.073},{"id":"caption-word-8-2","text":"that","start":9.093,"end":9.365}]},{"id":"caption-group-9","frame":2,"start":9.388,"end":10.078,"text":"quiet worry","words":[{"id":"caption-word-9-0","text":"quiet","start":9.388,"end":9.705},{"id":"caption-word-9-1","text":"worry","start":9.733,"end":10.05}]},{"id":"caption-group-10","frame":2,"start":10.078,"end":10.668,"text":"you never","words":[{"id":"caption-word-10-0","text":"you","start":10.078,"end":10.304},{"id":"caption-word-10-1","text":"never","start":10.324,"end":10.641}]},{"id":"caption-group-11","frame":2,"start":10.668,"end":11.824,"text":"say out loud","words":[{"id":"caption-word-11-0","text":"say","start":10.668,"end":10.895},{"id":"caption-word-11-1","text":"out","start":10.914,"end":11.141},{"id":"caption-word-11-2","text":"loud","start":11.16,"end":11.704}]},{"id":"caption-group-12","frame":3,"start":13.251,"end":13.823,"text":"There has","words":[{"id":"caption-word-12-0","text":"There","start":13.251,"end":13.558},{"id":"caption-word-12-1","text":"has","start":13.584,"end":13.803}]},{"id":"caption-group-13","frame":3,"start":13.823,"end":14.204,"text":"to be","words":[{"id":"caption-word-13-0","text":"to","start":13.823,"end":13.998},{"id":"caption-word-13-1","text":"be","start":14.013,"end":14.188}]},{"id":"caption-group-14","frame":3,"start":14.204,"end":15.251,"text":"a better way","words":[{"id":"caption-word-14-0","text":"a","start":14.204,"end":14.335},{"id":"caption-word-14-1","text":"better","start":14.346,"end":14.697},{"id":"caption-word-14-2","text":"way","start":14.727,"end":15.209}]},{"id":"caption-group-15","frame":3,"start":15.251,"end":16.823,"text":"Meet lookatu The child","words":[{"id":"caption-word-15-0","text":"Meet","start":15.251,"end":15.514},{"id":"caption-word-15-1","text":"lookatu","start":15.537,"end":16.194},{"id":"caption-word-15-2","text":"The","start":16.252,"end":16.471},{"id":"caption-word-15-3","text":"child","start":16.49,"end":16.796}]},{"id":"caption-group-16","frame":3,"start":16.823,"end":17.442,"text":"safety app","words":[{"id":"caption-word-16-0","text":"safety","start":16.823,"end":17.174},{"id":"caption-word-16-1","text":"app","start":17.204,"end":17.423}]},{"id":"caption-group-17","frame":3,"start":17.442,"end":18.061,"text":"that gives","words":[{"id":"caption-word-17-0","text":"that","start":17.442,"end":17.705},{"id":"caption-word-17-1","text":"gives","start":17.728,"end":18.035}]},{"id":"caption-group-18","frame":3,"start":18.061,"end":18.633,"text":"you peace","words":[{"id":"caption-word-18-0","text":"you","start":18.061,"end":18.281},{"id":"caption-word-18-1","text":"peace","start":18.3,"end":18.606}]},{"id":"caption-group-19","frame":3,"start":18.633,"end":19.469,"text":"of mind","words":[{"id":"caption-word-19-0","text":"of","start":18.633,"end":18.808},{"id":"caption-word-19-1","text":"mLine truncated
|
||||
var DURATION = 54.733;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.82 }, { scale: 1, duration: 0.2, ease: "back.out(2.6)" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.caption-line { line-height: 1.1 !important; }
|
||||
</style>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f1-bg { position: absolute; inset: 0; }
|
||||
.f1-orn { position: absolute; z-index: 1; }
|
||||
.f1-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f1-z { position: absolute; z-index: 2; }
|
||||
.f1-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f1-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f1-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f1-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f1-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f1-bub { position: absolute; z-index: 2; display: flex; align-items: center; justify-content: center; border-radius: 70px; }
|
||||
.f1-bub span { font-size: 168px; color: #2D2D2D; }
|
||||
.f1-b1 { left: 80px; top: 250px; width: 760px; height: 360px; }
|
||||
.f1-b2 { left: 240px; top: 700px; width: 760px; height: 360px; background: #FDE68A; }
|
||||
.f1-tail1 { position: absolute; z-index: 3; left: 190px; top: 584px; width: 70px; height: 70px; background: #FFFFFF; border-right: 5px solid #2D2D2D; border-bottom: 5px solid #2D2D2D; transform: rotate(45deg); }
|
||||
.f1-tail2 { position: absolute; z-index: 3; left: 820px; top: 1034px; width: 70px; height: 70px; background: #FDE68A; border-right: 5px solid #2D2D2D; border-bottom: 5px solid #2D2D2D; transform: rotate(45deg); }
|
||||
.f1-ph { left: 390px; top: 1160px; width: 300px; height: 420px; background: #2D2D2D; border: 6px solid #2D2D2D; border-radius: 56px; box-shadow: 12px 12px 0 rgba(45,45,45,0.25); display: flex; align-items: center; justify-content: center; }
|
||||
.f1-ph b { font-family: "Fredoka", sans-serif; font-weight: 600; font-size: 170px; color: #FDE68A; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="01-hook" data-width="1080" data-height="1920" data-duration="6.14">
|
||||
<div id="scene1-bg" class="clip f1-bg" data-start="0" data-duration="6.14" data-track-index="0" style="background:#A8D8F0"></div>
|
||||
<div class="f1-orn" style="left:-70px;top:90px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(-4deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f1-orn" style="left:800px;top:40px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(10deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f1-orn" style="left:900px;top:1150px;width:150px;height:150px"><svg viewBox="0 0 100 100" style="transform:rotate(14deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f1-orn" style="left:-50px;top:1330px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(8deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f1-orn" style="left:760px;top:1380px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(5deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f1-bub f1-card f1-b1"><span class="f1-d">WHERE?</span></div>
|
||||
<div class="f1-tail1"></div>
|
||||
<div class="f1-bub f1-card f1-b2"><span class="f1-d">WHAT?</span></div>
|
||||
<div class="f1-tail2"></div>
|
||||
<div class="f1-z f1-ph"><b>?</b></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 5.64;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f1-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f1-b1", { scale: 0.2, opacity: 0, rotation: -8 }, { scale: 1, opacity: 1, rotation: -2, duration: 0.55, ease: "back.out(1.9)" }, 0.3);
|
||||
tl.fromTo(".f1-tail1", { opacity: 0 }, { opacity: 1, duration: 0.2 }, 0.6499999999999999);
|
||||
tl.fromTo(".f1-b2", { scale: 0.2, opacity: 0, rotation: 8 }, { scale: 1, opacity: 1, rotation: 2, duration: 0.55, ease: "back.out(1.9)" }, 2.12);
|
||||
tl.fromTo(".f1-tail2", { opacity: 0 }, { opacity: 1, duration: 0.2 }, 2.47);
|
||||
tl.fromTo(".f1-ph", { scale: 0, rotation: 12 }, { scale: 1, rotation: 0, duration: 0.6, ease: "back.out(1.7)" }, 3);
|
||||
tl.to(".f1-ph", { rotation: -5, duration: 0.18, ease: "sine.inOut" }, 3.6)
|
||||
.to(".f1-ph", { rotation: 5, duration: 0.3, ease: "sine.inOut" }, ">")
|
||||
.to(".f1-ph", { rotation: -3, duration: 0.3, ease: "sine.inOut" }, ">")
|
||||
.to(".f1-ph", { rotation: 0, duration: 0.25, ease: "sine.inOut" }, ">");
|
||||
window.__timelines["01-hook"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f2-bg { position: absolute; inset: 0; }
|
||||
.f2-orn { position: absolute; z-index: 1; }
|
||||
.f2-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f2-z { position: absolute; z-index: 2; }
|
||||
.f2-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f2-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f2-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f2-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f2-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f2-row { position: absolute; z-index: 2; left: 100px; width: 880px; height: 320px; display: flex; align-items: center; padding: 0 44px; gap: 34px; }
|
||||
.f2-row svg { width: 130px; height: 130px; flex: none; background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 50%; padding: 22px; box-sizing: border-box; }
|
||||
.f2-row span { font-size: 92px; color: #2D2D2D; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="02-worry" data-width="1080" data-height="1920" data-duration="7.511">
|
||||
<div id="scene2-bg" class="clip f2-bg" data-start="0" data-duration="7.511" data-track-index="0" style="background:#F5F0E6"></div>
|
||||
<div class="f2-orn" style="left:880px;top:90px;width:170px;height:170px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f2-orn" style="left:-60px;top:150px;width:220px;height:220px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f2-orn" style="left:780px;top:1360px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(-6deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f2-orn" style="left:50px;top:1420px;width:130px;height:130px"><svg viewBox="0 0 100 100" style="transform:rotate(-14deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f2-orn" style="left:900px;top:700px;width:170px;height:170px"><svg viewBox="0 0 100 100" style="transform:rotate(20deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
|
||||
<div class="f2-row f2-card f2-r0" style="top:250px;background:#FDE68A"><svg viewBox="0 0 100 100"><path d="M20 34 L50 62 L80 34" fill="none" stroke="#2D2D2D" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 12 L50 40 L80 12" fill="none" stroke="#2D2D2D" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="f2-d">SCROLLING</span></div>
|
||||
<div class="f2-row f2-card f2-r1" style="top:650px;background:#F7C8D4"><svg viewBox="0 0 100 100"><polygon points="58,6 24,54 48,54 40,94 76,42 52,42" fill="#F8635F" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/></svg><span class="f2-d">ARGUING</span></div>
|
||||
<div class="f2-row f2-card f2-r2" style="top:1050px;background:#D4A5E8"><svg viewBox="0 0 100 100"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/></svg><span class="f2-d">WORRYING</span></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 7.111;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f2-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
tl.fromTo(".f2-r0", { x: -260, opacity: 0, rotation: -12 }, { x: 0, opacity: 1, rotation: -3, duration: 0.6, ease: "back.out(1.5)" }, 0.38);
|
||||
tl.fromTo(".f2-r1", { x: 260, opacity: 0, rotation: 10 }, { x: 0, opacity: 1, rotation: 2.5, duration: 0.6, ease: "back.out(1.5)" }, 1.66);
|
||||
tl.fromTo(".f2-r2", { x: -260, opacity: 0, rotation: -8 }, { x: 0, opacity: 1, rotation: -2, duration: 0.6, ease: "back.out(1.5)" }, 3.09);
|
||||
window.__timelines["02-worry"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f3-bg { position: absolute; inset: 0; }
|
||||
.f3-orn { position: absolute; z-index: 1; }
|
||||
.f3-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f3-z { position: absolute; z-index: 2; }
|
||||
.f3-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f3-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f3-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f3-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f3-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f3-eye { display: block; position: relative; width: 128.8px; height: 128.8px; border-radius: 50%; background: #FFFFFF; border: 6.8999999999999995px solid #2D2D2D; box-sizing: border-box; text-shadow: none; }
|
||||
.f3-pup { position: absolute; left: 50%; top: 50%; width: 50.6px; height: 50.6px; margin: -25.3px 0 0 -25.3px; border-radius: 50%; background: #2D2D2D; }
|
||||
.f3-badge { left: 50%; top: 250px; margin-left: -330px; width: 660px; justify-content: center; background: #FDE68A; }
|
||||
.f3-badge span { font-size: 62px; }
|
||||
.f3-wmwrap { left: 0; right: 0; top: 700px; }
|
||||
.f3-shield { left: 50%; top: 380px; margin-left: -115px; width: 230px; height: 260px; }
|
||||
.f3-peace { left: 50%; top: 1130px; margin-left: -400px; width: 800px; height: 200px; display: flex; align-items: center; justify-content: center; gap: 36px; }
|
||||
.f3-peace span { font-size: 78px; }
|
||||
.f3-peace svg { width: 100px; height: 100px; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="03-meet" data-width="1080" data-height="1920" data-duration="8.644">
|
||||
<div id="scene3-bg" class="clip f3-bg" data-start="0" data-duration="8.644" data-track-index="0" style="background:#7ECDC0"></div>
|
||||
<div class="f3-orn" style="left:790px;top:60px;width:280px;height:280px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f3-orn" style="left:-80px;top:200px;width:330px;height:330px"><svg viewBox="0 0 100 100" style="transform:rotate(-3deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f3-orn" style="left:-40px;top:1250px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(10deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f3-orn" style="left:820px;top:1330px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(-12deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f3-orn" style="left:130px;top:1010px;width:120px;height:120px"><svg viewBox="0 0 100 100" style="transform:rotate(8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f3-orn" style="left:830px;top:1020px;width:140px;height:140px"><svg viewBox="0 0 100 100" style="transform:rotate(-10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f3-z f3-pill f3-badge"><span class="f3-d">A BETTER WAY</span></div>
|
||||
<svg class="f3-z f3-shield" viewBox="0 0 100 112"><path d="M50 4 L92 18 V54 C92 82 70 100 50 108 C30 100 8 82 8 54 V18 Z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="4.5" stroke-linejoin="round"/><path d="M30 56 L45 71 L72 40" fill="none" stroke="#F8635F" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<div class="f3-z f3-wmwrap">
|
||||
<div class="f3-wm f3-d" style="display:flex;align-items:flex-end;justify-content:center;font-size:230px;color:#2D2D2D;text-shadow:8px 8px 0 #FFFFFF;line-height:1">
|
||||
<span>l</span>
|
||||
<span class="f3-eye" style="margin:0 6.8999999999999995px 13.799999999999999px 11.5px"><i class="f3-pup"></i></span>
|
||||
<span class="f3-eye" style="margin:0 11.5px 13.799999999999999px 6.8999999999999995px"><i class="f3-pup"></i></span>
|
||||
<span>katu</span>
|
||||
</div></div>
|
||||
<div class="f3-z f3-card f3-peace"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M50 86 C8 56 10 18 32 18 C42 18 48 25 50 32 C52 25 58 18 68 18 C90 18 92 56 50 86 Z" fill="#F8635F" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg><span class="f3-d">PEACE OF MIND</span></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 8.144;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f3-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f3-badge", { y: -80, opacity: 0, scale: 0.7 }, { y: 0, opacity: 1, scale: 1, duration: 0.55, ease: "back.out(1.8)" }, 0.38);
|
||||
tl.fromTo(".f3-shield", { scale: 0, rotation: -20, opacity: 0 }, { scale: 1, rotation: 0, opacity: 1, duration: 0.65, ease: "back.out(1.9)" }, 1.48);
|
||||
tl.fromTo(".f3-wm", { scale: 0.3, opacity: 0, y: 60 }, { scale: 1, opacity: 1, y: 0, duration: 0.75, ease: "back.out(1.6)" }, 2.38);
|
||||
|
||||
const pups = document.querySelectorAll(".f3-pup"), r = 23;
|
||||
tl.fromTo(pups, { x: 0, y: 0 }, { x: -r, y: -r * 0.4, duration: 0.35, ease: "power2.out" }, 2.9699999999999998)
|
||||
.to(pups, { x: r, y: r * 0.3, duration: 0.5, ease: "power2.inOut" }, 3.57)
|
||||
.to(pups, { x: 0, y: 0, duration: 0.35, ease: "power2.out" }, 4.32);
|
||||
tl.fromTo(".f3-peace", { y: 100, opacity: 0, rotation: -3 }, { y: 0, opacity: 1, rotation: 0, duration: 0.6, ease: "back.out(1.6)" }, 5.43);
|
||||
tl.to(".f3-peace svg", { scale: 1.2, duration: 0.3, ease: "sine.inOut" }, 6.13).to(".f3-peace svg", { scale: 1, duration: 0.3, ease: "sine.inOut" }, ">");
|
||||
window.__timelines["03-meet"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f4-bg { position: absolute; inset: 0; }
|
||||
.f4-orn { position: absolute; z-index: 1; }
|
||||
.f4-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f4-z { position: absolute; z-index: 2; }
|
||||
.f4-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f4-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f4-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f4-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f4-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f4-label { position: absolute; left: 0; right: 0; top: 130px; text-align: center; }
|
||||
.f4-label .f4-pill { background: #FDE68A; padding: 14px 44px; } .f4-label span { font-size: 56px; }
|
||||
.f4-ringbox { position: absolute; left: 90px; top: 250px; width: 380px; height: 380px; }
|
||||
.f4-ringbox svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.f4-time { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 104px; }
|
||||
.f4-lock { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.f4-lock svg { position: static; width: 170px; height: 170px; }
|
||||
.f4-state { position: absolute; left: 50px; top: 740px; width: 460px; text-align: center; }
|
||||
.f4-state .f4-pill { padding: 14px 0; width: 100%; justify-content: center; box-shadow: 6px 6px 0 #2D2D2D; } .f4-state span { font-size: 56px; }
|
||||
.f4-s2 { position: absolute; left: 0; top: 0; background: #F8635F; color: #FFFFFF; opacity: 0; }
|
||||
.f4-bar { position: absolute; left: 70px; top: 930px; width: 420px; height: 26px; border: 4px solid #2D2D2D; border-radius: 20px; background: #F5F0E6; overflow: hidden; }
|
||||
.f4-bar i { display: block; height: 100%; width: 100%; background: #A8E6CF; transform-origin: left center; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="04-limits" data-width="1080" data-height="1920" data-duration="7.099">
|
||||
<div id="scene4-bg" class="clip f4-bg" data-start="0" data-duration="7.099" data-track-index="0" style="background:#F5F0E6"></div>
|
||||
<div class="f4-orn" style="left:-70px;top:120px;width:240px;height:240px"><svg viewBox="0 0 100 100" style="transform:rotate(5deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f4-orn" style="left:840px;top:90px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f4-orn" style="left:30px;top:1050px;width:140px;height:140px"><svg viewBox="0 0 100 100" style="transform:rotate(-8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f4-orn" style="left:770px;top:1180px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(4deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f4-orn" style="left:40px;top:1400px;width:190px;height:190px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
|
||||
<div class="f4-label f4-z"><span class="f4-pill"><span class="f4-d">SCREEN TIME</span></span></div>
|
||||
<div class="f4-phone">
|
||||
<div class="f4-notch"></div>
|
||||
<div class="f4-ringbox">
|
||||
<svg viewBox="0 0 380 380"><circle cx="190" cy="190" r="150" fill="none" stroke="#E8E0D4" stroke-width="40"/><circle class="f4-arc" cx="190" cy="190" r="150" fill="none" stroke="#7ECDC0" stroke-width="40" stroke-linecap="round" stroke-dasharray="942.5" stroke-dashoffset="0" transform="rotate(-90 190 190)"/></svg>
|
||||
<div class="f4-time f4-d">1:00</div>
|
||||
<div class="f4-lock"><svg viewBox="0 0 100 100"><rect x="20" y="42" width="60" height="46" rx="10" fill="#F8635F" stroke="#2D2D2D" stroke-width="5"/><path d="M33 42 V30 a17 17 0 0 1 34 0 V42" fill="none" stroke="#2D2D2D" stroke-width="6" stroke-linecap="round"/><circle cx="50" cy="65" r="6" fill="#2D2D2D"/></svg></div>
|
||||
</div>
|
||||
<div class="f4-state"><div class="f4-pill f4-s1" style="background:#A8E6CF"><span class="f4-d">LIMIT SET</span></div><div class="f4-pill f4-s2"><span class="f4-d">TIME'S UP</span></div></div>
|
||||
<div class="f4-bar"><i></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 6.599;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f4-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
const t0 = 0.38, tEnd = 3.79;
|
||||
tl.fromTo(".f4-label", { y: -60, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, ease: "back.out(1.6)" }, t0);
|
||||
tl.fromTo(".f4-phone", { y: 160, opacity: 0, rotation: 4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.7, ease: "power3.out" }, t0);
|
||||
tl.set(".f4-lock", { opacity: 0 }, 0);
|
||||
tl.fromTo(".f4-state", { scale: 0.6, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.45, ease: "back.out(1.8)" }, 0.61);
|
||||
tl.to(".f4-arc", { strokeDashoffset: 942.5, duration: tEnd - 0.99, ease: "none" }, 0.99);
|
||||
tl.to(".f4-bar i", { scaleX: 0, duration: tEnd - 0.99, ease: "none" }, 0.99);
|
||||
const cd = { v: 60 }, tm = document.querySelector(".f4-time");
|
||||
tl.to(cd, { v: 0, duration: tEnd - 0.99, ease: "none", onUpdate: () => { const s = Math.ceil(cd.v); tm.textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); } }, 0.99);
|
||||
tl.to(".f4-time", { opacity: 0, duration: 0.2 }, tEnd);
|
||||
tl.to(".f4-arc", { opacity: 0, duration: 0.1 }, tEnd - 0.05);
|
||||
tl.fromTo(".f4-lock", { opacity: 0, scale: 0.3, rotation: -25 }, { opacity: 1, scale: 1, rotation: 0, duration: 0.55, ease: "back.out(2)" }, tEnd);
|
||||
tl.to(".f4-s1", { opacity: 0, duration: 0.15 }, tEnd).to(".f4-s2", { opacity: 1, duration: 0.2 }, tEnd);
|
||||
tl.to(".f4-phone", { rotation: -2, duration: 0.12, ease: "sine.inOut" }, tEnd + 0.1).to(".f4-phone", { rotation: 2, duration: 0.16, ease: "sine.inOut" }, ">").to(".f4-phone", { rotation: 0, duration: 0.14, ease: "sine.inOut" }, ">");
|
||||
window.__timelines["04-limits"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f5-bg { position: absolute; inset: 0; }
|
||||
.f5-orn { position: absolute; z-index: 1; }
|
||||
.f5-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f5-z { position: absolute; z-index: 2; }
|
||||
.f5-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f5-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f5-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f5-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f5-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f5-row { position: absolute; left: 36px; width: 488px; height: 170px; display: flex; align-items: center; gap: 22px; padding: 0 24px; background: #FFFFFF; border: 4px solid #2D2D2D; border-radius: 34px; box-shadow: 6px 6px 0 #2D2D2D; }
|
||||
.f5-ic { flex: none; width: 92px; height: 92px; border-radius: 50%; border: 4px solid #2D2D2D; display: flex; align-items: center; justify-content: center; font-size: 52px; }
|
||||
.f5-nm { font-size: 46px; flex: 1; }
|
||||
.f5-tg { flex: none; position: relative; width: 120px; height: 66px; border-radius: 40px; border: 4px solid #2D2D2D; background: #D9D4C7; }
|
||||
.f5-kn { position: absolute; left: 4px; top: 4px; width: 50px; height: 50px; border-radius: 50%; background: #FFFFFF; border: 4px solid #2D2D2D; box-sizing: border-box; width: 50px; }
|
||||
.f5-blk { position: absolute; right: -12px; top: -22px; background: #F8635F; color: #FFFFFF; border: 4px solid #2D2D2D; border-radius: 30px; padding: 4px 16px; font-size: 30px; opacity: 0; }
|
||||
.f5-head { position: absolute; left: 0; right: 0; top: 80px; text-align: center; font-size: 56px; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="05-apps" data-width="1080" data-height="1920" data-duration="6.203">
|
||||
<div id="scene5-bg" class="clip f5-bg" data-start="0" data-duration="6.203" data-track-index="0" style="background:#A8D8F0"></div>
|
||||
<div class="f5-orn" style="left:-90px;top:90px;width:320px;height:320px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f5-orn" style="left:880px;top:150px;width:170px;height:170px"><svg viewBox="0 0 100 100" style="transform:rotate(14deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f5-orn" style="left:-50px;top:1180px;width:250px;height:250px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f5-orn" style="left:800px;top:1250px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f5-orn" style="left:900px;top:760px;width:110px;height:110px"><svg viewBox="0 0 100 100" style="transform:rotate(-12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f5-phone">
|
||||
<div class="f5-notch"></div>
|
||||
<div class="f5-head f5-d">APPS</div>
|
||||
<div class="f5-row f5-r0" style="top:190px"><div class="f5-ic f5-d" style="background:#A8E6CF">L</div><div class="f5-nm f5-d">Learn</div><div class="f5-tg f5-t0"><div class="f5-kn f5-k0"></div></div></div>
|
||||
<div class="f5-row f5-r1" style="top:400px"><div class="f5-ic f5-d" style="background:#A8D8F0">D</div><div class="f5-nm f5-d">Draw</div><div class="f5-tg f5-t1"><div class="f5-kn f5-k1"></div></div></div>
|
||||
<div class="f5-row f5-r2" style="top:610px"><div class="f5-ic f5-d" style="background:#FFCBA4">G</div><div class="f5-nm f5-d">Games</div><div class="f5-tg f5-t2"><div class="f5-kn f5-k2"></div></div><div class="f5-blk f5-d f5-bl2">BLOCKED</div></div>
|
||||
<div class="f5-row f5-r3" style="top:820px"><div class="f5-ic f5-d" style="background:#D4A5E8">V</div><div class="f5-nm f5-d">Videos</div><div class="f5-tg f5-t3"><div class="f5-kn f5-k3"></div></div><div class="f5-blk f5-d f5-bl3">BLOCKED</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 5.703;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f5-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f5-phone", { y: 160, opacity: 0, rotation: -4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.7, ease: "power3.out" }, 0.38);
|
||||
tl.fromTo(".f5-r0", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 0.88);
|
||||
tl.fromTo(".f5-r1", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 1.16);
|
||||
tl.fromTo(".f5-r2", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 1.44);
|
||||
tl.fromTo(".f5-r3", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 1.72);
|
||||
tl.to(".f5-k0", { x: 54, duration: 0.3, ease: "back.out(1.7)" }, 2.12).to(".f5-t0", { backgroundColor: "#A8E6CF", duration: 0.3 }, 2.12);
|
||||
tl.to(".f5-k1", { x: 54, duration: 0.3, ease: "back.out(1.7)" }, 2.57).to(".f5-t1", { backgroundColor: "#A8E6CF", duration: 0.3 }, 2.57);
|
||||
tl.to(".f5-r2", { opacity: 0.55, duration: 0.3 }, 3.49).fromTo(".f5-bl2", { opacity: 0, scale: 0.3, rotation: -10 }, { opacity: 1, scale: 1, rotation: -4, duration: 0.4, ease: "back.out(2)" }, 3.49);
|
||||
tl.to(".f5-r3", { opacity: 0.55, duration: 0.3 }, 3.79).fromTo(".f5-bl3", { opacity: 0, scale: 0.3, rotation: -10 }, { opacity: 1, scale: 1, rotation: -4, duration: 0.4, ease: "back.out(2)" }, 3.79);
|
||||
window.__timelines["05-apps"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f6-bg { position: absolute; inset: 0; }
|
||||
.f6-orn { position: absolute; z-index: 1; }
|
||||
.f6-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f6-z { position: absolute; z-index: 2; }
|
||||
.f6-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f6-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f6-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f6-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f6-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f6-btn { position: absolute; left: 70px; top: 100px; width: 420px; justify-content: center; padding: 18px 0; background: #7ECDC0; box-shadow: 6px 6px 0 #2D2D2D; }
|
||||
.f6-btn span { font-size: 50px; }
|
||||
.f6-map { position: absolute; left: 36px; top: 260px; width: 488px; height: 780px; border: 5px solid #2D2D2D; border-radius: 40px; background: #A8E6CF; overflow: hidden; }
|
||||
.f6-map svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
.f6-pin { position: absolute; left: 244px; top: 360px; width: 120px; margin-left: -60px; height: 150px; margin-top: -150px; }
|
||||
.f6-pin svg { width: 100%; height: 100%; }
|
||||
.f6-pulse { position: absolute; left: 244px; top: 360px; width: 200px; height: 200px; margin: -100px 0 0 -100px; border: 6px solid #F8635F; border-radius: 50%; opacity: 0; }
|
||||
.f6-note { left: 0; right: 0; top: 1430px; text-align: center; }
|
||||
.f6-note .f6-pill { background: #FDE68A; } .f6-note span { font-size: 62px; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="06-location" data-width="1080" data-height="1920" data-duration="7.013">
|
||||
<div id="scene6-bg" class="clip f6-bg" data-start="0" data-duration="7.013" data-track-index="0" style="background:#FFCBA4"></div>
|
||||
<div class="f6-orn" style="left:-80px;top:90px;width:280px;height:280px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f6-orn" style="left:890px;top:120px;width:150px;height:150px"><svg viewBox="0 0 100 100" style="transform:rotate(10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f6-orn" style="left:880px;top:1230px;width:220px;height:220px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f6-orn" style="left:-90px;top:1180px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(4deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f6-orn" style="left:40px;top:700px;width:100px;height:100px"><svg viewBox="0 0 100 100" style="transform:rotate(-12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f6-phone">
|
||||
<div class="f6-notch"></div>
|
||||
<div class="f6-pill f6-d f6-btn"><span class="f6-d">CHECK NOW</span></div>
|
||||
<div class="f6-map">
|
||||
<svg viewBox="0 0 488 780">
|
||||
<rect x="300" y="0" width="188" height="780" fill="#A8D8F0"/>
|
||||
<rect x="30" y="40" width="190" height="150" rx="26" fill="#8ADBB6" stroke="#2D2D2D" stroke-width="3"/>
|
||||
<rect x="30" y="520" width="230" height="200" rx="26" fill="#8ADBB6" stroke="#2D2D2D" stroke-width="3"/>
|
||||
<path d="M0 250 H488 M150 0 V780 M0 470 H300 M360 60 L120 780" fill="none" stroke="#FFFFFF" stroke-width="22" stroke-linecap="round"/>
|
||||
<path d="M0 250 H488 M150 0 V780 M0 470 H300 M360 60 L120 780" fill="none" stroke="#E8E0D4" stroke-width="3" stroke-dasharray="14 18"/>
|
||||
</svg>
|
||||
<div class="f6-pulse f6-pu1"></div><div class="f6-pulse f6-pu2"></div>
|
||||
<div class="f6-pin"><svg viewBox="0 0 100 125"><path d="M50 118 C14 74 12 54 12 42 a38 38 0 0 1 76 0 C88 54 86 74 50 118 Z" fill="#F8635F" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/><circle cx="50" cy="42" r="14" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="f6-z f6-note"><span class="f6-pill"><span class="f6-d">ONLY WHEN NEEDED</span></span></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 6.513;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f6-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f6-phone", { y: 160, opacity: 0, rotation: 4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.7, ease: "power3.out" }, 0.38);
|
||||
tl.fromTo(".f6-map", { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1, duration: 0.5, ease: "power3.out" }, 0.61);
|
||||
tl.set(".f6-pin", { opacity: 0 }, 0);
|
||||
tl.to(".f6-btn", { scale: 0.9, duration: 0.12, ease: "power2.in" }, 1.8).to(".f6-btn", { scale: 1, duration: 0.25, ease: "back.out(2.5)" }, ">");
|
||||
tl.fromTo(".f6-pin", { y: -520, opacity: 1, scaleY: 1.1 }, { y: 0, opacity: 1, scaleY: 1, duration: 0.7, ease: "bounce.out" }, 2.2);
|
||||
tl.fromTo(".f6-pu1", { scale: 0.3, opacity: 0.9 }, { scale: 2.6, opacity: 0, duration: 1.1, ease: "power2.out" }, 2.95);
|
||||
tl.fromTo(".f6-pu2", { scale: 0.3, opacity: 0.9 }, { scale: 2.6, opacity: 0, duration: 1.1, ease: "power2.out" }, 3.45);
|
||||
tl.fromTo(".f6-note", { y: 80, opacity: 0, scale: 0.8 }, { y: 0, opacity: 1, scale: 1, duration: 0.55, ease: "back.out(1.8)" }, 3.89);
|
||||
window.__timelines["06-location"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f7-bg { position: absolute; inset: 0; }
|
||||
.f7-orn { position: absolute; z-index: 1; }
|
||||
.f7-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f7-z { position: absolute; z-index: 2; }
|
||||
.f7-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f7-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f7-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f7-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f7-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f7-big { position: absolute; z-index: 2; left: 110px; width: 860px; height: 400px; display: flex; align-items: center; justify-content: center; gap: 30px; }
|
||||
.f7-big span { font-size: 138px; }
|
||||
.f7-big svg { width: 120px; height: 120px; flex: none; }
|
||||
.f7-heart { position: absolute; z-index: 3; left: 50%; top: 720px; margin-left: -110px; width: 220px; height: 220px; }
|
||||
.f7-heart svg { width: 100%; height: 100%; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="07-balance" data-width="1080" data-height="1920" data-duration="5.968">
|
||||
<div id="scene7-bg" class="clip f7-bg" data-start="0" data-duration="5.968" data-track-index="0" style="background:#F7C8D4"></div>
|
||||
<div class="f7-orn" style="left:-60px;top:100px;width:240px;height:240px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f7-orn" style="left:880px;top:120px;width:160px;height:160px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f7-orn" style="left:800px;top:1230px;width:280px;height:280px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f7-orn" style="left:-80px;top:1210px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f7-orn" style="left:60px;top:760px;width:110px;height:110px"><svg viewBox="0 0 100 100" style="transform:rotate(-10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f7-big f7-card f7-c1" style="top:240px;background:#A8D8F0"><svg viewBox="0 0 100 100" style="transform:rotate(8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg><span class="f7-d">FREEDOM</span></div>
|
||||
<div class="f7-heart f7-h"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M50 86 C8 56 10 18 32 18 C42 18 48 25 50 32 C52 25 58 18 68 18 C90 18 92 56 50 86 Z" fill="#F8635F" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f7-big f7-card f7-c2" style="top:1000px;background:#A8E6CF"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg><span class="f7-d">CALM</span></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 5.468;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f7-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f7-c1", { x: -300, opacity: 0, rotation: -10 }, { x: 0, opacity: 1, rotation: -2, duration: 0.65, ease: "back.out(1.5)" }, 0.81);
|
||||
tl.fromTo(".f7-c2", { x: 300, opacity: 0, rotation: 10 }, { x: 0, opacity: 1, rotation: 2, duration: 0.65, ease: "back.out(1.5)" }, 2.63);
|
||||
tl.fromTo(".f7-h", { scale: 0, opacity: 0, rotation: -30 }, { scale: 1, opacity: 1, rotation: 0, duration: 0.7, ease: "back.out(2.2)" }, 3.49);
|
||||
tl.to(".f7-h", { scale: 1.18, duration: 0.3, ease: "sine.inOut" }, 4.29).to(".f7-h", { scale: 1, duration: 0.3, ease: "sine.inOut" }, ">");
|
||||
window.__timelines["07-balance"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f8-bg { position: absolute; inset: 0; }
|
||||
.f8-orn { position: absolute; z-index: 1; }
|
||||
.f8-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f8-z { position: absolute; z-index: 2; }
|
||||
.f8-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f8-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f8-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f8-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f8-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f8-big { position: absolute; z-index: 2; left: 110px; width: 860px; height: 400px; display: flex; align-items: center; justify-content: center; gap: 36px; }
|
||||
.f8-big span { font-size: 124px; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="08-platforms" data-width="1080" data-height="1920" data-duration="4.183">
|
||||
<div id="scene8-bg" class="clip f8-bg" data-start="0" data-duration="4.183" data-track-index="0" style="background:#FDE68A"></div>
|
||||
<div class="f8-orn" style="left:800px;top:90px;width:250px;height:250px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f8-orn" style="left:-60px;top:130px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f8-orn" style="left:-70px;top:1230px;width:290px;height:290px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f8-orn" style="left:880px;top:1300px;width:150px;height:150px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f8-orn" style="left:520px;top:1440px;width:100px;height:100px"><svg viewBox="0 0 100 100" style="transform:rotate(-8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f8-big f8-card f8-c1" style="top:290px;background:#A8E6CF"><svg viewBox="0 0 100 100" style="width:150px;height:150px;flex:none"><rect x="26" y="6" width="48" height="88" rx="12" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5"/><rect x="34" y="18" width="32" height="56" rx="4" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3"/><circle cx="50" cy="83" r="4" fill="#2D2D2D"/></svg><span class="f8-d">ANDROID</span></div>
|
||||
<div class="f8-big f8-card f8-c2" style="top:790px;background:#A8D8F0"><svg viewBox="0 0 100 100" style="width:150px;height:150px;flex:none"><rect x="26" y="6" width="48" height="88" rx="12" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5"/><rect x="34" y="18" width="32" height="56" rx="4" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3"/><circle cx="50" cy="83" r="4" fill="#2D2D2D"/></svg><span class="f8-d">iOS</span></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 3.783;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f8-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f8-c1", { y: 160, opacity: 0, rotation: -6, scale: 0.8 }, { y: 0, opacity: 1, rotation: -2, scale: 1, duration: 0.6, ease: "back.out(1.6)" }, 0.96);
|
||||
tl.fromTo(".f8-c2", { y: 160, opacity: 0, rotation: 6, scale: 0.8 }, { y: 0, opacity: 1, rotation: 2, scale: 1, duration: 0.6, ease: "back.out(1.6)" }, 1.92);
|
||||
window.__timelines["08-platforms"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<style>
|
||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
||||
.f9-bg { position: absolute; inset: 0; }
|
||||
.f9-orn { position: absolute; z-index: 1; }
|
||||
.f9-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.f9-z { position: absolute; z-index: 2; }
|
||||
.f9-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
||||
.f9-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
||||
.f9-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
||||
.f9-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
||||
.f9-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
||||
|
||||
.f9-eye { display: block; position: relative; width: 140px; height: 140px; border-radius: 50%; background: #FFFFFF; border: 7.5px solid #2D2D2D; box-sizing: border-box; text-shadow: none; }
|
||||
.f9-pup { position: absolute; left: 50%; top: 50%; width: 55px; height: 55px; margin: -27.5px 0 0 -27.5px; border-radius: 50%; background: #2D2D2D; }
|
||||
.f9-wmwrap { left: 0; right: 0; top: 400px; }
|
||||
.f9-tag { left: 50%; top: 840px; margin-left: -400px; width: 800px; height: 190px; display: flex; align-items: center; justify-content: center; background: #FFFFFF; }
|
||||
.f9-tag span { font-size: 64px; }
|
||||
.f9-chips { left: 0; right: 0; top: 1130px; display: flex; justify-content: center; gap: 40px; }
|
||||
.f9-chips .f9-pill { padding: 16px 40px; } .f9-chips span { font-size: 56px; }
|
||||
</style>
|
||||
<div id="root" data-composition-id="09-cta" data-width="1080" data-height="1920">
|
||||
<div id="scene9-bg" class="clip f9-bg" data-start="0" data-duration="5.772" data-track-index="0" style="background:#D4A5E8"></div>
|
||||
<div class="f9-orn" style="left:780px;top:70px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f9-orn" style="left:-80px;top:160px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f9-orn" style="left:850px;top:1250px;width:250px;height:250px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
||||
<div class="f9-orn" style="left:-90px;top:1220px;width:330px;height:330px"><svg viewBox="0 0 100 100" style="transform:rotate(3deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f9-orn" style="left:140px;top:1010px;width:130px;height:130px"><svg viewBox="0 0 100 100" style="transform:rotate(-10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
<div class="f9-orn" style="left:810px;top:1000px;width:120px;height:120px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
||||
|
||||
<div class="f9-z f9-wmwrap">
|
||||
<div class="f9-wm f9-d" style="display:flex;align-items:flex-end;justify-content:center;font-size:250px;color:#2D2D2D;text-shadow:9px 9px 0 #FFFFFF;line-height:1">
|
||||
<span>l</span>
|
||||
<span class="f9-eye" style="margin:0 7.5px 15px 12.5px"><i class="f9-pup"></i></span>
|
||||
<span class="f9-eye" style="margin:0 12.5px 15px 7.5px"><i class="f9-pup"></i></span>
|
||||
<span>katu</span>
|
||||
</div></div>
|
||||
<div class="f9-z f9-card f9-tag"><span class="f9-d">LOOK OUT FOR THEM</span></div>
|
||||
<div class="f9-z f9-chips"><span class="f9-pill f9-ch1" style="background:#A8E6CF"><span class="f9-d">ANDROID</span></span><span class="f9-pill f9-ch2" style="background:#A8D8F0"><span class="f9-d">iOS</span></span></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const DUR = 5.772;
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const orns = document.querySelectorAll(".f9-orn");
|
||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
||||
orns.forEach((el, i) => {
|
||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
||||
});
|
||||
|
||||
tl.fromTo(".f9-wm", { scale: 0.4, opacity: 0, y: 70 }, { scale: 1, opacity: 1, y: 0, duration: 0.75, ease: "back.out(1.6)" }, 0.12);
|
||||
|
||||
const pups = document.querySelectorAll(".f9-pup"), r = 25;
|
||||
tl.fromTo(pups, { x: 0, y: 0 }, { x: -r, y: -r * 0.4, duration: 0.35, ease: "power2.out" }, 0.7)
|
||||
.to(pups, { x: r, y: r * 0.3, duration: 0.5, ease: "power2.inOut" }, 1.2999999999999998)
|
||||
.to(pups, { x: 0, y: 0, duration: 0.35, ease: "power2.out" }, 2.05);
|
||||
tl.fromTo(".f9-tag", { y: 90, opacity: 0, rotation: -4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.6, ease: "back.out(1.6)" }, 1.04);
|
||||
tl.fromTo(".f9-ch1", { y: 70, opacity: 0, scale: 0.7 }, { y: 0, opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.8)" }, 2.27);
|
||||
tl.fromTo(".f9-ch2", { y: 70, opacity: 0, scale: 0.7 }, { y: 0, opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.8)" }, 2.45);
|
||||
tl.to(".f9-wm", { scale: 1.04, duration: 0.5, ease: "sine.inOut" }, 3.28).to(".f9-wm", { scale: 1, duration: 0.5, ease: "sine.inOut" }, ">");
|
||||
window.__timelines["09-cta"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
@@ -0,0 +1,274 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Daisy Days — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Daisy Days' design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — the sunny-garden pastel palette (cream + turquoise/pink/butter/mint/
|
||||
lavender/peach/sky + coral accent), charcoal 3px outlines, hard offset shadows (6/4px, no blur),
|
||||
the Fredoka + Quicksand pairing, generous radii, headline text-shadow on color, dot bullets, and
|
||||
the hand-drawn SVG ornament layer. Composition + frame scale rewritten. Motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
cream: "#F5F0E6"
|
||||
turquoise: "#7ECDC0"
|
||||
soft-pink: "#F7C8D4"
|
||||
butter: "#FDE68A"
|
||||
mint: "#A8E6CF"
|
||||
lavender: "#D4A5E8"
|
||||
peach: "#FFCBA4"
|
||||
sky: "#A8D8F0"
|
||||
coral: "#F8635F"
|
||||
text-dark: "#2D2D2D"
|
||||
text-muted: "#6B6B6B"
|
||||
white: "#FFFFFF"
|
||||
|
||||
borders: { primary: "3px solid text-dark", thin: "2px solid text-dark" }
|
||||
shadows: { default: "6px 6px 0 text-dark", small: "4px 4px 0 text-dark", text-headline: "3px 3px 0 text-dark", text-headline-soft: "3px 3px 0 rgba(0,0,0,0.2)" }
|
||||
|
||||
typography:
|
||||
# — reading ramp (Quicksand) —
|
||||
body: { fontFamily: "Quicksand", cqw: 0.95, weight: 500, lineHeight: 1.6 }
|
||||
body-strong:{ fontFamily: "Quicksand", cqw: 0.95, weight: 600, lineHeight: 1.5 }
|
||||
meta: { fontFamily: "Quicksand", cqw: 0.78, weight: 600, lineHeight: 1.45 }
|
||||
# — display ramp (Fredoka One / Fredoka 600 — single weight, never italic) —
|
||||
badge: { fontFamily: "Fredoka One", cqw: 0.9, tracking: "0.02em" }
|
||||
label-display:{ fontFamily: "Fredoka One", cqw: 1.3, lineHeight: 1.3, tracking: "0.02em" }
|
||||
subtitle:{ fontFamily: "Fredoka One", cqw: 1.8, lineHeight: 1.2, tracking: "0.02em" }
|
||||
quote: { fontFamily: "Fredoka One", cqw: 2.6, lineHeight: 1.35 }
|
||||
title: { fontFamily: "Fredoka One", cqw: 3.0, lineHeight: 1.15, tracking: "0.02em" }
|
||||
headline:{ fontFamily: "Fredoka One", cqw: 4.5, lineHeight: 1.1, tracking: "0.02em" }
|
||||
display: { fontFamily: "Fredoka One", cqw: 6.5, lineHeight: 1.1, tracking: "0.02em" }
|
||||
|
||||
spacing:
|
||||
pad-slide: "3cqw"
|
||||
radius: "20px"
|
||||
radius-lg: "28px"
|
||||
radius-pill: "50px"
|
||||
radius-round: "50%"
|
||||
|
||||
components:
|
||||
card:
|
||||
backgroundColor: "{colors.white}"
|
||||
border: "3px solid {colors.text-dark}"
|
||||
rounded: "{spacing.radius} (28px featured)"
|
||||
shadow: "6px 6px 0 {colors.text-dark}"
|
||||
description: "The universal container; white-on-pastel is standard."
|
||||
framed-header:
|
||||
backgroundColor: "pastel cap + {colors.white} body"
|
||||
border: "3px solid {colors.text-dark} (one continuous)"
|
||||
rounded: "{spacing.radius-lg}"
|
||||
shadow: "6px 6px 0 {colors.text-dark}"
|
||||
description: "Pastel header strip flush above a white body — one unit, one shadow."
|
||||
badge-pill:
|
||||
backgroundColor: "{colors.butter}"
|
||||
border: "3px solid {colors.text-dark}"
|
||||
rounded: "{spacing.radius-pill}"
|
||||
typography: "{typography.badge}"
|
||||
shadow: "4px 4px 0 {colors.text-dark}"
|
||||
description: "Section tag. white-space:nowrap."
|
||||
circle-marker:
|
||||
backgroundColor: "any pastel"
|
||||
textColor: "{colors.white} (dark on butter)"
|
||||
border: "3px solid {colors.text-dark}"
|
||||
rounded: "50%"
|
||||
size: "bullet 20 / icon 44 / dot 48 / step 90"
|
||||
typography: "Fredoka numeral"
|
||||
description: "Steps carry a small shadow."
|
||||
bullet-dot:
|
||||
backgroundColor: "{colors.butter}"
|
||||
border: "2px solid {colors.text-dark}"
|
||||
rounded: "50%"
|
||||
size: "20px (::before, 4px from line top)"
|
||||
description: "Lists never use glyph bullets."
|
||||
ornament:
|
||||
type: "hand-drawn SVG (daisy, star, sun, cloud, rainbow)"
|
||||
stroke: "2.1px {colors.text-dark}"
|
||||
placement: "z-index:1 behind content (z-index:2), cropping past the frame edge"
|
||||
description: "3–7 per frame, clustered at corners/edges."
|
||||
quote-mark:
|
||||
typography: "oversized Fredoka quote glyph"
|
||||
color: "{colors.soft-pink} (charcoal stroke)"
|
||||
description: "Above a quote body."
|
||||
text-headline-shadow:
|
||||
shadow: "3px 3px 0 {colors.text-dark} (soft 20% variant on pink/mint)"
|
||||
appliesTo: "Fredoka headlines on saturated surfaces (cream headlines sit flat)"
|
||||
description: "Makes the headline read 'outlined' like the shapes."
|
||||
---
|
||||
|
||||
# Daisy Days — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Daisy Days at frame scale is a **cheerful, childlike system** — picture-book illustration meets
|
||||
sticker-sheet kawaii. Every shape carries a **3px charcoal outline**, every elevated element a
|
||||
**solid hard offset shadow** (no blur), every surface a sunny-garden pastel. The voice is one
|
||||
pairing: **Fredoka One** (chunky rounded, single weight) for every headline, **Quicksand** for
|
||||
every body and meta line. The signature is the **hand-drawn SVG ornament layer** — daisies, stars,
|
||||
suns, clouds, rainbows clustering at corners and cropping past the edge.
|
||||
|
||||
The palette is **multi-pastel with one warm pop**: cream canvas, seven pastel surfaces, and
|
||||
`{colors.coral}` reserved for small high-attention markers only. Headlines on a saturated surface
|
||||
get a 3px charcoal text-shadow (so they read "outlined" like the shapes) and switch to white;
|
||||
headlines on cream sit flat in charcoal. Depth is 2D and graphic — thick outline + hard offset =
|
||||
sticker-on-paper.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Cream default + rotating pastel surfaces**; `{colors.coral}` is a marker accent, never a surface.
|
||||
- **Fredoka One** headlines + **Quicksand 500/600** body — strict by role, never crossed.
|
||||
- **3px charcoal outline + hard offset shadow** (6/4px, zero blur) on every elevated shape.
|
||||
- **Generous radii** — 20px cards, 28px featured, pill badges, full-circle markers; no square corners.
|
||||
- **Headline text-shadow on saturated surfaces** (white text); flat charcoal on cream.
|
||||
- **Dot bullets** (outlined butter discs, never glyphs) + a **3–7 ornament wreath** per frame.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one Fredoka headline or content card dominates at 3–6× its neighbor.
|
||||
- **Silence** — **one content container per frame** surrounded by an ornament wreath; the **info-card grid is the one dense exception**. Empty corners read as broken.
|
||||
- **Restraint** — cream or **one** pastel surface; coral is a small-marker accent (never a surface); charcoal borders + hard offset shadows only; no ninth color.
|
||||
- **Reference** — aim at a **children's picture-book / sticker-sheet kawaii zine**; failure looks like a **flat, square-cornered, blurred-shadow corporate slide**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad-slide` ~3cqw; ornaments deliberately bleed past the edge.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Borders stay 3px/2px; radii stay
|
||||
20/28/50px; shadow offsets scale in `cqw` so the sticker offset holds proportionally.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. Default ground `{colors.cream}`; rotate saturated pastels
|
||||
(turquoise / soft-pink / butter / mint / lavender / peach / sky) for tonal mood. **Cards are white**
|
||||
on any surface. **Borders + shadows are always `{colors.text-dark}` charcoal** — never colored,
|
||||
never blurred, never `rgba` (save the soft text-shadow variant). `{colors.coral}` is the lone
|
||||
high-saturation accent — small markers (step circles, dots, headers) **only**, never a surface.
|
||||
Body is charcoal/muted; pastels carry **no semantic meaning**. No ninth color.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading ramp** (Quicksand 500 body 0.95cqw, 600 emphasis, meta) carries copy; the
|
||||
**display ramp** (Fredoka One `label-display` 1.3cqw → `display` 6.5cqw, single weight) carries
|
||||
every headline, title, quote, and marker numeral.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; meta is chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `display`; 4–6 → `headline`; 7+ → `title`.
|
||||
- **Fredoka One for all display, Quicksand for all body — never crossed.** Fredoka is single-weight (no italic, no underline, no alt weights); Quicksand stays 500/600/700. Fredoka tracking 0.02em; Quicksand body never uppercase.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
2D graphic depth — hard offset shadow, solid charcoal, zero blur, bottom-right:
|
||||
|
||||
- **`shadows.default` 6px** cards, frames, badges, chart containers.
|
||||
- **`shadows.small` 4px** small cards, step circles, avatars.
|
||||
- **Text-headline shadow** — 3px charcoal on Fredoka headlines over saturated surfaces (20% soft variant on pink/mint); cream headlines flat.
|
||||
- **Outline + offset** together are the sticker-on-paper signature.
|
||||
|
||||
**Ceiling:** no blurred shadow, no `rgba` (except the soft text-shadow), no gradient, no glow; an element either casts a hard charcoal offset or none.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **20px** cards, **28px** featured, **50px** pill (badges, counter), **50%** all circles, **4px** legend swatch. Zero square corners — every region is rounded.
|
||||
|
||||
## Components
|
||||
|
||||
- **card / framed-header** — the white-fill bordered containers (one shadow). **badge-pill** — butter section tag.
|
||||
- **circle-marker** family (bullet 20 / icon 44 / dot 48 / step 90) + **bullet-dot** — outlined pastel discs, Fredoka numerals.
|
||||
- **ornament** — the hand-drawn SVG sticker layer (3–7 per frame). **quote-mark** — soft-pink Fredoka anchor. **text-headline-shadow** — the on-color headline treatment.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> One content container per frame + a 3–7 ornament wreath; empty corners read as broken.
|
||||
|
||||
### 1 · Cover (identity · move: ornament wreath · saturated · centered)
|
||||
|
||||
**Ground** a saturated pastel (e.g. `{colors.turquoise}`). **Composes** badge-pill, display, body sub, 3–7 ornaments, counter. **Focal** a 1–2 line Fredoka `display` in **white with the 3px charcoal text-shadow**, centered, under a butter badge-pill. **Chrome** counter pill. **Accent** the ornament wreath (daisies + stars at corners, cropping past edges). **Silence** content centered; ornaments fill the edges. **Fixed** text-shadow on color, 3px outlines, charcoal shadows. **Free** surface color, ornament mix/positions, copy. **Density** full-but-not-crowded.
|
||||
|
||||
### 2 · Info Cards (catalog · move: 3-up white cards · cream · the dense frame)
|
||||
|
||||
**Ground** `{colors.cream}`, `pad-slide`. **Composes** headline (flat charcoal), 3× card (circle-icon + Fredoka title + Quicksand body), a couple ornaments. **Focal** three white 3px-bordered cards with 6px shadows. **Chrome** flat headline. **Accent** the pastel circle-icons (rotate turquoise/coral/lavender). **Silence** tight — the density exception (fewer ornaments here). **Fixed** white cards, 3px + 6px, 20px radius. **Free** card content, icon hues. **Density** dense-exception.
|
||||
|
||||
### 3 · Process Steps (sequence · move: rotating circle markers · peach · centered)
|
||||
|
||||
**Ground** `{colors.peach}` (or another pastel). **Composes** headline (white + text-shadow), 3–4 step circles + `→` arrows, ornaments. **Focal** a row of 90px outlined step circles, fills **rotating coral → mint → sky → lavender**, Fredoka white numerals, linked by Fredoka arrows. **Chrome** white headline with text-shadow. **Accent** the rotating circle fills. **Silence** moderate. **Fixed** 3px circles + small shadow, rotating fills, arrow glyphs. **Free** step count, labels. **Density** standard.
|
||||
|
||||
### 4 · Quote (quote · move: quote-mark anchor · soft-pink · centered)
|
||||
|
||||
**Ground** `{colors.soft-pink}`. **Composes** a white quote card (28px radius, 6px shadow), quote-mark, Fredoka quote, Quicksand attribution, ornaments. **Focal** a Fredoka `quote` in charcoal inside the white card, under an oversized soft-pink quote-mark. **Chrome** Quicksand 700 attribution. **Accent** the quote-mark + a star or two. **Silence** card centered, ornaments at corners. **Fixed** quote-mark anchor, white card. **Free** quote, attribution. **Density** moderate.
|
||||
|
||||
### 5 · Framed Section (feature · move: cap+body card · cream)
|
||||
|
||||
**Ground** `{colors.cream}`. **Composes** framed-header (pastel cap + white body), bullet-dot list, ornaments. **Focal** a framed-header — a pastel cap strip (Fredoka title, optional text-shadow) above a white body with a butter-dot bullet list. **Accent** the cap color + bullet dots. **Silence** moderate. **Fixed** one continuous 3px border + one shadow, butter dot bullets. **Free** cap color, list. **Density** standard.
|
||||
|
||||
### 6 · Closing (closer · move: ornament wreath · saturated · centered)
|
||||
|
||||
**Ground** a saturated pastel (e.g. `{colors.lavender}`). **Composes** badge-pill, display (white + text-shadow), 3–7 ornaments. **Focal** a Fredoka `display` sign-off in white with the charcoal text-shadow, centered. **Accent** the ornament wreath. **Silence** content centered. **Fixed** text-shadow on color, ornaments fill corners. **Free** sign-off, surface, ornaments. **Density** full.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Pair **Fredoka One headlines + Quicksand body** strictly by role.
|
||||
- Outline every shape **3px charcoal + a hard offset shadow** (6/4px, no blur); white card fills on any surface.
|
||||
- Give Fredoka headlines on **saturated surfaces a 3px charcoal text-shadow + white text**; flat charcoal on cream.
|
||||
- Use **outlined butter-disc bullets** (never glyphs); cluster **3–7 hand-drawn ornaments** per frame (corners, cropping past edges).
|
||||
- Keep **one content container per frame**; rotate marker colors (coral → mint → sky → lavender → butter); reserve coral for small markers.
|
||||
- Center most frames; lean cream for content-dense frames, saturated for cover/closer/quote.
|
||||
|
||||
### Don't
|
||||
|
||||
- No square corners; no blurred or `rgba` shadows (save the soft text-shadow).
|
||||
- No colored borders (charcoal only); no coral surface; no ninth color.
|
||||
- No third font; no Quicksand headline or Fredoka body; no italic/underline Fredoka; no uppercase Quicksand body.
|
||||
- No glyph bullets; no empty corners (ornaments fill them); no two competing content panels.
|
||||
- Don't blow a headline edge-to-edge — fit to measure.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| -------------- | -------------------------- | -------------------------------- | ---------- |
|
||||
| Cover | centered, corner ornaments | centered, taller, more ornaments | centered |
|
||||
| Info Cards | 3 across | stacked | 2+1 |
|
||||
| Process Steps | horizontal + arrows | vertical, arrows rotate down | 2×2 |
|
||||
| Quote | centered card | centered, taller | centered |
|
||||
| Framed Section | cap + body | cap + body taller | cap + body |
|
||||
| Closing | centered, wreath | centered, wreath | centered |
|
||||
|
||||
`pad-slide` holds on the short edge; re-step display above the 1.4cqw floor. On tighter ratios keep
|
||||
the ornament count toward the upper end (5–7) so corners never read empty.
|
||||
|
||||
## Approved Real Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Ornaments and pastels are content-agnostic; counters/badges carry per-deck text.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, dates, or counts at frame scale. Render slots as `— figure —`, `{metric}`,
|
||||
`N`. Step numbers and any chart values carry placeholders until the script supplies them; the slide
|
||||
counter is decorative chrome.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one Fredoka headline or content card dominates per frame.
|
||||
- **Silence** — one container per frame surrounded by an ornament wreath; only the info-card grid runs dense.
|
||||
- **Color** — cream or one pastel surface; coral markers only; charcoal borders/shadows; no ninth hue.
|
||||
- **Type** — Fredoka headlines (text-shadow + white on saturated, flat charcoal on cream), Quicksand body; ≥1.4cqw floor.
|
||||
- **Depth** — 3px outline + hard offset (no blur, no rgba save soft text-shadow); rounded corners only.
|
||||
- **Ornaments** — 3–7 per frame, cropping past edges; no empty corners. **Bullets** — outlined discs, never glyphs.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source uses scroll-snap nav, no transition spec.
|
||||
- **Fonts:** the source names _Fredoka One_; Google now serves the **Fredoka** variable family — request `Fredoka:wght@500;600;700` and set display weight 600 (visually equal to Fredoka One), with `Fredoka One` kept first in the stack for environments that still serve it. Quicksand loads normally. CJK: ZCOOL XiaoWei (display) / Yozai (body).
|
||||
- **9:16 / 1:1 are guidance**; keep ornament count high so corners stay filled per ratio.
|
||||
- Ornaments (daisy/star/sun/cloud/rainbow), markers, and framed headers are CSS/SVG-only; recoloring SVG ornaments requires editing their stroke values.
|
||||
- **Contrast:** keep `{colors.text-muted}` off pastel surfaces (cream/white cards only); small text on saturated grounds should be charcoal or white.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
|
||||
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
|
||||
"paths": {
|
||||
"blocks": "compositions",
|
||||
"components": "compositions/components",
|
||||
"assets": "assets"
|
||||
},
|
||||
"media": {
|
||||
"autoProxy": true
|
||||
},
|
||||
"authoringSkill": "product-launch-video"
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1080, height=1920" />
|
||||
<script src="assets/vendor/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1080px;
|
||||
height: 1920px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1080px;
|
||||
height: 1920px;
|
||||
overflow: hidden;
|
||||
background: #F5F0E6;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="54.733"
|
||||
data-width="1080"
|
||||
data-height="1920"
|
||||
>
|
||||
<div
|
||||
id="el-01-hook"
|
||||
class="scene"
|
||||
data-composition-id="01-hook"
|
||||
data-composition-src="compositions/frames/01-hook.html"
|
||||
data-start="0"
|
||||
data-duration="6.14"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
<audio
|
||||
id="el-01-hook-voice"
|
||||
src="assets/voice/01.wav"
|
||||
data-start="0"
|
||||
data-duration="5.64"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-02-worry"
|
||||
class="scene"
|
||||
data-composition-id="02-worry"
|
||||
data-composition-src="compositions/frames/02-worry.html"
|
||||
data-start="5.64"
|
||||
data-duration="7.511"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
<audio
|
||||
id="el-02-worry-voice"
|
||||
src="assets/voice/02.wav"
|
||||
data-start="5.64"
|
||||
data-duration="7.111"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-03-meet"
|
||||
class="scene"
|
||||
data-composition-id="03-meet"
|
||||
data-composition-src="compositions/frames/03-meet.html"
|
||||
data-start="12.751"
|
||||
data-duration="8.644"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
<audio
|
||||
id="el-03-meet-voice"
|
||||
src="assets/voice/03.wav"
|
||||
data-start="12.751"
|
||||
data-duration="8.144"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-04-limits"
|
||||
class="scene"
|
||||
data-composition-id="04-limits"
|
||||
data-composition-src="compositions/frames/04-limits.html"
|
||||
data-start="20.895"
|
||||
data-duration="7.099"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
<audio
|
||||
id="el-04-limits-voice"
|
||||
src="assets/voice/04.wav"
|
||||
data-start="20.895"
|
||||
data-duration="6.599"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-05-apps"
|
||||
class="scene"
|
||||
data-composition-id="05-apps"
|
||||
data-composition-src="compositions/frames/05-apps.html"
|
||||
data-start="27.494"
|
||||
data-duration="6.203"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
<audio
|
||||
id="el-05-apps-voice"
|
||||
src="assets/voice/05.wav"
|
||||
data-start="27.494"
|
||||
data-duration="5.703"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-06-location"
|
||||
class="scene"
|
||||
data-composition-id="06-location"
|
||||
data-composition-src="compositions/frames/06-location.html"
|
||||
data-start="33.197"
|
||||
data-duration="7.013"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
<audio
|
||||
id="el-06-location-voice"
|
||||
src="assets/voice/06.wav"
|
||||
data-start="33.197"
|
||||
data-duration="6.513"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-07-balance"
|
||||
class="scene"
|
||||
data-composition-id="07-balance"
|
||||
data-composition-src="compositions/frames/07-balance.html"
|
||||
data-start="39.71"
|
||||
data-duration="5.968"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
<audio
|
||||
id="el-07-balance-voice"
|
||||
src="assets/voice/07.wav"
|
||||
data-start="39.71"
|
||||
data-duration="5.468"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-08-platforms"
|
||||
class="scene"
|
||||
data-composition-id="08-platforms"
|
||||
data-composition-src="compositions/frames/08-platforms.html"
|
||||
data-start="45.178"
|
||||
data-duration="4.183"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
<audio
|
||||
id="el-08-platforms-voice"
|
||||
src="assets/voice/08.wav"
|
||||
data-start="45.178"
|
||||
data-duration="3.783"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<div
|
||||
id="el-09-cta"
|
||||
class="scene"
|
||||
data-composition-id="09-cta"
|
||||
data-composition-src="compositions/frames/09-cta.html"
|
||||
data-start="48.961"
|
||||
data-duration="5.772"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
<audio
|
||||
id="el-09-cta-voice"
|
||||
src="assets/voice/09.wav"
|
||||
data-start="48.961"
|
||||
data-duration="5.772"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<!-- captions -->
|
||||
<div
|
||||
id="el-captions"
|
||||
data-track-kind="captions"
|
||||
class="scene"
|
||||
data-composition-id="captions"
|
||||
data-composition-src="compositions/captions.html"
|
||||
data-start="0"
|
||||
data-duration="54.733"
|
||||
data-track-index="2"
|
||||
></div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
// ── frame transitions (injected by transitions.mjs) ──
|
||||
(function () { var tl = window.__timelines["main"];
|
||||
tl.to("#el-01-hook", { opacity: 0, duration: 0.5, ease: "power2.inOut" }, 5.64);
|
||||
tl.fromTo("#el-02-worry", { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, 5.64);
|
||||
tl.to("#el-02-worry", { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, 12.751);
|
||||
tl.fromTo("#el-03-meet", { scale: 0.5, opacity: 0, filter: "blur(8px)" }, { scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, 12.751);
|
||||
tl.to("#el-03-meet", { x: -1080, duration: 0.5, ease: "power3.inOut" }, 20.895);
|
||||
tl.fromTo("#el-04-limits", { x: 1080, opacity: 1 }, { x: 0, opacity: 1, duration: 0.5, ease: "power3.inOut" }, 20.895);
|
||||
tl.to("#el-04-limits", { x: -1080, duration: 0.5, ease: "power3.inOut" }, 27.494);
|
||||
tl.fromTo("#el-05-apps", { x: 1080, opacity: 1 }, { x: 0, opacity: 1, duration: 0.5, ease: "power3.inOut" }, 27.494);
|
||||
tl.to("#el-05-apps", { x: -1080, duration: 0.5, ease: "power3.inOut" }, 33.197);
|
||||
tl.fromTo("#el-06-location", { x: 1080, opacity: 1 }, { x: 0, opacity: 1, duration: 0.5, ease: "power3.inOut" }, 33.197);
|
||||
tl.to("#el-06-location", { opacity: 0, duration: 0.5, ease: "power2.inOut" }, 39.71);
|
||||
tl.fromTo("#el-07-balance", { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, 39.71);
|
||||
tl.to("#el-07-balance", { opacity: 0, duration: 0.5, ease: "power2.inOut" }, 45.178);
|
||||
tl.fromTo("#el-08-platforms", { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, 45.178);
|
||||
tl.to("#el-08-platforms", { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, 48.961);
|
||||
tl.fromTo("#el-09-cta", { scale: 0.5, opacity: 0, filter: "blur(8px)" }, { scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, 48.961);
|
||||
tl.to({}, { duration: 54.733 }, 0); // full-span anchor — main.duration() == composition total (Studio master duration)
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "lookatu-promo",
|
||||
"name": "lookatu-promo",
|
||||
"createdAt": "2026-10-14T22:04:45.006Z"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "lookatu-promo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npx --yes hyperframes@0.8.158 preview",
|
||||
"check": "npx --yes hyperframes@0.8.158 check",
|
||||
"render": "npx --yes hyperframes@0.8.158 render",
|
||||
"publish": "npx --yes hyperframes@0.8.158 publish"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user