// ===== demos/transition/wipe-transitions/BlindsSlice.tsx ===== // blinds-slice|百叶窗切条错峰擦除 // FakeDashboard A → B。12 根 160px 竖条,从左到右 delay=列号×2f, // 每条 10f 内完成翻换:条内 A scaleX 1→0(origin 左缘)与 B scaleX 0→1 // (origin 右缘)共用同一进度 p(Easing.in(cubic)),交接缝恒等于 // x+160(1-p),数学上无露底。缝上亮线(柔光+暗描边+白核)随波扫过。 // 波 20–52f;52f 起摘罩(整页 B 直出、条结构与亮线全部卸载), // 52–150f 真静止 98f ≥ 40f。帧确定,无随机源。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion'; import { FakeDashboard } from '../../_fixtures/Fixtures'; const STRIPS = 12; const W = 160; // 每条宽 12×160 = 1920 const WAVE_START = 20; const STAGGER = 2; // 列号 × 2f const FLIP = 10; // 每条 10f 完成翻换 const WAVE_END = WAVE_START + (STRIPS - 1) * STAGGER + FLIP; // 52 // 条内某页的切片:外层 160 宽裁剪,内层整页 1920 负 margin 对位 const Slice: React.FC<{ x: number; variant: 'A' | 'B' }> = ({ x, variant }) => (
); export const BlindsSlice: React.FC = () => { const frame = useCurrentFrame(); // 摘罩:波完成后条结构全部卸载,B 整页直出 if (frame >= WAVE_END) { return ( ); } const seams: { x: number; opacity: number }[] = []; const strips = Array.from({ length: STRIPS }).map((_, i) => { const x = i * W; const start = WAVE_START + i * STAGGER; const end = start + FLIP; // 未开始:纯 A 切片;已完成:纯 B 切片 if (frame < start) { return (
); } if (frame >= end) { return (
); } // 翻换中:A、B 共用同一进度 p——A 宽 160(1-p) 靠左,B 宽 160p 靠右, // 交接点恒为 x+160(1-p),无露底 const p = interpolate(frame, [start, end], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic), }); // 缝亮线:进出各 2f 线性淡入淡出 const seamOpacity = Math.min( interpolate(frame, [start, start + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), interpolate(frame, [end - 2, end], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }), ); seams.push({ x: x + W * (1 - p), opacity: seamOpacity }); return (
{/* A:向左缘收缩 */}
{/* B:从右缘展开 */}
); }); return ( {strips} {/* 缝亮线:白底判例——纯提亮不可见,柔光 + 暗描边 + 白核三层 */} {seams.length > 0 && ( {seams.map((s, i) => ( ))} )} ); }; // ===== demos/transition/wipe-transitions/ClockWipe.tsx ===== // clock-wipe|时钟扫描擦除 // FakeDashboard A → B。30–90f 一根隐形雷达指针从 12 点方向顺时针扫一圈, // B 页在上层用大扇形 clip-path polygon 逐帧张开;扫描沿带亮线(白核+暗描边+柔光)。 // 90–96f 亮线淡出,96f 起摘罩(B 直接满屏、无 clip-path、亮线卸载), // 96–150f 真静止 54f ≥ 40f。帧确定,无随机。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion'; import { FakeDashboard } from '../../_fixtures/Fixtures'; const CX = 960; const CY = 540; const R = 1400; // 大于中心到角的距离 ~1101,扇形完全盖角 const SEGS = 72; // 顶点数固定且够密,避免锯齿跳变 // 12 点方向为 0°,顺时针(屏幕坐标 y 向下) const polar = (deg: number, r: number): [number, number] => { const a = (deg * Math.PI) / 180; return [CX + r * Math.sin(a), CY - r * Math.cos(a)]; }; const fanClip = (theta: number): string => { const pts: string[] = [`${CX}px ${CY}px`]; for (let i = 0; i <= SEGS; i++) { const [x, y] = polar((theta * i) / SEGS, R); pts.push(`${x.toFixed(1)}px ${y.toFixed(1)}px`); } return `polygon(${pts.join(', ')})`; }; export const ClockWipe: React.FC = () => { const frame = useCurrentFrame(); // 30–90f 指针 0→360°,linear(时钟扫描要匀速才像雷达) const theta = interpolate(frame, [30, 90], [0, 360], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const wipeDone = frame >= 90; // 亮线:扫描期间常亮,90–96f 线性淡出,96f 起条件卸载(摘罩判例) const lineOpacity = interpolate(frame, [90, 96], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const lineMounted = frame >= 30 && frame < 96; const [x2, y2] = polar(theta, R); return ( {/* 底层:A 页。擦完后卸载(上层 B 已满屏) */} {!wipeDone && ( )} {/* 上层:B 页。扫描期挂扇形 clip-path,擦完后摘罩直出 */} {frame >= 30 && ( )} {/* 扫描亮线:柔光 + 暗描边 + 白核,从屏心指向当前角度 */} {lineMounted && ( {/* 柔光带(QA 后加码 1.5x:白底看不清就加深+加宽) */} {/* 暗描边:白底上"提亮"不可见,加深保证浅色区也读得出指针 */} {/* 白核 */} )} ); };