// 组合:命令面板 × 深浅扫场(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) => (
))}
)}
); };