SHiNE-server/shine-UI/Dev_Docs/features/interactive-network-graph.md

22 KiB
Raw Blame History

Интерактивная карта связей (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.jsnetworkGraphUsers (связанный мульти-граф из 10 человек для лаборатории).
  • styles/network-graph.css — все стили .fg-*.

Данные (read-only, сервер не трогаем)

Единый источник — authService.getUserConnectionsGraph(login) (один запрос: логин → прямые связи). network-view.jsbuildGraphModel() нормализует роли (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 порог суммарной
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 (палец) → onNodeHovergraph.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) — кластеры разъезжаются, не накладываясь.
  • Камера-доводчик: при фиксации ветки, если её «веер» упирается в край экрана, камера мягко дотягивается (glideCameraTocamTargetX/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) с кинематографичным наездом:

  • Камера-полёт + зум (diveTodiveTargetId/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 наложений). Радиус растёт вместе с зумом родителя.
  • Глубина «аквариума» (contextTargetOfdepthScale/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.

Полиш (партия 1): веер детей раскрывается полукругом «наружу» (от пути назад, DEEP_FAN, по sibIndex) — не перекрывает нить-крошку; LOD с гистерезисом (LOD_ZOOM_UP=1.6/DOWN=1.4 — без мигания у порога); двойной тап по фону и сильный pinch-out на мин. зуме = быстрый выход; префетч аватарок детей при наведении/нырке.

Фишки (партия 2, лаборатория):

  • Поиск + телепорт — строка .fg-search; Enter → graph.findNode(имя) → камера летит к узлу (dive в «Вселенной», иначе перецентр).
  • Хлебные крошки.fg-breadcrumb «Иван Нина Ада» (движок шлёт onDiveChange(path), API getDivePath()); клик по корню — полный сброс, по предку — навигация на тот уровень.
  • Бейдж числа связей.fg-node-badge (число из degreeById, обновляется в updateBadges).
  • Цветовые кластеры — мягкая аура узла по типу связи (CSS is-family/friend/business/contact).

Линии-«жгуты» (партия 4, по референсу — плазменный композитинг):

  • Сияющие — ОДИН центральный S-путь (cubic Bézier) + ТРИ наложенных слоя с ОДИНАКОВЫМ d (объём из толщины+размытия, НЕ из геометрии — никаких расходящихся линий): Настоящий НЕОН (видимый ореол вокруг яркого ядра; поле/трубка в mix-blend-mode: screen — свет складывается аддитивно с тёмным фоном, а в пересечениях у центра ярче — энергохаб):
    • .fg-plasma-flare — плазменное облако: 16px, #00bfff, opacity 0.42, feGaussianBlur stdDev=6, screen (+ «дыхание» 3.6с);
    • .fg-plasma-tube — направляющий свет: 6px, #00e5ff, opacity 0.85, feGaussianBlur stdDev=2, screen;
    • .fg-plasma-core — ядро: 2px, #dffaff (светло-голубо-белое), opacity 1, без размытия. Толщина/насыщенность подогнаны под референс (толстая яркая голубая плазма, гладкие края). S-волна спокойная/изящная (amp до 13px). Размытие — именно SVG-фильтры (#fg-plasma-blur6/2), т.к. CSS-filter на <path> в части мобильных WebView не применяется (отсюда был «плоский»/«канатный» вид). ⚠️ Это НЕ Canvas-движок (не библиотека force-graph): связи — реальные SVG <path>, фильтры применяются. Прозрачность слоёв inline (× spotlight/глубину). Тяжёлый blur только у сияющих (их мало) — перф.
  • Не-сияущие — мягкое свечение в цвете связи (семья/друзья/бизнес/контакт): широкая полупрозрачная подложка + тонкое ядро, без SVG-blur (дёшево). «Похоже, но тише».

Фишки (партия 3, лаборатория):

  • Общие связи — среди друзей человека один помечен как «общий» (он и твой друг тоже): золотой ободок + ★ (CSS is-common; в лаб-генерации addDeepLevels подставляет узнаваемого друга Ивана).
  • Доступность — визуально скрытый (sr-only) текстовый список графа .fg-a11y (центр + связи 1-го уровня) для скринридеров; обновляется в updateA11y при перестроении. Полезно и для реального пути.

Автопроверки (?fgtest): js/pages/network/selftest.js автозапускается в лаборатории при ?fgtest, прогоняет 17 ассертов (центровка/collision/полукруг/spotlight/переключение/LOD/поиск/крошки/бейдж/выход) через детерминированные dev-хелперы движка graph.debugState() и graph.pumpForTest() (синхронно докручивают кадры до покоя — не зависят от троттлинга rAF). Результат → консоль и window.__fgTestResults. В обычной работе не активны.

⚠️ Эксперименты на ветках pixel-web (паутина) и pixel-aquarium (Smart Zoom) — для отката. Реальный путь /network-view не затронут: deep-код под tier ≥ 2 / hasDeep, dive — только tier≥2 (в реальном пути их нет), depthScale/Blur по умолчанию нейтральны, updateLod выходит при !hasDeep.

Ограничения / на будущее

  • Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи» упираются в API (отдаёт только прямые связи) — требуют доработки сервера.
  • Превью в простое троттлит requestAnimationFrame (физика не идёт между вызовами) — для замеров прокачивать кадры; в активном табе всё работает на 60 FPS.