← Catalog
Edit Props (live)
Source
845 linesimport React from "react";
import {
AbsoluteFill,
Img,
Sequence,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
loadInter("normal", { weights: ["400", "700", "900"] });
loadInter("italic", { weights: ["400", "900"] });
const FONT = "'Inter', sans-serif";
const W = 1080;
const H = 1920;
const HALF_W = W / 2;
// Demo images — use existing assets from public/visuals
const IMG_A = "visuals/asset_df924ceb.jpg";
const IMG_B = "visuals/asset_9e785f61.jpg";
const IMG_C = "visuals/asset_cf2b0344.jpg";
// ─── Easing helpers ───────────────────────────────────────────────
function smoothstep(t: number): number {
const c = Math.max(0, Math.min(1, t));
return c * c * (3 - 2 * c);
}
function springPop(t: number): number {
// Overshoot spring: 0 → ~1.08 → 1.0
if (t >= 1) return 1;
const c = Math.max(0, t);
return 1 - Math.exp(-6 * c) * Math.cos(c * Math.PI * 2.5);
}
// Punch curve: fast → slow → fast
function punchCurve(t: number): number {
// Remap linear t through a punch-shaped curve
// Returns a new progress value that slows around t=0.4
if (t <= 0) return 0;
if (t >= 1) return 1;
if (t < 0.3) {
// Fast approach: cover 0→0.5 in first 30% of time
return interpolate(t, [0, 0.3], [0, 0.5]);
} else if (t < 0.65) {
// Slow zone: cover 0.5→0.65 in 35% of time
return interpolate(t, [0.3, 0.65], [0.5, 0.65]);
} else {
// Fast exit: cover 0.65→1.0 in last 35%
return interpolate(t, [0.65, 1], [0.65, 1]);
}
}
// ─── Section label ─────────────────────────────────────────────
const SectionTitle: React.FC<{ text: string; sub?: string }> = ({ text, sub }) => {
const frame = useCurrentFrame();
const op = interpolate(frame, [0, 10], [0, 1], { extrapolateRight: "clamp" });
return (
<div
style={{
position: "absolute",
top: 60,
left: 0,
right: 0,
textAlign: "center",
opacity: op,
zIndex: 100,
}}
>
<div
style={{
fontSize: 52,
fontWeight: 900,
fontFamily: FONT,
color: "#fff",
letterSpacing: -1.5,
textShadow: "0 4px 24px rgba(0,0,0,0.8)",
}}
>
{text}
</div>
{sub && (
<div
style={{
fontSize: 28,
fontWeight: 400,
fontFamily: FONT,
color: "rgba(255,255,255,0.6)",
marginTop: 8,
textShadow: "0 2px 12px rgba(0,0,0,0.8)",
}}
>
{sub}
</div>
)}
</div>
);
};
// ─── Split label (BEFORE / AFTER) ──────────────────────────────
const SplitLabels: React.FC = () => (
<>
<div
style={{
position: "absolute",
bottom: 80,
left: 0,
width: HALF_W,
textAlign: "center",
zIndex: 100,
}}
>
<span
style={{
fontSize: 32,
fontWeight: 700,
fontFamily: FONT,
color: "#ff6b6b",
padding: "8px 24px",
background: "rgba(0,0,0,0.7)",
borderRadius: 12,
}}
>
LINEAR
</span>
</div>
<div
style={{
position: "absolute",
bottom: 80,
left: HALF_W,
width: HALF_W,
textAlign: "center",
zIndex: 100,
}}
>
<span
style={{
fontSize: 32,
fontWeight: 700,
fontFamily: FONT,
color: "#51cf66",
padding: "8px 24px",
background: "rgba(0,0,0,0.7)",
borderRadius: 12,
}}
>
EASED
</span>
</div>
{/* Center divider */}
<div
style={{
position: "absolute",
left: HALF_W - 2,
top: 0,
width: 4,
height: H,
background: "rgba(255,255,255,0.3)",
zIndex: 100,
}}
/>
</>
);
// ─── Speed curve visualizer ─────────────────────────────────────
const CurveGraph: React.FC<{
curveFn: (t: number) => number;
label: string;
color: string;
x: number;
y: number;
w: number;
h: number;
}> = ({ curveFn, label, color, x, y, w, h }) => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = frame / durationInFrames;
// Draw curve as SVG path
const points: string[] = [];
const STEPS = 80;
for (let i = 0; i <= STEPS; i++) {
const t = i / STEPS;
const val = curveFn(t);
const px = t * w;
const py = h - val * h;
points.push(`${i === 0 ? "M" : "L"}${px},${py}`);
}
// Current position dot
const curVal = curveFn(progress);
const dotX = progress * w;
const dotY = h - curVal * h;
return (
<div style={{ position: "absolute", left: x, top: y }}>
<div
style={{
fontSize: 20,
fontWeight: 700,
fontFamily: FONT,
color,
marginBottom: 8,
textAlign: "center",
width: w,
}}
>
{label}
</div>
<svg width={w} height={h} style={{ overflow: "visible" }}>
{/* Grid */}
<rect
x={0}
y={0}
width={w}
height={h}
fill="rgba(255,255,255,0.05)"
stroke="rgba(255,255,255,0.15)"
strokeWidth={1}
/>
{/* 1.0x baseline */}
<line
x1={0}
y1={h * 0.5}
x2={w}
y2={h * 0.5}
stroke="rgba(255,255,255,0.15)"
strokeDasharray="4,4"
/>
{/* Curve */}
<path d={points.join(" ")} fill="none" stroke={color} strokeWidth={3} />
{/* Playhead */}
<line
x1={dotX}
y1={0}
x2={dotX}
y2={h}
stroke="rgba(255,255,255,0.4)"
strokeWidth={1}
/>
{/* Dot */}
<circle cx={dotX} cy={dotY} r={8} fill={color} />
</svg>
</div>
);
};
// ─── Section 1: ZOOM-IN comparison ───────────────────────────
const ZoomComparison: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const linear = frame / durationInFrames;
const eased = Easing.inOut(Easing.cubic)(linear);
const scaleLinear = interpolate(linear, [0, 1], [1, 1.25]);
const scaleEased = interpolate(eased, [0, 1], [1, 1.25]);
return (
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
<SectionTitle text="ZOOM-IN" sub="Ken Burns motion on AI assets" />
{/* Left: linear */}
<div
style={{
position: "absolute",
left: 0,
top: 200,
width: HALF_W,
height: H - 400,
overflow: "hidden",
}}
>
<Img
src={staticFile(IMG_A)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: `scale(${scaleLinear})`,
}}
/>
</div>
{/* Right: eased */}
<div
style={{
position: "absolute",
left: HALF_W,
top: 200,
width: HALF_W,
height: H - 400,
overflow: "hidden",
}}
>
<Img
src={staticFile(IMG_A)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: `scale(${scaleEased})`,
}}
/>
</div>
<SplitLabels />
</AbsoluteFill>
);
};
// ─── Section 2: SLOW-PUSH comparison ───────────────────────────
const SlowPushComparison: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const linear = frame / durationInFrames;
const eased = Easing.inOut(Easing.cubic)(linear);
const scaleL = interpolate(linear, [0, 1], [1, 1.12]);
const translateL = interpolate(linear, [0, 1], [0, -25]);
const scaleE = interpolate(eased, [0, 1], [1, 1.12]);
const translateE = interpolate(eased, [0, 1], [0, -25]);
return (
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
<SectionTitle text="SLOW PUSH" sub="Subtle forward drift on reveals" />
<div
style={{
position: "absolute",
left: 0,
top: 200,
width: HALF_W,
height: H - 400,
overflow: "hidden",
}}
>
<Img
src={staticFile(IMG_B)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: `scale(${scaleL}) translateY(${translateL}px)`,
}}
/>
</div>
<div
style={{
position: "absolute",
left: HALF_W,
top: 200,
width: HALF_W,
height: H - 400,
overflow: "hidden",
}}
>
<Img
src={staticFile(IMG_B)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: `scale(${scaleE}) translateY(${translateE}px)`,
}}
/>
</div>
<SplitLabels />
</AbsoluteFill>
);
};
// ─── Section 3: ASSET POP-IN comparison ──────────────────────
const PopInComparison: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Trigger pop after 0.5s
const triggerFrame = Math.round(0.5 * fps);
const elapsed = Math.max(0, frame - triggerFrame);
const t = Math.min(elapsed / (fps * 0.5), 1); // 0.5s animation
// Linear: just opacity
const opLinear = t;
const scaleLinear = 1;
// Spring: scale pop + opacity
const opSpring = Math.min(t * 3, 1);
const scaleSpring = t === 0 ? 0 : interpolate(springPop(t), [0, 1], [0.8, 1]);
const BG = "#509ADB";
const rectW = 400;
const rectH = 700;
const rectY = 500;
return (
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
<SectionTitle text="ASSET REVEAL" sub="How AI images enter the frame" />
{/* Left: linear fade */}
<AbsoluteFill>
<div
style={{
position: "absolute",
left: 0,
top: 200,
width: HALF_W,
height: H - 400,
backgroundColor: BG,
overflow: "hidden",
}}
>
{/* Noise hint */}
<div
style={{
position: "absolute",
inset: 0,
background:
"repeating-conic-gradient(rgba(0,0,0,0.03) 0% 25%, transparent 0% 50%) 0 0 / 4px 4px",
}}
/>
<div
style={{
position: "absolute",
left: (HALF_W - rectW) / 2,
top: rectY - 200,
width: rectW,
height: rectH,
overflow: "hidden",
opacity: opLinear,
transform: `scale(${scaleLinear})`,
}}
>
<Img
src={staticFile(IMG_C)}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
</div>
</AbsoluteFill>
{/* Right: spring pop */}
<AbsoluteFill>
<div
style={{
position: "absolute",
left: HALF_W,
top: 200,
width: HALF_W,
height: H - 400,
backgroundColor: BG,
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
inset: 0,
background:
"repeating-conic-gradient(rgba(0,0,0,0.03) 0% 25%, transparent 0% 50%) 0 0 / 4px 4px",
}}
/>
<div
style={{
position: "absolute",
left: (HALF_W - rectW) / 2,
top: rectY - 200,
width: rectW,
height: rectH,
overflow: "hidden",
opacity: opSpring,
transform: `scale(${scaleSpring})`,
transformOrigin: "center center",
}}
>
<Img
src={staticFile(IMG_C)}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
</div>
</AbsoluteFill>
<SplitLabels />
</AbsoluteFill>
);
};
// ─── Section 4: PUNCH REVEAL ──────────────────────────────────
const PunchComparison: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const linear = frame / durationInFrames;
const punched = punchCurve(linear);
// Pan across image
const translateLinear = interpolate(linear, [0, 1], [-80, 80]);
const scaleLinear = interpolate(linear, [0, 1], [1.1, 1.3]);
const translatePunch = interpolate(punched, [0, 1], [-80, 80]);
const scalePunch = interpolate(punched, [0, 1], [1.1, 1.3]);
return (
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
<SectionTitle text="PUNCH REVEAL" sub="Fast approach → slow hit → fast exit" />
<div
style={{
position: "absolute",
left: 0,
top: 200,
width: HALF_W,
height: H - 400,
overflow: "hidden",
}}
>
<Img
src={staticFile(IMG_A)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: `scale(${scaleLinear}) translateX(${translateLinear}px)`,
}}
/>
</div>
<div
style={{
position: "absolute",
left: HALF_W,
top: 200,
width: HALF_W,
height: H - 400,
overflow: "hidden",
}}
>
<Img
src={staticFile(IMG_A)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
transform: `scale(${scalePunch}) translateX(${translatePunch}px)`,
}}
/>
</div>
<SplitLabels />
{/* Speed curve graphs at bottom */}
<CurveGraph
curveFn={(t) => t}
label="LINEAR"
color="#ff6b6b"
x={40}
y={H - 340}
w={HALF_W - 80}
h={120}
/>
<CurveGraph
curveFn={punchCurve}
label="PUNCH"
color="#51cf66"
x={HALF_W + 40}
y={H - 340}
w={HALF_W - 80}
h={120}
/>
</AbsoluteFill>
);
};
// ─── Section 5: COLLAGE STAGGER ─────────────────────────────
const CollageComparison: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const RECTS = 3;
const images = [IMG_A, IMG_B, IMG_C];
const colors = ["#509ADB", "#57CEB2", "#FFB080"];
const totalSec = durationInFrames / fps;
// Layout: 3 stacked rects per side
const rectH = 350;
const rectW = 420;
const startY = 250;
const gap = 30;
return (
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
<SectionTitle text="COLLAGE BUILD-UP" sub="Staggered asset reveal timing" />
<SplitLabels />
{/* Left: even stagger */}
{Array.from({ length: RECTS }).map((_, i) => {
const evenDelay = (i * totalSec) / RECTS;
const evenFrame = Math.round(evenDelay * fps);
const elapsed = Math.max(0, frame - evenFrame);
const t = Math.min(elapsed / (fps * 0.3), 1);
const op = t;
const y = startY + i * (rectH + gap);
return (
<div
key={`l-${i}`}
style={{
position: "absolute",
left: (HALF_W - rectW) / 2,
top: y,
width: rectW,
height: rectH,
overflow: "hidden",
opacity: op,
border: `3px solid ${colors[i]}`,
borderRadius: 12,
}}
>
<Img
src={staticFile(images[i])}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
);
})}
{/* Right: accelerating stagger with spring */}
{Array.from({ length: RECTS }).map((_, i) => {
// Accelerating: first one waits longer, subsequent come faster
const delays = [0, 0.55, 0.85]; // normalized: 0%, 55%, 85% through
const accelDelay = delays[i] * totalSec;
const accelFrame = Math.round(accelDelay * fps);
const elapsed = Math.max(0, frame - accelFrame);
const t = Math.min(elapsed / (fps * 0.4), 1);
const op = Math.min(t * 2.5, 1);
const sc = t === 0 ? 0.85 : interpolate(springPop(t), [0, 1], [0.85, 1]);
const y = startY + i * (rectH + gap);
return (
<div
key={`r-${i}`}
style={{
position: "absolute",
left: HALF_W + (HALF_W - rectW) / 2,
top: y,
width: rectW,
height: rectH,
overflow: "hidden",
opacity: op,
transform: `scale(${sc})`,
transformOrigin: "center center",
border: `3px solid ${colors[i]}`,
borderRadius: 12,
}}
>
<Img
src={staticFile(images[i])}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
);
})}
</AbsoluteFill>
);
};
// ─── Title / End card ──────────────────────────────────────
const TitleCard: React.FC<{ title: string; subtitle: string }> = ({ title, subtitle }) => {
const frame = useCurrentFrame();
const op = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const translateY = interpolate(frame, [0, 15], [30, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
return (
<AbsoluteFill
style={{
backgroundColor: "#0a0a0a",
justifyContent: "center",
alignItems: "center",
}}
>
<div style={{ opacity: op, transform: `translateY(${translateY}px)`, textAlign: "center" }}>
<div
style={{
fontSize: 72,
fontWeight: 900,
fontFamily: FONT,
color: "#fff",
letterSpacing: -2,
lineHeight: "1.1em",
}}
>
{title}
</div>
<div
style={{
fontSize: 32,
fontWeight: 400,
fontFamily: FONT,
color: "rgba(255,255,255,0.5)",
marginTop: 24,
}}
>
{subtitle}
</div>
</div>
</AbsoluteFill>
);
};
// ─── All curves overview ────────────────────────────────────
const CurveOverview: React.FC = () => {
const easeInOut = (t: number) => Easing.inOut(Easing.cubic)(t);
const easeOut = (t: number) => Easing.out(Easing.cubic)(t);
const easeIn = (t: number) => Easing.in(Easing.cubic)(t);
return (
<AbsoluteFill
style={{
backgroundColor: "#0a0a0a",
justifyContent: "center",
alignItems: "center",
}}
>
<SectionTitle text="SPEED CURVES" sub="Available presets for scenes" />
<CurveGraph
curveFn={(t) => t}
label="LINEAR (current)"
color="#ff6b6b"
x={60}
y={240}
w={440}
h={160}
/>
<CurveGraph
curveFn={easeInOut}
label="EASE IN-OUT"
color="#51cf66"
x={560}
y={240}
w={440}
h={160}
/>
<CurveGraph
curveFn={easeIn}
label="EASE IN"
color="#ffd43b"
x={60}
y={520}
w={440}
h={160}
/>
<CurveGraph
curveFn={easeOut}
label="EASE OUT"
color="#74c0fc"
x={560}
y={520}
w={440}
h={160}
/>
<CurveGraph
curveFn={punchCurve}
label="PUNCH"
color="#e599f7"
x={60}
y={800}
w={440}
h={160}
/>
<CurveGraph
curveFn={smoothstep}
label="SMOOTHSTEP"
color="#ff922b"
x={560}
y={800}
w={440}
h={160}
/>
</AbsoluteFill>
);
};
// ─── Main demo composition ─────────────────────────────────
export const SpeedRampDemo: React.FC = () => {
const SEC = 30; // frames per second
// Section timings (in frames)
const sections = [
{ start: 0, dur: 2.5 * SEC }, // Title
{ start: 2.5 * SEC, dur: 4 * SEC }, // Zoom comparison
{ start: 6.5 * SEC, dur: 4 * SEC }, // Slow push comparison
{ start: 10.5 * SEC, dur: 3.5 * SEC }, // Asset pop-in
{ start: 14 * SEC, dur: 4 * SEC }, // Punch reveal
{ start: 18 * SEC, dur: 4 * SEC }, // Collage stagger
{ start: 22 * SEC, dur: 4 * SEC }, // Curve overview
{ start: 26 * SEC, dur: 2 * SEC }, // End card
];
return (
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
<Sequence from={sections[0].start} durationInFrames={sections[0].dur}>
<TitleCard title="SPEED RAMPING" subtitle="Linear vs Eased motion comparison" />
</Sequence>
<Sequence from={sections[1].start} durationInFrames={sections[1].dur}>
<ZoomComparison />
</Sequence>
<Sequence from={sections[2].start} durationInFrames={sections[2].dur}>
<SlowPushComparison />
</Sequence>
<Sequence from={sections[3].start} durationInFrames={sections[3].dur}>
<PopInComparison />
</Sequence>
<Sequence from={sections[4].start} durationInFrames={sections[4].dur}>
<PunchComparison />
</Sequence>
<Sequence from={sections[5].start} durationInFrames={sections[5].dur}>
<CollageComparison />
</Sequence>
<Sequence from={sections[6].start} durationInFrames={sections[6].dur}>
<CurveOverview />
</Sequence>
<Sequence from={sections[7].start} durationInFrames={sections[7].dur}>
<TitleCard
title="NEXT STEP"
subtitle="Integrate into the render pipeline"
/>
</Sequence>
</AbsoluteFill>
);
};
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: SpeedRampDemo
// 0 props detected · 0 port(s)
Speed Ramp Demo
speed_ramp_demoSpeed Ramp Demo — Andy's hand-crafted Remotion scene. 0 props. no ports detected — source-only preview.
Workflow Inputs (0)
This block has no workflow-connected inputs.
Meta
- Updated
- 4/21/2026