Catalog

Edit Props (live)

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

Source

212 lines
import React from "react";
import {
  AbsoluteFill,
  Img,
  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 LogoSpotlightProps {
  logo: string;                    // file path or URL
  wordmark?: string;               // optional text label
  wordmarkLayout?: "below" | "side" | "none";  // default "below"
  tile_bg?: string;                // dark chip for monochrome logos
  backgroundColor?: string;        // scene bg, default "#f2eee4"
  accentColor?: string;            // glow/accent behind tile, default "#ffffff"
  iconScale?: number;              // 0-1 scale of viewport width. Defaults: 0.36 (below), 0.26 (side)
  positionY?: number;              // 0-1 vertical anchor. Default 0.45 (slightly higher than center).
  captionText?: string;
  captionStyle?: "italic" | "bold";
  captionPosition?: "top" | "bottom";
  fontColor?: string;              // wordmark + caption color, default "#111"
}

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

export const LogoSpotlight: React.FC<LogoSpotlightProps> = ({
  logo,
  wordmark,
  wordmarkLayout = "below",
  tile_bg,
  backgroundColor = "#f2eee4",
  accentColor = "#ffffff",
  iconScale,
  positionY = 0.45,
  captionText,
  captionStyle = "italic",
  captionPosition = "top",
  fontColor = "#111",
}) => {
  const frame = useCurrentFrame();
  const { fps, width, height } = useVideoConfig();

  // Layout-aware default sizes — below gets a bigger icon, side has to share
  // horizontal space with the wordmark so the icon must be smaller.
  const effectiveScale = iconScale ?? (wordmarkLayout === "side" ? 0.26 : 0.36);
  const iconSize = Math.floor(width * effectiveScale);
  const resolved = logo.startsWith("http") ? logo : staticFile(logo);

  // Spring in with scale + slight overshoot
  const enter = spring({
    frame: frame - 4,
    fps,
    config: { mass: 1, damping: 11, stiffness: 120 },
  });
  const scale = interpolate(enter, [0, 1], [0.3, 1]);
  const opacity = interpolate(frame, [0, 14], [0, 1], { extrapolateRight: "clamp" });

  // Subtle float + rotation after entry for life (kept gentle so icon never
  // bleeds off the sides)
  const t = frame / fps;
  const floatY = Math.sin(t * 1.3) * 6;
  const rotate = Math.sin(t * 0.9) * 0.6;

  // Wordmark enters a beat after the icon
  const wmEnter = spring({
    frame: frame - 16,
    fps,
    config: { mass: 1, damping: 14, stiffness: 140 },
  });
  const wmY = interpolate(wmEnter, [0, 1], [40, 0]);
  const wmOp = interpolate(wmEnter, [0, 0.4, 1], [0, 1, 1]);

  // Caption enters almost immediately
  const capOp = interpolate(frame, [2, 16], [0, 1], { extrapolateRight: "clamp" });
  const capY = interpolate(frame, [2, 16], [10, 0], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Wordmark font scales with icon. In side layout it has to share horizontal
  // space so it can't be too big — set conservatively so a 6-char word fits.
  const wordmarkFontSize = wordmarkLayout === "side"
    ? Math.floor(iconSize * 0.58)
    : Math.floor(iconSize * 0.34);

  const tileRadius = Math.floor(iconSize * 0.22);

  const iconNode = tile_bg ? (
    <div
      style={{
        width: iconSize,
        height: iconSize,
        borderRadius: tileRadius,
        background: tile_bg,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        overflow: "hidden",
        boxShadow: "0 30px 60px rgba(0,0,0,0.25), 0 10px 20px rgba(0,0,0,0.15)",
      }}
    >
      <Img src={resolved} style={{ width: "68%", height: "68%", objectFit: "contain" }} />
    </div>
  ) : (
    <Img
      src={resolved}
      style={{
        width: iconSize,
        height: iconSize,
        objectFit: "contain",
        filter: "drop-shadow(0 22px 40px rgba(0,0,0,0.22))",
      }}
    />
  );

  return (
    <AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
      {/* Soft accent glow behind the logo */}
      <div
        style={{
          position: "absolute",
          top: `${positionY * 100}%`,
          left: "50%",
          width: iconSize * 1.8,
          height: iconSize * 1.8,
          transform: "translate(-50%, -50%)",
          background: `radial-gradient(circle, ${accentColor}55 0%, transparent 60%)`,
          filter: "blur(50px)",
          opacity,
          pointerEvents: "none",
        }}
      />

      {/* Caption */}
      {captionText && (
        <div
          style={{
            position: "absolute",
            left: 0,
            right: 0,
            top: captionPosition === "top" ? 180 : undefined,
            bottom: captionPosition === "bottom" ? 200 : undefined,
            textAlign: "center",
            fontFamily: FONT,
            fontStyle: captionStyle === "italic" ? "italic" : "normal",
            fontWeight: captionStyle === "italic" ? 500 : 800,
            fontSize: 64,
            color: fontColor,
            letterSpacing: -1.2,
            padding: "0 50px",
            opacity: capOp,
            transform: `translateY(${capY}px)`,
            zIndex: 5,
          }}
        >
          {captionText}
        </div>
      )}

      {/* Logo + wordmark group — anchored at positionY, centered horizontally,
          bounded so nothing bleeds off the viewport edges. */}
      <div
        style={{
          position: "absolute",
          top: `${positionY * 100}%`,
          left: "50%",
          transform: "translate(-50%, -50%)",
          maxWidth: "92%",
          display: "flex",
          flexDirection: wordmarkLayout === "side" ? "row" : "column",
          alignItems: "center",
          justifyContent: "center",
          gap: wordmarkLayout === "side" ? Math.floor(iconSize * 0.18) : Math.floor(iconSize * 0.1),
        }}
      >
        <div
          style={{
            transform: `translateY(${floatY}px) rotate(${rotate}deg) scale(${scale})`,
            opacity,
          }}
        >
          {iconNode}
        </div>

        {wordmark && wordmarkLayout !== "none" && (
          <div
            style={{
              fontFamily: FONT,
              fontSize: wordmarkFontSize,
              fontWeight: 800,
              color: fontColor,
              letterSpacing: -2.5,
              lineHeight: 1,
              opacity: wmOp,
              transform: `translateY(${wmY}px)`,
            }}
          >
            {wordmark}
          </div>
        )}
      </div>
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: LogoSpotlight
// 12 props detected · 5 port(s)

Logo Spotlight

logo_spotlight

Logo Spotlight — Andy's hand-crafted Remotion scene. 12 props. logo:IMAGE, iconScale:IMAGE, captionText:TEXT, +2 more.

Workflow Inputs (5)

  • Logo
    logo
    IMAGERequired
  • Icon Scale
    iconScale
    IMAGE
  • Caption Text
    captionText
    TEXT
  • Caption Style
    captionStyle
    TEXT
  • Caption Position
    captionPosition
    TEXT

Config Fields (7)

  • tile_bg
  • wordmark
  • fontColor
  • positionY
  • accentColor
  • wordmarkLayout
  • backgroundColor

Meta

Updated
4/21/2026