{
  "slug": "dataviz-landscape-open",
  "title": "Dataviz Landscape Open",
  "tagline": "A dark-scene opener of tributary flow-line terrain — many flow lines merge into a main stream, fictional ID labels float on the lines, and the camera flies slowly through with heavy depth of field.",
  "description": "A dark-scene opener of tributary flow-line terrain — many flow lines merge into a main stream, fictional ID labels float on the lines, and the camera flies sl",
  "category": "intro",
  "tags": [
    "shotcraft",
    "remotion",
    "intro",
    "opening",
    "data",
    "camera"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.55,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/dataviz-landscape-open/poster-fece904119c1.jpg",
    "video": "https://gimgs.net/motion/media/dataviz-landscape-open/video-af45b40bc532.mp4"
  },
  "variants": [
    {
      "key": "dataviz-landscape-open",
      "label": "dataviz-landscape-open",
      "description": "A dark-scene opener of tributary flow-line terrain — many flow lines merge into a main stream, fictional ID labels float on the lines, and the camera flies slowly through with heavy depth of field.",
      "use": "",
      "video": "https://gimgs.net/motion/media/dataviz-landscape-open/video-af45b40bc532.mp4",
      "poster": "https://gimgs.net/motion/media/dataviz-landscape-open/poster-fece904119c1.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/dataviz-landscape-open.html",
  "json_url": "https://gimgs.net/motion/dataviz-landscape-open.json",
  "template_url": "https://gimgs.net/motion/dataviz-landscape-open/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/dataviz-landscape-open.md",
  "updated_at": "2026-09-24T07:39:54.295Z",
  "prompt": "## When to use\n\nA brand-level abstract opener (a \"data universe\" metaphor), leading into a bright product segment or wordmark; division of labor with glow-flyline-moves: that card is line-connection narrative between cards within a segment, this one is a full-frame landscape exclusive to the opening.\n\n## Intention\n\nThe brand-level opener category attested in Linear Releases launch films: shoot the data world behind the product as a dark-scene landscape — bundles of flow lines merging from deep in the frame into one main stream (a metaphor for \"countless workflows converging into one product\"). The realism comes from three things: 1) fictional issue/task ID labels floating on the lines (viewers recognize \"this is the kind of thing I see every day\"); 2) heavy depth of field — a large soft-focus foreground flow, a legible mid-ground of labels, and a fading background — three depth layers make a flat SVG read as space; 3) a slow, steady camera flythrough — this is an atmosphere segment, not a showcase of speed, which is left to later shots. Hand-built-UI compliance basis: not a recreated real scene (this screen doesn't exist on any page), so it follows the quality-plus-expressive-clarity bar per Core Principle 1 (revised).\n\n## Duration & energy\n\n- Duration: 5–8s (an atmospheric opening segment, at most once per film).\n- Energy: Low start, building slowly (a starting position, leaving room to climb in later shots).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\nLinear Releases 发布片开场实证的品牌级开场品类：把产品背后的数据世界\n拍成一片**暗场地景**——支流线束从画面深处汇入一条主干（\"无数工作流\n汇成一个产品\"的隐喻），真实感来自三样东西：①虚构 issue/任务 ID 标签\n浮在线上（观众认出\"这是我每天看到的那种东西\"）；②重景深——近景大虚焦\n流过、中景标签清晰可读、远景渐隐，三层纵深让平面 SVG 读作空间；\n③相机低速匀稳飞越——这是氛围段不是炫技段，速度感交给后面的镜头。\n手搓 UI 合规依据：非复刻场景（页面上不存在此画面），按核心理念 1\n修订版走质量+表达明确性门槛。\n\n## 动作阶段\n| 阶段 | 帧段（@30fps 参考） | 内容 |\n|------|------|------|\n| 1 生长 | f0–45 | 流线自远端向汇点 draw-on 生长（out-cubic），主干先行、支流错峰跟进 |\n| 2 飞越 | f30–150 | 相机沿主干方向低速推移（与生长期跨骑），标签非均匀错峰浮现 |\n| 3 交棒 | 末 30–45f | 汇点方向留亮部引导视线，接 shot-transitions 藏切/B 式穿暗场，或减速停稳接字标 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 线束规模 | 支流 8–14 条汇入 1 条主干；分层 近 2–3 / 中 5–8 / 远 4–6 | 超过 ~16 条读作屏保/科技模板；主干必须唯一（单主角原则的地景版） |\n| 汇流拓扑 | 支流以 bezier 切向汇入主干（切线连续），汇点偏画面一侧 1/3 处 | 垂直相交读作电路图不是水系；汇点居中构图死板 |\n| 景深三层 | 近景 blur 10–16px + 线宽 6–10px + 亮度 0.15–0.25；中景清晰 2–3px 亮度 0.5–0.8;远景 1–1.5px 亮度 0.2–0.35 | 近景不虚焦则三层塌成一张平面图；远景过亮抢中景 |\n| 相机飞越 | 世界画布 ≥2× 视口，横移速度 2–5px/f 匀稳 + 视差（近 1.4×/中 1×/远 0.6×），可叠极缓 zoom 1.0→1.06 | 快了变追逐镜头（那是 crash-zoom 的活）；无视差立刻穿帮为平面滚动 |\n| 视差可见性 | 近/远层线条必须与运动方向成角（斜率明显）或带画内可见特征点（端点/交叉/节点标记穿过画面） | **平行于运动方向的线横移时沿自身滑动，视差机制再对也不可见**（L2 评估判例：0.6×/1×/1.4× 全写对，正常播放只有中景在动） |\n| 流动感 | draw-on 完成后线条保持\"活\"：沿线明暗脉冲/虚线相位低速漂向汇点（速度 1–2px/f，透明度 ≤0.3 叠加） | 生长期结束后无流动元素，后半段退化成\"路线图飞越\"不是\"数据流\" |\n| 标签 | 中景层 5–9 枚：方块图钉 + 等宽字 ID（如 APP-1843），错峰 6–10f 间隔淡入 + 沿线微漂 | ID 一律虚构（合规底线）；标签挂近景/远景层会因虚焦/过暗不可读；图钉方块要落在线上（钉线分离读作浮尘不是锚点） |\n| 生长节奏 | 主干 30–40f out-cubic 先行，支流每条错峰 3–6f | 全部同帧生长读作 loading 动画 |\n| 收尾 | 交棒帧前 ≥15f 标签停止新增、相机斜率连续（不急刹） | 氛围段急刹是一次可见的顿挫（glow-orb 收敛判例同源） |\n\n## 已知坑\n- 借鉴来源：Linear Releases 开场（进化轮 #3 breakdown #7），单例样本\n  ——参数给区间不给教条，首次实战以真实素材回验\n- 与 glow-flyline-moves 别混用在相邻段落：两卡都是\"暗场发光线\"，\n  背靠背出现读作全片只会一招；同片都要用时中间隔 ≥2 个亮场镜头\n- 暗场进出成本同 glow 卡：接亮场产品段用 shot-transitions 藏切族垫，\n  别生切明暗\n- 线条纯白全亮会糊成蛛网——亮度按层拉开（见参数表），中景才是主角层；\n  主干/支流给 1–2px 微辉光（soft glow）压出\"发光线\"氛围，全无辉光读作线框图（Q4 单点光效许可内）\n- 标签字号在目标画幅下 ≥18px 等效，否则\"可读\"沦为装饰噪点（Q6 同源）\n- 声音：铺 riser/低频 pad 底噪，标签浮现不逐枚打点（批量不逐个发声，\n  S2 同源）；交棒帧一次 whoosh/impact 交给下一镜头\n\n## 参考实现\ndemos/opening/dataviz-landscape-open/\n（DatavizLandscapeOpen.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.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/opening/dataviz-landscape-open/DatavizLandscapeOpen.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/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.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": 167,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.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/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx =====\nimport React from 'react';\nimport {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion';\n\n/**\n * DatavizLandscapeOpen — 暗场支流线束地景开场\n * 配方卡: references/shots/opening/dataviz-landscape-open.md\n *\n * 隐喻: 无数团队工作流(支流)汇成一个产品(主干)。\n * 三层景深: 近景大虚焦流过 / 中景标签清晰可读 / 远景渐隐。\n * 相机: 低速匀稳横移 + 视差 + 极缓 zoom, 收尾不急刹。\n */\n\nconst W = 1920;\nconst H = 1080;\nconst DUR = 165; // 5.5s @30fps\nconst WORLD_W = 4200;\n\n// ---------- easing ----------\nconst outCubic = (t: number) => 1 - Math.pow(1 - t, 3);\n\nconst growth = (frame: number, start: number, dur: number) => {\n  const t = Math.min(1, Math.max(0, (frame - start) / dur));\n  return outCubic(t);\n};\n\n// ---------- geometry ----------\ntype Pt = {x: number; y: number};\n\n// 主干: 缓和的水系曲线 (世界坐标)\nconst trunkY = (x: number) => 480 + 55 * Math.sin((x - 300) / 1050);\nconst trunkSlope = (x: number) => (55 / 1050) * Math.cos((x - 300) / 1050);\n\nconst trunkPath = () => {\n  const pts: string[] = [];\n  for (let x = -300; x <= WORLD_W + 100; x += 50) {\n    pts.push(`${x === -300 ? 'M' : 'L'} ${x} ${trunkY(x).toFixed(1)}`);\n  }\n  return pts.join(' ');\n};\n\n// 三次贝塞尔求值 (用于把标签钉在线上)\nconst cubicAt = (p0: Pt, p1: Pt, p2: Pt, p3: Pt, t: number): Pt => {\n  const u = 1 - t;\n  return {\n    x: u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x,\n    y: u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y,\n  };\n};\n\n// 支流: 起点在深处四散, 以切向汇入主干 (切线连续)\ntype Trib = {p0: Pt; p1: Pt; p2: Pt; p3: Pt; opacity: number; growStart: number; growDur: number};\n\nconst makeTrib = (\n  start: Pt,\n  mergeX: number,\n  opacity: number,\n  growStart: number,\n  growDur: number,\n): Trib => {\n  const end: Pt = {x: mergeX, y: trunkY(mergeX)};\n  const slope = trunkSlope(mergeX);\n  const len = Math.hypot(1, slope);\n  // P2 沿主干切线反向后退 => 到达时切线连续\n  const p2: Pt = {x: end.x - (330 * 1) / len, y: end.y - (330 * slope) / len};\n  const p1: Pt = {\n    x: start.x + (end.x - start.x) * 0.35,\n    y: start.y + (end.y - start.y) * 0.12,\n  };\n  return {p0: start, p1, p2, p3: end, opacity, growStart, growDur};\n};\n\n// 中景支流 6 条 (主角层), 错峰 4–6f\nconst MID_TRIBS: Trib[] = [\n  makeTrib({x: -380, y: 130}, 1020, 0.62, 6, 32),\n  makeTrib({x: -260, y: 880}, 1180, 0.55, 10, 32),\n  makeTrib({x: -60, y: 40}, 1330, 0.7, 15, 32),\n  makeTrib({x: 60, y: 960}, 1500, 0.5, 19, 32),\n  makeTrib({x: 320, y: 210}, 1680, 0.6, 24, 32),\n  makeTrib({x: 420, y: 790}, 1840, 0.55, 30, 32),\n];\n\nconst tribPath = (t: Trib) =>\n  `M ${t.p0.x} ${t.p0.y} C ${t.p1.x} ${t.p1.y}, ${t.p2.x} ${t.p2.y}, ${t.p3.x} ${t.p3.y}`;\n\n// 远景 4 条: 更细更暗, 向更深处的远点收拢\n// (控制点形式, 供节点标记取样——卡\"视差可见性\": 特征点穿过画面)\ntype FarLine = {p0: Pt; p1: Pt; p2: Pt; p3: Pt; growStart: number; growDur: number; op: number};\nconst FAR_LINES: FarLine[] = [\n  {p0: {x: -300, y: 260}, p1: {x: 700, y: 250}, p2: {x: 2400, y: 330}, p3: {x: 3400, y: 490}, growStart: 12, growDur: 36, op: 0.3},\n  {p0: {x: -200, y: 700}, p1: {x: 800, y: 690}, p2: {x: 2500, y: 610}, p3: {x: 3450, y: 492}, growStart: 16, growDur: 36, op: 0.26},\n  {p0: {x: -350, y: 400}, p1: {x: 900, y: 390}, p2: {x: 2600, y: 420}, p3: {x: 3500, y: 493}, growStart: 22, growDur: 36, op: 0.33},\n  {p0: {x: -250, y: 590}, p1: {x: 850, y: 600}, p2: {x: 2650, y: 560}, p3: {x: 3520, y: 494}, growStart: 27, growDur: 36, op: 0.24},\n];\nconst farPath = (l: FarLine) =>\n  `M ${l.p0.x} ${l.p0.y} C ${l.p1.x} ${l.p1.y}, ${l.p2.x} ${l.p2.y}, ${l.p3.x} ${l.p3.y}`;\n// 远景节点标记: 每条线 3 枚暗点, 随层平移穿过画面 → 视差可辨\nconst FAR_NODE_TS = [0.28, 0.55, 0.82];\n\n// 近景: 大虚焦斜向流过前景 (与横移运动方向成明显角度——\n// 卡\"视差可见性\": 平行于运动方向的线沿自身滑动不可见)\nconst NEAR_LINES = [\n  {d: 'M 130 1270 C 480 830, 880 360, 1290 -130', growStart: 2, growDur: 40, op: 0.22, w: 9},\n  {d: 'M 1480 1240 C 1830 800, 2180 330, 2540 -110', growStart: 10, growDur: 40, op: 0.18, w: 7},\n];\n\n// ---------- 标签 (中景层, 全部虚构 ID) ----------\ntype LabelSpec = {trib: number; t: number; id: string; appear: number; above: boolean};\n\n// 错峰 6–10f 间隔; 最后一枚 f103, 距交棒帧 (f120) ≥15f 无新增\nconst LABELS: LabelSpec[] = [\n  {trib: 0, t: 0.62, id: 'OKR-1024', appear: 48, above: true},\n  {trib: 1, t: 0.58, id: 'TEAM-4417', appear: 56, above: false},\n  {trib: 2, t: 0.66, id: 'KR-2093', appear: 66, above: true},\n  {trib: 3, t: 0.6, id: 'SYNC-3308', appear: 74, above: false},\n  {trib: 4, t: 0.68, id: 'OBJ-2471', appear: 84, above: true},\n  {trib: 5, t: 0.64, id: 'PLAN-9124', appear: 93, above: false},\n  {trib: 4, t: 0.86, id: 'GOAL-7752', appear: 103, above: true},\n];\n\n// 汇点 (偏画面一侧, 收尾亮部引导视线)\nconst CONV: Pt = {x: 1850, y: trunkY(1850)};\n\n// ---------- component ----------\nexport const DatavizLandscapeOpen: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 相机: 匀稳横移 3.2px/f (2–5 区间), 全程斜率恒定 (不急刹)\n  const camX = frame * 3.2;\n  // 极缓 zoom 1.0 → 1.06\n  const zoom = interpolate(frame, [0, DUR - 1], [1, 1.06]);\n\n  // 视差: 近 1.4× / 中 1× / 远 0.6×\n  const farX = -camX * 0.6;\n  const midX = -camX * 1.0;\n  const nearX = -camX * 1.4;\n\n  // 主干先行: f0–38 out-cubic\n  const trunkGrow = growth(frame, 0, 38);\n\n  // 流动感 (卡\"流动感\"行): draw-on 完成后, 虚线相位低速漂向汇点\n  // 速度 1.5px/f (卡 1–2), 叠加透明度 ≤0.3; 生长完成后才淡入\n  const flowOffset = -frame * 1.5; // dashoffset 递减 = 相位沿画线方向(向汇点)漂移\n  const flowDash = '14 56'; // 世界像素单位\n  const flowIn = (gEnd: number) =>\n    interpolate(frame, [gEnd, gEnd + 20], [0, 1], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n    });\n\n  // 收尾亮部 (交棒段 f120+ 缓升, 引导视线向汇点/右侧)\n  const handoffGlow = interpolate(frame, [118, 160], [0, 0.4], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const layerStyle = (tx: number): React.CSSProperties => ({\n    position: 'absolute',\n    left: 0,\n    top: 0,\n    width: WORLD_W,\n    height: H,\n    transform: `translateX(${tx}px)`,\n  });\n\n  return (\n    <AbsoluteFill style={{backgroundColor: '#050505', overflow: 'hidden'}}>\n      {/* zoom 容器: 以画面中心为原点 */}\n      <AbsoluteFill style={{transform: `scale(${zoom})`, transformOrigin: '50% 50%'}}>\n        {/* 底色微渐变, 避免纯黑死平 */}\n        <AbsoluteFill\n          style={{\n            background:\n              'radial-gradient(120% 90% at 62% 45%, #0b0b0d 0%, #060607 55%, #040404 100%)',\n          }}\n        />\n\n        {/* ---- 远景层 (视差 0.6×) ---- */}\n        <div style={layerStyle(farX)}>\n          <svg width={WORLD_W} height={H} style={{position: 'absolute'}}>\n            {FAR_LINES.map((l, i) => {\n              const g = growth(frame, l.growStart, l.growDur);\n              return (\n                <g key={i}>\n                  <path\n                    d={farPath(l)}\n                    fill=\"none\"\n                    stroke=\"#ffffff\"\n                    strokeWidth={1.3}\n                    strokeLinecap=\"round\"\n                    opacity={l.op}\n                    pathLength={1}\n                    strokeDasharray={1}\n                    strokeDashoffset={1 - g}\n                  />\n                  {/* 节点标记: 可见特征点穿过画面, 让 0.6× 层位移可辨 */}\n                  {FAR_NODE_TS.map((t, j) => {\n                    const p = cubicAt(l.p0, l.p1, l.p2, l.p3, t);\n                    return (\n                      <circle\n                        key={j}\n                        cx={p.x}\n                        cy={p.y}\n                        r={3}\n                        fill=\"#ffffff\"\n                        opacity={t <= g ? l.op + 0.08 : 0}\n                      />\n                    );\n                  })}\n                </g>\n              );\n            })}\n          </svg>\n        </div>\n\n        {/* ---- 中景层 (主角层, 视差 1×) ---- */}\n        <div style={layerStyle(midX)}>\n          <svg width={WORLD_W} height={H} style={{position: 'absolute'}}>\n            {/* 微辉光垫层: 1–2px soft glow 压出\"发光线\"(卡坑注, Q4 单点许可内) */}\n            <g style={{filter: 'blur(3px)'}}>\n              <path\n                d={trunkPath()}\n                fill=\"none\"\n                stroke=\"#ffffff\"\n                strokeWidth={7}\n                strokeLinecap=\"round\"\n                opacity={0.16}\n                pathLength={1}\n                strokeDasharray={1}\n                strokeDashoffset={1 - trunkGrow}\n              />\n              {MID_TRIBS.map((t, i) => (\n                <path\n                  key={i}\n                  d={tribPath(t)}\n                  fill=\"none\"\n                  stroke=\"#ffffff\"\n                  strokeWidth={5.5}\n                  strokeLinecap=\"round\"\n                  opacity={t.opacity * 0.18}\n                  pathLength={1}\n                  strokeDasharray={1}\n                  strokeDashoffset={1 - growth(frame, t.growStart, t.growDur)}\n                />\n              ))}\n            </g>\n            {/* 主干 (唯一) */}\n            <path\n              d={trunkPath()}\n              fill=\"none\"\n              stroke=\"#ffffff\"\n              strokeWidth={2.8}\n              strokeLinecap=\"round\"\n              opacity={0.8}\n              pathLength={1}\n              strokeDasharray={1}\n              strokeDashoffset={1 - trunkGrow}\n            />\n            {/* 支流, 错峰生长 */}\n            {MID_TRIBS.map((t, i) => (\n              <path\n                key={i}\n                d={tribPath(t)}\n                fill=\"none\"\n                stroke=\"#ffffff\"\n                strokeWidth={2.2}\n                strokeLinecap=\"round\"\n                opacity={t.opacity}\n                pathLength={1}\n                strokeDasharray={1}\n                strokeDashoffset={1 - growth(frame, t.growStart, t.growDur)}\n              />\n            ))}\n            {/* 流动感: 虚线相位沿线漂向汇点 (1.5px/f, 叠加透明度 ≤0.3) */}\n            <path\n              d={trunkPath()}\n              fill=\"none\"\n              stroke=\"#ffffff\"\n              strokeWidth={2.8}\n              strokeLinecap=\"round\"\n              opacity={0.3 * flowIn(38)}\n              strokeDasharray={flowDash}\n              strokeDashoffset={flowOffset}\n            />\n            {MID_TRIBS.map((t, i) => (\n              <path\n                key={`flow-${i}`}\n                d={tribPath(t)}\n                fill=\"none\"\n                stroke=\"#ffffff\"\n                strokeWidth={2.2}\n                strokeLinecap=\"round\"\n                opacity={0.26 * flowIn(t.growStart + t.growDur)}\n                strokeDasharray={flowDash}\n                strokeDashoffset={flowOffset}\n              />\n            ))}\n          </svg>\n\n          {/* 标签: 方块图钉 + 等宽字虚构 ID, 错峰淡入 + 沿线微漂 */}\n          {LABELS.map((l) => {\n            const trib = MID_TRIBS[l.trib];\n            const base = cubicAt(trib.p0, trib.p1, trib.p2, trib.p3, l.t);\n            const fadeIn = interpolate(frame, [l.appear, l.appear + 12], [0, 1], {\n              extrapolateLeft: 'clamp',\n              extrapolateRight: 'clamp',\n            });\n            // 沿线微漂: 出现后沿切线方向缓移 ~6px\n            const drift = interpolate(frame, [l.appear, DUR - 1], [0, 6], {\n              extrapolateLeft: 'clamp',\n              extrapolateRight: 'clamp',\n            });\n            const ahead = cubicAt(trib.p0, trib.p1, trib.p2, trib.p3, Math.min(1, l.t + 0.02));\n            const dx = ahead.x - base.x;\n            const dy = ahead.y - base.y;\n            const dl = Math.hypot(dx, dy) || 1;\n            const px = base.x + (dx / dl) * drift;\n            const py = base.y + (dy / dl) * drift;\n            // 图钉锚在线上 (中心 = 线上取样点, 卡坑注: 钉线分离读作浮尘);\n            // 文字沿垂直方向偏移, 避开线体\n            const textOff = l.above ? -38 : 20;\n            return (\n              <React.Fragment key={l.id}>\n                <div\n                  style={{\n                    position: 'absolute',\n                    left: px - 8,\n                    top: py - 8,\n                    width: 16,\n                    height: 16,\n                    backgroundColor: '#ffffff',\n                    opacity: fadeIn * 0.9,\n                  }}\n                />\n                <span\n                  style={{\n                    position: 'absolute',\n                    left: px + 14,\n                    top: py + textOff,\n                    fontFamily: 'Menlo, \"SF Mono\", Consolas, monospace',\n                    fontSize: 22,\n                    letterSpacing: 3,\n                    color: '#e8e8e8',\n                    whiteSpace: 'nowrap',\n                    opacity: fadeIn * 0.92,\n                  }}\n                >\n                  {l.id}\n                </span>\n              </React.Fragment>\n            );\n          })}\n\n          {/* 交棒亮部: 汇点方向留亮, 引导视线接下一镜头 */}\n          <div\n            style={{\n              position: 'absolute',\n              left: CONV.x - 420,\n              top: CONV.y - 260,\n              width: 840,\n              height: 520,\n              background:\n                'radial-gradient(50% 50% at 50% 50%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.12) 45%, rgba(255,255,255,0) 72%)',\n              opacity: handoffGlow,\n              filter: 'blur(18px)',\n              pointerEvents: 'none',\n            }}\n          />\n        </div>\n\n        {/* ---- 近景层 (大虚焦, 视差 1.4×) ---- */}\n        <div style={{...layerStyle(nearX), filter: 'blur(13px)'}}>\n          <svg width={WORLD_W} height={H} style={{position: 'absolute'}}>\n            {NEAR_LINES.map((l, i) => (\n              <path\n                key={i}\n                d={l.d}\n                fill=\"none\"\n                stroke=\"#ffffff\"\n                strokeWidth={l.w}\n                strokeLinecap=\"round\"\n                opacity={l.op}\n                pathLength={1}\n                strokeDasharray={1}\n                strokeDashoffset={1 - growth(frame, l.growStart, l.growDur)}\n              />\n            ))}\n          </svg>\n        </div>\n\n        {/* 轻 vignette 收住四角 */}\n        <AbsoluteFill\n          style={{\n            background:\n              'radial-gradient(115% 95% at 50% 50%, rgba(0,0,0,0) 62%, rgba(0,0,0,0.5) 100%)',\n            pointerEvents: 'none',\n          }}\n        />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}