// 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, }) => (
{a.name}
); 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 ; })}
); };