SHA256
Переработка экрана «Связи» в интерактивный нод-граф с премиальными переходами. Движок (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>
87 lines
4.0 KiB
JavaScript
87 lines
4.0 KiB
JavaScript
// Лабораторный режим карты связей (network-view/lab).
|
|
//
|
|
// Назначение: на мок-данных (без бэкенда) щупать физику force-графа, панорамирование,
|
|
// центрирование и навигацию между пользователями. Используется связанный мульти-граф
|
|
// networkGraphUsers: у каждого пользователя свой набор связей, тап по узлу переключает
|
|
// карту на сеть этого человека (как реальный путь, но локально).
|
|
|
|
import { renderHeader } from '../../components/header.js';
|
|
import { networkGraphUsers } from '../../mock-data.js';
|
|
import { createForceGraph, buildModelFromTz } from './force-graph.js';
|
|
import { openNodeMenu } from './node-menu.js';
|
|
|
|
const START_LOGIN = 'ivan';
|
|
|
|
function helpText() {
|
|
return [
|
|
'Лаборатория карты связей (мок-данные, без сервера).',
|
|
'• Тащите по экрану — карта свободно перемещается (pan).',
|
|
'• Тап по узлу — он стягивается в центр и карта перестраивается под его сеть.',
|
|
'• Тап по центральному узлу — здесь открылся бы профиль.',
|
|
'• Долгое нажатие — контекстное меню (в реальном пути «Связи»).',
|
|
'',
|
|
'Доступные люди: Иван, Алиса, Павел, Елена, Дмитрий, Олег, Нина, Марина, Света, Кирилл.',
|
|
].join('\n');
|
|
}
|
|
|
|
// Граф пользователя по логину; если такого нет в датасете — одинокий узел (без связей).
|
|
function graphForLogin(login) {
|
|
const key = String(login || '').trim().toLowerCase();
|
|
if (networkGraphUsers[key]) return networkGraphUsers[key];
|
|
return { focusUser: { id: key, login: key, name: key, avatar: null, status: '' }, connections: [] };
|
|
}
|
|
|
|
export function renderNetworkLab({ navigate }) {
|
|
const screen = document.createElement('section');
|
|
screen.className = 'network-screen';
|
|
|
|
const appScreenEl = document.getElementById('app-screen');
|
|
appScreenEl?.classList.add('network-scroll-lock');
|
|
|
|
const stage = document.createElement('div');
|
|
stage.className = 'network-stage fg-stage';
|
|
|
|
const header = renderHeader({
|
|
title: 'Связи · лаборатория',
|
|
leftAction: {
|
|
label: '←',
|
|
onClick: () => navigate('network-view'),
|
|
},
|
|
rightActions: [
|
|
{ label: '?', onClick: () => window.alert(helpText()) },
|
|
],
|
|
});
|
|
header.classList.add('network-header-overlay');
|
|
|
|
const model = buildModelFromTz(graphForLogin(START_LOGIN));
|
|
|
|
stage.append(header);
|
|
screen.append(stage);
|
|
|
|
// Монтируем движок синхронно (не через rAF): размеры сцены движок берёт с запасом
|
|
// (window.innerWidth) и корректирует через ResizeObserver, когда сцена попадёт в DOM.
|
|
const graph = createForceGraph({
|
|
stage,
|
|
model,
|
|
// тап по узлу — переключаем карту на сеть выбранного человека
|
|
onNodeTap: (node) => { graph.setModel(buildModelFromTz(graphForLogin(node.login))); },
|
|
onCenterTap: (node) => window.alert(`Профиль: ${node.name || node.login || node.id}`),
|
|
onNodeLongPress: (node, point) => openNodeMenu({
|
|
login: node.name || node.login || node.id,
|
|
relationType: node.relationType,
|
|
point,
|
|
actions: [
|
|
{ label: 'Профиль', onClick: () => window.alert(`Профиль: ${node.name || node.login}`) },
|
|
{ label: 'Написать', onClick: () => window.alert(`Написать: ${node.name || node.login}`) },
|
|
],
|
|
}),
|
|
});
|
|
|
|
screen.cleanup = () => {
|
|
graph.destroy();
|
|
appScreenEl?.classList.remove('network-scroll-lock');
|
|
};
|
|
|
|
return screen;
|
|
}
|