Year-in-Review Recap (Remotion)
A cinematic 10-second annual recap video built with Remotion — a giant year number springs in with a radial particle burst, three animated stat cards count up key metrics, placeholder image frames for the year's best moments slide in sequentially with captions, and a bold 'What's Next' teaser closes with a pulsing gold arrow and staggered bullet points. Dark navy background with indigo, gold, and violet accents throughout.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Composition,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
Easing,
Sequence,
} from "remotion";
// ── Brand / theme ──────────────────────────────────────────────────────
const YEAR = "2025";
const COMPANY = "Orion Labs";
const TAGLINE = "A Year of Bold Moves";
// ── Color palette ──────────────────────────────────────────────────────
const PALETTE = {
bg: "#04060f",
indigo: "#6366f1",
violet: "#8b5cf6",
gold: "#f59e0b",
goldLight: "#fcd34d",
cyan: "#06b6d4",
emerald: "#10b981",
rose: "#f43f5e",
white: "#ffffff",
dim: "rgba(255,255,255,0.45)",
dimmer: "rgba(255,255,255,0.25)",
};
// ── Seeded deterministic "random" ──────────────────────────────────────
function seededRand(seed: number): number {
const x = Math.sin(seed + 1) * 10000;
return x - Math.floor(x);
}
// ── Particle burst data ─────────────────────────────────────────────────
interface Particle {
angle: number;
speed: number;
size: number;
color: string;
delay: number;
}
const PARTICLE_COLORS = [
PALETTE.gold,
PALETTE.goldLight,
PALETTE.indigo,
PALETTE.violet,
PALETTE.cyan,
PALETTE.rose,
];
const PARTICLES: Particle[] = Array.from({ length: 48 }, (_, i) => ({
angle: (i / 48) * Math.PI * 2 + seededRand(i * 3) * 0.4,
speed: 80 + seededRand(i * 7) * 220,
size: 3 + seededRand(i * 11) * 9,
color: PARTICLE_COLORS[i % PARTICLE_COLORS.length],
delay: Math.floor(seededRand(i * 17) * 12),
}));
// ── Stats ───────────────────────────────────────────────────────────────
interface Stat {
label: string;
value: number;
formatted: string;
icon: string;
color: string;
}
const STATS: Stat[] = [
{ label: "Commits Shipped", value: 4_812, formatted: "4,812", icon: "⬡", color: PALETTE.indigo },
{ label: "Launches", value: 24, formatted: "24", icon: "◈", color: PALETTE.gold },
{ label: "Active Users", value: 186_000, formatted: "186K", icon: "◎", color: PALETTE.emerald },
];
// ── Moments ─────────────────────────────────────────────────────────────
interface Moment {
month: string;
title: string;
caption: string;
bg: string;
accent: string;
}
const MOMENTS: Moment[] = [
{
month: "March",
title: "v2 Launch",
caption: "Shipped the biggest release in company history",
bg: "linear-gradient(135deg, #1e1b4b 0%, #312e81 100%)",
accent: PALETTE.indigo,
},
{
month: "July",
title: "1M Requests / Day",
caption: "Crossed the million-request milestone",
bg: "linear-gradient(135deg, #1c1917 0%, #78350f 100%)",
accent: PALETTE.gold,
},
{
month: "November",
title: "Team Doubles",
caption: "Grew from 12 to 24 talented people",
bg: "linear-gradient(135deg, #052e16 0%, #14532d 100%)",
accent: PALETTE.emerald,
},
];
// ── Arrow SVG path ───────────────────────────────────────────────────────
// Drawn purely with divs and borders — no SVG import needed
// ── Utility: cross-section opacity ─────────────────────────────────────
function sectionOpacity(
frame: number,
inStart: number,
inEnd: number,
outStart: number,
outEnd: number
): number {
return interpolate(frame, [inStart, inEnd, outStart, outEnd], [0, 1, 1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.quad),
});
}
// ── Shared background layer ─────────────────────────────────────────────
const Background: React.FC = () => (
<AbsoluteFill
style={{
background: `radial-gradient(ellipse at 30% 20%, rgba(99,102,241,0.08) 0%, transparent 55%),
radial-gradient(ellipse at 75% 80%, rgba(245,158,11,0.06) 0%, transparent 50%),
${PALETTE.bg}`,
overflow: "hidden",
}}
>
{/* Subtle grid lines */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage: `
linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px)
`,
backgroundSize: "80px 80px",
}}
/>
{/* Vignette */}
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.55) 100%)",
}}
/>
</AbsoluteFill>
);
// ── Scene 1: Year number + particle burst (frames 0–80) ─────────────────
const ParticleBurstEl: React.FC<{
particle: Particle;
frame: number;
}> = ({ particle, frame }) => {
const f = Math.max(0, frame - particle.delay);
const progress = spring({
frame: f,
fps: 30,
from: 0,
to: 1,
config: { damping: 14, stiffness: 120, mass: 0.6 },
});
const dist = progress * particle.speed;
const x = Math.cos(particle.angle) * dist;
const y = Math.sin(particle.angle) * dist;
const opacity = interpolate(progress, [0, 0.15, 0.7, 1], [0, 1, 0.9, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
width: particle.size,
height: particle.size,
borderRadius: "50%",
background: particle.color,
opacity,
transform: `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`,
boxShadow: `0 0 ${particle.size * 2}px ${particle.color}88`,
}}
/>
);
};
const SceneYear: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
const opacity = sectionOpacity(frame, 0, 20, 68, 80);
// Year number springs in
const yearScale = spring({
frame,
fps,
from: 0.3,
to: 1,
config: { damping: 13, stiffness: 130, mass: 1 },
});
const yearOpacity = interpolate(frame, [0, 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Company / tagline stagger
const tagOpacity = interpolate(frame, [20, 36], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const tagY = interpolate(frame, [20, 36], [16, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
// Gold line width
const lineW = interpolate(frame, [16, 40], [0, 280], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.exp),
});
return (
<AbsoluteFill
style={{
opacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{/* Particle burst layer */}
{PARTICLES.map((p, i) => (
<ParticleBurstEl key={i} particle={p} frame={frame} />
))}
{/* Giant year */}
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 200,
letterSpacing: "-0.06em",
lineHeight: 1,
opacity: yearOpacity,
transform: `scale(${yearScale})`,
background: `linear-gradient(135deg, ${PALETTE.gold} 0%, ${PALETTE.goldLight} 35%, ${PALETTE.indigo} 70%, ${PALETTE.violet} 100%)`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
userSelect: "none",
position: "relative",
zIndex: 2,
}}
>
{YEAR}
</div>
{/* Gold divider */}
<div
style={{
width: lineW,
height: 3,
background: `linear-gradient(90deg, ${PALETTE.gold}, ${PALETTE.violet})`,
borderRadius: 3,
marginTop: 12,
marginBottom: 20,
opacity: tagOpacity,
zIndex: 2,
}}
/>
{/* Company + tagline */}
<div
style={{
opacity: tagOpacity,
transform: `translateY(${tagY}px)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 6,
zIndex: 2,
}}
>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 26,
color: PALETTE.white,
letterSpacing: "0.12em",
textTransform: "uppercase",
}}
>
{COMPANY}
</div>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
fontSize: 18,
color: PALETTE.dim,
letterSpacing: "0.06em",
}}
>
{TAGLINE}
</div>
</div>
</AbsoluteFill>
);
};
// ── Scene 2: Top Highlights + 3 stat cards (frames 80–175) ─────────────
interface StatCardProps {
stat: Stat;
index: number;
localFrame: number;
fps: number;
}
const StatCard: React.FC<StatCardProps> = ({ stat, index, localFrame, fps }) => {
const STAGGER = 14;
const delay = 18 + index * STAGGER;
const f = Math.max(0, localFrame - delay);
const cardOpacity = interpolate(f, [0, 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const cardY = interpolate(f, [0, 20], [28, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const cardScale = spring({
frame: f,
fps,
from: 0.85,
to: 1,
config: { damping: 14, stiffness: 160, mass: 0.7 },
});
// Count-up
const countProgress = spring({
frame: f,
fps,
from: 0,
to: stat.value,
config: { damping: 18, stiffness: 65, mass: 1 },
});
// Format count display
const displayVal = Math.round(countProgress);
let displayStr: string;
if (stat.value >= 100_000) {
displayStr = `${Math.round(displayVal / 1000)}K`;
} else {
displayStr = displayVal.toLocaleString("en-US");
}
return (
<div
style={{
opacity: cardOpacity,
transform: `translateY(${cardY}px) scale(${cardScale})`,
background: "rgba(255,255,255,0.04)",
border: `1px solid ${stat.color}44`,
borderRadius: 20,
padding: "32px 40px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 10,
flex: 1,
minWidth: 0,
boxShadow: `0 0 40px ${stat.color}18, inset 0 1px 0 rgba(255,255,255,0.07)`,
position: "relative",
overflow: "hidden",
}}
>
{/* Glow corner */}
<div
style={{
position: "absolute",
top: -60,
right: -60,
width: 160,
height: 160,
borderRadius: "50%",
background: `radial-gradient(ellipse, ${stat.color}22 0%, transparent 70%)`,
}}
/>
{/* Icon */}
<div
style={{
fontSize: 28,
color: stat.color,
lineHeight: 1,
marginBottom: 4,
}}
>
{stat.icon}
</div>
{/* Count */}
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 800,
fontSize: 58,
letterSpacing: "-0.03em",
lineHeight: 1,
color: stat.color,
textShadow: `0 0 40px ${stat.color}66`,
}}
>
{displayStr}
</div>
{/* Label */}
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 500,
fontSize: 14,
color: PALETTE.dim,
textTransform: "uppercase",
letterSpacing: "0.12em",
textAlign: "center",
}}
>
{stat.label}
</div>
</div>
);
};
const SceneHighlights: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
const localFrame = frame - 80;
const opacity = sectionOpacity(frame, 80, 96, 163, 175);
const titleOpacity = interpolate(localFrame, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const titleY = interpolate(localFrame, [0, 20], [-20, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
return (
<AbsoluteFill
style={{
opacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "0 80px",
gap: 0,
}}
>
{/* BG glow */}
<div
style={{
position: "absolute",
top: "35%",
left: "50%",
width: 800,
height: 400,
transform: "translate(-50%, -50%)",
background:
"radial-gradient(ellipse, rgba(99,102,241,0.1) 0%, transparent 70%)",
pointerEvents: "none",
}}
/>
{/* Section heading */}
<div
style={{
opacity: titleOpacity,
transform: `translateY(${titleY}px)`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 38,
color: PALETTE.white,
letterSpacing: "-0.01em",
marginBottom: 12,
alignSelf: "flex-start",
}}
>
Top Highlights
</div>
{/* Accent line */}
<div
style={{
opacity: titleOpacity,
alignSelf: "flex-start",
width: interpolate(localFrame, [6, 28], [0, 140], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.exp),
}),
height: 3,
background: `linear-gradient(90deg, ${PALETTE.gold}, ${PALETTE.indigo})`,
borderRadius: 3,
marginBottom: 48,
}}
/>
{/* Stat cards row */}
<div
style={{
display: "flex",
gap: 24,
width: "100%",
}}
>
{STATS.map((stat, i) => (
<StatCard
key={stat.label}
stat={stat}
index={i}
localFrame={localFrame}
fps={fps}
/>
))}
</div>
</AbsoluteFill>
);
};
// ── Scene 3: Best moments — 3 image frames slide in (frames 175–255) ───
interface MomentFrameProps {
moment: Moment;
index: number;
localFrame: number;
fps: number;
}
const MomentCard: React.FC<MomentFrameProps> = ({ moment, index, localFrame, fps }) => {
const STAGGER = 18;
const delay = 12 + index * STAGGER;
const f = Math.max(0, localFrame - delay);
const cardX = interpolate(f, [0, 22], [80, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const cardOpacity = interpolate(f, [0, 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const imgReveal = spring({
frame: f,
fps,
from: 0,
to: 1,
config: { damping: 16, stiffness: 100, mass: 0.8 },
});
return (
<div
style={{
opacity: cardOpacity,
transform: `translateX(${cardX}px)`,
flex: 1,
minWidth: 0,
borderRadius: 16,
overflow: "hidden",
border: `1px solid ${moment.accent}33`,
boxShadow: `0 8px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04)`,
display: "flex",
flexDirection: "column",
background: PALETTE.bg,
}}
>
{/* Image placeholder area */}
<div
style={{
width: "100%",
height: 160,
background: moment.bg,
position: "relative",
overflow: "hidden",
flexShrink: 0,
}}
>
{/* Shimmer reveal overlay */}
<div
style={{
position: "absolute",
inset: 0,
background: PALETTE.bg,
transform: `scaleX(${1 - imgReveal})`,
transformOrigin: "left center",
}}
/>
{/* Decorative shapes inside placeholder */}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 60,
height: 60,
borderRadius: "50%",
background: `${moment.accent}33`,
border: `2px solid ${moment.accent}66`,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 11,
color: moment.accent,
letterSpacing: "0.06em",
textTransform: "uppercase",
textAlign: "center",
}}
>
{moment.month}
</div>
</div>
{/* Subtle scanlines */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage:
"repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.15) 3px, rgba(0,0,0,0.15) 4px)",
}}
/>
</div>
{/* Caption area */}
<div
style={{
padding: "18px 20px",
display: "flex",
flexDirection: "column",
gap: 6,
background: "rgba(255,255,255,0.03)",
flex: 1,
}}
>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 17,
color: PALETTE.white,
lineHeight: 1.2,
}}
>
{moment.title}
</div>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
fontSize: 13,
color: PALETTE.dim,
lineHeight: 1.4,
}}
>
{moment.caption}
</div>
{/* Accent tag */}
<div
style={{
marginTop: 6,
display: "inline-flex",
alignSelf: "flex-start",
background: `${moment.accent}22`,
border: `1px solid ${moment.accent}44`,
borderRadius: 6,
padding: "3px 10px",
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 600,
fontSize: 11,
color: moment.accent,
letterSpacing: "0.08em",
textTransform: "uppercase",
}}
>
{moment.month}
</span>
</div>
</div>
</div>
);
};
const SceneMoments: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
const localFrame = frame - 175;
const opacity = sectionOpacity(frame, 175, 190, 243, 255);
const titleOpacity = interpolate(localFrame, [0, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const titleY = interpolate(localFrame, [0, 18], [-18, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
return (
<AbsoluteFill
style={{
opacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "0 80px",
gap: 0,
}}
>
{/* BG glow */}
<div
style={{
position: "absolute",
bottom: "25%",
right: "20%",
width: 600,
height: 400,
background:
"radial-gradient(ellipse, rgba(245,158,11,0.08) 0%, transparent 65%)",
pointerEvents: "none",
}}
/>
{/* Section title */}
<div
style={{
opacity: titleOpacity,
transform: `translateY(${titleY}px)`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 38,
color: PALETTE.white,
letterSpacing: "-0.01em",
marginBottom: 12,
alignSelf: "flex-start",
}}
>
Best Moments
</div>
{/* Accent line */}
<div
style={{
opacity: titleOpacity,
alignSelf: "flex-start",
width: interpolate(localFrame, [4, 26], [0, 130], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.exp),
}),
height: 3,
background: `linear-gradient(90deg, ${PALETTE.gold}, ${PALETTE.rose})`,
borderRadius: 3,
marginBottom: 40,
}}
/>
{/* Moment cards */}
<div
style={{
display: "flex",
gap: 24,
width: "100%",
alignItems: "stretch",
}}
>
{MOMENTS.map((m, i) => (
<MomentCard
key={m.title}
moment={m}
index={i}
localFrame={localFrame}
fps={fps}
/>
))}
</div>
</AbsoluteFill>
);
};
// ── Scene 4: "What's Next" teaser (frames 255–300) ──────────────────────
const AnimatedArrow: React.FC<{ frame: number }> = ({ frame }) => {
// Pulsing translate
const pulse = interpolate(
frame % 30,
[0, 15, 30],
[0, 12, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.sine) }
);
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 0,
transform: `translateX(${pulse}px)`,
}}
>
{/* Arrow shaft */}
<div
style={{
width: 80,
height: 4,
background: `linear-gradient(90deg, ${PALETTE.gold}00, ${PALETTE.gold})`,
borderRadius: 2,
}}
/>
{/* Arrowhead — right-pointing chevron built with borders */}
<div
style={{
width: 0,
height: 0,
borderTop: "14px solid transparent",
borderBottom: "14px solid transparent",
borderLeft: `22px solid ${PALETTE.gold}`,
}}
/>
</div>
);
};
const SceneWhatsNext: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
const localFrame = frame - 255;
const opacity = interpolate(localFrame, [0, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
const headScale = spring({
frame: localFrame,
fps,
from: 0.8,
to: 1,
config: { damping: 14, stiffness: 150, mass: 0.9 },
});
const headOpacity = interpolate(localFrame, [0, 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const taglineOpacity = interpolate(localFrame, [14, 30], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const taglineY = interpolate(localFrame, [14, 30], [12, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
const arrowOpacity = interpolate(localFrame, [24, 38], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const bulletOpacity = interpolate(localFrame, [28, 44], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Glowing orb pulsing
const orbScale = interpolate(
localFrame % 60,
[0, 30, 60],
[1, 1.12, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.sine) }
);
const NEXT_ITEMS = [
"Ship 40 new resources",
"Launch mobile SDK",
"Open-source the core",
];
return (
<AbsoluteFill
style={{
opacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 0,
}}
>
{/* Gold glow orb */}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
width: 700,
height: 500,
transform: `translate(-50%, -50%) scale(${orbScale})`,
background: `radial-gradient(ellipse, ${PALETTE.gold}14 0%, ${PALETTE.indigo}0a 50%, transparent 75%)`,
pointerEvents: "none",
}}
/>
{/* "What's Next" heading */}
<div
style={{
opacity: headOpacity,
transform: `scale(${headScale})`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 88,
letterSpacing: "-0.04em",
lineHeight: 1,
background: `linear-gradient(135deg, ${PALETTE.white} 0%, ${PALETTE.gold} 50%, ${PALETTE.goldLight} 100%)`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: 24,
}}
>
What's Next
</div>
{/* Tagline + arrow */}
<div
style={{
opacity: taglineOpacity,
transform: `translateY(${taglineY}px)`,
display: "flex",
alignItems: "center",
gap: 20,
marginBottom: 48,
}}
>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 500,
fontSize: 20,
color: PALETTE.dim,
letterSpacing: "0.06em",
}}
>
{YEAR + 1} is going to be even bigger
</div>
<div style={{ opacity: arrowOpacity }}>
<AnimatedArrow frame={localFrame} />
</div>
</div>
{/* Bullet list */}
<div
style={{
opacity: bulletOpacity,
display: "flex",
flexDirection: "column",
gap: 14,
alignItems: "center",
}}
>
{NEXT_ITEMS.map((item, i) => {
const itemOpacity = interpolate(localFrame, [28 + i * 6, 44 + i * 6], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const itemX = interpolate(localFrame, [28 + i * 6, 40 + i * 6], [-20, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
return (
<div
key={item}
style={{
opacity: itemOpacity,
transform: `translateX(${itemX}px)`,
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<div
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: PALETTE.gold,
boxShadow: `0 0 12px ${PALETTE.gold}88`,
flexShrink: 0,
}}
/>
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 500,
fontSize: 18,
color: "rgba(255,255,255,0.7)",
}}
>
{item}
</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};
// ── Main composition ────────────────────────────────────────────────────
export const RemotionYearReview: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: PALETTE.bg, overflow: "hidden" }}>
<Background />
{/* Scene 1: Year + particles (0–80) */}
{frame < 82 && <SceneYear frame={frame} fps={fps} />}
{/* Scene 2: Highlights + stat cards (78–175) */}
{frame >= 78 && frame < 177 && (
<SceneHighlights frame={frame} fps={fps} />
)}
{/* Scene 3: Best moments (173–255) */}
{frame >= 173 && frame < 257 && (
<SceneMoments frame={frame} fps={fps} />
)}
{/* Scene 4: What's next (253–300) */}
{frame >= 253 && (
<SceneWhatsNext frame={frame} fps={fps} />
)}
</AbsoluteFill>
);
};
// ── Remotion Root ───────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
<Composition
id="RemotionYearReview"
component={RemotionYearReview}
durationInFrames={300}
fps={30}
width={1280}
height={720}
/>
);Year-in-Review Recap
A “Wrapped”-style annual recap video rendered entirely in Remotion, spanning four distinct scenes that cross-fade with interpolated opacity transitions. The composition opens with a massive year number (2025) springing in at scale with a surrounding particle burst of 48 deterministic dots radiating outward, driven by spring() with per-particle delays. Company name and tagline fade up below, separated by an animated gradient divider line.
Scene two presents three stat cards — Commits Shipped, Launches, and Active Users — each entering with a staggered slide-up and scale spring, while the displayed number counts up using spring() mapped to the final value. Scene three slides in three moment cards sequentially from the right, each containing a stylized placeholder image region that wipes open left-to-right via a scaleX overlay, a title, caption, and an accent month tag. The final scene reveals the “What’s Next” heading in gradient gold text with a pulsing animated arrow and a staggered bullet list of upcoming goals.
Design choices include a deep #04060f cinema background with a subtle dot grid, a four-stop gradient palette (indigo, violet, gold, emerald), per-scene radial glow overlays, and a persistent vignette unifying all transitions. All data lives in typed constant arrays near the top of react.tsx — replace YEAR, COMPANY, STATS, and MOMENTS to adapt the video to any brand or year.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1280 × 720 |
| FPS | 30 |
| Duration | 10 s (300 frames) |
Timeline
| Time | Action |
|---|---|
| 0:00 – 0:02 (frames 0–60) | Year number springs in with particle burst; company name and tagline fade up |
| 0:02 – 0:03 (frames 60–90) | Cross-fade transition into Highlights scene |
| 0:03 – 0:05 (frames 80–160) | Three stat cards enter with stagger; numbers count up via spring |
| 0:06 – 0:08 (frames 175–240) | Best Moments — three image cards slide in right-to-left with wipe reveal |
| 0:08 – 0:10 (frames 255–300) | “What’s Next” heading scales in with gold gradient; arrow pulses; bullets stagger in |