Motion · Interação

Collab Cursor Moves

Two ways collaborative cursors perform — dialogue-duet, a two-cursor dark-stage duet (approach/circle/hand off the light/zoom into a transition), and cast-ensemble, a five-cursor ambient crowd layer (staggered entrance + drift + a typing cameo + gathering around).

1920×108016:930 fps4.72s · 142 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#interaction

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

Variantes

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

When to use

Narrative segments about collaboration/multi-user/handoff themes; A carries a pure narrative beat with no UI at all, B gives a canvas scene the "the team is here" warmth.

Intention

The library's cursors have so far only been "operating tools" (input-trigger-moves' cursor-performance is a single cursor's click performance). This card upgrades named collaborative cursors to actors: style A stages two named cursors in a pure dark scene, telling a "design-to-dev handoff" story purely through their positional choreography — no UI element at all, and viewers still follow the plot, because cursor position IS the dialogue; style B has five cursors enter staggered, then keep drifting as ambient particles for the "the team is here" feel, with one cursor stopping to type (a cameo bit). SVG cursors plus name-tag chips cost almost nothing and carry a huge narrative density — the cheapest visual proof of "collaboration" as a theme.

Duration & energy

  • Duration: A ~4.7s (140f) / B ~4.7s (140f).
  • Energy: A medium (dense narrative) / B low-medium (an ambient layer, can pad any duration).

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

库内光标一直是"操作工具"(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

Reference implementation (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

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
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 "Collab Cursor Moves" (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.0Abrir template bruto ↗
// ===== 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
—

Mais nesta categoria