Remotion LabRemotion Lab
返回模板庫

程式碼控制畫面元件的動態視覺化

以代碼視窗打字動畫為起點,透過貝茲曲線連線將六個元件節點連接到右側組裝完成的預覽畫面,展示「用程式碼控制畫面」的核心概念。

程式碼元件連線動畫視覺化打字動畫
提示詞(可直接修改內容)
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)",
};

export const CODE-CONTROLS-DURATION-FRAMES = 300;

const codeLines = [
  { text: "const video = compose({", indent: 0, color: "#C792EA" },
  { text: "  title:  <Title />",     indent: 0, color: "#82AAFF" },
  { text: "  chart:  <BarChart />",  indent: 0, color: "#82AAFF" },
  { text: "  image:  <Image />",     indent: 0, color: "#82AAFF" },
  { text: "  button: <Button />",    indent: 0, color: "#82AAFF" },
  { text: "  text:   <Caption />",   indent: 0, color: "#82AAFF" },
  { text: "  icon:   <Icon />",      indent: 0, color: "#82AAFF" },
  { text: "});",                     indent: 0, color: "#C792EA" },
];

const components = [
  { label: "Title",   icon: "T", color: "#4DA3FF", targetX: 1380, targetY: 180 },
  { label: "Chart",   icon: "▊", color: "#F59E0B", targetX: 1500, targetY: 320 },
  { label: "Image",   icon: "◻", color: "#10B981", targetX: 1340, targetY: 460 },
  { label: "Button",  icon: "▣", color: "#EF4444", targetX: 1520, targetY: 580 },
  { label: "Caption", icon: "≡", color: "#A78BFA", targetX: 1360, targetY: 700 },
  { label: "Icon",    icon: "★", color: "#EC4899", targetX: 1480, targetY: 840 },
];

const CODE-RIGHT-X = 620;
const CODE-TOP-Y = 230;
const CODE-LINE-HEIGHT = 42;

const CursorBlink: React.FC<{ frame: number }> = ({ frame }) => {
  const visible = Math.floor(frame / 15) % 2 === 0;
  return <div style={{ width: 2, height: 22, backgroundColor: colors.accent, opacity: visible ? 0.9 : 0, marginLeft: 4, display: "inline-block" }} />;
};

const AssembledScreen: React.FC<{ progress: number; assembleProgress: number }> = ({ progress, assembleProgress }) => {
  const screenOpacity = interpolate(progress, [0, 1], [0, 1]);
  return (
    <div style={{ position: "absolute", right: 100, top: 140, width: 560, height: 700, borderRadius: 20, border: `2px solid rgba(77, 163, 255, ${0.3 * screenOpacity})`, background: `rgba(255,255,255,${0.03 * screenOpacity})`, opacity: screenOpacity, overflow: "hidden" }}>
      <div style={{ height: 36, background: "rgba(255,255,255,0.05)", display: "flex", alignItems: "center", padding: "0 12px", gap: 8 }}>
        {["#EF4444", "#F59E0B", "#10B981"].map((c, i) => <div key={i} style={{ width: 10, height: 10, borderRadius: "50%", backgroundColor: c, opacity: 0.7 }} />)}
      </div>
      <div style={{ padding: 20, display: "flex", flexDirection: "column", gap: 16, opacity: assembleProgress }}>
        <div style={{ height: 48, borderRadius: 8, background: "linear-gradient(90deg, #4DA3FF33, #4DA3FF11)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 24, fontWeight: 700, color: "#4DA3FF" }}>My Video Title</div>
        <div style={{ height: 140, borderRadius: 8, background: "rgba(245,158,11,0.08)", display: "flex", alignItems: "flex-end", justifyContent: "center", gap: 12, padding: "12px 20px" }}>
          {[60, 90, 45, 110, 75, 95].map((h, i) => <div key={i} style={{ width: 36, height: h * assembleProgress, borderRadius: "4px 4px 0 0", background: "linear-gradient(180deg, #F59E0B, #F59E0B88)", opacity: 0.8 }} />)}
        </div>
        <div style={{ height: 120, borderRadius: 8, background: "rgba(16,185,129,0.08)", display: "flex", alignItems: "center", justifyContent: "center", gap: 8 }}>
          <span style={{ fontSize: 40, opacity: 0.5 }}>◻</span>
          <span style={{ fontSize: 18, color: "#10B981", opacity: 0.6 }}>Image</span>
        </div>
        <div style={{ display: "flex", gap: 16 }}>
          <div style={{ flex: 1, height: 52, borderRadius: 12, background: "linear-gradient(135deg, #EF4444, #EF444488)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18, fontWeight: 600, color: "#fff" }}>Subscribe</div>
          <div style={{ flex: 2, height: 52, borderRadius: 12, background: "rgba(167,139,250,0.1)", display: "flex", alignItems: "center", padding: "0 16px", fontSize: 16, color: "#A78BFA" }}>A caption goes here...</div>
        </div>
        <div style={{ display: "flex", justifyContent: "center", gap: 24, marginTop: 4 }}>
          {["★", "♥", "▶", "◆"].map((icon, i) => <span key={i} style={{ fontSize: 28, color: "#EC4899", opacity: 0.7 }}>{icon}</span>)}
        </div>
      </div>
    </div>
  );
};

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

  const codeBlockOpacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
  const codeBlockY = interpolate(frame, [0, 25], [40, 0], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });

  const lineGrowProgress = (index: number) => {
    const start = 60 + index * 8;
    return interpolate(frame, [start, start + 30], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
  };

  const componentAppear = (index: number) => {
    const start = 100 + index * 10;
    return spring({ frame: Math.max(0, frame - start), fps, config: { damping: 12, stiffness: 100 } });
  };

  const screenProgress = interpolate(frame, [170, 200], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
  const assembleProgress = interpolate(frame, [200, 250], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
  const questionOpacity = interpolate(frame, [240, 270], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
  const questionY = interpolate(frame, [240, 270], [20, 0], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });

  const particles = Array.from({ length: 16 }, (_, i) => {
    const baseX = (i * 137) % 1920;
    const baseY = (i * 197) % 1080;
    const speed = 0.2 + (i % 5) * 0.1;
    const y = baseY + Math.sin((frame + i * 25) * speed * 0.03) * 20;
    const x = baseX + Math.cos((frame + i * 40) * speed * 0.02) * 15;
    const size = 1.5 + (i % 3);
    const opacity = interpolate(Math.sin((frame + i * 35) * 0.035), [-1, 1], [0.03, 0.15]);
    return { x, y, size, opacity };
  });

  const codeLineStartY = (index: number) => CODE-TOP-Y + 48 + index * CODE-LINE-HEIGHT;

  return (
    <AbsoluteFill style={{ backgroundColor: colors.background, fontFamily: "'Noto Sans TC', 'Inter', sans-serif" }}>
      <svg width={1920} height={1080} style={{ position: "absolute", top: 0, left: 0 }}>
        {particles.map((p, i) => <circle key={i} cx={p.x} cy={p.y} r={p.size} fill={colors.accent} opacity={p.opacity} />)}
      </svg>
      <div style={{ position: "absolute", left: 80, top: CODE-TOP-Y, width: 520, opacity: codeBlockOpacity, transform: `translateY(${codeBlockY}px)` }}>
        <div style={{ background: "rgba(255,255,255,0.04)", borderRadius: "16px 16px 0 0", padding: "10px 16px", display: "flex", gap: 8, alignItems: "center" }}>
          {["#EF4444", "#F59E0B", "#10B981"].map((c, i) => <div key={i} style={{ width: 10, height: 10, borderRadius: "50%", backgroundColor: c, opacity: 0.6 }} />)}
          <span style={{ marginLeft: 12, fontSize: 14, color: "rgba(255,255,255,0.3)" }}>animation.tsx</span>
        </div>
        <div style={{ background: "rgba(255,255,255,0.02)", borderRadius: "0 0 16px 16px", border: "1px solid rgba(77,163,255,0.15)", borderTop: "none", padding: "20px 24px" }}>
          {codeLines.map((line, i) => {
            const typeDelay = 5 + i * 7;
            const charCount = line.text.length;
            const charsVisible = Math.floor(interpolate(frame, [typeDelay, typeDelay + 15], [0, charCount], { extrapolateRight: "clamp", extrapolateLeft: "clamp" }));
            const lineOpacity = interpolate(frame, [typeDelay, typeDelay + 5], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
            const isCurrentlyTyping = charsVisible < charCount && charsVisible > 0;
            return (
              <div key={i} style={{ fontSize: 20, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", lineHeight: CODE-LINE-HEIGHT + "px", color: line.color, opacity: lineOpacity, display: "flex", alignItems: "center", whiteSpace: "pre" }}>
                <span style={{ color: "rgba(255,255,255,0.2)", marginRight: 16, fontSize: 14, width: 20, textAlign: "right" }}>{i + 1}</span>
                {line.text.slice(0, charsVisible)}
                {isCurrentlyTyping && <CursorBlink frame={frame} />}
              </div>
            );
          })}
        </div>
      </div>
      <svg width={1920} height={1080} style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none" }}>
        <defs>
          {components.map((comp, i) => (
            <linearGradient key={i} id={`line-grad-${i}`} x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" stopColor={colors.accent} stopOpacity={0.6} />
              <stop offset="100%" stopColor={comp.color} stopOpacity={0.8} />
            </linearGradient>
          ))}
        </defs>
        {components.map((comp, i) => {
          const progress = lineGrowProgress(i);
          if (progress <= 0) return null;
          const startX = CODE-RIGHT-X;
          const startY = codeLineStartY(i + 1);
          const endX = comp.targetX;
          const endY = comp.targetY;
          const lineOpacity = interpolate(assembleProgress, [0, 0.8], [1, 0.15], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
          const cpX1 = startX + (endX - startX) * 0.4;
          const cpY1 = startY;
          const cpX2 = startX + (endX - startX) * 0.6;
          const cpY2 = endY;
          const pathData = `M ${startX} ${startY} C ${cpX1} ${cpY1}, ${cpX2} ${cpY2}, ${endX} ${endY}`;
          const approxLen = 800;
          const dashOffset = approxLen * (1 - progress);
          return <path key={i} d={pathData} fill="none" stroke={`url(#line-grad-${i})`} strokeWidth={2} strokeDasharray={approxLen} strokeDashoffset={dashOffset} strokeLinecap="round" opacity={lineOpacity} />;
        })}
      </svg>
      {components.map((comp, i) => {
        const appear = componentAppear(i);
        if (appear <= 0.01) return null;
        const nodeOpacity = interpolate(assembleProgress, [0.3, 0.8], [1, 0], { extrapolateRight: "clamp", extrapolateLeft: "clamp" });
        const glowPhase = Math.sin((frame + i * 20) * 0.06);
        const glowOpacity = interpolate(glowPhase, [-1, 1], [0.3, 0.7]);
        return (
          <div key={i} style={{ position: "absolute", left: comp.targetX - 40, top: comp.targetY - 40, width: 80, height: 80, opacity: appear * nodeOpacity, transform: `scale(${appear})` }}>
            <div style={{ position: "absolute", inset: -20, borderRadius: "50%", background: `radial-gradient(circle, ${comp.color}40, transparent 70%)`, opacity: glowOpacity }} />
            <div style={{ width: 80, height: 80, borderRadius: 16, background: `linear-gradient(135deg, ${comp.color}20, ${comp.color}08)`, border: `1.5px solid ${comp.color}60`, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2 }}>
              <span style={{ fontSize: 24 }}>{comp.icon}</span>
              <span style={{ fontSize: 11, fontWeight: 600, color: comp.color, letterSpacing: 0.5 }}>{comp.label}</span>
            </div>
          </div>
        );
      })}
      <AssembledScreen progress={screenProgress} assembleProgress={assembleProgress} />
      {assembleProgress > 0.8 && (
        <div style={{ position: "absolute", right: 280, top: 440, opacity: interpolate(assembleProgress, [0.8, 1], [0, 0.6], { extrapolateRight: "clamp", extrapolateLeft: "clamp" }) }}>
          <svg width={80} height={80} viewBox="0 0 80 80">
            <circle cx={40} cy={40} r={38} fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.3)" strokeWidth={2} />
            <polygon points="32,22 62,40 32,58" fill="rgba(255,255,255,0.8)" />
          </svg>
        </div>
      )}
      <div style={{ position: "absolute", bottom: 40, width: "100%", display: "flex", justifyContent: "center", opacity: questionOpacity, transform: `translateY(${questionY}px)` }}>
        <div style={{ background: "rgba(77,163,255,0.08)", border: "1px solid rgba(77,163,255,0.2)", borderRadius: 16, padding: "16px 40px", display: "flex", alignItems: "center", gap: 12 }}>
          <span style={{ fontSize: 28, color: colors.accent }}>?</span>
          <span style={{ fontSize: 28, fontWeight: 500, color: colors.text, lineHeight: 1.5 }}>
            有沒有其他套件,也是用<span style={{ color: colors.accent, fontWeight: 700 }}>程式碼</span>控制畫面的呢?
          </span>
        </div>
      </div>
      <AbsoluteFill style={{ backgroundColor: colors.background, opacity: interpolate(frame, [CODE-CONTROLS-DURATION-FRAMES - 15, CODE-CONTROLS-DURATION-FRAMES], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp" }) }} />
    </AbsoluteFill>
  );
};

登入後查看完整程式碼