{
  "slug": "theme-switch-moves",
  "title": "Theme switch: sweep and ripple",
  "tagline": "Two theme-switch moves — theme-sweep, a diagonal wipe (the skin changes wherever the boundary passes), and palette-ripple, a combo move (a ⌘K panel collapses to a point, a ripple spreads from it to re-skin).",
  "description": "Two theme-switch moves — theme-sweep, a diagonal wipe (the skin changes wherever the boundary passes), and palette-ripple, a combo move (a ⌘K panel collapses ",
  "category": "interaction",
  "tags": [
    "shotcraft",
    "remotion",
    "interaction"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/theme-switch-moves/poster-32710264d35e.jpg",
    "video": "https://gimgs.net/motion/media/theme-switch-moves/video-407c5995fbd5.mp4"
  },
  "variants": [
    {
      "key": "theme-sweep-toggle",
      "label": "theme-sweep-toggle",
      "description": "A dark-version clip-path polygon sweeps a 15° diagonal edge across the screen (out poly3, fast-then-slow, ~38f), with a 4px white bright line and an 18px glow at the boundary; the sweep fades out over 2f after finishing, and the dark version settles with a 1→0.995→1 scale pulse",
      "use": "Directly showcasing a theme feature",
      "video": "https://gimgs.net/motion/media/theme-switch-moves/video-407c5995fbd5.mp4",
      "poster": "https://gimgs.net/motion/media/theme-switch-moves/poster-32710264d35e.jpg"
    },
    {
      "key": "palette-theme-ripple",
      "label": "palette-theme-ripple",
      "description": "The panel springs down with back(1.9), text types in character by character, Enter triggers the panel to ease-in shrink to 0 with a white highlight core pinning its position, then a circular clip radius expands 12→1250px with cubic-out, its edge carrying a 5px white ring glowing in both directions",
      "use": "A highlight passage combining the command palette with the theme change",
      "video": "https://gimgs.net/motion/media/theme-switch-moves/video-62d3b3f29b6e.mp4",
      "poster": "https://gimgs.net/motion/media/theme-switch-moves/poster-palette-theme-ripple-2a734df3d738.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/theme-switch-moves.html",
  "json_url": "https://gimgs.net/motion/theme-switch-moves.json",
  "template_url": "https://gimgs.net/motion/theme-switch-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/theme-switch-moves.md",
  "updated_at": "2026-09-24T07:40:28.344Z",
  "prompt": "## When to use\n\nDark-mode/theme feature narrative passages; the same UI \"changes color before your eyes\" rather than cutting to a new scene\n\n## Intention\n\nTwo ways to shoot light/dark mode switching. A (Notion/Figma/Linear-style): a 15° diagonal boundary band with a bright line sweeps from top-left to bottom-right; where the boundary passes, light mode **changes in place** to dark mode, both themes coexisting in the same layout for an instant — the viewer sees \"the same UI changing its skin,\" not a transition. B (a combo variant): a ⌘K panel is typed \"dark\" and entered, the panel collapses to a bright point, and a dark ripple **spreads out from that point** across the whole UI — a fully readable cause-and-effect chain of \"a command triggers the re-skin.\" The linchpin for the combo: the ripple's center must be the panel's collapse point, and the start frame must be the frame the collapse completes.\n\n## Duration & energy\n\n- Duration: A: 3–4s / B: 5–6s\n- Energy: A medium / B medium-high (the combo move has a complete cause-and-effect chain)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n深浅模式切换的两种拍法。A（Notion/Figma/Linear 惯用）：15° 斜向\n边界带亮线从左上扫到右下，边界经过处浅色**就地**变深色，同布局\n双主题共存一瞬——观众看到的是\"同一个 UI 换肤\"，不是转场。\nB（组合变异）：⌘K 面板输入 \"dark\" 回车，面板收缩成一个亮点，\n深色涟漪**从该点**荡开扫过全 UI——\"命令引发换肤\"的因果链完整可读。\n组合命门：涟漪圆心必须是面板收缩点、起始帧必须是收缩完成帧。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A theme-sweep-toggle | 深色版 clip-path polygon 15° 斜边扫场（out poly3 先快后缓 ~38f），边界 4px 白亮线+18px 辉光，扫完 2f 淡出；深版 scale 1→0.995→1 坐实 | 主题功能直接展示 |\n| B palette-theme-ripple | 面板 back(1.9) 弹落→逐字输入→回车面板 ease-in 收缩到 0 + 白色高光核钉住位置→圆形 clip 半径 12→1250px cubic-out 荡开，边缘 5px 白环双向辉光 | 命令面板+主题联动的高光段 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 深色版制作 | 对色板逐键手工映射（bg/card/border 各自定深色值） | 滤镜反转出来的深色是错的（图像/头像全反） |\n| A 斜边超扫 | 底端超出 SLANT+40px | 不超扫右下角残留浅色三角 |\n| A 坐实 | scale 0.995 幅度偏微妙（demo 自评） | 实战可加大到 0.985 |\n| B 因果钉扎 | 高光核在收缩点挂 10f，涟漪圆心=该点坐标 | 圆心错位/错帧就断了\"命令→换肤\"因果（组合命门） |\n| B 浅色层 | 扫场完成后条件卸载 | 真静止前提 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 两版布局必须逐像素一致——布局有出入就读作\"切换到另一个页面\"，\n  手法即失效\n- 真实页面实战：深浅两版分别真实截图（Q1），不做滤镜假深色\n- 一支片主题切换 ≤1 次：它是功能叙事不是转场，反复切读作演示事故\n- 声音：A 扫场一声长 whoosh；B 回车一声确认 + 涟漪荡开接 whoosh\n  （S4 拟音优先）\n\n## 参考实现\ndemos/interaction/theme-switch-moves/\n（PaletteThemeRipple.tsx / ThemeSweepToggle.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx\n- demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx, demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx"
    },
    {
      "path": "demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx =====\n// 组合：命令面板 × 深浅扫场（palette-theme-ripple）\n// FakeDashboard 静置 → 整屏压暗+blur → ⌘K 面板弹落 → 模拟输入\"dark\"\n// （灰块字符逐个弹出）→ 回车帧面板 5f 收缩成一个亮点 → 深色扫场边界\n// **从该亮点位置**荡开（clip-path 圆扩张 + 亮边缘环）扫过全 UI 变深色版 → 真静止。\n// 组合命门：扫场起点必须是面板收缩点（960,470），收缩完成帧=扫场起始帧\n// （RIPPLE=73），从别处/别帧开始就断了因果。\n// 关键帧：0–15 浅色静置 → 15–22 压暗 blur → 22–30 面板弹落（超调）→\n// 38–62 逐字输入 d/a/r/k → 68 回车 → 68–73 面板收缩成亮点 → 73–95 扫场\n// 荡开 → 95–170 深色真静止 75f。深色版为手工反转 G 色板的本地 DarkDashboard。\n// 帧确定，无随机源。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, FakeDashboard } from '../../_fixtures/Fixtures';\n\nconst DIM_START = 15;\nconst PANEL_IN = 22;\nconst TYPE_FRAMES = [38, 46, 54, 62]; // d a r k\nconst ENTER = 68;\nconst RIPPLE = 73; // 面板收缩完成帧 = 扫场起始帧（组合命门）\nconst RIPPLE_END = 95;\nconst ORIGIN = { x: 960, y: 470 }; // 面板中心 = 收缩点 = 扫场圆心\nconst MAX_R = 1250;\n\n// 手工反转 G 色板的深色版\nconst D = {\n  bg: '#1a1a1c', panel: '#232325', line: '#3a3a38', bar: '#6f6f6d',\n  ink: '#e8e8e6', mid: '#7a7a78', card: '#262628', border: '#454543',\n  side: '#0e0e10', sideBar: '#555553',\n};\n\nconst DarkCard: React.FC<{ seed: number }> = ({ seed }) => {\n  const titleW = 45 + ((seed * 37) % 40);\n  const lines = 2 + (seed % 3);\n  return (\n    <div style={{ width: '100%', height: '100%', background: D.card, border: `2px solid ${D.border}`, borderRadius: 14, padding: 18, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 10 }}>\n      <div style={{ height: 16, width: `${titleW}%`, background: D.bar, borderRadius: 8 }} />\n      {Array.from({ length: lines }).map((_, i) => (\n        <div key={i} style={{ height: 10, width: `${88 - i * 14 - (seed % 5) * 3}%`, background: D.line, borderRadius: 5 }} />\n      ))}\n      <div style={{ marginTop: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>\n        <div style={{ width: 26, height: 26, borderRadius: 13, background: D.mid }} />\n        <div style={{ height: 10, width: 64, background: D.line, borderRadius: 5 }} />\n      </div>\n    </div>\n  );\n};\n\n// 与 FakeDashboard variant A 布局逐像素一致的深色版——扫场读作\"同一 UI 换肤\"\nconst DarkDashboard: React.FC = () => (\n  <div style={{ width: 1920, height: 1080, background: D.bg, display: 'flex' }}>\n    <div style={{ width: 220, background: D.side, padding: '28px 22px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 18 }}>\n      <div style={{ width: 40, height: 40, borderRadius: 10, background: '#8a8a88' }} />\n      {Array.from({ length: 7 }).map((_, i) => (\n        <div key={i} style={{ height: 12, width: `${60 + ((i * 29) % 35)}%`, background: D.sideBar, borderRadius: 6 }} />\n      ))}\n    </div>\n    <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>\n      <div style={{ height: 72, background: D.panel, borderBottom: `2px solid ${D.line}`, display: 'flex', alignItems: 'center', padding: '0 32px', gap: 20, boxSizing: 'border-box' }}>\n        <div style={{ height: 18, width: 180, background: D.bar, borderRadius: 9 }} />\n        <div style={{ marginLeft: 'auto', height: 36, width: 320, background: D.card, border: `2px solid ${D.line}`, borderRadius: 18, boxSizing: 'border-box' }} />\n        <div style={{ width: 36, height: 36, borderRadius: 18, background: D.mid }} />\n      </div>\n      <div style={{ flex: 1, padding: 36, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridAutoRows: '1fr', gap: 28, boxSizing: 'border-box' }}>\n        {Array.from({ length: 6 }).map((_, i) => (\n          <DarkCard key={i} seed={i + 1} />\n        ))}\n      </div>\n    </div>\n  </div>\n);\n\nexport const PaletteThemeRipple: React.FC = () => {\n  const f = useCurrentFrame();\n\n  // 压暗 + blur：面板阶段生效，扫场完成后浅色层已被卸载\n  const dim = interpolate(f, [DIM_START, DIM_START + 7], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // 面板弹落（back-out 超调）\n  const panelT = interpolate(f, [PANEL_IN, PANEL_IN + 8], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.back(1.9)),\n  });\n  // 回车后 5f 收缩成一点（ease-in，越缩越快）\n  const shrink = interpolate(f, [ENTER, RIPPLE], [1, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n  const panelMounted = f >= PANEL_IN && f < RIPPLE;\n  const panelScale = f < ENTER ? panelT : shrink;\n  const panelDropY = f < ENTER ? interpolate(panelT, [0, 1], [-120, 0]) : 0;\n\n  // 逐字输入：每个灰块 4f 内弹出\n  const charScale = (i: number) =>\n    interpolate(f, [TYPE_FRAMES[i], TYPE_FRAMES[i] + 4], [0, 1], {\n      extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n      easing: Easing.out(Easing.back(2.2)),\n    });\n  const typedCount = TYPE_FRAMES.filter((t) => f >= t).length;\n\n  // 扫场：圆从收缩点荡开（ease-out，先快后缓），边缘带亮环\n  const r = interpolate(f, [RIPPLE, RIPPLE_END], [12, MAX_R], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const rippling = f >= RIPPLE && f < RIPPLE_END;\n  const done = f >= RIPPLE_END;\n  const ringOpacity = interpolate(f, [RIPPLE, RIPPLE_END], [0.95, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // 亮点：收缩完成前后 3f 的高光核（把\"收缩点=扫场起点\"钉死给观众看）\n  const dotOn = f >= ENTER + 2 && f < RIPPLE + 3;\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: D.bg, position: 'relative', overflow: 'hidden' }}>\n      {/* 浅色层：扫场完成后条件卸载（真静止） */}\n      {!done && (\n        <div style={{ position: 'absolute', inset: 0, filter: dim > 0 ? `brightness(${1 - dim * 0.45}) blur(${dim * 6}px)` : 'none' }}>\n          <FakeDashboard variant=\"A\" />\n        </div>\n      )}\n\n      {/* 深色层：从收缩点起被圆形 clip 揭开；完成后铺满且无 clip */}\n      {(rippling || done) && (\n        <div style={{ position: 'absolute', inset: 0, clipPath: done ? 'none' : `circle(${r}px at ${ORIGIN.x}px ${ORIGIN.y}px)` }}>\n          <DarkDashboard />\n        </div>\n      )}\n\n      {/* 扫场亮边缘环 */}\n      {rippling && (\n        <div\n          style={{\n            position: 'absolute',\n            left: ORIGIN.x - r,\n            top: ORIGIN.y - r,\n            width: r * 2,\n            height: r * 2,\n            borderRadius: '50%',\n            border: '5px solid rgba(255,255,255,0.9)',\n            opacity: ringOpacity,\n            boxShadow: '0 0 40px rgba(255,255,255,0.5), inset 0 0 30px rgba(255,255,255,0.35)',\n          }}\n        />\n      )}\n\n      {/* 收缩亮点 */}\n      {dotOn && (\n        <div\n          style={{\n            position: 'absolute',\n            left: ORIGIN.x - 11,\n            top: ORIGIN.y - 11,\n            width: 22,\n            height: 22,\n            borderRadius: 11,\n            background: '#ffffff',\n            boxShadow: '0 0 46px 14px rgba(255,255,255,0.85)',\n          }}\n        />\n      )}\n\n      {/* ⌘K 命令面板 */}\n      {panelMounted && (\n        <div\n          style={{\n            position: 'absolute',\n            left: ORIGIN.x - 340,\n            top: ORIGIN.y - 130 + panelDropY,\n            width: 680,\n            height: 260,\n            transform: `scale(${Math.max(panelScale, 0.001)})`,\n            transformOrigin: 'center center',\n            background: G.card,\n            border: `2px solid ${G.border}`,\n            borderRadius: 18,\n            boxShadow: '0 30px 80px rgba(0,0,0,0.4)',\n            padding: 24,\n            boxSizing: 'border-box',\n            display: 'flex',\n            flexDirection: 'column',\n            gap: 16,\n          }}\n        >\n          {/* 输入行：⌘K 标识 + 逐字灰块 */}\n          <div style={{ height: 62, border: `2px solid ${G.line}`, borderRadius: 12, display: 'flex', alignItems: 'center', gap: 14, padding: '0 18px', boxSizing: 'border-box' }}>\n            <div style={{ padding: '6px 12px', borderRadius: 8, background: G.side, color: '#fff', fontFamily: 'Helvetica, Arial, sans-serif', fontWeight: 800, fontSize: 22 }}>⌘K</div>\n            {TYPE_FRAMES.map((_, i) => (\n              <div key={i} style={{ width: 30, height: 30, borderRadius: 6, background: G.ink, transform: `scale(${charScale(i)})` }} />\n            ))}\n            {/* 光标：帧确定闪烁（每 8f 翻转） */}\n            <div style={{ width: 4, height: 34, background: G.ink, opacity: Math.floor(f / 8) % 2 === 0 ? 1 : 0 }} />\n          </div>\n          {/* 结果行：输入完成后首行加深表示选中 */}\n          {Array.from({ length: 3 }).map((_, i) => (\n            <div key={i} style={{ height: 44, borderRadius: 10, background: i === 0 && typedCount === 4 ? G.line : G.panel, display: 'flex', alignItems: 'center', gap: 14, padding: '0 16px', boxSizing: 'border-box' }}>\n              <div style={{ width: 24, height: 24, borderRadius: 6, background: G.mid }} />\n              <div style={{ height: 12, width: `${34 + i * 16}%`, background: G.bar, borderRadius: 6 }} />\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n\n\n// ===== demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx =====\n// theme-sweep-toggle —— 深浅模式扫场\n// 同一 dashboard 深浅两版叠放（深版手工映射 G 色板），上层深版用 clip-path\n// polygon 15° 斜边从左上扫到右下（先快后缓），边界带 2px 亮线；\n// 扫完深版整体 scale 0.995→1 \"坐实\"。f=70 后全静止（70f）。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\n\nconst CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;\n\n// 浅色板 = Fixtures G；深色板手工反转映射\ntype Pal = {\n  bg: string; panel: string; line: string; bar: string;\n  mid: string; card: string; border: string; side: string; sideBar: string;\n};\nconst LIGHT: Pal = {\n  bg: '#ececea', panel: '#f7f7f6', line: '#dcdcda', bar: '#c2c2c0',\n  mid: '#8f8f8d', card: '#ffffff', border: '#d8d8d6', side: '#3a3a3a', sideBar: '#5a5a58',\n};\nconst DARK: Pal = {\n  bg: '#1c1c1b', panel: '#242423', line: '#3a3a38', bar: '#6e6e6c',\n  mid: '#8f8f8d', card: '#2c2c2b', border: '#454543', side: '#0f0f0e', sideBar: '#6a6a68',\n};\n\n// 带色板参数的 dashboard（结构同 Fixtures.FakeDashboard variant A）\nconst Dash: React.FC<{ p: Pal }> = ({ p }) => (\n  <div style={{ width: 1920, height: 1080, background: p.bg, display: 'flex' }}>\n    <div style={{ width: 220, background: p.side, padding: '28px 22px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 18 }}>\n      <div style={{ width: 40, height: 40, borderRadius: 10, background: p.sideBar }} />\n      {Array.from({ length: 7 }).map((_, i) => (\n        <div key={i} style={{ height: 12, width: `${60 + ((i * 29) % 35)}%`, background: p.sideBar, borderRadius: 6 }} />\n      ))}\n    </div>\n    <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>\n      <div style={{ height: 72, background: p.panel, borderBottom: `2px solid ${p.line}`, display: 'flex', alignItems: 'center', padding: '0 32px', gap: 20, boxSizing: 'border-box' }}>\n        <div style={{ height: 18, width: 180, background: p.bar, borderRadius: 9 }} />\n        <div style={{ marginLeft: 'auto', height: 36, width: 320, background: p.card, border: `2px solid ${p.line}`, borderRadius: 18, boxSizing: 'border-box' }} />\n        <div style={{ width: 36, height: 36, borderRadius: 18, background: p.mid }} />\n      </div>\n      <div style={{ flex: 1, padding: 36, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gridAutoRows: '1fr', gap: 28, boxSizing: 'border-box' }}>\n        {Array.from({ length: 6 }).map((_, i) => {\n          const titleW = 45 + (((i + 1) * 37) % 40);\n          const lines = 2 + ((i + 1) % 3);\n          return (\n            <div key={i} style={{ background: p.card, border: `2px solid ${p.border}`, borderRadius: 14, padding: 18, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 10 }}>\n              <div style={{ height: 16, width: `${titleW}%`, background: p.bar, borderRadius: 8 }} />\n              {Array.from({ length: lines }).map((_, j) => (\n                <div key={j} style={{ height: 10, width: `${88 - j * 14 - ((i + 1) % 5) * 3}%`, background: p.line, borderRadius: 5 }} />\n              ))}\n              <div style={{ marginTop: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>\n                <div style={{ width: 26, height: 26, borderRadius: 13, background: p.mid }} />\n                <div style={{ height: 10, width: 64, background: p.line, borderRadius: 5 }} />\n              </div>\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  </div>\n);\n\n// 时间轴\nconst SWEEP0 = 14; // 扫场开始（前 14f 初始静置）\nconst SWEEP1 = 52; // 扫场结束\nconst SETTLE0 = 52;\nconst SETTLE1 = 64; // 坐实结束 → 之后全静止\n\nconst SLANT = 1080 * Math.tan((15 * Math.PI) / 180); // ≈ 289px，15° 斜边\n\nexport const ThemeSweepToggle: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 边界顶端 x：先快后缓（poly(3) out）；从左外扫到右外+SLANT 保证底边也扫尽\n  const p = interpolate(frame, [SWEEP0, SWEEP1], [-20, 1920 + SLANT + 40], {\n    easing: Easing.out(Easing.poly(3)),\n    ...CL,\n  });\n\n  // 坐实：0.995 → 1\n  const settle = interpolate(frame, [SETTLE0, SETTLE0 + 1, SETTLE1], [1, 0.995, 1], {\n    easing: Easing.out(Easing.cubic),\n    ...CL,\n  });\n\n  // 亮线透明度：扫场期间可见，扫完 6f 内淡出\n  const lineOp = interpolate(frame, [SWEEP0, SWEEP0 + 4, SWEEP1 - 4, SWEEP1 + 2], [0, 1, 1, 0], CL);\n  const sweeping = frame >= SWEEP0 && frame < SWEEP1 + 2;\n\n  return (\n    <div style={{ width: 1920, height: 1080, position: 'relative', overflow: 'hidden', background: LIGHT.bg }}>\n      {/* 底层浅色版 */}\n      <Dash p={LIGHT} />\n      {/* 上层深色版，clip-path 斜切揭出 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          clipPath: `polygon(0 0, ${p}px 0, ${p - SLANT}px 1080px, 0 1080px)`,\n          transform: `scale(${settle})`,\n          transformOrigin: '50% 50%',\n        }}\n      >\n        <Dash p={DARK} />\n      </div>\n      {/* 2px 亮线边界（条件卸载） */}\n      {sweeping && (\n        <div\n          style={{\n            position: 'absolute',\n            left: p - SLANT / 2 - 2,\n            top: 540 - 620,\n            width: 4,\n            height: 1240,\n            background: '#ffffff',\n            boxShadow: '0 0 18px 4px rgba(255,255,255,0.75)',\n            transform: 'rotate(15deg)',\n            transformOrigin: '50% 50%',\n            opacity: lineOp,\n          }}\n        />\n      )}\n    </div>\n  );\n};\n\n\n"
}