Доработка режима «Интерактивная паутина» (только лаборатория, deep-режим «Вселенная»): Взаимодействие (по запросу): наведение ≠ клик. - Hover-превью: навёл мышь/палец на узел — его ветка ВРЕМЕННО выплывает; убрал — втягивается. (pointerover/out для мыши, pointerdown/up для пальца → onNodeHover → graph.setHover; флаг hovered). - Фиксация кликом: тап/клик → graph.toggleExpand ставит pinned — ветка остаётся раскрытой и после ухода курсора; повторный тап снимает фиксацию. Эффект = pinned || hovered (expandTargetOf). Этап 2 «Мегамасштаб»: - Collision-расталкивание: раскрытая ветка усиливает отталкивание соседей 1-го уровня пропорционально expandP (EXPAND_REPULSION=2.4) — кластеры разъезжаются, не накладываясь. - Свободный зум: колесо мыши (onWheel) + щипок двумя пальцами (activePointers/pinching), zoom 0.55–2.6 «к точке»; мир — CSS-scale, линии (SVG) пересчитываются в экранных координатах × zoom. - Камера-доводчик: при фиксации ветки, если её веер упирается в край, камера мягко дотягивается (glideCameraTo → camTargetX/Y, lerp CAM_GLIDE_K в tick); любой жест отменяет доводчик. - Синхро-пульс: сияющие/трековые «световоды» дышат толщиной/размытием 3.6с в такт ободку узла. Реальный путь /network-view не затронут: deep-код под tier≥2/hasDeep, hover-колбэк даёт только лаборатория. Ветка экспериментальная (отдельно от pixel-08.06/PR). Бамп client.version → 1.2.144. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
119 lines
14 KiB
Markdown
119 lines
14 KiB
Markdown
# Интерактивная карта связей (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.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 + инжект `<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` — ветка
|
||
остаётся раскрытой и после ухода курсора. Повторный тап снимает фиксацию.
|
||
- Эффективное раскрытие = `pinned || hovered` (см. `expandTargetOf`), прогресс `expandP` (~400мс).
|
||
- **Глобальный сброс:** тап по корню (Иван) → `collapseAll()` снимает `pinned` и `hovered` со всех.
|
||
- **Адаптивное расталкивание (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`) на нажатие/раскрытие/натяжение.
|
||
|
||
> ⚠️ Эксперимент на ветке `pixel-web` (для отката). Реальный путь `/network-view` не затронут:
|
||
> весь deep-код под `tier ≥ 2` / `hasDeep`, hover-колбэк передаёт только лаборатория.
|
||
|
||
## Ограничения / на будущее
|
||
- Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи»
|
||
упираются в API (отдаёт только прямые связи) — требуют доработки сервера.
|
||
- Превью в простое троттлит `requestAnimationFrame` (физика не идёт между вызовами) — для замеров
|
||
прокачивать кадры; в активном табе всё работает на 60 FPS.
|