Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами. Движок (js/pages/network/force-graph.js): - diffing-переходы: общие узлы перелетают, новые расцветают каскадом, исчезнувшие — Ghost-слой (800мс, на месте); - мягкая радиальная пружина + отталкивание (органичная орбита), упругий влёт фокуса; - динамическая вязкость на старте (трение 0.92→0.82, отталкивание ослаблено) — мягкий разлёт без тряски; - жёсткая заморозка (kill-switch) при затухании — нет «треска», экономия батареи; - линии — SVG <path> Безье (изогнутые нити), прорастание; жесты pan с инерцией; - хард-лимит DOM-аватарок (остальное — SVG-точки). Интеграция и UX: - adapter.js: getUserConnectionsGraph → модель движка (сервер не трогаем, read-only); - фильтры (Все/Семья/Друзья/Сияющие), контекстное меню (node-menu.js), нижний сниппет, профиль; - прицел в центре, дыхание фокуса, свечение сияющих; - лаборатория network-view/lab на мок-данных (networkGraphUsers) для тестов без бэкенда. Документация: shine-UI/Dev_Docs/features/interactive-network-graph.md. Бамп client.version 1.2.135 -> 1.2.136. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.2 KiB
7.2 KiB
Интерактивная карта связей (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-слой. - Ghost-слой: снимок всего старого графа (узлы + линии) на полноэкранном overlay, застывает
на месте,
scale 1→0.7+opacity 0.5→0за 800мс, затем удаляется (красивый шлейф истории). - Физика: мягкая радиальная пружина к орбите + взаимное отталкивание (charge) → органичная, слегка неровная орбита; фокус влетает в центр упруго. Координаты узлов на трансформах (GPU).
- Каскадный bloom: новые узлы скрыты в центре и «выстреливают» по очереди (
order × 40мс). - Динамическая вязкость: первые ~600мс после перестроения трение завышено (0.92), отталкивание ослаблено (×0.45) → гасит «взрыв», затем плавно к базе (0.82) — мягкое «резиновое» появление.
- Жёсткая заморозка (kill-switch): когда сумма |vx|+|vy| < 0.03 — скорости обнуляются, координаты
округляются до целых пикселей,
cancelAnimationFrame(sleep). Нет «треска», батарея не страдает. - Линии: SVG
<path> Q(квадратичные Безье) — изящные изогнутые нити, тонкие/полупрозрачные; при движении изгиб реагирует на скорость; новые линии прорастают (stroke-dashoffset). - Жесты: свайп-pan с инерцией (новое касание прерывает); короткий тап — центрирование + нижний
сниппет; долгий тап — контекстное меню (в
#modal-root, позиция поgetBoundingClientRect); тап по центру — профиль. - Фильтры слоёв: Все / Семья / Друзья / Сияющие (плавное скрытие/перераспределение).
- Поллиш: «прицел» в центре (+пульс при захвате фокуса), «дыхание» фокуса (бесконечная CSS-анимация размера 1.48–1.52x, GPU, не будит rAF), свечение «сияющих», хард-лимит ~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.82 / 0.92 / 36 | базовое трение / стартовая вязкость / длительность (~600мс) |
SLEEP_V |
0.03 | порог суммарной |
FOCUS_SCALE |
1.5 | базовый масштаб фокуса |
MAX_FULL_NODES |
90 | хард-лимит полных аватарок (далее — точки) |
Локальный запуск / проверка
- 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 (отдаёт только прямые связи) — требуют доработки сервера.
lerpX/lerpYв движке больше не используются для отрисовки — кандидат на чистку.- Превью в простое троттлит
requestAnimationFrame(физика не идёт между вызовами) — для замеров прокачивать кадры; в активном табе всё работает на 60 FPS.