Catalog

Edit Props (live)

TEXT
IMAGE
config
config
config
config
config

Source

393 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";
import { loadFont as loadMono } from "@remotion/google-fonts/JetBrainsMono";

loadInter("normal", { weights: ["400", "700", "900"] });
loadMono("normal", { weights: ["400", "500"] });

const MONO = "'JetBrains Mono', monospace";
const ACCENT = "#D97757";
const CURSOR_COLORS = ["#5EEAD4", "#FACC15", "#60A5FA", "#F472B6"];

const isVideo = (s: string) => /\.(mp4|mov|webm)(\?|$)/i.test(s);
const resolveStatic = (s: string) => (s.startsWith("http") ? s : staticFile(s));

interface CursorDef {
  color: string;
  from: [number, number];
  to: [number, number];
  startF: number;
}

interface ClaudeCodeLogoSplitProps {
  promptText?: string;
  typingCps?: number;
  splitHeadSrc?: string;
  topBackground?: string;
  accentShapesColor?: string;
  cursors?: CursorDef[];
  // When set, replaces the pixel-drawn wordmark with this image (wiped in L→R).
  logoImageSrc?: string;
}

type Letter = string[];
const ALPHABET: Record<string, Letter> = {
  C: [".XXX.", "X...X", "X....", "X....", "X....", "X...X", ".XXX."],
  L: ["X....", "X....", "X....", "X....", "X....", "X....", "XXXXX"],
  A: [".XXX.", "X...X", "X...X", "XXXXX", "X...X", "X...X", "X...X"],
  U: ["X...X", "X...X", "X...X", "X...X", "X...X", "X...X", ".XXX."],
  D: ["XXXX.", "X...X", "X...X", "X...X", "X...X", "X...X", "XXXX."],
  E: ["XXXXX", "X....", "X....", "XXX..", "X....", "X....", "XXXXX"],
  O: [".XXX.", "X...X", "X...X", "X...X", "X...X", "X...X", ".XXX."],
  " ": [".....", ".....", ".....", ".....", ".....", ".....", "....."],
};

const MouseCursor: React.FC<{ color: string; size?: number }> = ({ color, size = 64 }) => (
  <svg width={size} height={size * (32 / 28)} viewBox="0 0 28 32" style={{ display: "block", overflow: "visible" }}>
    <path
      d="M 5 3 L 5 23 L 10 18 L 13 26 L 17 24 L 14 16 L 21 15 Z"
      fill={color}
      stroke="#0d0d0f"
      strokeWidth={1.6}
      strokeLinejoin="round"
      strokeLinecap="round"
    />
  </svg>
);

const PixelLetter: React.FC<{
  letter: string;
  cell: number;
  color: string;
}> = ({ letter, cell, color }) => {
  const glyph = ALPHABET[letter.toUpperCase()] || ALPHABET[" "];
  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      {glyph.map((row, r) => (
        <div key={r} style={{ display: "flex" }}>
          {row.split("").map((c, x) => (
            <div
              key={x}
              style={{
                width: cell,
                height: cell,
                background: c === "X" ? color : "transparent",
                boxShadow: c === "X" ? `2px 2px 0 ${color}66` : undefined,
              }}
            />
          ))}
        </div>
      ))}
    </div>
  );
};

// Word cell-widths: each letter is 5 cells wide, gap between letters = 1 cell.
const wordCellWidth = (text: string) => text.length * 5 + Math.max(0, text.length - 1);

const PixelWord: React.FC<{
  text: string;
  cell: number;
  color: string;
  revealCells: number; // total cells revealed by the shared sweep
}> = ({ text, cell, color, revealCells }) => {
  const totalCells = wordCellWidth(text);
  const cellsShown = Math.max(0, Math.min(totalCells, revealCells));
  const clipRightPct = ((totalCells - cellsShown) / totalCells) * 100;
  // scanline: thin bright edge at the current reveal column, while still sweeping
  const sweeping = cellsShown > 0 && cellsShown < totalCells;
  const scanX = cellsShown * cell;
  return (
    <div style={{ position: "relative" }}>
      <div
        style={{
          display: "flex",
          gap: cell,
          clipPath: `inset(0 ${clipRightPct}% 0 0)`,
          WebkitClipPath: `inset(0 ${clipRightPct}% 0 0)`,
        }}
      >
        {text.split("").map((ch, i) => (
          <PixelLetter key={i} letter={ch} cell={cell} color={color} />
        ))}
      </div>
      {sweeping && (
        <div
          style={{
            position: "absolute",
            left: scanX - 2,
            top: -4,
            width: 4,
            height: 7 * cell + 8,
            background: "rgba(255, 255, 255, 0.8)",
            boxShadow: "0 0 16px rgba(255, 212, 178, 0.9)",
            pointerEvents: "none",
          }}
        />
      )}
    </div>
  );
};

export const ClaudeCodeLogoSplit: React.FC<ClaudeCodeLogoSplitProps> = ({
  promptText = "Build an MVP for my food delivery app",
  typingCps = 28,
  splitHeadSrc,
  topBackground = "#0d0d0f",
  cursors,
  logoImageSrc,
}) => {
  const frame = useCurrentFrame();
  const { fps, width, height } = useVideoConfig();

  const hasHead = !!splitHeadSrc;
  const splitY = Math.round(height * 0.5);
  const contentBottom = hasHead ? splitY : height;
  const logoCenterY = hasHead ? Math.round(splitY * 0.42) : Math.round(height * 0.42);
  const inputY = hasHead ? Math.round(splitY * 0.82) : Math.round(height * 0.68);

  const cell = Math.floor(width / 50);

  // Shared L→R sweep across both words.
  // Longest word = CLAUDE (35 cells). Progress runs cells 0..35 over ~22 frames.
  const maxCells = wordCellWidth("CLAUDE");
  const sweepT = interpolate(frame, [3, 25], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });
  const revealCells = sweepT * maxCells;

  const defaultCursors: CursorDef[] = [
    { color: CURSOR_COLORS[0], from: [-0.15, 0.25], to: [0.78, 0.24], startF: 14 },
    { color: CURSOR_COLORS[1], from: [1.15, 0.10], to: [0.85, 0.46], startF: 18 },
    { color: CURSOR_COLORS[2], from: [1.18, 0.85], to: [0.30, 0.62], startF: 22 },
    { color: CURSOR_COLORS[3], from: [-0.18, 0.95], to: [0.16, 0.54], startF: 26 },
  ];
  const cursorList = cursors && cursors.length ? cursors : defaultCursors;

  const promptFadeIn = interpolate(frame, [30, 42], [0, 1], { extrapolateRight: "clamp" });
  const typeStart = 34;
  const typeChars = Math.max(0, Math.floor(((frame - typeStart) / fps) * typingCps));
  const typedPrompt = promptText.slice(0, typeChars);
  const cursorBlink = typeChars < promptText.length && Math.floor(frame / 9) % 2 === 0;

  return (
    <AbsoluteFill style={{ backgroundColor: topBackground, overflow: "hidden" }}>
      {/* Top region: logo + input (full-frame when no head, else top half) */}
      <div
        style={{
          position: "absolute",
          top: 0,
          left: 0,
          right: 0,
          height: contentBottom,
          background: topBackground,
          overflow: "hidden",
        }}
      >
        {logoImageSrc ? (
          <div
            style={{
              position: "absolute",
              top: 0,
              left: 0,
              right: 0,
              height: contentBottom,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              clipPath: `inset(0 ${(1 - sweepT) * 100}% 0 0)`,
              WebkitClipPath: `inset(0 ${(1 - sweepT) * 100}% 0 0)`,
            }}
          >
            <Img
              src={resolveStatic(logoImageSrc)}
              style={{
                width: "100%",
                height: "100%",
                objectFit: "contain",
                display: "block",
              }}
            />
          </div>
        ) : (
          <div
            style={{
              position: "absolute",
              top: logoCenterY,
              left: 0,
              right: 0,
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              gap: 14,
              transform: "translateY(-50%)",
            }}
          >
            <PixelWord text="CLAUDE" cell={cell} color={ACCENT} revealCells={revealCells} />
            <PixelWord text="CODE" cell={cell} color={ACCENT} revealCells={revealCells} />
          </div>
        )}

        {logoImageSrc && sweepT > 0 && sweepT < 1 && (
          <div
            style={{
              position: "absolute",
              top: 0,
              left: `${sweepT * 100}%`,
              width: 6,
              height: contentBottom,
              transform: "translateX(-3px)",
              background: "rgba(255,255,255,0.85)",
              boxShadow: "0 0 22px rgba(255,212,178,0.95), 0 0 40px rgba(217,119,87,0.6)",
              pointerEvents: "none",
              zIndex: 5,
            }}
          />
        )}

        {cursorList.map((cur, i) => {
          const t = spring({
            frame: frame - cur.startF,
            fps,
            config: { mass: 0.9, damping: 15, stiffness: 160 },
          });
          const x = interpolate(t, [0, 1], [cur.from[0], cur.to[0]]) * width;
          const yBase = interpolate(t, [0, 1], [cur.from[1], cur.to[1]]) * contentBottom;
          const wobble = t > 0.9 ? Math.sin((frame - cur.startF) * 0.15) * 3 : 0;
          const y = yBase + wobble;
          const op = interpolate(frame - cur.startF, [0, 4], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });
          const pop = spring({
            frame: frame - (cur.startF + 18),
            fps,
            config: { mass: 0.6, damping: 11, stiffness: 220 },
          });
          const popScale = interpolate(pop, [0, 1], [1.25, 1]);
          return (
            <div
              key={i}
              style={{
                position: "absolute",
                left: x,
                top: y,
                opacity: op,
                transform: `scale(${popScale})`,
                pointerEvents: "none",
                filter: "drop-shadow(0 4px 8px rgba(0,0,0,0.45))",
              }}
            >
              <MouseCursor color={cur.color} size={64} />
            </div>
          );
        })}

        {!logoImageSrc && (
          <div
            style={{
              position: "absolute",
              top: inputY,
              left: 0,
              right: 0,
              display: "flex",
              justifyContent: "center",
              opacity: promptFadeIn,
            }}
          >
            <div
              style={{
                background: "#000000",
                border: "1px solid rgba(255,255,255,0.18)",
                borderRadius: 10,
                padding: "16px 22px",
                color: "#ffffff",
                fontFamily: MONO,
                fontSize: 28,
                width: "82%",
              }}
            >
              <span style={{ color: ACCENT, marginRight: 12 }}>{">"}</span>
              {typedPrompt}
              {cursorBlink && (
                <span
                  style={{
                    display: "inline-block",
                    width: 12,
                    height: 28,
                    background: "#ffffff",
                    verticalAlign: "text-bottom",
                    marginLeft: 2,
                  }}
                />
              )}
            </div>
          </div>
        )}
      </div>

      {hasHead && (
        <>
          <div
            style={{
              position: "absolute",
              top: splitY,
              left: 0,
              right: 0,
              bottom: 0,
              background: "#1a1a1c",
              overflow: "hidden",
            }}
          >
            {isVideo(splitHeadSrc!) ? (
              <OffthreadVideo
                src={resolveStatic(splitHeadSrc!)}
                muted
                style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
              />
            ) : (
              <Img
                src={resolveStatic(splitHeadSrc!)}
                style={{
                  width: "100%",
                  height: "100%",
                  objectFit: "cover",
                  objectPosition: "center -25px",
                  display: "block",
                }}
              />
            )}
          </div>
          <div
            style={{
              position: "absolute",
              top: splitY,
              left: 0,
              right: 0,
              height: 3,
              transform: "translateY(-1.5px)",
              background: "rgba(255,255,255,0.15)",
              boxShadow: "0 0 14px rgba(255,255,255,0.22)",
              pointerEvents: "none",
              zIndex: 8,
            }}
          />
        </>
      )}
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: ClaudeCodeLogoSplit
// 7 props detected · 2 port(s)

Claude Code Logo Split

claude_code_logo_split

Claude Code Logo Split — Andy's hand-crafted Remotion scene. 7 props. promptText:TEXT, logoImageSrc:IMAGE.

Workflow Inputs (2)

  • Prompt Text
    promptText
    TEXT
  • Logo Image Src
    logoImageSrc
    IMAGE

Config Fields (5)

  • cursors
  • typingCps
  • splitHeadSrc
  • topBackground
  • accentShapesColor

Meta

Updated
4/21/2026