// ===== 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,
}) => (
);
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 (
{/* 点阵无限画布底 */}
{/* 灰阶便签环境道具 */}
{[
{ 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) => (
))}
{/* 中央大便签:Rita 打字的舞台 */}
{TYPED.slice(0, typedCount)}
50 && typedCount < TYPED.length ? (caretOn ? 1 : 0.15) : 0,
}} />
{/* 五枚光标 */}
{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
;
})}
);
};
// ===== 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 }) => (
0.6 ? `0 0 ${22 * badgeLit}px ${color}` : 'none',
}}>
{name}
);
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 (
{/* 暗场微光晕,给纯黑一点空间感 */}
{f <= 132 && (
104 ? 1 - easeInCubic(clamp01((f - 104) / 18)) : 1)} />
)}
);
};