# Интерактивная карта связей (force-directed graph) Экран **«Связи»** (`network-view`) — интерактивная нод-граф карта вместо статичного списка: фокусный пользователь в центре, связи на орбите, навигация тапом/свайпом, премиальные переходы в духе нативного iOS. ## Где код - `js/pages/network/force-graph.js` — **движок** (физика, рендер, жизненный цикл узлов, жесты). - `js/pages/network/adapter.js` — реальные данные → нейтральная модель движка. - `js/pages/network/node-menu.js` — общее контекстное меню узла. - `js/pages/network/lab.js` — лаборатория (`network-view/lab`) на мок-данных, без бэкенда. - `js/pages/network-view.js` — страница: шапка, поиск, фильтры, история, склейка с движком. - `js/mock-data.js` — `networkGraphUsers` (связанный мульти-граф из 10 человек для лаборатории). - `styles/network-graph.css` — все стили `.fg-*`. ## Данные (read-only, сервер не трогаем) Единый источник — `authService.getUserConnectionsGraph(login)` (один запрос: логин → прямые связи). `network-view.js` → `buildGraphModel()` нормализует роли (parent/child/sibling/spouse/friend/contact), направление и метки; `adapter.engineModelFromGraphModel()` превращает это в модель движка: `{ focusId, nodes:[{ id, login, name, avatar, relationType, strength, shining, tier }] }`. ## Модель движка и API `createForceGraph({ stage, model, onNodeTap, onCenterTap, onNodeLongPress })` → `{ setModel(model), setFilter(pred), recenter(id), getFocusNode(), destroy() }`. ## Ключевые механики - **Diffing-переходы (непрерывность состояний):** при смене фокуса общие узлы (тот же `id`) не пересоздаются, а перелетают на новые места; новые «расцветают» (bloom) каскадом из центра; исчезнувшие уходят в Ghost-слой. - **CSS-bloom (разлёт без тряски):** разлёт/перелёт узлов делают нативные CSS-переходы на `transform` (компоновщик, `cubic-bezier(0.16,1,0.3,1)`, `BLOOM_MS` со ступенчатой задержкой `order × 40мс`), а НЕ JS-физика. Работает даже при троттлинге rAF; цикл лишь ведёт лучи за узлами (`syncPositionsFromDOM`). Завершение — гарантированно по таймеру (`endCssBloom`). - **Ghost-слой:** снимок только **аватарок** старого графа (без линий — иначе старые связи висят «ошмётками»). Полноэкранный overlay, застывает на месте, `scale 1→0.7` + `opacity 0.5→0` за **1000мс**, затем удаляется (мягкий породистый шлейф истории). - **Прорастание линий (Edge Growth):** новая линия тянется к ФИНАЛЬНОЙ точке узла и раскрывается `stroke-dasharray`(=длина пути) + `stroke-dashoffset`(длина→0), синхронно с разлётом узла (`growP = текущая дистанция / финальная`) → кончик «вытягивается» из центра вслед за аватаркой. Старые линии при этом исчезают мгновенно. Только для новых узлов; переезжающие — линия следует за ними. - **Физика (только до-settle):** после CSS-разлёта — лёгкая радиальная пружина + отталкивание для органичного покачивания; после фильтра физика НЕ включается (фиксация на равномерных углах). - **Жёсткая заморозка (kill-switch):** когда сумма |vx|+|vy| < 0.03 — скорости обнуляются, координаты округляются, `cancelAnimationFrame` (sleep). Нет «треска», батарея не страдает. - **Обычные линии:** SVG ` Q` (квадратичные Безье) — тонкие матовые дуги (`stroke-width ~1.0–1.2`), градиент с глубоким уходом в прозрачность: неон-центр `0.42` → цвет роли `0.07` у аватарки (растворяются в фоне, не спорят с сияющими). Изгиб реагирует на скорость. - **Сияющие связи (двухслойный «световод», Neon Layering):** два пути на одну связь — широкий размытый GLOW (`stroke-width 4`, неон, `filter: blur(2px)`, `opacity 0.4`) + тонкий чёткий CORE (`1.5px`, `#e0f7fc`). Линия остаётся изящной, но обретает объёмное OLED-свечение; растут оба синхронно (общий dashoffset). Никаких бегущих импульсов. - **Жесты:** свайп-pan с инерцией (новое касание прерывает); короткий тап — центрирование + нижний сниппет; долгий тап — контекстное меню (в `#modal-root`, позиция по `getBoundingClientRect`); тап по центру — профиль. Нажатия на чипы фильтров гасят `pointerdown` (stopPropagation), чтобы сцена не перехватила указатель (`setPointerCapture`) и не «съела» click кнопки. - **Фильтры слоёв (Все / Семья / Друзья / Сияющие):** CSS-переходы 300мс — несоответствующие узлы и их линии гаснут НА МЕСТЕ (`opacity 0` + `scale 0.8`), оставшиеся плавно переплывают на равномерные углы, затем жёсткая фиксация без физики (ноль тряски, мгновенный sleep). - **Живой фон (Nebula):** под центром — глубокое размытое сине-голубое облако (`.fg-stage::before`, `blur 80→96px`), бесконечная анимация 7с: «дышит» радиусом/яркостью и переливается индиго↔ультрамарин (`hue-rotate`). На компоновщике (GPU), не будит rAF; подписи имён остаются контрастными. - **Стеклянные чипы фильтров (frosted glass):** `background: rgba(255,255,255,0.03)`, `backdrop-filter: blur(12px)`, граница `0.5px solid rgba(255,255,255,0.1)`; активный — подсвечен сине-голубым. - **Поллиш:** «дыхание» фокуса (бесконечная CSS-анимация размера, GPU, не будит rAF); свечение «сияющих» узлов — мягкая медленная пульсация (3.6с) многослойной `box-shadow` + размытый ореол через SVG-фильтр `#fg-shine-glow`; тестовые фото-аватарки (`NETWORK_PHOTOS`); хард-лимит ~90 DOM-аватарок (остальное — SVG-точки). ## Параметры тюнинга (константы в начале `force-graph.js`) | Константа | Значение | Назначение | |---|---|---| | `ORBIT_MIN / ORBIT_MAX` | 150 / 240 | радиус орбиты (защитный отступ от центра — подписи не наезжают) | | `K_RADIAL` | 0.035 | жёсткость орбитальной пружины (мягко) | | `K_FOCUS` | 0.12 | жёсткость пружины фокуса к центру | | `CHARGE` / `CHARGE_START_FACTOR` | 1400 / 0.45 | отталкивание (на старте ослаблено) | | `FRICTION` / `FRICTION_BOOST` / `BOOST_FRAMES` | 0.80 / 0.94 / 42 | базовое трение / стартовая вязкость / длительность (~700мс) | | `BLOOM_MS` / `BLOOM_STAGGER` | 900 / 40 | длительность CSS-разлёта / задержка между узлами (каскад) | | `SLEEP_V` | 0.03 | порог суммарной |v| для заморозки | | `FOCUS_SCALE` | 1.5 | базовый масштаб фокуса | | `MAX_FULL_NODES` | 90 | хард-лимит полных аватарок (далее — точки) | Прочее (вшито в код): Ghost-слой — 1000мс; CSS-переход фильтра — 300мс; пульсация сияния — 3.6с; прорастание линий привязано к прогрессу разлёта узла (а не к отдельному таймеру). ## Локальный запуск / проверка - Dev-сервер: `.claude/shine-ui-dev-server.cjs` (Node, порт 7321, SPA-fallback + инжект ``). - Лаборатория (без бэкенда): `http://localhost:7321/network-view/lab` — мок `networkGraphUsers`, тап по узлам переключает сети. - Реальный путь (`/network-view`) требует живого `wss://shineup.me/ws` (локально — `ws_open_error`, это норма). ## Режим «Интерактивная паутина» (ветка `pixel-web`, эксперимент, только лаборатория) Включается чипом «🌌 Вселенная». Дальние уровни (2-3) по умолчанию скрыты и раскрываются локально: - **Hover-превью (наведение):** навёл мышь/палец на узел — его ветка временно выплывает; убрал — втягивается. Реализация: `pointerover/out` (мышь) и `pointerdown/up` (палец) → `onNodeHover` → `graph.setHover(node|null)`; узел получает флаг `hovered`. - **Фиксация кликом (pin):** тап/клик по узлу → `graph.toggleExpand` ставит флаг `pinned` — ветка остаётся раскрытой и после ухода курсора. Повторный клик по раскрытому узлу **сворачивает** его (надёжный toggle: `isOpen = pinned || expandP>0.5` → сброс `pinned`+`hovered`). - Эффективное раскрытие = `pinned || hovered` (см. `expandTargetOf`), прогресс `expandP` (~400мс). - **Spotlight-затемнение:** пока есть закреплённая ветка, остальные тускнеют до `SPOTLIGHT_DIM=0.25` (узлы и их линии), фокус и закреплённая/наведённая ветка — 100%. Плавно через `spotCur` (lerp). - **Узлы 2-го уровня — полноценные аватарки:** фото-лицо (pravatar) + имя, `DEEP2_SCALE=0.62` (≈radius 16px), `DEEP2_OPACITY=0.85`. Не «пустые кружки», а видимые друзья друзей. - **Глобальный сброс:** тап по корню (Иван) → `collapseAll()` снимает `pinned`/`hovered` → 100% яркость. - **Адаптивное расталкивание (collision):** раскрытая ветка усиливает отталкивание соседних узлов 1-го уровня пропорционально `expandP` (`EXPAND_REPULSION=2.4`) — кластеры разъезжаются, не накладываясь. - **Камера-доводчик:** при фиксации ветки, если её «веер» упирается в край экрана, камера мягко дотягивается (`glideCameraTo` → `camTargetX/Y`, lerp `CAM_GLIDE_K` в tick). Любой жест отменяет доводчик. - **Свободный зум:** колесо мыши (`onWheel`) и щипок двумя пальцами (`activePointers`/`pinching`) — масштаб `zoom` (0.55–2.6), «к точке» под курсором/центром щипка; мир масштабируется CSS-`scale`, линии (отдельный SVG) пересчитываются в экранных координатах (× `zoom`). - **Синхро-пульс линий:** сияющие/трековые «световоды» (`.fg-edge-glow`/`.fg-edge-core`) «дышат» толщиной/размытием 3.6с — в такт ободку сияющего узла (в покое SVG не перерисовывается → синхронно). - Мерцающие микрозвёзды 3-го уровня (`fg-star-twinkle`), хаптика (`navigator.vibrate`) на нажатие/раскрытие/натяжение. ### Умный фокус (Smart Zoom / «аквариум») — ветка `pixel-aquarium` **Наведение** (hover/палец) на узел — лёгкое превью ветки (раскрытие на месте, без камеры). **Клик/тап по ЛЮБОМУ узлу** — **погружение (dive)** с кинематографичным наездом: - **Камера-полёт + зум** (`diveTo` → `diveTargetId`/`diveZoom=1.7`, лёт в `tick` с `DIVE_FLY_K` ≈600мс): узел плавно центрируется (offset ~0) и **вырастает до единого видимого размера** `HERO_VISUAL=1.4` независимо от уровня (`depthScale = HERO_VISUAL / baseScaleOf`); его прямые дети — до `DIVE_CHILD_VISUAL`. - **Адаптивный радиус орбиты (фикс слипания):** дети раскладываются на кольце `ringR = max(baseR + радиус_родителя, число_детей × 13)` — НЕ лезут на (увеличенный зумом) родитель и друг на друга (проверено: мин. дистанция 125px, 0 наложений). Радиус растёт вместе с зумом родителя. - **Глубина «аквариума»** (`contextTargetOf` → `depthScale`/`depthBlur`/`spotCur`, лерп): Иван и боковые ветки **уменьшаются** (root ×0.55, фон ×0.55) + уходят в **blur 3px** + тускнеют до 0.25 → задний план. - **Железный Spotlight (единый активный путь):** `diveTo` сначала гасит ВСЕ прежние pin/hover, затем раскрывает только путь к новой цели. Открыто → путь Иван→…→узел = 1.0, остальное = 0.25; переключение веток сбрасывает прежнюю; **выход/`exitDive`/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает**. - **Нить-крошка**: путь (`divePathSet`/`onPath`) горит ярким «световодом» — виден путь назад к Ивану. - **Pinch-to-Zoom + LOD**: щипок/колесо меняют `zoom`; при `zoom ≥ LOD_ZOOM (1.55)` видимые точки 3-го уровня **дорисовываются как аватарки** (`updateLod`/`setNodeLod`), при отдалении — обратно в точки. - Глубина — фейк-3D через масштаб + CSS-`blur` (GPU), без WebGL. > ⚠️ Эксперименты на ветках `pixel-web` (паутина) и `pixel-aquarium` (Smart Zoom) — для отката. > Реальный путь `/network-view` не затронут: deep-код под `tier ≥ 2` / `hasDeep`, dive — только tier≥2 > (в реальном пути их нет), depthScale/Blur по умолчанию нейтральны, `updateLod` выходит при `!hasDeep`. ## Ограничения / на будущее - Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи» упираются в API (отдаёт только прямые связи) — требуют доработки сервера. - Превью в простое троттлит `requestAnimationFrame` (физика не идёт между вызовами) — для замеров прокачивать кадры; в активном табе всё работает на 60 FPS.