Perspective Squeeze
Two lines of heavy type endlessly trade height while corner-pinned perspective bends the flat stack into receding planes
Installation
$ pnpm dlx shadcn@latest add @remocn/perspective-squeezeUsage
// src/Root.tsx
import { Composition } from "remotion";
import { PerspectiveSqueeze } from "@/components/remocn/perspective-squeeze";
const PerspectiveSqueezeScene = () => (
<PerspectiveSqueeze
upperText="REMOCN"
lowerText="BEST"
upperFontFamily="Impact, 'Arial Narrow', sans-serif"
lowerFontFamily="Impact, 'Arial Narrow', sans-serif"
/>
);
export const RemotionRoot = () => (
<Composition
id="PerspectiveSqueeze"
component={PerspectiveSqueezeScene}
durationInFrames={225}
fps={30}
width={1920}
height={1080}
/>
);Both lines are forced to exactly the block's width via SVG textLength, so any
pair of words lines up flush. The vertical squeeze scales each line about its
own baseline, and the layout re-derives from the scaled heights every frame —
the gap between the lines never changes. The perspective is a true projective
corner-pin (matrix3d), interpolated on the corners, so straight lines stay
straight while parallels converge.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
upperText / lowerText | string | "REMOCN" / "BEST" | The two lines. Both are stretched to the block's full width |
upperFontFamily / lowerFontFamily | string | "sans-serif" | Font of each line — heavy condensed faces read best |
upperFontSize / lowerFontSize | number | 603 / 124 | Font size of each line in block pixels |
fill | string | "#ffffff" | Fill color of the type |
width / height | number | 1920 / 1080 | Design space all positions are authored in. The stage scales to fit the actual composition, centered |
blockWidth / blockHeight | number | 990 / 546 | Size of the cropped block the corner pin acts on — cropping before pinning keeps the corners near the artwork |
upperBlockPosition / lowerBlockPosition | [number, number] | [960, 314] / [960, 860] | Center of each pinned copy in comp pixels |
cornerKeyframes | number[] | [0, 45, 70] | Frames at which the corner poses land |
upperCornersMid / upperCornersEnd | Corners | tutorial poses | Corner poses of the upper copy, as four `[x, y]` points in block space |
lowerCornersMid / lowerCornersEnd | Corners | tutorial poses | Corner poses of the lower copy |
cornersRest | Corners | the block's own rectangle | Identity pose every departure is measured from |
scaleKeyframes | number[] | [0, 60, 118, 161, 196, 225] | Frames of the squish track. The last keyframe repeats the first, closing the loop |
upperScaleY / lowerScaleY | number[] | tutorial tracks | Vertical scale percentages at each keyframe — the lines trade height in opposition |
capRatio | number | 0.65 | Cap height as a fraction of em, used to stack the two lines |
lineGap | number | 50 | Constant gap between the lines in block pixels |
className | string | — | Optional className passed to the wrapper |
Inspired by 5 Clean Text Animations by Mapal.