Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7b0e631b9 | ||
|
|
0b42fdeae1 |
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
name: ad-creative-video
|
||||||
|
description: This skill should be used when the user asks to "make a video ad", "create an animated ad", "build a performance/UGC-style ad", "batch-produce ad creative variations", "generate ad variants for A/B testing", "swap headline/offer/CTA across many ad versions", or "export one ad in multiple aspect ratios for Meta/TikTok/Reels". Covers ad hook structures, hook→CTA message-match, data-driven variant generation (1 template × CSV = N ads), multi-aspect export, and platform specs.
|
||||||
|
version: 0.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ad Creative Video
|
||||||
|
|
||||||
|
Build ONE motion-graphics ad template, then mass-produce variants from a data table — different hooks, offers, CTAs, and products — so a performance marketer can A/B test dozens of versions across placements and aspect ratios. The craft is making the template fully data-driven and message-matched, so 40 variants stay on-brand and only the tested variable changes.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Performance/UGC-style video ads where the first 1.5–3s must stop the scroll.
|
||||||
|
- Batch variant generation: one template rendered against many rows (headline/offer/CTA/product swaps) for A/B testing.
|
||||||
|
- Multi-aspect export of the same ad for Meta Feed, Reels/Stories, TikTok, YouTube.
|
||||||
|
|
||||||
|
Not for sale/discount countdown promos, and not for testimonial/review-driven ads — those are different structures. Stay on the batch-variation, test-everything angle.
|
||||||
|
|
||||||
|
## The two rules that make variants worth running
|
||||||
|
|
||||||
|
1. **Isolate one variable per variant.** A test only teaches something if exactly one thing changes. Hold layout, motion, colors, and timing constant; swap *only* the field under test (hook, or offer, or CTA). Mixing two changes makes the winner uninterpretable.
|
||||||
|
2. **Message-match the hook to the CTA.** The promise made in the first 3 seconds must be the promise the button pays off. A "stop wasting 2 hours a day" hook ends on "Save 2 hours — try free," not a generic "Shop now." The hook and CTA are a matched pair in every row of the data table.
|
||||||
|
|
||||||
|
## Ad anatomy (15–30s)
|
||||||
|
|
||||||
|
| Beat | Job | Budget |
|
||||||
|
|---|---|---|
|
||||||
|
| Hook (0–3s) | Stop the scroll; state the problem or pattern-interrupt | 0–3s |
|
||||||
|
| Context | Make the viewer feel the problem / agitate | 3–8s |
|
||||||
|
| Payoff | Show the product as the solution, one clear benefit | 8–18s |
|
||||||
|
| Proof | One concrete number, demo, or result | 18–25s |
|
||||||
|
| CTA (last 3s) | Single action, message-matched to the hook | hold ≥2s |
|
||||||
|
|
||||||
|
Land the hook's emotional trigger before the 2-second mark — judgment forms in ~1.7s and scroll speed keeps rising. Test the hook FIRST; it moves CPA more than any other element.
|
||||||
|
|
||||||
|
## Hook taxonomy (the field you vary most)
|
||||||
|
|
||||||
|
| Hook type | Template | When it wins |
|
||||||
|
|---|---|---|
|
||||||
|
| Problem-first | "I didn't realize how much X was costing me until…" | pain is felt but unnamed |
|
||||||
|
| Pattern interrupt | "This is going to sound controversial, but…" | crowded feed, generic category |
|
||||||
|
| Curiosity gap | "Nobody talks about the one thing that…" | educated, skeptical audience |
|
||||||
|
| Immediate benefit | "Cut your X in half in 7 days" | clear, quantifiable outcome |
|
||||||
|
| Direct callout | "If you do X every morning, stop." | sharp audience segment |
|
||||||
|
|
||||||
|
Keep a column of 5–10 hook strings and let the batch render produce one ad per hook against the same body — that is the cleanest hook test possible.
|
||||||
|
|
||||||
|
## Data-driven template (everything is a prop)
|
||||||
|
|
||||||
|
Hardcode nothing the marketer might A/B test. The composition reads a single `variant` object; the renderer never edits the component. Make every animated value a pure function of `useCurrentFrame()` so each frame renders deterministically (no CSS transitions, no library timers — they desync the render).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {useCurrentFrame, interpolate, AbsoluteFill, useVideoConfig} from "remotion";
|
||||||
|
|
||||||
|
type Variant = {hook: string; benefit: string; proof: string; cta: string; bg: string; accent: string};
|
||||||
|
|
||||||
|
export const AdTemplate: React.FC<{v: Variant}> = ({v}) => {
|
||||||
|
const frame = useCurrentFrame(); const {fps} = useVideoConfig();
|
||||||
|
const hookIn = interpolate(frame, [0, 8], [40, 0], {extrapolateRight: "clamp"});
|
||||||
|
const ctaIn = interpolate(frame, [fps * 22, fps * 23], [0, 1], {extrapolateLeft: "clamp", extrapolateRight: "clamp"});
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{background: v.bg, fontFamily: "Inter, sans-serif"}}>
|
||||||
|
<h1 style={{transform: `translateY(${hookIn}px)`, opacity: interpolate(frame,[0,8],[0,1])}}>{v.hook}</h1>
|
||||||
|
{frame > fps * 8 && <p>{v.benefit}</p>}
|
||||||
|
{frame > fps * 18 && <strong>{v.proof}</strong>}
|
||||||
|
<button style={{opacity: ctaIn, background: v.accent}}>{v.cta}</button>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
See `references/batch-ad-pipeline.md` for the complete template, the CSV→props parser with validation, and the brand-lock theme object.
|
||||||
|
|
||||||
|
## 1 template × CSV = N variants
|
||||||
|
|
||||||
|
The payoff: author the ad once, then let a CSV drive the matrix. Each row is one fully-formed, message-matched variant. Render once per row.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# variants.csv: id,hook,benefit,proof,cta,bg,accent → one MP4 per row
|
||||||
|
node csv-to-props.js variants.csv ./props # writes props/<id>.json per row
|
||||||
|
for f in props/*.json; do
|
||||||
|
id=$(basename "$f" .json)
|
||||||
|
npx remotion render AdTemplate "out/${id}.mp4" --props="$f"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
To build a clean test matrix, hold every column constant and vary one: 5 hooks × 1 body = 5 ads (hook test); then take the winning hook × 3 CTAs (CTA test). `references/batch-ad-pipeline.md` has a matrix generator that expands variable lists into the CSV so combinatorial tests stay one-variable-at-a-time.
|
||||||
|
|
||||||
|
## Multi-aspect export
|
||||||
|
|
||||||
|
Each placement wants a native aspect. Compose against a center safe zone so one master reframes cleanly to all of them — don't letterbox, re-center.
|
||||||
|
|
||||||
|
| Aspect | Placements | Resolution | Keep-clear safe zone |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 9:16 | Reels, Stories, TikTok In-Feed | 1080×1920 | top ~14%, bottom ~20–35% (UI/caption), sides ~6% |
|
||||||
|
| 4:5 | Meta Feed (best feed real estate) | 1080×1350 | center 90%; CTA out of bottom 10% |
|
||||||
|
| 1:1 | Feed, broad reach | 1080×1080 | center square survives every crop |
|
||||||
|
| 16:9 | YouTube, in-stream | 1920×1080 | center 90% |
|
||||||
|
|
||||||
|
Design the hook text, product, and CTA inside the **1:1 center square** so they survive every crop. Drive aspect from a prop and render each per variant:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for ar in 9x16 4x5 1x1; do
|
||||||
|
npx remotion render AdTemplate "out/${id}-${ar}.mp4" --props="props/${id}.json" \
|
||||||
|
--props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep critical content out of the 9:16 bottom third — that is where the platform stacks captions, the CTA button, and the username. `references/platform-specs.md` has exact pixel safe zones and per-platform duration/format limits.
|
||||||
|
|
||||||
|
## Output checklist
|
||||||
|
|
||||||
|
- One variable per variant; layout/motion/timing identical across a test.
|
||||||
|
- Hook lands its trigger before 2s; hook and CTA are message-matched in every row.
|
||||||
|
- Every animated value is a pure function of `useCurrentFrame()` — no CSS/library timers.
|
||||||
|
- Dataset is a prop; nothing testable is hardcoded; one template renders the whole CSV.
|
||||||
|
- Each variant exported 9:16 + 4:5 + 1:1 (+16:9 if needed), key content in the center square.
|
||||||
|
- Brand stays locked across all variants via a single theme object.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
**Output contract:**
|
||||||
|
- A Remotion ad template registered as `<Composition>` (+ zod `schema` + `defaultProps`), every animated value frame-driven (no CSS transitions / library timers / `Date.now()` / `Math.random()`).
|
||||||
|
- Deliverable = the rendered `out/*.mp4` per variant per aspect (plus the project + CSV, so the marketer re-renders on new rows).
|
||||||
|
|
||||||
|
**Verify loop — render stills → inspect → encode.** Cheap PNGs first, full encode only once they're clean. Render with the **shipped** props (the real row), not just `defaultProps`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frame-exact stills across the hook→CTA arc, with a real variant's props
|
||||||
|
npx remotion still AdTemplate out/f-hook.png --frame=12 --props=props/v1.json # hook readable < 2s
|
||||||
|
npx remotion still AdTemplate out/f-mid.png --frame=300 --props=props/v1.json # benefit/proof
|
||||||
|
npx remotion still AdTemplate out/f-cta.png --frame=689 --props=props/v1.json # CTA, message-matched
|
||||||
|
# inspect each: fidelity (hook / offer / proof / CTA text exact, brand bg+accent right)
|
||||||
|
# AND artifacts (text overflow, off-canvas, CTA inside the 9:16 bottom third, missing font, wrong row binding)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-aspect / batch — verify one variant in EACH aspect before batch-rendering the matrix.** A layout bug repeats across every row × aspect; catch it once.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for ar in 9x16 4x5 1x1; do # one representative variant, every target aspect
|
||||||
|
npx remotion still AdTemplate "out/v1-${ar}.png" --frame=300 \
|
||||||
|
--props=props/v1.json --props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done
|
||||||
|
# stills clean in all aspects? → then batch-render every row × aspect:
|
||||||
|
for f in props/*.json; do id=$(basename "$f" .json); for ar in 9x16 4x5 1x1; do
|
||||||
|
npx remotion render AdTemplate "out/${id}-${ar}.mp4" --props="$f" --props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done; done
|
||||||
|
npx remotion render AdTemplate out/demo.gif --codec=gif --props=props/v1.json # README demo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before you finish:**
|
||||||
|
1. `npx remotion still` renders cleanly at hook / mid / CTA — no errors, no missing fonts/assets.
|
||||||
|
2. Hook/offer/proof/CTA text exact and brand colors right; nothing in the 9:16 bottom third or outside the center safe zone.
|
||||||
|
3. Frame-driven only — no CSS/library timers, `Date.now()`, or `Math.random()`.
|
||||||
|
4. Shipped row's props render correctly (not just `defaultProps`); one variable per variant holds.
|
||||||
|
5. One variant verified in 9:16 + 4:5 + 1:1 before the batch; MP4s play; (optional) GIF for the README.
|
||||||
|
|
||||||
|
## Reference files
|
||||||
|
|
||||||
|
- `references/batch-ad-pipeline.md` — the full runnable Remotion ad template, the CSV→typed-props parser with validation, a one-variable test-matrix generator, the brand-lock theme object, and the complete batch + multi-aspect render script.
|
||||||
|
- `references/platform-specs.md` — exact 2025/2026 pixel safe zones, durations, aspect ratios, and format limits for Meta Feed/Reels/Stories, TikTok In-Feed, and YouTube, plus a hook→CTA message-match worksheet.
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# Batch Ad Pipeline — full template, parser, matrix, render
|
||||||
|
|
||||||
|
This is the complete, runnable implementation behind the SKILL.md overview: a data-driven Remotion ad template, a CSV→props parser with validation, a one-variable test-matrix generator, a brand-lock theme, and the batch + multi-aspect render scripts. Hardcode nothing a marketer might want to A/B test.
|
||||||
|
|
||||||
|
## 1. The brand-lock theme
|
||||||
|
|
||||||
|
Keep every brand-controlled value in one object so 40 variants stay consistent and only the *tested* fields change per row. The variant data supplies copy and per-campaign accents; the theme supplies the locked look.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// theme.ts
|
||||||
|
export const theme = {
|
||||||
|
fontFamily: "Inter, system-ui, sans-serif",
|
||||||
|
ink: "#0B0B0F",
|
||||||
|
paper: "#FFFFFF",
|
||||||
|
radius: 20,
|
||||||
|
hookSize: 76, // px, on the 1080-wide master
|
||||||
|
bodySize: 44,
|
||||||
|
proofSize: 52,
|
||||||
|
ctaSize: 40,
|
||||||
|
pad: 80, // outer padding; also the side safe margin
|
||||||
|
durationInSeconds: 25,
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Aspect config
|
||||||
|
|
||||||
|
Drive layout from a single aspect prop so one composition reframes to every placement. Dimensions and the keep-clear bottom band differ per aspect.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// aspects.ts
|
||||||
|
export type Aspect = "9x16" | "4x5" | "1x1" | "16x9";
|
||||||
|
export const ASPECTS: Record<Aspect, {w: number; h: number; bottomSafe: number}> = {
|
||||||
|
"9x16": {w: 1080, h: 1920, bottomSafe: 540}, // captions + CTA + username stack here
|
||||||
|
"4x5": {w: 1080, h: 1350, bottomSafe: 135},
|
||||||
|
"1x1": {w: 1080, h: 1080, bottomSafe: 110},
|
||||||
|
"16x9": {w: 1920, h: 1080, bottomSafe: 110},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. The data-driven template
|
||||||
|
|
||||||
|
Every animated value is a pure function of `useCurrentFrame()` — no CSS transitions, no GSAP/library timers (they desync deterministic rendering and flicker in the export). The component reads one `variant` plus an `aspect`; the renderer never edits this file.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// AdTemplate.tsx
|
||||||
|
import {
|
||||||
|
AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring, Sequence,
|
||||||
|
} from "remotion";
|
||||||
|
import {theme} from "./theme";
|
||||||
|
import {ASPECTS, Aspect} from "./aspects";
|
||||||
|
|
||||||
|
export type Variant = {
|
||||||
|
id: string;
|
||||||
|
hook: string; // 0–3s scroll-stopper
|
||||||
|
benefit: string; // the single payoff claim
|
||||||
|
proof: string; // one concrete number / result
|
||||||
|
cta: string; // message-matched to the hook
|
||||||
|
accent: string; // per-campaign accent color
|
||||||
|
bg: string; // background color
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdTemplate: React.FC<{v: Variant; aspect: Aspect}> = ({v, aspect}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const {fps} = useVideoConfig();
|
||||||
|
const {bottomSafe} = ASPECTS[aspect];
|
||||||
|
|
||||||
|
// Hook rises + fades in over the first 8 frames, holds, then eases up and out at 8s.
|
||||||
|
const hookY = interpolate(frame, [0, 8], [48, 0], {extrapolateRight: "clamp"});
|
||||||
|
const hookOpacity =
|
||||||
|
interpolate(frame, [0, 8], [0, 1], {extrapolateRight: "clamp"}) *
|
||||||
|
interpolate(frame, [fps * 7, fps * 8], [1, 0], {extrapolateLeft: "clamp", extrapolateRight: "clamp"});
|
||||||
|
|
||||||
|
// CTA springs in for impact in the last 3s and holds.
|
||||||
|
const ctaProgress = spring({frame: frame - fps * 22, fps, config: {damping: 14, stiffness: 120}});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{backgroundColor: v.bg, fontFamily: theme.fontFamily, color: theme.ink}}>
|
||||||
|
<AbsoluteFill style={{padding: theme.pad, paddingBottom: bottomSafe, justifyContent: "center"}}>
|
||||||
|
|
||||||
|
{/* HOOK 0–8s */}
|
||||||
|
<Sequence durationInFrames={fps * 8}>
|
||||||
|
<h1 style={{fontSize: theme.hookSize, fontWeight: 800, lineHeight: 1.05,
|
||||||
|
transform: `translateY(${hookY}px)`, opacity: hookOpacity}}>
|
||||||
|
{v.hook}
|
||||||
|
</h1>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* BENEFIT 8–18s */}
|
||||||
|
<Sequence from={fps * 8} durationInFrames={fps * 10}>
|
||||||
|
<FadeUp delay={0}>
|
||||||
|
<p style={{fontSize: theme.bodySize, fontWeight: 600}}>{v.benefit}</p>
|
||||||
|
</FadeUp>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* PROOF 18–22s */}
|
||||||
|
<Sequence from={fps * 18} durationInFrames={fps * 4}>
|
||||||
|
<FadeUp delay={0}>
|
||||||
|
<strong style={{fontSize: theme.proofSize, color: v.accent,
|
||||||
|
fontVariantNumeric: "tabular-nums"}}>{v.proof}</strong>
|
||||||
|
</FadeUp>
|
||||||
|
</Sequence>
|
||||||
|
|
||||||
|
{/* CTA last 3s, message-matched to the hook */}
|
||||||
|
<Sequence from={fps * 22}>
|
||||||
|
<button style={{
|
||||||
|
alignSelf: "flex-start", marginTop: 40, padding: "22px 44px",
|
||||||
|
fontSize: theme.ctaSize, fontWeight: 700, border: "none",
|
||||||
|
borderRadius: theme.radius, color: theme.paper, backgroundColor: v.accent,
|
||||||
|
transform: `scale(${interpolate(ctaProgress, [0, 1], [0.85, 1])})`,
|
||||||
|
opacity: ctaProgress,
|
||||||
|
}}>
|
||||||
|
{v.cta}
|
||||||
|
</button>
|
||||||
|
</Sequence>
|
||||||
|
</AbsoluteFill>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FadeUp: React.FC<{children: React.ReactNode; delay: number}> = ({children, delay}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const o = interpolate(frame - delay, [0, 10], [0, 1], {extrapolateLeft: "clamp", extrapolateRight: "clamp"});
|
||||||
|
const y = interpolate(frame - delay, [0, 10], [24, 0], {extrapolateLeft: "clamp", extrapolateRight: "clamp"});
|
||||||
|
return <div style={{opacity: o, transform: `translateY(${y}px)`}}>{children}</div>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Register the composition (aspect + variant as props)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Root.tsx
|
||||||
|
import {Composition} from "remotion";
|
||||||
|
import {AdTemplate} from "./AdTemplate";
|
||||||
|
import {ASPECTS} from "./aspects";
|
||||||
|
import {theme} from "./theme";
|
||||||
|
|
||||||
|
const defaultVariant = {
|
||||||
|
id: "demo", hook: "Spending 2 hrs/day on reports?", benefit: "Auto-build them in one click.",
|
||||||
|
proof: "Teams save 11 hrs a week.", cta: "Save 2 hours — try free", accent: "#4F46E5", bg: "#FFFFFF",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RemotionRoot = () => {
|
||||||
|
const aspect = "9x16" as const; // overridden per-render via --props
|
||||||
|
const {w, h} = ASPECTS[aspect];
|
||||||
|
return (
|
||||||
|
<Composition
|
||||||
|
id="AdTemplate"
|
||||||
|
component={AdTemplate}
|
||||||
|
durationInFrames={theme.durationInSeconds * 30}
|
||||||
|
fps={30}
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
defaultProps={{v: defaultVariant, aspect}}
|
||||||
|
// Resolve real dimensions from the incoming aspect prop at render time:
|
||||||
|
calculateMetadata={({props}) => {
|
||||||
|
const a = ASPECTS[(props as any).aspect ?? "9x16"];
|
||||||
|
return {width: a.w, height: a.h};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`calculateMetadata` lets a single composition output any aspect from the `aspect` prop — no separate composition per ratio.
|
||||||
|
|
||||||
|
## 5. CSV → typed props, with validation
|
||||||
|
|
||||||
|
The dataset is the input. Parse, validate every row (a bad hex or empty CTA must fail loudly, not render a broken ad), and write one props file per variant.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// csv-to-props.js usage: node csv-to-props.js variants.csv ./props [aspect]
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const [, , csvPath, outDir, aspect = "9x16"] = process.argv;
|
||||||
|
const REQUIRED = ["id", "hook", "benefit", "proof", "cta", "accent", "bg"];
|
||||||
|
const isHex = (s) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(s);
|
||||||
|
|
||||||
|
const parseCSV = (text) => {
|
||||||
|
const [head, ...lines] = text.trim().split(/\r?\n/);
|
||||||
|
const cols = head.split(",").map((c) => c.trim());
|
||||||
|
return lines.filter(Boolean).map((line) => {
|
||||||
|
// simple split; wrap fields containing commas in double quotes in the CSV
|
||||||
|
const cells = line.match(/("([^"]|"")*"|[^,]*)/g).filter((_, i, a) => i < a.length - 1);
|
||||||
|
const row = {};
|
||||||
|
cols.forEach((c, i) => (row[c] = (cells[i] ?? "").replace(/^"|"$/g, "").replace(/""/g, '"').trim()));
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = parseCSV(fs.readFileSync(csvPath, "utf8"));
|
||||||
|
fs.mkdirSync(outDir, {recursive: true});
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
let errors = 0;
|
||||||
|
rows.forEach((row, n) => {
|
||||||
|
const where = `row ${n + 2} (${row.id || "no-id"})`;
|
||||||
|
REQUIRED.forEach((k) => { if (!row[k]) { console.error(`✗ ${where}: missing "${k}"`); errors++; } });
|
||||||
|
if (seen.has(row.id)) { console.error(`✗ ${where}: duplicate id`); errors++; }
|
||||||
|
seen.add(row.id);
|
||||||
|
if (row.hook && row.hook.length > 60) console.warn(`⚠ ${where}: hook >60 chars may clip in 9:16`);
|
||||||
|
["accent", "bg"].forEach((k) => { if (row[k] && !isHex(row[k])) { console.error(`✗ ${where}: "${k}" not a hex color`); errors++; } });
|
||||||
|
if (errors) return;
|
||||||
|
fs.writeFileSync(path.join(outDir, `${row.id}.json`), JSON.stringify({v: row, aspect}, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors) { console.error(`\n${errors} error(s) — no broken ads written.`); process.exit(1); }
|
||||||
|
console.log(`✓ ${rows.length} variant prop files written to ${outDir}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
Example `variants.csv` (one fully message-matched ad per row):
|
||||||
|
|
||||||
|
```csv
|
||||||
|
id,hook,benefit,proof,cta,accent,bg
|
||||||
|
v01_problem,Spending 2 hrs/day on reports?,Auto-build them in one click.,Teams save 11 hrs a week.,Save 2 hours — try free,#4F46E5,#FFFFFF
|
||||||
|
v02_interrupt,Your reporting tool is lying to you.,See the real numbers instantly.,Teams save 11 hrs a week.,See the real numbers — free,#DC2626,#0B0B0F
|
||||||
|
v03_curiosity,Nobody talks about this reporting trick.,One click builds the whole report.,Teams save 11 hrs a week.,Try the trick — free,#059669,#FFFFFF
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the CTA mirrors the hook in every row — that is the message-match discipline encoded into the data.
|
||||||
|
|
||||||
|
## 6. Test-matrix generator — one variable at a time
|
||||||
|
|
||||||
|
A test only teaches something if exactly one field changes. This expands a base variant plus a list of values for ONE field into a CSV, so a hook test or a CTA test stays clean and combinatorial mistakes are impossible.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// make-matrix.js usage: node make-matrix.js > variants.csv
|
||||||
|
const base = {benefit: "Auto-build them in one click.", proof: "Teams save 11 hrs a week.",
|
||||||
|
cta: "Save 2 hours — try free", accent: "#4F46E5", bg: "#FFFFFF"};
|
||||||
|
|
||||||
|
// Vary ONLY this field; everything else is held identical across the test.
|
||||||
|
const FIELD = "hook";
|
||||||
|
const values = [
|
||||||
|
"Spending 2 hrs/day on reports?",
|
||||||
|
"Your reporting tool is lying to you.",
|
||||||
|
"Nobody talks about this reporting trick.",
|
||||||
|
"Cut report time in half this week.",
|
||||||
|
"If you build reports by hand, stop.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const esc = (s) => (/[",]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s);
|
||||||
|
const cols = ["id", "hook", "benefit", "proof", "cta", "accent", "bg"];
|
||||||
|
console.log(cols.join(","));
|
||||||
|
values.forEach((val, i) => {
|
||||||
|
const row = {...base, [FIELD]: val, id: `${FIELD}_${String(i + 1).padStart(2, "0")}`};
|
||||||
|
console.log(cols.map((c) => esc(row[c])).join(","));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
To run a CTA test next, set `FIELD = "cta"`, paste the *winning* hook into `base.hook`, and list CTA variants. Same one-variable rule, next stage of the funnel.
|
||||||
|
|
||||||
|
## 7. Batch + multi-aspect render
|
||||||
|
|
||||||
|
Render every variant in every aspect a campaign needs. Concurrency keeps large batches fast.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# render-all.sh one MP4 per (variant × aspect)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
node make-matrix.js > variants.csv # 1) build the one-variable test matrix
|
||||||
|
ASPECTS=("9x16" "4x5" "1x1") # placements this campaign needs
|
||||||
|
|
||||||
|
for ar in "${ASPECTS[@]}"; do
|
||||||
|
node csv-to-props.js variants.csv "props/$ar" "$ar" # 2) typed, validated props per aspect
|
||||||
|
for f in "props/$ar"/*.json; do
|
||||||
|
id=$(basename "$f" .json)
|
||||||
|
npx remotion render AdTemplate "out/${id}_${ar}.mp4" \
|
||||||
|
--props="$f" --concurrency=4 --log=error # 3) render
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done. $(ls out/*.mp4 | wc -l) ad variants in ./out"
|
||||||
|
```
|
||||||
|
|
||||||
|
5 hooks × 3 aspects = 15 MP4s from one template and one command. Add a CTA stage and the same template produces the next test wave with zero new component code.
|
||||||
|
|
||||||
|
## Common pitfalls
|
||||||
|
|
||||||
|
- **Two variables changed at once** — the winner is uninterpretable. The matrix generator prevents this; keep using it.
|
||||||
|
- **Hook clips in 9:16** — long hooks overflow; the parser warns past 60 chars. Keep hooks short and high in the frame.
|
||||||
|
- **CTA hidden behind platform UI** — anything in the 9:16 bottom third gets covered by captions and the CTA button. The `bottomSafe` padding reserves that band.
|
||||||
|
- **Drift across variants** — accent/copy live in the data, everything else in `theme`. If a variant looks off-brand, the fix is the theme, not the row.
|
||||||
|
- **Animation flicker in export** — caused by CSS transitions or library timers. Every value must derive from `useCurrentFrame()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 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 mass-produce ad-creative variants from one template × a data table — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=ad-video-skills&utm_content=ref_footer&utm_term=ad-creative-video)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Platform Specs & Message-Match Worksheet
|
||||||
|
|
||||||
|
Exact pixel safe zones, durations, aspect ratios, and format limits per placement (2025/2026), plus a worksheet for building message-matched hook→CTA pairs before they go into the CSV. Verify against each platform's current Ads Manager docs before a big spend — placements and safe zones shift.
|
||||||
|
|
||||||
|
## Aspect ratio → placement map
|
||||||
|
|
||||||
|
| Aspect | Resolution | Native placements |
|
||||||
|
|---|---|---|
|
||||||
|
| 9:16 | 1080×1920 | TikTok In-Feed, Instagram/Facebook Reels, Stories, YouTube Shorts |
|
||||||
|
| 4:5 | 1080×1350 | Meta Feed (largest feed real estate, recommended for Feed video) |
|
||||||
|
| 1:1 | 1080×1080 | Meta Feed, broad/automatic placements, X |
|
||||||
|
| 16:9 | 1920×1080 | YouTube in-stream, landscape feed, in-article |
|
||||||
|
|
||||||
|
Master rule: design the hook, product, and CTA inside the **1:1 center square** so a single composition reframes to every aspect by re-centering, not letterboxing. Black bars on a vertical feed read as "ad" and underperform.
|
||||||
|
|
||||||
|
## Safe zones (keep-clear bands)
|
||||||
|
|
||||||
|
Platform UI — captions, CTA chips, usernames, profile icons, progress bars — overlaps the frame. Keep all critical content out of these bands.
|
||||||
|
|
||||||
|
| Aspect / placement | Top keep-clear | Bottom keep-clear | Sides |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 9:16 Reels/Stories (Meta) | ~14% (~270px on 1920h) | ~20–35% (~384–672px) | ~6% (~65px) |
|
||||||
|
| 9:16 TikTok In-Feed | ~130px | ~484px (right-side icon rail + caption) | right ~140px |
|
||||||
|
| 4:5 Meta Feed | ~5% | ~10% (keep CTA above it) | ~5% |
|
||||||
|
| 1:1 / 16:9 | center 90% safe | center 90% safe | center 90% safe |
|
||||||
|
|
||||||
|
The 9:16 bottom third is the single most violated zone — captions, the CTA button, and the handle stack there. The `bottomSafe` padding in `batch-ad-pipeline.md` reserves it.
|
||||||
|
|
||||||
|
## Duration & format limits
|
||||||
|
|
||||||
|
| Placement | Sweet spot | Hard limits | Format |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Meta Feed (4:5/1:1) | 6–15s | up to 240 min, ≤4GB | MP4/MOV, H.264 |
|
||||||
|
| Meta Reels/Stories (9:16) | 8–15s | Reels ≤90s, Stories ≤2 min | MP4/MOV |
|
||||||
|
| TikTok In-Feed (9:16) | 15–30s (TikTok cites 21–34s) | up to 10 min (non-Spark) | MP4/MOV, ≥540×960 |
|
||||||
|
| YouTube in-stream (16:9) | 15–30s | skippable after 5s | MP4 |
|
||||||
|
|
||||||
|
Practical default for performance video: **15–30s, with the hook trigger landing before 2s.** Most ad-account data shows attention falls off a cliff after ~15s, so front-load everything that matters.
|
||||||
|
|
||||||
|
## Hook → CTA message-match worksheet
|
||||||
|
|
||||||
|
Fill one row per ad *before* writing the CSV. The CTA must pay off the exact promise the hook made; if it doesn't, the click is wasted and the test is noisy. Each completed row becomes one record in `variants.csv`.
|
||||||
|
|
||||||
|
| Hook (0–3s promise) | Hook type | Single benefit | Proof point | CTA (pays off the hook) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| "Spending 2 hrs/day on reports?" | problem-first | one-click report build | "save 11 hrs/week" | "Save 2 hours — try free" |
|
||||||
|
| "Your reporting tool is lying to you." | pattern interrupt | shows real numbers instantly | "save 11 hrs/week" | "See the real numbers — free" |
|
||||||
|
| "Nobody talks about this reporting trick." | curiosity gap | one click builds it | "save 11 hrs/week" | "Try the trick — free" |
|
||||||
|
| "Cut report time in half this week." | immediate benefit | auto-built reports | "save 11 hrs/week" | "Cut it in half — free" |
|
||||||
|
| "If you build reports by hand, stop." | direct callout | automate it now | "save 11 hrs/week" | "Automate yours — free" |
|
||||||
|
|
||||||
|
Bad pairing to avoid: a "stop wasting 2 hours" hook ending on a generic "Shop now" — the promise and the payoff don't connect, so even a great hook converts poorly.
|
||||||
|
|
||||||
|
## Testing order (which field to vary first)
|
||||||
|
|
||||||
|
Run tests in funnel order; isolate one field per wave using the matrix generator:
|
||||||
|
|
||||||
|
1. **Hook** — biggest lever on CPA; test 5–8 hooks against one identical body.
|
||||||
|
2. **Offer / benefit** — take the winning hook, vary the core promise.
|
||||||
|
3. **CTA** — winning hook + offer, vary the closing action.
|
||||||
|
4. **Format / aspect** — same winning ad, compare placements.
|
||||||
|
|
||||||
|
Budget guide many teams use: ~60% to proven winners, ~30% to variations of winners, ~10% to fresh concepts. Run each test long enough for a real sample (commonly several hundred impressions per variant, ~1 week to smooth daily swings) before declaring a winner.
|
||||||
|
|
||||||
|
## Pre-flight checklist per variant
|
||||||
|
|
||||||
|
- Hook trigger lands before 2s and fits the frame (≤~60 chars in 9:16).
|
||||||
|
- CTA visibly pays off the hook's specific promise.
|
||||||
|
- Critical content inside the center square and outside every keep-clear band.
|
||||||
|
- Duration within the placement's sweet spot.
|
||||||
|
- Exported in every aspect the campaign's placements require.
|
||||||
|
- Exactly one field differs from the other variants in this test wave.
|
||||||
Whitespace-only changes.
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
name: launch-video
|
||||||
|
description: This skill should be used when the user asks to "make a launch video", "create a product hype/sizzle film", "build a teaser/trailer for a drop", "do a reveal montage with an end-card CTA", "cut a hero video to the beat", or "export a launch film in 16:9/9:16/1:1". Covers the hook→tease→reveal→feature-montage→end-card arc and multi-aspect export.
|
||||||
|
version: 0.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Launch Video
|
||||||
|
|
||||||
|
Make a premium, high-energy 15–60s launch film that stops the scroll and converts. Build the classic arc — hook → tease → reveal → feature montage → end-card CTA — cut to the music, and export for every platform aspect.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Product drops, feature announcements, trailers, hype reels (15–60s).
|
||||||
|
- "Sizzle" hero videos for a landing page.
|
||||||
|
- Beat-synced reveals where the brand moment lands on the drop.
|
||||||
|
|
||||||
|
## The arc
|
||||||
|
|
||||||
|
| Beat | Job | Typical share (of 30s) |
|
||||||
|
|---|---|---|
|
||||||
|
| Hook (0–3s) | A striking frame/motion that stops the scroll | 0–3s |
|
||||||
|
| Tease | Hint the product; build curiosity, beat-synced | 3–9s |
|
||||||
|
| Reveal | Product/logo hits on the music drop | 9–13s |
|
||||||
|
| Feature montage | Fast, kinetic, one benefit per shot | 13–25s |
|
||||||
|
| End card | Logo + tagline + CTA, clean hold | 25–30s |
|
||||||
|
|
||||||
|
Scale the same proportions for 15s (tighter) or 60s (longer montage, never a longer hook).
|
||||||
|
|
||||||
|
## Two non-negotiable rules
|
||||||
|
|
||||||
|
1. **Sound design leads picture.** Lock the track first, mark the beats and the drop, then cut visuals to those marks. Never score a finished cut — the reveal lands on the drop, not near it.
|
||||||
|
2. **Quality over quantity.** A few flawless shots beat many mediocre ones. Cut any shot that isn't premium.
|
||||||
|
|
||||||
|
## Beat-synced reveal on the drop
|
||||||
|
|
||||||
|
Find the drop's exact timecode in the audio, then time the reveal so the brand mark snaps to full at that frame, with a micro-overshoot for impact.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import gsap from "gsap";
|
||||||
|
const DROP = 9.0; // seconds, measured from the track
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
// tease builds tension up to the drop
|
||||||
|
tl.to(".tease", { opacity: 1, scale: 1.05, duration: DROP, ease: "power1.in" })
|
||||||
|
// reveal SNAPS on the drop frame
|
||||||
|
.set(".logo", { opacity: 1 }, DROP)
|
||||||
|
.fromTo(".logo", { scale: 1.18 }, { scale: 1.0, duration: .18, ease: "power3.out" }, DROP)
|
||||||
|
.fromTo(".flash", { opacity: .9 }, { opacity: 0, duration: .25 }, DROP); // white hit
|
||||||
|
audio.addEventListener("play", () => tl.play());
|
||||||
|
```
|
||||||
|
|
||||||
|
Cut on transients, not on a fixed grid: the hardest cuts go on kick/snare hits; ramp speed (time-remap) into the drop, then hard-cut out of it.
|
||||||
|
|
||||||
|
## Kinetic feature montage
|
||||||
|
|
||||||
|
One benefit per shot, ~0.6–1.0s each, hard cuts on the beat. Each shot = a bold keyword + a single supporting visual.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.feature { opacity:0; }
|
||||||
|
.feature.in .kw { animation: pop .35s cubic-bezier(.22,1,.36,1) forwards; }
|
||||||
|
.feature.in .word { display:inline-block; }
|
||||||
|
@keyframes pop { from { opacity:0; transform:translateY(24px) } to { opacity:1; transform:none } }
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// drive shots off beat marks measured from the track
|
||||||
|
const beats = [13.0, 13.8, 14.6, 15.4, 16.2]; // seconds
|
||||||
|
beats.forEach((t, i) => scheduleAt(t, () => showFeature(i)));
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep one motion language across the montage (same enter curve, same exit) so speed reads as confidence, not chaos.
|
||||||
|
|
||||||
|
## End card
|
||||||
|
|
||||||
|
Hold the logo + tagline + CTA still and clean for ≥2s. No busy motion competing with the CTA; let the brand settle. Land the brand moment on the strongest remaining beat, then a crisp button/URL.
|
||||||
|
|
||||||
|
## Multi-aspect export
|
||||||
|
|
||||||
|
Compose with a center safe zone so one master crops cleanly to all aspects.
|
||||||
|
|
||||||
|
| Aspect | Use | Resolution | Safe zone |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 16:9 | YouTube, landing hero, X | 1920×1080 | keep key content within center 90% |
|
||||||
|
| 9:16 | Reels, TikTok, Shorts, Stories | 1080×1920 | text within center 80% h; avoid top 12% / bottom 18% (UI) |
|
||||||
|
| 1:1 | Feed posts | 1080×1080 | center square of the 16:9 frame |
|
||||||
|
|
||||||
|
Design the hero/logo/CTA inside the **1:1 center square** so it survives every crop. Render the 16:9 master, then reframe (don't just letterbox) 9:16 and 1:1 from the same project.
|
||||||
|
|
||||||
|
## Output checklist
|
||||||
|
|
||||||
|
- Track locked first; reveal lands exactly on the drop.
|
||||||
|
- Hook earns the first 3 seconds.
|
||||||
|
- Montage: one benefit per shot, hard cuts on beats, consistent motion language.
|
||||||
|
- End card holds ≥2s with one clear CTA.
|
||||||
|
- 16:9 + 9:16 + 1:1 exports, key content in the center safe zone.
|
||||||
|
- Premium-only shots — nothing mediocre survives the cut.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
If the launch film is built/rendered in Remotion, treat it as the heavy tier: register a `<Composition>` (+ zod `schema` + `defaultProps`), drive every value off `useCurrentFrame()` — no GSAP timelines / audio-event callbacks / `Date.now()` / `Math.random()` at render time (bake beat + drop timecodes into props). Deliverable = the rendered `out/*.mp4` per aspect (plus the project).
|
||||||
|
|
||||||
|
**Verify loop — render stills → inspect → encode.** Sample the arc, with the **shipped** props (real headline/tagline/CTA), not just `defaultProps`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frame-exact stills across hook → reveal → end-card
|
||||||
|
npx remotion still Launch out/f-hook.png --frame=24 --props=props/launch.json # hook earns first 3s
|
||||||
|
npx remotion still Launch out/f-reveal.png --frame=270 --props=props/launch.json # logo SNAPS on the drop frame
|
||||||
|
npx remotion still Launch out/f-end.png --frame=899 --props=props/launch.json # end card: logo+tagline+CTA, clean hold
|
||||||
|
# inspect each: fidelity (product/logo, tagline, CTA text exact, brand colors right, reveal at the drop frame)
|
||||||
|
# AND artifacts (text overflow, off-canvas, CTA in the 9:16 bottom third, missing font, flash frame stuck on)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-aspect — verify the master in EACH aspect before encoding all of them.** A reframe bug (hero pushed off the safe zone) repeats across every aspect; catch it once.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for ar in 16x9 9x16 1x1; do # check reveal + end card per aspect
|
||||||
|
npx remotion still Launch "out/reveal-${ar}.png" --frame=270 \
|
||||||
|
--props=props/launch.json --props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done
|
||||||
|
# stills clean in all aspects? → then encode each:
|
||||||
|
for ar in 16x9 9x16 1x1; do
|
||||||
|
npx remotion render Launch "out/launch-${ar}.mp4" --props=props/launch.json --props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done
|
||||||
|
npx remotion render Launch out/demo.gif --codec=gif --props=props/launch.json # README demo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before you finish:**
|
||||||
|
1. `npx remotion still` renders cleanly at hook / reveal / end card — no errors, no missing fonts/assets.
|
||||||
|
2. Logo/tagline/CTA text exact, brand colors right; reveal lands on the drop frame; key content inside the center safe zone, clear of the 9:16 bottom third.
|
||||||
|
3. Frame-driven only — no GSAP/audio-callback timing, `Date.now()`, or `Math.random()` (beats/drop baked into props).
|
||||||
|
4. Shipped props render correctly (not just `defaultProps`); the end card holds ≥2s with one CTA.
|
||||||
|
5. Master verified in 16:9 + 9:16 + 1:1 before encoding; MP4s play; (optional) GIF for the README.
|
||||||
|
|
||||||
|
## Reference files
|
||||||
|
|
||||||
|
- `references/launch-structure.md` — a full shot-by-shot template with timecodes for a 30s film (scalable to 15–60s), beat-synced reveal timing, kinetic-montage shot recipes, the sound-design-leads-picture workflow, and detailed multi-aspect safe-area maps for 16:9 / 9:16 / 1:1.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Launch Video Structure — Cookbook
|
||||||
|
|
||||||
|
Shot-by-shot template, beat-sync timing, montage recipes, and multi-aspect maps for a premium 15–60s launch film.
|
||||||
|
|
||||||
|
## Sound-design-leads-picture workflow
|
||||||
|
|
||||||
|
1. **Pick/commission the track first.** The track defines the film's length and energy.
|
||||||
|
2. **Mark the structure in the audio**: intro, build, the **drop** (the single biggest moment), and the kick/snare transients through the montage. Note exact timecodes.
|
||||||
|
3. **Map beats → cuts.** Hard cuts land on transients; the reveal lands on the drop frame.
|
||||||
|
4. **Build picture to the marks.** Never finish a cut and then look for music. Score-first guarantees the reveal hits.
|
||||||
|
5. **Mix last**: add risers into the drop, an impact/whoosh on the reveal, and let the end-card breathe (drop music low, or a clean tag).
|
||||||
|
|
||||||
|
## Shot-by-shot template — 30s film (scalable)
|
||||||
|
|
||||||
|
```
|
||||||
|
00:00–00:03 HOOK
|
||||||
|
Visual : single striking frame — extreme close detail / impossible motion
|
||||||
|
Motion : slow push-in or a fast whip that resolves; NO logo yet
|
||||||
|
Audio : sub-bass swell begins, sparse
|
||||||
|
Cut : on first audible beat at ~03s
|
||||||
|
|
||||||
|
00:03–00:09 TEASE
|
||||||
|
Visual : fragments/silhouettes of the product, never the full thing
|
||||||
|
Motion : light leaks, partial reveals, parallax; tension rising
|
||||||
|
Audio : riser building toward the drop
|
||||||
|
Cut : quick cuts accelerating into the drop
|
||||||
|
|
||||||
|
00:09–00:13 REVEAL ← THE DROP at 00:09
|
||||||
|
Visual : product/logo SNAPS to full on the drop frame
|
||||||
|
Motion : scale 1.18→1.0 over 0.18s + white flash-out; settle clean
|
||||||
|
Audio : drop hits + impact stinger on the exact frame
|
||||||
|
Hold : let it sit ~1.2s before moving
|
||||||
|
|
||||||
|
00:13–00:25 FEATURE MONTAGE
|
||||||
|
Visual : one benefit per shot, ~0.6–1.0s each (≈8–10 shots)
|
||||||
|
Motion : bold keyword pop + one supporting visual; consistent enter/exit
|
||||||
|
Audio : hard cuts on kick/snare; energy sustained
|
||||||
|
Pattern: KEYWORD + UI/product moment, repeat with rhythm
|
||||||
|
|
||||||
|
00:25–00:30 END CARD
|
||||||
|
Visual : logo + tagline + CTA, centered, still
|
||||||
|
Motion : minimal — a soft settle, then hold ≥2s
|
||||||
|
Audio : music resolves; clean sonic tag
|
||||||
|
CTA : one action (URL or button), nothing competing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Re-proportioning
|
||||||
|
|
||||||
|
- **15s**: Hook 0–2s · Tease 2–5s · Reveal 5–7s · Montage 7–12s (4–5 shots) · End card 12–15s.
|
||||||
|
- **60s**: same hook (still ≤3s) · longer tease and a richer montage (14–16 shots, grouped in waves) · end card ≥3s.
|
||||||
|
|
||||||
|
The hook never gets longer — only the montage scales.
|
||||||
|
|
||||||
|
## Beat-synced reveal (timing detail)
|
||||||
|
|
||||||
|
```js
|
||||||
|
import gsap from "gsap";
|
||||||
|
const DROP = 9.0; // measured drop timecode (s)
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
// build tension toward the drop
|
||||||
|
tl.fromTo(".tease", { scale: 1.0, opacity: .6 },
|
||||||
|
{ scale: 1.08, opacity: 1, duration: DROP, ease: "power1.in" });
|
||||||
|
|
||||||
|
// REVEAL exactly on the drop
|
||||||
|
tl.set(".logo", { opacity: 1 }, DROP);
|
||||||
|
tl.fromTo(".logo", { scale: 1.18 }, { scale: 1.0, duration: .18, ease: "power3.out" }, DROP);
|
||||||
|
tl.fromTo(".flash", { opacity: .9 }, { opacity: 0, duration: .25, ease: "power2.out" }, DROP);
|
||||||
|
tl.to(".tease", { opacity: 0, duration: .12 }, DROP);
|
||||||
|
|
||||||
|
// hold the reveal
|
||||||
|
tl.to(".logo", { scale: 1.0, duration: 1.2 }); // dwell
|
||||||
|
|
||||||
|
audio.addEventListener("play", () => tl.play());
|
||||||
|
```
|
||||||
|
|
||||||
|
Speed-ramp into the drop (Remotion time-remap idea): play the last ~0.5s of the tease faster, then hard-cut to the reveal on the drop frame for a snap.
|
||||||
|
|
||||||
|
## Kinetic montage shot recipe
|
||||||
|
|
||||||
|
Each shot: a bold keyword + one product/UI visual, ~0.6–1.0s, hard cut on the beat.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.feature { position:absolute; inset:0; opacity:0; }
|
||||||
|
.feature.in { opacity:1; }
|
||||||
|
.feature .word { display:inline-block; opacity:0; transform:translateY(26px); }
|
||||||
|
.feature.in .word { animation: pop .32s cubic-bezier(.22,1,.36,1) forwards; }
|
||||||
|
.feature.in .word:nth-child(2){ animation-delay:.04s }
|
||||||
|
.feature.in .word:nth-child(3){ animation-delay:.08s }
|
||||||
|
@keyframes pop { to { opacity:1; transform:none; } }
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// schedule shots on measured beat marks
|
||||||
|
const beats = [13.0, 13.8, 14.6, 15.4, 16.2, 17.0, 17.8, 18.6]; // seconds
|
||||||
|
let i = 0;
|
||||||
|
function loop(now){
|
||||||
|
const t = (now - start) / 1000;
|
||||||
|
if (i < beats.length && t >= beats[i]) { showFeature(i); i++; }
|
||||||
|
if (i < beats.length) requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Montage discipline: keep ONE enter curve and ONE exit across all shots; vary content, never grammar. Alternate visual rhythm (full-frame ↔ detail) but keep cut cadence locked to the beat.
|
||||||
|
|
||||||
|
## Multi-aspect safe-area maps
|
||||||
|
|
||||||
|
Compose so a single master reframes to every aspect. Place hero/logo/CTA inside the smallest common safe zone — the 1:1 center square.
|
||||||
|
|
||||||
|
```
|
||||||
|
16:9 1920×1080
|
||||||
|
Safe: center 90% (96px H / 54px V margins)
|
||||||
|
Use : YouTube, landing hero, X timeline
|
||||||
|
|
||||||
|
9:16 1080×1920
|
||||||
|
Safe: text within center 80% height
|
||||||
|
Avoid: top 12% (status/clock area), bottom 18% (caption + UI buttons)
|
||||||
|
Use : Reels, TikTok, Shorts, Stories
|
||||||
|
|
||||||
|
1:1 1080×1080
|
||||||
|
Safe: this square == the design anchor; put logo/CTA here
|
||||||
|
Use : feed posts
|
||||||
|
```
|
||||||
|
|
||||||
|
Export workflow:
|
||||||
|
|
||||||
|
1. Build the 16:9 master with all key content inside the 1:1 center square.
|
||||||
|
2. For 9:16, **reframe** (pan/scale per shot to keep the subject centered) — do not letterbox.
|
||||||
|
3. For 1:1, crop the center square; verify text isn't clipped.
|
||||||
|
4. Re-check captions/CTA against each platform's UI overlay zones before export.
|
||||||
|
|
||||||
|
## Final QA checklist
|
||||||
|
|
||||||
|
- [ ] Reveal lands on the exact drop frame, with overshoot + flash.
|
||||||
|
- [ ] Hook holds attention in the first 3s without showing the product.
|
||||||
|
- [ ] Every montage shot is one benefit, premium quality, on a beat.
|
||||||
|
- [ ] End card holds ≥2s, single CTA, nothing competing.
|
||||||
|
- [ ] 16:9 / 9:16 / 1:1 each pass safe-area and caption checks.
|
||||||
|
- [ ] Audio mixed: riser → impact on drop → clean tag on end card.
|
||||||
|
|
||||||
|
---
|
||||||
|
Nail the reveal beat and the sound-leads-picture cut, and a launch film lands. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=ad-video-skills&utm_content=skill_footer&utm_term=launch-video)** — the AI motion agent for editable, on-brand motion graphics.
|
||||||
Whitespace-only changes.
@@ -0,0 +1,196 @@
|
|||||||
|
---
|
||||||
|
name: testimonial-video
|
||||||
|
description: This skill should be used when the user asks to "make an animated testimonial", "create a quote video", "build a review video", "turn a customer testimonial into a graphic", "animate a customer review", "make a social proof video", "add an animated star rating", or "turn a list of reviews into videos". Covers quote typography, staggered line reveals, kinetic emphasis on key phrases, animated star ratings, the author block (name/role/avatar/company), and templating one card across many quotes.
|
||||||
|
version: 0.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testimonial Video
|
||||||
|
|
||||||
|
Animate a written quote, review, or testimonial into a short, trustworthy social-proof clip. The craft is typographic, not flashy: a clean quote card where the words land in reading order, the key phrase gets emphasis exactly when it should, the stars fill to the real score, and the author signs off. One card design, driven by props, becomes a video for every review on the list.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
- Customer review → a 6–12s vertical/feed clip (the headline use case).
|
||||||
|
- A pull-quote or testimonial graphic with author name, role, avatar, company.
|
||||||
|
- An animated star rating (including half/fractional scores like 4.5).
|
||||||
|
- A batch: a list/CSV of reviews → one video each, same template.
|
||||||
|
|
||||||
|
Do **not** reach here for sale/discount promos or generic ad creative — this skill is strictly the quote / social-proof angle.
|
||||||
|
|
||||||
|
## The one rule: trust before motion
|
||||||
|
|
||||||
|
A testimonial earns its job by feeling *real*, not produced. Restraint reads as credibility; over-animation reads as an ad people skip.
|
||||||
|
|
||||||
|
1. **Reveal in reading order.** The eye must finish a line before the next arrives. Stagger lines (or clauses), never all words at once, and never reorder for visual flair.
|
||||||
|
2. **Quote the words exactly.** Do not paraphrase, trim mid-sentence, or invent a rating. If you emphasize a phrase, it must be the customer's phrase.
|
||||||
|
3. **The author is the proof.** A quote with no attributable human is just a slogan. Name + role + company (and avatar if real) is non-negotiable.
|
||||||
|
|
||||||
|
## The anatomy of a quote card
|
||||||
|
|
||||||
|
| Element | Job | Timing |
|
||||||
|
|---|---|---|
|
||||||
|
| Quote mark motif | Signals "this is a quote" before words read | Frame 0, scales/fades in first |
|
||||||
|
| Quote body | The testimonial, revealed line-by-line | Staggered, ~6–10 frames apart |
|
||||||
|
| Emphasis span | The one phrase that sells | Lands *after* its line settles |
|
||||||
|
| Star rating | The score, filled to the real value | Fills left→right after the quote |
|
||||||
|
| Author block | Name / role / company / avatar | Signs off last, slides up |
|
||||||
|
|
||||||
|
Build in that order; reveal in that order. The viewer reads the quote, believes it, sees the score, then learns who said it.
|
||||||
|
|
||||||
|
## Staggered line reveal
|
||||||
|
|
||||||
|
Split the quote into lines (or clauses), then offset each one's entrance. In Remotion, drive every line off `useCurrentFrame()` so it renders deterministically — never a CSS transition or a JS timer, which desync from the frame renderer.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { useCurrentFrame, spring, useVideoConfig } from "remotion";
|
||||||
|
|
||||||
|
const QuoteBody = ({ lines, startAt = 8, stagger = 6 }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
return (
|
||||||
|
<blockquote>
|
||||||
|
{lines.map((line, i) => {
|
||||||
|
const enter = spring({ frame: frame - startAt - i * stagger, fps,
|
||||||
|
config: { damping: 200 } }); // 0→1, no overshoot
|
||||||
|
return (
|
||||||
|
<span key={i} style={{ display: "block",
|
||||||
|
opacity: enter,
|
||||||
|
transform: `translateY(${(1 - enter) * 14}px)` }}>
|
||||||
|
{line}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep one motion language: same easing, same 14px rise, every line. Speed reads as confidence only when it's consistent. See `references/quote-card.md` for the full composition.
|
||||||
|
|
||||||
|
## Kinetic emphasis on the key phrase
|
||||||
|
|
||||||
|
Pick the single phrase that carries the testimonial ("saved us 10 hours a week") and let it arrive *after* its line has settled — a highlight sweep, a weight shift, or a color pop. Emphasis competing with the reveal cancels both.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const sweep = interpolate(frame, [hlStart, hlStart + 10], [0, 100],
|
||||||
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
// a highlighter wiping in behind the phrase, left→right
|
||||||
|
<mark style={{ background:
|
||||||
|
`linear-gradient(90deg, var(--accent) ${sweep}%, transparent ${sweep}%)`,
|
||||||
|
color: "inherit", padding: "0 .08em" }}>{phrase}</mark>
|
||||||
|
```
|
||||||
|
|
||||||
|
Emphasize at most one span per quote. Two highlights is no highlight.
|
||||||
|
|
||||||
|
## Animated star rating (fractional)
|
||||||
|
|
||||||
|
Render the real score, including halves. Use one full row of star glyphs as a mask, then wipe a filled layer to `rating / max` width — this handles 4.5 as cleanly as 5.0, with no half-star sprite.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const StarRating = ({ rating, max = 5, fillStart = 30, fillDur = 14 }) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const target = (rating / max) * 100;
|
||||||
|
const w = interpolate(frame, [fillStart, fillStart + fillDur], [0, target],
|
||||||
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||||||
|
return (
|
||||||
|
<div className="stars" role="img" aria-label={`${rating} out of ${max} stars`}>
|
||||||
|
<div className="track">★★★★★</div>
|
||||||
|
<div className="fill" style={{ width: `${w}%` }}>★★★★★</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.stars { position: relative; display: inline-block; font-size: 40px; letter-spacing: 4px; }
|
||||||
|
.track { color: rgba(255,255,255,.22); }
|
||||||
|
.fill { position: absolute; inset: 0; overflow: hidden; white-space: nowrap;
|
||||||
|
color: var(--accent); } /* clipped by width → fractional fill */
|
||||||
|
```
|
||||||
|
|
||||||
|
Fill left→right after the quote reads. Never animate stars before the words — the score must feel earned, not pre-loaded. Always set `aria-label` to the real value.
|
||||||
|
|
||||||
|
## The author block
|
||||||
|
|
||||||
|
Sign off last. The block slides up under the stars and holds still — stillness is what makes attribution read as fact.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<figcaption className="author">
|
||||||
|
{avatar && <img src={avatar} alt="" className="avatar" />}
|
||||||
|
<div>
|
||||||
|
<span className="name">{name}</span>
|
||||||
|
<span className="role">{role}{company && `, ${company}`}</span>
|
||||||
|
</div>
|
||||||
|
</figcaption>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a real avatar or none — a generic placeholder face actively destroys trust. Name is the loudest line in the block; role/company is secondary weight.
|
||||||
|
|
||||||
|
## Template × reviews — one video per quote
|
||||||
|
|
||||||
|
The payoff: make every field a prop, hardcode nothing, then render once per review.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
export const Testimonial = ({ quote, highlight, rating, name, role, company, avatar }) => { /* … */ };
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
for f in reviews/*.json; do
|
||||||
|
npx remotion render Testimonial "out/$(basename "$f" .json).mp4" --props="$f"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep colors/fonts/quote-mark/layout in one theme object so 50 testimonials stay brand-locked and only the words and score change. See `references/batch-and-typography.md` for the review schema, CSV→props parsing, quote-typography craft (smart quotes, hanging punctuation, line-splitting, orphans), and the batch script.
|
||||||
|
|
||||||
|
## Output checklist
|
||||||
|
|
||||||
|
- Lines reveal in reading order; one consistent enter motion, ~6–10 frame stagger.
|
||||||
|
- The quote is verbatim; exactly one emphasized span, landing after its line settles.
|
||||||
|
- Stars fill to the *real* score (halves supported) after the quote, with a correct `aria-label`.
|
||||||
|
- Author block (name + role + company, real avatar or none) signs off last and holds still.
|
||||||
|
- Every animated value derives from `useCurrentFrame()` — no CSS/JS timers.
|
||||||
|
- Every field is a prop; one template renders every review in the folder.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
**Output contract:**
|
||||||
|
- A Remotion `Testimonial` registered as `<Composition>` (+ zod `schema` + `defaultProps`), every value frame-driven (no CSS transitions / JS timers / `Date.now()` / `Math.random()`).
|
||||||
|
- Deliverable = the rendered `out/*.mp4` per review per aspect (plus the project + review data, so the user re-renders on new quotes).
|
||||||
|
|
||||||
|
**Verify loop — render stills → inspect → encode.** Cheap PNGs first; render with the **shipped** review's props, not just `defaultProps`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frame-exact stills across the reveal order: quote → stars → author
|
||||||
|
npx remotion still Testimonial out/f-quote.png --frame=30 --props=reviews/r1.json # lines in reading order, emphasis span
|
||||||
|
npx remotion still Testimonial out/f-stars.png --frame=48 --props=reviews/r1.json # stars filled to the REAL score (e.g. 4.5)
|
||||||
|
npx remotion still Testimonial out/f-author.png --frame=215 --props=reviews/r1.json # author block signed off, holding still
|
||||||
|
# inspect each: fidelity (quote VERBATIM, exactly one emphasized span, star fill matches rating, name/role/company exact)
|
||||||
|
# AND artifacts (text overflow/orphans, off-canvas, author in the 9:16 bottom third, missing font, broken avatar)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-aspect / batch — verify one review in EACH target aspect before batch-rendering the list.** A layout bug (overflowing quote, clipped author) repeats across every review × aspect; catch it once.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for ar in 9x16 1x1 16x9; do # one representative review, every aspect
|
||||||
|
npx remotion still Testimonial "out/r1-${ar}.png" --frame=215 \
|
||||||
|
--props=reviews/r1.json --props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done
|
||||||
|
# stills clean in all aspects? → then batch-render the list:
|
||||||
|
for f in reviews/*.json; do id=$(basename "$f" .json); for ar in 9x16 1x1 16x9; do
|
||||||
|
npx remotion render Testimonial "out/${id}-${ar}.mp4" --props="$f" --props-merge="{\"aspect\":\"${ar}\"}"
|
||||||
|
done; done
|
||||||
|
npx remotion render Testimonial out/demo.gif --codec=gif --props=reviews/r1.json # README demo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before you finish:**
|
||||||
|
1. `npx remotion still` renders cleanly at quote / stars / author — no errors, no missing fonts/avatar.
|
||||||
|
2. Quote is verbatim with one emphasized span; stars fill to the real score; name/role/company exact and inside the safe zone (clear of the 9:16 bottom third).
|
||||||
|
3. Frame-driven only — no CSS/JS timers, `Date.now()`, or `Math.random()`.
|
||||||
|
4. Shipped review's props render correctly (not just `defaultProps`); `aria-label` carries the real rating.
|
||||||
|
5. One review verified in 9:16 + 1:1 + 16:9 before the batch; MP4s play; (optional) GIF for the README.
|
||||||
|
|
||||||
|
## Reference files
|
||||||
|
|
||||||
|
- `references/quote-card.md` — a complete runnable Remotion `Testimonial` composition: staggered line reveal, key-phrase highlight sweep, fractional star fill, author block, theme object, and the timing map for a 9:16 card. Plus a dependency-free HTML/CSS variant.
|
||||||
|
- `references/batch-and-typography.md` — the review prop schema, CSV/JSON→props parsing and validation, quote typography craft (smart quotes, hanging quotation marks, line/clause splitting, orphan control, legibility in motion), star-rating accessibility, and the template×reviews batch render script for N videos.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Batch & Typography
|
||||||
|
|
||||||
|
How to feed many reviews into one card design, plus the quote-typography craft that separates a credible testimonial from a sloppy one.
|
||||||
|
|
||||||
|
## Review prop schema
|
||||||
|
|
||||||
|
One object per testimonial. Keep it flat so a CSV row maps to it directly.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type Review = {
|
||||||
|
quote: string; // verbatim — never paraphrased or trimmed mid-sentence
|
||||||
|
highlight?: string; // EXACT substring of `quote`; one phrase only
|
||||||
|
rating: number; // 0–max, halves allowed (4.5)
|
||||||
|
max?: number; // default 5
|
||||||
|
name: string; // required — a quote with no name is not social proof
|
||||||
|
role?: string;
|
||||||
|
company?: string;
|
||||||
|
avatar?: string; // real photo URL, or omit (never a stock face)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation (fail loud, before rendering 50 videos)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function validate(r: Review): string[] {
|
||||||
|
const errs: string[] = [];
|
||||||
|
if (!r.quote?.trim()) errs.push("quote is empty");
|
||||||
|
if (!r.name?.trim()) errs.push("name is required (no anonymous testimonials)");
|
||||||
|
const max = r.max ?? 5;
|
||||||
|
if (r.rating < 0 || r.rating > max) errs.push(`rating ${r.rating} out of 0–${max}`);
|
||||||
|
if (r.highlight && !r.quote.includes(r.highlight))
|
||||||
|
errs.push(`highlight "${r.highlight}" is not a substring of the quote`);
|
||||||
|
return errs;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `highlight ⊂ quote` check is the important one: it guarantees you only ever emphasize words the customer actually wrote. If the highlight isn't an exact substring, the emphasis silently disappears in the card — so block the render instead.
|
||||||
|
|
||||||
|
## CSV → props
|
||||||
|
|
||||||
|
Reviews usually arrive as a spreadsheet export. Map columns to the schema and coerce the rating.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { parse } from "csv-parse/sync";
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
|
|
||||||
|
// CSV columns: quote,highlight,rating,name,role,company,avatar
|
||||||
|
const rows = parse(readFileSync("reviews.csv"), { columns: true, trim: true });
|
||||||
|
mkdirSync("reviews", { recursive: true });
|
||||||
|
|
||||||
|
rows.forEach((row, i) => {
|
||||||
|
const r = {
|
||||||
|
quote: row.quote,
|
||||||
|
highlight: row.highlight || undefined,
|
||||||
|
rating: Number(row.rating),
|
||||||
|
name: row.name,
|
||||||
|
role: row.role || undefined,
|
||||||
|
company: row.company || undefined,
|
||||||
|
avatar: row.avatar || undefined,
|
||||||
|
};
|
||||||
|
const errs = validate(r);
|
||||||
|
if (errs.length) { console.error(`Row ${i + 1} (${r.name}):`, errs); process.exit(1); }
|
||||||
|
writeFileSync(`reviews/${String(i).padStart(3, "0")}-${slug(r.name)}.json`,
|
||||||
|
JSON.stringify(r, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch render — one video per review
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p out
|
||||||
|
for f in reviews/*.json; do
|
||||||
|
name=$(basename "$f" .json)
|
||||||
|
npx remotion render Testimonial "out/$name.mp4" --props="$f"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything visual lives in the single `theme` object in the composition (colors, fonts, quote-mark glyph, spacing). Because only the props change per render, 50 testimonials stay pixel-for-pixel brand-locked while the words and score vary. To re-theme the whole set, edit one object and re-run the loop.
|
||||||
|
|
||||||
|
For multi-aspect (9:16 feed + 1:1 post + 16:9 site), register the composition at each size with a shared center-safe layout and loop over both `reviews/*.json` and the aspect list.
|
||||||
|
|
||||||
|
## Quote typography craft
|
||||||
|
|
||||||
|
Motion is the easy part; legible, well-set type is what makes the quote trustworthy.
|
||||||
|
|
||||||
|
### Smart quotes, always
|
||||||
|
|
||||||
|
Straight quotes (`"`) read as un-proofed. Convert to typographic quotes before rendering.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const smarten = (s) => s
|
||||||
|
.replace(/(^|[\s([{])"/g, "$1“").replace(/"/g, "”") // “ ”
|
||||||
|
.replace(/(^|[\s([{])'/g, "$1‘").replace(/'/g, "’") // ‘ ’
|
||||||
|
.replace(/--/g, "—"); // em dash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hanging quotation marks
|
||||||
|
|
||||||
|
A leading quote glyph indents the first line and breaks the flush-left edge. Hang it into the margin so the text block stays optically aligned — the gold standard for set quotes.
|
||||||
|
|
||||||
|
```css
|
||||||
|
blockquote { hanging-punctuation: first; } /* Safari/WebKit */
|
||||||
|
/* Cross-engine fallback: pull the opening mark left by its own width */
|
||||||
|
blockquote::before { content: "\201C"; margin-left: -0.55em; color: var(--muted); }
|
||||||
|
```
|
||||||
|
|
||||||
|
When the card uses a large decorative quote mark (as in `quote-card.md`), drop the inline opening glyph so the mark isn't doubled.
|
||||||
|
|
||||||
|
### Splitting into lines / clauses
|
||||||
|
|
||||||
|
Reveal in reading order means splitting on meaning, not on a fixed character count. Prefer clause boundaries (`. ! ? , ; —`), then pack to a comfortable measure (~32–40 chars at card size). The `toLines` helper in `quote-card.md` does exactly this. Never split a short quote into one-word-per-line — that reads as a ransom note, not a testimonial.
|
||||||
|
|
||||||
|
### Orphans & widows
|
||||||
|
|
||||||
|
A single dangling word on the last line looks broken. Bind the last two words with a non-breaking space so they wrap together.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const noOrphan = (s) => s.replace(/ ([^ ]+)\s*$/, " $1");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legibility in motion
|
||||||
|
|
||||||
|
- Use a font that stays readable while moving — avoid thin weights and tight tracking; a serif or a humanist sans at 500–600 weight holds up best on a rising line.
|
||||||
|
- Animate **opacity + a small translate** only. Blur, rotation, and scale on body text hurt readability mid-reveal.
|
||||||
|
- Contrast: body text ≥ 4.5:1 against the card; the highlight accent should pass against both the card and the ink it sits behind.
|
||||||
|
- One emphasized span per quote. A second highlight halves the weight of the first.
|
||||||
|
|
||||||
|
## Star-rating accessibility & correctness
|
||||||
|
|
||||||
|
- The visual stars are decorative; expose the value to assistive tech with `role="img"` and `aria-label="4.5 out of 5 stars"` on the container (already in `quote-card.md`).
|
||||||
|
- For fractional scores, the width-clip method (`rating / max * 100%`) is exact for any decimal — no rounding to the nearest half-star, so 4.3 renders as 4.3.
|
||||||
|
- Never animate the stars before the quote is read, and never show more filled stars than the data supports. A faked rating is the fastest way to lose the trust the testimonial was meant to build.
|
||||||
|
- If a review has no rating, omit the star row entirely rather than defaulting to 5 — an invented score is worse than none.
|
||||||
|
|
||||||
|
## Pacing notes
|
||||||
|
|
||||||
|
- Hold the finished card still for the back half of the clip (≥3s) so it's screenshot-able and re-shareable — shares are the whole point of social proof.
|
||||||
|
- For a sequence of testimonials in one video, give each card a full read (~3–4s) and hard-cut between them; cross-dissolving quotes makes both unreadable.
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# Quote Card — complete runnable component
|
||||||
|
|
||||||
|
A full testimonial card you can drop into a Remotion project, plus a zero-dependency HTML/CSS variant for non-Remotion pipelines. Both reveal in reading order, emphasize one phrase, fill stars to the real score, and sign off with the author.
|
||||||
|
|
||||||
|
## Timing map (9:16, 30fps, ~10s)
|
||||||
|
|
||||||
|
| Frame | Beat |
|
||||||
|
|---|---|
|
||||||
|
| 0–8 | Quote mark scales in; card settles |
|
||||||
|
| 8–48 | Quote body, line-by-line (8 + i×6) |
|
||||||
|
| 44–54 | Key-phrase highlight sweep (after its line settles) |
|
||||||
|
| 56–70 | Stars fill left→right to `rating/max` |
|
||||||
|
| 72–88 | Author block slides up, holds |
|
||||||
|
| 88–300 | Everything holds still (screenshot-able) |
|
||||||
|
|
||||||
|
The card holds motionless for the back half — stillness is what makes a testimonial read as a fact rather than an ad.
|
||||||
|
|
||||||
|
## Remotion composition
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import {
|
||||||
|
AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate, Img,
|
||||||
|
} from "remotion";
|
||||||
|
|
||||||
|
export type Review = {
|
||||||
|
quote: string; // verbatim testimonial
|
||||||
|
highlight?: string; // exact substring of `quote` to emphasize
|
||||||
|
rating: number; // e.g. 4.5
|
||||||
|
max?: number; // default 5
|
||||||
|
name: string;
|
||||||
|
role?: string;
|
||||||
|
company?: string;
|
||||||
|
avatar?: string; // real photo URL, or omit
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
bg: "#0E1116",
|
||||||
|
ink: "#F5F7FA",
|
||||||
|
muted: "rgba(245,247,250,.55)",
|
||||||
|
accent: "#FFC24B",
|
||||||
|
font: "Inter, system-ui, sans-serif",
|
||||||
|
quoteFont: "'Newsreader', Georgia, serif",
|
||||||
|
quoteMark: "“", // left double quotation mark
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split a quote into clauses for reading-order reveal.
|
||||||
|
// Prefer sentence/clause boundaries; fall back to ~32-char wraps.
|
||||||
|
const toLines = (q: string): string[] => {
|
||||||
|
const parts = q.match(/[^.!?,;]+[.!?,;]?\s*/g) ?? [q];
|
||||||
|
const lines: string[] = [];
|
||||||
|
let buf = "";
|
||||||
|
for (const p of parts) {
|
||||||
|
if ((buf + p).length > 34 && buf) { lines.push(buf.trim()); buf = p; }
|
||||||
|
else buf += p;
|
||||||
|
}
|
||||||
|
if (buf.trim()) lines.push(buf.trim());
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a line, wrapping the highlight substring in <mark> if present.
|
||||||
|
const Line: React.FC<{ text: string; highlight?: string; sweep: number }> = ({
|
||||||
|
text, highlight, sweep,
|
||||||
|
}) => {
|
||||||
|
if (!highlight || !text.includes(highlight)) return <>{text}</>;
|
||||||
|
const [before, after] = text.split(highlight);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{before}
|
||||||
|
<mark
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, ${theme.accent} ${sweep}%, transparent ${sweep}%)`,
|
||||||
|
color: "inherit",
|
||||||
|
padding: "0 .08em",
|
||||||
|
borderRadius: 3,
|
||||||
|
WebkitBoxDecorationBreak: "clone",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{highlight}
|
||||||
|
</mark>
|
||||||
|
{after}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Testimonial: React.FC<Review> = ({
|
||||||
|
quote, highlight, rating, max = 5, name, role, company, avatar,
|
||||||
|
}) => {
|
||||||
|
const frame = useCurrentFrame();
|
||||||
|
const { fps } = useVideoConfig();
|
||||||
|
const lines = toLines(quote);
|
||||||
|
|
||||||
|
// Quote mark
|
||||||
|
const markIn = spring({ frame, fps, config: { damping: 14 } });
|
||||||
|
|
||||||
|
// Highlight sweep, after the body has revealed
|
||||||
|
const hlStart = 8 + lines.length * 6 + 4;
|
||||||
|
const sweep = interpolate(frame, [hlStart, hlStart + 10], [0, 100], {
|
||||||
|
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stars fill, after the quote
|
||||||
|
const fillStart = hlStart + 14;
|
||||||
|
const target = (rating / max) * 100;
|
||||||
|
const starW = interpolate(frame, [fillStart, fillStart + 14], [0, target], {
|
||||||
|
extrapolateLeft: "clamp", extrapolateRight: "clamp",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Author block slides up last
|
||||||
|
const authStart = fillStart + 16;
|
||||||
|
const auth = spring({ frame: frame - authStart, fps, config: { damping: 200 } });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AbsoluteFill style={{ background: theme.bg, fontFamily: theme.font,
|
||||||
|
padding: 120, justifyContent: "center", color: theme.ink }}>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
style={{ fontFamily: theme.quoteFont, fontSize: 200, lineHeight: .6,
|
||||||
|
color: theme.accent, opacity: markIn, transform: `scale(${markIn})`,
|
||||||
|
transformOrigin: "left top" }}
|
||||||
|
>
|
||||||
|
{theme.quoteMark}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<blockquote style={{ fontFamily: theme.quoteFont, fontSize: 64,
|
||||||
|
lineHeight: 1.25, margin: "24px 0 48px", fontWeight: 500 }}>
|
||||||
|
{lines.map((line, i) => {
|
||||||
|
const enter = spring({ frame: frame - (8 + i * 6), fps,
|
||||||
|
config: { damping: 200 } });
|
||||||
|
return (
|
||||||
|
<span key={i} style={{ display: "block", opacity: enter,
|
||||||
|
transform: `translateY(${(1 - enter) * 14}px)` }}>
|
||||||
|
<Line text={line} highlight={highlight} sweep={sweep} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
{/* Stars */}
|
||||||
|
<div role="img" aria-label={`${rating} out of ${max} stars`}
|
||||||
|
style={{ position: "relative", display: "inline-block", fontSize: 52,
|
||||||
|
letterSpacing: 6, marginBottom: 56 }}>
|
||||||
|
<div style={{ color: "rgba(245,247,250,.20)" }}>{"★".repeat(max)}</div>
|
||||||
|
<div style={{ position: "absolute", inset: 0, overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap", width: `${starW}%`, color: theme.accent }}>
|
||||||
|
{"★".repeat(max)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Author */}
|
||||||
|
<figcaption style={{ display: "flex", alignItems: "center", gap: 20,
|
||||||
|
opacity: auth, transform: `translateY(${(1 - auth) * 16}px)` }}>
|
||||||
|
{avatar && (
|
||||||
|
<Img src={avatar} alt=""
|
||||||
|
style={{ width: 88, height: 88, borderRadius: "50%", objectFit: "cover" }} />
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<span style={{ fontSize: 38, fontWeight: 700 }}>{name}</span>
|
||||||
|
<span style={{ fontSize: 30, color: theme.muted }}>
|
||||||
|
{[role, company].filter(Boolean).join(", ")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</figcaption>
|
||||||
|
</AbsoluteFill>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Register it with a duration long enough for the back-half hold:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Composition id="Testimonial" component={Testimonial}
|
||||||
|
durationInFrames={300} fps={30} width={1080} height={1920}
|
||||||
|
defaultProps={{
|
||||||
|
quote: "We shipped in days, not months. Onboarding saved us 10 hours a week.",
|
||||||
|
highlight: "saved us 10 hours a week",
|
||||||
|
rating: 4.5, name: "Dana Reyes", role: "Head of Ops", company: "Northwind",
|
||||||
|
}} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why these choices
|
||||||
|
|
||||||
|
- **`spring` with `damping: 200`** gives a clean rise with no overshoot — testimonials should feel composed, not bouncy. The quote mark uses lighter damping (14) for a touch of life on the one decorative element.
|
||||||
|
- **Highlight as a gradient `<mark>`** wipes a real highlighter behind the phrase without a second element or a mask, and survives line wraps via `box-decoration-break: clone`.
|
||||||
|
- **Stars as two stacked glyph rows clipped by width** render any fractional score (4.5, 4.3) with no half-star asset, and the filled row inherits the brand accent.
|
||||||
|
- **Author after stars** matches reading psychology: claim → proof → who. Reordering weakens trust.
|
||||||
|
|
||||||
|
## Zero-dependency HTML/CSS variant
|
||||||
|
|
||||||
|
For pipelines that capture a headless-browser timeline (or just a web embed). Animations are CSS `@keyframes` with per-line `animation-delay`; if rendering frame-exact video, prefer the Remotion version above so frames stay deterministic.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<figure class="card" style="--accent:#FFC24B; --rating:4.5; --max:5">
|
||||||
|
<div class="qmark" aria-hidden="true">“</div>
|
||||||
|
<blockquote>
|
||||||
|
<span style="--i:0">We shipped in days, not months.</span>
|
||||||
|
<span style="--i:1">Onboarding <mark>saved us 10 hours a week.</mark></span>
|
||||||
|
</blockquote>
|
||||||
|
<div class="stars" role="img" aria-label="4.5 out of 5 stars">
|
||||||
|
<div class="track">★★★★★</div><div class="fill">★★★★★</div>
|
||||||
|
</div>
|
||||||
|
<figcaption>
|
||||||
|
<img src="dana.jpg" alt="" class="avatar">
|
||||||
|
<div><b>Dana Reyes</b><small>Head of Ops, Northwind</small></div>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card { --bg:#0E1116; --ink:#F5F7FA; background:var(--bg); color:var(--ink);
|
||||||
|
font-family:Inter,system-ui,sans-serif; padding:120px; max-width:1080px; }
|
||||||
|
.qmark { font:200px/.6 'Newsreader',Georgia,serif; color:var(--accent);
|
||||||
|
animation:pop .4s cubic-bezier(.22,1,.36,1) both; }
|
||||||
|
blockquote { font:500 64px/1.25 'Newsreader',Georgia,serif; margin:24px 0 48px; }
|
||||||
|
blockquote span { display:block; opacity:0;
|
||||||
|
animation:rise .5s cubic-bezier(.22,1,.36,1) both;
|
||||||
|
animation-delay:calc(.27s + var(--i) * .2s); }
|
||||||
|
mark { background:linear-gradient(90deg,var(--accent) 100%,transparent 100%);
|
||||||
|
color:inherit; padding:0 .08em; border-radius:3px;
|
||||||
|
-webkit-box-decoration-break:clone; box-decoration-break:clone;
|
||||||
|
animation:sweep .35s ease-out both; animation-delay:1s; }
|
||||||
|
@keyframes sweep { from { background-size:0 100% } to { background-size:100% 100% } }
|
||||||
|
.stars { position:relative; display:inline-block; font-size:52px; letter-spacing:6px; }
|
||||||
|
.track { color:rgba(245,247,250,.20); }
|
||||||
|
.fill { position:absolute; inset:0; overflow:hidden; white-space:nowrap;
|
||||||
|
color:var(--accent); width:0; animation:fill .5s ease-out 1.3s forwards; }
|
||||||
|
@keyframes fill { to { width:calc(var(--rating) / var(--max) * 100%) } }
|
||||||
|
figcaption { display:flex; align-items:center; gap:20px; margin-top:56px;
|
||||||
|
opacity:0; animation:rise .5s ease-out 1.8s both; }
|
||||||
|
.avatar { width:88px; height:88px; border-radius:50%; object-fit:cover; }
|
||||||
|
figcaption b { font-size:38px; } figcaption small { display:block; font-size:30px;
|
||||||
|
color:rgba(245,247,250,.55); }
|
||||||
|
@keyframes pop { from { opacity:0; transform:scale(.7) } to { opacity:1; transform:none } }
|
||||||
|
@keyframes rise { from { opacity:0; transform:translateY(14px) } to { opacity:1; transform:none } }
|
||||||
|
```
|
||||||
|
|
||||||
|
The CSS `mark` sweep uses `background-size` (animatable) rather than a gradient color-stop, since color-stop percentages don't animate in CSS. The star `--rating/--max` math is the same fractional fill as the Remotion version.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 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 turn a list of reviews/quotes into testimonial videos, swap text, and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=ad-video-skills&utm_content=ref_footer&utm_term=testimonial-video)
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/ad-creative-video
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/launch-video
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 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>`.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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>
|
||||||
|
```
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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).
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/testimonial-video
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"ad-creative-video": {
|
||||||
|
"source": "iart-ai/ad-video-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/ad-creative-video/SKILL.md",
|
||||||
|
"computedHash": "94c33e3435abd802773c4b7e6058f3d47456bc56354d87b1e6fc2b689757735d"
|
||||||
|
},
|
||||||
|
"launch-video": {
|
||||||
|
"source": "iart-ai/ad-video-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/launch-video/SKILL.md",
|
||||||
|
"computedHash": "a55b82d2ca76de5bb80a1217594b11d1bf1ae4485b487bcad7ab256e2f116790"
|
||||||
|
},
|
||||||
|
"testimonial-video": {
|
||||||
|
"source": "iart-ai/ad-video-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/testimonial-video/SKILL.md",
|
||||||
|
"computedHash": "0d71fa501ffc13fc979d4d93865785c16741c6b4041ccbb3926eaf0da0f486da"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.hyperframes/artifacts/
|
|
||||||
node_modules/
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
<!--
|
|
||||||
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>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
635d4927-f2e8-44dd-9cbf-1b5d4f285f53
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":1,"paths":["renders/video.mp4"]}
|
|
||||||
@@ -1,939 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# .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.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{"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."}}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"preferences": {
|
|
||||||
"style_preset.product-launch-video": {
|
|
||||||
"value": "daisy-days",
|
|
||||||
"confirmed_in": [
|
|
||||||
"lookatu-promo"
|
|
||||||
],
|
|
||||||
"updated_at": "2026-10-14T22:05:31.928Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sightings": {}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
---
|
|
||||||
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
@@ -1,11 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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.
@@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
@@ -1,635 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
|||||||
No assets were captured. No website, logo or screenshots supplied. Use invented phone UI mock-ups and a typographic lookatu wordmark.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{ "title": "lookatu", "description": "Child-safety app (Android and iOS) that lets parents control kids' phone usage and track location when needed.", "colors": [], "fonts": [] }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "lookatu-promo",
|
|
||||||
"name": "lookatu-promo",
|
|
||||||
"createdAt": "2026-10-14T22:04:45.006Z"
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"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