Catalog

Edit Props (live)

IMAGE
IMAGE
TEXT
TEXT
config
config
config
config
config
config
config
config

Source

395 lines
import 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_chart

Split Chart — Andy's hand-crafted Remotion scene. 12 props. logo:IMAGE, logoWordmark:IMAGE, chartTitle:TEXT, +1 more.

Workflow Inputs (4)

  • Logo
    logo
    IMAGE
  • Logo Wordmark
    logoWordmark
    IMAGE
  • Chart Title
    chartTitle
    TEXTRequired
  • Chart Subtitle
    chartSubtitle
    TEXT

Config Fields (8)

  • headSrc
  • chartBars
  • chartUnit
  • dataSource
  • accentColor
  • neutralColor
  • headStartFrame
  • backgroundColor

Meta

Updated
4/21/2026