Catalog

Edit Props (live)

IMAGE
TEXT
TEXT
TEXT
config
config
config
config

Source

199 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", "700", "900"] });

interface LogoGridStrikeProps {
  // Array of logo file paths (e.g. ["logos/chatgpt.png", "logos/claude.png", ...])
  // Will be split into rows (default 2 rows).
  logos: string[];
  rows?: number;              // default 2
  strikeStartAt?: number;     // 0..1 normalized scene progress when strike begins (default 0.55)
  strikeColor?: string;       // default "#ff3b30"
  backgroundColor?: string;   // default "#f2eee4"
  captionText?: string;
  captionStyle?: "italic" | "bold";
  captionPosition?: "top" | "bottom";
}

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

export const LogoGridStrike: React.FC<LogoGridStrikeProps> = ({
  logos,
  rows = 2,
  strikeStartAt = 0.55,
  strikeColor = "#ff3b30",
  backgroundColor = "#f2eee4",
  captionText,
  captionStyle = "italic",
  captionPosition = "top",
}) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames, width, height } = useVideoConfig();

  const per_row = Math.ceil(logos.length / rows);
  const rowArr: string[][] = [];
  for (let r = 0; r < rows; r++) {
    rowArr.push(logos.slice(r * per_row, (r + 1) * per_row));
  }

  // Row slide-ins: row 0 from left, row 1 from right (alternate)
  const rowEnter = (r: number) =>
    spring({
      frame: Math.max(0, frame - r * 6),
      fps,
      config: { mass: 1, damping: 14, stiffness: 120 },
    });

  // Strike progress
  const strikeStartFrame = Math.floor(durationInFrames * strikeStartAt);
  const strikeProgress = interpolate(
    frame,
    [strikeStartFrame, strikeStartFrame + Math.floor(fps * 0.55)],
    [0, 1],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic) }
  );

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

  // Layout math — grid is centered vertically
  const gridGap = 26;
  const tileSize = Math.min(180, Math.floor((width * 0.8 - gridGap * (per_row - 1)) / per_row));
  const rowHeight = tileSize + gridGap;
  const gridHeight = rowHeight * rows - gridGap;
  const gridTop = (height - gridHeight) / 2;
  const rowWidth = tileSize * per_row + gridGap * (per_row - 1);
  const gridLeft = (width - rowWidth) / 2;

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

      {/* Caption */}
      {captionText && (
        <div
          style={{
            position: "absolute",
            left: 0,
            right: 0,
            top: captionPosition === "top" ? gridTop - 150 : undefined,
            bottom: captionPosition === "bottom" ? height - gridTop - gridHeight - 140 : undefined,
            textAlign: "center",
            fontFamily: FONT,
            fontStyle: captionStyle === "italic" ? "italic" : "normal",
            fontWeight: captionStyle === "italic" ? 500 : 800,
            fontSize: 64,
            color: "#111",
            letterSpacing: -1.2,
            opacity: capOp,
            transform: `translateY(${capY}px)`,
            textShadow: "0 2px 6px rgba(0,0,0,0.08)",
            padding: "0 40px",
            zIndex: 5,
          }}
        >
          {captionText}
        </div>
      )}

      {/* Logo grid */}
      {rowArr.map((rowLogos, r) => {
        const enter = rowEnter(r);
        const fromLeft = r % 2 === 0;
        const startX = fromLeft ? -width : width;
        const translateX = interpolate(enter, [0, 1], [startX, 0]);
        const opacity = interpolate(enter, [0, 0.2, 1], [0, 1, 1]);
        return (
          <div
            key={r}
            style={{
              position: "absolute",
              top: gridTop + r * rowHeight,
              left: gridLeft,
              display: "flex",
              gap: gridGap,
              transform: `translateX(${translateX}px)`,
              opacity,
            }}
          >
            {rowLogos.map((src, i) => (
              <LogoTile key={`${r}-${i}`} src={src} size={tileSize} />
            ))}
          </div>
        );
      })}

      {/* Red strikethrough — one per row, slightly tilted for energy */}
      {rowArr.map((_, r) => {
        const y = gridTop + r * rowHeight + tileSize / 2;
        const lineLength = rowWidth + 160; // overshoot past both sides
        const startProgress = Math.max(0, (strikeProgress - r * 0.08) / (1 - r * 0.08));
        const scaleX = Math.min(1, Math.max(0, startProgress));
        return (
          <div
            key={`strike-${r}`}
            style={{
              position: "absolute",
              top: y - 5,
              left: gridLeft - 80,
              width: lineLength,
              height: 10,
              background: strikeColor,
              borderRadius: 6,
              transform: `rotate(${r === 0 ? -2.5 : 2}deg) scaleX(${scaleX})`,
              transformOrigin: "left center",
              boxShadow: `0 0 16px ${strikeColor}aa, 0 6px 16px rgba(0,0,0,0.18)`,
              zIndex: 10,
            }}
          />
        );
      })}
    </AbsoluteFill>
  );
};

const LogoTile: React.FC<{ src: string; size: number }> = ({ src, size }) => {
  const resolved = src.startsWith("http") ? src : staticFile(src);
  return (
    <div
      style={{
        width: size,
        height: size,
        borderRadius: TILE_RADIUS,
        background: "#0a0a0a",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        overflow: "hidden",
        boxShadow: "0 14px 28px rgba(0,0,0,0.22), 0 4px 10px rgba(0,0,0,0.12)",
      }}
    >
      <Img src={resolved} style={{ width: "70%", height: "70%", objectFit: "contain" }} />
    </div>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: LogoGridStrike
// 8 props detected · 4 port(s)

Logo Grid Strike

logo_grid_strike

Logo Grid Strike — Andy's hand-crafted Remotion scene. 8 props. logos:IMAGE, captionText:TEXT, captionStyle:TEXT, +1 more.

Workflow Inputs (4)

  • Logos
    logos
    IMAGERequired
  • Caption Text
    captionText
    TEXT
  • Caption Style
    captionStyle
    TEXT
  • Caption Position
    captionPosition
    TEXT

Config Fields (4)

  • rows
  • strikeColor
  • strikeStartAt
  • backgroundColor

Meta

Updated
4/21/2026