SlideIn
Move a layer in from a chosen direction, with an optional fade.
import React, { type CSSProperties, type ReactNode } from "react";
import { AbsoluteFill } from "remotion";
import { SlideIn } from "./system";
const ink = "#141414", lime = "#b6f36b";
const center: CSSProperties = { display: "flex", alignItems: "center", justifyContent: "center" };
const card: CSSProperties = { background: "#fff", border: "1px solid #ddd", borderRadius: 20, padding: 32 };
function Stage({ children, dark = false, style }: {
children: ReactNode;
dark?: boolean;
style?: CSSProperties;
}) {
return <AbsoluteFill style={{ fontFamily: "Arial, Helvetica, sans-serif", color: dark ? "#fff" : ink, background: dark ? ink : "#f7f7f5", ...center, ...style }}>{children}</AbsoluteFill>;
}
export function SlideInExample() { return <Stage><div style={{ display: "flex", gap: 24 }}>{["Plan", "Build", "Launch"].map((text, i) => <SlideIn key={text} at={0.3 + i * 0.35} from={i === 0 ? "left" : i === 2 ? "right" : "down"} distance={120} duration={1}><div style={{ ...card, width: 260, height: 310, display: "flex", flexDirection: "column", justifyContent: "space-between", background: i === 2 ? lime : "#fff" }}><span style={{ fontSize: 26 }}>0{i + 1}</span><strong style={{ fontSize: 50, letterSpacing: "-0.05em" }}>{text}</strong></div></SlideIn>)}</div></Stage>; }
Installation
Run once from your product repo. The installer includes SlideIn, its source, and the agent skill.
npx -y https://20cuts.com/releases/v0.3.0/20cuts-create-0.3.0.tgz video/20cutsAlready installed? Import it in your video project. Full setup guide
import { SlideIn } from "./system/motion";Usage
The starter already includes SlideInExample in Remotion Studio. To edit a copy, save the Code tab as src/SlideInExample.tsx and register it below.
Register in Remotion
Use the complete example from the Code tab. For a separate entry point, replace src/index.tsx with this registration, then run npm run studio.
import { Composition, registerRoot } from "remotion";
import { SlideInExample } from "./SlideInExample";
registerRoot(() => (
<Composition
id="SlideInExample"
component={SlideInExample}
width={1280}
height={720}
fps={30}
durationInFrames={180}
/>
));Props
| Prop | Type | Default |
|---|---|---|
childrenReact content to display. | ReactNode | Required |
atStart time in seconds within the current Remotion Sequence. | number | 0 |
durationAnimation duration in seconds. | number | 0.6 |
fromDirection the content enters from. | "up" | "down" | "left" | "right" | "up" |
distanceTravel distance in pixels. | number | 40 |
fadeFade in while translating. | boolean | true |
styleStyles applied to the component wrapper. | CSSProperties | — |
MotionProps
export type MotionProps = MotionTiming & {children: ReactNode; style?: CSSProperties};| Prop | Type | Default |
|---|---|---|
atStart time in seconds within the current Remotion Sequence. | number | — |
durationAnimation duration in seconds. | number | — |
childrenReact content to display. | ReactNode | Required |
styleStyles applied to the component wrapper. | CSSProperties | — |
MotionTiming
export type MotionTiming = {at?: number; duration?: number};| Prop | Type | Default |
|---|---|---|
atStart time in seconds within the current Remotion Sequence. | number | — |
durationAnimation duration in seconds. | number | — |
Source
Read the component files
import React, {Children, type CSSProperties, type ReactNode} from "react";
import {useCurrentFrame, useVideoConfig} from "remotion";
import {finite, lerp, nonNegative, phase, positive, reveal, staggerAt, swapState, type MotionTiming, type Point} from "./core.js";
export type MotionProps = MotionTiming & {children: ReactNode; style?: CSSProperties};
export function FadeIn({children, at = 0, duration = 0.6, style}: MotionProps) {
const p = reveal(useCurrentFrame(), useVideoConfig().fps, at, duration);
return <div style={{...style, opacity: p}}>{children}</div>;
}
export type SlideInProps = MotionProps & {from?: "up" | "down" | "left" | "right"; distance?: number; fade?: boolean};
export function SlideIn({children, at = 0, duration = 0.6, from = "up", distance = 40, fade = true, style}: SlideInProps) {
const p = reveal(useCurrentFrame(), useVideoConfig().fps, at, duration);
nonNegative(distance, "distance");
if (!["up", "down", "left", "right"].includes(from)) throw new TypeError("from must be up, down, left, or right.");
const axis = from === "left" || from === "right" ? "X" : "Y";
const sign = from === "left" || from === "up" ? -1 : 1;
return <div style={{...style, opacity: fade ? p : 1, transform: `translate${axis}(${sign * distance * (1 - p)}px)`}}>{children}</div>;
}
export type ScaleInProps = MotionProps & {from?: number; origin?: string};
export function ScaleIn({children, at = 0, duration = 0.6, from = 0.88, origin = "50% 50%", style}: ScaleInProps) {
const p = reveal(useCurrentFrame(), useVideoConfig().fps, at, duration);
nonNegative(from, "from");
return <div style={{...style, opacity: p, transform: `scale(${lerp(from, 1, p)})`, transformOrigin: origin}}>{children}</div>;
}
export type AnchoredRevealProps = MotionProps & {anchor: Point; from?: number};
export function AnchoredReveal({children, anchor, from = 0.75, at = 0, duration = 0.6, style}: AnchoredRevealProps) {
finite(anchor.x, "anchor.x"); finite(anchor.y, "anchor.y");
return <ScaleIn at={at} duration={duration} from={from} origin={`${anchor.x}px ${anchor.y}px`} style={style}>{children}</ScaleIn>;
}
export type StaggerCascadeProps = MotionProps & {stagger?: number; distance?: number; itemStyle?: CSSProperties};
export function StaggerCascade({children, at = 0, duration = 0.55, stagger = 0.09, distance = 24, style, itemStyle}: StaggerCascadeProps) {
nonNegative(stagger, "stagger"); nonNegative(distance, "distance"); positive(duration, "duration"); finite(at, "at");
return <div style={style}>{Children.map(children, (child, i) => <SlideIn key={i} at={staggerAt(at, i, stagger)} duration={duration} from="down" distance={distance} style={itemStyle}>{child}</SlideIn>)}</div>;
}
export type ValueSwapProps = MotionTiming & {from: ReactNode; to: ReactNode; style?: CSSProperties};
/** Both values occupy the same grid cell, reserving the larger value's geometry. */
export function ValueSwap({from, to, at = 0, duration = 0.5, style}: ValueSwapProps) {
const state = swapState(phase(useCurrentFrame(), useVideoConfig().fps, at, duration));
return <span style={{display: "inline-grid", ...style}}>
<span aria-hidden={state.side !== "from"} style={{gridArea: "1 / 1", opacity: state.side === "from" ? state.opacity : 0}}>{from}</span>
<span aria-hidden={state.side !== "to"} style={{gridArea: "1 / 1", opacity: state.side === "to" ? state.opacity : 0}}>{to}</span>
</span>;
}
import {Easing} from "remotion";
/** All motion is evaluated from the local Remotion frame, including frames before a Sequence. */
export type MotionTiming = {at?: number; duration?: number};
export type Point = {x: number; y: number};
export type CameraPose = Point & {zoom: number};
export const out = Easing.bezier(0.16, 1, 0.3, 1);
export const inOut = Easing.bezier(0.65, 0, 0.35, 1);
export function finite(value: number, name: string): number {
if (!Number.isFinite(value)) throw new TypeError(`${name} must be finite.`);
return value;
}
export function positive(value: number, name: string): number {
if (finite(value, name) <= 0) throw new RangeError(`${name} must be greater than zero.`);
return value;
}
export function nonNegative(value: number, name: string): number {
if (finite(value, name) < 0) throw new RangeError(`${name} must be zero or greater.`);
return value;
}
export function unit(value: number, name = "progress"): number {
return Math.min(1, Math.max(0, finite(value, name)));
}
export function lerp(from: number, to: number, p: number): number {
finite(from, "from"); finite(to, "to");
const t = unit(p);
return finite(from * (1 - t) + to * t, "interpolated value");
}
export function phase(frame: number, fps: number, at = 0, duration = 0.6): number {
finite(frame, "frame"); positive(fps, "fps"); finite(at, "at"); positive(duration, "duration");
return unit((frame / fps - at) / duration);
}
export function reveal(frame: number, fps: number, at = 0, duration = 0.6): number {
return out(phase(frame, fps, at, duration));
}
export function staggerAt(at: number, index: number, interval: number): number {
finite(at, "at"); nonNegative(index, "index"); nonNegative(interval, "stagger");
return at + index * interval;
}
export function swapState(progress: number): {side: "from" | "to"; opacity: number} {
const p = unit(progress);
return {side: p < 0.5 ? "from" : "to", opacity: Math.min(1, Math.abs(p - 0.5) * 4)};
}
export function cameraTransform(from: CameraPose, to: CameraPose, p: number, width: number, height: number): string {
positive(from.zoom, "from.zoom"); positive(to.zoom, "to.zoom");
positive(width, "width"); positive(height, "height");
const x = lerp(from.x, to.x, p), y = lerp(from.y, to.y, p), zoom = lerp(from.zoom, to.zoom, p);
const tx = finite(width / 2 - x * zoom, "camera translation x");
const ty = finite(height / 2 - y * zoom, "camera translation y");
return `translate(${tx}px, ${ty}px) scale(${zoom})`;
}
export function focusStyle(progress: number, blur: number, dim: number): {filter: string; opacity: number} {
nonNegative(blur, "blur");
if (finite(dim, "dim") < 0 || dim > 1) throw new RangeError("dim must be between zero and one.");
const p = unit(progress);
return {filter: `blur(${blur * p}px)`, opacity: lerp(1, dim, p)};
}
export function connectorGeometry(from: Point, to: Point, bend: number): {path: string; point: (p: number) => Point} {
finite(from.x, "from.x"); finite(from.y, "from.y"); finite(to.x, "to.x"); finite(to.y, "to.y"); finite(bend, "bend");
const middle = {x: lerp(from.x, to.x, 0.5), y: finite(lerp(from.y, to.y, 0.5) - bend, "curve midpoint")};
return {
path: `M ${from.x} ${from.y} Q ${middle.x} ${middle.y} ${to.x} ${to.y}`,
point(p) { const t = unit(p); return {x: lerp(lerp(from.x, middle.x, t), lerp(middle.x, to.x, t), t), y: lerp(lerp(from.y, middle.y, t), lerp(middle.y, to.y, t), t)}; },
};
}