// ===== demos/transition/circle-match-iris/CircleMatchIris.tsx ===== // 圆心匹配光圈切(match-cut × iris-reveal 组合): // 帧 0–30:景 A(列表面板)hold,第 2 行 44px 圆形头像做两次脉冲 + 扩散光环提示"看这里"; // 帧 30–75:景 B 以 clip-path: circle(r at CX CY) 从 22px 炸开到 2100px(Easing.inOut(cubic)), // 景 B 是深色圆环图表页,圆环半径同步从 22px 长到 170px——在光圈吃满全屏前就"接住"头像的圆; // 帧 45–100:圆环描边 sweep 到 78%,中央大数字随之浮现计数;帧 100–140 全属性静止收尾(≥35f)。 // 命门:两景的圆严格同心——CX/CY 写死为 FakeDashboard B 第 2 行头像的屏幕坐标常量。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { FakeDashboard, G } from '../../_fixtures/Fixtures'; // FakeDashboard variant B 第 2 行左侧 44px 头像的圆心(手算自 fixture 布局) const CX = 308; const CY = 384.8; export const CircleMatchIris: React.FC = () => { const f = useCurrentFrame(); // ---- 景 A:头像脉冲(帧 0–30,两次呼吸) ---- const pulseT = Math.min(f, 30) / 30; const scale = f < 30 ? 1 + 0.45 * Math.abs(Math.sin(pulseT * Math.PI * 2)) : 1; // 两道扩散光环 const waves = [0, 14].map((start) => { const p = interpolate(f, [start, start + 16], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return { r: 22 + p * 40, o: f < start + 16 ? 0.85 * (1 - p) : 0 }; }); // ---- 光圈:景 B 从同一圆心炸开 ---- const irisR = interpolate(f, [30, 75], [22, 2100], { easing: Easing.inOut(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // ---- 景 B 圆环:半径从 22 长到 170,"接住"头像的圆 ---- const ringR = interpolate(f, [30, 70], [22, 170], { easing: Easing.inOut(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const ringW = interpolate(f, [30, 70], [12, 40], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); // 描边 sweep 到 78% const sweep = interpolate(f, [45, 100], [0, 0.78], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const circ = 2 * Math.PI * ringR; const num = Math.round(sweep * 100); const numOpacity = interpolate(f, [68, 88], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const furnitureOpacity = interpolate(f, [60, 85], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (