Remotion LabRemotion Lab
返回模板庫

AI 音訊控制滑桿

8 秒動畫,左側三條垂直滑桿(Tone、Speed、Style)依序描邊出現並滑動至指定位置,中間貝茲曲線延伸至右側的影片、Podcast、商店三個應用場景圖示,最終所有圖示發光脈衝。

slidercontrolvisualization滑桿AI音訊
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  Sequence,
  interpolate,
  spring,
  Audio,
  staticFile,
} 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 AUDIO = {
  tick: staticFile("audio/connection/tick.wav"),
  whoosh: staticFile("audio/connection/woosh.wav"),
  whooshOut: staticFile("audio/connection/whoosh-out.mp3"),
  ding: staticFile("audio/connection/ding.mp3"),
  microRiser: staticFile("audio/connection/micro-riser.mp3"),
  tinyPop: staticFile("audio/connection/tiny-pop.mp3"),
  softClick: staticFile("audio/connection/soft-click.wav"),
  satisfyingFill: staticFile("audio/connection/satisfying-fill.wav"),
};

const SliderControl: React.FC<{
  x: number;
  y: number;
  height?: number;
  handlePosition: number;
  strokeProgress?: number;
  trackColor?: string;
  label?: string;
}> = ({
  x,
  y,
  height = 200,
  handlePosition = 0.5,
  strokeProgress = 1,
  trackColor = colors.accent,
  label,
}) => {
  const trackLen = height;
  const dashOffset = trackLen * (1 - strokeProgress);
  const handleY = y + height - handlePosition * height;

  return (
    <g>
      <line
        x1={x} y1={y} x2={x} y2={y + height}
        stroke="rgba(255,255,255,0.15)" strokeWidth="6" strokeLinecap="round"
        strokeDasharray={trackLen} strokeDashoffset={dashOffset}
      />
      <line
        x1={x} y1={y + height} x2={x} y2={handleY}
        stroke={trackColor} strokeWidth="6" strokeLinecap="round"
        opacity={strokeProgress}
      />
      {[0, 0.25, 0.5, 0.75, 1].map((t, i) => (
        <line
          key={i}
          x1={x - 10} y1={y + height - t * height}
          x2={x - 5} y2={y + height - t * height}
          stroke="rgba(255,255,255,0.3)" strokeWidth="2"
          opacity={strokeProgress}
        />
      ))}
      <circle cx={x} cy={handleY} r="14" fill={trackColor} opacity={strokeProgress} />
      <circle cx={x} cy={handleY} r="6" fill={colors.background} opacity={strokeProgress} />
      {label && (
        <text
          x={x} y={y + height + 35}
          fontSize="18" fontFamily="'Inter', 'Noto Sans TC', sans-serif"
          fontWeight="600" fill={colors.dimmed}
          textAnchor="middle" opacity={strokeProgress * 0.8}
        >
          {label}
        </text>
      )}
    </g>
  );
};

const ConnectionLine: React.FC<{
  x1: number; y1: number; x2: number; y2: number;
  progress: number; color?: string;
}> = ({ x1, y1, x2, y2, progress, color = "rgba(255,255,255,0.3)" }) => {
  const cpX1 = x1 + (x2 - x1) * 0.4;
  const cpX2 = x1 + (x2 - x1) * 0.6;
  const pathD = `M${x1},${y1} C${cpX1},${y1} ${cpX2},${y2} ${x2},${y2}`;
  const totalLen = 800;
  const dashOffset = totalLen * (1 - progress);

  return (
    <path
      d={pathD} stroke={color} strokeWidth="2.5" fill="none"
      strokeDasharray={totalLen} strokeDashoffset={dashOffset}
      strokeLinecap="round"
    />
  );
};

export const Scene03-Control: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const svgW = 1920;
  const svgH = 1080;
  const sliderBaseX = 420;
  const sliderSpacing = 120;
  const sliderTopY = 300;
  const sliderHeight = 380;
  const iconBaseX = 1350;
  const iconTopY = 260;
  const iconSpacing = 230;

  const sliderStroke1 = interpolate(frame, [0, 35], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const sliderStroke2 = interpolate(frame, [8, 40], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const sliderStroke3 = interpolate(frame, [15, 45], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const handle1Pos = interpolate(frame, [30, 55], [0, 0.7], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const handle2Pos = interpolate(frame, [35, 58], [0, 0.45], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const handle3Pos = interpolate(frame, [38, 60], [0, 0.85], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const fineTune1 = interpolate(frame, [120, 155], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const fineTune2 = interpolate(frame, [128, 158], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const fineTune3 = interpolate(frame, [135, 160], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const finalHandle1 = handle1Pos + fineTune1 * 0.15;
  const finalHandle2 = handle2Pos + fineTune2 * 0.3;
  const finalHandle3 = handle3Pos - fineTune3 * 0.2;

  const conn1Progress = interpolate(frame, [50, 80], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const conn2Progress = interpolate(frame, [58, 85], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const conn3Progress = interpolate(frame, [65, 90], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const icon1Stroke = interpolate(frame, [80, 110], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const icon2Stroke = interpolate(frame, [92, 118], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const icon3Stroke = interpolate(frame, [104, 128], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const glowIntensity = frame > 120
    ? interpolate(frame, [120, 150], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })
    : 0;

  const pulsePhase = frame > 160
    ? 0.3 + 0.15 * Math.sin(((frame - 160) / 25) * Math.PI * 2)
    : 0;

  const sliderGroupSlide = spring({ frame, fps, config: { damping: 18, mass: 1.2 } });
  const sliderGroupX = interpolate(sliderGroupSlide, [0, 1], [-120, 0]);

  const iconGroupSlide = spring({ frame: frame - 75, fps, config: { damping: 18, mass: 1.2 } });
  const iconGroupX = interpolate(iconGroupSlide, [0, 1], [120, 0]);

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

  const s1x = sliderBaseX;
  const s2x = sliderBaseX + sliderSpacing;
  const s3x = sliderBaseX + sliderSpacing * 2;
  const icon1Y = iconTopY;
  const icon2Y = iconTopY + iconSpacing;
  const icon3Y = iconTopY + iconSpacing * 2;
  const s1HandleY = sliderTopY + sliderHeight - finalHandle1 * sliderHeight;
  const s2HandleY = sliderTopY + sliderHeight - finalHandle2 * sliderHeight;
  const s3HandleY = sliderTopY + sliderHeight - finalHandle3 * sliderHeight;

  return (
    <AbsoluteFill style={{ backgroundColor: colors.background, opacity: fadeOut }}>
      <Sequence from={0} durationInFrames={30}><Audio src={AUDIO.tick} volume={0.4} /></Sequence>
      <Sequence from={8} durationInFrames={30}><Audio src={AUDIO.tick} volume={0.35} /></Sequence>
      <Sequence from={15} durationInFrames={30}><Audio src={AUDIO.tick} volume={0.35} /></Sequence>
      <Sequence from={30} durationInFrames={40}><Audio src={AUDIO.satisfyingFill} volume={0.4} /></Sequence>
      <Sequence from={50} durationInFrames={40}><Audio src={AUDIO.whoosh} volume={0.4} /></Sequence>
      <Sequence from={80} durationInFrames={25}><Audio src={AUDIO.ding} volume={0.45} /></Sequence>
      <Sequence from={92} durationInFrames={25}><Audio src={AUDIO.tinyPop} volume={0.4} /></Sequence>
      <Sequence from={104} durationInFrames={25}><Audio src={AUDIO.softClick} volume={0.4} /></Sequence>
      <Sequence from={118} durationInFrames={50}><Audio src={AUDIO.microRiser} volume={0.4} /></Sequence>
      <Sequence from={durationInFrames - 25} durationInFrames={25}><Audio src={AUDIO.whooshOut} volume={0.4} /></Sequence>

      <AbsoluteFill style={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
        <svg width={svgW} height={svgH} viewBox={`0 0 ${svgW} ${svgH}`}>
          <g opacity={1}>
            <ConnectionLine x1={s1x + 20} y1={s1HandleY} x2={iconBaseX - 60} y2={icon1Y} progress={conn1Progress} color={`rgba(77, 163, 255, ${0.25 + pulsePhase * 0.3})`} />
            <ConnectionLine x1={s2x + 20} y1={s2HandleY} x2={iconBaseX - 60} y2={icon2Y} progress={conn2Progress} color={`rgba(255, 107, 107, ${0.25 + pulsePhase * 0.3})`} />
            <ConnectionLine x1={s3x + 20} y1={s3HandleY} x2={iconBaseX - 60} y2={icon3Y} progress={conn3Progress} color={`rgba(255, 217, 61, ${0.25 + pulsePhase * 0.3})`} />
          </g>

          <g transform={`translate(${sliderGroupX}, 0)`}>
            <rect x={sliderBaseX - 60} y={sliderTopY - 60} width={sliderSpacing * 2 + 120} height={sliderHeight + 150} rx="20" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.08)" strokeWidth="1.5" opacity={Math.min(sliderStroke1, 1)} />
            <text x={sliderBaseX + sliderSpacing} y={sliderTopY - 25} fontSize="22" fontFamily="'Inter', 'Noto Sans TC', sans-serif" fontWeight="700" fill={colors.accent} textAnchor="middle" opacity={sliderStroke1 * 0.9} letterSpacing="3">CONTROL</text>
            <SliderControl x={s1x} y={sliderTopY} height={sliderHeight} handlePosition={finalHandle1} strokeProgress={sliderStroke1} trackColor={colors.accent} label="Tone" />
            <SliderControl x={s2x} y={sliderTopY} height={sliderHeight} handlePosition={finalHandle2} strokeProgress={sliderStroke2} trackColor="#FF6B6B" label="Speed" />
            <SliderControl x={s3x} y={sliderTopY} height={sliderHeight} handlePosition={finalHandle3} strokeProgress={sliderStroke3} trackColor="#FFD93D" label="Style" />
          </g>

          <g transform={`translate(${iconGroupX}, 0)`}>
            <rect x={iconBaseX - 80} y={iconTopY - 80} width="260" height={iconSpacing * 2 + 160} rx="20" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.08)" strokeWidth="1.5" opacity={icon1Stroke > 0 ? Math.min(icon1Stroke + 0.3, 1) : 0} />
            <text x={iconBaseX + 50} y={iconTopY - 45} fontSize="22" fontFamily="'Inter', 'Noto Sans TC', sans-serif" fontWeight="700" fill={colors.dimmed} textAnchor="middle" opacity={icon1Stroke * 0.9} letterSpacing="3">SCENARIOS</text>
            {/* Film icon */}
            <rect x={iconBaseX + 10} y={icon1Y - 45} width={80} height={80} rx="10" stroke={colors.accent} strokeWidth="3.5" fill="none" opacity={icon1Stroke} />
            <polygon points={`${iconBaseX + 30},${icon1Y - 30} ${iconBaseX + 30},${icon1Y + 30} ${iconBaseX + 80},${icon1Y}`} fill={colors.accent} opacity={icon1Stroke} />
            <text x={iconBaseX + 50} y={icon1Y + 65} fontSize="17" fontFamily="'Inter', 'Noto Sans TC', sans-serif" fontWeight="600" fill={colors.dimmed} textAnchor="middle" opacity={icon1Stroke * 0.8}>Video</text>
            {/* Mic icon */}
            <rect x={iconBaseX + 30} y={icon2Y - 40} width={40} height={55} rx="20" stroke="#FF6B6B" strokeWidth="3.5" fill="none" opacity={icon2Stroke} />
            <path d={`M${iconBaseX + 10},${icon2Y + 18} Q${iconBaseX + 10},${icon2Y + 45} ${iconBaseX + 50},${icon2Y + 45} Q${iconBaseX + 90},${icon2Y + 45} ${iconBaseX + 90},${icon2Y + 18}`} stroke="#FF6B6B" strokeWidth="3.5" fill="none" strokeLinecap="round" opacity={icon2Stroke} />
            <text x={iconBaseX + 50} y={icon2Y + 65} fontSize="17" fontFamily="'Inter', 'Noto Sans TC', sans-serif" fontWeight="600" fill={colors.dimmed} textAnchor="middle" opacity={icon2Stroke * 0.8}>Podcast</text>
            {/* Storefront icon */}
            <rect x={iconBaseX + 10} y={icon3Y - 20} width={80} height={55} stroke="#FFD93D" strokeWidth="3.5" fill="none" opacity={icon3Stroke} />
            <rect x={iconBaseX + 35} y={icon3Y + 10} width={30} height={25} rx="3" stroke="#FFD93D" strokeWidth="2" fill="none" opacity={icon3Stroke} />
            <text x={iconBaseX + 50} y={icon3Y + 65} fontSize="17" fontFamily="'Inter', 'Noto Sans TC', sans-serif" fontWeight="600" fill={colors.dimmed} textAnchor="middle" opacity={icon3Stroke * 0.8}>Commerce</text>
          </g>
        </svg>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

export const CONTROL-DURATION-FRAMES = 240;

登入後查看完整程式碼