SHiNE-server/shine-UI/Dev_Docs/features/interactive-network-graph.md
Pixel e6e96c4b0d Связи: чистка мёртвого кода в движке (lerp, неиспользуемые easing)
- Убран механизм lerpX/lerpY: координаты для отрисовки берутся из n.x/n.y, lerp нигде
  не читался кроме условия заморозки (lerpSettling). Удалены поля, advanceLerp(), EDGE_LERP
  и lerpSettling — граф засыпает чуть раньше (без визуальных изменений; проверено: frozen=true).
- Удалены неиспользуемые cubicBezier() и EASE_BLOOM (easing теперь делает CSS); easeOutCubic
  оставлен (нужен в stepTween для фолбэк-центрирования).
- Документация фичи актуализирована (убрана заметка про lerp как кандидата на чистку).
- Бамп client.version → 1.2.139.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 21:23:18 +03:00

96 lines
10 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`, это норма).
## Ограничения / на будущее
- Многоуровневая глубина (друзья друзей мельче, 3-й уровень точки), кластеры, «общие связи»
упираются в API (отдаёт только прямые связи) требуют доработки сервера.
- Превью в простое троттлит `requestAnimationFrame` (физика не идёт между вызовами) для замеров
прокачивать кадры; в активном табе всё работает на 60 FPS.