// segmented-thumb-hero —— 分段控件 thumb 位移当叙事主角拍特写 // 源:perplexity-promo 2.3–5s。胶囊分段控件(Ask/Computer)带阴影浮入, // 超大描边箭头光标滑入点击,白 thumb 左→右 ~8f ease-out 滑动, // 到位瞬间新选项前弹出小图标、旧图标收起。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, useVideoConfig } from 'remotion'; import { G } from '../../_fixtures/Fixtures'; const FONT = 'Helvetica, Arial, sans-serif'; // 超大描边箭头光标 const ArrowCursor: React.FC<{ x: number; y: number; press: number }> = ({ x, y, press }) => ( ); const SmileLaptop: React.FC<{ size: number }> = ({ size }) => ( ); const AskIcon: React.FC<{ size: number }> = ({ size }) => ( ); export const SegmentedThumbHero: React.FC = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // ---- 时间轴 ---- const FLOAT_IN = 0; // 控件浮入 0–18 const CURSOR_IN = 20; // 光标滑入 20–44 const CLICK = 48; // 按下 const SLIDE = 52; // thumb 开始滑 52–60 (~8f) const SLIDE_END = 60; // 控件浮入:从下 160px 带阴影弹簧浮入 const floatT = spring({ frame: frame - FLOAT_IN, fps, config: { damping: 14, stiffness: 120, mass: 0.9 } }); const ctrlY = interpolate(floatT, [0, 1], [200, 0]); // 控件几何(特写尺寸) const CW = 1080; // 控件宽 const CH = 220; const PAD = 16; const SEGW = (CW - PAD * 2) / 2; // 光标滑入:ease-out 从右下画外飘到 Computer 段上 const curT = interpolate(frame, [CURSOR_IN, CURSOR_IN + 24], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); const curX = interpolate(curT, [0, 1], [1780, 1210]); const curY = interpolate(curT, [0, 1], [1020, 620]); // 按下动作 const press = interpolate(frame, [CLICK, CLICK + 3, CLICK + 7], [0, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // thumb 位移:~8f ease-out const thumbT = interpolate(frame, [SLIDE, SLIDE_END], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); const thumbX = PAD + thumbT * SEGW; // 到位瞬间:Computer 图标弹出(过冲),Ask 图标收起 const iconIn = spring({ frame: frame - SLIDE_END, fps, config: { damping: 10, stiffness: 220, mass: 0.6 } }); const laptopScale = frame >= SLIDE_END ? iconIn : 0; const askScale = interpolate(frame, [SLIDE, SLIDE + 6], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic), }); // 点击涟漪 const rippleT = interpolate(frame, [CLICK, CLICK + 12], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); const labelStyle = (active: boolean): React.CSSProperties => ({ fontFamily: FONT, fontWeight: 700, fontSize: 72, color: active ? G.ink : G.mid, transition: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 22, width: SEGW, height: CH - PAD * 2, position: 'relative', zIndex: 2, }); const askActive = thumbT < 0.5; return ( {/* 分段控件 */} {/* 白色 thumb */} {/* 点击涟漪 */} {rippleT > 0 && rippleT < 1 && ( )} {/* Ask 段 */} Ask {/* Computer 段 */} Computer ); };