// ===== demos/transition/transition-travel/LetterformZoom.tsx ===== import React, { useId } from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; // letterform-zoom〔转场〕:巨型标题 "DASH" 字腔透出新页面,镜头急速推进 // 字母 A 的三角字腔,洞被撑满全屏的瞬间新页面接管,残余笔画滑出画外。 // 结构:底层 FakeDashboard B 静置全屏(微 dolly);上层"米灰盖板"用 // SVG (白底 + 黑字)在字形处挖洞——洞里即透出 B;对盖板整组做 // transform-origin 对准 A 字腔中心的指数 scale 1→28(60f,前 20f 慢 // 后 40f 陡)。scale 过临界值后盖板快速退场 = 撤掉 mask,B 全屏接管。 const FS = 560; // 标题字号 // A 字腔(三角洞)中心:Helvetica Bold 度量估算—— // "DASH" 总宽 ≈ 2.833em,居中起点 x≈167,A 占 571–975 → 腔心 x≈773; // 基线 y=741(大写居中),腔心约在基线上方 0.42em → y≈508。 const ORIGIN = { x: 773, y: 508 }; const BASELINE = 741; const ZOOM_MAX = 28; const titleFont: React.CSSProperties = { fontFamily: 'Helvetica Neue, Helvetica, Arial, sans-serif', fontWeight: 900, fontSize: FS, }; export const LetterformZoom: React.FC = () => { const frame = useCurrentFrame(); // mask ID 按实例生成,多实例同场不串引(useId 的 «:» 在 url() 里非法,需清洗) const cutId = `lfz-cut-${useId().replace(/[^a-zA-Z0-9]/g, '')}`; // 0–25f 建立 hold;25–85f 推进。慢起 bezier 叠指数尺度 = 前段慢、后段陡 const t = interpolate(frame, [25, 85], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.6, 0, 0.85, 0.5), }); const scale = Math.pow(ZOOM_MAX, t); // 指数推进:等比的"穿越"速度感 // scale 过临界值(洞已撑满画面中部)→ 盖板残余笔画边外飞边撤场(撤 mask) const plateOpacity = interpolate(scale, [15, 24], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 速度模糊:期望视觉模糊量随推进升高;CSS filter 会被 transform 放大, // 故除以 scale 补偿 const visBlur = interpolate(t, [0, 0.45, 1], [0, 1.5, 16], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const blurCss = visBlur / scale; // 新页面微 dolly:推进期跟着轻推 1→1.1,接管后 25f 内落定回 1(收势) const bScale = frame < 85 ? interpolate(t, [0, 1], [1, 1.1]) : interpolate(frame, [85, 110], [1.1, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); return ( {/* 新页面:先只在字腔里透出,接管后全屏(110–140f 静止收尾) */}
{/* 米灰盖板(字形挖洞)+ 字缘描边 + 副标灰条:整组指数推进后飞出画外 */} {plateOpacity > 0 && (
0.02 ? `blur(${blurCss}px)` : undefined, }} > DASH {/* 字缘细描边:让"洞"的轮廓在米灰上读得清 */} DASH {/* 副标灰条:随盖板一起被甩出画外,强化"页面元素残余"感 */}
)} ); }; // ===== demos/transition/transition-travel/SharedElementMorph.tsx ===== import React from 'react'; import { AbsoluteFill, Easing, interpolate, useCurrentFrame } from 'remotion'; import { FakeDashboard, Card, G } from '../../_fixtures/Fixtures'; // shared-element-morph〔转场〕:全屏特写面板收缩、位移、长出圆角, // 严丝合缝飞落进 dashboard 网格里它所属的卡片槽位,落座带 3% 过冲。 // 观众感觉是同一个物体被镜头送回原位(FLIP 共享元素转场)。 // // 目标槽位 = FakeDashboard A 中列第二行那格(seed 5),按 fixtures 布局精确推算: // 侧栏 220 + 网格 padding 36 → 列宽 (1920-220-72-56)/3 = 524 // 头部 72 + padding 36 → 行高 (1008-72-28)/2 = 454 // 中列 x = 220+36+524+28 = 808;第二行 y = 72+36+454+28 = 590 const SLOT = { x: 808, y: 590, w: 524, h: 454, r: 14, seed: 5 }; const FULL = { x: 0, y: 0, w: 1920, h: 1080, r: 0 }; // 节拍:0–35 全屏特写 hold | 35–60 morph 25f(bezier 0.4,0,0.2,1 → 1.03) // 60–70 过冲回弹落座 | 70–130 静止收尾 const MORPH_START = 35; const MORPH_END = 60; const SETTLE_END = 70; const lerp = (a: number, b: number, t: number) => a + (b - a) * t; export const SharedElementMorph: React.FC = () => { const frame = useCurrentFrame(); // 位置/尺寸/圆角共用同一条进度曲线:25f 冲到 1.03,再 10f 弹回 1 const drive = interpolate(frame, [MORPH_START, MORPH_END], [0, 1.03], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.4, 0, 0.2, 1), }); const settle = interpolate(frame, [MORPH_END, SETTLE_END], [1.03, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); const p = frame <= MORPH_END ? drive : settle; const x = lerp(FULL.x, SLOT.x, p); const y = lerp(FULL.y, SLOT.y, p); const w = lerp(FULL.w, SLOT.w, p); const h = lerp(FULL.h, SLOT.h, p); const r = lerp(FULL.r, SLOT.r, p); // 内容是同一张卡:按目标尺寸渲染,等比放大铺满全屏(全屏 = 这张卡的特写), // 收缩过程中内容随容器一起缩小,强化"同一个物体"的感知 const contentScale = w / SLOT.w; // 投影随尺寸缩小:悬空时大而深,落座后收敛到 fixtures 卡片原生投影量级 const ps = Math.min(Math.max(p, 0), 1); const shadowY = lerp(36, 2, ps); const shadowBlur = lerp(110, 8, ps); const shadowAlpha = lerp(0.32, 0.06, ps); // 背景 dashboard 先 0.9 透明度待命,落座瞬间提到 1 const bgOpacity = interpolate(frame, [MORPH_END - 2, MORPH_END + 3], [0.9, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); return (
{/* 共享元素:全屏特写 → 精确飞落进槽位 */}
); };