Motion · Ánh sáng & nhấn

Chuyển động quầng sáng & đường bay

Ba kiểu quầng sáng & đường bay trong cảnh tối: glow-orb-ambient — nhiễu nền phát sáng, flyline-arc — đường sáng kết nối, và orb-flyline-relay — tổ hợp cộng hưởng cùng khung hình.

1920×108016:930 fps5.06s · 152 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#effects#data

Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt

Biến thể

glow-orb-ambientglow-orb-ambient · Quầng sáng nền
Ba khối quầng sáng radial 500-700px + blur(100px), trôi dạt theo hai hàm sin; quầng sáng mép thẻ được điều khiển bởi giá trị max trọng số theo khoảng cách quầng sáng [180,720]px → [1,0].
Lót nền cho đoạn tối; tạo cảm giác thẻ đang thở
flyline-arcflyline-arc · Đường sáng kết nối
Đường bezier vẽ tay lấy mẫu 100 đoạn, lớn dần trong 22f out-cubic; đầu sáng gắn có điều kiện và dẫn đường, độ mờ từng đoạn giảm dần theo khoảng cách tới đầu; điểm đến nhấp nháy viền, có thể nối tiếp tạo relay.
Hướng luồng dữ liệu/liên hệ giữa các chỉ số; kể chuyện cho dashboard
orb-flyline-relayorb-flyline-relay · Cộng hưởng quầng sáng & đường bay
Mối hàn giữa A và B: đợt bừng sáng của quầng và nhịp đập của thẻ dùng chung khung hình đáp, sáng lên 1+1.6×surge, tăng trong 5f và tan trong 15f.
Đoạn cao trào của cảnh tối; kết thúc bằng relay ba thẻ

Khi nào dùng

Bầu không khí và kể chuyện dữ liệu của đoạn nền tối duy nhất trong phim: dùng A để lót nhiễu nền, B để kể hướng luồng dữ liệu, C khi nền cần "đáp lời" tiền cảnh; chất giống trang web Linear.

Ý đồ

Đây là thẻ đầu tiên trong thư viện sống hoàn toàn trong cảnh tối. Tiền lệ "trên nền trắng không thể phát sáng thấy được" đọc ngược lại chính là: muốn chơi với ánh sáng, phải vào nền tối trước. A là hơi thở của không gian — ba quầng sáng làm mờ nặng trôi dạt hữu cơ gần nền đen, mép thẻ tối ở giữa phát sáng mạnh hơn khi quầng gần nhất tiến lại, nền không còn chết nữa; B là động từ của dữ liệu — một cung sáng "đánh" từ thẻ A sang thẻ B, điểm đầu sáng dẫn đường, đuôi sáng mờ dần, điểm đến nhấp nháy viền, lần đầu cho mối liên hệ giữa các chỉ số một hướng đi; C là mối hàn giữa A và B — khung hình mà đường bay đáp xuống làm quầng sáng gần đó bừng sáng hơn trong cùng một nhịp, lớp không khí và lớp sự kiện cộng hưởng cùng khung hình chứ không diễn riêng lẻ (chốt của tổ hợp: tách khung cộng hưởng ra là suy thoái thành hai thẻ đặt cạnh nhau).

Thời lượng & năng lượng

  • Thời lượng: A: ~5s / B: ~4.7s / C: ~5.2s
  • Năng lượng: A: Thấp (mức nhiễu nền) / B: Trung bình / C: Trung bình cao

Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)

意图

这是库内第一张整卡住在暗场的卡。白底提亮不可见判例反过来读就是: 要玩光,先去暗场。A 是空间的呼吸——近黑底三团重 blur 光斑有机漂移, 中央深卡边缘随最近光斑靠近而泛光,背景不再是死的;B 是数据的动词—— 发光弧线从 A 卡"打"到 B 卡,亮点头领跑、渐隐光尾,落点描边脉冲点亮, 指标关联第一次有了方向;C 是 A+B 的焊接——飞线落点帧邻近光斑同帧 涨亮一拍,氛围层与事件层同帧共振,互相搭腔而非各演各的(组合命门: 拆开共振帧就退化成两张卡并置)。

三式选型

| 式 | 做法 | 适用 | |----|------|------| | A glow-orb-ambient | 三团 500–700px radial 光斑 + blur(100px),双正弦漂移;卡缘辉光按光斑距离 [180,720]px→[1,0] 加权取 max 驱动 | 暗场段落铺底;卡呼吸感 | | B flyline-arc | 手写 bezier 100 段采样,22f out-cubic 生长;光头条件挂载领跑,段 opacity 按离头距离渐隐;落点描边脉冲,可接力 | 数据流向/指标关联;dashboard 叙事 | | C orb-flyline-relay | A+B 焊接:光斑 surge 与卡脉冲共用落点帧,涨亮 1+1.6×surge、5f 起升 15f 消散 | 暗场高光段;三卡接力收束 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 光斑 | 峰值 opacity 0.32/0.22/0.18;漂移振幅合计 ≥240px、周期 92–134f 交叉不等 | 同周期漂移读作整体晃动不是"有机" | | A 卡缘辉光 | box-shadow 0→28px 白 0.25 | 无辉光光斑与卡互不认识 | | A 收敛 | 90–120f out-sine 收敛冻结(起始斜率≈0.94 近似连续),末 30f 真静止 | 硬停一帧读作卡死;缓动收敛才有真静止 | | B 线体 | 白线 4.5px + 9px 深色衬底;暗场版可去衬底 | 浅底无衬底白线直接消失(demo 判例) | | B 光头 | 白点 9px + halo,仅生长期条件挂载 | opacity 到 0 不算摘罩,永不真静止 | | B 脉冲 | 描边扩散 out-cubic 6f / 消散线性 12f | 白底禁提亮——用加深罩 0.16 替代 | | C 共振 | 落点帧同帧起 surge;卡 8f 亮到 1 + 描边脉冲残余 0.3 | 错 2f 以上共振读不出"搭腔" | | C 时间轴 | 动作 f98 前结束、光斑 f120 冻结,末 35f 真静止 | 氛围层最后冻,事件层先收 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 暗场段与纸墨浅底主视觉切换成本高——全片暗场段 ≤1 段,进出

用 shot-transitions 藏切族垫,别生切明暗

  • 光斑漂移末段必须缓动收敛到静止再冻结——匀速漂移直接 clamp

是一次可见的急刹(A 式 out-sine 收敛判例)

  • 实战品牌色版:光斑用品牌色对(如蓝紫),灰阶参数只保证明度关系,

色相对比要重调

  • B 接力线 ≥3 条时改用 C——纯飞线连打读作大屏模板,共振才有作者感
  • 声音:B/C 每次落点一声轻打点,A 无声(sound-design §4.5)

参考实现

demos/effects/glow-flyline-moves/ (FlylineArc.tsx / GlowOrbAmbient.tsx / OrbFlylineRelay.tsx)

Mã nguồn tham chiếu (Remotion)

  • demos/effects/glow-flyline-moves/FlylineArc.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/glow-flyline-moves/FlylineArc.tsx
  • demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx
  • demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx

Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.

Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.

  1. Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
  2. Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
  3. Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
curl -s https://gimgs.net/motion/glow-flyline-moves.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/effects/glow-flyline-moves/FlylineArc.tsx, demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx, demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("glow-flyline-moves")

Prompt

Use the video-shotcraft shot recipe "Chuyển động quầng sáng & đường bay" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/glow-flyline-moves.json
Raw TSX: https://gimgs.net/motion/glow-flyline-moves/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/effects/glow-flyline-moves/FlylineArc.tsx, demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx, demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.06s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

19,830 chars · Apache-2.0Mở template gốc ↗
// ===== demos/effects/glow-flyline-moves/FlylineArc.tsx =====
// flyline-arc —— 飞线连接
// FakeDashboard A 微暗化(brightness 0.92),左上卡→右下卡一条贝塞尔弧线
// "打"过去(22f, out-cubic 生长),亮点头部领跑、亮头暗尾拖尾;到达帧目标卡
// 3px+ ink 描边脉冲 + 加深脉冲(白底禁提亮)。随后第二条线接力打到上中卡。
// 收尾真静止:全部动画在 f86 前结束,之后所有元素冻结。
import React, { useId } from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';

type Pt = { x: number; y: number };

// 手写 cubic bezier 采样
const bez = (p0: Pt, p1: Pt, p2: Pt, p3: Pt, t: number): Pt => {
  const u = 1 - t;
  return {
    x: u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x,
    y: u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y,
  };
};

const N = 100; // 采样段数

// 卡片几何(FakeDashboard variant A 实测网格)
const CARD_LT = { x: 256, y: 108, w: 524, h: 454, cx: 518, cy: 335 }; // 左上
const CARD_RB = { x: 1360, y: 590, w: 524, h: 454, cx: 1622, cy: 817 }; // 右下
const CARD_TM = { x: 808, y: 108, w: 524, h: 454, cx: 1070, cy: 335 }; // 上中

// 一条飞线:深色衬底 + 亮头暗尾白线分段 + 光头领跑(条件挂载)
const Flyline: React.FC<{
  frame: number;
  start: number; // 生长起始帧
  haloId: string; // 光头径向渐变 ID(父组件按实例生成)
  p0: Pt; p1: Pt; p2: Pt; p3: Pt;
}> = ({ frame, start, haloId, p0, p1, p2, p3 }) => {
  const DUR = 22;
  if (frame < start) return null;
  const e = interpolate(frame, [start, start + DUR], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const growing = frame < start + DUR;
  // 到达后 10f 内尾部亮度线性抹匀 → 之后完全静止
  const settle = interpolate(frame, [start + DUR, start + DUR + 10], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const pts: Pt[] = [];
  const nDrawn = Math.max(2, Math.ceil(e * N) + 1);
  for (let i = 0; i < nDrawn; i++) {
    const t = Math.min((i / N), e);
    pts.push(bez(p0, p1, p2, p3, t));
  }
  const head = bez(p0, p1, p2, p3, e);
  pts[pts.length - 1] = head;

  const underlay = pts.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');

  // 亮头暗尾:每段按"离头距离"给 opacity;到达后 settle 抹匀到 1
  const segs = [];
  for (let i = 0; i < pts.length - 1; i++) {
    const tSeg = Math.min(i / N, e) / Math.max(e, 0.001); // 0 尾 → 1 头
    const grad = 0.4 + 0.6 * tSeg * tSeg;
    const op = grad + (1 - grad) * settle;
    segs.push(
      <line
        key={i}
        x1={pts[i].x} y1={pts[i].y} x2={pts[i + 1].x} y2={pts[i + 1].y}
        stroke="#fafafa" strokeWidth={4.5} strokeLinecap="round" strokeOpacity={op}
      />
    );
  }

  return (
    <g>
      {/* 深色衬底:保证浅底可见 */}
      <polyline
        points={underlay} fill="none" stroke="#2f2f2f"
        strokeWidth={9} strokeLinecap="round" strokeLinejoin="round" strokeOpacity={0.92}
      />
      {segs}
      {/* 光头:仅生长期挂载,摘罩即真静止 */}
      {growing && (
        <g>
          <circle cx={head.x} cy={head.y} r={26} fill={`url(#${haloId})`} />
          <circle cx={head.x} cy={head.y} r={9} fill="#ffffff" stroke="#2f2f2f" strokeWidth={3} />
        </g>
      )}
    </g>
  );
};

// 目标卡脉冲:3px+ ink 描边 + 加深罩(白底不许提亮) —— 条件挂载
const CardPulse: React.FC<{
  frame: number;
  at: number; // 触发帧
  rect: { x: number; y: number; w: number; h: number };
}> = ({ frame, at, rect }) => {
  if (frame < at || frame > at + 18) return null;
  // 扩散(起升)用 out-cubic 6f,消散用线性 12f
  const amp =
    frame <= at + 6
      ? interpolate(frame, [at, at + 6], [0, 1], {
          easing: Easing.out(Easing.cubic),
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        })
      : interpolate(frame, [at + 6, at + 18], [1, 0], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        });
  return (
    <g>
      <rect
        x={rect.x + 2} y={rect.y + 2} width={rect.w - 4} height={rect.h - 4}
        rx={14} fill={`rgba(0,0,0,${(0.16 * amp).toFixed(3)})`}
      />
      <rect
        x={rect.x + 2} y={rect.y + 2} width={rect.w - 4} height={rect.h - 4}
        rx={14} fill="none" stroke="#2f2f2f" strokeWidth={4} strokeOpacity={amp}
      />
    </g>
  );
};

export const FlylineArc: React.FC = () => {
  const frame = useCurrentFrame();
  // 渐变 ID 按实例生成,多实例同场不串引(useId 的 «:» 在 url() 里非法,需清洗)
  const haloId = `headHalo-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;

  // 时间轴:线1 生长 10–32f → 卡RB脉冲 32–50f;线2 生长 46–68f → 卡TM脉冲 68–86f
  // f86 后全静止(140f 总长 → 静止 54f ≥ 35f)
  const L1 = { p0: { x: CARD_LT.cx, y: CARD_LT.cy }, p1: { x: 818, y: 60 }, p2: { x: 1322, y: 380 }, p3: { x: CARD_RB.cx, y: CARD_RB.cy } };
  const L2 = { p0: { x: CARD_RB.cx, y: CARD_RB.cy }, p1: { x: 1760, y: 560 }, p2: { x: 1360, y: 150 }, p3: { x: CARD_TM.cx, y: CARD_TM.cy } };

  return (
    <div style={{ width: 1920, height: 1080, position: 'relative', background: '#e0e0de' }}>
      {/* 背景微暗化 0.92,常量、不动画 */}
      <div style={{ filter: 'brightness(0.92)' }}>
        <FakeDashboard variant="A" />
      </div>
      <svg
        width={1920} height={1080} viewBox="0 0 1920 1080"
        style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
      >
        <defs>
          <radialGradient id={haloId}>
            <stop offset="0%" stopColor="rgba(0,0,0,0.38)" />
            <stop offset="55%" stopColor="rgba(0,0,0,0.18)" />
            <stop offset="100%" stopColor="rgba(0,0,0,0)" />
          </radialGradient>
        </defs>
        <CardPulse frame={frame} at={32} rect={CARD_RB} />
        <CardPulse frame={frame} at={68} rect={CARD_TM} />
        <Flyline frame={frame} start={10} haloId={haloId} {...L1} />
        <Flyline frame={frame} start={46} haloId={haloId} {...L2} />
      </svg>
    </div>
  );
};


// ===== demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx =====
// glow-orb-ambient|暗场光斑呼吸
// 近黑底上三团大光斑(radial-gradient 亮灰 + blur100)用 seed hash 驱动
// 多正弦叠加做有机漂移;中央深色描边卡的边缘辉光随最近光斑距离呼吸。
// 0–20f 光斑淡入,中段正常速度漂移,90–120f 缓动收敛到静止,末 30f 真静止。
import React from 'react';
import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion';

// 库内标准伪随机(帧确定)
const h = (n: number) => {
  const s = Math.sin(n * 127.3) * 43758.5453;
  return s - Math.floor(s);
};

const W = 1920;
const H = 1080;
const CX = W / 2;
const CY = H / 2;

type Orb = {
  size: number;      // 直径 500–700
  peak: number;      // 亮度峰值
  bx: number; by: number; // 基准中心
  p1: number; p2: number; // 两个正弦周期 90–140f
  ax1: number; ax2: number; ay1: number; ay2: number; // 振幅(合计 ≥240px)
  seed: number;
};

const ORBS: Orb[] = [
  { size: 680, peak: 0.32, bx: 600, by: 400, p1: 96, p2: 134, ax1: 170, ax2: 115, ay1: 160, ay2: 120, seed: 1 },
  { size: 580, peak: 0.22, bx: 1360, by: 560, p1: 110, p2: 92, ax1: 160, ax2: 120, ay1: 175, ay2: 105, seed: 2 },
  { size: 500, peak: 0.18, bx: 940, by: 860, p1: 128, p2: 98, ax1: 150, ax2: 125, ay1: 145, ay2: 118, seed: 3 },
];

const TAU = Math.PI * 2;

const orbPos = (o: Orb, t: number) => {
  const f1 = h(o.seed * 7 + 1) * TAU;
  const f2 = h(o.seed * 7 + 2) * TAU;
  const f3 = h(o.seed * 7 + 3) * TAU;
  const f4 = h(o.seed * 7 + 4) * TAU;
  const x = o.bx + o.ax1 * Math.sin((TAU * t) / o.p1 + f1) + o.ax2 * Math.sin((TAU * t) / o.p2 + f2);
  const y = o.by + o.ay1 * Math.sin((TAU * t) / o.p2 + f3) + o.ay2 * Math.sin((TAU * t) / o.p1 + f4);
  return { x, y };
};

export const GlowOrbAmbient: React.FC = () => {
  const f = useCurrentFrame();

  // 有效时间:0–90f 匀速,90–120f 用 out-sine 减速收敛(起始斜率≈0.94,近似连续),
  // f≥120 clamp 恒定 => 末 30f 所有位置/阴影完全静止。
  const t =
    f <= 90
      ? f
      : 90 +
        interpolate(f, [90, 120], [0, 18], {
          easing: Easing.out(Easing.sin),
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        });

  // 淡入 0–20f(在末 30f 之前早已结束)
  const fadeIn = interpolate(f, [0, 20], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  const positions = ORBS.map((o) => orbPos(o, t));

  // 卡缘呼吸:最近光斑距离 -> 辉光强度(按光斑峰值加权取最大)
  let glow = 0;
  ORBS.forEach((o, i) => {
    const d = Math.hypot(positions[i].x - CX, positions[i].y - CY);
    const p = interpolate(d, [180, 720], [1, 0], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
    });
    glow = Math.max(glow, p * (o.peak / 0.32));
  });
  const shadowBlur = 28 * glow;
  const shadowSpread = 10 * glow;
  const shadowAlpha = 0.25 * glow;

  return (
    <AbsoluteFill style={{ background: '#1d1d1b', overflow: 'hidden' }}>
      {ORBS.map((o, i) => (
        <div
          key={i}
          style={{
            position: 'absolute',
            left: positions[i].x - o.size / 2,
            top: positions[i].y - o.size / 2,
            width: o.size,
            height: o.size,
            borderRadius: '50%',
            background: `radial-gradient(circle, rgba(232,232,228,${o.peak}) 0%, rgba(232,232,228,${o.peak * 0.5}) 42%, rgba(232,232,228,0) 70%)`,
            filter: 'blur(100px)',
            opacity: fadeIn,
          }}
        />
      ))}
      {/* 中央深色描边卡 */}
      <div
        style={{
          position: 'absolute',
          left: CX - 280,
          top: CY - 165,
          width: 560,
          height: 330,
          boxSizing: 'border-box',
          background: '#262624',
          border: '1.5px solid #6a6a68',
          borderRadius: 16,
          boxShadow: `0 0 ${shadowBlur}px ${shadowSpread}px rgba(255,255,255,${shadowAlpha})`,
          padding: 32,
          display: 'flex',
          flexDirection: 'column',
          gap: 16,
        }}
      >
        <div style={{ height: 18, width: '55%', background: '#4a4a48', borderRadius: 9 }} />
        <div style={{ height: 11, width: '82%', background: '#3a3a38', borderRadius: 6 }} />
        <div style={{ height: 11, width: '68%', background: '#3a3a38', borderRadius: 6 }} />
        <div style={{ marginTop: 'auto', display: 'flex', gap: 10, alignItems: 'center' }}>
          <div style={{ width: 28, height: 28, borderRadius: 14, background: '#4a4a48' }} />
          <div style={{ height: 11, width: 90, background: '#3a3a38', borderRadius: 6 }} />
        </div>
      </div>
    </AbsoluteFill>
  );
};


// ===== demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx =====
// orb-flyline-relay|光斑飞线接力
// 组合变异 = glow-orb-ambient + flyline-arc。
// 近黑底(#1d1d1b)+ 两团 blur(100px) 灰亮光斑多正弦漂移当氛围层;
// 三张深色描边卡三角布局、初始半暗(0.55)。飞线 A→B 落点帧:卡 B 亮起
// (opacity→1 + 描边亮白脉冲) 且邻近光斑同帧涨亮一拍(组合共振证明点);
// 线二 B→C 接力,C 亮起收束。光斑 95–120f out-sine 减速收敛,
// 全部动画 f120 前结束,末 35f 真静止。
import React, { useId } from 'react';
import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion';

// 库内标准伪随机(帧确定)
const h = (n: number) => {
  const s = Math.sin(n * 127.3) * 43758.5453;
  return s - Math.floor(s);
};

const TAU = Math.PI * 2;

type Pt = { x: number; y: number };

// 手写 cubic bezier 采样
const bez = (p0: Pt, p1: Pt, p2: Pt, p3: Pt, t: number): Pt => {
  const u = 1 - t;
  return {
    x: u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x,
    y: u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y,
  };
};

const N = 100;

// ===== 布局:三张 420×250 深色卡呈三角 =====
const CARD_W = 420;
const CARD_H = 250;
const CARDS = {
  A: { x: 230, y: 170 },   // 左上(center 440, 295)
  B: { x: 1270, y: 300 },  // 右中(center 1480, 425)
  C: { x: 700, y: 740 },   // 下中(center 910, 865)
};
const center = (c: { x: number; y: number }) => ({ x: c.x + CARD_W / 2, y: c.y + CARD_H / 2 });

// ===== 光斑(氛围层)=====
type Orb = {
  size: number; peak: number;
  bx: number; by: number;
  p1: number; p2: number;
  ax1: number; ax2: number; ay1: number; ay2: number;
  seed: number;
  surgeAt: number; // 与哪个落点帧共振
};
const ORBS: Orb[] = [
  // 邻近卡 B —— 线1落点(f42)同帧涨亮
  { size: 720, peak: 0.26, bx: 1500, by: 330, p1: 104, p2: 138, ax1: 130, ax2: 95, ay1: 120, ay2: 92, seed: 1, surgeAt: 42 },
  // 邻近卡 C —— 线2落点(f76)同帧涨亮
  { size: 640, peak: 0.2, bx: 900, by: 830, p1: 122, p2: 94, ax1: 125, ax2: 88, ay1: 128, ay2: 90, seed: 2, surgeAt: 76 },
];

const orbPos = (o: Orb, t: number) => {
  const f1 = h(o.seed * 7 + 1) * TAU;
  const f2 = h(o.seed * 7 + 2) * TAU;
  const f3 = h(o.seed * 7 + 3) * TAU;
  const f4 = h(o.seed * 7 + 4) * TAU;
  const x = o.bx + o.ax1 * Math.sin((TAU * t) / o.p1 + f1) + o.ax2 * Math.sin((TAU * t) / o.p2 + f2);
  const y = o.by + o.ay1 * Math.sin((TAU * t) / o.p2 + f3) + o.ay2 * Math.sin((TAU * t) / o.p1 + f4);
  return { x, y };
};

// 涨亮一拍:起升 5f out-cubic,消散 15f 线性 → 落点帧 +20f 内结束
const surge = (frame: number, at: number) => {
  if (frame < at || frame > at + 20) return 0;
  return frame <= at + 5
    ? interpolate(frame, [at, at + 5], [0, 1], {
        easing: Easing.out(Easing.cubic),
        extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
      })
    : interpolate(frame, [at + 5, at + 20], [1, 0], {
        extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
      });
};

// ===== 发光飞线:亮头领跑 + 渐隐尾,到达后整线线性消散并摘罩 =====
const Flyline: React.FC<{
  frame: number;
  start: number; // 生长起始帧
  haloId: string; // 光头径向渐变 ID(父组件按实例生成)
  p0: Pt; p1: Pt; p2: Pt; p3: Pt;
}> = ({ frame, start, haloId, p0, p1, p2, p3 }) => {
  const DUR = 24;      // 生长
  const HOLD = 4;      // 到达后停一拍
  const FADE = 14;     // 消散(线性,帧时间解耦)
  if (frame < start || frame >= start + DUR + HOLD + FADE) return null; // 条件挂载=摘罩

  const e = interpolate(frame, [start, start + DUR], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });
  const growing = frame < start + DUR;
  const fade = interpolate(frame, [start + DUR + HOLD, start + DUR + HOLD + FADE], [1, 0], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });

  const pts: Pt[] = [];
  const nDrawn = Math.max(2, Math.ceil(e * N) + 1);
  for (let i = 0; i < nDrawn; i++) {
    const t = Math.min(i / N, e);
    pts.push(bez(p0, p1, p2, p3, t));
  }
  const head = bez(p0, p1, p2, p3, e);
  pts[pts.length - 1] = head;

  const poly = pts.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');

  // 亮头暗尾:段 opacity 随离头距离衰减(渐隐尾)
  const segs = [];
  for (let i = 0; i < pts.length - 1; i++) {
    const tSeg = Math.min(i / N, e) / Math.max(e, 0.001); // 0 尾 → 1 头
    const grad = 0.12 + 0.88 * tSeg * tSeg;
    segs.push(
      <line
        key={i}
        x1={pts[i].x} y1={pts[i].y} x2={pts[i + 1].x} y2={pts[i + 1].y}
        stroke="#f4f4f0" strokeWidth={5} strokeLinecap="round"
        strokeOpacity={grad * fade}
      />
    );
  }

  return (
    <g>
      {/* 宽幅低透明白 = 辉光衬底(暗底上可见) */}
      <polyline
        points={poly} fill="none" stroke="#e8e8e4"
        strokeWidth={14} strokeLinecap="round" strokeLinejoin="round"
        strokeOpacity={0.18 * fade}
      />
      {segs}
      {/* 亮点头领跑:仅生长期挂载 */}
      {growing && (
        <g>
          <circle cx={head.x} cy={head.y} r={34} fill={`url(#${haloId})`} />
          <circle cx={head.x} cy={head.y} r={8} fill="#ffffff" />
        </g>
      )}
    </g>
  );
};

// ===== 深色描边卡:半暗 → 落点帧亮起 + 描边亮白脉冲 =====
const DarkCard: React.FC<{
  frame: number;
  litAt: number; // 亮起帧(Infinity = 一直半暗;A 用 8 表示开场自亮)
  x: number; y: number; seed: number;
}> = ({ frame, litAt, x, y, seed }) => {
  const lit = interpolate(frame, [litAt, litAt + 8], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });
  const op = 0.55 + 0.45 * lit;
  // 描边亮白脉冲:起升 6f out-cubic → 消散 16f 线性,残余 0.3 常量
  const pulse =
    frame < litAt
      ? 0
      : frame <= litAt + 6
        ? interpolate(frame, [litAt, litAt + 6], [0, 1], {
            easing: Easing.out(Easing.cubic),
            extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
          })
        : interpolate(frame, [litAt + 6, litAt + 22], [1, 0.3], {
            extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
          });
  const borderCol = `rgba(${Math.round(90 + 165 * pulse)},${Math.round(90 + 165 * pulse)},${Math.round(88 + 164 * pulse)},1)`;
  const glow = pulse > 0 ? `0 0 ${24 * pulse}px ${6 * pulse}px rgba(255,255,255,${(0.3 * pulse).toFixed(3)})` : 'none';
  const titleW = 45 + ((seed * 37) % 40);
  return (
    <div
      style={{
        position: 'absolute', left: x, top: y, width: CARD_W, height: CARD_H,
        boxSizing: 'border-box', background: '#262624',
        border: `1.5px solid ${borderCol}`, borderRadius: 14,
        boxShadow: glow, opacity: op,
        padding: 24, display: 'flex', flexDirection: 'column', gap: 12,
      }}
    >
      <div style={{ height: 15, width: `${titleW}%`, background: '#4a4a48', borderRadius: 8 }} />
      <div style={{ height: 10, width: '80%', background: '#383836', borderRadius: 5 }} />
      <div style={{ height: 10, width: '62%', background: '#383836', borderRadius: 5 }} />
      <div style={{ marginTop: 'auto', display: 'flex', gap: 9, alignItems: 'center' }}>
        <div style={{ width: 24, height: 24, borderRadius: 12, background: '#4a4a48' }} />
        <div style={{ height: 10, width: 78, background: '#383836', borderRadius: 5 }} />
      </div>
    </div>
  );
};

export const OrbFlylineRelay: React.FC = () => {
  const frame = useCurrentFrame();
  // 渐变 ID 按实例生成,多实例同场不串引(useId 的 «:» 在 url() 里非法,需清洗)
  const haloId = `orbHeadHalo-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;

  // 光斑有效时间:0–95f 匀速漂移,95–120f out-sine 减速收敛,f≥120 恒定 → 末 35f 真静止
  const t =
    frame <= 95
      ? frame
      : 95 +
        interpolate(frame, [95, 120], [0, 15], {
          easing: Easing.out(Easing.sin),
          extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
        });

  // 光斑淡入 0–18f
  const fadeIn = interpolate(frame, [0, 18], [0, 1], {
    easing: Easing.out(Easing.cubic),
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });

  // 时间轴:卡A f8 自亮(发起) → 线1 f18–42 A→B → f42 卡B亮+光斑1涨 →
  // 线2 f52–76 B→C → f76 卡C亮+光斑2涨 → 全部动画 f98 前结束,光斑 f120 冻结
  const cA = center(CARDS.A);
  const cB = center(CARDS.B);
  const cC = center(CARDS.C);
  const L1 = { p0: cA, p1: { x: 820, y: 90 }, p2: { x: 1300, y: 180 }, p3: cB };
  const L2 = { p0: cB, p1: { x: 1620, y: 820 }, p2: { x: 1240, y: 1000 }, p3: cC };

  return (
    <AbsoluteFill style={{ background: '#1d1d1b', overflow: 'hidden' }}>
      {/* 氛围层:两团大 blur 光斑,落点帧同帧涨亮(组合共振) */}
      {ORBS.map((o, i) => {
        const pos = orbPos(o, t);
        const s = surge(frame, o.surgeAt);
        const a = Math.min(0.85, o.peak * (1 + 1.6 * s));
        return (
          <div
            key={i}
            style={{
              position: 'absolute',
              left: pos.x - o.size / 2,
              top: pos.y - o.size / 2,
              width: o.size,
              height: o.size,
              borderRadius: '50%',
              background: `radial-gradient(circle, rgba(234,234,230,${a.toFixed(3)}) 0%, rgba(234,234,230,${(a * 0.5).toFixed(3)}) 42%, rgba(234,234,230,0) 70%)`,
              filter: 'blur(100px)',
              opacity: fadeIn,
            }}
          />
        );
      })}

      {/* 三张深色描边卡:A 开场自亮发起,B/C 随落点亮起 */}
      <DarkCard frame={frame} litAt={8} x={CARDS.A.x} y={CARDS.A.y} seed={1} />
      <DarkCard frame={frame} litAt={42} x={CARDS.B.x} y={CARDS.B.y} seed={2} />
      <DarkCard frame={frame} litAt={76} x={CARDS.C.x} y={CARDS.C.y} seed={3} />

      {/* 飞线接力层 */}
      <svg
        width={1920} height={1080} viewBox="0 0 1920 1080"
        style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
      >
        <defs>
          <radialGradient id={haloId}>
            <stop offset="0%" stopColor="rgba(255,255,255,0.55)" />
            <stop offset="55%" stopColor="rgba(255,255,255,0.22)" />
            <stop offset="100%" stopColor="rgba(255,255,255,0)" />
          </radialGradient>
        </defs>
        <Flyline frame={frame} start={18} haloId={haloId} {...L1} />
        <Flyline frame={frame} start={52} haloId={haloId} {...L2} />
      </svg>
    </AbsoluteFill>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/effects/glow-flyline-moves/FlylineArc.tsx, demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx, demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

Cùng danh mục