// ===== demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx =====
// 组合:命令面板 × 深浅扫场(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) => (
))}
)}
);
};
// ===== demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx =====
// theme-sweep-toggle —— 深浅模式扫场
// 同一 dashboard 深浅两版叠放(深版手工映射 G 色板),上层深版用 clip-path
// polygon 15° 斜边从左上扫到右下(先快后缓),边界带 2px 亮线;
// 扫完深版整体 scale 0.995→1 "坐实"。f=70 后全静止(70f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
const CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;
// 浅色板 = Fixtures G;深色板手工反转映射
type Pal = {
bg: string; panel: string; line: string; bar: string;
mid: string; card: string; border: string; side: string; sideBar: string;
};
const LIGHT: Pal = {
bg: '#ececea', panel: '#f7f7f6', line: '#dcdcda', bar: '#c2c2c0',
mid: '#8f8f8d', card: '#ffffff', border: '#d8d8d6', side: '#3a3a3a', sideBar: '#5a5a58',
};
const DARK: Pal = {
bg: '#1c1c1b', panel: '#242423', line: '#3a3a38', bar: '#6e6e6c',
mid: '#8f8f8d', card: '#2c2c2b', border: '#454543', side: '#0f0f0e', sideBar: '#6a6a68',
};
// 带色板参数的 dashboard(结构同 Fixtures.FakeDashboard variant A)
const Dash: React.FC<{ p: Pal }> = ({ p }) => (
{Array.from({ length: 7 }).map((_, i) => (
))}
{Array.from({ length: 6 }).map((_, i) => {
const titleW = 45 + (((i + 1) * 37) % 40);
const lines = 2 + ((i + 1) % 3);
return (
{Array.from({ length: lines }).map((_, j) => (
))}
);
})}
);
// 时间轴
const SWEEP0 = 14; // 扫场开始(前 14f 初始静置)
const SWEEP1 = 52; // 扫场结束
const SETTLE0 = 52;
const SETTLE1 = 64; // 坐实结束 → 之后全静止
const SLANT = 1080 * Math.tan((15 * Math.PI) / 180); // ≈ 289px,15° 斜边
export const ThemeSweepToggle: React.FC = () => {
const frame = useCurrentFrame();
// 边界顶端 x:先快后缓(poly(3) out);从左外扫到右外+SLANT 保证底边也扫尽
const p = interpolate(frame, [SWEEP0, SWEEP1], [-20, 1920 + SLANT + 40], {
easing: Easing.out(Easing.poly(3)),
...CL,
});
// 坐实:0.995 → 1
const settle = interpolate(frame, [SETTLE0, SETTLE0 + 1, SETTLE1], [1, 0.995, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
// 亮线透明度:扫场期间可见,扫完 6f 内淡出
const lineOp = interpolate(frame, [SWEEP0, SWEEP0 + 4, SWEEP1 - 4, SWEEP1 + 2], [0, 1, 1, 0], CL);
const sweeping = frame >= SWEEP0 && frame < SWEEP1 + 2;
return (
{/* 底层浅色版 */}
{/* 上层深色版,clip-path 斜切揭出 */}
{/* 2px 亮线边界(条件卸载) */}
{sweeping && (
)}
);
};