Add TikTok video skills (caption-animation, countdown-video, lower-thirds, short-form-video)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011XhxckAYjGMLrLECWuKBzg
This commit is contained in:
18 files changed
+1842
No files matched your search
@@ -0,0 +1,24 @@
|
||||
# Lower Thirds
|
||||
|
||||
Build a clean, branded lower third — the name/role caption that lives in the bottom band of the frame — that animates in, holds long enough to read, and exits without stealing the shot. The craft is restraint: a crisp reveal, a strong type hierarchy, correct safe-area placement, and one template that drives a whole roster of speakers. For creators producing interviews, panels, podcasts, and broadcast-style titles.
|
||||
|
||||
## When it activates
|
||||
|
||||
- "Make an animated lower third" or "add lower thirds."
|
||||
- "Create a name tag/name lower third" or "do a speaker name overlay."
|
||||
- "Build broadcast titles" or "animate a title/name/role caption."
|
||||
- "Template lower thirds across a list of names."
|
||||
|
||||
## Example prompts
|
||||
|
||||
- "Add a lower third with the speaker's name and role, branded to my colors."
|
||||
- "Build a staggered slide-in name plate for a podcast guest."
|
||||
- "Template one lower-third design across a roster of 12 panelists."
|
||||
|
||||
## What's inside
|
||||
|
||||
- `SKILL.md` — enter/exit animation, in/out timing and dwell, safe-area conventions, and brand styling.
|
||||
- `references/lower-third-component.md` — a reusable lower-third component.
|
||||
|
||||
---
|
||||
Part of **[Short-Form Video Skills](../../)** · Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=tiktok-video-skills&utm_content=skill_footer&utm_term=lower-thirds)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: lower-thirds
|
||||
description: This skill should be used when the user asks to "make an animated lower third", "add lower thirds", "create a name tag/name lower third", "build broadcast titles", "animate a title/name/role caption", "do a speaker name overlay", or "template lower thirds across a list of names". Covers enter/exit animation (slide/wipe/mask reveal), in/out timing & dwell, safe-area & broadcast conventions, name-vs-role type hierarchy, brand styling, and rendering one design across many names.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Lower Thirds
|
||||
|
||||
Build a clean, branded lower third — the name/role caption that lives in the bottom band of the frame — that animates in, holds long enough to read, and exits without stealing the shot. The craft is restraint: a crisp reveal, a strong type hierarchy, correct safe-area placement, and one template that drives a whole roster of speakers.
|
||||
|
||||
## When to use
|
||||
|
||||
- Speaker/interview name tags, panel rosters, talking-head captions.
|
||||
- Broadcast-style titles, news straps, podcast/webinar name plates.
|
||||
- Templating one design across a list of names/titles → many clips (the payoff).
|
||||
|
||||
## Anatomy
|
||||
|
||||
A lower third is two text lines plus a graphic bed, placed in the lower band:
|
||||
|
||||
| Element | Role | Type treatment |
|
||||
|---|---|---|
|
||||
| **Name** (primary) | Who | Largest, heaviest, tightest tracking |
|
||||
| **Role/title** (secondary) | What/where | ~45–60% of name size, lighter weight, often a brand accent color |
|
||||
| **Bar / panel / accent** | Reads against any background | Solid or 70–85% opacity; an accent rule ties to brand |
|
||||
|
||||
Keep it to two lines. A name line + role line is the whole job — extra lines erode the hierarchy and crowd the safe area.
|
||||
|
||||
## Three rules that prevent cheap-looking titles
|
||||
|
||||
1. **Never hard-cut a title on or off.** A bare appear/disappear reads as a mistake. Even a 12-frame fade or slide makes it feel produced.
|
||||
2. **Stagger, don't slam.** Animate the bar first, then the name, then the role, each offset by 3–6 frames. Everything arriving at once feels flat; a short cascade feels designed.
|
||||
3. **Hold long enough to read twice.** Roughly 3–5s on screen (read it aloud twice). Too short and it's missed; too long and it nags. Animate in/out, never mid-segment.
|
||||
|
||||
## Enter & exit animations
|
||||
|
||||
Pick one motion language and reuse it. The most reliable, premium-reading reveals:
|
||||
|
||||
| Reveal | Feel | How |
|
||||
|---|---|---|
|
||||
| **Slide + fade** | Clean, default | Translate in ~24–40px while fading; ease-out on enter |
|
||||
| **Wipe bar** | Broadcast | A bar scales/sweeps in (`scaleX`/clip), text fades behind it |
|
||||
| **Mask reveal** | Premium | Clip text to a moving rectangle so letters "wipe on" |
|
||||
|
||||
Use asymmetric easing: a snappy `ease-out` (`cubic-bezier(.22,1,.36,1)`) on enter, a quicker `ease-in` on exit. Exit faster than you entered (~⅔ the duration) so the title gets out of the way.
|
||||
|
||||
```css
|
||||
/* Mask reveal: text wipes on left→right, no plugin needed */
|
||||
.lt-name { clip-path: inset(0 100% 0 0); animation: wipe .5s cubic-bezier(.22,1,.36,1) forwards; }
|
||||
@keyframes wipe { to { clip-path: inset(0 0 0 0); } }
|
||||
```
|
||||
|
||||
## In/out timing & dwell
|
||||
|
||||
Think in three phases. Budget frames, not vibes (at 30fps):
|
||||
|
||||
| Phase | Budget | Note |
|
||||
|---|---|---|
|
||||
| Enter | 12–18f (0.4–0.6s) | Staggered: bar → name → role |
|
||||
| Hold | 90–150f (3–5s) | The readable dwell; scale with line length |
|
||||
| Exit | 8–12f (0.27–0.4s) | Faster than enter |
|
||||
|
||||
In code, drive every state from the current frame (or a paused timeline), not wall-clock time — a video renderer demands deterministic frames. Express enter/hold/exit as a single progress curve so the same component works live in a browser and frame-by-frame in a renderer.
|
||||
|
||||
## Safe area & broadcast conventions
|
||||
|
||||
The lower third must survive every screen and platform UI. Compose inside the **title-safe area** (inner ~90% / margin 5%) and respect platform chrome:
|
||||
|
||||
| Frame | Anchor | Avoid |
|
||||
|---|---|---|
|
||||
| 16:9 (1920×1080) | Left edge at ~5% margin; baseline ~80–85% down | Outer 5% (title-safe), action-safe outer 10% |
|
||||
| 9:16 (1080×1920) | Same left margin; **lift the band up** | Bottom ~18% (captions/UI), top ~12% |
|
||||
| 1:1 (1080×1080) | Lower band, center-left | Outer 5% |
|
||||
|
||||
Anchor to the **left** and grow rightward so names of different lengths stay pinned and never re-center awkwardly. Keep the role line within the same left margin as the name. For 9:16, do not park the title at the very bottom — platform captions and buttons sit there.
|
||||
|
||||
## Type hierarchy & brand styling
|
||||
|
||||
Hierarchy is the whole design. Make the name unmistakably dominant:
|
||||
|
||||
- **Size contrast** ≥ 1.6× (name vs role). If unsure, push the name bigger, not the role.
|
||||
- **Weight + color**: name in bold/near-white; role in a lighter weight and a **brand accent** color (one accent, used consistently).
|
||||
- **Contrast for legibility**: text rides over unknown footage. Use a solid/semi-opaque bar, a subtle scrim, or a soft text shadow so it reads over both bright and dark plates.
|
||||
- **Brand lock**: pin font, accent color, bar style, and corner radius in one theme object so every title across a series matches.
|
||||
|
||||
## Templating one design across a list
|
||||
|
||||
The reason to build this in code: change the text/data, re-export — no re-animating. Make name/role/theme **props**, never hardcoded, then map a roster to instances.
|
||||
|
||||
```jsx
|
||||
const ROSTER = [
|
||||
{ name: "Ada Lovelace", role: "Founder, Analytical Engines" },
|
||||
{ name: "Grace Hopper", role: "Rear Admiral, US Navy" },
|
||||
{ name: "Alan Turing", role: "Mathematician" },
|
||||
];
|
||||
// One <LowerThird> design; map the list → many instances (sequence them in a renderer)
|
||||
{ROSTER.map((p, i) => <LowerThird key={p.name} {...p} theme={BRAND} startFrame={i * 150} />)}
|
||||
```
|
||||
|
||||
In a renderer, make the roster an input prop and render once per row (`--props`) to emit one branded clip per person — same animation, same brand, only the text changes. See `references/lower-third-component.md` for the full runnable component and batch script.
|
||||
|
||||
## Output checklist
|
||||
|
||||
- Two lines only: dominant name, secondary role (≥1.6× size contrast).
|
||||
- Animated in and out (no hard cut); enter staggered bar→name→role; exit faster.
|
||||
- Hold 3–5s (readable twice); motion only at the ends, never mid-segment.
|
||||
- Left-anchored inside title-safe; 9:16 band lifted above the bottom-18% UI zone.
|
||||
- Text legible over any plate (bar/scrim/shadow); one brand accent throughout.
|
||||
- Name/role/theme are props — one template renders the whole roster.
|
||||
|
||||
## Deliver & verify (rendered stills → MP4)
|
||||
|
||||
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
|
||||
|
||||
The lower third ships as a Remotion composition (`<Composition>` + zod `schema` + `defaultProps`, name/role/theme as props) — enter/dwell/exit all driven by `useCurrentFrame()`, never `Date.now()` / `Math.random()` / timers. Deliverable = `out/*.mp4` + the project (re-render per roster row). 9:16 vertical (1080×1920) is the default.
|
||||
|
||||
**Verify loop — render stills → inspect → encode.** Sample the three phases so you catch a stagger or safe-area bug before encoding (or batching the whole roster).
|
||||
|
||||
```bash
|
||||
# Stills at enter / dwell / exit — WITH SHIPPED PROPS (a real roster row, not defaults)
|
||||
npx remotion still LowerThird out/f-enter.png --frame=10 --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
npx remotion still LowerThird out/f-dwell.png --frame=75 --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
npx remotion still LowerThird out/f-exit.png --frame=N --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
|
||||
# Inspect each PNG — focus the DWELL frame (fully on screen):
|
||||
# - hierarchy holds: name dominant (>=1.6x role), role in brand accent; both fully revealed, not mid-wipe
|
||||
# - brand lock: font, accent color, bar/scrim correct; legible over the plate
|
||||
# - 9:16 safe area: band LIFTED above the bottom ~20-35% UI zone, clear of top ~12% and the right action rail; left-anchored inside title-safe
|
||||
# - enter/exit frames show partial reveal (animation actually runs), not a hard cut
|
||||
|
||||
npx remotion render LowerThird out/lower-third.mp4 --props='{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}'
|
||||
npx remotion render LowerThird out/demo.gif --codec=gif # README proof clip
|
||||
```
|
||||
|
||||
**Batch roster**: verify ONE row via stills *before* rendering all names — catch a layout/overflow bug once, not N times. Use `npx remotion compositions` for `durationInFrames`/`fps` to pick the dwell + exit frames.
|
||||
|
||||
**Before you finish:**
|
||||
1. Stills render cleanly at enter, dwell, and exit — no missing font/brand assets.
|
||||
2. At the dwell frame: name dominant over role, brand accent + bar correct, fully (not partly) revealed.
|
||||
3. The band is inside the 9:16 safe area — lifted above the bottom UI zone, clear of top and right rail — at every checked frame.
|
||||
4. Frame-driven only — no `Date.now()` / `Math.random()` / timers; enter/exit show motion, never a hard cut.
|
||||
5. A real roster row (not `defaultProps`) renders correctly; MP4 encoded, GIF optional.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/lower-third-component.md` — a complete runnable lower-third: a Remotion component with staggered spring enter, readable hold, and faster exit driven purely by frame; a typed theme object for brand lock; the list→many-instances sequencing pattern; a per-row batch render script; plus a dependency-free CSS/HTML version of the same design with mask-reveal and wipe-bar variants.
|
||||
@@ -0,0 +1,268 @@
|
||||
# Lower-Third Component — runnable code
|
||||
|
||||
A complete, production-grade lower third you can drop in. Two paths:
|
||||
|
||||
1. **Remotion** (frame-deterministic, renders to MP4, batches over a roster).
|
||||
2. **CSS/HTML** (no dependencies, for the web or a quick preview) with mask-reveal and wipe-bar variants.
|
||||
|
||||
Both share the same design language: a left-anchored bar, a dominant name, a brand-accent role line, a staggered enter, a readable hold, and a faster exit.
|
||||
|
||||
---
|
||||
|
||||
## 1. Remotion component (props-driven, frame-deterministic)
|
||||
|
||||
The animation is a pure function of `frame`, so it renders identically every time and stays in sync with the renderer. Enter staggers the bar, name, then role; exit is computed from the end of the clip so the title leaves faster than it arrived.
|
||||
|
||||
```tsx
|
||||
// LowerThird.tsx
|
||||
import {
|
||||
AbsoluteFill, Sequence, interpolate, spring,
|
||||
useCurrentFrame, useVideoConfig,
|
||||
} from "remotion";
|
||||
|
||||
// --- Brand lock: change once, every title matches -------------------------
|
||||
export type Theme = {
|
||||
font: string;
|
||||
bar: string; // panel/bar color
|
||||
name: string; // primary text color
|
||||
role: string; // accent color for the role line
|
||||
radius: number;
|
||||
marginPct: number; // left/safe margin as % of width
|
||||
};
|
||||
export const BRAND: Theme = {
|
||||
font: "Inter, system-ui, sans-serif",
|
||||
bar: "rgba(12,14,22,0.86)",
|
||||
name: "#FFFFFF",
|
||||
role: "#6EE7F0", // single brand accent
|
||||
radius: 10,
|
||||
marginPct: 5,
|
||||
};
|
||||
|
||||
export type LowerThirdProps = {
|
||||
name: string;
|
||||
role: string;
|
||||
theme?: Theme;
|
||||
holdSeconds?: number; // readable dwell, default 4s
|
||||
};
|
||||
|
||||
// Staggered enter via spring; exit via a quick fade/slide near the end.
|
||||
const useEnterExit = (delayFrames: number, holdSeconds: number) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps, durationInFrames } = useVideoConfig();
|
||||
|
||||
// ENTER: spring 0→1, offset by delayFrames (the cascade)
|
||||
const enter = spring({
|
||||
frame: frame - delayFrames,
|
||||
fps,
|
||||
config: { damping: 200, stiffness: 180, mass: 0.6 },
|
||||
});
|
||||
|
||||
// EXIT: faster than enter — last 10 frames of the clip
|
||||
const exitStart = durationInFrames - 10;
|
||||
const exit = interpolate(frame, [exitStart, durationInFrames], [1, 0], {
|
||||
extrapolateLeft: "clamp",
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
const opacity = enter * exit;
|
||||
const x = interpolate(enter, [0, 1], [-32, 0]); // slide in from left
|
||||
return { opacity, x };
|
||||
};
|
||||
|
||||
const Line: React.FC<{
|
||||
delay: number; holdSeconds: number; children: React.ReactNode;
|
||||
size: number; weight: number; color: string; tracking?: number;
|
||||
}> = ({ delay, holdSeconds, children, size, weight, color, tracking = 0 }) => {
|
||||
const { opacity, x } = useEnterExit(delay, holdSeconds);
|
||||
return (
|
||||
<div style={{
|
||||
opacity,
|
||||
transform: `translateX(${x}px)`,
|
||||
fontSize: size, fontWeight: weight, color,
|
||||
letterSpacing: tracking, lineHeight: 1.15, whiteSpace: "nowrap",
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LowerThird: React.FC<LowerThirdProps> = ({
|
||||
name, role, theme = BRAND, holdSeconds = 4,
|
||||
}) => {
|
||||
const { width, height } = useVideoConfig();
|
||||
const bar = useEnterExit(0, holdSeconds); // bar leads the cascade
|
||||
|
||||
const left = (width * theme.marginPct) / 100;
|
||||
const baseline = height * 0.80; // sit ~80% down, inside title-safe
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ fontFamily: theme.font }}>
|
||||
<div style={{
|
||||
position: "absolute", left, top: baseline,
|
||||
opacity: bar.opacity,
|
||||
transform: `translateX(${bar.x}px)`,
|
||||
}}>
|
||||
{/* panel behind text guarantees legibility over any plate */}
|
||||
<div style={{
|
||||
background: theme.bar, borderRadius: theme.radius,
|
||||
padding: "14px 22px 16px",
|
||||
borderLeft: `4px solid ${theme.role}`, // accent rule ties to brand
|
||||
backdropFilter: "blur(6px)",
|
||||
}}>
|
||||
<Line delay={4} holdSeconds={holdSeconds} size={46} weight={800} color={theme.name} tracking={-0.5}>
|
||||
{name}
|
||||
</Line>
|
||||
<Line delay={8} holdSeconds={holdSeconds} size={26} weight={500} color={theme.role}>
|
||||
{role}
|
||||
</Line>
|
||||
</div>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Why each choice matters:
|
||||
- **`spring` per line, offset by `delay`** gives the bar→name→role cascade. Same offsets everywhere keep titles consistent.
|
||||
- **Exit derived from `durationInFrames`** means any clip length exits cleanly without per-instance tuning, and it's quicker than the enter (10f vs the spring's settle).
|
||||
- **`whiteSpace: nowrap` + left anchor** keeps long and short names pinned to the same edge — no awkward re-centering across a roster.
|
||||
- **`borderLeft` accent + semi-opaque panel** is the brand tie plus the legibility guarantee over unknown footage.
|
||||
|
||||
### Roster → many instances (one design, a whole panel)
|
||||
|
||||
Sequence the same component down a list. Each person gets a slot; only the text changes.
|
||||
|
||||
```tsx
|
||||
// Roster.tsx
|
||||
import { Series } from "remotion";
|
||||
import { LowerThird, BRAND } from "./LowerThird";
|
||||
|
||||
const ROSTER = [
|
||||
{ name: "Ada Lovelace", role: "Founder, Analytical Engines" },
|
||||
{ name: "Grace Hopper", role: "Rear Admiral, US Navy" },
|
||||
{ name: "Alan Turing", role: "Mathematician" },
|
||||
];
|
||||
|
||||
export const Roster: React.FC = () => (
|
||||
<Series>
|
||||
{ROSTER.map((p) => (
|
||||
<Series.Sequence key={p.name} durationInFrames={150 /* ~5s @30fps */}>
|
||||
<LowerThird name={p.name} role={p.role} theme={BRAND} />
|
||||
</Series.Sequence>
|
||||
))}
|
||||
</Series>
|
||||
);
|
||||
```
|
||||
|
||||
### Batch: one branded clip per person
|
||||
|
||||
Make the roster an input prop and render once per row, so each speaker becomes a standalone overlay clip — identical animation and brand, only the text differs.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# render one transparent-friendly lower-third clip per person in roster.json
|
||||
# roster.json: [{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}, ...]
|
||||
set -euo pipefail
|
||||
mkdir -p out
|
||||
jq -c '.[]' roster.json | while read -r row; do
|
||||
slug=$(echo "$row" | jq -r '.name' | tr '[:upper:] ' '[:lower:]-')
|
||||
npx remotion render LowerThird "out/${slug}.mp4" --props="$row"
|
||||
done
|
||||
```
|
||||
|
||||
For overlay-ready output with transparency, render the `LowerThird` composition (transparent background) to a codec that keeps alpha, e.g. `--codec=prores --prores-profile=4444`, or `--image-format=png` for a PNG sequence.
|
||||
|
||||
---
|
||||
|
||||
## 2. CSS / HTML version (no dependencies)
|
||||
|
||||
The same design without a framework — for the web, OBS browser sources, or a fast look-test. Includes both a **slide+fade** default and a **mask-reveal** name, plus a **wipe-bar** variant.
|
||||
|
||||
```html
|
||||
<div class="lt" style="--accent:#6EE7F0">
|
||||
<div class="lt-bar">
|
||||
<div class="lt-name">Ada Lovelace</div>
|
||||
<div class="lt-role">Founder, Analytical Engines</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:root { font-family: Inter, system-ui, sans-serif; }
|
||||
|
||||
.lt {
|
||||
position: absolute; left: 5%; top: 80%; /* title-safe, left-anchored */
|
||||
}
|
||||
.lt-bar {
|
||||
display: inline-block;
|
||||
background: rgba(12,14,22,0.86);
|
||||
backdrop-filter: blur(6px);
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 14px 22px 16px;
|
||||
/* bar leads the cascade: slide + fade in, then a faster fade out */
|
||||
animation: lt-in .5s cubic-bezier(.22,1,.36,1) both,
|
||||
lt-out .3s ease-in 4.5s forwards; /* hold ~4s, exit faster */
|
||||
}
|
||||
.lt-name {
|
||||
font-size: 46px; font-weight: 800; color: #fff; letter-spacing: -.5px;
|
||||
line-height: 1.15; white-space: nowrap;
|
||||
/* MASK REVEAL: letters wipe on left→right, staggered after the bar */
|
||||
clip-path: inset(0 100% 0 0);
|
||||
animation: lt-wipe .5s cubic-bezier(.22,1,.36,1) .12s forwards;
|
||||
}
|
||||
.lt-role {
|
||||
font-size: 26px; font-weight: 500; color: var(--accent);
|
||||
line-height: 1.15; white-space: nowrap;
|
||||
opacity: 0;
|
||||
animation: lt-fade .4s ease-out .22s forwards; /* role arrives last */
|
||||
}
|
||||
|
||||
@keyframes lt-in { from { opacity: 0; transform: translateX(-32px); }
|
||||
to { opacity: 1; transform: none; } }
|
||||
@keyframes lt-out { to { opacity: 0; transform: translateX(-16px); } }
|
||||
@keyframes lt-wipe { to { clip-path: inset(0 0 0 0); } }
|
||||
@keyframes lt-fade { to { opacity: 1; } }
|
||||
|
||||
/* Mobile/9:16: lift the band above the bottom UI/caption zone */
|
||||
@media (max-aspect-ratio: 1/1) { .lt { top: 70%; } }
|
||||
</style>
|
||||
```
|
||||
|
||||
### Wipe-bar variant (broadcast feel)
|
||||
|
||||
Swap the bar animation so a colored bar sweeps in and the text rides behind it:
|
||||
|
||||
```css
|
||||
.lt-bar.wipe {
|
||||
position: relative; overflow: hidden;
|
||||
animation: lt-in .45s cubic-bezier(.22,1,.36,1) both,
|
||||
lt-out .3s ease-in 4.5s forwards;
|
||||
}
|
||||
.lt-bar.wipe::before {
|
||||
content: ""; position: absolute; inset: 0;
|
||||
background: var(--accent);
|
||||
transform-origin: left;
|
||||
animation: lt-sweep .55s cubic-bezier(.76,0,.24,1) both; /* sweep across, then reveal */
|
||||
}
|
||||
@keyframes lt-sweep { 0% { transform: scaleX(0); } 60% { transform: scaleX(1); }
|
||||
100% { transform: scaleX(0); transform-origin: right; } }
|
||||
```
|
||||
|
||||
The CSS clock (`animation`) is fine for live/web playback. For a deterministic MP4, prefer the Remotion path above — a frame renderer and CSS timers disagree, which causes flicker or desync at export.
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Knob |
|
||||
|---|---|
|
||||
| Slower/faster reveal | enter duration 12–18f / `.5s` |
|
||||
| Longer dwell | `holdSeconds` / the `4.5s` exit delay |
|
||||
| Bigger name dominance | name `fontSize` (keep ≥1.6× the role) |
|
||||
| Rebrand everything | the `BRAND` theme object / `--accent` |
|
||||
| 9:16 placement | `baseline 0.80→0.70` / the media query |
|
||||
|
||||
---
|
||||
## Built by the team behind iart.ai
|
||||
|
||||
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can generate branded lower-thirds from a list of names/titles, one-click — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=lower-thirds)
|
||||
Reference in new issue
Block a user