reelcn
GitHubBack to landing

Split doors

The outgoing scene splits down the middle and its two halves slide apart like doors, revealing the next scene.

SplitDoors, default
SplitDoors, default thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/split-doors.json

Files land in src/reelcn/ with the items it builds on. Dependencies: @remotion/transitions, remotion.

Usage

<TransitionSeries>
  <TransitionSeries.Sequence durationInFrames={60}>
    <TitleCard />
  </TransitionSeries.Sequence>
  <TransitionSeries.Transition presentation={splitDoors()} timing={linearTiming({ durationInFrames: 24 })} />
  <TransitionSeries.Sequence durationInFrames={90}>
    <ProductShot />
  </TransitionSeries.Sequence>
</TransitionSeries>

Props

PropTypeDescription
direction"vertical" | "horizontal"Which way the doors travel. Defaults to horizontal in landscape and square, vertical in portrait.
shadowColorstringShadow the doors cast on the revealed scene.

Built from

Use

  • Revealing a product shot, logo or new chapter behind the current scene
  • Moving from a title card into the main content

Avoid

  • An outgoing scene that plays audio or video, since it renders once per door. Use circle-burst

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/split-doors.tsx
/**
 * @title Split Doors
 * @category transitions
 * @description The outgoing scene splits down the middle and its two halves slide apart like doors, revealing the next scene.
 * @duration 24
 * @use Revealing a product shot, logo or new chapter behind the current scene
 * @use Moving from a title card into the main content
 * @avoid An outgoing scene that plays audio or video, since it renders once per door. Use `circle-burst`
 * @tags doors, split, open, reveal
 * @example
 * <TransitionSeries>
 *   <TransitionSeries.Sequence durationInFrames={60}>
 *     <TitleCard />
 *   </TransitionSeries.Sequence>
 *   <TransitionSeries.Transition presentation={splitDoors()} timing={linearTiming({ durationInFrames: 24 })} />
 *   <TransitionSeries.Sequence durationInFrames={90}>
 *     <ProductShot />
 *   </TransitionSeries.Sequence>
 * </TransitionSeries>
 */
import type { TransitionPresentation, TransitionPresentationComponentProps } from "@remotion/transitions";
import { AbsoluteFill, interpolate } from "remotion";
import { alpha, CLAMP, easings, useViewport } from "./core";

export type SplitDoorsProps = {
  /** Which way the doors travel. Defaults to `horizontal` in landscape and square, `vertical` in portrait. */
  direction?: "horizontal" | "vertical";
  /** Shadow the doors cast on the revealed scene. */
  shadowColor?: string;
};

function SplitDoorsPresentation({
  children,
  presentationDirection,
  presentationProgress,
  passedProps,
}: TransitionPresentationComponentProps<SplitDoorsProps>) {
  const { width, height, isPortrait, u } = useViewport();
  const horizontal = (passedProps.direction ?? (isPortrait ? "vertical" : "horizontal")) === "horizontal";
  const open = interpolate(presentationProgress, [0, 1], [0, 1], { ...CLAMP, easing: easings.gentle });

  if (presentationDirection === "entering") {
    // The incoming scene shows through the widening gap, shaded at the door edges and settling from a slight zoom.
    const gap = (1 - open) * 50;
    const shadow = passedProps.shadowColor ?? alpha("#000000", 0.5);
    const fade = u(120);
    return (
      <AbsoluteFill style={{ clipPath: horizontal ? `inset(0 ${gap}%)` : `inset(${gap}% 0)` }}>
        <AbsoluteFill style={{ scale: String(1.08 - 0.08 * open) }}>{children}</AbsoluteFill>
        <AbsoluteFill
          style={{
            opacity: 1 - open,
            background: `linear-gradient(${horizontal ? "90deg" : "180deg"}, ${shadow} ${gap}%, transparent calc(${gap}% + ${fade}px), transparent calc(${100 - gap}% - ${fade}px), ${shadow} ${100 - gap}%)`,
          }}
        />
      </AbsoluteFill>
    );
  }

  // The outgoing scene is drawn once per door; each door shows its half and travels its own size off-screen.
  return (
    <AbsoluteFill>
      {[0, 1].map((door) => {
        const sign = door === 0 ? -1 : 1;
        return (
          <div
            key={door}
            style={{
              position: "absolute",
              overflow: "hidden",
              left: horizontal ? (door * width) / 2 : 0,
              top: horizontal ? 0 : (door * height) / 2,
              width: horizontal ? width / 2 : width,
              height: horizontal ? height : height / 2,
              translate: horizontal ? `${sign * open * 100}% 0` : `0 ${sign * open * 100}%`,
            }}
          >
            <div
              style={{
                position: "absolute",
                left: horizontal ? (-door * width) / 2 : 0,
                top: horizontal ? 0 : (-door * height) / 2,
                width,
                height,
              }}
            >
              {children}
            </div>
          </div>
        );
      })}
    </AbsoluteFill>
  );
}

export function splitDoors(props: SplitDoorsProps = {}): TransitionPresentation<SplitDoorsProps> {
  return { component: SplitDoorsPresentation, props };
}

On this page