Perspective Squeeze
Two lines of heavy type endlessly trade height while corner-pinned perspective bends the flat stack into receding planes
$ 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.
Props16
| Prop | Type | Default |
|---|---|---|
| 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 | number603 / 124 Font size of each line in block pixels | |
| fill | string"#ffffff" Fill color of the type | |
| width / height | number1920 / 1080 Design space all positions are authored in. The stage scales to fit the actual composition, centered | |
| blockWidth / blockHeight | number990 / 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 | Cornerstutorial poses Corner poses of the upper copy, as four `[x, y]` points in block space | |
| lowerCornersMid / lowerCornersEnd | Cornerstutorial poses Corner poses of the lower copy | |
| cornersRest | Cornersthe 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 | number0.65 Cap height as a fraction of em, used to stack the two lines | |
| lineGap | number50 Constant gap between the lines in block pixels | |
| className | string— Optional className passed to the wrapper | |
- upperText / lowerTextstring
The two lines. Both are stretched to the block's full width
Default
"REMOCN" / "BEST" - upperFontFamily / lowerFontFamilystring
Font of each line — heavy condensed faces read best
Default
"sans-serif" - upperFontSize / lowerFontSizenumber
Font size of each line in block pixels
Default
603 / 124 - fillstring
Fill color of the type
Default
"#ffffff" - width / heightnumber
Design space all positions are authored in. The stage scales to fit the actual composition, centered
Default
1920 / 1080 - blockWidth / blockHeightnumber
Size of the cropped block the corner pin acts on — cropping before pinning keeps the corners near the artwork
Default
990 / 546 - upperBlockPosition / lowerBlockPosition[number, number]
Center of each pinned copy in comp pixels
Default
[960, 314] / [960, 860] - cornerKeyframesnumber[]
Frames at which the corner poses land
Default
[0, 45, 70] - upperCornersMid / upperCornersEndCorners
Corner poses of the upper copy, as four `[x, y]` points in block space
Default
tutorial poses - lowerCornersMid / lowerCornersEndCorners
Corner poses of the lower copy
Default
tutorial poses - cornersRestCorners
Identity pose every departure is measured from
Default
the block's own rectangle - scaleKeyframesnumber[]
Frames of the squish track. The last keyframe repeats the first, closing the loop
Default
[0, 60, 118, 161, 196, 225] - upperScaleY / lowerScaleYnumber[]
Vertical scale percentages at each keyframe — the lines trade height in opposition
Default
tutorial tracks - capRationumber
Cap height as a fraction of em, used to stack the two lines
Default
0.65 - lineGapnumber
Constant gap between the lines in block pixels
Default
50 - classNamestring
Optional className passed to the wrapper
Inspired by 5 Clean Text Animations by Mapal.