Remotion LabRemotion Lab
返回模板庫

六工具展示:Cloudflare、GitHub、Lemon Squeezy、Resend、Tavily、Gemini

30 秒六工具 Grid 展示:3×2 卡片排列,依序放大聚焦 Cloudflare(#1)、GitHub(#2)、Lemon Squeezy(#3)、Resend(#4)、Tavily(#5)、Gemini(#6),每張卡片縮放進場後放大至全屏顯示工具名稱,再縮回 Grid。

工具選擇CloudflareGitHubResendTavilyGemini
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
} from "remotion";

export const TOOL-SHOWCASE-DURATION-FRAMES = 900;

const TOOLS = [
  { name: "Cloudflare", number: "1", color: "#FF6D2E" },
  { name: "GitHub", number: "2", color: "#8B5CF6" },
  { name: "Lemon Squeezy", number: "3", color: "#7C3AED" },
  { name: "Resend", number: "4", color: "#00D4AA" },
  { name: "Tavily", number: "5", color: "#6366F1" },
  { name: "Gemini", number: "6", color: "#4285F4" },
];

const COLS = 3;
const GAP = 12;
const CELL-W = (1920 - GAP * (COLS + 1)) / COLS;
const CELL-H = 380;
const GRID-W = COLS * CELL-W + (COLS - 1) * GAP;
const GRID-H = 2 * CELL-H + GAP;
const GRID-LEFT = (1920 - GRID-W) / 2;
const GRID-TOP = (1080 - GRID-H) / 2;

const getCellRect = (index: number) => {
  const col = index % COLS;
  const row = Math.floor(index / COLS);
  return {
    x: GRID-LEFT + col * (CELL-W + GAP),
    y: GRID-TOP + row * (CELL-H + GAP),
    w: CELL-W,
    h: CELL-H,
  };
};

const GRID-INTRO = 40;
const TOOL-DURATION = 130;
const ZOOM-IN = 25;
const HOLD = 75;
const ZOOM-OUT = 30;

export const Scene129-ToolShowcase: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const globalFadeIn = interpolate(frame, [0, 10], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const globalFadeOut = interpolate(frame, [870, 900], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const masterOpacity = frame < 870 ? globalFadeIn : globalFadeOut;

  const gridSpring = spring({ frame, fps, config: { damping: 14, mass: 0.6, stiffness: 120 } });
  const gridScale = interpolate(gridSpring, [0, 1], [0.88, 1]);
  const gridOpacity = interpolate(gridSpring, [0, 0.4], [0, 1], { extrapolateRight: "clamp" });

  let activeIndex = -1;
  let zoomProgress = 0;
  for (let i = 0; i < TOOLS.length; i++) {
    const toolStart = GRID-INTRO + i * TOOL-DURATION;
    const toolEnd = toolStart + TOOL-DURATION;
    if (frame >= toolStart && frame < toolEnd) {
      activeIndex = i;
      const localFrame = frame - toolStart;
      if (localFrame < ZOOM-IN) {
        zoomProgress = interpolate(localFrame, [0, ZOOM-IN], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
      } else if (localFrame < ZOOM-IN + HOLD) {
        zoomProgress = 1;
      } else {
        zoomProgress = interpolate(localFrame, [ZOOM-IN + HOLD, ZOOM-IN + HOLD + ZOOM-OUT], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
      }
      break;
    }
  }

  let labelOpacity = 0;
  let labelScale = 0;
  if (activeIndex >= 0) {
    const toolStart = GRID-INTRO + activeIndex * TOOL-DURATION;
    const localFrame = frame - toolStart;
    const labelSpring = spring({ frame: Math.max(0, localFrame - ZOOM-IN - 5), fps, config: { damping: 10, mass: 0.4, stiffness: 200 } });
    labelScale = interpolate(labelSpring, [0, 1], [0.3, 1]);
    labelOpacity = interpolate(labelSpring, [0, 0.35], [0, 1], { extrapolateRight: "clamp" });
    if (localFrame >= ZOOM-IN + HOLD) {
      labelOpacity *= interpolate(localFrame, [ZOOM-IN + HOLD, ZOOM-IN + HOLD + 15], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
    }
  }

  return (
    <AbsoluteFill style={{ background: "#0A0E14", opacity: masterOpacity }}>
      {/* Grid */}
      <div
        style={{
          position: "absolute", top: 0, left: 0, width: 1920, height: 1080,
          transform: `scale(${gridScale})`, transformOrigin: "center center", opacity: gridOpacity,
        }}
      >
        {TOOLS.map((tool, i) => {
          const cell = getCellRect(i);
          const cellDelay = 5 + i * 5;
          const cellSpring = spring({ frame: Math.max(0, frame - cellDelay), fps, config: { damping: 12, mass: 0.4, stiffness: 180 } });
          const cellScale = interpolate(cellSpring, [0, 1], [0.8, 1]);
          const cellOpacity = interpolate(cellSpring, [0, 0.3], [0, 1], { extrapolateRight: "clamp" });
          const isActive = i === activeIndex;
          const dimAmount = !isActive && zoomProgress > 0 ? interpolate(zoomProgress, [0, 1], [1, 0.15]) : 1;

          return (
            <div
              key={i}
              style={{
                position: "absolute", left: cell.x, top: cell.y, width: cell.w, height: cell.h,
                borderRadius: 12, overflow: "hidden", transform: `scale(${cellScale})`,
                opacity: cellOpacity * dimAmount, border: "1px solid rgba(255,255,255,0.1)",
                boxSizing: "border-box", background: `${tool.color}08`,
                display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 16,
              }}
            >
              <div style={{ fontSize: 80, fontWeight: 900, color: tool.color, fontFamily: "'Inter', sans-serif" }}>
                {tool.number}
              </div>
              <div style={{ fontSize: 48, fontWeight: 800, color: "#FFFFFF", fontFamily: "'Inter', sans-serif" }}>
                {tool.name}
              </div>
              <div
                style={{
                  position: "absolute", top: 10, left: 10, width: 36, height: 36, borderRadius: "50%",
                  background: tool.color, display: "flex", alignItems: "center", justifyContent: "center",
                  fontSize: 20, fontWeight: 800, color: "#FFFFFF",
                  opacity: zoomProgress > 0.3 && isActive ? 0 : 0.9,
                }}
              >
                {tool.number}
              </div>
            </div>
          );
        })}
      </div>

      {/* Zoomed overlay */}
      {activeIndex >= 0 && zoomProgress > 0 && (() => {
        const cell = getCellRect(activeIndex);
        const x = interpolate(zoomProgress, [0, 1], [cell.x, 0]);
        const y = interpolate(zoomProgress, [0, 1], [cell.y, 0]);
        const w = interpolate(zoomProgress, [0, 1], [cell.w, 1920]);
        const h = interpolate(zoomProgress, [0, 1], [cell.h, 1080]);
        const borderRadius = interpolate(zoomProgress, [0, 1], [12, 0]);
        const tool = TOOLS[activeIndex];
        return (
          <div style={{ position: "absolute", left: x, top: y, width: w, height: h, borderRadius, overflow: "hidden", zIndex: 10, background: `${tool.color}08`, display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 24 }}>
            <div style={{ fontSize: 160, fontWeight: 900, color: tool.color, fontFamily: "'Inter', sans-serif" }}>{tool.number}</div>
            <div style={{ fontSize: 96, fontWeight: 800, color: "#FFFFFF", fontFamily: "'Inter', sans-serif" }}>{tool.name}</div>
          </div>
        );
      })()}

      {/* Label overlay */}
      {activeIndex >= 0 && labelOpacity > 0 && (
        <div
          style={{
            position: "absolute", bottom: 50, left: "50%",
            transform: `translateX(-50%) scale(${labelScale})`, opacity: labelOpacity,
            display: "flex", alignItems: "center", gap: 24, background: "rgba(0,0,0,0.8)",
            backdropFilter: "blur(12px)", borderRadius: 24, padding: "20px 44px",
            border: `2px solid ${TOOLS[activeIndex].color}50`,
            zIndex: 20,
          }}
        >
          <span style={{ fontFamily: "'Inter', sans-serif", fontSize: 72, fontWeight: 800, color: TOOLS[activeIndex].color }}>
            {TOOLS[activeIndex].number}.
          </span>
          <span style={{ fontFamily: "'Inter', sans-serif", fontSize: 78, fontWeight: 700, color: "#FFFFFF" }}>
            {TOOLS[activeIndex].name}
          </span>
        </div>
      )}
    </AbsoluteFill>
  );
};

登入後查看完整程式碼