Remotion LabRemotion Lab
返回模板庫

10 人註冊 0 人付費

以股市崩盤線圖展示「10 人註冊、0 人付費」的產品困境:折線從高點急速墜落,flatline,最後「0 人付費」文字猛然砸下。

數據視覺化折線圖崩盤營收
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
} from "remotion";

const fonts = { main: "'Inter', 'Noto Sans TC', sans-serif" };

const PATH-POINTS: [number, number][] = [
  [0, 100], [45, 90], [90, 108], [135, 85], [175, 95],
  [215, 78], [255, 92], [290, 88], [320, 110], [345, 140],
  [368, 185], [390, 235], [408, 285], [425, 320], [442, 348],
  [460, 362], [520, 362], [600, 362], [700, 362], [790, 362],
];

function getVisiblePoints(frame: number): [number, number][] {
  if (frame < 15) return [];
  const progress = Math.min(1, (frame - 15) / 50);
  const numPoints = progress * (PATH-POINTS.length - 1);
  const fullPoints = Math.floor(numPoints);
  const frac = numPoints - fullPoints;
  const visible: [number, number][] = PATH-POINTS.slice(0, fullPoints + 1);
  if (frac > 0 && fullPoints < PATH-POINTS.length - 1) {
    const p0 = PATH-POINTS[fullPoints];
    const p1 = PATH-POINTS[fullPoints + 1];
    visible[visible.length - 1] = [p0[0] + (p1[0] - p0[0]) * frac, p0[1] + (p1[1] - p0[1]) * frac];
  }
  return visible;
}

function pointsToPolyline(pts: [number, number][]): string {
  return pts.map(([x, y]) => `${x},${y}`).join(" ");
}

function pointsToAreaPath(pts: [number, number][], bottomY: number): string {
  if (pts.length < 2) return "";
  const start = pts[0];
  const end = pts[pts.length - 1];
  const lineSegment = pts.map(([x, y]) => `${x},${y}`).join(" L ");
  return `M ${start[0]},${bottomY} L ${lineSegment} L ${end[0]},${bottomY} Z`;
}

function getCrashProgress(frame: number): number {
  return interpolate(frame, [42, 62], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
}

function lerpColor(c1: [number, number, number], c2: [number, number, number], t: number): string {
  const r = Math.round(c1[0] + (c2[0] - c1[0]) * t);
  const g = Math.round(c1[1] + (c2[1] - c1[1]) * t);
  const b = Math.round(c1[2] + (c2[2] - c1[2]) * t);
  return `rgb(${r},${g},${b})`;
}

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

  const fadeIn = interpolate(frame, [0, 8], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const fadeOut = interpolate(frame, [120, 150], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const masterOpacity = frame < 120 ? fadeIn : fadeOut;

  const axesSpring = spring({ frame: Math.max(0, frame - 5), fps, config: { damping: 14, mass: 0.6, stiffness: 200 } });
  const axesOpacity = interpolate(axesSpring, [0, 0.4], [0, 1], { extrapolateRight: "clamp" });
  const axesScale = interpolate(axesSpring, [0, 1], [0.95, 1]);

  const visiblePoints = getVisiblePoints(frame);
  const polylinePoints = pointsToPolyline(visiblePoints);
  const areaPath = pointsToAreaPath(visiblePoints, 362);
  const crashProgress = getCrashProgress(frame);
  const lineColor = lerpColor([0, 212, 170], [255, 107, 107], crashProgress);

  const signupSpring = spring({ frame: Math.max(0, frame - 50), fps, config: { damping: 12, mass: 0.5, stiffness: 200 } });
  const signupOpacity = interpolate(signupSpring, [0, 0.4], [0, 1], { extrapolateRight: "clamp" });
  const signupY = interpolate(signupSpring, [0, 1], [20, 0]);

  const zeroPaySpring = spring({ frame: Math.max(0, frame - 60), fps, config: { damping: 6, mass: 0.7, stiffness: 180 } });
  const zeroPayScale = interpolate(zeroPaySpring, [0, 1], [0.3, 1]);
  const zeroPayOpacity = interpolate(zeroPaySpring, [0, 0.25], [0, 1], { extrapolateRight: "clamp" });

  const glowPulse = frame >= 75 && frame <= 120
    ? interpolate(Math.sin(((frame - 75) / 20) * Math.PI * 2), [-1, 1], [0.3, 0.7], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })
    : 0;

  const chartLeft = (1920 - 860) / 2;
  const chartTop = (1080 - 480) / 2 - 40;

  return (
    <AbsoluteFill style={{ background: "linear-gradient(135deg, #0A0E14 0%, #131A24 100%)", opacity: masterOpacity, overflow: "hidden" }}>
      <AbsoluteFill style={{ background: "radial-gradient(ellipse 70% 50% at 50% 70%, rgba(255, 107, 107, 0.12) 0%, transparent 70%)", opacity: glowPulse, pointerEvents: "none" }} />

      <div style={{ position: "absolute", left: chartLeft, top: chartTop, width: 860, display: "flex", flexDirection: "column", alignItems: "center", opacity: axesOpacity, transform: `scale(${axesScale})`, transformOrigin: "center center" }}>
        <div style={{ alignSelf: "flex-start", marginLeft: 30, marginBottom: 12, opacity: signupOpacity, transform: `translateY(${signupY}px)`, fontFamily: fonts.main, fontSize: 42, fontWeight: 700, color: "#4DA3FF", letterSpacing: "0.02em", textShadow: "0 0 20px rgba(77, 163, 255, 0.5)" }}>
          10 人註冊
        </div>

        <svg width="800" height="400" viewBox="0 0 800 400" fill="none">
          {[80, 160, 240, 320].map((y) => (
            <line key={`hgrid-${y}`} x1="30" y1={y} x2="790" y2={y} stroke="rgba(255,255,255,0.08)" strokeWidth="1" strokeDasharray="4 6" />
          ))}
          {[130, 230, 330, 430, 530, 660].map((x) => (
            <line key={`vgrid-${x}`} x1={x} y1="20" x2={x} y2="370" stroke="rgba(255,255,255,0.05)" strokeWidth="1" strokeDasharray="3 7" />
          ))}
          <line x1="30" y1="20" x2="30" y2="370" stroke="rgba(255,255,255,0.2)" strokeWidth="1.5" strokeLinecap="round" />
          <line x1="30" y1="370" x2="795" y2="370" stroke="rgba(255,255,255,0.2)" strokeWidth="1.5" strokeLinecap="round" />
          <polyline points="788,364 797,370 788,376" stroke="rgba(255,255,255,0.2)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
          <text x="14" y="375" fill="rgba(255,255,255,0.25)" fontSize="14" fontFamily="monospace" textAnchor="middle">0</text>
          {visiblePoints.length >= 2 && crashProgress > 0 && (
            <path d={areaPath} fill="rgba(255, 107, 107, 0.08)" opacity={crashProgress} />
          )}
          {visiblePoints.length >= 2 && (
            <polyline points={polylinePoints} stroke={lineColor} strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round" fill="none" style={{ filter: `drop-shadow(0 0 6px ${lineColor})` }} />
          )}
          {visiblePoints.length >= 1 && (
            <circle cx={visiblePoints[visiblePoints.length - 1][0]} cy={visiblePoints[visiblePoints.length - 1][1]} r="5" fill={lineColor} style={{ filter: `drop-shadow(0 0 8px ${lineColor})` }} />
          )}
          {frame >= 62 && (
            <text x="500" y="356" fill="rgba(255, 107, 107, 0.55)" fontSize="13" fontFamily="monospace" letterSpacing="3" opacity={interpolate(frame, [62, 72], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })}>
              ─── FLATLINE ───
            </text>
          )}
        </svg>

        <div style={{ marginTop: 32, opacity: zeroPayOpacity, transform: `scale(${zeroPayScale})`, transformOrigin: "center top", fontFamily: fonts.main, fontSize: 96, fontWeight: 900, color: "#FF6B6B", letterSpacing: "-0.02em", textAlign: "center", textShadow: "0 0 40px rgba(255, 107, 107, 0.7), 0 4px 24px rgba(255, 107, 107, 0.4)", lineHeight: 1 }}>
          0 人付費
        </div>
      </div>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼