StealThis .dev
Remotion Medium

Podcast Audiogram (Remotion)

A podcast audiogram with animated waveform bars, guest avatar, episode info, and progress bar — rendered with Remotion at 1280×720 30fps.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import {
  AbsoluteFill,
  Composition,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

const SHOW_NAME = "The Dev Show";
const EPISODE = "EP 42 · Building in Public";
const GUEST = "Jane Smith";
const BAR_COUNT = 40;
const ACCENT = "#8b5cf6";

function pseudoRandom(seed: number): number {
  const x = Math.sin(seed * 127.1) * 43758.5453;
  return x - Math.floor(x);
}

const WaveformBar: React.FC<{
  index: number;
  frame: number;
  total: number;
}> = ({ index, frame, total }) => {
  const noise = pseudoRandom(index * 7 + frame * 0.4);
  const base = 0.15 + noise * 0.85;
  const center = Math.abs(index - total / 2) / (total / 2);
  const heightPct = base * (1 - center * 0.4);
  const barWidth = 8;
  const gap = 4;

  return (
    <div
      style={{
        width: barWidth,
        height: `${heightPct * 100}%`,
        backgroundColor: ACCENT,
        borderRadius: 4,
        opacity: 0.6 + heightPct * 0.4,
        marginRight: gap,
        transition: "height 0.05s",
      }}
    />
  );
};

const ProgressBar: React.FC<{ frame: number; total: number }> = ({ frame, total }) => {
  const progress = interpolate(frame, [0, total], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const elapsed = Math.floor((frame / 30) * 1);
  const mins = Math.floor(elapsed / 60);
  const secs = elapsed % 60;
  const timeStr = `${mins}:${secs.toString().padStart(2, "0")}`;

  return (
    <div style={{ position: "absolute", bottom: 80, left: 100, right: 100 }}>
      <div
        style={{
          height: 4,
          backgroundColor: "rgba(255,255,255,0.15)",
          borderRadius: 2,
          overflow: "hidden",
        }}
      >
        <div
          style={{
            width: `${progress * 100}%`,
            height: "100%",
            backgroundColor: ACCENT,
            borderRadius: 2,
          }}
        />
      </div>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          marginTop: 8,
        }}
      >
        <span
          style={{
            fontFamily: "system-ui, sans-serif",
            fontSize: 14,
            color: "rgba(255,255,255,0.5)",
          }}
        >
          {timeStr}
        </span>
        <span
          style={{
            fontFamily: "system-ui, sans-serif",
            fontSize: 14,
            color: "rgba(255,255,255,0.5)",
          }}
        >
          42:18
        </span>
      </div>
    </div>
  );
};

export const PodcastAudiogram: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const titleOpacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const titleY = spring({
    frame,
    fps,
    from: -20,
    to: 0,
    config: { damping: 16, stiffness: 80 },
  });

  const waveOpacity = interpolate(frame, [15, 35], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const avatarScale = spring({
    frame,
    fps,
    from: 0,
    to: 1,
    config: { damping: 12, stiffness: 100 },
  });

  return (
    <AbsoluteFill style={{ backgroundColor: "#0f0f1a" }}>
      {/* Subtle gradient */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: `radial-gradient(ellipse at 50% 30%, ${ACCENT}12 0%, transparent 60%)`,
        }}
      />

      {/* Avatar */}
      <div
        style={{
          position: "absolute",
          top: 100,
          left: "50%",
          transform: `translateX(-50%) scale(${avatarScale})`,
        }}
      >
        <div
          style={{
            width: 100,
            height: 100,
            borderRadius: "50%",
            background: `linear-gradient(135deg, ${ACCENT}, #ec4899)`,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          <span
            style={{
              fontFamily: "system-ui, sans-serif",
              fontWeight: 700,
              fontSize: 40,
              color: "#fff",
            }}
          >
            {GUEST[0]}
          </span>
        </div>
      </div>

      {/* Show info */}
      <div
        style={{
          position: "absolute",
          top: 220,
          left: 0,
          right: 0,
          textAlign: "center",
          opacity: titleOpacity,
          transform: `translateY(${titleY}px)`,
        }}
      >
        <div
          style={{
            fontFamily: "system-ui, sans-serif",
            fontWeight: 700,
            fontSize: 36,
            color: "#ffffff",
            marginBottom: 8,
          }}
        >
          {SHOW_NAME}
        </div>
        <div
          style={{
            fontFamily: "system-ui, sans-serif",
            fontWeight: 400,
            fontSize: 20,
            color: "rgba(255,255,255,0.6)",
          }}
        >
          {EPISODE}
        </div>
        <div
          style={{
            fontFamily: "system-ui, sans-serif",
            fontWeight: 500,
            fontSize: 18,
            color: ACCENT,
            marginTop: 6,
          }}
        >
          with {GUEST}
        </div>
      </div>

      {/* Waveform */}
      <div
        style={{
          position: "absolute",
          top: 360,
          left: 100,
          right: 100,
          height: 120,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          opacity: waveOpacity,
        }}
      >
        {Array.from({ length: BAR_COUNT }).map((_, i) => (
          <WaveformBar key={i} index={i} frame={frame} total={BAR_COUNT} />
        ))}
      </div>

      <ProgressBar frame={frame} total={durationInFrames} />
    </AbsoluteFill>
  );
};

export const RemotionRoot: React.FC = () => (
  <Composition
    id="PodcastAudiogram"
    component={PodcastAudiogram}
    durationInFrames={300}
    fps={30}
    width={1280}
    height={720}
  />
);

Podcast Audiogram

A podcast audiogram with procedural waveform visualization, guest avatar, show and episode info, and a progress bar. The waveform bars animate continuously using a deterministic pseudo-random function — no actual audio input needed. The avatar scales in with a spring, while show info and waveform fade in with staggered timing.

Composition specs

PropertyValue
Resolution1280 x 720
FPS30
Duration10 s (300 frames)

Customization

  • SHOW_NAME — podcast title displayed below the avatar
  • EPISODE — episode number and title string
  • GUEST — guest name (first letter used for avatar initial)
  • ACCENT — theme color for waveform bars and progress bar (default #8b5cf6)