Remotion LabRemotion Lab
返回模板庫

我們開始!能量爆發動畫

5 秒高能開場動畫:同心脈衝環從中心擴散;播放圖示彈跳出現;「我們開始」主文字以彈簧動畫升入;能量射線從中心輻射展開;持續階段添加旋轉軌道環與漂浮星點,最後快速淡出。

introenergyCTA開場播放
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  Sequence,
  interpolate,
  spring,
  Audio,
  staticFile,
} from "remotion";

const GREEN = "#00D4AA";
const BLUE = "#4DA3FF";
const TEXT-COLOR = "#FFFFFF";
const FONT-FAMILY = "'Noto Sans TC', 'Inter', sans-serif";
const BACKGROUND = "#0B0F17";

const AUDIO = {
  softClick: staticFile("audio/connection/soft-click.wav"),
  whooshOut: staticFile("audio/connection/whoosh-out.mp3"),
  ding: staticFile("audio/connection/ding.mp3"),
  microRiser: staticFile("audio/connection/micro-riser.mp3"),
  softImpact: staticFile("audio/connection/soft-impact.wav"),
  satisfyingFill: staticFile("audio/connection/satisfying-fill.wav"),
};

const PlayIcon: React.FC<{ size?: number; strokeProgress?: number; fillOpacity?: number; glowIntensity?: number }> = ({
  size = 120, strokeProgress = 1, fillOpacity = 0.15, glowIntensity = 0,
}) => {
  const totalLen = 400;
  const dashOffset = totalLen * (1 - strokeProgress);
  return (
    <svg width={size} height={size} viewBox="0 0 120 120" fill="none" style={{ filter: glowIntensity > 0 ? `drop-shadow(0 0 ${8 + glowIntensity * 18}px ${GREEN}90)` : "none" }}>
      <circle cx="60" cy="60" r="52" stroke={GREEN} strokeWidth="3.5" fill={GREEN} fillOpacity={fillOpacity * strokeProgress} strokeLinecap="round" strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
      <path d="M46 34 L46 86 L90 60 Z" stroke={TEXT-COLOR} strokeWidth="3" strokeLinejoin="round" fill={TEXT-COLOR} fillOpacity={strokeProgress * 0.9} strokeDasharray={totalLen} strokeDashoffset={dashOffset} />
    </svg>
  );
};

const Sparkle: React.FC<{ x: number; y: number; size?: number; opacity?: number; rotation?: number; color?: string }> = ({
  x, y, size = 20, opacity = 1, rotation = 0, color = GREEN,
}) => (
  <svg style={{ position: "absolute", left: x - size / 2, top: y - size / 2, transform: `rotate(${rotation}deg)`, pointerEvents: "none" }} width={size} height={size} viewBox="0 0 20 20" fill="none">
    <path d="M10 2 L11.5 8.5 L18 10 L11.5 11.5 L10 18 L8.5 11.5 L2 10 L8.5 8.5 Z" fill={color} opacity={opacity} />
  </svg>
);

const EnergyRay: React.FC<{ angle: number; innerRadius: number; outerRadius: number; progress: number; color?: string; width?: number }> = ({
  angle, innerRadius, outerRadius, progress, color = GREEN, width = 2.5,
}) => {
  const rad = (angle * Math.PI) / 180;
  const cx = 960, cy = 540;
  const x1 = cx + Math.cos(rad) * innerRadius, y1 = cy + Math.sin(rad) * innerRadius;
  const x2 = cx + Math.cos(rad) * outerRadius, y2 = cy + Math.sin(rad) * outerRadius;
  const lineLen = outerRadius - innerRadius;
  return <line x1={x1} y1={y1} x2={x2} y2={y2} stroke={color} strokeWidth={width} strokeLinecap="round" strokeDasharray={lineLen} strokeDashoffset={lineLen * (1 - progress)} opacity={progress * 0.7} />;
};

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

  const CENTER-X = 960, CENTER-Y = 540;

  const pulseRingRadius = interpolate(frame, [0, 15], [0, 120], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const pulseRingOpacity = interpolate(frame, [0, 5, 12, 15], [0, 0.6, 0.3, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const pulseRing2Radius = interpolate(frame, [4, 18], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const pulseRing2Opacity = interpolate(frame, [4, 8, 15, 18], [0, 0.4, 0.2, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const playSpring = spring({ frame: frame - 15, fps, config: { damping: 8, mass: 0.6, stiffness: 200 } });
  const playScale = interpolate(playSpring, [0, 1], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const playOpacity = interpolate(frame, [15, 22], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const playStroke = interpolate(frame, [15, 35], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const textSpring = spring({ frame: frame - 20, fps, config: { damping: 9, mass: 0.7, stiffness: 180 } });
  const textScale = interpolate(textSpring, [0, 1], [0.3, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const textOpacity = interpolate(frame, [20, 30], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const textY = interpolate(textSpring, [0, 1], [40, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const burstProgress = interpolate(frame, [22, 45], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const burstFade = interpolate(frame, [35, 55], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const energyRays = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330].map((angle, i) => ({
    angle, inner: 100 + (i % 3) * 5, outer: 200 + (i % 2) * 10,
  }));

  const impactRingRadius = interpolate(frame, [22, 50], [60, 280], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const impactRingOpacity = interpolate(frame, [22, 30, 45, 50], [0, 0.5, 0.15, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const revealSparkles = [
    { angle: -30, dist: 180, delay: 25, size: 24 },
    { angle: 45, dist: 200, delay: 30, size: 20 },
    { angle: 135, dist: 190, delay: 35, size: 22 },
    { angle: -135, dist: 175, delay: 28, size: 18 },
    { angle: 80, dist: 220, delay: 33, size: 16 },
    { angle: -80, dist: 210, delay: 38, size: 20 },
  ];

  const isHoldPhase = frame >= 60 && frame < 120;

  const textGlow = isHoldPhase ? 0.3 + 0.35 * Math.sin(((frame - 60) / 25) * Math.PI * 2) : 0;
  const playGlow = isHoldPhase ? 0.2 + 0.3 * Math.sin(((frame - 60 + 10) / 25) * Math.PI * 2) : 0;
  const playFloat = isHoldPhase ? Math.sin(((frame - 60) / 30) * Math.PI * 2) * 5 : 0;

  const ringRotation = frame >= 30 ? (frame - 30) * 0.8 : 0;
  const ringOpacity = isHoldPhase ? 0.15 + 0.1 * Math.sin(((frame - 60) / 35) * Math.PI * 2) : 0;

  const idleSparkles = [
    { angle: 0, dist: 250, phase: 0, size: 18 },
    { angle: 90, dist: 260, phase: 15, size: 14 },
    { angle: 180, dist: 240, phase: 30, size: 16 },
    { angle: 270, dist: 255, phase: 45, size: 12 },
  ];

  const globalFadeOut = interpolate(frame, [120, 145], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const scaleOut = interpolate(frame, [120, 145], [1, 0.85], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const contentVisible = frame >= 15;

  return (
    <AbsoluteFill style={{ backgroundColor: BACKGROUND, opacity: globalFadeOut }}>
      <Sequence from={0} durationInFrames={25}><Audio src={AUDIO.microRiser} volume={0.5} /></Sequence>
      <Sequence from={20} durationInFrames={40}><Audio src={AUDIO.ding} volume={0.55} /></Sequence>
      <Sequence from={15} durationInFrames={20}><Audio src={AUDIO.softImpact} volume={0.4} /></Sequence>
      <Sequence from={60} durationInFrames={40}><Audio src={AUDIO.satisfyingFill} volume={0.3} /></Sequence>
      <Sequence from={120} durationInFrames={30}><Audio src={AUDIO.whooshOut} volume={0.45} /></Sequence>

      <AbsoluteFill style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", transform: `scale(${scaleOut})` }}>
        {frame < 20 && (
          <svg style={{ position: "absolute", left: 0, top: 0, width: 1920, height: 1080, pointerEvents: "none" }} viewBox="0 0 1920 1080" fill="none">
            <circle cx={CENTER-X} cy={CENTER-Y} r={pulseRingRadius} stroke={GREEN} strokeWidth="3" fill="none" opacity={pulseRingOpacity} />
            <circle cx={CENTER-X} cy={CENTER-Y} r={pulseRing2Radius} stroke={BLUE} strokeWidth="2" fill="none" opacity={pulseRing2Opacity} />
          </svg>
        )}

        {contentVisible && (
          <>
            {frame >= 22 && frame < 55 && (
              <svg style={{ position: "absolute", left: 0, top: 0, width: 1920, height: 1080, pointerEvents: "none" }} viewBox="0 0 1920 1080" fill="none">
                <circle cx={CENTER-X} cy={CENTER-Y} r={impactRingRadius} stroke={GREEN} strokeWidth="2.5" fill="none" opacity={impactRingOpacity} />
              </svg>
            )}

            {frame >= 22 && frame < 58 && (
              <svg style={{ position: "absolute", left: 0, top: 0, width: 1920, height: 1080, pointerEvents: "none" }} viewBox="0 0 1920 1080" fill="none">
                {energyRays.map((ray, i) => (
                  <EnergyRay key={i} angle={ray.angle} innerRadius={ray.inner} outerRadius={ray.outer} progress={burstProgress * burstFade} color={i % 3 === 0 ? GREEN : i % 3 === 1 ? BLUE : TEXT-COLOR} width={i % 2 === 0 ? 2.5 : 1.5} />
                ))}
              </svg>
            )}

            {frame >= 30 && (
              <svg style={{ position: "absolute", left: CENTER-X - 200, top: CENTER-Y - 200, width: 400, height: 400, pointerEvents: "none", transform: `rotate(${ringRotation}deg)` }} viewBox="0 0 400 400" fill="none">
                <circle cx="200" cy="200" r="180" stroke={GREEN} strokeWidth="1.5" fill="none" opacity={ringOpacity} strokeDasharray="12 18" />
                <circle cx="200" cy="200" r="155" stroke={BLUE} strokeWidth="1" fill="none" opacity={ringOpacity * 0.6} strokeDasharray="8 24" />
              </svg>
            )}

            <div style={{ position: "absolute", left: CENTER-X - 50, top: CENTER-Y - 160 + playFloat, transform: `scale(${playScale})`, opacity: playOpacity }}>
              <PlayIcon size={100} strokeProgress={playStroke} fillOpacity={0.15} glowIntensity={playGlow} />
            </div>

            <div style={{ position: "absolute", left: 0, right: 0, top: CENTER-Y - 20, textAlign: "center", opacity: textOpacity, transform: `scale(${textScale}) translateY(${textY}px)` }}>
              <span style={{ fontSize: 88, fontWeight: 800, fontFamily: FONT-FAMILY, color: TEXT-COLOR, letterSpacing: 12, textShadow: textGlow > 0 ? `0 0 ${20 + textGlow * 40}px ${GREEN}80` : "none" }}>
                我們開始
              </span>
            </div>

            {revealSparkles.map((sp, i) => {
              if (frame < sp.delay) return null;
              const elapsed = frame - sp.delay;
              const spOpacity = interpolate(elapsed, [0, 5, 20, 35], [0, 1, 0.7, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
              if (spOpacity <= 0) return null;
              const rad = (sp.angle * Math.PI) / 180;
              const expandDist = sp.dist + elapsed * 1.5;
              return <Sparkle key={`reveal-${i}`} x={CENTER-X + Math.cos(rad) * expandDist} y={CENTER-Y + Math.sin(rad) * expandDist} size={sp.size} opacity={spOpacity} rotation={elapsed * 5} color={i % 2 === 0 ? GREEN : BLUE} />;
            })}

            {isHoldPhase && idleSparkles.map((sp, i) => {
              const elapsed = frame - 60;
              const currentAngle = sp.angle + elapsed * 1.2 + sp.phase;
              const rad = (currentAngle * Math.PI) / 180;
              const twinkle = 0.4 + 0.6 * Math.abs(Math.sin(((elapsed + sp.phase) / 15) * Math.PI));
              return <Sparkle key={`idle-${i}`} x={CENTER-X + Math.cos(rad) * sp.dist} y={CENTER-Y + Math.sin(rad) * sp.dist} size={sp.size} opacity={twinkle} rotation={elapsed * 3 + i * 45} color={i % 2 === 0 ? GREEN : BLUE} />;
            })}
          </>
        )}
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

export const LETS-START-DURATION-FRAMES = 150;

登入後查看完整程式碼