Catalog

Edit Props (live)

TEXT
config
config
config
config
config

Source

418 lines
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
  Easing,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";

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

const FONT = "'Inter', sans-serif";

interface BrandFlowProps {
  brandName?: string;
  accentPalette?: string[];
  fontSamples?: string[];
  outputSrc?: string;
  backgroundColor?: string;
  captionText?: string;
}

const DEFAULT_PALETTE = ["#509ADB", "#57CEB2", "#FFB080", "#FFD166", "#FF6B6B"];
const DEFAULT_FONTS = ["Aa", "Aa"];

const hex2rgba = (hex: string, a: number) => {
  const h = hex.replace("#", "");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
};

export const BrandFlow: React.FC<BrandFlowProps> = ({
  brandName,
  accentPalette,
  fontSamples,
  outputSrc,
  backgroundColor = "#509ADB",
  captionText,
}) => {
  const frame = useCurrentFrame();
  const { fps, width, height } = useVideoConfig();
  const palette = (accentPalette && accentPalette.length >= 2) ? accentPalette : DEFAULT_PALETTE;
  const fonts = (fontSamples && fontSamples.length > 0) ? fontSamples : DEFAULT_FONTS;

  // Phase timing (frames @ 30fps; remotion passes fps via hook but these are
  // absolute frame anchors — good for a 3-4s scene).
  const F_FILE_IN = 4;      // file icon springs in
  const F_FILE_SET = 16;    // file fully visible, about to open
  const F_SWATCH_START = 18; // swatches + fonts begin flying out
  const F_SWATCH_END = 42;
  const F_OUTPUT_IN = 44;   // styled mockup fades up
  const F_OUTPUT_SET = 60;

  // FILE ICON — springs in from nothing
  const fileEnter = spring({
    frame: frame - F_FILE_IN,
    fps,
    config: { mass: 1, damping: 12, stiffness: 140 },
  });
  const fileScale = interpolate(fileEnter, [0, 1], [0.2, 1]);
  const fileOp = interpolate(frame, [F_FILE_IN, F_FILE_IN + 10], [0, 1], {
    extrapolateRight: "clamp",
  });
  // Slight gentle bob during hold
  const bob = Math.sin((frame - F_FILE_SET) / 10) * 4;

  // Icon itself fades out as elements fly from it
  const fileExit = interpolate(frame, [F_SWATCH_END - 4, F_OUTPUT_SET], [1, 0.35], {
    extrapolateLeft: "clamp", extrapolateRight: "clamp",
  });

  // Output mockup fade-up
  const outputEnter = spring({
    frame: frame - F_OUTPUT_IN,
    fps,
    config: { mass: 1, damping: 14, stiffness: 120 },
  });
  const outputScale = interpolate(outputEnter, [0, 1], [0.7, 1]);
  const outputOp = interpolate(frame, [F_OUTPUT_IN, F_OUTPUT_IN + 14], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Caption
  const capOp = interpolate(frame, [8, 22], [0, 1], { extrapolateRight: "clamp" });
  const capY = interpolate(frame, [8, 22], [14, 0], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Center anchor
  const cx = width / 2;
  const cy = height / 2;

  // FILE ICON SIZE — big enough to read without dominating
  const iconW = Math.floor(width * 0.34);
  const iconH = Math.floor(iconW * 1.15);

  // SWATCH POSITIONS — arc above the file, evenly spaced
  const swatchSize = Math.floor(width * 0.11);
  const swatchRadiusX = Math.floor(width * 0.28);
  const swatchRadiusY = Math.floor(height * 0.14);
  const nSwatches = Math.min(palette.length, 5);

  // FONT SAMPLE POSITIONS — below the file, two sides
  const fontSize = Math.floor(width * 0.11);

  return (
    <AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
      {/* Subtle radial glow for depth */}
      <div
        style={{
          position: "absolute",
          top: `${50}%`,
          left: "50%",
          width: width * 1.2,
          height: height * 0.6,
          transform: "translate(-50%, -50%)",
          background: "transparent",
          pointerEvents: "none",
        }}
      />

      {/* CAPTION */}
      {captionText ? (
        <div
          style={{
            position: "absolute",
            top: 120,
            left: 0,
            right: 0,
            textAlign: "center",
            fontFamily: FONT,
            fontWeight: 800,
            fontSize: 56,
            color: "#ffffff",
            letterSpacing: -1,
            padding: "0 50px",
            opacity: capOp,
            transform: `translateY(${capY}px)`,
            textShadow: "0 4px 20px rgba(0,0,0,0.3)",
            zIndex: 10,
          }}
        >
          {captionText}
        </div>
      ) : null}

      {/* BRAND NAME chip, optional */}
      {brandName ? (
        <div
          style={{
            position: "absolute",
            bottom: 140,
            left: 0,
            right: 0,
            textAlign: "center",
            fontFamily: FONT,
            fontWeight: 700,
            fontSize: 36,
            color: "#ffffff",
            letterSpacing: 2,
            textTransform: "uppercase",
            opacity: outputOp,
          }}
        >
          {brandName}
        </div>
      ) : null}

      {/* FILE ICON — brand.zip-style folder */}
      <div
        style={{
          position: "absolute",
          left: cx - iconW / 2,
          top: cy - iconH / 2 + bob,
          width: iconW,
          height: iconH,
          opacity: fileOp * fileExit,
          transform: `scale(${fileScale})`,
          transformOrigin: "center center",
          zIndex: 5,
        }}
      >
        {/* Folder body */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            borderRadius: Math.floor(iconW * 0.08),
            background: "linear-gradient(160deg, #ffffff 0%, #e5e8ee 100%)",
            boxShadow:
              "0 30px 70px rgba(0,0,0,0.35), 0 10px 24px rgba(0,0,0,0.2)",
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            justifyContent: "center",
            padding: Math.floor(iconW * 0.08),
          }}
        >
          {/* Header tab */}
          <div
            style={{
              position: "absolute",
              top: Math.floor(iconH * 0.08),
              left: Math.floor(iconW * 0.10),
              width: Math.floor(iconW * 0.32),
              height: Math.floor(iconH * 0.07),
              borderRadius: Math.floor(iconW * 0.025),
              background: palette[0],
            }}
          />
          {/* File content lines */}
          <div style={{ width: "80%", marginTop: Math.floor(iconH * 0.14) }}>
            {[0.88, 0.72, 0.94, 0.58, 0.8].map((w, i) => (
              <div
                key={i}
                style={{
                  width: `${w * 100}%`,
                  height: Math.floor(iconH * 0.04),
                  margin: `${Math.floor(iconH * 0.025)}px 0`,
                  borderRadius: 4,
                  background: "rgba(0,0,0,0.14)",
                }}
              />
            ))}
          </div>
          {/* Extension label */}
          <div
            style={{
              position: "absolute",
              bottom: Math.floor(iconH * 0.08),
              right: Math.floor(iconW * 0.10),
              fontFamily: FONT,
              fontWeight: 800,
              fontSize: Math.floor(iconH * 0.07),
              color: palette[0],
              letterSpacing: 1,
            }}
          >
            BRAND
          </div>
        </div>
      </div>

      {/* COLOR SWATCHES — fly out from the file in an upward arc */}
      {palette.slice(0, nSwatches).map((color, i) => {
        // Stagger: swatch i starts 2 frames after swatch i-1
        const startFrame = F_SWATCH_START + i * 2;
        const endFrame = startFrame + 22;
        const progress = interpolate(frame, [startFrame, endFrame], [0, 1], {
          extrapolateLeft: "clamp",
          extrapolateRight: "clamp",
          easing: Easing.out(Easing.cubic),
        });
        // Angle: spread swatches along a 160° arc above center
        const angleDeg = -80 + (160 / (nSwatches - 1 || 1)) * i;
        const angleRad = (angleDeg * Math.PI) / 180;
        const targetX = cx + Math.sin(angleRad) * swatchRadiusX - swatchSize / 2;
        const targetY = cy - swatchRadiusY - Math.abs(Math.cos(angleRad)) * swatchRadiusY * 0.5 - swatchSize / 2;
        const startX = cx - swatchSize / 2;
        const startY = cy - swatchSize / 2;
        const x = interpolate(progress, [0, 1], [startX, targetX]);
        const y = interpolate(progress, [0, 1], [startY, targetY]);
        const scale = interpolate(progress, [0, 1], [0.3, 1]);
        const fade = interpolate(frame, [F_OUTPUT_IN + 8, F_OUTPUT_SET], [1, 0.25], {
          extrapolateLeft: "clamp", extrapolateRight: "clamp",
        });

        return (
          <div
            key={`sw-${i}`}
            style={{
              position: "absolute",
              left: x,
              top: y,
              width: swatchSize,
              height: swatchSize,
              borderRadius: "50%",
              background: color,
              transform: `scale(${scale})`,
              opacity: progress * fade,
              boxShadow: `0 12px 30px ${hex2rgba(color, 0.45)}, 0 2px 8px rgba(0,0,0,0.25)`,
              border: "3px solid rgba(255,255,255,0.85)",
              zIndex: 6,
            }}
          />
        );
      })}

      {/* FONT SAMPLES — slide in left/right below the file */}
      {fonts.slice(0, 2).map((sample, i) => {
        const startFrame = F_SWATCH_START + 6 + i * 3;
        const progress = interpolate(frame, [startFrame, startFrame + 22], [0, 1], {
          extrapolateLeft: "clamp",
          extrapolateRight: "clamp",
          easing: Easing.out(Easing.cubic),
        });
        const dir = i === 0 ? -1 : 1;
        const offsetX = dir * width * 0.25;
        const targetX = cx + offsetX - fontSize * 0.9;
        const targetY = cy + iconH * 0.55;
        const startX = cx - fontSize * 0.9;
        const x = interpolate(progress, [0, 1], [startX, targetX]);
        const y = interpolate(progress, [0, 1], [cy, targetY]);
        const fade = interpolate(frame, [F_OUTPUT_IN + 8, F_OUTPUT_SET], [1, 0.25], {
          extrapolateLeft: "clamp", extrapolateRight: "clamp",
        });

        return (
          <div
            key={`fn-${i}`}
            style={{
              position: "absolute",
              left: x,
              top: y,
              fontFamily: i === 0 ? FONT : "Georgia, serif",
              fontWeight: i === 0 ? 900 : 400,
              fontSize,
              color: "#ffffff",
              letterSpacing: -1,
              textShadow: "0 4px 16px rgba(0,0,0,0.35)",
              opacity: progress * fade,
              zIndex: 6,
            }}
          >
            {sample}
          </div>
        );
      })}

      {/* STYLED OUTPUT — a constructed brand-identity poster using the
          extracted palette + name. We deliberately do NOT show outputSrc here:
          a real product screenshot mid-animation reads as "random screenshot
          replaced the swatches." Keeping this synthetic keeps the metaphor
          (palette + fonts → branded artifact) intact. */}
      <BrandIdentityCard
        palette={palette}
        brandName={brandName || "Brand"}
        opacity={outputOp}
        scale={outputScale}
        cx={cx}
        cy={cy}
        width={width}
        height={height}
      />
    </AbsoluteFill>
  );
};

const BrandIdentityCard: React.FC<{
  palette: string[];
  brandName: string;
  opacity: number;
  scale: number;
  cx: number;
  cy: number;
  width: number;
  height: number;
}> = ({ palette, brandName, opacity, scale, cx, cy, width, height }) => {
  const w = Math.floor(width * 0.7);
  const h = Math.floor(height * 0.5);
  const accent = palette[0];
  const accent2 = palette[1] || palette[0];
  const accent3 = palette[2] || palette[0];
  return (
    <div
      style={{
        position: "absolute",
        left: cx - w / 2,
        top: cy - h / 2,
        width: w,
        height: h,
        borderRadius: 32,
        overflow: "hidden",
        boxShadow: "0 50px 110px rgba(0,0,0,0.45), 0 16px 40px rgba(0,0,0,0.28)",
        transform: `scale(${scale})`,
        opacity,
        transformOrigin: "center center",
        zIndex: 8,
        background: "#ffffff",
        fontFamily: FONT,
        display: "flex",
        flexDirection: "column",
      }}
    >
      {/* Top accent stripe */}
      <div style={{ height: Math.floor(h * 0.16), background: accent, display: "flex", alignItems: "center", padding: "0 36px" }}>
        <div style={{ width: 18, height: 18, borderRadius: 6, background: "#fff", marginRight: 14 }} />
        <div style={{ color: "#fff", fontWeight: 800, fontSize: 28, letterSpacing: 2, textTransform: "uppercase" }}>
          {brandName}
        </div>
      </div>
      {/* Big serif headline */}
      <div style={{ padding: "32px 40px 8px 40px", fontFamily: "Georgia, serif", fontWeight: 700, fontSize: 78, color: "#1d1d1f", letterSpacing: -1.5, lineHeight: 1 }}>
        Made with<br />
        <span style={{ color: accent }}>Claude</span>.
      </div>
      {/* Subtitle */}
      <div style={{ padding: "0 40px", fontWeight: 500, fontSize: 26, color: "#666", marginTop: 10 }}>
        On-brand, every export.
      </div>
      {/* Palette swatches */}
      <div style={{ flex: 1 }} />
      <div style={{ display: "flex", padding: "0 40px 36px 40px", gap: 14 }}>
        {palette.slice(0, 4).map((c, i) => (
          <div key={i} style={{ flex: 1, height: 70, borderRadius: 14, background: c, boxShadow: "0 4px 12px rgba(0,0,0,0.10)" }} />
        ))}
      </div>
      {/* Bottom underscore accent */}
      <div style={{ height: 8, background: `linear-gradient(90deg, ${accent} 0%, ${accent2} 50%, ${accent3} 100%)` }} />
    </div>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: BrandFlow
// 6 props detected · 1 port(s)

Brand Flow

brand_flow

Brand Flow — Andy's hand-crafted Remotion scene. 6 props. captionText:TEXT.

Workflow Inputs (1)

  • Caption Text
    captionText
    TEXT

Config Fields (5)

  • brandName
  • outputSrc
  • fontSamples
  • accentPalette
  • backgroundColor

Meta

Updated
4/21/2026