// ===== 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 && (
)}
);
};
// ===== 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 && (
)}
);
};