Catalog

Edit Props (live)

TEXT
TEXT
VIDEO
config
config
config
config
config
config
config
config

Source

353 lines
import React from "react";
import {
  AbsoluteFill,
  Img,
  OffthreadVideo,
  Sequence,
  staticFile,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  Easing,
} from "remotion";
import { loadFont as loadZilla } from "@remotion/google-fonts/ZillaSlab";
import { loadFont as loadLora } from "@remotion/google-fonts/Lora";

// Fonts:
//   - Zilla Slab 900 for the scramble caps (closer to the reference slab than
//     Roboto Slab — narrower shoulders, flatter serifs).
//   - Lora 500 for the Claude wordmark (refined serif, matches Tiempos /
//     Copernicus family the real Anthropic mark uses).
loadZilla("normal", { weights: ["700"] });
loadLora("normal", { weights: ["500", "600"] });

// Scramble-to-reveal two-line text hook. Letters in each line tumble through
// random glyphs (cipher-style) before locking on the final character. Inspired
// by the "MANAGED / AGENTS" opener in the reference reel.
//
// Optional Claude sparkle outro: after the scramble settles, the frame fades
// to cream and the real claude.svg sparkle fades in. Then "Claude" appears
// letter-by-letter to the right of the sparkle, each letter ramping from
// transparent through a mid-grey to full black.

const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#@$%&*!?><[]{}+-";

interface GlitchTextRevealProps {
  topText: string;
  bottomText: string;
  topColor?: string;        // default Claude orange
  bottomColor?: string;     // default white
  backgroundColor?: string; // default "#000"
  scrambleDurationS?: number; // how long the scramble lasts, default 1.4s
  holdAfterS?: number;      // hold on settled text, default 0.5s
  showClaudeOutro?: boolean; // if true, fade to cream + Claude reveal clip
  outroBackground?: string; // cream bg for outro, matched to Anthropic brand
  sparkleColor?: string;    // unused (sparkle is part of the outro clip)
  outroVideoSrc?: string;   // relative path under public/visuals — defaults
                             // to the real Anthropic Claude reveal clip
                             // extracted from the reference IG reel
}

const SLAB = "'Zilla Slab', serif";
const SERIF = "'Lora', 'Tiempos Text', serif";

// Pseudo-random based on seed — stable across frames for a given letter slot
function rand(seed: number): number {
  const x = Math.sin(seed) * 43758.5453;
  return x - Math.floor(x);
}

function scrambledChar(i: number, frame: number, finalChar: string, locked: boolean): string {
  if (locked) return finalChar;
  if (finalChar === " ") return " ";
  // Tumble glyph every 3 frames
  const tick = Math.floor(frame / 3);
  const r = rand(i * 13.37 + tick * 7.77);
  return GLYPHS[Math.floor(r * GLYPHS.length)];
}

const ScrambleLine: React.FC<{
  text: string;
  color: string;
  startFrame: number;
  settleDurationFrames: number;
  frame: number;
  fontSize: number;
  letterSpacing?: number;
}> = ({ text, color, startFrame, settleDurationFrames, frame, fontSize, letterSpacing = -1 }) => {
  const localFrame = frame - startFrame;
  const letters = text.split("");
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        gap: 0,
        fontFamily: SLAB,
        fontSize,
        fontWeight: 700,
        letterSpacing,
        lineHeight: 1.0,
      }}
    >
      {letters.map((ch, i) => {
        const lockFrame = (i / Math.max(letters.length - 1, 1)) * settleDurationFrames;
        const locked = localFrame >= lockFrame;
        const displayed = scrambledChar(i, localFrame, ch, locked);
        const sinceLock = locked ? localFrame - lockFrame : 0;
        const flashing = locked && sinceLock < 4;
        return (
          <span
            key={i}
            style={{
              color: flashing ? "#ffffff" : color,
              backgroundColor: flashing ? color : "transparent",
              padding: flashing ? "0 2px" : 0,
              opacity: localFrame < 0 ? 0 : 1,
              transition: "color 120ms, background-color 120ms",
              fontVariantNumeric: "tabular-nums",
            }}
          >
            {displayed}
          </span>
        );
      })}
    </div>
  );
};

// Real Claude sparkle — uses the actual claude.svg asset. To animate the
// arms we apply a dual-axis scale pulse with 180° phase offset: scaleX and
// scaleY oscillate in opposition, so the horizontal arms grow while the
// vertical ones shrink and vice versa. The net effect reads as "arms
// morphing" even though the SVG itself is a single continuous path and
// can't be manipulated per-arm. A very slow rotation adds organic life.
const ClaudeSparkle: React.FC<{
  size: number;
  frame: number;
  fps: number;
  speedHz?: number;
}> = ({ size, frame, fps, speedHz = 0.9 }) => {
  const t = (frame / fps) * speedHz * Math.PI * 2;
  // scaleX / scaleY oscillate in opposition → sparkle alternates between
  // "wide" (horizontal arms long) and "tall" (vertical arms long) silhouette.
  const sx = 1 + Math.sin(t) * 0.08;               // 0.92..1.08
  const sy = 1 + Math.sin(t + Math.PI) * 0.08;     // opposite phase
  // Slow rotation — one full turn every ~30s so it's barely perceptible
  // but adds micro-life to the shape.
  const rot = (frame / fps) * 12;                  // deg/s
  return (
    <div
      style={{
        width: size,
        height: size,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Img
        src={staticFile("logos/claude.svg")}
        style={{
          width: size,
          height: size,
          objectFit: "contain",
          transform: `rotate(${rot}deg) scale(${sx}, ${sy})`,
          transformOrigin: "center",
          willChange: "transform",
        }}
      />
    </div>
  );
};

// Renders "Claude" as individual letters that stamp in one by one. Each
// letter opacity-fades 0→1 over 3 frames with a 4-frame stagger — matches
// the tight typewriter feel of the reference. A small translateY (6→0px)
// gives each letter a subtle settle. Letters stay at full near-black the
// whole time; only opacity + Y animate.
const ClaudeWordmark: React.FC<{
  startFrame: number;
  frame: number;
  fontSize: number;
  staggerFrames?: number;
  perLetterFadeFrames?: number;
  color?: string;
}> = ({
  startFrame,
  frame,
  fontSize,
  staggerFrames = 4,
  perLetterFadeFrames = 3,
  color = "#141414",
}) => {
  const text = "Claude";
  return (
    <div
      style={{
        fontFamily: SERIF,
        fontSize,
        fontWeight: 500,
        letterSpacing: -2,
        display: "inline-flex",
        lineHeight: 1.0,
        color,
      }}
    >
      {text.split("").map((ch, i) => {
        const letterStart = startFrame + i * staggerFrames;
        const op = interpolate(
          frame,
          [letterStart, letterStart + perLetterFadeFrames],
          [0, 1],
          { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
        );
        const y = interpolate(
          frame,
          [letterStart, letterStart + perLetterFadeFrames + 2],
          [6, 0],
          { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
        );
        return (
          <span
            key={i}
            style={{
              opacity: op,
              transform: `translateY(${y}px)`,
              display: "inline-block",
            }}
          >
            {ch}
          </span>
        );
      })}
    </div>
  );
};

export const GlitchTextReveal: React.FC<GlitchTextRevealProps> = ({
  topText,
  bottomText,
  topColor = "#D97757",
  bottomColor = "#ffffff",
  backgroundColor = "#000000",
  scrambleDurationS = 1.4,
  holdAfterS = 0.5,
  showClaudeOutro = false,
  outroBackground = "#F0E8DB",  // warmer cream to match Anthropic brand bg
  sparkleColor = "#D97757",
  outroVideoSrc = "visuals/claude_reveal.mp4",
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const scrambleFrames = Math.round(scrambleDurationS * fps);
  const holdFrames = Math.round(holdAfterS * fps);
  const totalPhase1 = scrambleFrames + holdFrames;

  // Phase transition — hard cut + brief (8 frame) cross-fade.
  const outroStartFrame = totalPhase1 + 4;
  const crossFade = interpolate(frame, [outroStartFrame, outroStartFrame + 8], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.inOut(Easing.cubic),
  });
  const inOutro = showClaudeOutro && frame >= outroStartFrame;

  // Sparkle gentle fade-in (opacity + soft scale 0.85→1.0), then holds.
  // No bounce — reference has a calm fade, not a spring pop.
  const sparkleStartFrame = outroStartFrame + 6;
  const sparkleOpacity = interpolate(
    frame,
    [sparkleStartFrame, sparkleStartFrame + 12],
    [0, 1],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
  );
  const sparkleScale = interpolate(
    frame,
    [sparkleStartFrame, sparkleStartFrame + 14],
    [0.85, 1.0],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
  );

  // Wordmark letters start appearing ~14 frames after sparkle (sparkle needs
  // to establish itself first).
  const wordmarkStartFrame = sparkleStartFrame + 14;

  const phase1Opacity = inOutro ? 1 - crossFade : 1;

  // Mix bg colors during cross-fade by layering outro bg above phase1 bg.
  return (
    <AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
      {/* Outro background layer (cream) — fades in during transition */}
      {inOutro && (
        <div
          style={{
            position: "absolute",
            inset: 0,
            backgroundColor: outroBackground,
            opacity: crossFade,
          }}
        />
      )}

      {/* PHASE 1 — scramble text */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          gap: 20,
          opacity: phase1Opacity,
        }}
      >
        <ScrambleLine
          text={topText}
          color={topColor}
          startFrame={4}
          settleDurationFrames={scrambleFrames}
          frame={frame}
          fontSize={180}
        />
        <ScrambleLine
          text={bottomText}
          color={bottomColor}
          startFrame={10}
          settleDurationFrames={scrambleFrames}
          frame={frame}
          fontSize={180}
        />
      </div>

      {/* PHASE 2 — Claude reveal: plays the real Anthropic animation clip
          (sparkle arms pulse + typewriter wordmark) as an OffthreadVideo so
          we get the exact motion instead of a synthetic reconstruction. */}
      {showClaudeOutro && outroVideoSrc && (
        <Sequence from={outroStartFrame} layout="none">
          <div
            style={{
              position: "absolute",
              inset: 0,
              opacity: crossFade,
            }}
          >
            <OffthreadVideo
              src={outroVideoSrc.startsWith("http") ? outroVideoSrc : staticFile(outroVideoSrc)}
              muted
              style={{
                width: "100%",
                height: "100%",
                objectFit: "cover",
                display: "block",
              }}
            />
          </div>
        </Sequence>
      )}
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: GlitchTextReveal
// 11 props detected · 3 port(s)

Glitch Text Reveal

glitch_text_reveal

Glitch Text Reveal — Andy's hand-crafted Remotion scene. 11 props. topText:TEXT, bottomText:TEXT, outroVideoSrc:VIDEO.

Workflow Inputs (3)

  • Top Text
    topText
    TEXTRequired
  • Bottom Text
    bottomText
    TEXTRequired
  • Outro Video Src
    outroVideoSrc
    VIDEO

Config Fields (8)

  • topColor
  • holdAfterS
  • bottomColor
  • sparkleColor
  • backgroundColor
  • outroBackground
  • showClaudeOutro
  • scrambleDurationS

Meta

Updated
4/21/2026