Catalog

Edit Props (live)

IMAGE
TEXT
TEXT
TEXT
config
config
config
config
config
config

Source

298 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", "800", "900"] });

interface BranchNode {
  label: string;
  logo?: string;             // optional logo path
  color?: string;            // optional bg override
  tile_bg?: string;          // optional chip bg for monochrome logos
}

interface MindmapExpandProps {
  centralLabel: string;
  centralLogo?: string;
  centralColor?: string;       // default "#1a1a1a"
  centralTextColor?: string;   // default "#ffffff"
  branches: BranchNode[];      // 4-8 nodes recommended
  backgroundColor?: string;    // default "#f2eee4"
  lineColor?: string;          // default "#111111"
  accentColor?: string;        // default "#1d9bf0"
  captionText?: string;
  captionPosition?: "top" | "bottom";
}

const FONT = "'Inter', sans-serif";
const resolveStatic = (s: string) => (s.startsWith("http") ? s : staticFile(s));

export const MindmapExpand: React.FC<MindmapExpandProps> = ({
  centralLabel,
  centralLogo,
  centralColor = "#1a1a1a",
  centralTextColor = "#ffffff",
  branches,
  backgroundColor = "#f2eee4",
  lineColor = "#111111",
  accentColor = "#1d9bf0",
  captionText,
  captionPosition = "top",
}) => {
  const frame = useCurrentFrame();
  const { fps, width, height } = useVideoConfig();

  const centerX = width / 2;
  const centerY = height / 2;
  const centralRadius = 180;
  const branchRadius = 130;
  const orbitRadius = Math.min(width, height) * 0.38;

  // Central node enters first
  const centralEnter = spring({
    frame,
    fps,
    config: { mass: 1, damping: 12, stiffness: 120 },
  });
  const centralScale = interpolate(centralEnter, [0, 1], [0, 1]);

  // Subtle breathing on the central node
  const t = frame / fps;
  const pulse = 1 + Math.sin(t * 1.4) * 0.02;

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

  // Evenly distribute branches around central node
  const branchCount = branches.length;
  const branchData = branches.map((b, i) => {
    // Start from top, go clockwise
    const angle = (i / branchCount) * Math.PI * 2 - Math.PI / 2;
    return {
      branch: b,
      x: centerX + Math.cos(angle) * orbitRadius,
      y: centerY + Math.sin(angle) * orbitRadius,
      angle,
    };
  });

  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" ? 140 : undefined,
            bottom: captionPosition === "bottom" ? 140 : undefined,
            textAlign: "center",
            fontFamily: FONT,
            fontSize: 56,
            fontWeight: 600,
            fontStyle: "italic",
            color: "#111",
            letterSpacing: -1,
            opacity: capOp,
            transform: `translateY(${capY}px)`,
            padding: "0 60px",
            zIndex: 10,
          }}
        >
          {captionText}
        </div>
      )}

      {/* Connector lines — animated draw-on */}
      <svg
        style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
        width={width}
        height={height}
      >
        {branchData.map((bd, i) => {
          const lineDelay = 8 + i * 3;
          const lineT = interpolate(frame, [lineDelay, lineDelay + 14], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
            easing: Easing.out(Easing.cubic),
          });
          // Line goes from central edge to branch edge
          const cx1 = centerX + Math.cos(bd.angle) * centralRadius;
          const cy1 = centerY + Math.sin(bd.angle) * centralRadius;
          const cx2 = bd.x - Math.cos(bd.angle) * branchRadius;
          const cy2 = bd.y - Math.sin(bd.angle) * branchRadius;
          const endX = cx1 + (cx2 - cx1) * lineT;
          const endY = cy1 + (cy2 - cy1) * lineT;
          return (
            <line
              key={i}
              x1={cx1}
              y1={cy1}
              x2={endX}
              y2={endY}
              stroke={lineColor}
              strokeWidth={5}
              strokeLinecap="round"
              strokeDasharray="2 8"
              opacity={0.65}
            />
          );
        })}
      </svg>

      {/* Branch nodes — spring in after line reaches */}
      {branchData.map((bd, i) => {
        const nodeStart = 16 + i * 3;
        const nodeEnter = spring({
          frame: frame - nodeStart,
          fps,
          config: { mass: 1, damping: 13, stiffness: 140 },
        });
        const nodeScale = interpolate(nodeEnter, [0, 1], [0, 1]);
        const nodeOp = interpolate(nodeEnter, [0, 0.3, 1], [0, 1, 1]);
        const branchFloat = Math.sin(t * 1.2 + i * 0.7) * 4;
        return (
          <Node
            key={i}
            x={bd.x}
            y={bd.y + branchFloat}
            scale={nodeScale}
            opacity={nodeOp}
            radius={branchRadius}
            label={bd.branch.label}
            logo={bd.branch.logo}
            bg={bd.branch.color || "#ffffff"}
            tile_bg={bd.branch.tile_bg}
            textColor={"#111"}
            shadow
          />
        );
      })}

      {/* Central node */}
      <Node
        x={centerX}
        y={centerY}
        scale={centralScale * pulse}
        opacity={1}
        radius={centralRadius}
        label={centralLabel}
        logo={centralLogo}
        bg={centralColor}
        textColor={centralTextColor}
        big
        shadow
      />
    </AbsoluteFill>
  );
};

const Node: React.FC<{
  x: number;
  y: number;
  scale: number;
  opacity: number;
  radius: number;
  label: string;
  logo?: string;
  bg: string;
  textColor: string;
  tile_bg?: string;
  big?: boolean;
  shadow?: boolean;
}> = ({ x, y, scale, opacity, radius, label, logo, bg, textColor, tile_bg, big, shadow }) => {
  const size = radius * 2;
  const fontSize = big ? Math.floor(radius * 0.32) : Math.floor(radius * 0.24);
  const logoSize = Math.floor(radius * 0.8);
  return (
    <div
      style={{
        position: "absolute",
        left: x - radius,
        top: y - radius,
        width: size,
        height: size,
        borderRadius: "50%",
        background: bg,
        color: textColor,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        gap: logo && label ? 6 : 0,
        transform: `scale(${scale})`,
        opacity,
        boxShadow: shadow
          ? "0 24px 44px rgba(0,0,0,0.18), 0 8px 16px rgba(0,0,0,0.12)"
          : "none",
        fontFamily: FONT,
        padding: 14,
        textAlign: "center",
        overflow: "hidden",
      }}
    >
      {logo &&
        (tile_bg ? (
          <div
            style={{
              width: logoSize,
              height: logoSize,
              borderRadius: Math.floor(logoSize * 0.22),
              background: tile_bg,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              overflow: "hidden",
            }}
          >
            <Img
              src={resolveStatic(logo)}
              style={{ width: "70%", height: "70%", objectFit: "contain" }}
            />
          </div>
        ) : (
          <Img
            src={resolveStatic(logo)}
            style={{ width: logoSize, height: logoSize, objectFit: "contain" }}
          />
        ))}
      {label && (
        <div
          style={{
            fontSize,
            fontWeight: big ? 900 : 800,
            letterSpacing: -1,
            lineHeight: 1.05,
          }}
        >
          {label}
        </div>
      )}
    </div>
  );
};

Schema (Zod)

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

Mindmap Expand

mindmap_expand

Mindmap Expand — Andy's hand-crafted Remotion scene. 10 props. centralLogo:IMAGE, centralTextColor:TEXT, captionText:TEXT, +1 more.

Workflow Inputs (4)

  • Central Logo
    centralLogo
    IMAGE
  • Central Text Color
    centralTextColor
    TEXT
  • Caption Text
    captionText
    TEXT
  • Caption Position
    captionPosition
    TEXT

Config Fields (6)

  • branches
  • lineColor
  • accentColor
  • centralColor
  • centralLabel
  • backgroundColor

Meta

Updated
4/21/2026