// 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 && (
<>
{/* 复选框 */}
>
)}
);
};
// 行→卡:位置沿贝塞尔弧线飞、尺寸/圆角/内容布局同步插值
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 (
);
};