Typography — New Text Animations
Install this video
Adds the full composition, its remocn dependencies, and the prompt to your Remotion project via the shadcn registry.
$ pnpm dlx shadcn@latest add kapishdima/remocn-demo/typographyRender it locally
Renders the MP4 on your machine with the Remotion CLI.
$ pnpm dlx remotion render typography out/typography.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95The prompt
Reconstructed draftA reconstruction of the prompt this video was generated from.
Make a showcase video for the new text animations we added to remocn. The gimmick should be that each animation introduces itself by animating its own name — so the typewriter effect types out "typewriter", the split-text effect splits "split-text", and so on down the list. Put it over an image backdrop rather than a flat color, and keep the transitions between each one dynamic and kinetic so it doesn't feel like a static list. Keep it snappy, no long holds, just effect after effect proving itself. Use remocn's actual text animation components for all of it.
The code
The exact source the AI wrote — the same files the install command puts in your project.
import React, { type ReactNode } from "react";
import { AbsoluteFill, Easing, Loop, Sequence, interpolate, useCurrentFrame } from "remotion";
import { demoAsset } from "@/lib/demo-assets";
import {
TransitionSeries,
linearTiming,
type TransitionPresentation,
} from "@remotion/transitions";
import { fade, type FadeProps } from "@remotion/transitions/fade";
import { loadFont } from "@remotion/google-fonts/Manrope";
import { loadFont as loadMono } from "@remotion/google-fonts/JetBrainsMono";
import { RemocnUIProvider } from "@/lib/remocn-ui";
import { Backdrop } from "@/components/remocn/backdrop";
// Typography animations — the cast of this showcase.
import { SoftBlurIn } from "@/components/remocn/soft-blur-in";
import { Typewriter } from "@/components/remocn/typewriter";
import { FocusBlurResolve } from "@/components/remocn/focus-blur-resolve";
import { PerCharacterRise } from "@/components/remocn/per-character-rise";
import { TopDownLetters } from "@/components/remocn/top-down-letters";
import { BottomUpLetters } from "@/components/remocn/bottom-up-letters";
import { SpringScaleIn } from "@/components/remocn/spring-scale-in";
import { MicroScaleFade } from "@/components/remocn/micro-scale-fade";
import { ScaleDownFade } from "@/components/remocn/scale-down-fade";
import { MaskRevealUp } from "@/components/remocn/mask-reveal-up";
import { LineByLineSlide } from "@/components/remocn/line-by-line-slide";
import { KineticCenterBuild } from "@/components/remocn/kinetic-center-build";
import { ShortSlideDown } from "@/components/remocn/short-slide-down";
import { ShortSlideRight } from "@/components/remocn/short-slide-right";
import { BlurOutUp } from "@/components/remocn/blur-out-up";
import { FadeThrough } from "@/components/remocn/fade-through";
import { SharedAxisY } from "@/components/remocn/shared-axis-y";
import { SharedAxisZ } from "@/components/remocn/shared-axis-z";
import { PerWordCrossfade } from "@/components/remocn/per-word-crossfade";
// Manrope, bound to the CSS variable every remocn typography component reads.
const { fontFamily } = loadFont("normal", {
subsets: ["latin"],
weights: ["400", "500", "600", "700", "800"],
});
const FONT_STACK = `${fontFamily}, sans-serif`;
const INK = "#fafafa";
// Monospace for the install command pill.
const { fontFamily: monoFamily } = loadMono("normal", {
subsets: ["latin"],
weights: ["400", "500"],
});
const MONO_STACK = `${monoFamily}, ui-monospace, SFMono-Regular, Menlo, monospace`;
// Global slow-down for the full-screen demos. Components advance their internal
// clock by `useCurrentFrame() * speed`, so < 1 plays the motion more slowly and
// gives each animation room to breathe.
const SPEED = 0.85;
// Trims the held tail of each montage scene so the faster effects don't sit
// around — keeps the pacing snappy. (Enter still completes before Stage's exit.)
const DUR_SCALE = 0.85;
const NAME_SIZE = 70;
const NAME_WEIGHT = 700;
const CARD_SIZE = 26;
// ---------------------------------------------------------------------------
// Animation registry — one source of truth used by BOTH the overview grid
// (small sample) and the full-screen montage (the effect spelled out by name).
// ---------------------------------------------------------------------------
type Anim = {
name: string;
/** montage scene length, frames @30fps */
dur: number;
/** full-screen render; `speed` slows the effect's internal clock. */
full: (speed: number) => ReactNode;
/** small grid-card sample (looped) */
card: () => ReactNode;
};
const ANIMATIONS: Anim[] = [
{
name: "soft-blur-in",
dur: 90,
full: (s) => <SoftBlurIn text="Soft blur in" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <SoftBlurIn text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "typewriter",
dur: 80,
full: (s) => (
<Centered>
<Typewriter text="Typewriter" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} cursorColor={INK} charsPerSecond={16} speed={s} />
</Centered>
),
card: () => (
<Centered cardHeight>
<Typewriter text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} cursorColor={INK} charsPerSecond={14} speed={SPEED} />
</Centered>
),
},
{
name: "focus-blur-resolve",
dur: 90,
full: (s) => <FocusBlurResolve text="Focus blur resolve" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <FocusBlurResolve text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "per-character-rise",
dur: 96,
full: (s) => <PerCharacterRise text="Per character rise" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <PerCharacterRise text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "top-down-letters",
dur: 92,
// Tighter cascade + slightly higher speed so 16 letters finish well within
// the scene (default stagger 3 × speed 0.62 ran past it).
full: () => <TopDownLetters text="Top down letters" staggerDelay={2} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={0.9} />,
card: () => <TopDownLetters text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "bottom-up-letters",
dur: 94,
full: () => <BottomUpLetters text="Bottom up letters" staggerDelay={2} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={0.9} />,
card: () => <BottomUpLetters text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "spring-scale-in",
dur: 74,
full: (s) => <SpringScaleIn text="Spring scale in" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <SpringScaleIn text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "micro-scale-fade",
dur: 74,
full: (s) => <MicroScaleFade text="Micro scale fade" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <MicroScaleFade text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "scale-down-fade",
dur: 74,
full: (s) => <ScaleDownFade text="Scale down fade" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <ScaleDownFade text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} />,
},
{
name: "mask-reveal-up",
dur: 86,
full: (s) => <MaskRevealUp text={"Mask reveal\nup"} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <MaskRevealUp text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} />,
},
{
name: "line-by-line-slide",
dur: 92,
full: (s) => <LineByLineSlide text={"Line by line\nslide"} fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <LineByLineSlide text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "kinetic-center-build",
dur: 90,
full: (s) => <KineticCenterBuild text="Kinetic center build" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <KineticCenterBuild text="Remocn UI" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "short-slide-down",
dur: 94,
full: (s) => <ShortSlideDown text="Short slide down" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <ShortSlideDown text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "short-slide-right",
dur: 86,
full: (s) => <ShortSlideRight text="Short slide right" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <ShortSlideRight text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "blur-out-up",
dur: 84,
full: (s) => <BlurOutUp text="Blur out up" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <BlurOutUp text="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} />,
},
{
name: "fade-through",
dur: 82,
full: (s) => <FadeThrough fromText="Static text" toText="Fade through" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <FadeThrough fromText="Before" toText="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "shared-axis-y",
dur: 84,
full: (s) => <SharedAxisY fromText="Outgoing" toText="Shared axis y" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <SharedAxisY fromText="Before" toText="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "shared-axis-z",
dur: 90,
full: (s) => <SharedAxisZ fromText="Outgoing" toText="Shared axis z" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <SharedAxisZ fromText="Before" toText="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
{
name: "per-word-crossfade",
dur: 100,
full: (s) => <PerWordCrossfade fromText="Word by word" toText="Per word crossfade" fontSize={NAME_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={s} />,
card: () => <PerWordCrossfade fromText="Word by word" toText="Remocn" fontSize={CARD_SIZE} fontWeight={NAME_WEIGHT} color={INK} speed={SPEED} />,
},
];
// A relative full-frame box so absolute-centred components (and Sequenced ones
// like Typewriter) land in the middle of their slot.
const Centered: React.FC<{ children: ReactNode; cardHeight?: boolean }> = ({
children,
cardHeight,
}) => (
<AbsoluteFill style={{ alignItems: "center", justifyContent: "center" }}>
<div
style={{
position: "relative",
width: "100%",
height: cardHeight ? CARD_SIZE * 1.8 : NAME_SIZE * 1.6,
}}
>
{children}
</div>
</AbsoluteFill>
);
// ---------------------------------------------------------------------------
// Transitions — NO camera movement. Every hand-off is a short, in-place opacity
// cross-fade; the scene never slides, scales, or pushes. The visual change is
// carried entirely by the text effects: each one animates in on the spot, and
// Stage dissolves it out on the spot. The "transition" lives in the animation.
// ---------------------------------------------------------------------------
type Trans = { dur: number; presentation: () => TransitionPresentation<FadeProps> };
const td = (dur: number, presentation: () => TransitionPresentation<FadeProps>): Trans => ({
dur,
presentation,
});
const DISSOLVE: Trans = td(12, () => fade());
// In-place exit: as a scene ends, its text dissolves away where it stands
// (opacity + a soft blur, NO translate/scale) so it has cleared before the next
// text resolves — no two words overlap, and nothing moves across the frame.
const STAGE_EXIT = 16;
const Stage: React.FC<{ dur: number; transOut: number; children: ReactNode }> = ({
dur,
transOut,
children,
}) => {
const frame = useCurrentFrame();
const exitEnd = dur - transOut + 2;
const p = interpolate(frame, [exitEnd - STAGE_EXIT, exitEnd], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.in(Easing.cubic),
});
return (
<AbsoluteFill
style={{
opacity: 1 - p,
filter: p > 0.001 ? `blur(${p * 10}px)` : undefined,
}}
>
{children}
</AbsoluteFill>
);
};
// ---------------------------------------------------------------------------
// Hand-drawn mark: each path is normalised to pathLength 1, then "drawn" by
// sweeping its dash offset from hidden (1) to fully shown (0).
// ---------------------------------------------------------------------------
const IntroIcon: React.FC<{ size?: number }> = ({ size = 86 }) => {
const frame = useCurrentFrame();
const appear = interpolate(frame, [0, 8], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const draw1 = interpolate(frame, [2, 30], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.cubic),
});
const draw2 = interpolate(frame, [16, 46], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.cubic),
});
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={INK}
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
style={{ opacity: appear }}
>
<path
pathLength={1}
strokeDasharray={1}
strokeDashoffset={draw1}
d="M14 19L11.1069 10.7479C9.76348 6.91597 9.09177 5 8 5C6.90823 5 6.23652 6.91597 4.89309 10.7479L2 19M4.5 12H11.5"
/>
<path
pathLength={1}
strokeDasharray={1}
strokeDashoffset={draw2}
d="M21.9692 13.9392V18.4392M21.9692 13.9392C22.0164 13.1161 22.0182 12.4891 21.9194 11.9773C21.6864 10.7709 20.4258 10.0439 19.206 9.89599C18.0385 9.75447 17.1015 10.055 16.1535 11.4363M21.9692 13.9392L19.1256 13.9392C18.6887 13.9392 18.2481 13.9603 17.8272 14.0773C15.2545 14.7925 15.4431 18.4003 18.0233 18.845C18.3099 18.8944 18.6025 18.9156 18.8927 18.9026C19.5703 18.8724 20.1955 18.545 20.7321 18.1301C21.3605 17.644 21.9692 16.9655 21.9692 15.9392V13.9392Z"
/>
</svg>
);
};
// ---------------------------------------------------------------------------
// Overview grid: every effect plays on its own card; the cards wave in
// diagonally, hold, then the target card pulls focus and the camera dives in.
// ---------------------------------------------------------------------------
const GRID = 168;
const GRID_ZOOM = 34; // closing camera push
const GRID_ZOOM_START = GRID - GRID_ZOOM;
const GRID_FOCUS = 20; // pre-dive focus pull on the target card
const CARD_LOOP = 80;
const GRID_COLS = 5;
const GRID_CELLS = ANIMATIONS.length + 1; // effects + one closing brand tile
const GRID_ROWS = Math.ceil(GRID_CELLS / GRID_COLS);
const ZOOM_TARGET = 0; // soft-blur-in — also the first montage scene
// Card centre as a fraction of the frame, used as the zoom transform-origin.
const ZOOM_PAD_X = 70;
const ZOOM_PAD_TOP = 128;
const ZOOM_PAD_BOTTOM = 60;
const ZOOM_GAP = 16;
const targetCol = ZOOM_TARGET % GRID_COLS;
const targetRow = Math.floor(ZOOM_TARGET / GRID_COLS);
const cellW = (1280 - 2 * ZOOM_PAD_X - (GRID_COLS - 1) * ZOOM_GAP) / GRID_COLS;
const cellH =
(720 - ZOOM_PAD_TOP - ZOOM_PAD_BOTTOM - (GRID_ROWS - 1) * ZOOM_GAP) / GRID_ROWS;
const ZOOM_ORIGIN_X =
((ZOOM_PAD_X + targetCol * (cellW + ZOOM_GAP) + cellW / 2) / 1280) * 100;
const ZOOM_ORIGIN_Y =
((ZOOM_PAD_TOP + targetRow * (cellH + ZOOM_GAP) + cellH / 2) / 720) * 100;
// Diagonal entrance wave shared by every tile.
const cardEntrance = (frame: number, index: number) => {
const col = index % GRID_COLS;
const row = Math.floor(index / GRID_COLS);
const appear = (col + row) * 3;
const enter = interpolate(frame - appear, [0, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.bezier(0.22, 1, 0.36, 1),
});
return { appear, enter };
};
const GridCard: React.FC<{
anim: Anim;
index: number;
zoom: number;
focus: number;
}> = ({ anim, index, zoom, focus }) => {
const frame = useCurrentFrame();
const { appear, enter } = cardEntrance(frame, index);
const isTarget = index === ZOOM_TARGET;
// Pre-dive focus: the target brightens and lifts; the rest dim back so the
// camera clearly lands on the effect we continue into. Then the dive fades
// every non-target card out entirely.
const dim = isTarget ? 0 : focus * 0.55;
const zoomFade = isTarget ? 1 : 1 - zoom;
return (
<div
style={{
position: "relative",
overflow: "hidden",
borderRadius: 16,
border: `1px solid rgba(255,255,255,${0.08 + (isTarget ? focus * 0.55 : 0)})`,
background: "rgba(255,255,255,0.04)",
boxShadow:
isTarget && focus > 0.01
? `0 0 ${focus * 46}px rgba(255,255,255,${focus * 0.14})`
: "0 12px 30px rgba(0,0,0,0.28)",
opacity: enter * zoomFade * (1 - dim),
translate: `0px ${(1 - enter) * 18}px`,
scale:
(0.94 + enter * 0.06) *
(isTarget ? 1 + focus * 0.06 : 1 - focus * 0.03),
}}
>
{/* glassy top sheen */}
<div
style={{
position: "absolute",
inset: 0,
background:Showing the first 400 of 1031 lines. View the full file on GitHub.