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

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

PropTypeDefault
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

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

    Default1920 / 1080

  • blockWidth / blockHeightnumber

    Size of the cropped block the corner pin acts on — cropping before pinning keeps the corners near the artwork

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

    Defaulttutorial poses

  • lowerCornersMid / lowerCornersEndCorners

    Corner poses of the lower copy

    Defaulttutorial poses

  • cornersRestCorners

    Identity pose every departure is measured from

    Defaultthe 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

    Defaulttutorial tracks

  • capRationumber

    Cap height as a fraction of em, used to stack the two lines

    Default0.65

  • lineGapnumber

    Constant gap between the lines in block pixels

    Default50

  • classNamestring

    Optional className passed to the wrapper

Inspired by 5 Clean Text Animations by Mapal.