Связи (pixel-aquarium, 10.06): фикс физики раскрытия + камера-наезд + железный spotlight
Исправлены критические баги «аквариума» по разбору (видео): 1. Слипание узлов → адаптивный радиус орбиты (фикс collision). Дети раскладываются на кольце ringR = max(baseR + радиус_родителя, число_детей×13): не лезут на (увеличенный зумом) родитель и друг на друга. Проверено: мин. дистанция 125px, 0 наложений (было — все в одной точке). 2. Умный наезд камеры на КЛИК по любому узлу (раньше 1-й уровень раскрывался на месте). diveTo центрирует узел (offset ~0), zoom 1.7; узел и дети растут до единого видимого размера (HERO_VISUAL/baseScaleOf, DIVE_CHILD_VISUAL) — крупно и читаемо. Наведение остаётся лёгким превью. 3. Железный Spotlight (единый активный путь): diveTo гасит ВСЕ прежние pin/hover, затем раскрывает только путь к цели. Открыто → путь=1.0, остальное=0.25; переключение веток сбрасывает прежнюю; exitDive/тап по Ивану → ВСЕ узлы гарантированно 1.0 + камера отъезжает. (Проверено программно.) Реальный путь /network-view не затронут (вся глубина под tier≥2/hasDeep). Бамп client.version → 1.2.147. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a8852f64b
commit
3012f0799b
@ -1,2 +1,2 @@
|
||||
client.version=1.2.146
|
||||
server.version=1.2.130
|
||||
client.version=1.2.147
|
||||
server.version=1.2.131
|
||||
|
||||
@ -114,18 +114,22 @@
|
||||
- Мерцающие микрозвёзды 3-го уровня (`fg-star-twinkle`), хаптика (`navigator.vibrate`) на нажатие/раскрытие/натяжение.
|
||||
|
||||
### Умный фокус (Smart Zoom / «аквариум») — ветка `pixel-aquarium`
|
||||
Клик по узлу разный по уровню (гибрид): **1-й уровень** — раскрытие ветки НА МЕСТЕ (как выше);
|
||||
**2-й уровень+** — **погружение (dive)**:
|
||||
- **Камера-полёт + зум** (`diveTo` → `diveTargetId`/`diveZoom=1.7`, лёт в `tick` с `DIVE_FLY_K`): узел
|
||||
плавно центрируется и **вырастает** (`DIVE_TARGET_MUL=2.1` → ~герой-размер).
|
||||
**Наведение** (hover/палец) на узел — лёгкое превью ветки (раскрытие на месте, без камеры).
|
||||
**Клик/тап по ЛЮБОМУ узлу** — **погружение (dive)** с кинематографичным наездом:
|
||||
- **Камера-полёт + зум** (`diveTo` → `diveTargetId`/`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 наложений). Радиус растёт вместе с зумом родителя.
|
||||
- **Глубина «аквариума»** (`contextTargetOf` → `depthScale`/`depthBlur`/`spotCur`, лерп): Иван и боковые
|
||||
ветки **уменьшаются** (root ×0.55, фон ×0.55) + уходят в **blur 3px** + тускнеют до 0.25 → задний план.
|
||||
- **Нить-крошка**: путь Иван → … → узел (`divePathSet`/`onPath`) горит ярким «световодом» — видно путь назад.
|
||||
- **Всплытие**: повторный клик по узлу-цели → `exitDive` (камера/зум плавно возвращаются к корню);
|
||||
клик по Ивану → `collapseAll` (полный сброс + всплытие).
|
||||
- **Железный 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` — пере-рендер DOM на пороге),
|
||||
при отдалении сворачиваются обратно в светящиеся точки.
|
||||
уровня **дорисовываются как аватарки** (`updateLod`/`setNodeLod`), при отдалении — обратно в точки.
|
||||
- Глубина — фейк-3D через масштаб + CSS-`blur` (GPU), без WebGL.
|
||||
|
||||
> ⚠️ Эксперименты на ветках `pixel-web` (паутина) и `pixel-aquarium` (Smart Zoom) — для отката.
|
||||
|
||||
@ -66,9 +66,10 @@ const CAM_GLIDE_MARGIN = 18; // зазор от края экрана при
|
||||
// Умный фокус (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_ZOOM = 1.7; // зум камеры при погружении (наезд ~600мс)
|
||||
const DIVE_FLY_K = 0.13; // скорость «полёта» камеры/зума к узлу (lerp за кадр) ≈ 600мс до цели
|
||||
const HERO_VISUAL = 1.4; // желаемый ВИДИМЫЙ масштаб нырнутого узла — одинаков для tier-1/2/3 (читаемо)
|
||||
const DIVE_CHILD_VISUAL = 0.95; // желаемый видимый масштаб его прямых детей (крупно/читаемо)
|
||||
const DIVE_PATH_MUL = 0.72; // предки на пути назад — чуть мельче (видимая «цепочка крошек»)
|
||||
const DIVE_ROOT_MUL = 0.55; // корень (Иван) уходит вглубь сильнее всех
|
||||
const DIVE_OFFPATH_MUL = 0.55; // боковые ветки (вне пути) — уменьшаются на задний план
|
||||
@ -167,7 +168,13 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
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); };
|
||||
let childCountByParent = new Map(); // parentId → число детей (для адаптивного радиуса орбиты, без слипания)
|
||||
const rebuildIndex = () => {
|
||||
nodeById = new Map(nodes.map((n) => [String(n.id), n]));
|
||||
hasDeep = nodes.some((n) => n.tier >= 2);
|
||||
childCountByParent = new Map();
|
||||
for (const n of nodes) { if (n.tier >= 2 && n.parentId) childCountByParent.set(n.parentId, (childCountByParent.get(n.parentId) || 0) + 1); }
|
||||
};
|
||||
|
||||
// Spotlight: при закреплённой кликом ветке остальной граф тускнеет до SPOTLIGHT_DIM (0.25), чтобы
|
||||
// взгляд держался на раскрытом кластере. Узел «в свете», если он фокус, либо его корневая ветка
|
||||
@ -200,14 +207,23 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
if (k !== _pathSetKey) { _pathSetKey = k; _pathSet = divePathSet(); }
|
||||
return _pathSet;
|
||||
}
|
||||
// Базовый масштаб узла по его роли/уровню (как в makeNodeState) — чтобы привести героя и его детей
|
||||
// к ОДИНАКОВОМУ видимому размеру независимо от tier (depthScale = желаемый_видимый / базовый).
|
||||
function baseScaleOf(n) {
|
||||
if (n.isFocus) return FOCUS_SCALE;
|
||||
if (n.tier >= 3) return n.lod === 'full' ? 0.42 : 1;
|
||||
if (n.tier === 2) return DEEP2_SCALE;
|
||||
return PRIMARY_SCALE;
|
||||
}
|
||||
// Контекст узла: целевые { прозрачность, множитель масштаба, размытие } для эффекта «аквариума»/spotlight.
|
||||
// Погружение (dive) имеет приоритет: нырнутый узел крупный/чёткий, путь назад — чёткий, фон — мелкий/blur.
|
||||
function contextTargetOf(n) {
|
||||
if (diveTargetId) {
|
||||
const ps = ensurePathSet();
|
||||
const id = String(n.id);
|
||||
if (id === diveTargetId) return { op: 1, scale: DIVE_TARGET_MUL, blur: 0 }; // герой-центр
|
||||
if (String(n.parentId) === diveTargetId) return { op: 1, scale: 1, blur: 0 }; // его дети — новая ветка
|
||||
// герой и его прямые дети — до фиксированного ВИДИМОГО масштаба (множитель = желаемый/базовый)
|
||||
if (id === diveTargetId) return { op: 1, scale: Math.max(0.8, Math.min(3.8, HERO_VISUAL / baseScaleOf(n))), blur: 0 };
|
||||
if (String(n.parentId) === diveTargetId) return { op: 1, scale: Math.max(0.8, Math.min(3.2, DIVE_CHILD_VISUAL / baseScaleOf(n))), blur: 0 };
|
||||
if (ps.has(id)) return { op: 1, scale: n.isFocus ? DIVE_ROOT_MUL : DIVE_PATH_MUL, blur: 0 }; // путь назад
|
||||
return { op: SPOTLIGHT_DIM, scale: DIVE_OFFPATH_MUL, blur: DIVE_BLUR }; // фон (расфокус)
|
||||
}
|
||||
@ -500,7 +516,13 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
const p = nodeById.get(n.parentId);
|
||||
if (!p) { n.opacity = 0; continue; }
|
||||
const e = p.expandP || 0; // насколько раскрыт родитель
|
||||
const r = (tier === 2 ? DEEP_R2 : DEEP_R3) * e; // при e=0 — в центре родителя, при 1 — на орбите
|
||||
// АДАПТИВНЫЙ радиус орбиты (фикс слипания): дети не лезут на (возможно увеличенный зумом) родитель
|
||||
// и не накладываются друг на друга. Клиренс = радиус родителя + базовый отступ; место = по числу детей.
|
||||
const baseR = tier === 2 ? DEEP_R2 : DEEP_R3;
|
||||
const pr = 26 * (p.scale || 1) * (p.depthScale || 1); // визуальный радиус родителя (world-единицы)
|
||||
const cnt = childCountByParent.get(n.parentId) || 1;
|
||||
const ringR = Math.max(baseR + pr, cnt * 13); // расталкивание: круговой «полукруг/орбита» без наложений
|
||||
const r = ringR * e; // при e=0 — в центре родителя, при 1 — на полной орбите
|
||||
n.x = p.x + Math.cos(n.deepAngle) * r;
|
||||
n.y = p.y + Math.sin(n.deepAngle) * r;
|
||||
const baseOp = tier === 2 ? DEEP2_OPACITY : DEEP3_OPACITY;
|
||||
@ -1107,9 +1129,11 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
// узел вырастает в центр, путь назад к Ивану остаётся ярким, фон уходит в расфокус (см. contextTargetOf).
|
||||
function diveTo(node) {
|
||||
const n = node && (nodeById.get(String(node.id)) || node);
|
||||
if (!n || n.isFocus || n.tier < 2) return; // ныряем только в глубокие узлы
|
||||
if (diveTargetId === String(n.id)) { exitDive(); return; } // повтор по цели — всплыть назад
|
||||
// раскрываем весь путь (предки до Ивана) — чтобы цель и её дети гарантированно были видимы
|
||||
if (!n || n.isFocus) return; // в сам фокус (Иван) не ныряем
|
||||
if (diveTargetId === String(n.id)) { exitDive(); return; } // повтор по цели — всплыть назад (полный сброс)
|
||||
// ЕДИНЫЙ активный путь (железный spotlight): гасим ВСЕ прежние фиксации/ховеры, затем раскрываем
|
||||
// только путь к новой цели (предки до Ивана) — чтобы цель и её дети были видимы, прочее не «копилось».
|
||||
for (const m of nodes) { m.pinned = false; m.hovered = false; }
|
||||
let cur = n; let guard = 0;
|
||||
while (cur && guard++ < 16) { if (!cur.isFocus) cur.pinned = true; if (cur.isFocus) break; const p = nodeById.get(cur.parentId); if (!p) break; cur = p; }
|
||||
diveTargetId = String(n.id);
|
||||
@ -1119,9 +1143,10 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
haptic([12, 30, 8, 40]); // «погружение» — нарастающий импульс
|
||||
wake();
|
||||
}
|
||||
// Всплытие: выходим из погружения на уровень назад (камера и зум плавно возвращаются к корню).
|
||||
// Всплытие/закрытие ветки: ПОЛНЫЙ сброс — снимаем все фиксации/ховеры (дети втягиваются),
|
||||
// камера и зум плавно возвращаются на весь граф, ВСЕ узлы гарантированно вернут opacity 1.
|
||||
function exitDive() {
|
||||
if (!diveTargetId) return;
|
||||
for (const m of nodes) { m.pinned = false; m.hovered = false; }
|
||||
diveTargetId = null;
|
||||
surfacing = true;
|
||||
haptic(10);
|
||||
|
||||
@ -47,10 +47,10 @@ function helpText() {
|
||||
'• Долгое нажатие — контекстное меню (в реальном пути «Связи»).',
|
||||
'• Чипы под шапкой — фильтры слоёв: Все / Семья / Друзья / Сияющие.',
|
||||
'• Чип «Вселенная» — режим «Интерактивная паутина» (глубина 2-3 уровней, фейковые связи).',
|
||||
' Наведи мышь/палец на узел 1-го уровня — его микро-связи временно выплывают (превью);',
|
||||
' клик/тап — раскрытие ФИКСИРУЕТСЯ. Клик по маленькому узлу 2-го уровня — «умный наезд»:',
|
||||
' камера летит к нему, он вырастает в центр, Иван уходит вглубь (нить-крошка назад горит).',
|
||||
' Клик по нему ещё раз — всплыть назад. Тап по центру (Ивану) — полный сброс.',
|
||||
' Наведи мышь/палец на узел — его связи временно выплывают (превью). КЛИК по узлу — «умный',
|
||||
' наезд»: камера летит и центрирует его, он вырастает, друзья раскрываются крупно вокруг,',
|
||||
' путь назад к Ивану горит нитью, остальное затемняется. Клик по нему ещё раз — всплыть.',
|
||||
' Тап по центру (Ивану) — полный сброс (всё на 100%, камера отъезжает).',
|
||||
' Колесо мыши / щипок двумя пальцами — зум; при сильном приближении точки 3-го уровня',
|
||||
' превращаются в аватарки. Свайп — pan.',
|
||||
'',
|
||||
@ -198,10 +198,9 @@ export function renderNetworkLab({ navigate }) {
|
||||
// в режиме «Вселенная» вокруг нового центра процедурно генерится дальняя орбита.
|
||||
onNodeTap: (node) => {
|
||||
if (deepMode) {
|
||||
// 2-й уровень+ → Smart Zoom (умный наезд камеры, «аквариум»): узел выплывает в центр,
|
||||
// Иван уходит вглубь, нить-крошка назад горит. 1-й уровень → раскрытие ветки НА МЕСТЕ.
|
||||
if ((node.tier || 1) >= 2) graph.diveTo(node);
|
||||
else graph.toggleExpand(node);
|
||||
// Умный фокус: клик по ЛЮБОМУ узлу (Нина/её друг) — камера наезжает и центрирует его, ветка
|
||||
// раскрывается, путь назад к Ивану горит, остальное затемняется (0.25). Повтор по нему — всплыть.
|
||||
graph.diveTo(node);
|
||||
return;
|
||||
}
|
||||
// обычный режим: перецентрирование на выбранного человека (+ трек прохождения)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user