packages/template-prompt-to-motion-graphics/src/skills/typography.md
Always use string slicing for typewriter effects. Never use per-character opacity.
Incorrect (per-character opacity - breaks cursor positioning):
{
text
.split("")
.map((char, i) => (
<span style={{ opacity: i < typedCount ? 1 : 0 }}>{char}</span>
));
}
<span>|</span>;
Correct (string slicing - cursor follows text):
const typedText = FULL_TEXT.slice(0, typedChars);
<span>{typedText}</span>
<span style={{ opacity: caretOpacity }}>▌</span>
Blinking cursors should fade smoothly, not flash on/off abruptly.
Incorrect (abrupt blink):
const caretVisible = Math.floor(frame / 15) % 2 === 0;
<span style={{ opacity: caretVisible ? 1 : 0 }}>|</span>;
Correct (smooth blink):
const CURSOR_BLINK_FRAMES = 16;
const caretOpacity = interpolate(
frame % CURSOR_BLINK_FRAMES,
[0, CURSOR_BLINK_FRAMES / 2, CURSOR_BLINK_FRAMES],
[1, 0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
<span style={{ opacity: caretOpacity }}>▌</span>;
Prevent layout shifts by using the longest word to set container width.
Incorrect (width jumps between words):
<div style={{ position: "relative" }}>
<span>{WORDS[currentIndex]}</span>
</div>
Correct (stable width from longest word):
const longestWord = WORDS.reduce(
(a, b) => (a.length >= b.length ? a : b),
WORDS[0],
);
<div style={{ position: "relative" }}>
<div style={{ visibility: "hidden" }}>{longestWord}</div>
<div style={{ position: "absolute", left: 0, top: 0 }}>
{WORDS[currentIndex]}
</div>
</div>;
Use overlapping layers for smooth highlight transitions.
const typedOpacity = interpolate(
frame,
[highlightStart - 8, highlightStart + 8],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
const finalOpacity = interpolate(
frame,
[highlightStart, highlightStart + 8],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
{
/* Typing layer */
}
<div style={{ opacity: typedOpacity }}>{typedText}</div>;
{
/* Final layer with highlight */
}
<div style={{ position: "absolute", inset: 0, opacity: finalOpacity }}>
<span>{preText}</span>
<span style={{ backgroundColor: COLOR_HIGHLIGHT }}>{HIGHLIGHT_WORD}</span>
<span>{postText}</span>
</div>;