Catalog

Edit Props (live)

Source

845 lines
import 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_demo

Speed 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