StealThis .dev
Remotion Medium

Wedding Invitation Video (Remotion)

An elegant portrait-format wedding invitation animated in Remotion — parchment-cream background, dusty-rose and gold palette, SVG botanical paths that draw themselves in from both sides and along a top crown, couple names entering in large italic serif typography with a spring-driven ampersand, a gold ornament border that traces the full frame, date and venue blocks fading in below, and a final shower of falling rose petals to close.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import React from "react";
import {
  AbsoluteFill,
  Composition,
  Easing,
  Sequence,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

// ── Palette & constants ───────────────────────────────────────────────────────
const CREAM = "#fdf6ee";
const IVORY = "#f9f0e3";
const DUSTY_ROSE = "#c9877a";
const DUSTY_ROSE_LIGHT = "#e8b4ac";
const DUSTY_ROSE_DEEP = "#a05f55";
const GOLD = "#c9a84c";
const GOLD_LIGHT = "#e8d08a";
const GOLD_DEEP = "#9a6f22";
const SAGE = "#8aad8a";
const SAGE_LIGHT = "#b3c9b3";
const TEXT_DARK = "#3d2b1f";
const TEXT_MID = "#7a5c4a";
const TEXT_LIGHT = "#b09480";

const BRIDE_NAME = "Isabella";
const GROOM_NAME = "Sebastian";
const WEDDING_DATE = "September 14, 2026";
const WEDDING_DAY = "Saturday";
const VENUE_NAME = "Villa Rosa Gardens";
const VENUE_CITY = "Tuscany, Italy";
const CEREMONY_TIME = "4:30 in the afternoon";
const INVITE_LINE = "Together with their families";
const REQUEST_LINE = "request the honour of your presence";

// ── Utility: clamp interpolate ────────────────────────────────────────────────
function interp(
  frame: number,
  input: [number, number],
  output: [number, number],
  easing?: (t: number) => number
) {
  return interpolate(frame, input, output, {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing,
  });
}

// ── Background: parchment texture + soft vignette ────────────────────────────
const Background: React.FC<{ frame: number }> = ({ frame }) => {
  const reveal = interp(frame, [0, 40], [0, 1], Easing.out(Easing.quad));

  return (
    <>
      {/* Base parchment */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: `linear-gradient(170deg, ${CREAM} 0%, ${IVORY} 40%, #f5ead8 80%, #f0e2cc 100%)`,
          opacity: reveal,
        }}
      />
      {/* Subtle grain overlay via repeating diagonal lines */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          backgroundImage: `repeating-linear-gradient(
            47deg,
            transparent 0px,
            transparent 18px,
            rgba(160, 120, 80, 0.018) 18px,
            rgba(160, 120, 80, 0.018) 19px
          )`,
          opacity: reveal,
        }}
      />
      {/* Warm center glow */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: `radial-gradient(ellipse 70% 55% at 50% 48%, rgba(255,240,210,0.55) 0%, transparent 80%)`,
          opacity: reveal,
        }}
      />
      {/* Vignette */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: `radial-gradient(ellipse 90% 90% at 50% 50%, transparent 55%, rgba(120,80,40,0.18) 100%)`,
          opacity: reveal,
          pointerEvents: "none",
        }}
      />
    </>
  );
};

// ── Decorative border that traces itself around the frame ─────────────────────
const TracingBorder: React.FC<{ frame: number }> = ({ frame }) => {
  // Border traces from 0% → 100% over frames 10-70
  const progress = interp(frame, [10, 70], [0, 1], Easing.inOut(Easing.cubic));
  const opacity = interp(frame, [8, 20], [0, 1]);

  // The border is four SVG lines: top, right, bottom, left
  // Total perimeter = 2*(1080+1920) = 6000 but we work in fractional units
  // We'll use a single SVG rect with strokeDasharray trick
  const W = 1080;
  const H = 1920;
  const INSET = 48;
  const rectW = W - INSET * 2;
  const rectH = H - INSET * 2;
  const perimeter = 2 * (rectW + rectH);
  const dashOffset = perimeter * (1 - progress);

  // Inner decorative border (slightly tighter)
  const INSET2 = 62;
  const rect2W = W - INSET2 * 2;
  const rect2H = H - INSET2 * 2;
  const perimeter2 = 2 * (rect2W + rect2H);
  const dash2Offset = perimeter2 * (1 - progress);

  return (
    <svg
      style={{ position: "absolute", inset: 0, opacity }}
      width={W}
      height={H}
      viewBox={`0 0 ${W} ${H}`}
    >
      {/* Outer gold border */}
      <rect
        x={INSET}
        y={INSET}
        width={rectW}
        height={rectH}
        rx={6}
        ry={6}
        fill="none"
        stroke={GOLD}
        strokeWidth={2.2}
        strokeDasharray={perimeter}
        strokeDashoffset={dashOffset}
        strokeLinecap="round"
      />
      {/* Inner rose border (slightly delayed) */}
      <rect
        x={INSET2}
        y={INSET2}
        width={rect2W}
        height={rect2H}
        rx={4}
        ry={4}
        fill="none"
        stroke={DUSTY_ROSE_LIGHT}
        strokeWidth={1.1}
        strokeDasharray={perimeter2}
        strokeDashoffset={dash2Offset}
        strokeLinecap="round"
        opacity={0.7}
      />
      {/* Corner diamond ornaments - appear after border is drawn */}
      {progress > 0.85 &&
        [
          [INSET, INSET],
          [W - INSET, INSET],
          [W - INSET, H - INSET],
          [INSET, H - INSET],
        ].map(([cx, cy], i) => {
          const cornerOpacity = interp(frame, [75 + i * 3, 88 + i * 3], [0, 1]);
          return (
            <g key={i} opacity={cornerOpacity}>
              <polygon
                points={`${cx},${cy - 9} ${cx + 6},${cy} ${cx},${cy + 9} ${cx - 6},${cy}`}
                fill={GOLD}
                opacity={0.85}
              />
              <polygon
                points={`${cx},${cy - 6} ${cx + 4},${cy} ${cx},${cy + 6} ${cx - 4},${cy}`}
                fill={GOLD_LIGHT}
                opacity={0.6}
              />
            </g>
          );
        })}
    </svg>
  );
};

// ── SVG floral branch drawing in from sides ───────────────────────────────────
const FloralLeft: React.FC<{ frame: number }> = ({ frame }) => {
  const drawProgress = interp(
    frame,
    [15, 90],
    [0, 1],
    Easing.inOut(Easing.cubic)
  );
  const opacity = interp(frame, [12, 28], [0, 1]);
  const translateX = spring({
    frame: Math.max(0, frame - 10),
    fps: 30,
    from: -40,
    to: 0,
    config: { damping: 18, stiffness: 80 },
  });

  // Main stem path (SVG path length ~ 420)
  const stemLength = 420;
  const stemDash = stemLength * (1 - drawProgress);

  // Leaves appear progressively
  const leaf1 = interp(frame, [40, 60], [0, 1], Easing.out(Easing.back(1.5)));
  const leaf2 = interp(frame, [52, 72], [0, 1], Easing.out(Easing.back(1.4)));
  const leaf3 = interp(frame, [60, 80], [0, 1], Easing.out(Easing.back(1.3)));
  const leaf4 = interp(frame, [70, 90], [0, 1], Easing.out(Easing.back(1.4)));
  const bloom1 = interp(frame, [65, 85], [0, 1], Easing.out(Easing.back(2)));
  const bloom2 = interp(frame, [78, 98], [0, 1], Easing.out(Easing.back(1.8)));

  return (
    <svg
      style={{
        position: "absolute",
        left: 40,
        top: 180,
        opacity,
        transform: `translateX(${translateX}px)`,
      }}
      width={260}
      height={680}
      viewBox="0 0 260 680"
    >
      {/* Main curving stem */}
      <path
        d="M 210 680 C 200 580 160 500 140 420 C 120 340 90 290 70 200 C 55 130 40 80 20 10"
        fill="none"
        stroke={SAGE}
        strokeWidth={2.5}
        strokeLinecap="round"
        strokeDasharray={stemLength}
        strokeDashoffset={stemDash}
      />
      {/* Secondary stem branch */}
      <path
        d="M 140 420 C 165 390 190 360 220 330"
        fill="none"
        stroke={SAGE}
        strokeWidth={1.8}
        strokeLinecap="round"
        strokeDasharray="160"
        strokeDashoffset={160 * (1 - interp(frame, [50, 80], [0, 1]))}
      />
      {/* Leaf 1 - large lower left */}
      <g
        transform={`translate(155, 510) rotate(-30) scale(${leaf1})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={28} ry={12} fill={SAGE} opacity={0.55} />
        <ellipse cx={0} cy={0} rx={26} ry={10} fill={SAGE_LIGHT} opacity={0.4} />
        <line x1={-25} y1={0} x2={22} y2={0} stroke={SAGE} strokeWidth={0.8} opacity={0.6} />
      </g>
      {/* Leaf 2 - mid */}
      <g
        transform={`translate(105, 380) rotate(25) scale(${leaf2})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={22} ry={9} fill={SAGE} opacity={0.5} />
        <ellipse cx={0} cy={0} rx={20} ry={7} fill={SAGE_LIGHT} opacity={0.35} />
      </g>
      {/* Leaf 3 - upper */}
      <g
        transform={`translate(65, 250) rotate(-20) scale(${leaf3})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={24} ry={10} fill={SAGE} opacity={0.5} />
        <ellipse cx={0} cy={0} rx={22} ry={8} fill={SAGE_LIGHT} opacity={0.35} />
        <line x1={-20} y1={0} x2={18} y2={0} stroke={SAGE} strokeWidth={0.8} opacity={0.5} />
      </g>
      {/* Leaf 4 - top */}
      <g
        transform={`translate(35, 130) rotate(15) scale(${leaf4})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={18} ry={7} fill={SAGE} opacity={0.45} />
      </g>
      {/* Bloom 1 - rose flower lower */}
      <g transform={`translate(175, 470) scale(${bloom1})`} style={{ transformOrigin: "0 0" }}>
        {[0, 60, 120, 180, 240, 300].map((angle, i) => (
          <ellipse
            key={i}
            cx={Math.cos((angle * Math.PI) / 180) * 14}
            cy={Math.sin((angle * Math.PI) / 180) * 14}
            rx={10}
            ry={6}
            fill={DUSTY_ROSE_LIGHT}
            opacity={0.65}
            transform={`rotate(${angle}, ${Math.cos((angle * Math.PI) / 180) * 14}, ${Math.sin((angle * Math.PI) / 180) * 14})`}
          />
        ))}
        <circle cx={0} cy={0} r={7} fill={DUSTY_ROSE} opacity={0.8} />
        <circle cx={0} cy={0} r={4} fill={GOLD_LIGHT} opacity={0.7} />
      </g>
      {/* Bloom 2 - smaller upper */}
      <g transform={`translate(50, 195) scale(${bloom2})`} style={{ transformOrigin: "0 0" }}>
        {[0, 72, 144, 216, 288].map((angle, i) => (
          <ellipse
            key={i}
            cx={Math.cos((angle * Math.PI) / 180) * 10}
            cy={Math.sin((angle * Math.PI) / 180) * 10}
            rx={7}
            ry={4.5}
            fill={DUSTY_ROSE_LIGHT}
            opacity={0.55}
            transform={`rotate(${angle}, ${Math.cos((angle * Math.PI) / 180) * 10}, ${Math.sin((angle * Math.PI) / 180) * 10})`}
          />
        ))}
        <circle cx={0} cy={0} r={5} fill={DUSTY_ROSE} opacity={0.75} />
        <circle cx={0} cy={0} r={2.5} fill={GOLD_LIGHT} opacity={0.65} />
      </g>
      {/* Small buds along stem */}
      {[
        { x: 130, y: 445, r: 4 },
        { x: 80, y: 310, r: 3.5 },
        { x: 55, y: 165, r: 3 },
        { x: 200, y: 340, r: 3.5 },
      ].map((bud, i) => {
        const budOp = interp(frame, [45 + i * 8, 62 + i * 8], [0, 1]);
        return (
          <g key={i} opacity={budOp}>
            <ellipse
              cx={bud.x}
              cy={bud.y}
              rx={bud.r * 1.2}
              ry={bud.r * 0.7}
              fill={DUSTY_ROSE}
              opacity={0.5}
            />
            <circle cx={bud.x} cy={bud.y} r={bud.r * 0.5} fill={GOLD} opacity={0.4} />
          </g>
        );
      })}
    </svg>
  );
};

const FloralRight: React.FC<{ frame: number }> = ({ frame }) => {
  const drawProgress = interp(
    frame,
    [20, 95],
    [0, 1],
    Easing.inOut(Easing.cubic)
  );
  const opacity = interp(frame, [17, 32], [0, 1]);
  const translateX = spring({
    frame: Math.max(0, frame - 15),
    fps: 30,
    from: 40,
    to: 0,
    config: { damping: 18, stiffness: 80 },
  });

  const stemLength = 440;
  const stemDash = stemLength * (1 - drawProgress);

  const leaf1 = interp(frame, [45, 65], [0, 1], Easing.out(Easing.back(1.5)));
  const leaf2 = interp(frame, [55, 75], [0, 1], Easing.out(Easing.back(1.4)));
  const leaf3 = interp(frame, [65, 85], [0, 1], Easing.out(Easing.back(1.3)));
  const bloom1 = interp(frame, [70, 92], [0, 1], Easing.out(Easing.back(1.9)));
  const bloom2 = interp(frame, [85, 108], [0, 1], Easing.out(Easing.back(1.7)));

  return (
    <svg
      style={{
        position: "absolute",
        right: 40,
        top: 200,
        opacity,
        transform: `translateX(${translateX}px)`,
      }}
      width={260}
      height={680}
      viewBox="0 0 260 680"
    >
      {/* Mirror of left stem */}
      <path
        d="M 50 680 C 60 580 100 500 120 420 C 140 340 170 290 190 200 C 205 130 220 80 240 10"
        fill="none"
        stroke={SAGE}
        strokeWidth={2.5}
        strokeLinecap="round"
        strokeDasharray={stemLength}
        strokeDashoffset={stemDash}
      />
      {/* Secondary branch */}
      <path
        d="M 120 420 C 95 390 70 360 40 330"
        fill="none"
        stroke={SAGE}
        strokeWidth={1.8}
        strokeLinecap="round"
        strokeDasharray="160"
        strokeDashoffset={160 * (1 - interp(frame, [55, 85], [0, 1]))}
      />
      {/* Leaves */}
      <g
        transform={`translate(105, 510) rotate(30) scale(${leaf1})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={28} ry={12} fill={SAGE} opacity={0.55} />
        <ellipse cx={0} cy={0} rx={26} ry={10} fill={SAGE_LIGHT} opacity={0.4} />
        <line x1={-25} y1={0} x2={22} y2={0} stroke={SAGE} strokeWidth={0.8} opacity={0.6} />
      </g>
      <g
        transform={`translate(155, 375) rotate(-25) scale(${leaf2})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={24} ry={10} fill={SAGE} opacity={0.5} />
        <ellipse cx={0} cy={0} rx={22} ry={8} fill={SAGE_LIGHT} opacity={0.35} />
      </g>
      <g
        transform={`translate(195, 240) rotate(18) scale(${leaf3})`}
        style={{ transformOrigin: "0 0" }}
      >
        <ellipse cx={0} cy={0} rx={22} ry={9} fill={SAGE} opacity={0.5} />
        <ellipse cx={0} cy={0} rx={20} ry={7} fill={SAGE_LIGHT} opacity={0.35} />
      </g>
      {/* Blooms */}
      <g transform={`translate(90, 470) scale(${bloom1})`} style={{ transformOrigin: "0 0" }}>
        {[0, 60, 120, 180, 240, 300].map((angle, i) => (
          <ellipse
            key={i}
            cx={Math.cos((angle * Math.PI) / 180) * 15}
            cy={Math.sin((angle * Math.PI) / 180) * 15}
            rx={11}
            ry={6.5}
            fill={DUSTY_ROSE_LIGHT}
            opacity={0.65}
            transform={`rotate(${angle + 15}, ${Math.cos((angle * Math.PI) / 180) * 15}, ${Math.sin((angle * Math.PI) / 180) * 15})`}
          />
        ))}
        <circle cx={0} cy={0} r={7.5} fill={DUSTY_ROSE} opacity={0.8} />
        <circle cx={0} cy={0} r={4} fill={GOLD_LIGHT} opacity={0.7} />
      </g>
      <g transform={`translate(210, 195) scale(${bloom2})`} style={{ transformOrigin: "0 0" }}>
        {[0, 72, 144, 216, 288].map((angle, i) => (
          <ellipse
            key={i}
            cx={Math.cos((angle * Math.PI) / 180) * 10}
            cy={Math.sin((angle * Math.PI) / 180) * 10}
            rx={7}
            ry={4}
            fill={DUSTY_ROSE_LIGHT}
            opacity={0.55}
            transform={`rotate(${angle + 10}, ${Math.cos((angle * Math.PI) / 180) * 10}, ${Math.sin((angle * Math.PI) / 180) * 10})`}
          />
        ))}
        <circle cx={0} cy={0} r={5} fill={DUSTY_ROSE} opacity={0.75} />
        <circle cx={0} cy={0} r={2.5} fill={GOLD_LIGHT} opacity={0.65} />
      </g>
      {/* Buds */}
      {[
        { x: 130, y: 440, r: 4 },
        { x: 175, y: 305, r: 3.5 },
        { x: 55, y: 340, r: 3 },
      ].map((bud, i) => {
        const budOp = interp(frame, [48 + i * 8, 65 + i * 8], [0, 1]);
        return (
          <g key={i} opacity={budOp}>
            <ellipse
              cx={bud.x}
              cy={bud.y}
              rx={bud.r * 1.2}
              ry={bud.r * 0.7}
              fill={DUSTY_ROSE}
              opacity={0.5}
            />
            <circle cx={bud.x} cy={bud.y} r={bud.r * 0.5} fill={GOLD} opacity={0.4} />
          </g>
        );
      })}
    </svg>
  );
};

// ── Top floral crown (center top) ─────────────────────────────────────────────
const FloralCrown: React.FC<{ frame: number }> = ({ frame }) => {
  const drawProgress = interp(frame, [25, 80], [0, 1], Easing.inOut(Easing.cubic));
  const opacity = interp(frame, [22, 40], [0, 1]);

  const centerArcLength = 600;
  const arcDash = centerArcLength * (1 - drawProgress);

  const bloom = (delay: number) =>
    interp(frame, [delay, delay + 22], [0, 1], Easing.out(Easing.back(2)));

  return (
    <svg
      style={{
        position: "absolute",
        top: 88,
        left: "50%",
        transform: "translateX(-50%)",
        opacity,
      }}
      width={600}
      height={200}
      viewBox="0 0 600 200"
    >
      {/* Horizontal vine arc */}
      <path
        d="M 30 140 C 80 100 150 80 200 90 C 250 100 280 85 300 80 C 320 75 350 95 400 90 C 450 85 520 100 570 140"
        fill="none"
        stroke={SAGE}
        strokeWidth={2.2}
        strokeLinecap="round"
        strokeDasharray={centerArcLength}
        strokeDashoffset={arcDash}
      />
      {/* Shorter inner arc */}
      <path
        d="M 100 145 C 150 118 220 108 300 104 C 380 100 450 112 500 140"
        fill="none"
        stroke={SAGE_LIGHT}
        strokeWidth={1.4}
        strokeLinecap="round"
        strokeDasharray="420"
        strokeDashoffset={420 * (1 - interp(frame, [30, 85], [0, 1]))}
        opacity={0.6}
      />
      {/* Central large bloom */}
      <g transform={`translate(300, 72) scale(${bloom(62)})`} style={{ transformOrigin: "0 0" }}>
        {[0, 45, 90, 135, 180, 225, 270, 315].map((angle, i) => (
          <ellipse
            key={i}
            cx={Math.cos((angle * Math.PI) / 180) * 18}
            cy={Math.sin((angle * Math.PI) / 180) * 18}
            rx={14}
            ry={7}
            fill={i % 2 === 0 ? DUSTY_ROSE_LIGHT : "#f2c8c2"}
            opacity={0.7}
            transform={`rotate(${angle}, ${Math.cos((angle * Math.PI) / 180) * 18}, ${Math.sin((angle * Math.PI) / 180) * 18})`}
          />
        ))}
        <circle cx={0} cy={0} r={9} fill={DUSTY_ROSE} opacity={0.85} />
        <circle cx={0} cy={0} r={5} fill={GOLD} opacity={0.75} />
      </g>
      {/* Side blooms */}
      {[
        { x: 165, y: 82, scale: bloom(68), petals: 5, size: 11 },
        { x: 435, y: 82, scale: bloom(72), petals: 5, size: 11 },
        { x: 80, y: 118, scale: bloom(75), petals: 5, size: 8 },
        { x: 520, y: 118, scale: bloom(78), petals: 5, size: 8 },
      ].map((b, idx) => (
        <g
          key={idx}
          transform={`translate(${b.x}, ${b.y}) scale(${b.scale})`}
          style={{ transformOrigin: "0 0" }}
        >
          {Array.from({ length: b.petals }).map((_, i) => {
            const angle = (i / b.petals) * 360;
            return (
              <ellipse
                key={i}
                cx={Math.cos((angle * Math.PI) / 180) * b.size}
                cy={Math.sin((angle * Math.PI) / 180) * b.size}
                rx={b.size * 0.85}
                ry={b.size * 0.45}
                fill={DUSTY_ROSE_LIGHT}
                opacity={0.6}
                transform={`rotate(${angle}, ${Math.cos((angle * Math.PI) / 180) * b.size}, ${Math.sin((angle * Math.PI) / 180) * b.size})`}
              />
            );
          })}
          <circle cx={0} cy={0} r={b.size * 0.45} fill={DUSTY_ROSE} opacity={0.8} />
          <circle cx={0} cy={0} r={b.size * 0.22} fill={GOLD_LIGHT} opacity={0.7} />
        </g>
      ))}
      {/* Scattered leaves along vine */}
      {[
        { x: 130, y: 105, rot: 30, rx: 16, ry: 6 },
        { x: 240, y: 90, rot: -20, rx: 14, ry: 5.5 },
        { x: 360, y: 90, rot: 22, rx: 14, ry: 5.5 },
        { x: 470, y: 105, rot: -28, rx: 16, ry: 6 },
      ].map((l, i) => {
        const leafOp = interp(frame, [40 + i * 6, 58 + i * 6], [0, 1]);
        return (
          <ellipse
            key={i}
            cx={l.x}
            cy={l.y}
            rx={l.rx}
            ry={l.ry}
            fill={SAGE}
            opacity={leafOp * 0.55}
            transform={`rotate(${l.rot}, ${l.x}, ${l.y})`}
          />
        );
      })}
    </svg>
  );
};

// ── Gold divider ornament ─────────────────────────────────────────────────────
const GoldDivider: React.FC<{ frame: number; startFrame: number; y?: number }> = ({
  frame,
  startFrame,
  y = 0,
}) => {
  const progress = interp(
    frame,
    [startFrame, startFrame + 30],
    [0, 1],
    Easing.out(Easing.cubic)
  );
  const opacity = interp(frame, [startFrame, startFrame + 12], [0, 1]);
  const lineLength = 280;
  const lineDash = lineLength * (1 - progress);

  return (
    <svg
      style={{ position: "absolute", left: "50%", top: y, transform: "translateX(-50%)", opacity }}
      width={600}
      height={30}
      viewBox="0 0 600 30"
    >
      {/* Left line */}
      <line
        x1={300}
        y1={15}
        x2={300 - lineLength * progress}
        y2={15}
        stroke={GOLD}
        strokeWidth={1.2}
        strokeLinecap="round"
        opacity={0.7}
      />
      {/* Right line */}
      <line
        x1={300}
        y1={15}
        x2={300 + lineLength * progress}
        y2={15}
        stroke={GOLD}
        strokeWidth={1.2}
        strokeLinecap="round"
        opacity={0.7}
      />
      {/* Center diamond */}
      <g opacity={progress}>
        <polygon
          points="300,6 308,15 300,24 292,15"
          fill={GOLD}
          opacity={0.8}
        />
        <polygon
          points="300,10 305,15 300,20 295,15"
          fill={GOLD_LIGHT}
          opacity={0.6}
        />
      </g>
      {/* Side decorations */}
      <g opacity={interp(frame, [startFrame + 20, startFrame + 35], [0, 1])}>
        <circle cx={300 - lineLength + 8} cy={15} r={3} fill={GOLD} opacity={0.6} />
        <circle cx={300 + lineLength - 8} cy={15} r={3} fill={GOLD} opacity={0.6} />
        <circle cx={300 - lineLength * 0.5} cy={15} r={2} fill={GOLD} opacity={0.45} />
        <circle cx={300 + lineLength * 0.5} cy={15} r={2} fill={GOLD} opacity={0.45} />
      </g>
    </svg>
  );
};

// ── Typography: announcement line ─────────────────────────────────────────────
const AnnouncementLine: React.FC<{ frame: number }> = ({ frame }) => {
  const opacity = interp(frame, [40, 62], [0, 1], Easing.out(Easing.quad));
  const translateY = spring({
    frame: Math.max(0, frame - 40),
    fps: 30,
    from: 16,
    to: 0,
    config: { damping: 18, stiffness: 100 },
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 310,
        left: 0,
        right: 0,
        textAlign: "center",
        opacity,
        transform: `translateY(${translateY}px)`,
      }}
    >
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 28,
          fontWeight: 400,
          color: TEXT_MID,
          letterSpacing: 3.5,
          textTransform: "uppercase",
        }}
      >
        {INVITE_LINE}
      </div>
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 26,
          fontWeight: 400,
          color: TEXT_LIGHT,
          letterSpacing: 2,
          marginTop: 6,
          fontStyle: "italic",
        }}
      >
        {REQUEST_LINE}
      </div>
    </div>
  );
};

// ── Main names ────────────────────────────────────────────────────────────────
const CoupleNames: React.FC<{ frame: number; fps: number }> = ({
  frame,
  fps,
}) => {
  const brideDelay = 72;
  const groomDelay = 100;
  const ampersandDelay = 90;

  const brideOpacity = interp(frame, [brideDelay, brideDelay + 20], [0, 1]);
  const groomOpacity = interp(frame, [groomDelay, groomDelay + 20], [0, 1]);
  const ampOpacity = interp(frame, [ampersandDelay, ampersandDelay + 18], [0, 1]);
  const ampScale = spring({
    frame: Math.max(0, frame - ampersandDelay),
    fps,
    from: 0.4,
    to: 1,
    config: { damping: 11, stiffness: 120 },
  });

  const brideTranslate = spring({
    frame: Math.max(0, frame - brideDelay),
    fps,
    from: -30,
    to: 0,
    config: { damping: 16, stiffness: 90 },
  });
  const groomTranslate = spring({
    frame: Math.max(0, frame - groomDelay),
    fps,
    from: 30,
    to: 0,
    config: { damping: 16, stiffness: 90 },
  });

  // Subtle shimmer on names
  const shimmer = interp(
    frame,
    [90, 130, 150],
    [0, 0.12, 0],
    Easing.inOut(Easing.sin)
  );

  return (
    <div
      style={{
        position: "absolute",
        top: 760,
        left: 0,
        right: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: 0,
      }}
    >
      {/* Bride name */}
      <div
        style={{
          opacity: brideOpacity,
          transform: `translateX(${brideTranslate}px)`,
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 118,
          fontWeight: 400,
          fontStyle: "italic",
          color: TEXT_DARK,
          letterSpacing: -1,
          lineHeight: 1.05,
          textShadow: shimmer > 0
            ? `0 0 ${shimmer * 100}px ${GOLD}88`
            : "none",
        }}
      >
        {BRIDE_NAME}
      </div>

      {/* Ampersand */}
      <div
        style={{
          opacity: ampOpacity,
          transform: `scale(${ampScale})`,
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 86,
          fontWeight: 400,
          fontStyle: "italic",
          color: DUSTY_ROSE,
          lineHeight: 0.9,
          margin: "4px 0",
        }}
      >
        &amp;
      </div>

      {/* Groom name */}
      <div
        style={{
          opacity: groomOpacity,
          transform: `translateX(${groomTranslate}px)`,
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 118,
          fontWeight: 400,
          fontStyle: "italic",
          color: TEXT_DARK,
          letterSpacing: -1,
          lineHeight: 1.05,
          textShadow: shimmer > 0
            ? `0 0 ${shimmer * 100}px ${GOLD}88`
            : "none",
        }}
      >
        {GROOM_NAME}
      </div>
    </div>
  );
};

// ── Date & venue block ────────────────────────────────────────────────────────
const DateVenueBlock: React.FC<{ frame: number; fps: number }> = ({
  frame,
  fps,
}) => {
  const blockDelay = 118;
  const blockOpacity = interp(frame, [blockDelay, blockDelay + 22], [0, 1]);
  const blockTranslate = spring({
    frame: Math.max(0, frame - blockDelay),
    fps,
    from: 20,
    to: 0,
    config: { damping: 16, stiffness: 90 },
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 1240,
        left: 0,
        right: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: 0,
        opacity: blockOpacity,
        transform: `translateY(${blockTranslate}px)`,
      }}
    >
      {/* Day of week */}
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 22,
          fontWeight: 400,
          color: TEXT_LIGHT,
          letterSpacing: 6,
          textTransform: "uppercase",
          marginBottom: 4,
        }}
      >
        {WEDDING_DAY}
      </div>
      {/* Date */}
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 44,
          fontWeight: 400,
          color: TEXT_DARK,
          letterSpacing: 1.5,
          lineHeight: 1.1,
        }}
      >
        {WEDDING_DATE}
      </div>
      {/* Time */}
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 24,
          fontWeight: 400,
          fontStyle: "italic",
          color: TEXT_MID,
          letterSpacing: 1,
          marginTop: 8,
        }}
      >
        {CEREMONY_TIME}
      </div>
    </div>
  );
};

const VenueBlock: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const blockDelay = 130;
  const blockOpacity = interp(frame, [blockDelay, blockDelay + 22], [0, 1]);
  const blockTranslate = spring({
    frame: Math.max(0, frame - blockDelay),
    fps,
    from: 20,
    to: 0,
    config: { damping: 16, stiffness: 90 },
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 1440,
        left: 0,
        right: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        opacity: blockOpacity,
        transform: `translateY(${blockTranslate}px)`,
      }}
    >
      {/* Venue name */}
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 38,
          fontWeight: 400,
          color: TEXT_DARK,
          letterSpacing: 0.5,
          lineHeight: 1.2,
          textAlign: "center",
        }}
      >
        {VENUE_NAME}
      </div>
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 26,
          fontWeight: 400,
          fontStyle: "italic",
          color: TEXT_MID,
          letterSpacing: 2,
          marginTop: 6,
        }}
      >
        {VENUE_CITY}
      </div>
    </div>
  );
};

// ── Rose petal confetti ────────────────────────────────────────────────────────
type PetalData = {
  id: number;
  startX: number;
  startY: number;
  size: number;
  speed: number;
  sway: number;
  swayFreq: number;
  rotSpeed: number;
  delay: number;
  color: string;
};

const PETAL_COUNT = 38;

const seededRandom = (seed: number) => {
  const x = Math.sin(seed + 1) * 10000;
  return x - Math.floor(x);
};

const PETALS: PetalData[] = Array.from({ length: PETAL_COUNT }, (_, i) => {
  const r1 = seededRandom(i * 7);
  const r2 = seededRandom(i * 7 + 1);
  const r3 = seededRandom(i * 7 + 2);
  const r4 = seededRandom(i * 7 + 3);
  const r5 = seededRandom(i * 7 + 4);
  const r6 = seededRandom(i * 7 + 5);
  const colors = [DUSTY_ROSE_LIGHT, DUSTY_ROSE, "#f2c8c2", "#e8d0c8", GOLD_LIGHT, "#fce4e0"];
  return {
    id: i,
    startX: r1 * 1080,
    startY: -20 - r2 * 80,
    size: 14 + r3 * 22,
    speed: 180 + r4 * 280,
    sway: 40 + r5 * 80,
    swayFreq: 0.8 + r6 * 1.4,
    rotSpeed: 60 + seededRandom(i * 7 + 6) * 200,
    delay: 140 + Math.floor(seededRandom(i * 13) * 30),
    color: colors[Math.floor(seededRandom(i * 11) * colors.length)],
  };
});

const RosePetalConfetti: React.FC<{ frame: number }> = ({ frame }) => {
  const containerOpacity = interp(frame, [138, 150], [0, 1]);

  return (
    <div style={{ position: "absolute", inset: 0, opacity: containerOpacity, pointerEvents: "none" }}>
      {PETALS.map((petal) => {
        const localFrame = frame - petal.delay;
        if (localFrame <= 0) return null;

        const fallT = localFrame / 30; // seconds
        const fallDist = fallT * (petal.speed / 6);
        const y = petal.startY + fallDist;
        const x =
          petal.startX +
          Math.sin(fallT * petal.swayFreq * Math.PI) * petal.sway;
        const rotation = fallT * petal.rotSpeed;
        const opacity = Math.min(1, localFrame / 12) * (1 - Math.max(0, (y - 1800) / 200));

        if (y > 2000 || opacity <= 0) return null;

        return (
          <div
            key={petal.id}
            style={{
              position: "absolute",
              left: x,
              top: y,
              width: petal.size,
              height: petal.size * 0.65,
              background: petal.color,
              borderRadius: "50% 30% 50% 30%",
              transform: `rotate(${rotation}deg)`,
              opacity: opacity * 0.75,
              boxShadow: `0 1px 4px rgba(120,60,50,0.12)`,
            }}
          />
        );
      })}
    </div>
  );
};

// ── Bottom footer text ────────────────────────────────────────────────────────
const FooterText: React.FC<{ frame: number }> = ({ frame }) => {
  const opacity = interp(frame, [140, 158], [0, 1]);

  return (
    <div
      style={{
        position: "absolute",
        bottom: 110,
        left: 0,
        right: 0,
        textAlign: "center",
        opacity,
      }}
    >
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 20,
          fontWeight: 400,
          fontStyle: "italic",
          color: TEXT_LIGHT,
          letterSpacing: 2,
        }}
      >
        Reception to follow
      </div>
      <div
        style={{
          fontFamily: "Georgia, 'Times New Roman', serif",
          fontSize: 16,
          fontWeight: 400,
          color: TEXT_LIGHT,
          letterSpacing: 3,
          marginTop: 6,
          opacity: 0.7,
          textTransform: "uppercase",
        }}
      >
        Black tie preferred
      </div>
    </div>
  );
};

// ── Main composition component ────────────────────────────────────────────────
export const RemotionWeddingInvite: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  // Global fade-out in the last 15 frames
  const globalOpacity = interpolate(
    frame,
    [durationInFrames - 15, durationInFrames],
    [1, 0],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  );

  // Subtle "breathing" scale on the whole composition
  const breathe = interpolate(
    frame,
    [0, 90, 180],
    [1, 1.008, 1],
    {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
      easing: Easing.inOut(Easing.sin),
    }
  );

  return (
    <AbsoluteFill
      style={{
        backgroundColor: CREAM,
        opacity: globalOpacity,
        overflow: "hidden",
        transform: `scale(${breathe})`,
      }}
    >
      {/* Layer 1: Parchment background */}
      <Background frame={frame} />

      {/* Layer 2: Decorative tracing border */}
      <TracingBorder frame={frame} />

      {/* Layer 3: Floral botanicals */}
      <FloralLeft frame={frame} />
      <FloralRight frame={frame} />
      <FloralCrown frame={frame} />

      {/* Layer 4: Announcement copy */}
      <AnnouncementLine frame={frame} />

      {/* Layer 5: Gold divider top */}
      <Sequence from={60}>
        <GoldDivider frame={frame} startFrame={60} y={714} />
      </Sequence>

      {/* Layer 6: Couple names */}
      <CoupleNames frame={frame} fps={fps} />

      {/* Layer 7: Gold divider middle */}
      <Sequence from={112}>
        <GoldDivider frame={frame} startFrame={112} y={1198} />
      </Sequence>

      {/* Layer 8: Date + venue */}
      <DateVenueBlock frame={frame} fps={fps} />
      <VenueBlock frame={frame} fps={fps} />

      {/* Layer 9: Gold divider bottom */}
      <Sequence from={140}>
        <GoldDivider frame={frame} startFrame={140} y={1580} />
      </Sequence>

      {/* Layer 10: Footer */}
      <FooterText frame={frame} />

      {/* Layer 11: Rose petal confetti */}
      <RosePetalConfetti frame={frame} />
    </AbsoluteFill>
  );
};

// ── Remotion Root ─────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
  <Composition
    id="RemotionWeddingInvite"
    component={RemotionWeddingInvite}
    durationInFrames={180}
    fps={30}
    width={1080}
    height={1920}
  />
);

Wedding Invitation Video

A romantic 6-second animated wedding invitation crafted entirely in Remotion. The composition opens on a warm parchment background that blooms into view, immediately followed by a gold-and-dusty-rose decorative border tracing itself around all four edges using SVG strokeDashoffset animation. Simultaneously, botanical floral branches draw in from both sides and a crown of blooms unfurls across the top, built from SVG paths and procedurally placed ellipse petals with staggered spring entrances.

The typographic centrepiece — the couple’s names in large italic serif at 118 px — enters with opposing horizontal spring slides, separated by a softly scaled gold ampersand. Each name carries a subtle gold shimmer that pulses once at peak presence. Below the names, the wedding date, day, time, venue, and location cascade in with gentle upward spring transitions, each introduced by a spreading gold line-and-diamond ornament divider.

The final quarter of the composition bursts into a confetti of 38 rose petals that fall, sway sinusoidally, and rotate at different speeds — all computed deterministically from a seeded pseudo-random generator so the output is frame-perfect reproducible. A quiet breathing scale on the root container and a smooth fade-out in the last 15 frames complete the cinematic feel.

Composition specs

PropertyValue
Resolution1080 × 1920
FPS30
Duration6 s (180 frames)

Timeline

TimeAction
0 – 0.8 s (0 – 25 f)Parchment background fades in; gold border begins tracing the frame
0.5 – 2.5 s (15 – 75 f)Botanical stems draw in from left, right, and top crown; leaves and blooms spring in with staggered delays
1.3 – 3.5 s (40 – 105 f)Announcement copy, couple names, and ampersand enter; first and second gold dividers spread outward
3.5 – 5.0 s (105 – 150 f)Date, time, venue block, and footer text fade and slide in; third gold divider appears
5.0 – 6.0 s (150 – 180 f)38 rose petals fall with sinusoidal sway; global fade-out begins at frame 165