Remotion LabRemotion Lab
返回模板庫

持續觸及痛點用戶

以天線發出雷達脈衝,波紋擴散至散落各處的人群,只有帶有閃電痛點標記的人被高亮並連線,視覺化「持續觸及有痛點的人」。

雷達觸及痛點人群
提示詞(可直接修改內容)
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
} from "remotion";

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

const Person: React.FC<{ x: number; y: number; color: string; opacity: number; scale: number }> = ({ x, y, color, opacity, scale }) => (
  <g opacity={opacity} transform={`translate(${x},${y}) scale(${scale}) translate(${-x},${-y})`}>
    <circle cx={x} cy={y - 18} r={10} fill={color} />
    <path d={`M ${x - 14} ${y + 18} Q ${x - 14} ${y - 4} ${x} ${y - 4} Q ${x + 14} ${y - 4} ${x + 14} ${y + 18}`} fill={color} />
  </g>
);

const Lightning: React.FC<{ x: number; y: number; opacity: number }> = ({ x, y, opacity }) => (
  <path
    d={`M ${x - 4} ${y - 12} L ${x + 2} ${y - 2} L ${x - 2} ${y - 2} L ${x + 4} ${y + 12} L ${x - 1} ${y + 1} L ${x + 3} ${y + 1} Z`}
    fill="#F59E0B"
    opacity={opacity}
  />
);

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

  const fadeIn = interpolate(frame, [0, 10], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const fadeOut = interpolate(frame, [160, 180], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
  const masterOpacity = Math.min(fadeIn, fadeOut);

  const CX = 400;
  const CY = 540;
  const towerSpring = spring({ frame: Math.max(0, frame - 5), fps, config: { damping: 12, stiffness: 80 } });

  const people = [
    { x: 900, y: 340, hasPain: true, delay: 15 },
    { x: 1050, y: 450, hasPain: false, delay: 18 },
    { x: 1200, y: 350, hasPain: true, delay: 20 },
    { x: 950, y: 600, hasPain: false, delay: 22 },
    { x: 1100, y: 700, hasPain: true, delay: 24 },
    { x: 1350, y: 550, hasPain: false, delay: 17 },
    { x: 1450, y: 400, hasPain: true, delay: 26 },
    { x: 1300, y: 720, hasPain: false, delay: 21 },
    { x: 1500, y: 620, hasPain: true, delay: 28 },
    { x: 1150, y: 540, hasPain: false, delay: 19 },
  ];

  const PULSE-START = 35;
  const PULSE-INTERVAL = 30;
  const NUM-PULSES = 5;

  const pulses = Array.from({ length: NUM-PULSES }, (_, i) => {
    const startFrame = PULSE-START + i * PULSE-INTERVAL;
    const t = Math.max(0, frame - startFrame);
    const r = interpolate(t, [0, 60], [0, 1200], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
    const opacity = interpolate(t, [0, 10, 40, 60], [0, 0.25, 0.1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
    return { r, opacity, active: t > 0 && t < 65 };
  });

  const connectionStart = 60;

  return (
    <AbsoluteFill style={{ background: "linear-gradient(135deg, #0A0E14 0%, #111825 100%)", display: "flex", alignItems: "center", justifyContent: "center", opacity: masterOpacity }}>
      <svg width={1920} height={1080} viewBox="0 0 1920 1080">
        <defs>
          <filter id="s140glow">
            <feGaussianBlur stdDeviation="4" result="blur" />
            <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
          </filter>
        </defs>

        <g opacity={towerSpring} filter="url(#s140glow)">
          <line x1={CX} y1={CY - 80} x2={CX} y2={CY + 60} stroke="#4DA3FF" strokeWidth={4} strokeLinecap="round" />
          <circle cx={CX} cy={CY - 85} r={8} fill="#4DA3FF" />
          <line x1={CX - 30} y1={CY + 60} x2={CX + 30} y2={CY + 60} stroke="#4DA3FF" strokeWidth={4} strokeLinecap="round" />
          <line x1={CX - 20} y1={CY - 50} x2={CX + 20} y2={CY - 50} stroke="#4DA3FF" strokeWidth={3} strokeLinecap="round" />
          <line x1={CX - 12} y1={CY - 20} x2={CX + 12} y2={CY - 20} stroke="#4DA3FF" strokeWidth={2.5} strokeLinecap="round" />
        </g>

        {pulses.map((p, i) =>
          p.active ? (
            <circle key={i} cx={CX} cy={CY} r={p.r} fill="none" stroke="#4DA3FF" strokeWidth={2} opacity={p.opacity} />
          ) : null,
        )}

        {people.map((p, i) => {
          const pSpring = spring({ frame: Math.max(0, frame - p.delay), fps, config: { damping: 14, stiffness: 100 } });
          const dist = Math.sqrt((p.x - CX) ** 2 + (p.y - CY) ** 2);
          const isReached = pulses.some((pulse) => pulse.active && Math.abs(pulse.r - dist) < 60);
          const connProgress = p.hasPain ? interpolate(frame - connectionStart - i * 3, [0, 20], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) : 0;
          const highlight = p.hasPain ? interpolate(frame - connectionStart - i * 3, [15, 25], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) : 0;
          const baseColor = p.hasPain ? "#F59E0B" : "rgba(255,255,255,0.25)";
          const litColor = highlight > 0.5 ? "#F59E0B" : baseColor;

          return (
            <g key={i}>
              {connProgress > 0 && p.hasPain && (
                <line x1={CX + 40} y1={CY} x2={CX + 40 + (p.x - CX - 55) * connProgress} y2={CY + (p.y - CY) * connProgress} stroke="#4DA3FF" strokeWidth={1.5} strokeDasharray="6 4" strokeDashoffset={-frame * 0.6} opacity={0.3 * connProgress} />
              )}
              {highlight > 0.5 && (
                <circle cx={p.x} cy={p.y} r={38} fill="none" stroke="#F59E0B" strokeWidth={1.5} opacity={0.25 * highlight} filter="url(#s140glow)" />
              )}
              <Person x={p.x} y={p.y} color={litColor} opacity={pSpring} scale={pSpring} />
              {p.hasPain && <Lightning x={p.x + 18} y={p.y - 35} opacity={pSpring * 0.7} />}
            </g>
          );
        })}

        {[1, 2, 3].map((n) => {
          const arcR = 30 + n * 25;
          return (
            <path key={n} d={`M ${CX + Math.cos(-0.6) * arcR} ${CY - 85 + Math.sin(-0.6) * arcR} A ${arcR} ${arcR} 0 0 1 ${CX + Math.cos(0.6) * arcR} ${CY - 85 + Math.sin(0.6) * arcR}`} fill="none" stroke="#4DA3FF" strokeWidth={2} strokeLinecap="round" opacity={0.2} />
          );
        })}
      </svg>
    </AbsoluteFill>
  );
};

登入後查看完整程式碼