Remotion LabRemotion Lab
返回模板庫

AI 設計工具三大使用情境動態圖解

以 SVG 圖示動畫展示三種 AI 輔助設計情境:三圖示同時出現後,逐步轉換為帶文字說明的條列式圖解,呈現「首頁→整站」、「換風格」與「復刻畫面」三種流程。

SVG圖示動態列表情境說明
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

const colors = {
  background: "#0B0F17",
  text: "#FFFFFF",
  accent: "#4DA3FF",
  dimmed: "rgba(255, 255, 255, 0.6)",
  cardBg: "rgba(255, 255, 255, 0.05)",
  border: "rgba(77, 163, 255, 0.3)",
};

const EX = {
  extrapolateRight: "clamp" as const,
  extrapolateLeft: "clamp" as const,
};

const LayoutIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (
  <svg width={size} height={size} viewBox="0 0 80 80" fill="none">
    <rect x="4" y="8" width="72" height="60" rx="6" stroke={color} strokeWidth="3" />
    <line x1="4" y1="20" x2="76" y2="20" stroke={color} strokeWidth="2" />
    <circle cx="14" cy="14" r="2.5" fill={color} />
    <circle cx="22" cy="14" r="2.5" fill={color} />
    <circle cx="30" cy="14" r="2.5" fill={color} />
    <rect x="10" y="26" width="26" height="14" rx="2" fill={color} opacity="0.8" />
    <rect x="10" y="44" width="26" height="18" rx="2" fill={color} opacity="0.3" />
    <rect x="42" y="26" width="28" height="36" rx="2" stroke={color} strokeWidth="2" strokeDasharray="4 3" opacity="0.5" />
    <path d="M62 12l2 4 4 2-4 2-2 4-2-4-4-2 4-2z" fill={color} opacity="0.9" />
  </svg>
);

const RestyleIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (
  <svg width={size} height={size} viewBox="0 0 80 80" fill="none">
    <rect x="16" y="6" width="40" height="52" rx="4" stroke={color} strokeWidth="3" />
    <line x1="24" y1="18" x2="48" y2="18" stroke={color} strokeWidth="2" opacity="0.5" />
    <line x1="24" y1="26" x2="44" y2="26" stroke={color} strokeWidth="2" opacity="0.5" />
    <line x1="24" y1="34" x2="40" y2="34" stroke={color} strokeWidth="2" opacity="0.5" />
    <rect x="50" y="40" width="20" height="12" rx="3" fill={color} opacity="0.7" />
    <line x1="60" y1="52" x2="60" y2="62" stroke={color} strokeWidth="3" />
    <rect x="54" y="62" width="12" height="14" rx="3" fill={color} opacity="0.9" />
    <circle cx="12" cy="68" r="6" fill={color} opacity="0.3" />
    <circle cx="26" cy="72" r="4" fill={color} opacity="0.6" />
  </svg>
);

const ImageToUiIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => (
  <svg width={size} height={size} viewBox="0 0 80 80" fill="none">
    <rect x="4" y="12" width="34" height="28" rx="4" stroke={color} strokeWidth="3" />
    <path d="M8 36l8-10 6 6 4-4 8 8H8z" fill={color} opacity="0.3" />
    <circle cx="14" cy="20" r="3" fill={color} opacity="0.5" />
    <path d="M44 26h10" stroke={color} strokeWidth="3" strokeLinecap="round" />
    <path d="M51 21l5 5-5 5" stroke={color} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
    <rect x="42" y="42" width="34" height="30" rx="4" stroke={color} strokeWidth="3" />
    <rect x="46" y="48" width="12" height="4" rx="1" fill={color} opacity="0.7" />
    <rect x="46" y="55" width="26" height="3" rx="1" fill={color} opacity="0.3" />
    <rect x="46" y="61" width="20" height="3" rx="1" fill={color} opacity="0.3" />
    <rect x="46" y="67" width="10" height="3" rx="1" fill={color} opacity="0.5" />
  </svg>
);

const allIcons = [LayoutIcon, RestyleIcon, ImageToUiIcon];

const items = [
  {
    Icon: LayoutIcon,
    lines: ["只有首頁+元件", "靠 AI 生出其他頁面", "直接生成前端畫面"],
  },
  {
    Icon: RestyleIcon,
    lines: ["拿到完整設計", "用 AI 快速改成想要的風格"],
  },
  {
    Icon: ImageToUiIcon,
    lines: ["沒有 Figma 原稿", "丟參考圖,復刻可操作畫面"],
  },
];

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

  const P1 = 2 * fps;
  const P2 = 12 * fps;
  const P3 = 17 * fps;
  const T = 20;

  const ROW-W = 180 * 3 + 120 * 2;
  const ROW-X = (1920 - ROW-W) / 2;
  const P1-CX = [ROW-X + 90, ROW-X + 390, ROW-X + 690];
  const P1-CY = 540;

  const enterOp = interpolate(frame, [5, 20], [0, 1], EX);
  const enterScale = spring({
    frame: Math.max(0, frame - 5),
    fps,
    config: { damping: 9, stiffness: 90 },
  });
  const sideOp = interpolate(frame, [P1, P1 + T], [1, 0], EX);

  const ITEM-CX = 700;
  const PADDING-Y = 140;
  const USABLE = 800;

  const getItemY = (idx: number, total: number) => {
    if (total === 1) return 540;
    if (total === 2) return PADDING-Y + USABLE * (idx === 0 ? 0.35 : 0.65);
    return PADDING-Y + USABLE * (idx === 0 ? 0.2 : idx === 1 ? 0.5 : 0.8);
  };

  const itemYs = items.map((_, i) => {
    if (i === 0) {
      if (frame < P2) return getItemY(0, 1);
      if (frame < P3) return interpolate(frame, [P2, P2 + T], [getItemY(0, 1), getItemY(0, 2)], EX);
      return interpolate(frame, [P3, P3 + T], [getItemY(0, 2), getItemY(0, 3)], EX);
    }
    if (i === 1) {
      if (frame < P3) return getItemY(1, 2);
      return interpolate(frame, [P3, P3 + T], [getItemY(1, 2), getItemY(1, 3)], EX);
    }
    return getItemY(2, 3);
  });

  const icon1CX = interpolate(frame, [P1, P1 + T], [P1-CX[0], ITEM-CX], EX);
  const icon1CY = itemYs[0];
  const icon1Card = interpolate(frame, [P1, P1 + T], [180, 160], EX);
  const icon1Dim = interpolate(frame, [P2, P2 + T], [1, 0.35], EX);

  const t0Left = icon1CX + icon1Card / 2 + 32;
  const t0Local = frame - P1;
  const t0Op = interpolate(t0Local, [10, 25], [0, 1], EX);
  const t0Slide = interpolate(t0Local, [10, 25], [40, 0], EX);
  const t0Dim = interpolate(frame, [P2, P2 + T], [1, 0.35], EX);

  return (
    <AbsoluteFill
      style={{
        backgroundColor: colors.background,
        fontFamily: "'Noto Sans TC', 'Inter', sans-serif",
        overflow: "hidden",
      }}
    >
      {sideOp > 0 &&
        [1, 2].map((i) => {
          const SideIcon = allIcons[i];
          return (
            <div
              key={`side-${i}`}
              style={{
                position: "absolute",
                left: P1-CX[i],
                top: P1-CY,
                transform: `translate(-50%, -50%) scale(${enterScale})`,
                opacity: enterOp * sideOp,
              }}
            >
              <div
                style={{
                  width: 180,
                  height: 180,
                  borderRadius: 28,
                  backgroundColor: "rgba(77, 163, 255, 0.08)",
                  border: `2px solid ${colors.border}`,
                  display: "flex",
                  justifyContent: "center",
                  alignItems: "center",
                }}
              >
                <SideIcon size={100} color={colors.accent} />
              </div>
            </div>
          );
        })}
      <div
        style={{
          position: "absolute",
          left: icon1CX,
          top: icon1CY,
          transform: `translate(-50%, -50%) scale(${enterScale})`,
          opacity: enterOp * icon1Dim,
        }}
      >
        <div
          style={{
            width: Math.round(icon1Card),
            height: Math.round(icon1Card),
            borderRadius: 28,
            backgroundColor: "rgba(77, 163, 255, 0.08)",
            border: `2px solid ${colors.border}`,
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
          }}
        >
          <LayoutIcon size={100} color={colors.accent} />
        </div>
      </div>
      {t0Op > 0 && (
        <div
          style={{
            position: "absolute",
            left: t0Left,
            top: itemYs[0],
            transform: `translateY(-50%) translateX(${t0Slide}px)`,
            opacity: t0Op * t0Dim,
          }}
        >
          {items[0].lines.map((line, j) => (
            <div
              key={j}
              style={{
                fontSize: 36,
                fontWeight: 500,
                color: j === 0 ? colors.text : colors.dimmed,
                lineHeight: 1.7,
              }}
            >
              {line}
            </div>
          ))}
        </div>
      )}
      {[1, 2].map((idx) => {
        const start = idx === 1 ? P2 : P3;
        if (frame < start - 5) return null;
        const local = frame - start;
        const rowOp = interpolate(local, [0, 15], [0, 1], EX);
        const iconSc = spring({
          frame: Math.max(0, local),
          fps,
          config: { damping: 12, stiffness: 100 },
        });
        const txtOp = interpolate(local, [10, 25], [0, 1], EX);
        const txtSlide = interpolate(local, [10, 25], [40, 0], EX);
        let dim = 1;
        if (idx === 1) dim = interpolate(frame, [P3, P3 + T], [1, 0.35], EX);
        const { Icon, lines } = items[idx];
        return (
          <div
            key={idx}
            style={{
              position: "absolute",
              left: ITEM-CX - 80,
              top: itemYs[idx],
              transform: "translateY(-50%)",
              opacity: rowOp * dim,
              display: "flex",
              alignItems: "center",
              gap: 32,
            }}
          >
            <div
              style={{
                transform: `scale(${iconSc})`,
                width: 160,
                height: 160,
                borderRadius: 28,
                backgroundColor: "rgba(77, 163, 255, 0.08)",
                border: `2px solid ${colors.border}`,
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                flexShrink: 0,
              }}
            >
              <Icon size={100} color={colors.accent} />
            </div>
            <div style={{ opacity: txtOp, transform: `translateX(${txtSlide}px)` }}>
              {lines.map((line, j) => (
                <div
                  key={j}
                  style={{
                    fontSize: 36,
                    fontWeight: 500,
                    color: j === 0 ? colors.text : colors.dimmed,
                    lineHeight: 1.7,
                  }}
                >
                  {line}
                </div>
              ))}
            </div>
          </div>
        );
      })}
    </AbsoluteFill>
  );
};

登入後查看完整程式碼