Remotion LabRemotion Lab
返回模板庫

三款遊戲機制大融合

用卡片動畫展示將小朋友上樓梯、派對遊戲、瑪利歐賽車三種機制合成全新遊戲的創意過程,以加法公式呈現。

遊戲設計機制融合創意卡片動畫
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
} from "remotion";

const colors = {
  background: "#0A0E14",
  backgroundGradient: "linear-gradient(135deg, #0A0E14 0%, #131A24 100%)",
  text: "#FFFFFF",
  accent: "#00D4AA",
  accentSecondary: "#4DA3FF",
  warning: "#FFB547",
  danger: "#FF6B6B",
  dimmed: "rgba(255, 255, 255, 0.6)",
  cardBg: "rgba(255, 255, 255, 0.05)",
};

const fonts = { main: "'Inter', 'Noto Sans TC', sans-serif" };

const GAMES = [
  { label: "小朋友上樓梯", subLabel: "往上跳躍", color: "#2ECC71" },
  { label: "派對遊戲", subLabel: "多人歡樂", color: colors.warning },
  { label: "瑪利歐賽車", subLabel: "道具機制", color: "#FF6B6B" },
];

const StairsIcon: React.FC<{ progress: number; frame: number }> = ({ progress, frame }) => {
  const drawP = interpolate(progress, [0, 1], [0, 1]);
  const bounce = Math.sin(frame * 0.1) * 2;
  return (
    <g>
      {[0, 1, 2, 3, 4].map((i) => {
        const sx = -30 + i * 14;
        const sy = 20 - i * 12;
        const stepOpacity = interpolate(drawP, [i * 0.15, i * 0.15 + 0.2], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
        return <rect key={i} x={sx} y={sy} width="16" height="8" rx="2" fill="#2ECC71" opacity={stepOpacity * 0.7} />;
      })}
      {drawP > 0.5 && (
        <g transform={`translate(${-30 + 14 * Math.min(4, Math.floor(drawP * 5))}, ${20 - 12 * Math.min(4, Math.floor(drawP * 5)) - 14 + bounce})`}>
          <circle cx="0" cy="-6" r="5" fill="#2ECC71" />
          <rect x="-4" y="-1" width="8" height="10" rx="2" fill="#2ECC71" opacity={0.8} />
        </g>
      )}
    </g>
  );
};

const PartyIcon: React.FC<{ progress: number; frame: number }> = ({ progress, frame }) => {
  const drawP = interpolate(progress, [0, 1], [0, 1]);
  const confettiPhase = frame * 0.08;
  return (
    <g>
      <path d="M0,-25 L-16,10 L16,10Z" fill={`${colors.warning}40`} stroke={colors.warning} strokeWidth="2" strokeLinejoin="round" opacity={drawP} />
      <circle cx="0" cy="-25" r="4" fill={colors.warning} opacity={drawP} />
      <line x1="-8" y1="-5" x2="8" y2="-5" stroke={colors.warning} strokeWidth="1.5" opacity={drawP * 0.5} />
      <line x1="-12" y1="3" x2="12" y2="3" stroke={colors.warning} strokeWidth="1.5" opacity={drawP * 0.5} />
      {drawP > 0.6 && [
        { x: -20, y: -10, c: "#FF6B6B" },
        { x: 18, y: -15, c: colors.accent },
        { x: -12, y: 15, c: colors.accentSecondary },
        { x: 22, y: 8, c: colors.warning },
        { x: 5, y: -30, c: "#9B59B6" },
      ].map((conf, i) => (
        <rect key={i} x={conf.x + Math.sin(confettiPhase + i * 2) * 3} y={conf.y + Math.cos(confettiPhase + i * 1.5) * 3} width="4" height="4" rx="1" fill={conf.c} opacity={0.7} transform={`rotate(${frame * 2 + i * 45}, ${conf.x}, ${conf.y})`} />
      ))}
    </g>
  );
};

const KartIcon: React.FC<{ progress: number; frame: number }> = ({ progress, frame }) => {
  const drawP = interpolate(progress, [0, 1], [0, 1]);
  const wheelSpin = frame * 5;
  return (
    <g>
      <rect x="-20" y="-8" width="40" height="16" rx="4" fill="#FF6B6B" opacity={drawP * 0.8} />
      <rect x="-14" y="-16" width="20" height="10" rx="3" fill="#FF6B6B" opacity={drawP * 0.6} />
      <g transform={`rotate(${wheelSpin}, -14, 12)`}>
        <circle cx="-14" cy="12" r="6" fill="#333" stroke="#555" strokeWidth="1.5" opacity={drawP} />
      </g>
      <g transform={`rotate(${wheelSpin}, 14, 12)`}>
        <circle cx="14" cy="12" r="6" fill="#333" stroke="#555" strokeWidth="1.5" opacity={drawP} />
      </g>
      {drawP > 0.5 && (
        <g transform="translate(24, -14)">
          <rect x="-6" y="-6" width="12" height="12" rx="2" fill={`${colors.warning}60`} stroke={colors.warning} strokeWidth="1.5" />
          <text x="0" y="4" textAnchor="middle" fontSize="15" fontWeight="800" fill={colors.warning}>?</text>
        </g>
      )}
    </g>
  );
};

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

  const gameSprings = GAMES.map((_, i) =>
    spring({ frame: Math.max(0, frame - (10 + i * 22)), fps, config: { damping: 10, mass: 0.6, stiffness: 120 } })
  );
  const iconDrawProgress = GAMES.map((_, i) =>
    interpolate(frame, [20 + i * 22, 60 + i * 22], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })
  );
  const plus1 = interpolate(frame, [45, 60], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const plus2 = interpolate(frame, [67, 82], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const equalsProgress = interpolate(frame, [120, 135], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const resultSpring = spring({ frame: Math.max(0, frame - 140), fps, config: { damping: 10, mass: 0.6, stiffness: 110 } });
  const resultScale = interpolate(resultSpring, [0, 1], [0.4, 1]);
  const resultOpacity = interpolate(frame, [140, 165], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const fadeOut = interpolate(frame, [210, 240], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const glowPulse = interpolate(Math.sin(frame * 0.04), [-1, 1], [0.03, 0.08]);

  const cardW = 300;
  const gap = 75;
  const icons = [StairsIcon, PartyIcon, KartIcon];

  return (
    <AbsoluteFill style={{ background: colors.backgroundGradient }}>
      <div style={{ position: "absolute", top: "40%", left: "50%", width: 900, height: 900, borderRadius: "50%", background: `radial-gradient(circle, ${colors.accent}08 0%, transparent 60%)`, transform: "translate(-50%, -50%)", opacity: glowPulse * fadeOut }} />
      <AbsoluteFill style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 75, opacity: fadeOut }}>
        <div style={{ display: "flex", alignItems: "center", gap }}>
          {GAMES.map((game, i) => {
            const s = interpolate(gameSprings[i], [0, 1], [0.5, 1]);
            const op = interpolate(gameSprings[i], [0, 0.3], [0, 1], { extrapolateRight: "clamp" });
            const IconComponent = icons[i];
            return (
              <React.Fragment key={game.label}>
                {i > 0 && (
                  <div style={{ fontSize: 72, fontWeight: 300, color: colors.dimmed, opacity: i === 1 ? plus1 : plus2, marginLeft: -gap / 2 - 10, marginRight: -gap / 2 - 10 }}>+</div>
                )}
                <div style={{ width: cardW, opacity: op, transform: `scale(${s})`, display: "flex", flexDirection: "column", alignItems: "center", gap: 18 }}>
                  <div style={{ width: cardW, height: 210, borderRadius: 20, background: `${game.color}10`, border: `2px solid ${game.color}35`, display: "flex", alignItems: "center", justifyContent: "center" }}>
                    <svg width="150" height="120" viewBox="-40 -35 80 70">
                      <IconComponent progress={iconDrawProgress[i]} frame={frame} />
                    </svg>
                  </div>
                  <div style={{ fontSize: 33, fontWeight: 700, fontFamily: fonts.main, color: game.color, textAlign: "center" }}>{game.label}</div>
                  <div style={{ fontSize: 22.5, fontWeight: 500, fontFamily: fonts.main, color: `${colors.dimmed}80`, textAlign: "center" }}>{game.subLabel}</div>
                </div>
              </React.Fragment>
            );
          })}
        </div>
        <div style={{ fontSize: 84, fontWeight: 300, color: colors.dimmed, opacity: equalsProgress }}>=</div>
        <div style={{ fontSize: 78, fontWeight: 800, fontFamily: fonts.main, letterSpacing: 4, textAlign: "center", opacity: resultOpacity, transform: `scale(${resultScale})` }}>
          <span style={{ background: "linear-gradient(90deg, #2ECC71, #FFB547, #FF6B6B)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", filter: `drop-shadow(0 0 20px ${colors.accent}30)` }}>
            我的全新遊戲
          </span>
        </div>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼