Catalog

Edit Props (live)

TEXT
TEXT
TEXT
config
config
config
config
config
config

Source

217 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 LogoRow {
  // File path of logo image (e.g. "logos/openai.png")
  logo: string;
  // Wordmark text displayed next to the logo.
  wordmark: string;
  // Optional backdrop color behind the logo — needed for monochrome or
  // transparent logos that would disappear on the scene's background.
  // e.g. "#1a1a1a" for a dark chip behind a white SVG.
  tile_bg?: string;
}

interface LogoListRevealProps {
  rows: LogoRow[];
  staggerS?: number;              // seconds between each row appearing (default 0.35)
  backgroundColor?: string;       // default "#f2eee4"
  captionText?: string;
  captionStyle?: "italic" | "bold";
  captionPosition?: "top" | "bottom";
  fontColor?: string;             // wordmark color, default "#111"
  highlightLast?: boolean;        // if true, last row gets a yellow highlighter (default false)
  highlightColor?: string;        // default "#ffe94a"
}

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

export const LogoListReveal: React.FC<LogoListRevealProps> = ({
  rows,
  staggerS = 0.35,
  backgroundColor = "#f2eee4",
  captionText,
  captionStyle = "italic",
  captionPosition = "bottom",
  fontColor = "#111",
  highlightLast = false,
  highlightColor = "#ffe94a",
}) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames, width, height } = useVideoConfig();

  const staggerFrames = Math.max(1, Math.floor(staggerS * fps));
  const rowCount = rows.length;

  // Font sizing: shrink as row count grows
  const wordmarkFontSize = rowCount <= 3 ? 130 : rowCount <= 4 ? 110 : 92;
  const iconSize = Math.floor(wordmarkFontSize * 1.05);
  const gap = Math.floor(wordmarkFontSize * 0.32);
  const listHeight = rowCount * iconSize + (rowCount - 1) * gap;
  const listTop = Math.max(260, Math.floor((height - listHeight) / 2) - 60);

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

  return (
    <AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
      {/* Soft radial wash */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: "transparent",
          pointerEvents: "none",
        }}
      />

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

      {/* Logo list */}
      <div
        style={{
          position: "absolute",
          top: listTop,
          left: 0,
          right: 0,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap,
        }}
      >
        {rows.map((row, i) => {
          const startFrame = i * staggerFrames + 4;
          const enter = spring({
            frame: frame - startFrame,
            fps,
            config: { mass: 1, damping: 15, stiffness: 130 },
          });
          const translateY = interpolate(enter, [0, 1], [80, 0]);
          const opacity = interpolate(enter, [0, 0.25, 1], [0, 1, 1]);
          const text = row.wordmark;
          const resolvedSrc = row.logo.startsWith("http") ? row.logo : staticFile(row.logo);
          const isLast = i === rows.length - 1;
          const showHighlight = highlightLast && isLast;

          return (
            <div
              key={i}
              style={{
                display: "flex",
                alignItems: "center",
                gap: Math.floor(wordmarkFontSize * 0.3),
                transform: `translateY(${translateY}px)`,
                opacity,
              }}
            >
              {row.tile_bg ? (
                <div
                  style={{
                    width: iconSize,
                    height: iconSize,
                    borderRadius: Math.floor(iconSize * 0.24),
                    background: row.tile_bg,
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    overflow: "hidden",
                    flexShrink: 0,
                    boxShadow: "0 6px 14px rgba(0,0,0,0.16)",
                  }}
                >
                  <Img
                    src={resolvedSrc}
                    style={{
                      width: "68%",
                      height: "68%",
                      objectFit: "contain",
                    }}
                  />
                </div>
              ) : (
                <Img
                  src={resolvedSrc}
                  style={{
                    width: iconSize,
                    height: iconSize,
                    objectFit: "contain",
                    flexShrink: 0,
                  }}
                />
              )}
              <div
                style={{
                  position: "relative",
                  fontFamily: FONT,
                  fontSize: wordmarkFontSize,
                  fontWeight: 800,
                  color: fontColor,
                  letterSpacing: -2.5,
                  lineHeight: 1,
                  padding: showHighlight ? "4px 18px" : 0,
                }}
              >
                {showHighlight && (
                  <div
                    style={{
                      position: "absolute",
                      inset: 0,
                      background: highlightColor,
                      borderRadius: 8,
                      transform: "skewX(-4deg)",
                      zIndex: -1,
                      opacity: 0.85,
                    }}
                  />
                )}
                {text}
              </div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};

Schema (Zod)

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

Logo List Reveal

logo_list_reveal

Logo List Reveal — Andy's hand-crafted Remotion scene. 9 props. captionText:TEXT, captionStyle:TEXT, captionPosition:TEXT.

Workflow Inputs (3)

  • Caption Text
    captionText
    TEXT
  • Caption Style
    captionStyle
    TEXT
  • Caption Position
    captionPosition
    TEXT

Config Fields (6)

  • rows
  • staggerS
  • fontColor
  • highlightLast
  • highlightColor
  • backgroundColor

Meta

Updated
4/21/2026