Motion · Tương tác

Con trỏ cộng tác diễn xuất

Hai kiểu con trỏ cộng tác đóng vai diễn viên — dialogue-duet song vũ hai con trỏ trên nền tối (tiến lại gần/vòng vị trí/trao đèn/phóng to thành chuyển cảnh) và cast-ensemble lớp nền năm con trỏ (bay vào so le + trôi dạt sine + cameo gõ chữ + tụ lại xem).

1920×108016:930 fps4.72s · 142 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#interaction

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ể

dialogue-duet
Hai con trỏ xanh dương/xanh lá có tên: vào cảnh theo bezier → tiến lại gần nhau đối thoại (xung nhịp thở) → vòng đổi vị trí theo cung trên dưới → nhãn tên sáng-tắt trao đèn → con trỏ xanh lá easeIn phóng to hàng chục lần thành mũi tên khổng lồ dùng che màn chuyển cảnh.
Tường thuật bàn giao hai vai trò (thiết kế → phát triển, bạn → AI); cần một nhịp thở tường thuật thuần túy không UI.
cast-ensemble
5 con trỏ màu có tên bật spring vào so le trên canvas xám → trôi dạt sine hai tần số khi thường trực → một con trỏ gõ máy đánh chữ hoàn thành một dòng trên sticky note → kết thúc cả nhóm easeInOut tụ lại xem.
Lớp không khí xuyên suốt phim cho sản phẩm canvas/bảng trắng; tạo sức sống nền khi nội dung chính cần cảm giác "còn có người khác".

Khi nào dùng

Đoạn tường thuật chủ đề cộng tác/nhiều người/bàn giao; kiểu A dựng một nhịp thở tường thuật thuần túy không UI, kiểu B tạo hơi ấm "có cả đội đang ở đây" cho cảnh canvas.

Ý đồ

Con trỏ trong thư viện từ trước đến nay chỉ là "công cụ thao tác" (cursor-performance trong input-trigger-moves là màn trình diễn click của một con trỏ đơn). Thẻ này nâng cấp con trỏ cộng tác có gắn tên lên thành diễn viên: kiểu A cho hai con trỏ có tên trên nền tối thuần túy dùng biên đạo vị trí kể chuyện "bàn giao thiết kế-phát triển" — không có một phần tử UI nào, người xem vẫn hiểu cốt truyện vì vị trí con trỏ chính là lời thoại; kiểu B cho năm con trỏ vào so le rồi tiếp tục trôi dạt làm hạt không khí, cảm giác "có cả đội đang ở đây" hoàn toàn dựa vào đó, trong đó một con trỏ còn dừng lại gõ chữ (vai cameo). Con trỏ SVG cộng chip tên gọi chi phí cực thấp mà mật độ tường thuật cực cao, là bằng chứng hình ảnh rẻ nhất cho chủ đề "cộng tác".

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

  • Thời lượng: A ~4.7s (140f) / B ~4.7s (140f).
  • Năng lượng: A trung bình (mật độ tường thuật cao) / B thấp-trung bình (lớp không khí, có thể đệm mọi độ dài).

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)

意图

库内光标一直是"操作工具"(input-trigger-moves 的 cursor-performance 是单光标点击表演)。本卡把带身份名牌的协作光标升格成演员: A 式两枚具名光标在纯暗场用位移编排讲"设计-开发交接"——没有一个 UI 元素,观众照样看懂了剧情,光标位置关系就是台词;B 式五枚光标错峰 入场后持续漂移当氛围粒子,"团队在场"感全靠它,其中一枚还能停下来 打字(cameo 戏份)。SVG 光标+名牌 chip 成本极低、叙事密度极高, 是"协作"主题最便宜的视觉论证。

两式选型

| 式 | 做法 | 适用 | |----|------|------| | A dialogue-duet | 蓝/绿两枚具名光标:贝塞尔入场→相互靠近对话(脉冲呼吸)→上下弧线绕位交换→名牌一亮一暗灯光交接→绿光标 easeIn 放大数十倍成巨箭头当转场遮挡 | 双角色交接叙事(设计→开发、你→AI);需要一段无 UI 的纯叙事呼吸拍 | | B cast-ensemble | 5 枚具名彩色光标错峰 spring 飞入灰阶画布→双频正弦漂移驻场→一枚在便签上打字机补全一行→结尾全员 easeInOut 聚拢围观 | 画布/白板类产品全片氛围层;主内容之外需要"别人也在"的背景活性 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A 位移 | 全部三次贝塞尔:入场 easeOut 24f / 靠近 easeInOut 30f / 绕位 easeInOut 38f / 甩出 easeIn 24f,无 linear | 直线+linear 立即读作"程序在挪图标";曲线+缓动才是"人在动" | | A 绕位 | 两枚各走半圆弧(上弧 180°→360° vs 下弧 0°→180°)同步交换左右位,R≈270px | 同弧同向会相撞感;上下分弧是"礼让"的身体语言 | | A 灯光交接 | 名牌 opacity 0.28+0.72·lit、saturate 0.35+0.65·lit、lit>0.6 加辉光;交接在绕位中段 70–84f easeInOut 完成 | 一亮一暗是"话语权移交";两个都亮读不出主从 | | A 转场放大 | 100–134f easeIn,scale 2.8→68 冲向画外角落,名牌同步淡出 | easeIn 加速是甩出的关键;巨箭头即前景遮挡物,接 transition-hidden-cut 语法 | | B 入场 | 5 枚 delay 0/5/9/13/17f 错峰,spring(damping 15);名牌晚 12f 淡入+上浮 10px | 名牌跟光标同帧出现读作贴纸;随减速淡入才是"人到了自报家门" | | B 漂移 | 双频正弦叠加(0.055/0.021 rad/f,每枚相位不同),幅度 ±46/30px | 单频正弦读作钟摆;双频不同相才是"人手的随机" | | B 打字 cameo | 落位后 60f 线性吐字 + 光标条 8f 周期闪 | 装饰性打字可 1f/字符级;正文交互打字回 typewriter-moves 节奏 | | B 聚拢 | 92f 起 26f easeInOut 到围观位,漂移幅度衰减到 25% 保留 | 聚拢后漂移不能归零——完全静止的光标群读作死机 |

已知坑

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

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

  • 名牌颜色=身份编码,全片必须一致(蓝=Designer 就一直蓝);中途换色观众会以为换了人
  • A 式是全片一段的"戏",≤1 次(P4);B 式是可全片驻场的氛围层,两式同片可共存但 A 的两枚主角要从 B 的群演里出(同名同色),否则读作两拨人
  • B 式漂移光标永远不许盖住正在阅读的主内容——氛围层要让戏,漂移范围避开主体安全区
  • 与 input-trigger-moves 分工:那是光标"操作 UI"(点击有 payload),本卡光标"表演给你看"(位移即剧情);光标要点东西时回那张卡
  • 声音:A 绕位配轻 whoosh、交接一声柔和 chime、放大甩出一声重 whoosh;B 入场每枚一声极轻 pop,漂移段静,打字段 keyboard 轻声

参考实现

demos/interaction/collab-cursor-moves/ (CursorCastEnsemble.tsx / CursorDialogueDuet.tsx) 原片出处:A figma-devmode 0:16–0:20 / B figma-devmode 0:05 + miro-promo 全片 + pitch-app 82–90s 打字 cameo

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

  • demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx
  • demos/interaction/collab-cursor-moves/CursorDialogueDuet.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/collab-cursor-moves/CursorDialogueDuet.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/collab-cursor-moves.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx, demos/interaction/collab-cursor-moves/CursorDialogueDuet.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("collab-cursor-moves")

Prompt

Use the video-shotcraft shot recipe "Con trỏ cộng tác diễn xuất" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/collab-cursor-moves.json
Raw TSX: https://gimgs.net/motion/collab-cursor-moves/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx, demos/interaction/collab-cursor-moves/CursorDialogueDuet.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.72s

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

11,578 chars · Apache-2.0Mở template gốc ↗
// ===== demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx =====
// cursor-cast-ensemble —— figma 0:05 (cursor-badge-cast) + miro 全片 cursor-ensemble-ambience
// 5 枚具名彩色光标从画外飞入(弹簧减速+名牌淡入),落位后在灰阶画布上持续漂移、
// 指向、聚拢;灰阶便签当环境道具;其中一枚(Rita)在便签上实时打字补全一行文本
// (pitch collaborator-cameo 的打字戏并入)。
import React from 'react';
import { useCurrentFrame, spring, interpolate } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const mulberry32 = (a: number) => () => {
  let t = (a += 0x6d2b79f5);
  t = Math.imul(t ^ (t >>> 15), t | 1);
  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};

const FPS = 30;
const clamp01 = (t: number) => Math.min(1, Math.max(0, t));
const easeInOut = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);

type Actor = {
  name: string; color: string;
  from: [number, number]; home: [number, number];
  delay: number; phase: number;
};

const ACTORS: Actor[] = [
  { name: 'Lisa', color: '#4C8DF6', from: [-160, 200], home: [430, 330], delay: 0, phase: 0.0 },
  { name: 'Lucas', color: '#2FBF71', from: [2080, 160], home: [1370, 290], delay: 5, phase: 1.7 },
  { name: 'Marta', color: '#F2994A', from: [-160, 900], home: [560, 760], delay: 9, phase: 3.1 },
  { name: 'Niko', color: '#4C8DF6', from: [2080, 950], home: [1420, 780], delay: 13, phase: 4.4 },
  { name: 'Rita', color: '#2FBF71', from: [900, -180], home: [960, 545], delay: 17, phase: 5.6 },
];

const TYPED = 'Our customers love it';

const CursorActor: React.FC<{ x: number; y: number; a: Actor; badge: number; scale?: number }> = ({
  x, y, a, badge, scale = 2.1,
}) => (
  <div style={{ position: 'absolute', left: x, top: y, transform: `scale(${scale})`, transformOrigin: '0 0', zIndex: 10 }}>
    <svg width={30} height={44} viewBox="0 0 13.5 20" style={{ display: 'block', overflow: 'visible' }}>
      <path d="M0.5 0.5 L0.5 17.2 L4.7 13.4 L7.3 19.5 L10 18.3 L7.4 12.3 L13 12.3 Z"
        fill={a.color} stroke="#ffffff" strokeWidth={1.1} strokeLinejoin="round" />
    </svg>
    <div style={{
      position: 'absolute', left: 24, top: 40, whiteSpace: 'nowrap',
      background: a.color, color: '#fff', borderRadius: 7, padding: '4px 11px',
      fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700, fontSize: 14,
      opacity: badge, transform: `translateY(${(1 - badge) * 10}px)`,
    }}>
      {a.name}
    </div>
  </div>
);

export const CursorCastEnsemble: React.FC = () => {
  const f = useCurrentFrame();
  const rnd = mulberry32(88);
  // 便签的固定随机旋转
  const noteRots = Array.from({ length: 4 }).map(() => (rnd() - 0.5) * 8);

  // 聚拢时刻:90 帧后所有光标向中央便签聚拢
  const gatherT = easeInOut(clamp01((f - 92) / 26));
  const GATHER: [number, number][] = [
    [770, 430], [1160, 420], [800, 640], [1130, 650], [960, 545],
  ];

  // 打字进度(Rita 落位后开始,60–130 帧)
  const typedCount = Math.floor(interpolate(f, [58, 118], [0, TYPED.length], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  }));
  const caretOn = Math.floor(f / 8) % 2 === 0;

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      {/* 点阵无限画布底 */}
      <div style={{
        position: 'absolute', inset: 0,
        backgroundImage: `radial-gradient(${G.line} 2.4px, transparent 2.4px)`,
        backgroundSize: '46px 46px',
      }} />
      {/* 灰阶便签环境道具 */}
      {[
        { x: 250, y: 170, s: 1 }, { x: 1490, y: 140, s: 2 },
        { x: 210, y: 700, s: 3 }, { x: 1530, y: 690, s: 4 },
      ].map((n, i) => (
        <div key={i} style={{
          position: 'absolute', left: n.x, top: n.y, width: 230, height: 210,
          background: '#e6e6e2', border: `2px solid ${G.border}`, borderRadius: 6,
          boxShadow: '0 4px 14px rgba(0,0,0,0.10)', transform: `rotate(${noteRots[i]}deg)`,
          padding: 18, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12,
        }}>
          <div style={{ height: 12, width: '70%', background: G.bar, borderRadius: 6 }} />
          <div style={{ height: 10, width: '86%', background: G.line, borderRadius: 5 }} />
          <div style={{ height: 10, width: '58%', background: G.line, borderRadius: 5 }} />
        </div>
      ))}
      {/* 中央大便签:Rita 打字的舞台 */}
      <div style={{
        position: 'absolute', left: 660, top: 420, width: 600, height: 240,
        background: '#ffffff', border: `2px solid ${G.border}`, borderRadius: 12,
        boxShadow: '0 6px 22px rgba(0,0,0,0.12)', padding: 30, boxSizing: 'border-box',
      }}>
        <div style={{ height: 14, width: 180, background: G.bar, borderRadius: 7, marginBottom: 24 }} />
        <div style={{
          fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700, fontSize: 44,
          color: G.ink, letterSpacing: -0.5, minHeight: 56,
        }}>
          {TYPED.slice(0, typedCount)}
          <span style={{
            display: 'inline-block', width: 4, height: 44, background: '#2FBF71',
            marginLeft: 3, verticalAlign: 'middle',
            opacity: f > 50 && typedCount < TYPED.length ? (caretOn ? 1 : 0.15) : 0,
          }} />
        </div>
      </div>

      {/* 五枚光标 */}
      {ACTORS.map((a, i) => {
        const s = spring({ frame: f - a.delay, fps: FPS, config: { damping: 15, stiffness: 90, mass: 0.9 } });
        let x = interpolate(s, [0, 1], [a.from[0], a.home[0]]);
        let y = interpolate(s, [0, 1], [a.from[1], a.home[1]]);
        // 落位后持续漂移 + 偶尔"指向"性的小冲刺
        const settled = clamp01((f - a.delay - 26) / 10);
        const driftX = Math.sin(f * 0.055 + a.phase) * 46 + Math.sin(f * 0.021 + a.phase * 2) * 30;
        const driftY = Math.cos(f * 0.047 + a.phase * 1.3) * 38 + Math.cos(f * 0.017 + a.phase) * 24;
        x += driftX * settled * (1 - gatherT * 0.55);
        y += driftY * settled * (1 - gatherT * 0.55);
        // 聚拢
        x = interpolate(gatherT, [0, 1], [x, GATHER[i][0] + driftX * 0.25]);
        y = interpolate(gatherT, [0, 1], [y, GATHER[i][1] + driftY * 0.25]);
        const badge = clamp01((f - a.delay - 12) / 12);
        return <CursorActor key={a.name} x={x} y={y} a={a} badge={badge} />;
      })}
    </div>
  );
};


// ===== demos/interaction/collab-cursor-moves/CursorDialogueDuet.tsx =====
// cursor-dialogue-duet —— figma-devmode 0:16–0:20
// 纯暗场上 Designer(蓝)/Developer(绿) 两枚具名光标做"对话式"双人舞:
// 相互靠近 → 绕位交换 → 名牌一亮一暗交接 → 一枚放大成巨箭头当转场。
import React from 'react';
import { useCurrentFrame, interpolate } from 'remotion';

const BLUE = '#4C8DF6';
const GREEN = '#2FBF71';
const DARK = '#101012';

const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
const easeInOutCubic = (t: number) =>
  t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const easeInCubic = (t: number) => t * t * t;

const clamp01 = (t: number) => Math.min(1, Math.max(0, t));

// 三次贝塞尔单段位移
const bez = (
  t: number,
  p0: [number, number], c1: [number, number], c2: [number, number], p3: [number, number],
): [number, number] => {
  const u = 1 - t;
  const x = u * u * u * p0[0] + 3 * u * u * t * c1[0] + 3 * u * t * t * c2[0] + t * t * t * p3[0];
  const y = u * u * u * p0[1] + 3 * u * u * t * c1[1] + 3 * u * t * t * c2[1] + t * t * t * p3[1];
  return [x, y];
};

const Cursor: React.FC<{
  x: number; y: number; scale: number; color: string; name: string;
  badgeLit: number; // 0 压暗 1 点亮
  badgeOpacity?: number;
}> = ({ x, y, scale, color, name, badgeLit, badgeOpacity = 1 }) => (
  <div style={{ position: 'absolute', left: x, top: y, transform: `scale(${scale})`, transformOrigin: '0 0' }}>
    <svg width={42} height={62} viewBox="0 0 13.5 20" style={{ display: 'block', overflow: 'visible' }}>
      <path
        d="M0.5 0.5 L0.5 17.2 L4.7 13.4 L7.3 19.5 L10 18.3 L7.4 12.3 L13 12.3 Z"
        fill={color} stroke="#ffffff" strokeWidth={1.1} strokeLinejoin="round"
      />
    </svg>
    <div style={{
      position: 'absolute', left: 34, top: 56, whiteSpace: 'nowrap',
      background: color, color: '#fff', borderRadius: 8, padding: '5px 13px',
      fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 700, fontSize: 16,
      opacity: badgeOpacity * (0.28 + 0.72 * badgeLit),
      filter: `saturate(${0.35 + 0.65 * badgeLit})`,
      boxShadow: badgeLit > 0.6 ? `0 0 ${22 * badgeLit}px ${color}` : 'none',
    }}>
      {name}
    </div>
  </div>
);

export const CursorDialogueDuet: React.FC = () => {
  const f = useCurrentFrame();
  const CX = 960; const CY = 520;
  const R = 270;

  // ---------- Designer(蓝) 轨迹 ----------
  let dx = 0; let dy = 0;
  // 入场:画外左 → 左位(弹性减速)
  {
    const t = easeOutCubic(clamp01(f / 24));
    [dx, dy] = bez(t, [-140, 260], [300, 300], [520, 620], [CX - R, CY]);
  }
  // 靠近:向 Developer 探身(26–56)
  if (f > 26) {
    const t = easeInOutCubic(clamp01((f - 26) / 30));
    const [nx, ny] = bez(t, [CX - R, CY], [CX - R + 120, CY - 150], [CX - 90, CY - 120], [CX - 120, CY - 24]);
    dx = nx; dy = ny;
  }
  // 绕位:从左侧经上方绕到右侧(60–98)
  if (f > 60) {
    const t = easeInOutCubic(clamp01((f - 60) / 38));
    const a = Math.PI + t * Math.PI; // 180° → 360°(上半弧)
    dx = CX + Math.cos(a) * (R - 40) * (1 - t * 0.15) - 0;
    dy = CY + Math.sin(a) * (R - 110);
    if (t < 0.08) { // 与靠近段衔接的小混合
      const m = t / 0.08;
      dx = (CX - 120) * (1 - m) + dx * m;
      dy = (CY - 24) * (1 - m) + dy * m;
    }
  }
  // 退场:加速甩出画面左下(104–128)
  if (f > 104) {
    const t = easeInCubic(clamp01((f - 104) / 24));
    dx = interpolate(t, [0, 1], [dx, -320]);
    dy = interpolate(t, [0, 1], [dy, 1240]);
  }

  // ---------- Developer(绿) 轨迹 ----------
  let gx = 0; let gy = 0;
  {
    const t = easeOutCubic(clamp01((f - 4) / 24));
    [gx, gy] = bez(clamp01(t), [2080, 820], [1700, 760], [1420, 420], [CX + R, CY]);
  }
  // 回应:微微后仰再前倾(30–58)
  if (f > 30) {
    const t = easeInOutCubic(clamp01((f - 30) / 28));
    const sway = Math.sin(t * Math.PI) * 70;
    gx = CX + R + sway * 0.6;
    gy = CY - Math.sin(t * Math.PI * 2) * 40;
  }
  // 绕位:从右侧经下方绕到左侧(60–98)
  if (f > 60) {
    const t = easeInOutCubic(clamp01((f - 60) / 38));
    const a = 0 + t * Math.PI; // 0° → 180°(下半弧)
    gx = CX + Math.cos(a) * (R - 40);
    gy = CY + Math.sin(a) * (R - 110);
  }
  // 终场:冲向中心并放大成巨箭头(100–140)
  const blow = easeInCubic(clamp01((f - 100) / 34));
  if (f > 100) {
    gx = interpolate(blow, [0, 1], [gx, -160]);
    gy = interpolate(blow, [0, 1], [gy, -140]);
  }
  const gScale = 2.8 + blow * 66;

  // ---------- 名牌灯光交接 ----------
  // 前半 Designer 点亮,绕位中段(f≈78)交接给 Developer
  const handoff = easeInOutCubic(clamp01((f - 70) / 14));
  const dLit = f < 24 ? easeOutCubic(clamp01(f / 24)) : 1 - handoff;
  const gLit = handoff;
  const dBadgeIn = easeOutCubic(clamp01((f - 12) / 14));
  const gBadgeIn = easeOutCubic(clamp01((f - 18) / 14));

  // 对话"脉冲"——靠近对话时轻微缩放呼吸
  const dPulse = f > 26 && f < 60 ? 1 + Math.sin((f - 26) * 0.5) * 0.05 : 1;

  return (
    <div style={{ width: 1920, height: 1080, background: DARK, position: 'relative', overflow: 'hidden' }}>
      {/* 暗场微光晕,给纯黑一点空间感 */}
      <div style={{
        position: 'absolute', inset: 0,
        background: `radial-gradient(ellipse 1100px 700px at 50% 48%, rgba(255,255,255,0.045), transparent 70%)`,
      }} />
      {f <= 132 && (
        <Cursor x={dx} y={dy} scale={2.8 * dPulse} color={BLUE} name="Designer"
          badgeLit={dLit} badgeOpacity={dBadgeIn * (f > 104 ? 1 - easeInCubic(clamp01((f - 104) / 18)) : 1)} />
      )}
      <Cursor x={gx} y={gy} scale={gScale} color={GREEN} name="Developer"
        badgeLit={gLit} badgeOpacity={gBadgeIn * (1 - clamp01((f - 102) / 12))} />
    </div>
  );
};


  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/interaction/collab-cursor-moves/CursorCastEnsemble.tsx, demos/interaction/collab-cursor-moves/CursorDialogueDuet.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