Remotion LabRemotion Lab
返回模板庫

音訊軟體選擇三條件

10 秒動畫,展示挑選音訊編輯軟體的三個評分條件:基本功能、MCP 串接支援、開源免費,每張條件卡片依序從左側滑入,配有數字徽章、SVG 圖示與文字說明。

criterialistsoftware條件音訊
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  Sequence,
  interpolate,
  spring,
  Audio,
  staticFile,
} from "remotion";

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

const AUDIO = {
  softClick: staticFile("audio/connection/soft-click.wav"),
  whoushOut: staticFile("audio/connection/whoosh-out.mp3"),
  ding: staticFile("audio/connection/ding.mp3"),
  tinyPop: staticFile("audio/connection/tiny-pop.mp3"),
  tick: staticFile("audio/connection/tick.wav"),
};

const NumberBadge: React.FC<{
  num: number;
  scale?: number;
  glowIntensity?: number;
}> = ({ num, scale = 1, glowIntensity = 0 }) => (
  <div
    style={{
      width: 50,
      height: 50,
      borderRadius: "50%",
      border: `3px solid ${colors.accentSecondary}`,
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      flexShrink: 0,
      transform: `scale(${scale})`,
      boxShadow: glowIntensity > 0
        ? `0 0 ${8 + glowIntensity * 16}px ${colors.accentSecondary}80`
        : "none",
      background: `rgba(0, 212, 170, ${0.08 + glowIntensity * 0.12})`,
    }}
  >
    <span style={{ fontSize: 24, fontWeight: 800, color: colors.accentSecondary, fontFamily: "'Inter', 'Noto Sans TC', sans-serif", lineHeight: 1 }}>
      {num}
    </span>
  </div>
);

const CriteriaCard: React.FC<{
  num: number;
  text: string;
  icon: React.ReactNode;
  opacity: number;
  translateX: number;
  badgeScale: number;
  badgeGlow: number;
}> = ({ num, text, icon, opacity, translateX, badgeScale, badgeGlow }) => (
  <div
    style={{
      width: 1400,
      height: 120,
      borderRadius: 16,
      border: "1.5px solid rgba(0, 212, 170, 0.3)",
      backgroundColor: "rgba(255, 255, 255, 0.05)",
      display: "flex",
      flexDirection: "row",
      alignItems: "center",
      gap: 28,
      paddingLeft: 32,
      paddingRight: 32,
      opacity,
      transform: `translateX(${translateX}px)`,
    }}
  >
    <NumberBadge num={num} scale={badgeScale} glowIntensity={badgeGlow} />
    <div style={{ display: "flex", justifyContent: "center", alignItems: "center", flexShrink: 0, width: 70, height: 70 }}>
      {icon}
    </div>
    <span style={{ fontSize: 36, fontWeight: 500, color: colors.text, fontFamily: "'Noto Sans TC', 'Inter', sans-serif", lineHeight: 1.4 }}>
      {text}
    </span>
  </div>
);

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

  const headerOpacity = interpolate(frame, [5, 25], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const headerSlideY = interpolate(frame, [5, 30], [-20, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const cardStartFrames = [40, 80, 120];
  const cardAnimations = cardStartFrames.map((startFrame) => {
    const cardSpring = spring({ frame: frame - startFrame, fps, config: { damping: 14, mass: 0.9, stiffness: 120 } });
    const translateX = interpolate(cardSpring, [0, 1], [-60, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
    const opacity = interpolate(frame, [startFrame, startFrame + 15], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
    const strokeProgress = interpolate(frame, [startFrame, startFrame + 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
    return { translateX, opacity, strokeProgress };
  });

  const badgeGlowBurst = interpolate(frame, [150, 160, 170], [0, 0.8, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const idlePulse = frame > 170 && frame < 275 ? 0.04 * Math.sin(((frame - 170) / 30) * Math.PI * 2) : 0;
  const badgeIdleScale = 1 + idlePulse;
  const badgeIdleGlow = frame > 170 && frame < 275 ? 0.15 + 0.1 * Math.sin(((frame - 170) / 30) * Math.PI * 2) : 0;
  const combinedBadgeGlow = Math.max(badgeGlowBurst, badgeIdleGlow);
  const combinedBadgeScale = frame >= 150 && frame <= 170
    ? 1 + 0.08 * Math.sin(((frame - 150) / 20) * Math.PI * 2)
    : badgeIdleScale;

  const fadeOut = interpolate(frame, [275, 300], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  // Simple placeholder icons (replace with your own SVGs)
  const makeIcon = (sp: number, color: string, shape: "file" | "plug" | "lock") => {
    const totalLen = 400;
    const dashOffset = totalLen * (1 - sp);
    if (shape === "file") return (
      <svg width={65} height={65} viewBox="0 0 80 80" fill="none">
        <path d="M22 10 L52 10 L62 22 L62 70 L22 70 Z" stroke={color} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M52 10 L52 22 L62 22" stroke={color} strokeWidth="2.5" strokeLinecap="round" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M8 45 L32 45" stroke="#4DA3FF" strokeWidth="3" strokeLinecap="round" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M24 37 L32 45 L24 53" stroke="#4DA3FF" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
      </svg>
    );
    if (shape === "plug") return (
      <svg width={65} height={65} viewBox="0 0 80 80" fill="none">
        <path d="M12 28 L28 28" stroke={colors.accentSecondary} strokeWidth="3" strokeLinecap="round" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M12 42 L28 42" stroke={colors.accentSecondary} strokeWidth="3" strokeLinecap="round" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <rect x="28" y="22" width="10" height="26" rx="3" stroke={colors.accentSecondary} strokeWidth="2.5" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <rect x="42" y="22" width="10" height="26" rx="3" stroke={color} strokeWidth="2.5" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M52 28 L68 28" stroke={color} strokeWidth="3" strokeLinecap="round" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M52 42 L68 42" stroke={color} strokeWidth="3" strokeLinecap="round" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
      </svg>
    );
    return (
      <svg width={65} height={65} viewBox="0 0 80 80" fill="none">
        <rect x="22" y="36" width="36" height="28" rx="5" stroke={color} strokeWidth="3" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M30 36 L30 24 Q30 12 40 12 Q50 12 50 24 L50 28" stroke={color} strokeWidth="3" strokeLinecap="round" fill="none" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
        <path d="M40 47 Q36 42 33 45 Q30 48 33 52 L40 58 L47 52 Q50 48 47 45 Q44 42 40 47 Z" fill="#FF6B6B" opacity={sp * 0.8} />
      </svg>
    );
  };

  const cards = [
    { num: 1, text: "軟體本身要能夠匯入音檔、編輯等基本功能", iconShape: "file" as const, iconColor: colors.accentSecondary },
    { num: 2, text: "希望要有 MCP 可以串接", iconShape: "plug" as const, iconColor: colors.accent },
    { num: 3, text: "如果是開源、免費的更好", iconShape: "lock" as const, iconColor: colors.accentSecondary },
  ];

  return (
    <AbsoluteFill style={{ backgroundColor: colors.background, opacity: fadeOut }}>
      <Sequence from={5} durationInFrames={30}><Audio src={AUDIO.softClick} volume={0.5} /></Sequence>
      <Sequence from={40} durationInFrames={20}><Audio src={AUDIO.tick} volume={0.4} /></Sequence>
      <Sequence from={43} durationInFrames={20}><Audio src={AUDIO.tinyPop} volume={0.35} /></Sequence>
      <Sequence from={80} durationInFrames={20}><Audio src={AUDIO.tick} volume={0.4} /></Sequence>
      <Sequence from={83} durationInFrames={20}><Audio src={AUDIO.tinyPop} volume={0.35} /></Sequence>
      <Sequence from={120} durationInFrames={20}><Audio src={AUDIO.tick} volume={0.4} /></Sequence>
      <Sequence from={123} durationInFrames={20}><Audio src={AUDIO.tinyPop} volume={0.35} /></Sequence>
      <Sequence from={150} durationInFrames={30}><Audio src={AUDIO.ding} volume={0.5} /></Sequence>
      <Sequence from={275} durationInFrames={25}><Audio src={AUDIO.whoushOut} volume={0.4} /></Sequence>

      <AbsoluteFill style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", paddingTop: 180 }}>
        <div style={{ opacity: headerOpacity, transform: `translateY(${headerSlideY}px)`, marginBottom: 60 }}>
          <span style={{ fontSize: 56, fontWeight: 700, color: colors.text, fontFamily: "'Noto Sans TC', 'Inter', sans-serif", letterSpacing: 2 }}>
            軟體選擇的<span style={{ color: colors.accentSecondary }}>三個條件</span>
          </span>
        </div>
        <div style={{ display: "flex", flexDirection: "column", gap: 32, alignItems: "center" }}>
          {cards.map((card, idx) => (
            <CriteriaCard
              key={card.num}
              num={card.num}
              text={card.text}
              icon={makeIcon(cardAnimations[idx].strokeProgress, card.iconColor, card.iconShape)}
              opacity={cardAnimations[idx].opacity}
              translateX={cardAnimations[idx].translateX}
              badgeScale={combinedBadgeScale}
              badgeGlow={combinedBadgeGlow}
            />
          ))}
        </div>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

export const SOFTWARE-CRITERIA-DURATION-FRAMES = 300;

登入後查看完整程式碼