Files
Soroush Asadi 4ffbcac9ee refactor: bundle the whole template suite under flat-artist/ + fix references
flat-artist is now the single container: all 16 template skills + the R&D
references/ moved inside flat-artist/. Cross-references updated — the orchestrator
points to bundled `<name>/SKILL.md`, sub-skills point to `../<name>/SKILL.md`,
and the R&D report path is relative. README catalog updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:31:53 +03:30

8.4 KiB
Raw Permalink Blame History

name, description
name description
kinetic-typography How to build animated-text systems for FlatRender Remotion templates — word/line/char reveals, mask wipes, typewriter, scale-pops, highlight sweeps, text-on-path, and number counters — Persian/RTL-aware and reusable. Use whenever a template's hero, caption, quote, title, price, or any text is the thing that moves. Persian is the priority; split by WORD, never by character.

Kinetic typography (animated text systems)

Type is a first-class motion element here, not a label. A masterpiece text shot is ~5 layers: the right split, eased per-unit timing, a hold sized to a real read, legibility over the background, and a single hero word. Amateurs stop at "the text fades in."

The one rule

Every value is a pure function of useCurrentFrame(). Never useFrame, Math.random(), Date.now(), setState, or useEffect-driven motion — the headless renderer samples frames out of order. For "random" jitter use rand(seed) from lib/anim.ts. Drive timing off useVideoConfig().fps; define const sec = (s: number) => Math.round(s * fps) — never hardcode 30.

Persian / RTL — get this right first (it's an Iran-facing product)

  • Split by WORD, not character. Persian script is connected/cursive — splitting on chars shatters letterforms and joins. Latin char-reveals are fine; Persian is word- or line-only. A safe split is text.split(/\s+/).filter(Boolean) — this preserves ZWNJ (نیم‌فاصله, ) inside words like «می‌شود» because ZWNJ is not whitespace. Never .split("") or .replace(//g, …) on Persian.
  • Every text node: fontFamily: FONT (Vazirmatn, from lib/fonts.ts), direction: "rtl", align right or center. The existing KineticQuote.tsx hardcodes Georgia/serif + pixel sizes + no RTL — do not copy that; it's a Latin-only relic.
  • Persian needs weight (headings 700900) and lineHeight: 1.41.6. Numerals: pick Persian (۱۲۳ via toLocaleString('fa-IR')) or Latin and stay consistent; prices/years are usually Persian digits. See ../persian-fonts/SKILL.md.
  • For RTL word reveals, the wrapping container does the ordering — keep flexWrap: "wrap" + direction: "rtl" and let words flow; don't manually reverse the array.

Size & position from layout tokens, never pixels

Read useLayout() from lib/aspect.ts: vmin(n), unit, isWide/isSquare/isTall. Hero type ≈ vmin(80110), body ≈ vmin(2840). Tune timing/scale per aspect — wider reads faster (tighter stagger), tall reads slower (looser). Add this pick helper to Layout (per R&D Tier-0) and use it:

const pick = <T,>(w: T, s: T, t: T) => (L.isWide ? w : L.isSquare ? s : t);
const stagger = pick(2, 3, 4); // frames between units

Animation patterns (all driven by frame - start)

Pattern Recipe Persian-safe?
Word reveal (default) split words; per word start = i*stagger; spring({frame: frame-start, fps})translateY(vmin) + opacity word-split
Line reveal wrap by line in <Sequence>s; each line springs up behind a clip-path edge
Char reveal / scatter split chars, per-char delay; rotate/scale in Latin only
Mask wipe clipPath: inset(0 ${100-p}% 0 0) (RTL: wipe from right → 0 0 0 ${100-p}%); p = interpolate(frame,[a,b],[0,100],{extrapolateRight:"clamp", easing: Easing.out(Easing.cubic)})
Typewriter text.slice(0, Math.floor(interpolate(frame,[a,b],[0, words.length]))) joined — slice by WORD for Persian, by char only for Latin; add a blinking caret frame % sec(0.8) < sec(0.4) word-slice
Scale-pop ("ta-da") scale = spring({config:{damping:12,mass:0.6,stiffness:180}}) or Easing.bezier(0.34,1.56,0.64,1) overshoot→settle
Highlight sweep gradient bar/background-clip:text shifting background-position per frame, or an accent rect growing under a key word
Text-on-path SVG <textPath href="#p">; animate startOffset by frame — Latin/numeric only (RTL on a path is unreliable)
Number counter Math.round(interpolate(frame,[a,b],[0, target],{extrapolateRight:"clamp", easing: Easing.out(Easing.cubic)})) then toLocaleString('fa-IR') (format fa)
Variable-weight pulse Vazirmatn ships a variable axis: fontVariationSettings: \'wght' ${interpolate(frame,[a,b],[300,900])}`(needs the variable woff2 registered infonts.ts`)

Reusable word-reveal component (the workhorse — Persian-correct, aspect-aware)

const RevealText: React.FC<{ text: string; start: number; color: string }> = ({ text, start, color }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const L = useLayout();
  const pick = <T,>(w: T, s: T, t: T) => (L.isWide ? w : L.isSquare ? s : t);
  const stagger = pick(2, 3, 4);
  const words = text.split(/\s+/).filter(Boolean); // keeps ZWNJ
  return (
    <div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center",
      direction: "rtl", fontFamily: FONT, fontWeight: 800, fontSize: L.vmin(96),
      lineHeight: 1.4, color, gap: `0 ${L.vmin(18)}px`, maxWidth: "86%",
      textShadow: `0 ${L.vmin(2)}px ${L.vmin(20)}px rgba(0,0,0,.6)` }}>
      {words.map((w, i) => {
        const s = spring({ frame: frame - start - i * stagger, fps,
          config: { damping: 16, mass: 0.7, stiffness: 120 } });
        return (
          <span key={i} style={{ display: "inline-block", opacity: s,
            transform: `translateY(${interpolate(s, [0, 1], [L.vmin(28), 0])}px)` }}>
            {w}
          </span>
        );
      })}
    </div>
  );
};

Follow-through upgrade: give a trailing accent word a looser spring (damping: 6) so it settles last.

Easing & spring (linear is the sound of an amateur)

  • Entrances → ease-out default (Easing.out(Easing.cubic)); hero titles → Easing.bezier(0.16,1,0.3,1). Exits → ease-in, sharper than the entrance. Snappy pop → back bezier (0.34,1.56,0.64,1).
  • interpolate for exact marks — always extrapolateLeft/Right: "clamp" (forgetting it is the #1 drift bug). spring for organic feel. Combine: interpolate(spring(...), [0,1], [vmin(28), 0]).
  • Spring cheats: clean reveal {damping:200,mass:0.5,stiffness:200} · default pop {damping:12,mass:0.6,stiffness:180} · bouncy {damping:8,mass:1,stiffness:120} · trailing wobble {damping:6,mass:1,stiffness:80}.

Timing budgets (@ whatever fps is)

Micro pop 814f · word stagger 24f · standard reveal 1828f · hero entrance 2840f · hold = a comfortable read (≥ sec(0.7) per text element before the next competes). Cut frames before adding them — over-animating reads as amateur. Anticipation: dip below start before launch (interpolate(frame,[0,6,30],[0,-0.12,1])).

Legibility over busy / 3D / video backgrounds

  • Scrim or textShadow: 0 0 vmin(20) rgba(0,0,0,.7), or a semi-transparent panel behind text.
  • Gradient text: WebkitBackgroundClip: "text", transparent fill, plus a drop-shadow for edge separation.
  • Colors come from colorSchema props (accentColor/secondaryColor/backgroundColor/textColor via lib/branding.ts) — pass user hex through mixHex/hexToRgba so a garish value doesn't break the look. Never hardcode #fff.
  • Captions (TikTok/Reels/Shorts) = high-contrast white/yellow + black outline, lower-middle third, inside the tightest safe zone. See ../remotion-aspect-ratios/SKILL.md.

Checklist

  • Persian text split by WORD; ZWNJ preserved; direction:"rtl" + fontFamily: FONT.
  • All sizes via vmin/unit; timing/stagger via pick(...) per aspect — verified in 16:9, 1:1, 9:16.
  • No linear easing; ≥1 overshoot-and-settle; staggered, not all on frame 0.
  • Every interpolate clamps both ends; no useFrame/random/Date.now; fps not 30.
  • Numbers formatted (fa-IR) and consistent; counter eases out.
  • Legible over the background (scrim/shadow); colors from props.
  • A real hold sized to reading; longest Persian string doesn't overflow, shortest doesn't look empty.
  • Re-render twice → identical pixels (deterministic).

Related: ../remotion-template-composition/SKILL.md, ../persian-fonts/SKILL.md, ../remotion-aspect-ratios/SKILL.md, ../remotion-design-styles/SKILL.md, ../remotion-svg-colors/SKILL.md, ../remotion-sound-effects/SKILL.md, ../remotion-music-picker/SKILL.md, ../flatrender-template-seo/SKILL.md.