Sports Highlight Reel (Remotion)
A cinematic multi-segment sports highlight reel intro built with Remotion for SportsPulse TV. Covers four key match plays — goal, save, red card, and final goal — each with spring-driven stat cards, lower-third chyrons, and a play counter bar. Features a dramatic dark broadcast aesthetic, yellow and red accent palette, animated confetti on the winning goal, and a polished outro with the final score split across both teams.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Easing,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
// ── Constants ─────────────────────────────────────────────────────────────────
const SHOW_NAME = "SportsPulse TV";
const SHOW_TAGLINE = "MATCH HIGHLIGHTS";
const TEAM_A = "CHICAGO HAWKS";
const TEAM_B = "DALLAS WOLVES";
const FINAL_SCORE_A = 3;
const FINAL_SCORE_B = 1;
const FINAL_SCORE = `${TEAM_A} ${FINAL_SCORE_A} — ${TEAM_B} ${FINAL_SCORE_B}`;
const WEBSITE = "SportsPulse.tv";
const ACCENT_YELLOW = "#f5c842";
const ACCENT_RED = "#e8001e";
const ACCENT_CYAN = "#00d4ff";
const BG_PRIMARY = "#0a0e1a";
const BG_SECONDARY = "#0f1520";
const TEXT_WHITE = "#ffffff";
const TEXT_DIM = "#8899aa";
interface Play {
type: string;
time: string;
name: string;
number: number;
position: string;
stat: string;
accent: string;
teamLabel: string;
}
const PLAYS: Play[] = [
{
type: "GOAL",
time: "14'",
name: "J. MARTINEZ",
number: 9,
position: "FORWARD",
stat: "14G 7A 22SH",
accent: ACCENT_YELLOW,
teamLabel: TEAM_A,
},
{
type: "SAVE",
time: "31'",
name: "R. OKONKWO",
number: 1,
position: "GOALKEEPER",
stat: "5CS 31SV 72%",
accent: ACCENT_CYAN,
teamLabel: TEAM_B,
},
{
type: "RED CARD",
time: "58'",
name: "K. BANKS",
number: 4,
position: "DEFENDER",
stat: "3G 1A 8YC",
accent: ACCENT_RED,
teamLabel: TEAM_B,
},
{
type: "GOAL",
time: "87'",
name: "L. CHEN",
number: 11,
position: "MIDFIELDER",
stat: "8G 12A 19SH",
accent: ACCENT_YELLOW,
teamLabel: TEAM_A,
},
];
// ── Scene boundaries ──────────────────────────────────────────────────────────
const SCENE_INTRO_START = 0;
const SCENE_INTRO_END = 40;
const SCENE_PLAY1_START = 40;
const SCENE_PLAY1_END = 90;
const SCENE_PLAY2_START = 90;
const SCENE_PLAY2_END = 140;
const SCENE_PLAY3_START = 140;
const SCENE_PLAY3_END = 200;
const SCENE_PLAY4_START = 200;
const SCENE_PLAY4_END = 260;
const SCENE_OUTRO_START = 260;
// ── Helpers ───────────────────────────────────────────────────────────────────
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function sceneFrame(frame: number, start: number) {
return Math.max(0, frame - start);
}
function sceneOpacity(
frame: number,
start: number,
end: number,
fadeIn = 6,
fadeOut = 8
): number {
const f = frame - start;
const dur = end - start;
if (f < 0 || frame >= end) return 0;
if (f < fadeIn) return f / fadeIn;
if (f > dur - fadeOut) return (dur - f) / fadeOut;
return 1;
}
// ── Sub-components ────────────────────────────────────────────────────────────
/** Animated stadium noise-like background with SVG turbulence grid */
const Background: React.FC<{ flashRed?: boolean; flashProgress?: number }> = ({
flashRed = false,
flashProgress = 0,
}) => {
const flashOverlay = flashRed
? `rgba(232, 0, 30, ${0.22 * Math.sin(flashProgress * Math.PI)})`
: "transparent";
return (
<AbsoluteFill style={{ background: BG_PRIMARY }}>
{/* Grid lines for broadcast feel */}
<svg
width="1280"
height="720"
style={{ position: "absolute", top: 0, left: 0, opacity: 0.07 }}
>
<defs>
<pattern
id="grid"
width="80"
height="80"
patternUnits="userSpaceOnUse"
>
<path
d="M 80 0 L 0 0 0 80"
fill="none"
stroke="#ffffff"
strokeWidth="0.5"
/>
</pattern>
{/* Animated noise texture */}
<filter id="noise">
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
stitchTiles="stitch"
/>
<feColorMatrix type="saturate" values="0" />
<feBlend in="SourceGraphic" mode="multiply" />
</filter>
</defs>
<rect width="1280" height="720" fill="url(#grid)" />
{/* Diagonal accent lines */}
<line
x1="0"
y1="720"
x2="400"
y2="0"
stroke={ACCENT_YELLOW}
strokeWidth="0.4"
opacity="0.3"
/>
<line
x1="880"
y1="0"
x2="1280"
y2="720"
stroke={ACCENT_YELLOW}
strokeWidth="0.4"
opacity="0.3"
/>
</svg>
{/* Vignette overlay */}
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.7) 100%)",
}}
/>
{/* Red flash overlay for red card event */}
<div
style={{
position: "absolute",
inset: 0,
background: flashOverlay,
pointerEvents: "none",
}}
/>
{/* Bottom gradient for lower-third area */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 200,
background: `linear-gradient(to top, ${BG_SECONDARY}ee, transparent)`,
}}
/>
</AbsoluteFill>
);
};
/** Top bar with show branding */
const TopBar: React.FC<{ opacity: number; sceneLabel: string }> = ({
opacity,
sceneLabel,
}) => (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 52,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
paddingLeft: 32,
paddingRight: 32,
background: `linear-gradient(to bottom, rgba(10,14,26,0.95), rgba(10,14,26,0.5))`,
opacity,
}}
>
{/* Show name */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div
style={{
width: 28,
height: 28,
background: ACCENT_YELLOW,
borderRadius: 4,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" fill="#0a0e1a" />
<path
d="M5 8 L7 10 L11 6"
stroke="#f5c842"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
</div>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 800,
fontSize: 15,
color: TEXT_WHITE,
letterSpacing: "0.08em",
textTransform: "uppercase",
}}
>
{SHOW_NAME}
</span>
</div>
{/* Live badge + scene label */}
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 11,
color: TEXT_DIM,
letterSpacing: "0.1em",
textTransform: "uppercase",
}}
>
{sceneLabel}
</span>
<div
style={{
background: ACCENT_RED,
borderRadius: 3,
padding: "2px 8px",
display: "flex",
alignItems: "center",
gap: 5,
}}
>
<div
style={{
width: 6,
height: 6,
borderRadius: "50%",
background: TEXT_WHITE,
}}
/>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 700,
fontSize: 11,
color: TEXT_WHITE,
letterSpacing: "0.08em",
}}
>
LIVE
</span>
</div>
</div>
</div>
);
/** Play counter bar — 4 segments, active one lit in given color */
const PlayCounterBar: React.FC<{
activeIndex: number;
accentColor: string;
barProgress: number;
}> = ({ activeIndex, accentColor, barProgress }) => (
<div
style={{
position: "absolute",
bottom: 88,
left: 32,
display: "flex",
gap: 6,
alignItems: "center",
transform: `translateY(${interpolate(barProgress, [0, 1], [20, 0])})`,
opacity: barProgress,
}}
>
{PLAYS.map((_, i) => (
<div
key={i}
style={{
width: 36,
height: 5,
borderRadius: 2,
background:
i < activeIndex
? "#ffffff44"
: i === activeIndex
? accentColor
: "#ffffff1a",
transition: "background 0.3s",
}}
/>
))}
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 11,
color: TEXT_DIM,
letterSpacing: "0.1em",
marginLeft: 6,
textTransform: "uppercase",
}}
>
PLAY {activeIndex + 1} OF {PLAYS.length}
</span>
</div>
);
/** Lower-third chyron for play events */
const LowerThird: React.FC<{
play: Play;
slideX: number;
opacity: number;
}> = ({ play, slideX, opacity }) => (
<div
style={{
position: "absolute",
bottom: 40,
left: 0,
right: 0,
display: "flex",
flexDirection: "column",
gap: 0,
transform: `translateX(${slideX}px)`,
opacity,
}}
>
{/* Event type bar */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 0,
marginLeft: 32,
}}
>
<div
style={{
background: play.accent,
padding: "5px 16px",
borderRadius: "3px 0 0 3px",
}}
>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 14,
color: play.accent === ACCENT_YELLOW ? "#0a0e1a" : TEXT_WHITE,
letterSpacing: "0.12em",
textTransform: "uppercase",
}}
>
{play.type}
</span>
</div>
<div
style={{
background: "#1a2035",
padding: "5px 14px",
borderRadius: "0 3px 3px 0",
borderLeft: `2px solid ${play.accent}`,
}}
>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 600,
fontSize: 13,
color: TEXT_DIM,
letterSpacing: "0.06em",
}}
>
{play.teamLabel} · {play.time}
</span>
</div>
</div>
</div>
);
/** Player stat card — springs in from the right */
const StatCard: React.FC<{
play: Play;
springX: number;
opacity: number;
}> = ({ play, springX, opacity }) => (
<div
style={{
position: "absolute",
right: 40,
top: "50%",
transform: `translateY(-50%) translateX(${springX}px)`,
opacity,
width: 280,
background: "#10192e",
border: `1px solid ${play.accent}33`,
borderRadius: 8,
overflow: "hidden",
}}
>
{/* Card header accent bar */}
<div
style={{
height: 4,
background: `linear-gradient(to right, ${play.accent}, ${play.accent}55)`,
}}
/>
<div style={{ padding: "16px 20px" }}>
{/* Jersey number + name */}
<div style={{ display: "flex", alignItems: "flex-end", gap: 12, marginBottom: 4 }}>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 52,
color: `${play.accent}22`,
lineHeight: 1,
letterSpacing: "-0.02em",
}}
>
{play.number}
</span>
<div style={{ paddingBottom: 6 }}>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 800,
fontSize: 18,
color: TEXT_WHITE,
letterSpacing: "0.04em",
}}
>
{play.name}
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 500,
fontSize: 11,
color: play.accent,
letterSpacing: "0.12em",
textTransform: "uppercase",
marginTop: 2,
}}
>
#{play.number} · {play.position}
</div>
</div>
</div>
{/* Divider */}
<div
style={{
height: 1,
background: `${play.accent}22`,
marginBottom: 12,
}}
/>
{/* Season stats */}
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 10,
color: TEXT_DIM,
letterSpacing: "0.1em",
textTransform: "uppercase",
marginBottom: 6,
}}
>
Season Stats
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 700,
fontSize: 15,
color: TEXT_WHITE,
letterSpacing: "0.08em",
fontVariantNumeric: "tabular-nums",
}}
>
{play.stat}
</div>
</div>
</div>
);
/** Confetti burst — colored rectangles */
const ConfettiBurst: React.FC<{ progress: number }> = ({ progress }) => {
const pieces = [
{ x: 640, y: 280, dx: -180, dy: -200, color: ACCENT_YELLOW, r: 0 },
{ x: 640, y: 280, dx: 160, dy: -180, color: ACCENT_RED, r: 45 },
{ x: 640, y: 280, dx: -80, dy: -220, color: TEXT_WHITE, r: -30 },
{ x: 640, y: 280, dx: 200, dy: -140, color: ACCENT_CYAN, r: 60 },
{ x: 640, y: 280, dx: -220, dy: -120, color: ACCENT_YELLOW, r: -20 },
{ x: 640, y: 280, dx: 100, dy: -240, color: ACCENT_RED, r: 15 },
{ x: 640, y: 280, dx: -140, dy: -160, color: ACCENT_CYAN, r: -45 },
{ x: 640, y: 280, dx: 240, dy: -200, color: TEXT_WHITE, r: 30 },
{ x: 640, y: 280, dx: -60, dy: -260, color: ACCENT_YELLOW, r: -60 },
{ x: 640, y: 280, dx: 180, dy: -100, color: ACCENT_RED, r: 20 },
{ x: 640, y: 280, dx: -200, dy: -80, color: ACCENT_CYAN, r: -15 },
{ x: 640, y: 280, dx: 60, dy: -200, color: TEXT_WHITE, r: 50 },
];
const eased = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;
return (
<div style={{ position: "absolute", inset: 0, pointerEvents: "none" }}>
{pieces.map((p, i) => {
const delay = i * 0.04;
const adj = clamp((progress - delay) / (1 - delay), 0, 1);
const easedAdj = adj < 0.5
? 2 * adj * adj
: 1 - Math.pow(-2 * adj + 2, 2) / 2;
const px = p.x + p.dx * easedAdj;
const py = p.y + p.dy * easedAdj + 80 * easedAdj * easedAdj;
const opacity = adj < 0.7 ? adj / 0.7 : (1 - adj) / 0.3;
const rot = p.r * easedAdj;
return (
<div
key={i}
style={{
position: "absolute",
left: px - 6,
top: py - 3,
width: 12,
height: 6,
background: p.color,
transform: `rotate(${rot}deg)`,
opacity: clamp(opacity, 0, 1),
borderRadius: 1,
}}
/>
);
})}
</div>
);
};
/** Large score display for Play 4 */
const ScoreUpdate: React.FC<{
scoreScale: number;
opacity: number;
currentScore: number;
}> = ({ scoreScale, opacity, currentScore }) => (
<div
style={{
position: "absolute",
left: "50%",
top: "50%",
transform: `translate(-50%, -50%) scale(${scoreScale})`,
opacity,
textAlign: "center",
}}
>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 11,
color: TEXT_DIM,
letterSpacing: "0.2em",
textTransform: "uppercase",
marginBottom: 8,
}}
>
SCORE UPDATE
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 24,
justifyContent: "center",
}}
>
<div style={{ textAlign: "center" }}>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 88,
color: ACCENT_YELLOW,
lineHeight: 1,
letterSpacing: "-0.04em",
}}
>
{currentScore}
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 11,
color: TEXT_DIM,
letterSpacing: "0.1em",
textTransform: "uppercase",
marginTop: 6,
}}
>
{TEAM_A}
</div>
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 300,
fontSize: 48,
color: "#ffffff44",
lineHeight: 1,
}}
>
—
</div>
<div style={{ textAlign: "center" }}>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 88,
color: TEXT_WHITE,
lineHeight: 1,
letterSpacing: "-0.04em",
}}
>
{FINAL_SCORE_B}
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 11,
color: TEXT_DIM,
letterSpacing: "0.1em",
textTransform: "uppercase",
marginTop: 6,
}}
>
{TEAM_B}
</div>
</div>
</div>
</div>
);
// ── SCENE: Intro (0–40) ───────────────────────────────────────────────────────
const SceneIntro: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Logo scale burst
const logoScale = spring({
frame,
fps,
from: 0.1,
to: 1,
config: { damping: 14, stiffness: 180 },
});
// Logo glow opacity
const glowOpacity = interpolate(frame, [0, 12, 30, 38], [0, 0.8, 0.6, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Tagline fade in
const taglineOpacity = interpolate(frame, [12, 22], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const taglineY = interpolate(frame, [12, 22], [20, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Red line sweep
const lineWidth = interpolate(frame, [18, 36], [0, 1280], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Scene fade out
const sceneOpacity = interpolate(frame, [32, 40], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div style={{ position: "absolute", inset: 0, opacity: sceneOpacity }}>
{/* Glow behind logo */}
<div
style={{
position: "absolute",
left: "50%",
top: "40%",
transform: "translate(-50%, -50%)",
width: 320,
height: 180,
borderRadius: "50%",
background: `radial-gradient(ellipse, ${ACCENT_YELLOW}55, transparent 70%)`,
opacity: glowOpacity,
}}
/>
{/* Show name */}
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 14,
transform: `scale(${logoScale})`,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
{/* Logo icon */}
<div
style={{
width: 56,
height: 56,
background: ACCENT_YELLOW,
borderRadius: 10,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: `0 0 24px ${ACCENT_YELLOW}88`,
}}
>
<svg width="32" height="32" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="12" fill="#0a0e1a" />
<path
d="M10 16 L14 20 L22 12"
stroke="#f5c842"
strokeWidth="3"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 52,
color: TEXT_WHITE,
letterSpacing: "0.04em",
textTransform: "uppercase",
}}
>
{SHOW_NAME}
</span>
</div>
</div>
{/* Tagline below */}
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
paddingTop: 140,
opacity: taglineOpacity,
transform: `translateY(${taglineY}px)`,
}}
>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 300,
fontSize: 22,
color: ACCENT_YELLOW,
letterSpacing: "0.32em",
textTransform: "uppercase",
}}
>
{SHOW_TAGLINE}
</span>
</div>
{/* Sweeping red accent line */}
<div
style={{
position: "absolute",
left: 0,
top: "calc(50% + 86px)",
height: 3,
width: lineWidth,
background: `linear-gradient(to right, ${ACCENT_RED}, ${ACCENT_YELLOW})`,
}}
/>
</div>
);
};
// ── SCENE: Generic Play Scene (40–260) ────────────────────────────────────────
const ScenePlay: React.FC<{
playIndex: number;
sceneStart: number;
sceneEnd: number;
flashRed?: boolean;
}> = ({ playIndex, sceneStart, sceneEnd, flashRed = false }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const f = sceneFrame(frame, sceneStart);
const dur = sceneEnd - sceneStart;
const play = PLAYS[playIndex];
// Scene-level fade in/out
const opacity = sceneOpacity(frame, sceneStart, sceneEnd, 8, 10);
// Lower third slide in from left
const ltSlideX = spring({
frame: f,
fps,
from: -500,
to: 0,
config: { damping: 18, stiffness: 160 },
});
// Lower third slide out
const ltSlideOut =
f > dur - 14
? interpolate(f, [dur - 14, dur], [0, -500], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.in(Easing.cubic),
})
: 0;
const ltSlide = f < dur - 14 ? ltSlideX : ltSlideOut;
// Stat card spring in from right
const cardSpringX = spring({
frame: Math.max(0, f - 10),
fps,
from: 320,
to: 0,
config: { damping: 16, stiffness: 140 },
});
// Stat card slide out
const cardSlideOut =
f > dur - 14
? interpolate(f, [dur - 14, dur], [0, 320], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.in(Easing.cubic),
})
: 0;
const cardX = f < dur - 14 ? cardSpringX : cardSlideOut;
// Play counter bar slide in
const barProgress = interpolate(f, [6, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Red flash for Play 3
const flashProgress = flashRed
? interpolate(f, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
})
: 0;
// Horizontal accent line above lower-third
const accentLineWidth = interpolate(f, [4, 20], [0, 640], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
return (
<div style={{ position: "absolute", inset: 0, opacity }}>
{/* Red flash for red card */}
{flashRed && (
<div
style={{
position: "absolute",
inset: 0,
background: `rgba(232, 0, 30, ${0.3 * Math.max(0, Math.sin(flashProgress * Math.PI * 2))})`,
pointerEvents: "none",
zIndex: 10,
}}
/>
)}
{/* Scene headline — play type centered */}
<div
style={{
position: "absolute",
left: 32,
top: "50%",
transform: "translateY(-50%)",
maxWidth: 640,
}}
>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 72,
color: play.accent,
lineHeight: 1,
letterSpacing: "-0.02em",
textShadow: `0 0 40px ${play.accent}55`,
}}
>
{play.type}
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 300,
fontSize: 20,
color: TEXT_DIM,
letterSpacing: "0.18em",
marginTop: 8,
textTransform: "uppercase",
}}
>
{play.teamLabel} · {play.time}
</div>
{/* Accent line */}
<div
style={{
height: 3,
width: accentLineWidth,
background: play.accent,
marginTop: 16,
borderRadius: 2,
}}
/>
</div>
{/* Stat card */}
<StatCard play={play} springX={cardX} opacity={clamp(barProgress * 2, 0, 1)} />
{/* Lower third chyron */}
<LowerThird play={play} slideX={ltSlide} opacity={1} />
{/* Play counter bar */}
<PlayCounterBar
activeIndex={playIndex}
accentColor={play.accent}
barProgress={barProgress}
/>
</div>
);
};
// ── SCENE: Play 4 with score & confetti (200–260) ────────────────────────────
const ScenePlay4: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const f = sceneFrame(frame, SCENE_PLAY4_START);
const dur = SCENE_PLAY4_END - SCENE_PLAY4_START;
const play = PLAYS[3];
const opacity = sceneOpacity(frame, SCENE_PLAY4_START, SCENE_PLAY4_END, 8, 10);
// Score springs in
const scoreScale = spring({
frame: Math.max(0, f - 14),
fps,
from: 0.1,
to: 1,
config: { damping: 12, stiffness: 200 },
});
const scoreOpacity = interpolate(f, [14, 26], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Confetti progress
const confettiProgress = interpolate(f, [20, 55], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Lower third
const ltSlideX = spring({
frame: f,
fps,
from: -500,
to: 0,
config: { damping: 18, stiffness: 160 },
});
const ltOut =
f > dur - 14
? interpolate(f, [dur - 14, dur], [0, -500], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.in(Easing.cubic),
})
: 0;
const barProgress = interpolate(f, [6, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Accent line
const accentLineWidth = interpolate(f, [4, 20], [0, 640], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
return (
<div style={{ position: "absolute", inset: 0, opacity }}>
{/* Confetti */}
<ConfettiBurst progress={confettiProgress} />
{/* Score display */}
<ScoreUpdate
scoreScale={scoreScale}
opacity={scoreOpacity}
currentScore={FINAL_SCORE_A}
/>
{/* Subtle play label top-left */}
<div
style={{
position: "absolute",
left: 32,
bottom: 160,
}}
>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 28,
color: play.accent,
letterSpacing: "0.04em",
}}
>
{play.type}
</div>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 400,
fontSize: 13,
color: TEXT_DIM,
letterSpacing: "0.1em",
textTransform: "uppercase",
}}
>
{play.name} #{play.number} · {play.time}
</div>
<div
style={{
height: 2,
width: accentLineWidth / 4,
background: play.accent,
marginTop: 8,
borderRadius: 1,
}}
/>
</div>
{/* Lower third */}
<LowerThird
play={play}
slideX={f < dur - 14 ? ltSlideX : ltOut}
opacity={1}
/>
{/* Play counter */}
<PlayCounterBar
activeIndex={3}
accentColor={play.accent}
barProgress={barProgress}
/>
</div>
);
};
// ── SCENE: Outro (260–300) ────────────────────────────────────────────────────
const SceneOutro: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const f = sceneFrame(frame, SCENE_OUTRO_START);
const opacity = interpolate(frame, [SCENE_OUTRO_START, SCENE_OUTRO_START + 12], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Team names slide in from sides
const teamAX = spring({
frame: f,
fps,
from: -300,
to: 0,
config: { damping: 16, stiffness: 150 },
});
const teamBX = spring({
frame: f,
fps,
from: 300,
to: 0,
config: { damping: 16, stiffness: 150 },
});
// Score scale
const scoreScale = spring({
frame: Math.max(0, f - 8),
fps,
from: 0.3,
to: 1,
config: { damping: 14, stiffness: 200 },
});
// Yellow accent line scale
const lineScale = interpolate(f, [6, 24], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Website fade in
const websiteOpacity = interpolate(f, [20, 32], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// "FINAL SCORE" label
const labelOpacity = interpolate(f, [2, 10], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
position: "absolute",
inset: 0,
opacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 0,
}}
>
{/* "FINAL SCORE" label */}
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 400,
fontSize: 13,
color: TEXT_DIM,
letterSpacing: "0.3em",
textTransform: "uppercase",
marginBottom: 20,
opacity: labelOpacity,
}}
>
FINAL SCORE
</div>
{/* Teams and score row */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 32,
}}
>
{/* Team A */}
<div
style={{
textAlign: "right",
transform: `translateX(${teamAX}px)`,
}}
>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 28,
color: TEXT_WHITE,
letterSpacing: "0.06em",
textTransform: "uppercase",
}}
>
{TEAM_A}
</div>
</div>
{/* Score */}
<div
style={{
transform: `scale(${scoreScale})`,
display: "flex",
alignItems: "center",
gap: 16,
}}
>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 80,
color: ACCENT_YELLOW,
lineHeight: 1,
letterSpacing: "-0.04em",
textShadow: `0 0 32px ${ACCENT_YELLOW}66`,
}}
>
{FINAL_SCORE_A}
</span>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 200,
fontSize: 48,
color: "#ffffff33",
}}
>
—
</span>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 80,
color: TEXT_WHITE,
lineHeight: 1,
letterSpacing: "-0.04em",
}}
>
{FINAL_SCORE_B}
</span>
</div>
{/* Team B */}
<div
style={{
textAlign: "left",
transform: `translateX(${teamBX}px)`,
}}
>
<div
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontWeight: 900,
fontSize: 28,
color: TEXT_WHITE,
letterSpacing: "0.06em",
textTransform: "uppercase",
}}
>
{TEAM_B}
</div>
</div>
</div>
{/* Yellow accent line */}
<div
style={{
height: 3,
width: 480 * lineScale,
background: `linear-gradient(to right, transparent, ${ACCENT_YELLOW}, transparent)`,
borderRadius: 2,
marginTop: 24,
}}
/>
{/* Website CTA */}
<div
style={{
marginTop: 28,
opacity: websiteOpacity,
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 14,
color: TEXT_DIM,
letterSpacing: "0.14em",
textTransform: "uppercase",
}}
>
Full match available at{" "}
<span
style={{
color: ACCENT_YELLOW,
fontWeight: 600,
}}
>
{WEBSITE}
</span>
</div>
{/* Bottom decoration */}
<div
style={{
position: "absolute",
bottom: 28,
display: "flex",
alignItems: "center",
gap: 8,
opacity: websiteOpacity,
}}
>
<div
style={{
width: 20,
height: 2,
background: ACCENT_YELLOW,
borderRadius: 1,
}}
/>
<span
style={{
fontFamily: "Inter, system-ui, sans-serif",
fontSize: 11,
color: TEXT_DIM,
letterSpacing: "0.2em",
textTransform: "uppercase",
}}
>
{SHOW_NAME}
</span>
<div
style={{
width: 20,
height: 2,
background: ACCENT_YELLOW,
borderRadius: 1,
}}
/>
</div>
</div>
);
};
// ── Root Composition ──────────────────────────────────────────────────────────
export default function SportsHighlightReel() {
const frame = useCurrentFrame();
// Determine top-bar scene label
let sceneLabel = "HIGHLIGHT REEL";
if (frame >= SCENE_PLAY1_START && frame < SCENE_PLAY1_END) {
sceneLabel = `PLAY 1 · ${PLAYS[0].type} · ${PLAYS[0].time}`;
} else if (frame >= SCENE_PLAY2_START && frame < SCENE_PLAY2_END) {
sceneLabel = `PLAY 2 · ${PLAYS[1].type} · ${PLAYS[1].time}`;
} else if (frame >= SCENE_PLAY3_START && frame < SCENE_PLAY3_END) {
sceneLabel = `PLAY 3 · ${PLAYS[2].type} · ${PLAYS[2].time}`;
} else if (frame >= SCENE_PLAY4_START && frame < SCENE_PLAY4_END) {
sceneLabel = `PLAY 4 · ${PLAYS[3].type} · ${PLAYS[3].time}`;
} else if (frame >= SCENE_OUTRO_START) {
sceneLabel = "FINAL SCORE";
}
// Top bar opacity (hides during intro logo burst)
const topBarOpacity = interpolate(frame, [24, 36], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Red card flash (scene play3, frames 0-24 within scene = 140-164 global)
const flashProgress = interpolate(
frame,
[SCENE_PLAY3_START, SCENE_PLAY3_START + 24],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const isPlay3 = frame >= SCENE_PLAY3_START && frame < SCENE_PLAY3_END;
return (
<AbsoluteFill
style={{
fontFamily: "Inter, system-ui, sans-serif",
background: BG_PRIMARY,
overflow: "hidden",
}}
>
{/* Persistent background */}
<Background
flashRed={isPlay3}
flashProgress={isPlay3 ? flashProgress : 0}
/>
{/* Persistent top bar */}
<TopBar opacity={topBarOpacity} sceneLabel={sceneLabel} />
{/* INTRO — frames 0-40 */}
{frame < SCENE_INTRO_END + 4 && <SceneIntro />}
{/* PLAY 1 — frames 40-90 */}
{frame >= SCENE_PLAY1_START - 2 && frame < SCENE_PLAY1_END + 4 && (
<ScenePlay
playIndex={0}
sceneStart={SCENE_PLAY1_START}
sceneEnd={SCENE_PLAY1_END}
/>
)}
{/* PLAY 2 — frames 90-140 */}
{frame >= SCENE_PLAY2_START - 2 && frame < SCENE_PLAY2_END + 4 && (
<ScenePlay
playIndex={1}
sceneStart={SCENE_PLAY2_START}
sceneEnd={SCENE_PLAY2_END}
/>
)}
{/* PLAY 3 (red card) — frames 140-200 */}
{frame >= SCENE_PLAY3_START - 2 && frame < SCENE_PLAY3_END + 4 && (
<ScenePlay
playIndex={2}
sceneStart={SCENE_PLAY3_START}
sceneEnd={SCENE_PLAY3_END}
flashRed
/>
)}
{/* PLAY 4 (final goal + confetti + score) — frames 200-260 */}
{frame >= SCENE_PLAY4_START - 2 && frame < SCENE_PLAY4_END + 4 && (
<ScenePlay4 />
)}
{/* OUTRO — frames 260-300 */}
{frame >= SCENE_OUTRO_START - 2 && <SceneOutro />}
</AbsoluteFill>
);
}Sports Highlight Reel
A broadcast-quality sports highlight reel built entirely in Remotion. The composition opens with a spring-driven logo burst for SportsPulse TV — the show name scales from 0.1 using spring({ damping: 14, stiffness: 180 }) alongside a radial yellow glow, a tagline fade, and a full-width red accent line sweeping across the frame. The dark base (#0a0e1a) is layered with an SVG grid pattern, diagonal accent lines, and a radial vignette to achieve a dramatic stadium-broadcast look.
Each of the four play segments runs for 50 frames and follows a consistent rhythm: a lower-third chyron slides in from the left (spring damping: 18, stiffness: 160), a player stat card springs in from the right (spring damping: 16, stiffness: 140), and a four-segment play counter bar advances at the bottom left. Every scene fades in and out smoothly using interpolate with frame-boundary clamps. Play 3 (red card) adds a sine-wave red flash overlay; Play 4 (final goal) replaces the stat card with a large spring-animated score display (damping: 12, stiffness: 200) and a confetti burst of twelve colored rectangles fanning out with staggered easing. The outro slides both team names in from opposite sides, scales the final score into view, draws a yellow gradient accent line, and fades in the website CTA.
All visual primitives — grid, glow, cards, chyrons, confetti — are pure inline React style objects. No external images or CSS files are used. The component is split into named sub-components (Background, TopBar, PlayCounterBar, LowerThird, StatCard, ConfettiBurst, ScoreUpdate, SceneIntro, ScenePlay, ScenePlay4, SceneOutro) for maintainability and readability.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1280 × 720 |
| FPS | 30 |
| Duration | 10.0 s (300 frames) |
Timeline
| Time | Frames | Action |
|---|---|---|
| 0:00 – 0:01.3 | 0 – 40 | Intro: SportsPulse TV logo burst, tagline fade, red line sweep |
| 0:01.3 – 0:03 | 40 – 90 | Play 1: GOAL · 14’ — J. Martinez stat card, lower third, play bar segment 1 |
| 0:03 – 0:04.7 | 90 – 140 | Play 2: SAVE · 31’ — R. Okonkwo goalkeeper card in cyan, segment 2 |
| 0:04.7 – 0:06.7 | 140 – 200 | Play 3: RED CARD · 58’ — K. Banks, red flash overlay, segment 3 |
| 0:06.7 – 0:08.7 | 200 – 260 | Play 4: GOAL · 87’ — L. Chen, score 3–1, confetti burst, segment 4 |
| 0:08.7 – 0:10 | 260 – 300 | Outro: Final score reveal, team names slide in, yellow accent line, website CTA |
Customization
SHOW_NAME— broadcast channel name shown in the logo burst and top bar (default:SportsPulse TV)SHOW_TAGLINE— subtitle displayed below the logo in the intro (default:MATCH HIGHLIGHTS)TEAM_A/TEAM_B— full team names used in score displays and lower thirdsFINAL_SCORE_A/FINAL_SCORE_B— integer scores shown in the Play 4 update and outroWEBSITE— the CTA domain faded in at the end of the outroPLAYS— array of four play objects; each controlstype,time,name,number,position,stat,accentcolor, andteamLabelACCENT_YELLOW/ACCENT_RED/ACCENT_CYAN— three broadcast accent colors applied to play types, glows, card header bars, and confettiBG_PRIMARY— root background color for the entire composition- Scene boundary constants (
SCENE_INTRO_END,SCENE_PLAY1_START, etc.) — adjust frame lengths for each segment without refactoring logic