Skip to content20cuts
Menu

CameraRig

Pan and zoom toward a world-space point without reflowing the scene.

MarkdownView source

Installation

Run once from your product repo. The installer includes CameraRig, its source, and the agent skill.

Terminal
npx -y https://20cuts.com/releases/v0.3.0/20cuts-create-0.3.0.tgz video/20cuts

Already installed? Import it in your video project. Full setup guide

Import
import { CameraRig } from "./system/motion";

Usage

The target’s x and y land in the center of the video. zoom controls how close you get.

Full scene · 1×
20cutsFadeInSlideInCameraRigBlurWordsAppFrameConnector
Detail · 1.7×
20cutsFadeInSlideInCameraRigBlurWordsAppFrameConnector
Coordinate example: the marked point is (900, 340).

Save your 1280×720 product screenshot as public/product.png. Use a composition of the same size: 30fps, 180 frames.

src/ProductZoom.tsx
Source
import { AbsoluteFill, Img, staticFile } from "remotion";
import { CameraRig } from "./system";

const wide = {x: 640, y: 360, zoom: 1};
const detail = {x: 900, y: 340, zoom: 1.7};

export default function ProductZoom() {
  return (
    <AbsoluteFill style={{background: "#fff", overflow: "hidden"}}>
      <CameraRig
        from={wide}
        to={detail}
        at={1}
        duration={1.2}
      >
        <Img
          src={staticFile("product.png")}
          style={{width: 1280, height: 720}}
        />
      </CameraRig>
    </AbsoluteFill>
  );
}

Replace detail with the center of the feature you want to show. The move starts at 1 second and settles at 2.2 seconds.

Return to the full scene

Hold the detail, then pull back at 4 seconds. The shared pose keeps the switch smooth.

src/ProductZoom.tsx · with return
Source
import { AbsoluteFill, Img, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import { CameraRig } from "./system";

const wide = {x: 640, y: 360, zoom: 1};
const detail = {x: 900, y: 340, zoom: 1.7};

export default function ProductZoom() {
  const seconds = useCurrentFrame() / useVideoConfig().fps;
  const returning = seconds >= 4;

  return (
    <AbsoluteFill style={{background: "#fff", overflow: "hidden"}}>
      <CameraRig
        from={returning ? detail : wide}
        to={returning ? wide : detail}
        at={returning ? 4 : 1}
        duration={1.2}
      >
        <Img
          src={staticFile("product.png")}
          style={{width: 1280, height: 720}}
        />
      </CameraRig>
    </AbsoluteFill>
  );
}
Register in Remotion

Save the scene above as src/ProductZoom.tsx. For a separate entry point, replace src/index.tsx with this registration, then run npm run studio.

src/index.tsx
import { Composition, registerRoot } from "remotion";
import ProductZoom from "./ProductZoom";

registerRoot(() => (
  <Composition
    id="ProductZoom"
    component={ProductZoom}
    width={1280}
    height={720}
    fps={30}
    durationInFrames={180}
  />
));

Props

PropTypeDefault
children

React content to display.

ReactNodeRequired
from

Starting world-space center point and zoom.

CameraPoseRequired
to

Ending world-space center point and zoom.

CameraPoseRequired
at

Start time in seconds within the current Remotion Sequence.

number0
duration

Animation duration in seconds.

number1.2
style

Styles applied to the component wrapper.

CSSProperties
  • Both poses are required. Their x and y coordinates identify the world point placed at the viewport center; zoom must be positive.
  • Viewport width and height come from useVideoConfig(). CameraRig has no width or height prop and does not reflow its children.
MotionTiming
MotionTiming type
Source
export type MotionTiming = {at?: number; duration?: number};
PropTypeDefault
at

Start time in seconds within the current Remotion Sequence.

number
duration

Animation duration in seconds.

number
CameraPose
CameraPose type
Source
export type CameraPose = Point & {zoom: number};
PropTypeDefault
x

World-space horizontal center in pixels.

numberRequired
y

World-space vertical center in pixels.

numberRequired
zoom

Positive magnification; 1 preserves scale.

numberRequired
Point
Point type
Source
export type Point = {x: number; y: number};
PropTypeDefault
x

Horizontal coordinate in pixels.

numberRequired
y

Vertical coordinate in pixels.

numberRequired

Source

Read the component files
motion/direction.tsx
Source
import React, {type CSSProperties, type ReactNode} from "react";
import {useCurrentFrame, useVideoConfig} from "remotion";
import {cameraTransform, focusStyle, inOut, phase, type CameraPose, type MotionTiming} from "./core.js";

export type FocusPullProps = MotionTiming & {children: ReactNode; direction?: "out" | "in"; blur?: number; dim?: number; style?: CSSProperties};
export function FocusPull({children, at = 0, duration = 0.7, direction = "out", blur = 6, dim = 0.45, style}: FocusPullProps) {
  const p = inOut(phase(useCurrentFrame(), useVideoConfig().fps, at, duration));
  if (direction !== "out" && direction !== "in") throw new TypeError("direction must be out or in.");
  return <div style={{...style, ...focusStyle(direction === "out" ? p : 1 - p, blur, dim)}}>{children}</div>;
}

export type CameraRigProps = MotionTiming & {children: ReactNode; from: CameraPose; to: CameraPose; style?: CSSProperties};
/** The point in each pose is the world coordinate to place at the viewport center. */
export function CameraRig({children, from, to, at = 0, duration = 1.2, style}: CameraRigProps) {
  const frame = useCurrentFrame(), {fps, width, height} = useVideoConfig();
  const p = inOut(phase(frame, fps, at, duration));
  return <div style={{position: "absolute", inset: 0, ...style, transform: cameraTransform(from, to, p, width, height), transformOrigin: "0 0"}}>{children}</div>;
}