← Catalog
Edit Props (live)
IMAGE
TEXT
TEXT
TEXT
config
config
config
config
Source
199 linesimport 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_strikeLogo Grid Strike — Andy's hand-crafted Remotion scene. 8 props. logos:IMAGE, captionText:TEXT, captionStyle:TEXT, +1 more.
Workflow Inputs (4)
- Logos
logosIMAGERequired - Caption Text
captionTextTEXT - Caption Style
captionStyleTEXT - Caption Position
captionPositionTEXT
Config Fields (4)
rowsstrikeColorstrikeStartAtbackgroundColor
Meta
- Updated
- 4/21/2026