Remotion LabRemotion Lab
返回模板庫

頁面捲動完成率漏斗

以漏斗圖視覺化「點進頁面 1000 人 vs 滑到底部 320 人」的落差,搭配滾動條動畫與 32% 完成率數字卡。

數據視覺化漏斗圖指標瀏覽器 mockup
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
} from "remotion";

const colors = {
  background: "#0A0E14",
  backgroundGradient: "linear-gradient(135deg, #0A0E14 0%, #131A24 100%)",
  accentSecondary: "#4DA3FF",
  warning: "#FFB547",
};
const fonts = { main: "'Inter', 'Noto Sans TC', sans-serif" };

function useCounter(frame: number, startFrame: number, endFrame: number, targetValue: number): number {
  const progress = interpolate(frame, [startFrame, endFrame], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const eased = 1 - Math.pow(1 - progress, 3);
  return Math.round(eased * targetValue);
}

function formatNumber(n: number): string {
  return n.toLocaleString("en-US");
}

export const PAGE-SCROLL-METRICS-DURATION-FRAMES = 210;

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

  const browserSpring = spring({ frame: Math.max(0, frame - 5), fps, config: { damping: 12, mass: 0.6, stiffness: 100 } });
  const browserScale = interpolate(browserSpring, [0, 1], [0.5, 1]);
  const browserOpacity = interpolate(browserSpring, [0, 0.4], [0, 1], { extrapolateRight: "clamp" });

  const topFunnelSpring = spring({ frame: Math.max(0, frame - 25), fps, config: { damping: 10, mass: 0.5, stiffness: 120 } });
  const topFunnelOpacity = interpolate(topFunnelSpring, [0, 0.4], [0, 1], { extrapolateRight: "clamp" });
  const topFunnelScale = interpolate(topFunnelSpring, [0, 1], [0.6, 1]);
  const topCount = useCounter(frame, 30, 75, 1000);

  const bottomFunnelSpring = spring({ frame: Math.max(0, frame - 75), fps, config: { damping: 10, mass: 0.5, stiffness: 120 } });
  const bottomFunnelOpacity = interpolate(bottomFunnelSpring, [0, 0.4], [0, 1], { extrapolateRight: "clamp" });
  const bottomFunnelScale = interpolate(bottomFunnelSpring, [0, 1], [0.6, 1]);
  const bottomCount = useCounter(frame, 80, 130, 320);

  const scrollProgress = interpolate(frame, [80, 145], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });

  const percentSpring = spring({ frame: Math.max(0, frame - 120), fps, config: { damping: 8, mass: 0.4, stiffness: 140 } });
  const percentScale = interpolate(percentSpring, [0, 1], [0.3, 1]);
  const percentOpacity = interpolate(percentSpring, [0, 0.3], [0, 1], { extrapolateRight: "clamp" });

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

  return (
    <AbsoluteFill style={{ background: colors.backgroundGradient }}>
      <AbsoluteFill
        style={{
          display: "flex",
          flexDirection: "row",
          alignItems: "center",
          justifyContent: "center",
          gap: 120,
          opacity: fadeOut,
        }}
      >
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 0 }}>
          <div style={{ opacity: topFunnelOpacity, transform: `scale(${topFunnelScale})`, transformOrigin: "center bottom", display: "flex", flexDirection: "column", alignItems: "center", gap: 18 }}>
            <div style={{ fontSize: 39, fontFamily: fonts.main, fontWeight: 700, color: colors.accentSecondary, letterSpacing: 2 }}>點進頁面的人</div>
            <div style={{ display: "flex", flexDirection: "row", gap: 9, flexWrap: "wrap", maxWidth: 510, justifyContent: "center" }}>
              {Array.from({ length: 10 }).map((_, i) => (
                <svg key={i} width="42" height="48" viewBox="0 0 28 32">
                  <circle cx="14" cy="9" r="7" fill={colors.accentSecondary} opacity={0.9} />
                  <path d="M4 28 C4 20 24 20 24 28" fill={colors.accentSecondary} opacity={0.9} />
                </svg>
              ))}
            </div>
            <div style={{ fontSize: 96, fontWeight: 900, fontFamily: fonts.main, color: colors.accentSecondary, letterSpacing: -2, lineHeight: 1 }}>{formatNumber(topCount)}</div>
          </div>

          <svg width="540" height="420" viewBox="0 0 360 280" style={{ opacity: topFunnelOpacity, marginTop: -15 }}>
            <defs>
              <linearGradient id="funnelTopGrad" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor={colors.accentSecondary} stopOpacity={0.5} />
                <stop offset="100%" stopColor={colors.warning} stopOpacity={0.4} />
              </linearGradient>
            </defs>
            <path d="M30 20 L330 20 L230 140 L130 140 Z" fill="url(#funnelTopGrad)" stroke={colors.accentSecondary} strokeWidth="2" strokeOpacity={0.8} />
            <path d="M130 140 L230 140 L200 200 L160 200 Z" fill="url(#funnelTopGrad)" stroke={colors.warning} strokeWidth="2" strokeOpacity={0.8} opacity={bottomFunnelOpacity} />
            <path d="M160 200 L200 200 L200 260 L160 260 Z" fill={colors.warning} fillOpacity={0.3} stroke={colors.warning} strokeWidth="2" opacity={bottomFunnelOpacity} />
            <path d="M180 264 L170 278 L190 278 Z" fill={colors.warning} opacity={bottomFunnelOpacity} />
          </svg>

          <div style={{ opacity: bottomFunnelOpacity, transform: `scale(${bottomFunnelScale})`, transformOrigin: "center top", display: "flex", flexDirection: "column", alignItems: "center", gap: 18, marginTop: -30 }}>
            <div style={{ display: "flex", flexDirection: "row", gap: 12, justifyContent: "center" }}>
              {Array.from({ length: 4 }).map((_, i) => (
                <svg key={i} width="42" height="48" viewBox="0 0 28 32">
                  <circle cx="14" cy="9" r="7" fill={colors.warning} opacity={0.9} />
                  <path d="M4 28 C4 20 24 20 24 28" fill={colors.warning} opacity={0.9} />
                </svg>
              ))}
            </div>
            <div style={{ fontSize: 96, fontWeight: 900, fontFamily: fonts.main, color: colors.warning, letterSpacing: -2, lineHeight: 1 }}>{formatNumber(bottomCount)}</div>
            <div style={{ fontSize: 39, fontFamily: fonts.main, fontWeight: 700, color: colors.warning, letterSpacing: 2 }}>滑到底部的人</div>
          </div>
        </div>

        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 48, opacity: browserOpacity, transform: `scale(${browserScale})` }}>
          <svg width="570" height="750" viewBox="0 0 380 500">
            <rect x="0" y="0" width="380" height="500" rx="12" fill="rgba(255,255,255,0.04)" stroke="rgba(255,255,255,0.12)" strokeWidth="1.5" />
            <rect x="0" y="0" width="380" height="48" rx="12" fill="rgba(255,255,255,0.07)" />
            <circle cx="20" cy="24" r="7" fill="#FF6B6B" opacity={0.8} />
            <circle cx="40" cy="24" r="7" fill="#FFB547" opacity={0.8} />
            <circle cx="60" cy="24" r="7" fill="#00D4AA" opacity={0.8} />
            <rect x="10" y="58" width="360" height="432" rx="4" fill="rgba(0,0,0,0.2)" />
            {[0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
              <rect key={i} x="20" y={175 + i * 22} width={i % 3 === 2 ? 240 : 340} height="8" rx="2" fill="rgba(255,255,255,0.08)" />
            ))}
            <rect x="362" y="60" width="8" height="430" rx="4" fill="rgba(255,255,255,0.05)" />
            <rect x="363" y={60 + scrollProgress * 340} width="6" height="80" rx="3" fill={colors.accentSecondary} opacity={frame >= 80 ? 0.7 : 0} />
          </svg>

          {percentOpacity > 0 && (
            <div style={{ opacity: percentOpacity, transform: `scale(${percentScale})`, display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }}>
              <div style={{ padding: "24px 60px", borderRadius: 16, background: `linear-gradient(135deg, ${colors.warning}22, ${colors.warning}11)`, border: `2px solid ${colors.warning}60`, display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
                <div style={{ fontSize: 108, fontWeight: 900, fontFamily: fonts.main, color: colors.warning, lineHeight: 1, letterSpacing: -2 }}>32%</div>
                <div style={{ fontSize: 30, fontFamily: fonts.main, fontWeight: 600, color: `${colors.warning}CC`, letterSpacing: 1 }}>Scroll Completion Rate</div>
              </div>
            </div>
          )}
        </div>
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼