// panel-to-canvas-materialize —— miro-promo 84–92s // 侧面板表格行复选框自动逐个打勾 → 按钮按下 → 三行内容飞出面板、 // 物化成画布上三张独立卡片落位(行→卡跨容器形态迁移,尺寸/形状插值)。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig, Easing } from 'remotion'; import { G } from '../../_fixtures/Fixtures'; const PANEL_X = 1210; const PANEL_Y = 90; const PANEL_W = 620; const ROW_H = 92; const ROWS_TOP = 210; // 面板内第一行的画面 y // 行 → 卡的目标位(画布左侧区域) const TARGETS = [ { x: 150, y: 150, rot: -2 }, { x: 480, y: 420, rot: 1.5 }, { x: 180, y: 660, rot: 2 }, ]; const CARD_W = 480; const CARD_H = 240; const CHECK_FRAMES = [12, 22, 32]; // 三个复选框打勾时刻 const BUTTON_FRAME = 46; // 按钮按下 const FLY_START = [54, 60, 66]; // 三行错峰起飞 export const PanelToCanvasMaterialize: React.FC = () => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const btnPress = interpolate(frame, [BUTTON_FRAME, BUTTON_FRAME + 3, BUTTON_FRAME + 9], [0, 1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return ( {/* 画布点阵底 */} {/* 侧面板 */}
{/* 面板标题 */}
{/* 表头 */}
{/* 行槽位(行飞走后留白) */} {[0, 1, 2].map((i) => ( ))} {/* 面板底部按钮 */}
0 ? G.ink : G.side, transform: `scale(${1 - btnPress * 0.06})`, display: 'flex', alignItems: 'center', justifyContent: 'center', }} >
Add all to canvas
{/* 飞行中/落位的三张卡(行→卡形态插值) */} {[0, 1, 2].map((i) => ( ))} {/* 光标 */} ); }; // 面板内的一行:复选框自动打勾;起飞后槽位塌陷成虚线留白 const RowSlot: React.FC<{ idx: number; frame: number; fps: number }> = ({ idx, frame, fps }) => { const checkF = CHECK_FRAMES[idx]; const flyF = FLY_START[idx]; const checked = frame >= checkF; const checkPop = spring({ frame: frame - checkF, fps, config: { damping: 10, stiffness: 260 } }); const flown = frame >= flyF; return (
{!flown && ( <> {/* 复选框 */}
{checked && ( )}
)}
); }; // 行→卡:位置沿贝塞尔弧线飞、尺寸/圆角/内容布局同步插值 const FlyingCard: React.FC<{ idx: number; frame: number; fps: number }> = ({ idx, frame, fps }) => { const flyF = FLY_START[idx]; if (frame < flyF) return null; const t = spring({ frame: frame - flyF, fps, config: { damping: 16, stiffness: 60 }, durationInFrames: 34 }); // 起点:面板内该行的屏幕位置/尺寸 const sx = PANEL_X + 30; const sy = ROWS_TOP + idx * ROW_H; const sw = PANEL_W - 60; const sh = ROW_H - 12; const tgt = TARGETS[idx]; // 弧线:中点向上抬,像被"倒"出来 const mx = (sx + tgt.x) / 2; const my = Math.min(sy, tgt.y) - 170; const u = t; const x = (1 - u) * (1 - u) * sx + 2 * (1 - u) * u * mx + u * u * tgt.x; const y = (1 - u) * (1 - u) * sy + 2 * (1 - u) * u * my + u * u * tgt.y; const w = sw + (CARD_W - sw) * u; const h = sh + (CARD_H - sh) * u; const rot = tgt.rot * u; const radius = 10 + 8 * u; const shadow = interpolate(u, [0, 1], [0.08, 0.16]); // 行内容(单行水平) → 卡内容(标题+行+footer) 交叉淡化 const rowOp = Math.max(0, 1 - u * 2.2); const cardOp = Math.max(0, (u - 0.45) / 0.55); return (
{/* 行形态内容 */}
{/* 卡形态内容 */}
); }; const Cursor: React.FC<{ frame: number }> = ({ frame }) => { // 光标:从画面中部移到按钮上并停留按下 const bx = PANEL_X + PANEL_W / 2; const by = PANEL_Y + 900 - 60; const x = interpolate(frame, [8, BUTTON_FRAME - 4], [900, bx], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.quad), }); const y = interpolate(frame, [8, BUTTON_FRAME - 4], [560, by], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.quad), }); const press = interpolate(frame, [BUTTON_FRAME, BUTTON_FRAME + 3, BUTTON_FRAME + 8], [1, 0.78, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return ( ); };