SHA256
Связи (pixel-aquarium): Умный фокус (Smart Zoom / «аквариум») — погружение в узел + LOD
Новая ветка для безопасного отката (от pixel-web). Режим «Вселенная», только лаборатория.
1. Гибрид клика: 1-й уровень → раскрытие ветки НА МЕСТЕ (как раньше); 2-й уровень+ → ПОГРУЖЕНИЕ.
2. Dive (умный наезд камеры, «аквариум», без перестройки графа):
- diveTo(node): пинит весь путь (предки до Ивана), ставит diveTargetId + diveZoom=1.7;
камера в tick плавно ЛЕТИТ и ЗУМИТ, центрируя узел (DIVE_FLY_K), узел ВЫРАСТАЕТ (×2.1 ~ герой).
- Глубина (contextTargetOf → depthScale/depthBlur/spotCur, лерп): Иван и боковые ветки
УМЕНЬШАЮТСЯ (root ×0.55) + уходят в BLUR 3px + тускнеют до 0.25 → задний план «аквариума».
- Нить-крошка: путь Иван→…→узел (divePathSet/onPath) горит ярким «световодом» — виден путь назад.
- Всплытие: повтор клика по цели → exitDive (камера/зум плавно к корню); клик по Ивану →
collapseAll (полный сброс + всплытие).
3. Pinch-to-Zoom + LOD 3-го уровня: при zoom≥1.55 видимые точки 3-го уровня дорисовываются как
читаемые аватарки (лицо+имя; updateLod/setNodeLod — пере-рендер DOM на пороге), при отдалении —
обратно в светящиеся точки. Узлам tier-3 добавлены фото-заглушки (pravatar) и имена.
Глубина — фейк-3D через масштаб + CSS-blur (GPU), без WebGL. Реальный путь /network-view не затронут:
dive только tier≥2 (в реале их нет), depthScale/Blur нейтральны по умолчанию, updateLod выходит при !hasDeep.
Бамп client.version → 1.2.146.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,18 @@ const SPOTLIGHT_DIM = 0.25; // прозрачность «затемнённ
|
||||
const CAM_GLIDE_K = 0.12; // скорость дотяжки (lerp за кадр)
|
||||
const CAM_GLIDE_MARGIN = 18; // зазор от края экрана при дотяжке, px
|
||||
|
||||
// Умный фокус (Smart Zoom / «аквариум»): клик по узлу 2-го уровня — камера летит и зумит к нему,
|
||||
// он вырастает в центр; Иван и боковые ветки уменьшаются + расфокус (blur) на задний план;
|
||||
// нить-крошка обратно к Ивану остаётся яркой. При сильном зуме точки 3-го уровня → аватарки (LOD).
|
||||
const DIVE_ZOOM = 1.7; // зум камеры при погружении
|
||||
const DIVE_FLY_K = 0.13; // скорость «полёта» камеры/зума к узлу (lerp за кадр)
|
||||
const DIVE_TARGET_MUL = 2.1; // множитель масштаба «нырнутого» узла (вырастает в герой-центр)
|
||||
const DIVE_PATH_MUL = 0.72; // предки на пути назад — чуть мельче (видимая «цепочка крошек»)
|
||||
const DIVE_ROOT_MUL = 0.55; // корень (Иван) уходит вглубь сильнее всех
|
||||
const DIVE_OFFPATH_MUL = 0.55; // боковые ветки (вне пути) — уменьшаются на задний план
|
||||
const DIVE_BLUR = 3; // размытие фоновых (вне пути) узлов — эффект расфокуса/глубины, px
|
||||
const LOD_ZOOM = 1.55; // порог зума, на котором точки 3-го уровня превращаются в аватарки
|
||||
|
||||
const RELATION_COLORS = {
|
||||
family: 'rgba(255, 159, 94, 0.92)',
|
||||
friend: 'rgba(120, 179, 255, 0.9)',
|
||||
@@ -152,6 +164,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
let nodeById = new Map(); // id → node (для поиска родителя у глубоких уровней)
|
||||
let hasDeep = false; // есть ли в графе узлы tier≥2 (включает deep-ветки рендера/раскладки)
|
||||
let spotActive = false; // активен ли «spotlight» (есть закреплённая ветка → остальные тускнеют)
|
||||
let diveTargetId = null; // id «нырнутого» узла (Smart Zoom); null — мы на верхнем уровне (Иван)
|
||||
let diveZoom = 1; // целевой зум активного погружения
|
||||
let surfacing = false; // идёт «всплытие» назад (камера/зум возвращаются к корню)
|
||||
const rebuildIndex = () => { nodeById = new Map(nodes.map((n) => [String(n.id), n])); hasDeep = nodes.some((n) => n.tier >= 2); };
|
||||
|
||||
// Spotlight: при закреплённой кликом ветке остальной граф тускнеет до SPOTLIGHT_DIM (0.25), чтобы
|
||||
@@ -169,6 +184,36 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
return (r.pinned || r.hovered) ? 1 : SPOTLIGHT_DIM;
|
||||
}
|
||||
|
||||
// Путь-«крошки» от корня (Иван) до нырнутого узла включительно — для подсветки нити назад и глубины.
|
||||
function divePathSet() {
|
||||
const set = new Set();
|
||||
if (!diveTargetId) return set;
|
||||
let cur = nodeById.get(diveTargetId); let guard = 0;
|
||||
while (cur && guard++ < 16) { set.add(String(cur.id)); if (cur.isFocus) break; const p = nodeById.get(cur.parentId); if (!p) break; cur = p; }
|
||||
set.add(String(focusId));
|
||||
return set;
|
||||
}
|
||||
let _pathSet = new Set();
|
||||
let _pathSetKey = ' | ||||