SHiNE-server/shine-UI/Dev_Docs/features/interactive-network-graph.md
Pixel 3012f0799b Связи (pixel-aquarium, 10.06): фикс физики раскрытия + камера-наезд + железный spotlight
Исправлены критические баги «аквариума» по разбору (видео):

1. Слипание узлов → адаптивный радиус орбиты (фикс collision). Дети раскладываются на кольце
   ringR = max(baseR + радиус_родителя, число_детей×13): не лезут на (увеличенный зумом) родитель
   и друг на друга. Проверено: мин. дистанция 125px, 0 наложений (было — все в одной точке).

2. Умный наезд камеры на КЛИК по любому узлу (раньше 1-й уровень раскрывался на месте). diveTo
   центрирует узел (offset ~0), zoom 1.7; узел и дети растут до единого видимого размера
   (HERO_VISUAL/baseScaleOf, DIVE_CHILD_VISUAL) — крупно и читаемо. Наведение остаётся лёгким превью.

3. Железный Spotlight (единый активный путь): diveTo гасит ВСЕ прежние pin/hover, затем раскрывает
   только путь к цели. Открыто → путь=1.0, остальное=0.25; переключение веток сбрасывает прежнюю;
   exitDive/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает. (Проверено программно.)

Реальный путь /network-view не затронут (вся глубина под tier≥2/hasDeep). Бамп client.version → 1.2.147.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 00:12:27 +03:00

144 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Интерактивная карта связей (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 `<path> Q` (квадратичные Безье) тонкие матовые дуги (`stroke-width ~1.01.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 + инжект `<base href="/">`).
- Лаборатория (без бэкенда): `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.552.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.