{
  "slug": "chart-live-moves",
  "title": "Chart Live Moves",
  "tagline": "Three ways to bring a chart alive — oscilloscope-stream real-time waveform with sudden spikes, unit-dot-swarm-regroup dot swarms migrating into a number, and axis-rescale-shock a new value blowing past the axis and forcing a rescale.",
  "description": "Three ways to bring a chart alive — oscilloscope-stream real-time waveform with sudden spikes, unit-dot-swarm-regroup dot swarms migrating into a number, and ",
  "category": "data",
  "tags": [
    "shotcraft",
    "remotion",
    "data"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.38,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/chart-live-moves/poster-ac9c48f4dd70.jpg",
    "video": "https://gimgs.net/motion/media/chart-live-moves/video-70475b2a96d2.mp4"
  },
  "variants": [
    {
      "key": "oscilloscope-stream",
      "label": "oscilloscope-stream",
      "description": "The sampling window pans 8px/frame + a cosine-envelope spike + a true stop on brake.",
      "use": "Monitoring/real-time products: a system that's \"alive.\"",
      "video": "https://gimgs.net/motion/media/chart-live-moves/video-70475b2a96d2.mp4",
      "poster": "https://gimgs.net/motion/media/chart-live-moves/poster-ac9c48f4dd70.jpg"
    },
    {
      "key": "unit-dot-swarm-regroup",
      "label": "unit-dot-swarm-regroup",
      "description": "Three acts of staggered spring migration (stiffness 150, 8f stagger), the final act arranges the dots into a number.",
      "use": "User-base/composition narrative: \"every dot is a person.\"",
      "video": "https://gimgs.net/motion/media/chart-live-moves/video-59c523ff1258.mp4",
      "poster": "https://gimgs.net/motion/media/chart-live-moves/poster-unit-dot-swarm-regroup-2ef89549ed82.jpg"
    },
    {
      "key": "axis-rescale-shock",
      "label": "axis-rescale-shock",
      "description": "Bursts 220px out of frame + a triple-linked rescale (ticks swap / gridlines densify / old line compresses) + an 8px shake.",
      "use": "Growth-curve highlight: literalizing \"off the charts.\"",
      "video": "https://gimgs.net/motion/media/chart-live-moves/video-ae667829cc19.mp4",
      "poster": "https://gimgs.net/motion/media/chart-live-moves/poster-axis-rescale-shock-36cfec6efff4.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/chart-live-moves.html",
  "json_url": "https://gimgs.net/motion/chart-live-moves.json",
  "template_url": "https://gimgs.net/motion/chart-live-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/chart-live-moves.md",
  "updated_at": "2026-09-24T07:39:49.631Z",
  "prompt": "## When to use\n\nData-narrative segments; telling \"real-time,\" \"every number is a person,\" and \"growth that outgrows the chart,\" respectively.\n\n## Intention\n\nA chart isn't an illustration, it's a plot. All three share a tested precondition: it must be a real chart context — real axis labels, real data, real copy; grayscale placeholder bars can't carry a data narrative's expressiveness. A, oscilloscope-stream: the write point stays pinned to the right edge, bobbing up and down; old data flows off to the left; midway a sudden spike (2.2× amplitude, the reading jumps and turns the accent color) — \"real-time\" doesn't need to be said, because things are happening. B, unit-dot-swarm-regroup: 320 dots (each ≈ 40 customers) scatter → cluster into three groups (floating real labels: Free/Pro/Enterprise) → line up into bars (real axis labels) → converge into a dot-matrix big number \"12,847\" — the same batch of dots, so viewers can follow one dot all the way to where it belongs. C, axis-rescale-shock: the line climbs normally, then the new value punches through the chart's top edge, bursting 220px out of the card (a thick accent-colored line pierces into the title area), holds half a beat, then the y-axis \"whooshes\" through a rescale (old ticks fly out / new ticks slide in / gridlines densify / the old line compresses into a flat baseline), the new value drops back in and a real-value label pops — using \"the axis can't hold it\" to perform growth.\n\n## Duration & energy\n\n- Duration: Each 4–6s.\n- Energy: Medium-high (the data is the plot).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n图表不是插图是剧情。三式共同前提（实测判例）：**必须真图表语境**\n——真轴标/真数据/真文案，灰阶占位条测不出数据叙事的表达力。\nA 示波流线：曲线写入点钉在右端上下起伏，旧数据左流出画，中途一次\n突发尖峰（振幅 2.2×、读数跳大变强调色）——\"实时\"不用说出口，还有事件；\nB 点阵重组：320 个点（每点≈40 customers）散布→聚三簇（浮 Free/Pro/\nEnterprise 真标签）→列队成柱（真轴标）→聚成点阵大数字 \"12,847\"——\n同一批点，观众能跟着某一颗看它归队；\nC 轴爆表：折线正常爬升，新值顶破图表上沿冲出卡片 220px（强调色粗线\n插进标题区），停半拍，y 轴\"哗\"地重标（旧刻度飞出/新刻度滑入/网格\n加密/旧线压扁成地平线），新值落回+弹真值标签——用\"坐标轴装不下\"演增长。\n\n## 三式选型\n| 式 | 核心动作 | 适用 |\n|----|----------|------|\n| A oscilloscope-stream | 采样窗平移 8px/f + 余弦包络尖峰 + 刹停真静止 | 监控/实时类产品：\"活着\"的系统 |\n| B unit-dot-swarm-regroup | 三幕 spring 错峰迁徙（stiffness 150、错峰 8f），终幕点阵拼数字 | 用户量/构成叙事：\"每个点是一个人\" |\n| C axis-rescale-shock | 冲出框 220px + 重标三联动（刻度换/网格密/旧线压扁）+ 震 8px | 增长曲线高光：\"off the charts\" 字面化 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 真语境 | 真标题/真轴标/真单位/真读数 | **入库前提**：灰阶版三条全数被裁，换真语境后全过 |\n| 主角色 | 强调色（模板片为琥珀）标事件/主角（尖峰读数、Pro 簇、爆表段） | 全灰读不出\"哪里是重点\" |\n| A 流速 | 8px/f | 5.5 已可感，8 更明确；波形纯函数禁随机 |\n| B 点数/点径 | 320 / 9px | 点太少拼不出数字，太小看不见个体 |\n| C 冲出幅度 | 卡顶外 220px | 80px 版不够\"爆\"；真的插进标题区才是事故感 |\n| 收尾 | 三式都要 ≥36f 真静止 | 数据图表尾漂读作还没播完 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- 三式是三种独立手法不是套餐——同片用两式以上时各 ≤1 次且错开段落\n- A 的尖峰事件、C 的重标只能发生一次；重复即廉价\n- B 终幕数字位图字形要预先手算（点数=位点数用满才整齐）\n- 与 dataviz-landscape-open（抽象数据地景开场）分工：那是开场氛围，\n  这三式是段落内的具体数据论证\n- 声音：A 尖峰一声警示 tick；B 每幕迁徙一声 whoosh；C 冲出框\n  一声 riser、重标一声\"哗啦\"\n\n## 参考实现\ndemos/data/chart-live-moves/\n（AxisRescaleShockV2.tsx / OscilloscopeStreamV2.tsx / UnitDotSwarmRegroupV2.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/data/chart-live-moves/AxisRescaleShockV2.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/AxisRescaleShockV2.tsx\n- demos/data/chart-live-moves/OscilloscopeStreamV2.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx\n- demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.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/data/chart-live-moves/AxisRescaleShockV2.tsx, demos/data/chart-live-moves/OscilloscopeStreamV2.tsx, demos/data/chart-live-moves/UnitDotSwarmRegroupV2.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/data/chart-live-moves/AxisRescaleShockV2.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/AxisRescaleShockV2.tsx"
    },
    {
      "path": "demos/data/chart-live-moves/OscilloscopeStreamV2.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx"
    },
    {
      "path": "demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.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": 161,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/AxisRescaleShockV2.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.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/data/chart-live-moves/AxisRescaleShockV2.tsx =====\n// axis-rescale-shock-v2 —— 轴爆表重标 v2（批次 6 \"改改再看\" 重做）\n// 相对 v1 的加码：爆表点冲出卡片顶 80→220px（真的冲进标题字区域）、冲出段折线\n// 加粗 10px 且变琥珀；重标瞬间\"哗\"——旧刻度数字向下飞出淡出、新刻度从上滑入，\n// 网格 4→8 根同帧加密；真图表语境：真标题 \"Monthly revenue\"、真轴刻度\n// $25k/$50k/$75k/$100k → $100k/$200k/$300k/$400k、真月份 x 轴、端点弹真值\n// 标签 \"$340k\"；卡片震动 3→8px。收尾 f120 后真静止 40f。\n// 帧确定性：数据硬编码，全部 frame 派生，无 Math.random / Date.now。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst AMBER = '#b45309';\n\nconst CARD_W = 1060;\nconst CARD_H = 600;\nconst CX = (1920 - CARD_W) / 2;\nconst CY = (1080 - CARD_H) / 2 + 40;\nconst PAD = 52;\nconst AXIS_W = 96; // 左侧 $ 刻度位\nconst PLOT_W = CARD_W - PAD * 2 - AXIS_W;\nconst PLOT_H = 360;\nconst PLOT_X = PAD + AXIS_W;\nconst PLOT_Y = 130;\n\n// 历史数据（$k，0–100 量程内温和爬升），最后一点爆表 340\nconst DATA = [22, 30, 26, 38, 35, 47, 44, 58, 55, 66, 72, 340];\nconst N = DATA.length;\nconst MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\nconst HOLD = 12;\nconst DRAW_END = HOLD + 34; // f46：历史段画完\nconst SHOCK_END = DRAW_END + 16; // f62：爆表点冲顶\nconst BEAT = SHOCK_END + 16; // f78：停半拍（悬在标题区）\nconst RESCALE_END = BEAT + 12; // f90：重标完成\nconst MARK_END = RESCALE_END + 8; // f98：端点标记弹出\nconst VAL_END = MARK_END + 10; // f108：真值标签弹出\n\nconst easeDraw = Easing.inOut(Easing.cubic);\n\nexport const AxisRescaleShockV2: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 量程：0–100 → 0–400，重标 12f out-cubic\n  const range = interpolate(frame, [BEAT, RESCALE_END], [100, 400], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const yOf = (v: number): number => PLOT_H - (v / range) * PLOT_H;\n\n  const drawT = interpolate(frame, [HOLD, DRAW_END], [0, N - 2], {\n    easing: easeDraw,\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const shockT = interpolate(frame, [DRAW_END + 2, SHOCK_END], [0, 1], {\n    easing: Easing.in(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 冲顶停位：卡片上沿之上 220px（PLOT_Y=130 → 相对绘图区顶 -350），真冲进标题字区\n  const SHOCK_Y = -(PLOT_Y + 220);\n  const rescaleP = interpolate(frame, [BEAT, RESCALE_END], [0, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const xOf = (i: number): number => (i / (N - 1)) * PLOT_W;\n\n  // 历史段点集\n  const basePts: string[] = [];\n  const upto = Math.min(drawT, N - 2);\n  for (let i = 0; i <= Math.floor(upto); i++) basePts.push(`${xOf(i).toFixed(2)},${yOf(DATA[i]).toFixed(2)}`);\n  if (upto < N - 2 && upto > Math.floor(upto)) {\n    const i = Math.floor(upto);\n    const f = upto - i;\n    const x = xOf(i) + (xOf(i + 1) - xOf(i)) * f;\n    const y = yOf(DATA[i]) + (yOf(DATA[i + 1]) - yOf(DATA[i])) * f;\n    basePts.push(`${x.toFixed(2)},${y.toFixed(2)}`);\n  }\n\n  // 爆表段单独一根（琥珀 + 10px 加粗），起点 = 历史最后一点\n  let headX = basePts.length ? Number(basePts[basePts.length - 1].split(',')[0]) : 0;\n  let headY = basePts.length ? Number(basePts[basePts.length - 1].split(',')[1]) : 0;\n  let shockSeg: string[] = [];\n  if (shockT > 0) {\n    const x0 = xOf(N - 2);\n    const y0 = yOf(DATA[N - 2]);\n    const x = x0 + (xOf(N - 1) - x0) * shockT;\n    const yEnd = SHOCK_Y + (yOf(DATA[N - 1]) - SHOCK_Y) * rescaleP;\n    const y = y0 + (yEnd - y0) * shockT;\n    shockSeg = [`${x0.toFixed(2)},${y0.toFixed(2)}`, `${x.toFixed(2)},${y.toFixed(2)}`];\n    headX = x;\n    headY = y;\n  }\n\n  // 重标\"哗\"：旧刻度向下飞出淡出、新刻度从上滑入\n  const swap = interpolate(frame, [BEAT, BEAT + 10], [0, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const OLD_TICKS = ['$25k', '$50k', '$75k', '$100k'];\n  const NEW_TICKS = ['$100k', '$200k', '$300k', '$400k'];\n\n  // 网格密度 4→8：新增的 4 根细网格随 swap 浮现\n  const denseOp = swap;\n\n  const markS = interpolate(frame, [RESCALE_END, MARK_END], [0, 1], {\n    easing: Easing.out(Easing.back(2.2)),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const valS = interpolate(frame, [MARK_END, VAL_END], [0, 1], {\n    easing: Easing.out(Easing.back(1.8)),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 爆表瞬间卡片震（±8px，8f 内衰减归零）\n  const kick =\n    frame >= SHOCK_END && frame < SHOCK_END + 8\n      ? 8 * (1 - (frame - SHOCK_END) / 8) * Math.sin((frame - SHOCK_END) * 2.6)\n      : 0;\n\n  const shockOn = frame >= DRAW_END; // 爆表段样式生效期\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 100, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"AXIS RESCALE SHOCK V2\" size={72} />\n      </div>\n\n      <div\n        style={{\n          position: 'absolute',\n          left: CX,\n          top: CY,\n          width: CARD_W,\n          height: CARD_H,\n          background: G.card,\n          border: `2px solid ${G.border}`,\n          borderRadius: 14,\n          boxSizing: 'border-box',\n          boxShadow: '0 2px 8px rgba(0,0,0,0.06)',\n          transform: `translateY(${kick.toFixed(2)}px)`,\n          overflow: 'visible', // 让爆表段真的越出卡片\n        }}\n      >\n        {/* 真卡头 */}\n        <div style={{ position: 'absolute', left: PAD, top: 34, fontFamily: 'Helvetica, Arial, sans-serif' }}>\n          <div style={{ fontSize: 30, fontWeight: 700, color: G.ink }}>Monthly revenue</div>\n          <div style={{ fontSize: 19, fontWeight: 500, color: G.mid, marginTop: 6 }}>FY2026 · all products · USD</div>\n        </div>\n\n        <div style={{ position: 'absolute', left: PLOT_X, top: PLOT_Y, width: PLOT_W, height: PLOT_H }}>\n          {/* 基础网格 4 根 + 重标同帧加密出的 4 根 */}\n          {[0, 1, 2, 3, 4].map((i) => (\n            <div key={`g${i}`} style={{ position: 'absolute', left: 0, right: 0, top: (PLOT_H / 4) * i, height: 2, background: G.line }} />\n          ))}\n          {[1, 3, 5, 7].map((i) => (\n            <div key={`gd${i}`} style={{ position: 'absolute', left: 0, right: 0, top: (PLOT_H / 8) * i, height: 1.5, background: G.line, opacity: 0.8 * denseOp }} />\n          ))}\n\n          {/* 刻度：同一格位，旧值下飞淡出 / 新值上方滑入 */}\n          {OLD_TICKS.map((v, i) => {\n            const y = (PLOT_H / 4) * (3 - i);\n            return (\n              <div key={`t${i}`} style={{ position: 'absolute', left: -AXIS_W, top: y - 13, width: AXIS_W - 14, height: 26, textAlign: 'right' }}>\n                <div\n                  style={{\n                    position: 'absolute',\n                    inset: 0,\n                    fontFamily: 'Helvetica',\n                    fontWeight: 700,\n                    fontSize: 21,\n                    color: G.mid,\n                    textAlign: 'right',\n                    opacity: 1 - swap,\n                    transform: `translateY(${(swap * 30).toFixed(2)}px)`,\n                  }}\n                >\n                  {v}\n                </div>\n                <div\n                  style={{\n                    position: 'absolute',\n                    inset: 0,\n                    fontFamily: 'Helvetica',\n                    fontWeight: 700,\n                    fontSize: 21,\n                    color: G.ink,\n                    textAlign: 'right',\n                    opacity: swap,\n                    transform: `translateY(${((swap - 1) * 30).toFixed(2)}px)`,\n                  }}\n                >\n                  {NEW_TICKS[i]}\n                </div>\n              </div>\n            );\n          })}\n          <div style={{ position: 'absolute', left: -AXIS_W, top: PLOT_H - 13, width: AXIS_W - 14, fontFamily: 'Helvetica', fontWeight: 700, fontSize: 21, color: G.mid, textAlign: 'right' }}>$0</div>\n\n          {/* 图表上沿强调线 */}\n          <div style={{ position: 'absolute', left: 0, right: 0, top: 0, height: 3, background: G.bar }} />\n\n          <svg width={PLOT_W} height={PLOT_H} style={{ position: 'absolute', inset: 0, overflow: 'visible' }}>\n            {/* 历史段 */}\n            <polyline points={basePts.join(' ')} fill=\"none\" stroke={G.ink} strokeWidth={5} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n            {/* 爆表段：琥珀 + 10px，重标落回后收敛回常规 */}\n            {shockSeg.length > 0 && (\n              <polyline\n                points={shockSeg.join(' ')}\n                fill=\"none\"\n                stroke={AMBER}\n                strokeWidth={shockOn && frame < RESCALE_END ? 10 : 6}\n                strokeLinejoin=\"round\"\n                strokeLinecap=\"round\"\n              />\n            )}\n            {/* 端点标记 */}\n            {markS > 0 && (\n              <>\n                <circle cx={headX} cy={headY} r={13 * markS} fill={AMBER} />\n                <circle cx={headX} cy={headY} r={22 * markS} fill=\"none\" stroke={AMBER} strokeWidth={3} opacity={0.55} />\n              </>\n            )}\n          </svg>\n\n          {/* 真值标签 \"$340k\"（端点旁弹出） */}\n          {valS > 0 && (\n            <div\n              style={{\n                position: 'absolute',\n                left: headX - 178,\n                top: headY - 27,\n                width: 150,\n                height: 54,\n                background: AMBER,\n                borderRadius: 10,\n                color: '#fff',\n                fontFamily: 'Helvetica, Arial, sans-serif',\n                fontWeight: 800,\n                fontSize: 30,\n                display: 'flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                transform: `scale(${valS.toFixed(4)})`,\n                transformOrigin: 'right center',\n              }}\n            >\n              $340k\n            </div>\n          )}\n\n          {/* x 轴真月份 */}\n          {MONTHS.map((m, i) => (\n            <div\n              key={`m${i}`}\n              style={{\n                position: 'absolute',\n                left: xOf(i) - 30,\n                top: PLOT_H + 16,\n                width: 60,\n                textAlign: 'center',\n                fontFamily: 'Helvetica, Arial, sans-serif',\n                fontSize: 18,\n                fontWeight: 600,\n                color: i === N - 1 ? AMBER : G.mid,\n              }}\n            >\n              {m}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/data/chart-live-moves/OscilloscopeStreamV2.tsx =====\n// oscilloscope-stream-v2 —— 示波流线 v2（批次 6 \"改改再看\" 重做）\n// 相对 v1 的加码：流速 5.5→8px/f；写入点亮点 1.5×（7→10.5）、余辉 90→160px；\n// 流动中途插入一次突发尖峰（振幅 2.2×、持续 ~20 帧写入后随流带走），尖峰经过写入点时\n// 卡头实时读数跳大并变琥珀；真图表语境：真标题/真 y 轴刻度/真时间轴/实时读数。\n// 刹停逻辑保留：f100–112 out-cubic 刹停，f112 后真静止 48f。\n// 帧确定性：波形与尖峰包络都是纯 worldX 函数，无 Math.random / Date.now。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst AMBER = '#b45309';\n\nconst CARD_W = 1080;\nconst CARD_H = 560;\nconst CX = (1920 - CARD_W) / 2;\nconst CY = (1080 - CARD_H) / 2 + 40;\nconst PAD = 46;\nconst AXIS_W = 64; // 左侧 y 轴刻度位\nconst PLOT_X = PAD + AXIS_W;\nconst PLOT_W = CARD_W - PAD * 2 - AXIS_W; // 924\nconst PLOT_H = 300;\nconst PLOT_Y = 158;\n\nconst HOLD = 12;\nconst FREEZE_START = 100;\nconst FREEZE_END = 112;\nconst SPEED = 8; // px/frame（v1 5.5 → 8，~240px/s）\nconst AMP = 0.72; // 基础振幅占半高比例（给尖峰留出头部空间）\n\n// 突发尖峰：worldX ∈ [288, 448]（写入发生在 f48–68，之后随流向左带走）\nconst SPIKE_X0 = 288;\nconst SPIKE_W = 160;\nconst SPIKE_GAIN = 1.2; // 峰值 = 1 + 1.2 = 2.2×\n\nconst env = (x: number): number => {\n  if (x <= SPIKE_X0 || x >= SPIKE_X0 + SPIKE_W) return 1;\n  const p = (x - SPIKE_X0) / SPIKE_W;\n  return 1 + SPIKE_GAIN * (0.5 - 0.5 * Math.cos(p * Math.PI * 2));\n};\n\n// 有效时间：HOLD 前为 0，之后线性流动，FREEZE 区间 easeOut 刹停为常数\nconst effTime = (frame: number): number => {\n  const t = (f: number) => Math.max(f - HOLD, 0) * SPEED;\n  if (frame <= FREEZE_START) return t(frame);\n  const brakeDist = (t(FREEZE_END) - t(FREEZE_START)) * 0.45;\n  return (\n    t(FREEZE_START) +\n    interpolate(frame, [FREEZE_START, FREEZE_END], [0, brakeDist], {\n      easing: Easing.out(Easing.cubic),\n      extrapolateRight: 'clamp',\n    })\n  );\n};\n\n// 波形：四个正弦叠加，x 单位 = 世界像素\nconst wave = (x: number): number =>\n  0.34 * Math.sin(x * 0.021) +\n  0.27 * Math.sin(x * 0.052 + 1.7) +\n  0.18 * Math.sin(x * 0.013 + 4.2) +\n  0.12 * Math.sin(x * 0.087 + 2.3);\n\nconst signal = (x: number): number => wave(x) * env(x) * AMP; // ∈ ~[-1.6, 1.6]，常态 [-0.65, 0.65]\nconst yOf = (x: number): number => PLOT_H / 2 - signal(x) * (PLOT_H / 2);\n// 读数映射：绘图区中线 = 1,000 req/s，上下沿 = 2,000 / 0\nconst valueOf = (x: number): number => Math.round(1000 + signal(x) * 1000);\n\nconst fmt = (n: number): string => n.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n\nconst Y_TICKS = ['2.0k', '1.5k', '1.0k', '0.5k', '0'];\nconst X_TICKS = ['-60s', '-50s', '-40s', '-30s', '-20s', '-10s', 'now'];\n\nexport const OscilloscopeStreamV2: React.FC = () => {\n  const frame = useCurrentFrame();\n  const T = effTime(frame);\n\n  const N = 270;\n  const pts: string[] = [];\n  for (let i = 0; i <= N; i++) {\n    const px = (i / N) * PLOT_W;\n    const worldX = T - (PLOT_W - px);\n    pts.push(`${px.toFixed(2)},${yOf(worldX).toFixed(2)}`);\n  }\n  const headY = yOf(T);\n\n  const frozen = frame >= FREEZE_END;\n  const glowOp = interpolate(frame, [FREEZE_START, FREEZE_END], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 余辉段：头部往回 160px（v1 90 → 160）\n  const TAIL = 160;\n  const tailPts: string[] = [];\n  if (!frozen) {\n    for (let i = 0; i <= 40; i++) {\n      const px = PLOT_W - TAIL + (i / 40) * TAIL;\n      const worldX = T - (PLOT_W - px);\n      tailPts.push(`${px.toFixed(2)},${yOf(worldX).toFixed(2)}`);\n    }\n  }\n\n  // 实时读数：尖峰经过写入点时跳大 + 变琥珀\n  const spikeK = Math.min(Math.max((env(T) - 1) / SPIKE_GAIN, 0), 1);\n  const hot = spikeK > 0.22;\n  const readout = fmt(valueOf(T));\n  const readScale = 1 + 0.32 * spikeK;\n  const dotColor = spikeK > 0.15 ? AMBER : G.ink;\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"OSCILLOSCOPE STREAM V2\" size={72} />\n      </div>\n\n      <div\n        style={{\n          position: 'absolute',\n          left: CX,\n          top: CY,\n          width: CARD_W,\n          height: CARD_H,\n          background: G.card,\n          border: `2px solid ${G.border}`,\n          borderRadius: 14,\n          boxSizing: 'border-box',\n          boxShadow: '0 2px 8px rgba(0,0,0,0.06)',\n        }}\n      >\n        {/* 真卡头：标题 + 副题 + 实时读数 */}\n        <div style={{ position: 'absolute', left: PAD, top: 38, fontFamily: 'Helvetica, Arial, sans-serif' }}>\n          <div style={{ fontSize: 30, fontWeight: 700, color: G.ink }}>Requests per second</div>\n          <div style={{ fontSize: 20, fontWeight: 500, color: G.mid, marginTop: 8 }}>api-gateway · production · last 60 s</div>\n        </div>\n        <div\n          style={{\n            position: 'absolute',\n            right: PAD,\n            top: 36,\n            textAlign: 'right',\n            fontFamily: 'Helvetica, Arial, sans-serif',\n            transform: `scale(${readScale.toFixed(4)})`,\n            transformOrigin: 'right top',\n          }}\n        >\n          <div style={{ fontSize: 46, fontWeight: 800, color: hot ? AMBER : G.ink, fontVariantNumeric: 'tabular-nums', letterSpacing: -1 }}>\n            {readout}\n          </div>\n          <div style={{ fontSize: 18, fontWeight: 600, color: hot ? AMBER : G.mid, marginTop: 2 }}>req/s · live</div>\n        </div>\n\n        {/* y 轴真刻度 */}\n        {Y_TICKS.map((t, i) => (\n          <div\n            key={`yt${i}`}\n            style={{\n              position: 'absolute',\n              left: PAD - 6,\n              top: PLOT_Y + (PLOT_H / 4) * i - 11,\n              width: AXIS_W - 8,\n              textAlign: 'right',\n              fontFamily: 'Helvetica, Arial, sans-serif',\n              fontSize: 20,\n              fontWeight: 600,\n              color: G.mid,\n            }}\n          >\n            {t}\n          </div>\n        ))}\n\n        <div style={{ position: 'absolute', left: PLOT_X, top: PLOT_Y, width: PLOT_W, height: PLOT_H }}>\n          {/* 网格 */}\n          {[0, 1, 2, 3, 4].map((i) => (\n            <div key={`h${i}`} style={{ position: 'absolute', left: 0, right: 0, top: (PLOT_H / 4) * i, height: 2, background: G.line, opacity: 0.7 }} />\n          ))}\n          {[0, 1, 2, 3, 4, 5, 6].map((i) => (\n            <div key={`v${i}`} style={{ position: 'absolute', top: 0, bottom: 0, left: (PLOT_W / 6) * i, width: 2, background: G.line, opacity: 0.45 }} />\n          ))}\n\n          <svg width={PLOT_W} height={PLOT_H} style={{ position: 'absolute', inset: 0, overflow: 'visible' }}>\n            <polyline points={pts.join(' ')} fill=\"none\" stroke={G.ink} strokeWidth={5} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n            {!frozen && glowOp > 0 && (\n              <polyline\n                points={tailPts.join(' ')}\n                fill=\"none\"\n                stroke=\"#6a6a68\"\n                strokeWidth={9}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                opacity={0.55 * glowOp}\n                style={{ filter: 'blur(2px)' }}\n              />\n            )}\n            {/* 写入点亮点（1.5×） */}\n            {!frozen && glowOp > 0 && (\n              <>\n                <circle cx={PLOT_W} cy={headY} r={24} fill={spikeK > 0.15 ? AMBER : '#8f8f8d'} opacity={0.35 * glowOp} style={{ filter: 'blur(5px)' }} />\n                <circle cx={PLOT_W} cy={headY} r={10.5} fill={dotColor} opacity={glowOp} />\n              </>\n            )}\n          </svg>\n        </div>\n\n        {/* x 轴真时间刻度 */}\n        <div style={{ position: 'absolute', left: PLOT_X, top: PLOT_Y + PLOT_H + 14, width: PLOT_W }}>\n          {X_TICKS.map((t, i) => (\n            <div\n              key={`xt${i}`}\n              style={{\n                position: 'absolute',\n                left: (PLOT_W / 6) * i - 40,\n                width: 80,\n                textAlign: 'center',\n                fontFamily: 'Helvetica, Arial, sans-serif',\n                fontSize: 19,\n                fontWeight: 600,\n                color: G.mid,\n              }}\n            >\n              {t}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx =====\n// unit-dot-swarm-regroup-v2 —— 单位点阵重组 v2（批次 6 \"改改再看\" 重做）\n// 相对 v1 的加码：点数 200→320、点径 7→9；真实叙事语境：图例 \"Each dot ≈ 40 customers\"，\n// 聚簇时每簇上方浮真标签（Free · 7,210 / Pro · 4,102 / Enterprise · 1,535，Pro 琥珀主角色），\n// 列队成柱时柱底浮真轴标 + 基线；终幕点阵聚成 \"12,847\"（1/2/,/8/4/7 位图字形），\n// 下方浮 \"Total customers\"。阶段间 hold 更短（14/38/36f 间隔）、迁徙 spring 更冲\n// （stiffness 110→150、DUR 26→20）。收尾 f126 后真静止 44f。\n// 帧确定性：伪随机全用 sin 散列，无 Math.random / Date.now。\nimport React from 'react';\nimport { useCurrentFrame, spring, interpolate } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst AMBER = '#b45309';\nconst FPS = 30;\nconst N = 320;\nconst DOT_R = 9;\n\nconst M1 = 12; // 聚簇\nconst M2 = 48; // 列队成柱\nconst M3 = 84; // 收拢成 12,847\nconst DUR = 20;\nconst STAG = 8;\n\n// 帧确定伪随机\nconst rnd = (i: number, salt: number): number => {\n  const x = Math.sin(i * 12.9898 + salt * 78.233) * 43758.5453;\n  return x - Math.floor(x);\n};\n\n// —— 阶段 0：散布（星群） ——\nconst scatter = (i: number): [number, number] => [\n  300 + rnd(i, 1) * 1320,\n  330 + rnd(i, 2) * 620,\n];\n\n// —— 分组：Free 180 / Pro 102 / Enterprise 38（每点 ≈ 40 customers） ——\nconst groupOf = (i: number): number => (i < 180 ? 0 : i < 282 ? 1 : 2);\nconst idxInGroup = (i: number): number => (i < 180 ? i : i < 282 ? i - 180 : i - 282);\nconst GROUP_N = [180, 102, 38];\nconst GROUP_LABEL = ['Free · 7,210', 'Pro · 4,102', 'Enterprise · 1,535'];\n\n// —— 阶段 1：三簇（圆盘散布，sqrt 半径均匀） ——\nconst CLUSTER_C: [number, number][] = [\n  [520, 620],\n  [980, 570],\n  [1400, 640],\n];\nconst CLUSTER_R = GROUP_N.map((n) => 58 + n * 0.46); // 141 / 105 / 75\nconst cluster = (i: number): [number, number] => {\n  const g = groupOf(i);\n  const r = Math.sqrt(rnd(i, 3)) * CLUSTER_R[g];\n  const a = rnd(i, 4) * Math.PI * 2;\n  return [CLUSTER_C[g][0] + r * Math.cos(a), CLUSTER_C[g][1] + r * Math.sin(a)];\n};\n\n// —— 阶段 2：三根柱（8 点宽网格自底向上码放，底边对齐） ——\nconst BAR_BASE = 840;\nconst BAR_X = [520, 980, 1400];\nconst SPACING = 20;\nconst bar = (i: number): [number, number] => {\n  const g = groupOf(i);\n  const j = idxInGroup(i);\n  const col = j % 8;\n  const row = Math.floor(j / 8);\n  return [BAR_X[g] + (col - 3.5) * SPACING, BAR_BASE - row * SPACING];\n};\n\n// —— 阶段 3：数字 \"12,847\" 点阵（5×7 位图 × 2×2 上采样 + 逗号） ——\nconst ONE = ['00100', '01100', '00100', '00100', '00100', '00100', '01110'];\nconst TWO = ['01110', '10001', '00001', '00010', '00100', '01000', '11111'];\nconst EIGHT = ['01110', '10001', '10001', '01110', '10001', '10001', '01110'];\nconst FOUR = ['00110', '01010', '10010', '11111', '00010', '00010', '00010'];\nconst SEVEN = ['11111', '00001', '00010', '00100', '00100', '00100', '00100'];\nconst CELL = 40;\nconst SUB = 20;\nconst Y0 = 390;\nconst cellPts = (x: number, y: number): [number, number][] => [\n  [x, y],\n  [x + SUB, y],\n  [x, y + SUB],\n  [x + SUB, y + SUB],\n];\nconst buildDigit = (bitmap: string[], x0: number): [number, number][] => {\n  const out: [number, number][] = [];\n  bitmap.forEach((rowStr, r) => {\n    rowStr.split('').forEach((c, col) => {\n      if (c === '1') out.push(...cellPts(x0 + col * CELL, Y0 + r * CELL));\n    });\n  });\n  return out;\n};\nconst DIGIT_PTS: [number, number][] = [\n  ...buildDigit(ONE, 350),\n  ...buildDigit(TWO, 590),\n  // 逗号：基线处两格，微斜的小尾巴\n  ...cellPts(845, Y0 + 5.4 * CELL),\n  ...cellPts(836, Y0 + 6.3 * CELL),\n  ...buildDigit(EIGHT, 920),\n  ...buildDigit(FOUR, 1160),\n  ...buildDigit(SEVEN, 1400),\n];\nconst digit = (i: number): [number, number] => {\n  const p = DIGIT_PTS[i % DIGIT_PTS.length];\n  const jx = (rnd(i, 5) - 0.5) * 7;\n  const jy = (rnd(i, 6) - 0.5) * 7;\n  return [p[0] + jx, p[1] + jy];\n};\n\nconst lerp = (a: [number, number], b: [number, number], t: number): [number, number] => [\n  a[0] + (b[0] - a[0]) * t,\n  a[1] + (b[1] - a[1]) * t,\n];\n\nconst fade = (frame: number, inA: number, inB: number, outA?: number, outB?: number): number => {\n  const fi = interpolate(frame, [inA, inB], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n  if (outA === undefined || outB === undefined) return fi;\n  const fo = interpolate(frame, [outA, outB], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n  return Math.min(fi, fo);\n};\n\nconst LABEL_FONT = 'Helvetica, Arial, sans-serif';\n\nexport const UnitDotSwarmRegroupV2: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  const dots = Array.from({ length: N }, (_, i) => {\n    const stag = rnd(i, 7) * STAG;\n    const mig = (start: number) =>\n      spring({\n        frame: frame - start - stag,\n        fps: FPS,\n        config: { damping: 11.5, stiffness: 150, mass: 0.8 },\n        durationInFrames: DUR,\n        durationRestThreshold: 0.0001,\n      });\n    let p = scatter(i);\n    p = lerp(p, cluster(i), mig(M1));\n    p = lerp(p, bar(i), mig(M2));\n    p = lerp(p, digit(i), mig(M3));\n    return p;\n  });\n\n  // 各阶段标签透明度（迁徙完成后浮现，下一次迁徙启动即撤）\n  const clusterLabelOp = fade(frame, 38, 46, M2, M2 + 8);\n  const barLabelOp = fade(frame, 72, 80, M3, M3 + 8);\n  const captionOp = fade(frame, 114, 126);\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 110, width: '100%', textAlign: 'center' }}>\n        <TitleBlock text=\"UNIT DOT SWARM REGROUP V2\" size={72} />\n      </div>\n\n      {/* 图例：每点的含义（全程挂角） */}\n      <div\n        style={{\n          position: 'absolute',\n          left: 90,\n          bottom: 70,\n          display: 'flex',\n          alignItems: 'center',\n          gap: 12,\n          fontFamily: LABEL_FONT,\n          fontSize: 24,\n          fontWeight: 600,\n          color: G.mid,\n        }}\n      >\n        <div style={{ width: 18, height: 18, borderRadius: 9, background: G.mid }} />\n        Each dot ≈ 40 customers\n      </div>\n\n      <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0 }}>\n        {dots.map((p, i) => (\n          <circle key={i} cx={p[0]} cy={p[1]} r={DOT_R} fill={groupOf(i) === 1 ? AMBER : groupOf(i) === 2 ? G.ink : G.mid} />\n        ))}\n      </svg>\n\n      {/* 阶段 1：簇标签（真人数） */}\n      {clusterLabelOp > 0 &&\n        CLUSTER_C.map((c, g) => (\n          <div\n            key={`cl${g}`}\n            style={{\n              position: 'absolute',\n              left: c[0] - 200,\n              top: c[1] - CLUSTER_R[g] - 66,\n              width: 400,\n              textAlign: 'center',\n              fontFamily: LABEL_FONT,\n              fontSize: 30,\n              fontWeight: 700,\n              color: g === 1 ? AMBER : G.ink,\n              opacity: clusterLabelOp,\n            }}\n          >\n            {GROUP_LABEL[g]}\n          </div>\n        ))}\n\n      {/* 阶段 2：柱底基线 + 真轴标 */}\n      {barLabelOp > 0 && (\n        <>\n          <div style={{ position: 'absolute', left: 380, width: 1160, top: BAR_BASE + 16, height: 3, background: G.bar, opacity: barLabelOp }} />\n          {BAR_X.map((x, g) => (\n            <div\n              key={`bl${g}`}\n              style={{\n                position: 'absolute',\n                left: x - 150,\n                top: BAR_BASE + 30,\n                width: 300,\n                textAlign: 'center',\n                fontFamily: LABEL_FONT,\n                fontSize: 28,\n                fontWeight: 700,\n                color: g === 1 ? AMBER : G.mid,\n                opacity: barLabelOp,\n              }}\n            >\n              {['Free', 'Pro', 'Enterprise'][g]}\n            </div>\n          ))}\n        </>\n      )}\n\n      {/* 终幕：数字下方真文案 */}\n      {captionOp > 0 && (\n        <div\n          style={{\n            position: 'absolute',\n            left: 0,\n            width: 1920,\n            top: Y0 + 7 * CELL + 60,\n            textAlign: 'center',\n            fontFamily: LABEL_FONT,\n            fontSize: 34,\n            fontWeight: 600,\n            color: G.mid,\n            opacity: captionOp,\n            letterSpacing: 2,\n          }}\n        >\n          Total customers\n        </div>\n      )}\n    </div>\n  );\n};\n\n\n"
}