// 组合:命令面板 × 深浅扫场(palette-theme-ripple)
// FakeDashboard 静置 → 整屏压暗+blur → ⌘K 面板弹落 → 模拟输入"dark"
// (灰块字符逐个弹出)→ 回车帧面板 5f 收缩成一个亮点 → 深色扫场边界
// **从该亮点位置**荡开(clip-path 圆扩张 + 亮边缘环)扫过全 UI 变深色版 → 真静止。
// 组合命门:扫场起点必须是面板收缩点(960,470),收缩完成帧=扫场起始帧
// (RIPPLE=73),从别处/别帧开始就断了因果。
// 关键帧:0–15 浅色静置 → 15–22 压暗 blur → 22–30 面板弹落(超调)→
// 38–62 逐字输入 d/a/r/k → 68 回车 → 68–73 面板收缩成亮点 → 73–95 扫场
// 荡开 → 95–170 深色真静止 75f。深色版为手工反转 G 色板的本地 DarkDashboard。
// 帧确定,无随机源。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';
const DIM_START = 15;
const PANEL_IN = 22;
const TYPE_FRAMES = [38, 46, 54, 62]; // d a r k
const ENTER = 68;
const RIPPLE = 73; // 面板收缩完成帧 = 扫场起始帧(组合命门)
const RIPPLE_END = 95;
const ORIGIN = { x: 960, y: 470 }; // 面板中心 = 收缩点 = 扫场圆心
const MAX_R = 1250;
// 手工反转 G 色板的深色版
const D = {
bg: '#1a1a1c', panel: '#232325', line: '#3a3a38', bar: '#6f6f6d',
ink: '#e8e8e6', mid: '#7a7a78', card: '#262628', border: '#454543',
side: '#0e0e10', sideBar: '#555553',
};
const DarkCard: React.FC<{ seed: number }> = ({ seed }) => {
const titleW = 45 + ((seed * 37) % 40);
const lines = 2 + (seed % 3);
return (
{Array.from({ length: lines }).map((_, i) => (
))}
);
};
// 与 FakeDashboard variant A 布局逐像素一致的深色版——扫场读作"同一 UI 换肤"
const DarkDashboard: React.FC = () => (
{Array.from({ length: 7 }).map((_, i) => (
))}
{Array.from({ length: 6 }).map((_, i) => (
))}
);
export const PaletteThemeRipple: React.FC = () => {
const f = useCurrentFrame();
// 压暗 + blur:面板阶段生效,扫场完成后浅色层已被卸载
const dim = interpolate(f, [DIM_START, DIM_START + 7], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// 面板弹落(back-out 超调)
const panelT = interpolate(f, [PANEL_IN, PANEL_IN + 8], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.back(1.9)),
});
// 回车后 5f 收缩成一点(ease-in,越缩越快)
const shrink = interpolate(f, [ENTER, RIPPLE], [1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});
const panelMounted = f >= PANEL_IN && f < RIPPLE;
const panelScale = f < ENTER ? panelT : shrink;
const panelDropY = f < ENTER ? interpolate(panelT, [0, 1], [-120, 0]) : 0;
// 逐字输入:每个灰块 4f 内弹出
const charScale = (i: number) =>
interpolate(f, [TYPE_FRAMES[i], TYPE_FRAMES[i] + 4], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.back(2.2)),
});
const typedCount = TYPE_FRAMES.filter((t) => f >= t).length;
// 扫场:圆从收缩点荡开(ease-out,先快后缓),边缘带亮环
const r = interpolate(f, [RIPPLE, RIPPLE_END], [12, MAX_R], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const rippling = f >= RIPPLE && f < RIPPLE_END;
const done = f >= RIPPLE_END;
const ringOpacity = interpolate(f, [RIPPLE, RIPPLE_END], [0.95, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// 亮点:收缩完成前后 3f 的高光核(把"收缩点=扫场起点"钉死给观众看)
const dotOn = f >= ENTER + 2 && f < RIPPLE + 3;
return (
{/* 浅色层:扫场完成后条件卸载(真静止) */}
{!done && (
0 ? `brightness(${1 - dim * 0.45}) blur(${dim * 6}px)` : 'none' }}>
)}
{/* 深色层:从收缩点起被圆形 clip 揭开;完成后铺满且无 clip */}
{(rippling || done) && (
)}
{/* 扫场亮边缘环 */}
{rippling && (
)}
{/* 收缩亮点 */}
{dotOn && (
)}
{/* ⌘K 命令面板 */}
{panelMounted && (
{/* 输入行:⌘K 标识 + 逐字灰块 */}
⌘K
{TYPE_FRAMES.map((_, i) => (
))}
{/* 光标:帧确定闪烁(每 8f 翻转) */}
{/* 结果行:输入完成后首行加深表示选中 */}
{Array.from({ length: 3 }).map((_, i) => (
))}
)}
);
};