Remotion LabRemotion Lab
返回模板庫

左右分割對比圖

左右分割雙資料集對比動畫,中央分隔線由上滑入,兩側標題淡入,四項指標分別從左右滑入並帶數字計數效果,搭配相對比例橫條。

圖表商務簡約
提示詞(可直接修改內容)
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import React from "react";

const LEFT = {
  title: "2024 年",
  color: "#3b82f6",
  metrics: [
    { label: "總營收", value: 4850000, unit: "元" },
    { label: "新用戶", value: 23400, unit: "人" },
    { label: "訂單數", value: 18750, unit: "筆" },
    { label: "滿意度", value: 88, unit: "%" },
  ],
};

const RIGHT = {
  title: "2025 年",
  color: "#8b5cf6",
  metrics: [
    { label: "總營收", value: 6320000, unit: "元" },
    { label: "新用戶", value: 31800, unit: "人" },
    { label: "訂單數", value: 25400, unit: "筆" },
    { label: "滿意度", value: 94, unit: "%" },
  ],
};

// Maximum values across both sides for bar scaling
const MAX-VALUES = LEFT.metrics.map((m, i) =>
  Math.max(m.value, RIGHT.metrics[i].value)
);

function formatNumber(value: number, unit: string): string {
  if (unit === "%" ) return value.toFixed(0) + "%";
  if (value >= 1000000) return (value / 10000).toFixed(0) + " 萬元";
  if (value >= 10000) return (value / 10000).toFixed(1) + " 萬" + unit;
  return value.toLocaleString() + " " + unit;
}

const METRIC-FRAMES = [25, 35, 45, 55];

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

  // Divider line scales in frames 0–20
  const dividerProgress = interpolate(frame, [0, 20], [0, 1], {
    extrapolateRight: "clamp",
  });

  // Title fade in at frame 15
  const titleProgress = spring({
    frame: Math.max(0, frame - 15),
    fps,
    config: { damping: 26, stiffness: 100 },
  });
  const titleOpacity = interpolate(titleProgress, [0, 1], [0, 1]);
  const titleY = interpolate(titleProgress, [0, 1], [-20, 0]);

  return (
    <AbsoluteFill
      style={{
        background: "#0f0f0f",
        fontFamily: "sans-serif",
        overflow: "hidden",
      }}
    >
      {/* Left panel */}
      <div
        style={{
          position: "absolute",
          top: 0,
          left: 0,
          width: "50%",
          height: "100%",
          background: "#0f172a",
        }}
      />

      {/* Right panel */}
      <div
        style={{
          position: "absolute",
          top: 0,
          right: 0,
          width: "50%",
          height: "100%",
          background: "#1a0533",
        }}
      />

      {/* Center divider line */}
      <div
        style={{
          position: "absolute",
          left: "50%",
          top: 0,
          width: 3,
          height: `${dividerProgress * 100}%`,
          background:
            "linear-gradient(180deg, #3b82f6 0%, #8b5cf6 100%)",
          transform: "translateX(-50%)",
          boxShadow: "0 0 16px rgba(139,92,246,0.6)",
        }}
      />

      {/* Left title */}
      <div
        style={{
          position: "absolute",
          top: 90,
          left: 0,
          width: "50%",
          textAlign: "center",
          opacity: titleOpacity,
          transform: `translateY(${titleY}px)`,
        }}
      >
        <div
          style={{
            fontSize: 54,
            fontWeight: 800,
            color: LEFT.color,
            letterSpacing: "0.04em",
          }}
        >
          {LEFT.title}
        </div>
        <div
          style={{
            marginTop: 8,
            fontSize: 20,
            color: "#64748b",
            letterSpacing: "0.06em",
          }}
        >
          年度業績總覽
        </div>
      </div>

      {/* Right title */}
      <div
        style={{
          position: "absolute",
          top: 90,
          right: 0,
          width: "50%",
          textAlign: "center",
          opacity: titleOpacity,
          transform: `translateY(${titleY}px)`,
        }}
      >
        <div
          style={{
            fontSize: 54,
            fontWeight: 800,
            color: RIGHT.color,
            letterSpacing: "0.04em",
          }}
        >
          {RIGHT.title}
        </div>
        <div
          style={{
            marginTop: 8,
            fontSize: 20,
            color: "#6d28d9",
            letterSpacing: "0.06em",
          }}
        >
          年度業績總覽
        </div>
      </div>

      {/* Left metrics */}
      {LEFT.metrics.map((metric, index) => {
        const startFrame = METRIC-FRAMES[index];
        const metricProgress = spring({
          frame: Math.max(0, frame - startFrame),
          fps,
          config: { damping: 24, stiffness: 105 },
        });

        const slideX = interpolate(metricProgress, [0, 1], [-120, 0], {
          extrapolateRight: "clamp",
        });
        const opacity = interpolate(metricProgress, [0, 0.5], [0, 1], {
          extrapolateRight: "clamp",
        });

        const countedValue = interpolate(
          frame,
          [30, 100],
          [0, metric.value],
          { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
        );
        const displayValue = Math.round(countedValue);
        const barWidth = (metric.value / MAX-VALUES[index]) * 280;

        return (
          <div
            key={metric.label}
            style={{
              position: "absolute",
              left: 80,
              top: 230 + index * 160,
              width: "calc(50% - 100px)",
              opacity,
              transform: `translateX(${slideX}px)`,
            }}
          >
            {/* Label */}
            <div
              style={{
                fontSize: 22,
                color: "#94a3b8",
                fontWeight: 500,
                marginBottom: 6,
                letterSpacing: "0.04em",
              }}
            >
              {metric.label}
            </div>

            {/* Animated number */}
            <div
              style={{
                fontSize: 48,
                fontWeight: 800,
                color: LEFT.color,
                lineHeight: 1.1,
                marginBottom: 10,
              }}
            >
              {formatNumber(displayValue, metric.unit)}
            </div>

            {/* Bar */}
            <div
              style={{
                height: 6,
                width: 320,
                background: "rgba(59,130,246,0.15)",
                borderRadius: 3,
                overflow: "hidden",
              }}
            >
              <div
                style={{
                  height: "100%",
                  width: barWidth,
                  background: `linear-gradient(90deg, ${LEFT.color} 0%, ${LEFT.color}88 100%)`,
                  borderRadius: 3,
                  boxShadow: `0 0 8px ${LEFT.color}66`,
                }}
              />
            </div>
          </div>
        );
      })}

      {/* Right metrics */}
      {RIGHT.metrics.map((metric, index) => {
        const startFrame = METRIC-FRAMES[index];
        const metricProgress = spring({
          frame: Math.max(0, frame - startFrame),
          fps,
          config: { damping: 24, stiffness: 105 },
        });

        const slideX = interpolate(metricProgress, [0, 1], [120, 0], {
          extrapolateRight: "clamp",
        });
        const opacity = interpolate(metricProgress, [0, 0.5], [0, 1], {
          extrapolateRight: "clamp",
        });

        const countedValue = interpolate(
          frame,
          [30, 100],
          [0, metric.value],
          { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
        );
        const displayValue = Math.round(countedValue);
        const barWidth = (metric.value / MAX-VALUES[index]) * 280;

        return (
          <div
            key={metric.label}
            style={{
              position: "absolute",
              right: 80,
              top: 230 + index * 160,
              width: "calc(50% - 100px)",
              opacity,
              transform: `translateX(${slideX}px)`,
              textAlign: "right",
            }}
          >
            {/* Label */}
            <div
              style={{
                fontSize: 22,
                color: "#a78bfa",
                fontWeight: 500,
                marginBottom: 6,
                letterSpacing: "0.04em",
              }}
            >
              {metric.label}
            </div>

            {/* Animated number */}
            <div
              style={{
                fontSize: 48,
                fontWeight: 800,
                color: RIGHT.color,
                lineHeight: 1.1,
                marginBottom: 10,
              }}
            >
              {formatNumber(displayValue, metric.unit)}
            </div>

            {/* Bar (right-aligned) */}
            <div
              style={{
                height: 6,
                width: 320,
                background: "rgba(139,92,246,0.15)",
                borderRadius: 3,
                overflow: "hidden",
                marginLeft: "auto",
              }}
            >
              <div
                style={{
                  height: "100%",
                  width: barWidth,
                  background: `linear-gradient(270deg, ${RIGHT.color} 0%, ${RIGHT.color}88 100%)`,
                  borderRadius: 3,
                  marginLeft: "auto",
                  boxShadow: `0 0 8px ${RIGHT.color}66`,
                }}
              />
            </div>
          </div>
        );
      })}
    </AbsoluteFill>
  );
};

登入後查看完整程式碼