← Catalog
Edit Props (live)
IMAGE
IMAGE
TEXT
TEXT
config
config
config
config
config
config
config
config
Source
395 linesimport React from "react";
import {
AbsoluteFill,
Img,
OffthreadVideo,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
spring,
staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
loadInter("normal", { weights: ["400", "500", "700", "900"] });
interface ChartBar {
label: string;
value: number;
highlight?: boolean; // paint in accent color instead of neutral
}
interface SplitChartProps {
logo?: string; // e.g. "logos/openai.png"
logoWordmark?: string; // e.g. "ChatGPT"
chartTitle: string; // e.g. "Hallucination Rate by Prompting Method"
chartSubtitle?: string; // e.g. "n = 1,200 prompts"
chartBars: ChartBar[];
chartUnit?: string; // e.g. "%", default "%"
dataSource?: string; // e.g. "Johns Hopkins APL, 2025"
accentColor?: string; // bar highlight color, default "#22c55e"
neutralColor?: string; // non-highlighted bar color, default "#3a1a2a"
backgroundColor?: string; // scene bg, default "#0a0612"
headSrc?: string | null; // talking head clip
headStartFrame?: number;
}
const FONT = "'Inter', sans-serif";
const isVideo = (s: string) => /\.(mp4|mov|webm)(\?|$)/i.test(s);
const resolveStatic = (s: string) => (s.startsWith("http") ? s : staticFile(s));
export const SplitChart: React.FC<SplitChartProps> = ({
logo,
logoWordmark,
chartTitle,
chartSubtitle,
chartBars,
chartUnit = "%",
dataSource,
accentColor = "#22c55e",
neutralColor = "#3a1a2a",
backgroundColor = "#0a0612",
headSrc,
headStartFrame,
}) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const maxVal = Math.max(...chartBars.map((b) => b.value), 1);
return (
<AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
{/* Top half — white card with chart */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "50%",
background: "#fff",
padding: "48px 60px 48px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 22,
overflow: "hidden",
}}
>
{/* Logo + wordmark header */}
{(logo || logoWordmark) && (
<ChartHeader logo={logo} wordmark={logoWordmark} />
)}
{/* Title */}
<ChartTitle text={chartTitle} subtitle={chartSubtitle} />
{/* Bars */}
<div style={{ display: "flex", flexDirection: "column", gap: 14, marginTop: 6 }}>
{chartBars.map((b, i) => (
<ChartBarRow
key={i}
bar={b}
index={i}
maxVal={maxVal}
unit={chartUnit}
accentColor={accentColor}
neutralColor={neutralColor}
fps={fps}
frame={frame}
/>
))}
</div>
{/* Data source footer */}
{dataSource && (
<div
style={{
marginTop: 14,
fontFamily: FONT,
fontSize: 22,
color: "#888",
textAlign: "center",
fontWeight: 500,
}}
>
Data: {dataSource}
</div>
)}
</div>
{/* Bottom half — talking head */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "50%",
overflow: "hidden",
background: backgroundColor,
}}
>
{headSrc ? (
isVideo(headSrc) ? (
<OffthreadVideo
src={resolveStatic(headSrc)}
muted
startFrom={headStartFrame}
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
/>
) : (
<Img
src={resolveStatic(headSrc)}
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
/>
)
) : (
<HeadPlaceholder />
)}
</div>
{/* Seam */}
<div
style={{
position: "absolute",
top: "50%",
left: 0,
right: 0,
height: 2,
transform: "translateY(-1px)",
background: "rgba(255,255,255,0.15)",
zIndex: 5,
pointerEvents: "none",
}}
/>
</AbsoluteFill>
);
};
const ChartHeader: React.FC<{ logo?: string; wordmark?: string }> = ({ logo, wordmark }) => {
const frame = useCurrentFrame();
const op = interpolate(frame, [0, 12], [0, 1], { extrapolateRight: "clamp" });
const y = interpolate(frame, [0, 12], [8, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
opacity: op,
transform: `translateY(${y}px)`,
justifyContent: "center",
}}
>
{logo && (
<Img
src={resolveStatic(logo)}
style={{
width: 72,
height: 72,
borderRadius: 18,
objectFit: "contain",
background: "#f4f2ed",
}}
/>
)}
{wordmark && (
<div
style={{
fontFamily: FONT,
fontSize: 52,
fontWeight: 800,
color: "#111",
letterSpacing: -1.5,
}}
>
{wordmark}
</div>
)}
</div>
);
};
const ChartTitle: React.FC<{ text: string; subtitle?: string }> = ({ text, subtitle }) => {
const frame = useCurrentFrame();
const op = interpolate(frame, [8, 22], [0, 1], { extrapolateRight: "clamp" });
const y = interpolate(frame, [8, 22], [10, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
return (
<div
style={{
textAlign: "center",
opacity: op,
transform: `translateY(${y}px)`,
}}
>
<div
style={{
fontFamily: FONT,
fontSize: 44,
fontWeight: 700,
color: "#111",
letterSpacing: -1.2,
lineHeight: 1.15,
}}
>
{text}
</div>
{subtitle && (
<div
style={{
fontFamily: FONT,
fontSize: 26,
fontWeight: 500,
color: "#666",
marginTop: 4,
}}
>
{subtitle}
</div>
)}
</div>
);
};
const ChartBarRow: React.FC<{
bar: ChartBar;
index: number;
maxVal: number;
unit: string;
accentColor: string;
neutralColor: string;
fps: number;
frame: number;
}> = ({ bar, index, maxVal, unit, accentColor, neutralColor, fps, frame }) => {
// Bars grow in staggered order
const startFrame = 26 + index * 6;
const grow = spring({
frame: frame - startFrame,
fps,
config: { mass: 1, damping: 18, stiffness: 140 },
});
const barWidthPct = (bar.value / maxVal) * 75; // 75% of row width max (leaves room for label)
const animatedWidthPct = grow * barWidthPct;
// Value counter animation
const counterT = interpolate(frame, [startFrame, startFrame + 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const shownValue = bar.value * counterT;
const rowOp = interpolate(frame, [startFrame - 4, startFrame + 6], [0, 1], {
extrapolateRight: "clamp",
});
const barColor = bar.highlight ? accentColor : neutralColor;
return (
<div
style={{
display: "grid",
gridTemplateColumns: "220px 1fr",
alignItems: "center",
gap: 14,
opacity: rowOp,
}}
>
{/* Label */}
<div
style={{
fontFamily: FONT,
fontSize: 22,
fontWeight: 600,
color: "#333",
textAlign: "right",
lineHeight: 1.15,
}}
>
{bar.label}
</div>
{/* Bar track */}
<div style={{ position: "relative", height: 52, display: "flex", alignItems: "center" }}>
<div
style={{
height: "100%",
width: `${animatedWidthPct}%`,
background: barColor,
borderRadius: 4,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
paddingRight: 14,
transition: "width 0.3s ease",
}}
>
{animatedWidthPct > 8 && (
<span
style={{
color: "#fff",
fontFamily: FONT,
fontSize: 24,
fontWeight: 700,
}}
>
{shownValue.toFixed(1)}{unit}
</span>
)}
</div>
{animatedWidthPct <= 8 && grow > 0.2 && (
<span
style={{
position: "absolute",
left: `${animatedWidthPct + 2}%`,
color: "#111",
fontFamily: FONT,
fontSize: 24,
fontWeight: 700,
}}
>
{shownValue.toFixed(1)}{unit}
</span>
)}
</div>
</div>
);
};
const HeadPlaceholder: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const pulse = 0.6 + Math.sin((frame / fps) * 1.2) * 0.08;
return (
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse at 50% 45%, rgba(140, 90, 220, 0.45) 0%, rgba(20, 8, 30, 1) 65%)",
}}
>
<div
style={{
position: "absolute",
top: "48%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 360,
height: 360,
borderRadius: "50%",
background: `radial-gradient(circle, rgba(240,220,255,${0.18 * pulse}) 0%, transparent 70%)`,
filter: "blur(30px)",
}}
/>
</div>
);
};
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: SplitChart
// 12 props detected · 4 port(s)
Split Chart
split_chartSplit Chart — Andy's hand-crafted Remotion scene. 12 props. logo:IMAGE, logoWordmark:IMAGE, chartTitle:TEXT, +1 more.
Workflow Inputs (4)
- Logo
logoIMAGE - Logo Wordmark
logoWordmarkIMAGE - Chart Title
chartTitleTEXTRequired - Chart Subtitle
chartSubtitleTEXT
Config Fields (8)
headSrcchartBarschartUnitdataSourceaccentColorneutralColorheadStartFramebackgroundColor
Meta
- Updated
- 4/21/2026