Remotion LabRemotion Lab
返回模板庫

卡拉 OK 逐字高亮

卡拉 OK 跟唱效果:以「月亮代表我的心」歌詞示範,每個字依序從灰色變成亮黃色,下方同步顯示進度條,模擬卡拉 OK 舞台字幕。

字幕卡拉OK逐字高亮歌詞進度條
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import React from "react";

const LYRICS = "月亮代表我的心";
const TOTAL-FRAMES = 210;
// 每個字的高亮起始幀(共 7 個字)
const CHAR-HIGHLIGHT-START = [15, 40, 65, 90, 115, 140, 165];
const HIGHLIGHT-DURATION = 28;

export const SubtitleKaraoke: React.FC = () => {
  const frame = useCurrentFrame();
  const { width, height } = useVideoConfig();

  // 整體進度(0~1)用於進度條
  const progress = Math.min(
    Math.max(0, (frame - 10) / (TOTAL-FRAMES - 30)),
    1
  );

  return (
    <AbsoluteFill
      style={{
        background: "linear-gradient(180deg, #0d0d1a 0%, #1a0a2e 100%)",
        width,
        height,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      {/* 背景光暈 */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(ellipse at 50% 50%, rgba(250,204,21,0.08) 0%, transparent 65%)",
        }}
      />

      {/* 標題 */}
      <div
        style={{
          fontSize: 28,
          color: "#9ca3af",
          fontFamily: "sans-serif",
          letterSpacing: 6,
          marginBottom: 60,
        }}
      >
        月亮代表我的心
      </div>

      {/* 歌詞區 */}
      <div
        style={{
          display: "flex",
          flexDirection: "row",
          gap: 12,
          alignItems: "center",
          marginBottom: 80,
        }}
      >
        {LYRICS.split("").map((char, i) => {
          const highlightStart = CHAR-HIGHLIGHT-START[i] ?? 999;
          const charProgress = interpolate(
            frame,
            [highlightStart, highlightStart + HIGHLIGHT-DURATION],
            [0, 1],
            { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
          );
          const isHighlighted = frame >= highlightStart;

          // 從灰色到亮黃色
          const r = Math.round(interpolate(charProgress, [0, 1], [156, 250]));
          const g = Math.round(interpolate(charProgress, [0, 1], [163, 204]));
          const b = Math.round(interpolate(charProgress, [0, 1], [175, 21]));
          const color = isHighlighted
            ? `rgb(${r},${g},${b})`
            : "#4b5563";

          const scale = isHighlighted
            ? 1 + interpolate(charProgress, [0, 0.5, 1], [0, 0.12, 0])
            : 1;

          return (
            <div
              key={i}
              style={{
                fontSize: 88,
                fontWeight: 700,
                color,
                fontFamily: "sans-serif",
                transform: `scale(${scale})`,
                textShadow: isHighlighted
                  ? "0 0 24px rgba(250,204,21,0.6)"
                  : "none",
                transition: "none",
              }}
            >
              {char}
            </div>
          );
        })}
      </div>

      {/* 進度條容器 */}
      <div
        style={{
          width: 640,
          height: 6,
          background: "rgba(255,255,255,0.12)",
          borderRadius: 3,
          overflow: "hidden",
        }}
      >
        <div
          style={{
            width: `${progress * 100}%`,
            height: "100%",
            background: "linear-gradient(90deg, #facc15, #f97316)",
            borderRadius: 3,
          }}
        />
      </div>

      {/* 音符裝飾 */}
      <div
        style={{
          position: "absolute",
          top: 200,
          left: 180,
          fontSize: 40,
          color: "rgba(250,204,21,0.2)",
        }}
      >

      </div>
      <div
        style={{
          position: "absolute",
          top: 160,
          right: 200,
          fontSize: 52,
          color: "rgba(250,204,21,0.15)",
        }}
      >

      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼