Remotion LabRemotion Lab
返回模板庫

空白檔案到完整程式碼的轉換動畫

以 SVG 描邊動畫呈現「空白檔案 → 帶能量流箭頭 → 完整程式碼」的視覺轉換過程,生動展示 AI 從零生成程式碼的概念。

SVG描邊動畫程式碼轉換粒子
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  Audio,
  Sequence,
  interpolate,
  spring,
  staticFile,
  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)",
};

export const FROM-SCRATCH-DURATION-FRAMES = 270;

const SFX = {
  woosh: staticFile("audio/connection/woosh.wav"),
  softImpact: staticFile("audio/connection/soft-impact.wav"),
  softClick: staticFile("audio/connection/soft-click.wav"),
  tinyPop: staticFile("audio/connection/tiny-pop.mp3"),
};

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

const EmptyFileIcon: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const pagePathLen = 1000;
  const pageDraw = interpolate(frame, [0, 50], [pagePathLen, 0], EX);
  const foldLen = 120;
  const foldDraw = interpolate(frame, [30, 50], [foldLen, 0], EX);
  const circlePerimeter = Math.PI * 2 * 50;
  const circleDraw = interpolate(frame, [40, 75], [circlePerimeter, 0], EX);
  const crossLen = 55;
  const cross1Draw = interpolate(frame, [65, 80], [crossLen, 0], EX);
  const cross2Draw = interpolate(frame, [70, 85], [crossLen, 0], EX);
  const cursorOpacity = frame > 25 ? (Math.floor(frame / 14) % 2 === 0 ? 0.7 : 0) : 0;
  const labelOpacity = interpolate(frame, [75, 90], [0, 1], EX);

  return (
    <svg width={260} height={360} viewBox="0 0 260 360">
      <defs>
        <linearGradient id="emptyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stopColor="#4DA3FF" />
          <stop offset="100%" stopColor="#6BB8FF" />
        </linearGradient>
      </defs>
      <path d="M30 15 L180 15 L230 65 L230 295 Q230 310 215 310 L45 310 Q30 310 30 295 Z" fill="rgba(77,163,255,0.04)" stroke="url(#emptyGrad)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={pagePathLen} strokeDashoffset={pageDraw} />
      <path d="M180 15 L180 65 L230 65" fill="rgba(77,163,255,0.06)" stroke="url(#emptyGrad)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={foldLen} strokeDashoffset={foldDraw} />
      <rect x="55" y="90" width="3" height="24" rx="1.5" fill="#4DA3FF" opacity={cursorOpacity} />
      <circle cx="130" cy="185" r="50" fill="none" stroke="rgba(77,163,255,0.25)" strokeWidth="2" strokeDasharray="8 6" opacity={interpolate(frame, [40, 60], [0, 1], EX)} />
      <circle cx="130" cy="185" r="50" fill="none" stroke="url(#emptyGrad)" strokeWidth="1.5" strokeDasharray={circlePerimeter} strokeDashoffset={circleDraw} strokeLinecap="round" opacity="0.4" />
      <line x1="112" y1="167" x2="148" y2="203" stroke="rgba(77,163,255,0.35)" strokeWidth="2.5" strokeLinecap="round" strokeDasharray={crossLen} strokeDashoffset={cross1Draw} />
      <line x1="148" y1="167" x2="112" y2="203" stroke="rgba(77,163,255,0.35)" strokeWidth="2.5" strokeLinecap="round" strokeDasharray={crossLen} strokeDashoffset={cross2Draw} />
      <text x="130" y="345" textAnchor="middle" fontSize="22" fontWeight="600" fontFamily="'Noto Sans TC', 'Inter', sans-serif" fill="rgba(77,163,255,0.7)" opacity={labelOpacity}>空白檔案</text>
    </svg>
  );
};

const BigArrow: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const shaftLen = 260;
  const shaftDraw = interpolate(frame, [0, 25], [shaftLen, 0], EX);
  const headLen = 90;
  const headDraw = interpolate(frame, [18, 35], [headLen, 0], EX);
  const glowOpacity = frame > 30 ? interpolate(Math.sin(frame * 0.07), [-1, 1], [0.25, 0.55]) : 0;
  const entranceScale = spring({ frame, fps, config: { damping: 12, stiffness: 60 } });
  const FLOW-COUNT = 8;
  const CYCLE = 40;
  const flowParticles = Array.from({ length: FLOW-COUNT }, (_, i) => {
    const offset = i * (CYCLE / FLOW-COUNT);
    const t = Math.max(0, frame - 10);
    const progress = ((t + offset) % CYCLE) / CYCLE;
    const px = progress * 280;
    const opacity = frame < 10 ? 0 : interpolate(progress, [0, 0.08, 0.85, 1], [0, 0.8, 0.8, 0]);
    const yOff = Math.sin(progress * Math.PI * 2 + i) * 6;
    const size = 3 + (i % 3);
    const color = i % 3 === 0 ? "#34D399" : i % 3 === 1 ? "#A78BFA" : "#4DA3FF";
    return { px, opacity, yOff, size, color };
  });
  const dashOffset = frame > 28 ? (frame - 28) * 4 : 0;
  const dashOpacity = frame > 28 ? 0.3 : 0;

  return (
    <div style={{ transform: `scale(${entranceScale})` }}>
      <svg width={320} height={120} viewBox="0 0 320 120">
        <defs>
          <linearGradient id="arrowGrad" x1="0%" y1="0%" x2="100%" y2="0%">
            <stop offset="0%" stopColor="#4DA3FF" />
            <stop offset="50%" stopColor="#A78BFA" />
            <stop offset="100%" stopColor="#34D399" />
          </linearGradient>
          <filter id="arrowGlow">
            <feGaussianBlur stdDeviation="6" result="blur" />
            <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
          </filter>
        </defs>
        <line x1="30" y1="60" x2="270" y2="60" stroke="url(#arrowGrad)" strokeWidth="14" strokeLinecap="round" opacity={glowOpacity} filter="url(#arrowGlow)" />
        <line x1="30" y1="60" x2="270" y2="60" stroke="url(#arrowGrad)" strokeWidth="4.5" strokeLinecap="round" strokeDasharray={shaftLen} strokeDashoffset={shaftDraw} />
        <line x1="30" y1="60" x2="270" y2="60" stroke="url(#arrowGrad)" strokeWidth="2" strokeLinecap="round" strokeDasharray="12 20" strokeDashoffset={-dashOffset} opacity={dashOpacity} />
        <path d="M255 35 L290 60 L255 85" fill="none" stroke="url(#arrowGrad)" strokeWidth="4.5" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={headLen} strokeDashoffset={headDraw} />
        {flowParticles.map((p, i) => <circle key={i} cx={20 + p.px} cy={60 + p.yOff} r={p.size} fill={p.color} opacity={p.opacity} />)}
      </svg>
    </div>
  );
};

const MONO = "'JetBrains Mono', 'Courier New', monospace";

const codeTextLines: { text: string; x: number; color: string; delay: number }[] = [
  { text: "import React from",   x: 50,  color: "#C792EA", delay: 18 },
  { text: '  "react";',          x: 50,  color: "#C3E88D", delay: 22 },
  { text: "",                    x: 0,   color: "",        delay: 0 },
  { text: "const App = () => {", x: 50,  color: "#89DDFF", delay: 28 },
  { text: "  const [s, set]",    x: 50,  color: "#82AAFF", delay: 33 },
  { text: "    = useState(0);",  x: 50,  color: "#F78C6C", delay: 37 },
  { text: "",                    x: 0,   color: "",        delay: 0 },
  { text: "  return (",          x: 50,  color: "#89DDFF", delay: 43 },
  { text: '    <div className',  x: 50,  color: "#FFCB6B", delay: 48 },
  { text: '      ="main">',     x: 50,  color: "#C3E88D", delay: 52 },
  { text: "      {s}",           x: 50,  color: "#F07178", delay: 56 },
  { text: "    </div>",          x: 50,  color: "#89DDFF", delay: 60 },
  { text: "  );",                x: 50,  color: "#89DDFF", delay: 64 },
  { text: "};",                  x: 50,  color: "#89DDFF", delay: 68 },
];

const FullFileIcon: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const pagePathLen = 1000;
  const pageDraw = interpolate(frame, [0, 45], [pagePathLen, 0], EX);
  const foldLen = 120;
  const foldDraw = interpolate(frame, [25, 45], [foldLen, 0], EX);
  const gutterOpacity = interpolate(frame, [15, 35], [0, 0.25], EX);
  const badgeCircleLen = Math.PI * 2 * 18;
  const badgeCircleDraw = interpolate(frame, [78, 92], [badgeCircleLen, 0], EX);
  const checkPathLen = 35;
  const checkDraw = interpolate(frame, [88, 100], [checkPathLen, 0], EX);
  const badgeScale = spring({ frame: Math.max(0, frame - 76), fps, config: { damping: 10, stiffness: 120 } });
  const glowOpacity = frame > 95 ? interpolate(Math.sin((frame - 95) * 0.06), [-1, 1], [0.12, 0.35]) : 0;
  const labelOpacity = interpolate(frame, [95, 110], [0, 1], EX);
  let lineNum = 0;

  return (
    <svg width={260} height={360} viewBox="0 0 260 360">
      <defs>
        <linearGradient id="fullGrad" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stopColor="#34D399" />
          <stop offset="100%" stopColor="#4DA3FF" />
        </linearGradient>
        <linearGradient id="checkGrad2" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stopColor="#34D399" />
          <stop offset="100%" stopColor="#6EE7B7" />
        </linearGradient>
      </defs>
      <ellipse cx="130" cy="170" rx="120" ry="140" fill="rgba(52,211,153,0.06)" opacity={glowOpacity} />
      <path d="M30 15 L180 15 L230 65 L230 295 Q230 310 215 310 L45 310 Q30 310 30 295 Z" fill="rgba(52,211,153,0.05)" stroke="url(#fullGrad)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={pagePathLen} strokeDashoffset={pageDraw} />
      <path d="M180 15 L180 65 L230 65" fill="rgba(52,211,153,0.08)" stroke="url(#fullGrad)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={foldLen} strokeDashoffset={foldDraw} />
      <line x1="44" y1="72" x2="44" y2="290" stroke="rgba(255,255,255,0.1)" strokeWidth="1" opacity={gutterOpacity} />
      {codeTextLines.map((line, i) => {
        if (line.text === "") return null;
        lineNum++;
        const y = 84 + i * 16;
        const opacity = interpolate(frame, [line.delay, line.delay + 10], [0, 1], EX);
        return (
          <g key={i} opacity={opacity}>
            <text x="40" y={y} textAnchor="end" fontSize="9" fontFamily={MONO} fill="rgba(255,255,255,0.2)">{lineNum}</text>
            <text x={line.x} y={y} fontSize="11" fontFamily={MONO} fontWeight="500" fill={line.color} opacity="0.85">{line.text}</text>
          </g>
        );
      })}
      <g transform={`translate(205, 280) scale(${badgeScale})`} style={{ transformOrigin: "0 0" }}>
        <circle cx="0" cy="0" r="18" fill="rgba(52,211,153,0.2)" stroke="url(#checkGrad2)" strokeWidth="2.5" strokeDasharray={badgeCircleLen} strokeDashoffset={badgeCircleDraw} strokeLinecap="round" />
        <path d="M-8 0 L-2 7 L10 -6" fill="none" stroke="url(#checkGrad2)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" strokeDasharray={checkPathLen} strokeDashoffset={checkDraw} />
      </g>
      <text x="130" y="345" textAnchor="middle" fontSize="22" fontWeight="600" fontFamily="'Noto Sans TC', 'Inter', sans-serif" fill="rgba(52,211,153,0.8)" opacity={labelOpacity}>完整程式碼</text>
    </svg>
  );
};

const Particles: React.FC<{ frame: number }> = ({ frame }) => {
  const dots = Array.from({ length: 14 }, (_, i) => {
    const baseX = (i * 157) % 1920;
    const baseY = (i * 193) % 1080;
    const x = baseX + Math.cos((frame + i * 50) * 0.02) * 20;
    const y = baseY + Math.sin((frame + i * 35) * 0.025) * 25;
    const size = 2 + (i % 3);
    const opacity = interpolate(Math.sin((frame + i * 40) * 0.035), [-1, 1], [0.03, 0.12]);
    return { x, y, size, opacity };
  });
  return (
    <svg width={1920} height={1080} style={{ position: "absolute", top: 0, left: 0 }}>
      {dots.map((d, i) => <circle key={i} cx={d.x} cy={d.y} r={d.size} fill={i % 3 === 0 ? "#34D399" : "#4DA3FF"} opacity={d.opacity} />)}
    </svg>
  );
};

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

  const vignetteOpacity = interpolate(frame, [0, 30], [0, 1], EX);
  const EMPTY-START = 10;
  const ARROW-START = 75;
  const COMPLETE-START = 120;

  const emptyScale = spring({ frame: Math.max(0, frame - EMPTY-START), fps, config: { damping: 14, stiffness: 80 } });
  const completeScale = spring({ frame: Math.max(0, frame - COMPLETE-START), fps, config: { damping: 12, stiffness: 70 } });
  const emptyOpacity = interpolate(frame, [EMPTY-START, EMPTY-START + 15], [0, 1], EX);
  const arrowOpacity = interpolate(frame, [ARROW-START, ARROW-START + 10], [0, 1], EX);
  const completeOpacity = interpolate(frame, [COMPLETE-START, COMPLETE-START + 15], [0, 1], EX);

  return (
    <AbsoluteFill style={{ backgroundColor: colors.background, fontFamily: "'Noto Sans TC', 'Inter', sans-serif", overflow: "hidden" }}>
      <Particles frame={frame} />
      <div style={{ position: "absolute", inset: 0, background: "radial-gradient(ellipse at center, transparent 35%, rgba(0,0,0,0.5) 100%)", opacity: vignetteOpacity, pointerEvents: "none" }} />
      <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", gap: 50, paddingBottom: 80 }}>
        <div style={{ opacity: emptyOpacity, transform: `scale(${emptyScale})` }}>
          <EmptyFileIcon frame={Math.max(0, frame - EMPTY-START)} fps={fps} />
        </div>
        <div style={{ opacity: arrowOpacity }}>
          <BigArrow frame={Math.max(0, frame - ARROW-START)} fps={fps} />
        </div>
        <div style={{ opacity: completeOpacity, transform: `scale(${completeScale})` }}>
          <FullFileIcon frame={Math.max(0, frame - COMPLETE-START)} fps={fps} />
        </div>
      </div>
      <Sequence from={EMPTY-START}><Audio src={SFX.softClick} volume={0.2} /></Sequence>
      {[35, 42, 49].map((f) => <Sequence from={f} key={`pop-${f}`}><Audio src={SFX.tinyPop} volume={0.12} /></Sequence>)}
      <Sequence from={ARROW-START}><Audio src={SFX.woosh} volume={0.35} /></Sequence>
      <Sequence from={COMPLETE-START}><Audio src={SFX.softImpact} volume={0.25} /></Sequence>
      <Sequence from={COMPLETE-START + 20}><Audio src={SFX.softClick} volume={0.15} /></Sequence>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼