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-squeeze

Usage

// 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

PropTypeDefaultDescription
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 / 124Font size of each line in block pixels
fill
string"#ffffff"Fill color of the type
width / height
number1920 / 1080Design space all positions are authored in. The stage scales to fit the actual composition, centered
blockWidth / blockHeight
number990 / 546Size 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 posesCorner poses of the upper copy, as four `[x, y]` points in block space
lowerCornersMid / lowerCornersEnd
Cornerstutorial posesCorner poses of the lower copy
cornersRest
Cornersthe block's own rectangleIdentity 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 tracksVertical scale percentages at each keyframe — the lines trade height in opposition
capRatio
number0.65Cap height as a fraction of em, used to stack the two lines
lineGap
number50Constant gap between the lines in block pixels
className
stringOptional className passed to the wrapper

Inspired by 5 Clean Text Animations by Mapal.