// ===== demos/transition/transition-hidden-cut/InvisibleCut.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { CameraMotionBlur } from '@remotion/motion-blur'; import { FakeDashboard, Card, G } from '../../_fixtures/Fixtures'; // invisible-cut:前景遮挡隐形切——一张放大到超出画幅的卡片带重运动模糊 // 从左侧贴脸扫过,糊满屏幕的瞬间背景从 A 无痕换成 B,卡片飞出右侧时 // 观众以为还是同一镜。(invisible-cut + foreground-occlusion-swipe) const SW_START = 40; // 卡片入场 const SW_END = 54; // 卡片出场(14f 横扫) const CUT = 47; // 硬切点:扫掠中点,卡片完全糊满画面的那一帧 // 卡片中扫掠曲线上的水平位置(容器 left,未缩放坐标) const xAt = (f: number) => // 卡片 scale(1.6) 后半宽 1280:起点右缘 -120 / 终点左缘 2120,均完全出画; // 中点 f47 覆盖 -280..2280,糊满整个 1920 画幅 interpolate(f, [SW_START, SW_END], [-2200, 2600], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.7, 1), }); const Scene: React.FC = () => { const frame = useCurrentFrame(); const x = xAt(frame); // 瞬时速度(px/帧),驱动斜切与残影强度 const v = xAt(frame + 0.5) - xAt(frame - 0.5); const sweeping = frame > SW_START - 2 && frame < SW_END + 3; // 背景被"带风"轻推:A 被拖向左,切成 B 后从右侧回稳——卖同一镜错觉 const shove = frame < CUT ? interpolate(frame, [SW_START, CUT], [0, -40], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.quad), }) : interpolate(frame, [CUT, CUT + 13], [40, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); return ( {/* 背景层:硬切藏在遮挡帧内 */}
{frame < CUT ? : }
{/* 手动残影:4 层拖尾(在主卡身后),保证遮挡窗口糊满全屏 */} {sweeping && [4, 3, 2, 1].map((i) => (
))} {/* 主卡:1600x1000 放大 1.6 倍(2560x1600 超出画幅),自带 blur 加强糊感 */} {sweeping && (
)}
); }; export const InvisibleCut: React.FC = () => ( ); // ===== demos/transition/transition-hidden-cut/LightLeakBurn.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { FakeDashboard, TitleBlock, G } from '../../_fixtures/Fixtures'; // light-leak-burn〔转场〕:一团琥珀橙柔光从右上角斜扫入画,亮度顶峰时 // 吞掉旧画面约七成(高光溢出、对比度被冲淡),光峰帧后切新页藏切点, // 光退散时新页已在光下就位——比白闪柔、有方向、有温度。 // 节拍:0–25 建立旧页 hold;25–52 光斜扫入、爬向峰值;52 峰值帧切页; // 52–95 光沿对角线退出、强度衰减;95–130 新页静止 hold。 const PEAK = 52; // 光峰帧 = 藏切点 export const LightLeakBurn: React.FC = () => { const frame = useCurrentFrame(); // 光团沿对角线的位移进度:右上外 → 左下外,贯穿 25–95 const sweep = interpolate(frame, [25, 95], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.4, 0, 0.3, 1), }); // 光强包络:25–PEAK 爬升(ease-in 有蓄力感),PEAK–95 收敛(ease-out 余温) const rise = interpolate(frame, [25, PEAK], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.quad), }); const fall = interpolate(frame, [PEAK, 95], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); const intensity = frame <= PEAK ? rise : fall; // 光团中心:从画面右上外侧斜扫到左下外侧(有方向的漏光) const cx = interpolate(sweep, [0, 1], [2350, -650]); const cy = interpolate(sweep, [0, 1], [-500, 1500]); // 三团琥珀光的偏移与直径(seed 正弦哈希做微差,避免完美同心) const blobs = [0, 1, 2].map((i) => { const j1 = (Math.sin(i * 127.3) * 43758) % 1; // -1..1 小数 const j2 = (Math.sin(i * 311.7) * 27183) % 1; return { dx: i * 260 - 260 + j1 * 90, // 沿扫掠方向拖尾排布 dy: i * 200 - 200 + j2 * 70, d: 1500 + i * 450, // 直径 1500 / 1950 / 2400 color: ['#f6c878', '#e8a44a', '#d98a2b'][i], alpha: [0.95, 0.8, 0.55][i], }; }); // 峰值时页面被光冲淡:对比度降、亮度抬(旧页被"烧穿"的感觉) const pageFilter = `contrast(${1 - intensity * 0.45}) brightness(${1 + intensity * 0.35})`; // 全屏暖色罩:峰值时约 8% const warmWash = intensity * 0.08; return ( {/* 页面层:光峰帧前是旧页(网格),之后是新页(列表+标题)——切点藏在最亮处 */} {frame <= PEAK ? ( ) : ( <>
)}
{/* 全屏暖色罩:峰值 8%,让高光溢出带温度 */} {/* 琥珀漏光层:3 团大直径径向渐变,blur 90px,screen 叠加,沿对角线扫过 */} {blobs.map((b, i) => (
))} {/* 峰值核心过曝:光心一小团接近白的热核,峰值帧吞掉约七成画面 */}
); }; // ===== demos/transition/transition-hidden-cut/VersusSlam.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { FakeDashboard, TitleBlock, G } from '../../_fixtures/Fixtures'; // versus-slam 对撞开屏:左右两个半屏画面(带 78° 斜切边)从画外加速对冲, // 沿斜缝砰地撞合;撞击帧白闪 + 整机震屏指数衰减 + "VS" 字块盖章压出,结尾静止 hold。 const IMPACT = 30; // 撞击帧(前 20f 建立 hold + 10f ease-in 对冲) // 斜缝几何:78° 斜边 → 1080 高度上水平偏移 1080/tan(78°) ≈ 230px,中线 x=960 ±115 const SEAM_TOP_X = 1075; // 缝顶端 x const SEAM_BOT_X = 845; // 缝底端 x // CSS 旋转顺时针为正:缝顶端偏右(1075 > 845)→ 正角度 ≈ +12° const SEAM_DEG = (Math.atan2(SEAM_TOP_X - SEAM_BOT_X, 1080) * 180) / Math.PI; export const VersusSlam: React.FC = () => { const frame = useCurrentFrame(); // 两半屏对冲:ease-in 加速,10f 从 ±1200px 冲到位 const leftX = interpolate(frame, [20, IMPACT], [-1200, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic), }); const rightX = -leftX; // 撞击帧起:整机震屏 12px 指数衰减(约 5f 收干) const since = frame - IMPACT; const env = since >= 0 ? 12 * Math.exp(-since / 1.6) : 0; const shakeX = env * Math.sin(since * 3.4); const shakeY = env * 0.6 * Math.sin(since * 4.1 + 0.7); // 白闪:撞击帧 0.9 → 0,3f 收掉 const flash = interpolate(frame, [IMPACT, IMPACT + 3], [0.9, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // "VS" 盖章:scale 1.6 → 1 带 back overshoot,6f 压出 const vsScale = interpolate(frame, [IMPACT, IMPACT + 6], [1.6, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(2.6)), }); const vsOpacity = interpolate(frame, [IMPACT, IMPACT + 2], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const impacted = frame >= IMPACT; return (
{/* 建立段的斜缝虚线预示(撞合后被实缝替代) */} {!impacted && ( )} {/* 左半屏:FakeDashboard A 裁左半,斜边 78° */}
{/* 右半屏:FakeDashboard B 裁右半 */}
{/* 撞合后的实体斜缝条 */} {impacted && (
)} {/* "VS" 字块盖章:贴缝、随缝倾斜 */} {impacted && (
)}
{/* 撞击白闪(不随震屏位移) */} ); };