Связи (pixel-web): фикс багов паутины — видимый 2-й уровень, удалён «прицел», toggle+spotlight
Исправлены замечания по видео (режим «Вселенная», только лаборатория):
1. «Невидимые друзья» (узлы 2-го уровня). Раньше — тусклые пустые кружки с инициалом.
Теперь tier-2 — полноценные аватарки: фото-лицо (pravatar) + имя, DEEP2_SCALE 0.5→0.62
(≈radius 16px), DEEP2_OPACITY 0.4→0.85; читаемый ободок и подпись (CSS).
2. Мусорный «прицел» (пунктирное кольцо у центра). Полностью удалён из движка:
элемент .fg-reticle, updateReticle/pulseReticle и все их вызовы, CSS .fg-reticle*.
На экране только аватарки и линии связей.
3. Логика toggle + spotlight:
- Повторный клик по раскрытому узлу теперь СВОРАЧИВАЕТ его (isOpen = pinned || expandP>0.5
→ сброс pinned+hovered) — работает, даже если ветка была раскрыта ховером.
- Spotlight: при закреплённой ветке остальные тускнеют до 0.25 (узлы и линии), фокус и
закреплённая/наведённая ветка — 100%; плавно через spotCur (lerp, см. spotTargetOf).
- Клик по центру (Иван) — collapseAll + возврат всему графу 100% яркости.
Реальный путь /network-view не затронут (deep-код под tier≥2/hasDeep). Ветка экспериментальная.
Бамп client.version → 1.2.145.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
72dc83daff
commit
f92e6c3cf1
@ -1,2 +1,2 @@
|
||||
client.version=1.2.144
|
||||
server.version=1.2.128
|
||||
client.version=1.2.145
|
||||
server.version=1.2.129
|
||||
|
||||
@ -61,7 +61,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-анимация
|
||||
- **Поллиш:** «дыхание» фокуса (бесконечная CSS-анимация
|
||||
размера, GPU, не будит rAF); свечение «сияющих» узлов — мягкая медленная пульсация (3.6с) многослойной
|
||||
`box-shadow` + размытый ореол через SVG-фильтр `#fg-shine-glow`; тестовые фото-аватарки (`NETWORK_PHOTOS`);
|
||||
хард-лимит ~90 DOM-аватарок (остальное — SVG-точки).
|
||||
@ -94,9 +94,14 @@
|
||||
втягивается. Реализация: `pointerover/out` (мышь) и `pointerdown/up` (палец) → `onNodeHover` →
|
||||
`graph.setHover(node|null)`; узел получает флаг `hovered`.
|
||||
- **Фиксация кликом (pin):** тап/клик по узлу → `graph.toggleExpand` ставит флаг `pinned` — ветка
|
||||
остаётся раскрытой и после ухода курсора. Повторный тап снимает фиксацию.
|
||||
остаётся раскрытой и после ухода курсора. Повторный клик по раскрытому узлу **сворачивает** его
|
||||
(надёжный toggle: `isOpen = pinned || expandP>0.5` → сброс `pinned`+`hovered`).
|
||||
- Эффективное раскрытие = `pinned || hovered` (см. `expandTargetOf`), прогресс `expandP` (~400мс).
|
||||
- **Глобальный сброс:** тап по корню (Иван) → `collapseAll()` снимает `pinned` и `hovered` со всех.
|
||||
- **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`) — кластеры разъезжаются, не накладываясь.
|
||||
- **Камера-доводчик:** при фиксации ветки, если её «веер» упирается в край экрана, камера мягко
|
||||
|
||||
@ -44,8 +44,8 @@ const MAX_FULL_NODES = 90; // хард-лимит полных DOM-ават
|
||||
|
||||
// «Мегамасштаб» — глубина 2-3 уровней (только лаборатория, фейковые данные). Узлы tier≥2 не
|
||||
// участвуют в физике: позиционируются детерминированно вокруг своего родителя (layoutDeep).
|
||||
const DEEP2_SCALE = 0.5; // узел 2-го уровня — вдвое меньше
|
||||
const DEEP2_OPACITY = 0.4; // и полупрозрачный
|
||||
const DEEP2_SCALE = 0.62; // узел 2-го уровня — ~вдвое меньше (radius ~16px), но с читаемым лицом/именем
|
||||
const DEEP2_OPACITY = 0.85; // почти непрозрачный — это полноценная аватарка, а не «дырка»
|
||||
const DEEP3_OPACITY = 0.9; // 3-й уровень — светящаяся микрозвезда (точка)
|
||||
const DEEP_R2 = 70; // радиус орбиты 2-го уровня вокруг родителя 1-го уровня, px
|
||||
const DEEP_R3 = 38; // радиус орбиты 3-го уровня вокруг родителя 2-го уровня, px
|
||||
@ -58,6 +58,7 @@ const ZOOM_WHEEL = 0.0016; // чувствительность колеса
|
||||
// Адаптивное расталкивание раскрытых веток (collision): пока ветка раскрыта (expandP→1), её узел
|
||||
// сильнее отталкивает соседей — кластеры «разъезжаются», как магниты, и не накладываются (паутина).
|
||||
const EXPAND_REPULSION = 2.4; // во сколько раз усиливается charge у полностью раскрытого узла
|
||||
const SPOTLIGHT_DIM = 0.25; // прозрачность «затемнённых» веток, когда какая-то ветка закреплена кликом
|
||||
// Камера-доводчик: мягкая дотяжка камеры, чтобы раскрытый кластер целиком попал в кадр (без рывков).
|
||||
const CAM_GLIDE_K = 0.12; // скорость дотяжки (lerp за кадр)
|
||||
const CAM_GLIDE_MARGIN = 18; // зазор от края экрана при дотяжке, px
|
||||
@ -131,11 +132,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
edgesSvg.setAttribute('class', 'fg-edges');
|
||||
const world = document.createElement('div');
|
||||
world.className = 'fg-world';
|
||||
// «Прицел» в центре экрана: сжимается, когда под центром никого нет, и расширяется,
|
||||
// когда под него попадает узел (визуальная зона фокуса при свободном панорамировании).
|
||||
const reticle = document.createElement('div');
|
||||
reticle.className = 'fg-reticle';
|
||||
stage.append(edgesSvg, world, reticle);
|
||||
stage.append(edgesSvg, world);
|
||||
ensureShineFilter(); // SVG-фильтр свечения «сияющих» (нужен до первой отрисовки узлов)
|
||||
|
||||
// Состояние камеры (панорамирование + зум)
|
||||
@ -154,8 +151,24 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
let focusId = '';
|
||||
let nodeById = new Map(); // id → node (для поиска родителя у глубоких уровней)
|
||||
let hasDeep = false; // есть ли в графе узлы tier≥2 (включает deep-ветки рендера/раскладки)
|
||||
let spotActive = false; // активен ли «spotlight» (есть закреплённая ветка → остальные тускнеют)
|
||||
const rebuildIndex = () => { nodeById = new Map(nodes.map((n) => [String(n.id), n])); hasDeep = nodes.some((n) => n.tier >= 2); };
|
||||
|
||||
// Spotlight: при закреплённой кликом ветке остальной граф тускнеет до SPOTLIGHT_DIM (0.25), чтобы
|
||||
// взгляд держался на раскрытом кластере. Узел «в свете», если он фокус, либо его корневая ветка
|
||||
// 1-го уровня закреплена (pinned) или временно наведена (hovered). Возвращает множитель прозрачности.
|
||||
function rootTier1(n) {
|
||||
let r = n; let guard = 0;
|
||||
while (r && r.tier >= 2 && guard++ < 8) r = nodeById.get(r.parentId) || null;
|
||||
return r;
|
||||
}
|
||||
function spotTargetOf(n) {
|
||||
if (!spotActive || n.isFocus) return 1;
|
||||
const r = rootTier1(n);
|
||||
if (!r) return SPOTLIGHT_DIM;
|
||||
return (r.pinned || r.hovered) ? 1 : SPOTLIGHT_DIM;
|
||||
}
|
||||
|
||||
// Управление циклом rAF
|
||||
let rafId = 0;
|
||||
let dragging = false;
|
||||
@ -264,6 +277,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
hidden: false,
|
||||
opacity: tier >= 2 ? op : 1,
|
||||
targetOpacity: op,
|
||||
spotCur: 1, // текущий множитель spotlight-затемнения (1 = полный свет)
|
||||
bloom: false,
|
||||
edgeGrow: 1, // прогресс «вырастания» линии этого узла 0→1 (для нового узла стартует с 0)
|
||||
el,
|
||||
@ -347,7 +361,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
node.parentId = String(src.parentId || '');
|
||||
node.deepAngle = Number(src.deepAngle) || node.deepAngle || hash01(`${src.id}~d`) * Math.PI * 2;
|
||||
node.track = Boolean(src.track);
|
||||
node.pinned = false; node.hovered = false; node.expandP = 0; // при перестроении глубина схлопывается
|
||||
node.pinned = false; node.hovered = false; node.expandP = 0; node.spotCur = 1; // при перестроении глубина схлопывается
|
||||
node.dotOnly = spec.dotOnly;
|
||||
node.strength = strength;
|
||||
node.relationType = src.relationType;
|
||||
@ -406,7 +420,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
camTargetX = null; camTargetY = null; // ручной зум отменяет доводчик
|
||||
applyWorldTransform();
|
||||
renderEdges();
|
||||
updateReticle();
|
||||
}
|
||||
function onWheel(ev) {
|
||||
ev.preventDefault();
|
||||
@ -419,7 +432,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
for (const n of nodes) {
|
||||
n.el.style.transform =
|
||||
`translate(calc(${n.x}px - 50%), calc(${n.y}px - 50%)) scale(${n.scale})`;
|
||||
n.el.style.opacity = String(n.opacity);
|
||||
n.el.style.opacity = String(n.opacity * (n.spotCur ?? 1)); // spotlight: затемнённые ветки тусклее
|
||||
n.el.style.pointerEvents = n.hidden && n.opacity <= 0.01 ? 'none' : '';
|
||||
}
|
||||
}
|
||||
@ -474,7 +487,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
for (const n of nodes) {
|
||||
if (n.tier < 2) continue;
|
||||
n.el.style.transform = `translate(calc(${n.x}px - 50%), calc(${n.y}px - 50%)) scale(${n.scale})`;
|
||||
n.el.style.opacity = String(n.opacity);
|
||||
n.el.style.opacity = String(n.opacity * (n.spotCur ?? 1)); // spotlight: затемнённые ветки тусклее
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,6 +567,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
// • СИЯЮЩАЯ — двухслойный неоновый «световод»: широкий размытый glow (под) + тонкий чёткий
|
||||
// core 1.5px (над) → изящно, но с объёмным OLED-свечением (см. .fg-edge-glow / .fg-edge-core).
|
||||
const shine = Boolean(n.shining) && !n.hidden;
|
||||
const sp = (n.spotCur ?? 1); // spotlight: линия тускнеет вместе со своим узлом (затемнённые ветки)
|
||||
const d = `M${x1.toFixed(1)} ${y1.toFixed(1)} Q${cpx.toFixed(1)} ${cpy.toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
||||
// ПРОРАСТАНИЕ из центра: dasharray = длина пути, dashoffset от длины к 0 по мере разлёта узла
|
||||
// (growP = доля пройденного узлом пути центр→орбита) → линия «вытягивается» из центра.
|
||||
@ -568,15 +582,16 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
const pe = parent.expandP || 0; // насколько раскрыт родитель (глубокие лучи видны вместе с детьми)
|
||||
if (n.tier >= 3) {
|
||||
// 3-й уровень: микрозвезда — еле заметная космическая ниточка, видна только при раскрытии
|
||||
if (pe > 0.02) parts.push(`<path d="${d}" fill="none" stroke="rgba(150, 205, 255, 0.7)" stroke-width="0.6" opacity="${(0.1 * pe).toFixed(2)}" />`);
|
||||
if (pe > 0.02) parts.push(`<path d="${d}" fill="none" stroke="rgba(150, 205, 255, 0.7)" stroke-width="0.6" opacity="${(0.1 * pe * sp).toFixed(2)}" />`);
|
||||
} else if (n.tier === 2) {
|
||||
// 2-й уровень: матовая паутинка, проявляется по мере раскрытия родителя (локальный bloom)
|
||||
if (pe > 0.02) parts.push(`<path d="${d}" fill="none" stroke="rgba(175, 200, 235, 0.9)" stroke-width="0.8" stroke-linecap="round" opacity="${(0.14 * pe).toFixed(2)}" />`);
|
||||
if (pe > 0.02) parts.push(`<path d="${d}" fill="none" stroke="rgba(175, 200, 235, 0.9)" stroke-width="0.8" stroke-linecap="round" opacity="${(0.14 * pe * sp).toFixed(2)}" />`);
|
||||
} else if (shine || n.track) {
|
||||
// glow (размытый, приглушённый) + core (тонкий, чёткий). Используется и для «трека прохождения»
|
||||
// (n.track) — линия к предыдущему фокусу горит так же ярко, показывая цепочку навигации.
|
||||
const gOp = (0.4 * nodeOpacity).toFixed(2);
|
||||
const cOpAttr = nodeOpacity < 0.995 ? ` opacity="${nodeOpacity.toFixed(2)}"` : '';
|
||||
const cOpVal = nodeOpacity * sp;
|
||||
const gOp = (0.4 * cOpVal).toFixed(2);
|
||||
const cOpAttr = cOpVal < 0.995 ? ` opacity="${cOpVal.toFixed(2)}"` : '';
|
||||
parts.push(`<path class="fg-edge-glow" d="${d}"${dashAttr} opacity="${gOp}" />`);
|
||||
parts.push(`<path class="fg-edge-core" d="${d}"${dashAttr}${cOpAttr} />`);
|
||||
} else {
|
||||
@ -590,28 +605,17 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
+ `<stop offset="1" stop-color="${relationColor(n.relationType)}" stop-opacity="0.07"/></linearGradient>`
|
||||
);
|
||||
const sw = (1.0 + n.strength * 0.2).toFixed(2); // 1.0–1.2px
|
||||
const op = nodeOpacity < 0.995 ? ` opacity="${nodeOpacity.toFixed(2)}"` : '';
|
||||
const opVal = nodeOpacity * sp;
|
||||
const op = opVal < 0.995 ? ` opacity="${opVal.toFixed(2)}"` : '';
|
||||
parts.push(`<path d="${d}" fill="none" stroke="url(#${gid})" stroke-width="${sw}" stroke-linecap="round"${op}${dashAttr} />`);
|
||||
}
|
||||
}
|
||||
edgesSvg.innerHTML = `<defs>${defs.join('')}</defs>${parts.join('')}`;
|
||||
}
|
||||
|
||||
function updateReticle() {
|
||||
// ближайший видимый узел к центру экрана (центр = camX/camY смещение от мировой точки 0,0)
|
||||
let best = Infinity;
|
||||
for (const n of nodes) {
|
||||
if (n.hidden) continue;
|
||||
const d = Math.hypot(camX + n.x * zoom, camY + n.y * zoom);
|
||||
if (d < best) best = d;
|
||||
}
|
||||
reticle.classList.toggle('is-locked', best < 46);
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderNodes();
|
||||
renderEdges();
|
||||
updateReticle();
|
||||
}
|
||||
|
||||
// --- Физика (пружины + отталкивание) ---------------------------------------
|
||||
@ -665,11 +669,13 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
return totalV;
|
||||
}
|
||||
|
||||
// Плавное приближение масштаба/прозрачности к целям + рост линии («прорастание»).
|
||||
// Плавное приближение масштаба/прозрачности к целям + рост линии («прорастание») + spotlight.
|
||||
function advanceVisual() {
|
||||
spotActive = nodes.some((n) => n.pinned); // есть закреплённая ветка → остальные тускнеют
|
||||
for (const n of nodes) {
|
||||
n.scale += (n.targetScale - n.scale) * 0.2;
|
||||
n.opacity += (n.targetOpacity - n.opacity) * 0.2;
|
||||
n.spotCur += (spotTargetOf(n) - n.spotCur) * 0.2; // плавное затемнение/прояснение веток
|
||||
// линия растёт только когда узел уже «выпущен» из центра (не скрыт) — догоняет его как нить
|
||||
if (!n.hidden && n.edgeGrow < 1) n.edgeGrow = Math.min(1, n.edgeGrow + 0.08);
|
||||
}
|
||||
@ -678,7 +684,8 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
// Не «успокоились» ли ещё визуальные параметры/рост линий (для условия заморозки).
|
||||
function visualSettling() {
|
||||
for (const n of nodes) {
|
||||
if (Math.abs(n.scale - n.targetScale) > 0.01 || Math.abs(n.opacity - n.targetOpacity) > 0.01 || n.edgeGrow < 1) return true;
|
||||
if (Math.abs(n.scale - n.targetScale) > 0.01 || Math.abs(n.opacity - n.targetOpacity) > 0.01
|
||||
|| Math.abs(n.spotCur - spotTargetOf(n)) > 0.01 || n.edgeGrow < 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -859,7 +866,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
layoutDeep();
|
||||
renderDeepNodes();
|
||||
renderEdges();
|
||||
updateReticle();
|
||||
schedule();
|
||||
return;
|
||||
}
|
||||
@ -956,8 +962,14 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
function toggleExpand(node) {
|
||||
const n = node && (nodeById.get(String(node.id)) || node);
|
||||
if (!n) return;
|
||||
n.pinned = !n.pinned;
|
||||
if (n.pinned) { haptic([10, 25, 6, 35, 3]); glideCameraTo(n); } // «выброс вселенной» + дотяжка камеры
|
||||
// надёжный toggle: клик по УЖЕ раскрытому узлу сворачивает его (даже если он был раскрыт ховером).
|
||||
const isOpen = n.pinned || (n.expandP || 0) > 0.5;
|
||||
if (isOpen) {
|
||||
n.pinned = false; n.hovered = false; // свернуть полностью
|
||||
haptic(8);
|
||||
} else {
|
||||
n.pinned = true; haptic([10, 25, 6, 35, 3]); glideCameraTo(n); // раскрыть + дотяжка камеры
|
||||
}
|
||||
wake();
|
||||
}
|
||||
// Ховер (наведение мышью / касание пальцем) — ВРЕМЕННОЕ раскрытие: ветка выплывает, пока курсор/палец
|
||||
@ -1078,7 +1090,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
applyWorldTransform();
|
||||
advancePanBend(); // упругий изгиб нитей догоняет палец во время свайпа
|
||||
renderEdges(); // рёбра следуют за камерой синхронно (дёшево)
|
||||
updateReticle();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1152,14 +1163,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
window.setTimeout(() => ghost.remove(), 1000); // удаление строго через 1000мс
|
||||
}
|
||||
|
||||
// Импульс центрального кольца — подтверждение «захвата» нового фокуса.
|
||||
function pulseReticle() {
|
||||
reticle.classList.remove('is-pulse');
|
||||
void reticle.offsetWidth;
|
||||
reticle.classList.add('is-pulse');
|
||||
window.setTimeout(() => reticle.classList.remove('is-pulse'), 620);
|
||||
}
|
||||
|
||||
// Во время CSS-bloom узлы двигает компоновщик — читаем их фактические позиции из DOM,
|
||||
// чтобы лучи (рисуются в JS) точно следовали за анимируемыми аватарками.
|
||||
function syncPositionsFromDOM() {
|
||||
@ -1296,7 +1299,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
cssBloom = true; // физика выключена; цикл лишь ведёт лучи за CSS-узлами
|
||||
cssBloomKind = 'bloom'; // после каскада — лёгкое упругое до-покачивание (boost)
|
||||
renderEdges();
|
||||
pulseReticle();
|
||||
cssBloomTimer = window.setTimeout(endCssBloom, BLOOM_MS + maxDelay + 80); // завершение гарантировано
|
||||
wake();
|
||||
}
|
||||
@ -1341,7 +1343,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
if (ro) ro.disconnect();
|
||||
edgesSvg.remove();
|
||||
world.remove();
|
||||
reticle.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -93,9 +93,11 @@ function addDeepLevels(model) {
|
||||
for (let i = 0; i < k2; i += 1) {
|
||||
const id2 = `${p.id}__d2_${i}`;
|
||||
const ang2 = (i / k2) * Math.PI * 2 + seed01(p.id) * 0.6;
|
||||
// узлы 2-го уровня — полноценные (видимые) аватарки: детерминированное фото-лицо (pravatar) + имя
|
||||
const face2 = `https://i.pravatar.cc/120?img=${1 + Math.floor(seed01(id2 + 'p') * 70)}`;
|
||||
extra.push({
|
||||
id: id2, login: id2, name: DEEP_NAMES[Math.floor(seed01(id2) * DEEP_NAMES.length)],
|
||||
avatar: null, photo: null, relationType: p.relationType || 'contact',
|
||||
avatar: null, photo: face2, relationType: p.relationType || 'contact',
|
||||
strength: 0.4, shining: false, tier: 2, parentId: String(p.id), deepAngle: ang2,
|
||||
});
|
||||
const k3 = 3 + Math.floor(seed01(id2 + ':3') * 3); // 3-5
|
||||
|
||||
@ -292,15 +292,15 @@
|
||||
.fg-dot.is-shine { box-shadow: 0 0 9px rgba(130, 235, 255, 0.75); }
|
||||
|
||||
/* === Глубина 2-3 уровней (прототип «Вселенная», только лаборатория) ============== */
|
||||
/* 2-й уровень — «друзья друзей»: масштаб/прозрачность задаёт движок; тут убираем жирные тени
|
||||
и делаем подпись мельче/тусклее, чтобы дальние узлы не спорили с основными. */
|
||||
/* 2-й уровень — «друзья друзей»: полноценная аватарка (лицо + имя), просто мельче основных.
|
||||
Масштаб/прозрачность задаёт движок; здесь — читаемый ободок и подпись (не «дырка»). */
|
||||
.fg-node.is-tier2 .node-dot {
|
||||
border-color: rgba(150, 180, 220, 0.4);
|
||||
box-shadow: none;
|
||||
border-color: rgba(170, 200, 240, 0.65);
|
||||
box-shadow: 0 2px 8px rgba(4, 8, 15, 0.4);
|
||||
}
|
||||
.fg-node.is-tier2 .fg-node-label {
|
||||
font-size: 9px;
|
||||
opacity: 0.55;
|
||||
opacity: 0.9;
|
||||
top: calc(100% + 1px);
|
||||
}
|
||||
|
||||
@ -337,30 +337,6 @@
|
||||
color: #efeaff;
|
||||
}
|
||||
|
||||
/* «Прицел» в центре экрана (зона фокуса) — позади узлов */
|
||||
.fg-reticle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: -32px 0 0 -32px;
|
||||
border-radius: 50%;
|
||||
border: 2px dashed rgba(150, 190, 255, 0.3);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.45;
|
||||
transition: width 200ms ease, height 200ms ease, margin 200ms ease, border-color 200ms ease, opacity 200ms ease;
|
||||
}
|
||||
|
||||
.fg-reticle.is-locked {
|
||||
width: 94px;
|
||||
height: 94px;
|
||||
margin: -47px 0 0 -47px;
|
||||
border-color: rgba(130, 235, 255, 0.65);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* «Призрак» старой карты при Z-переходе (эффект погружения) */
|
||||
.fg-ghost-layer {
|
||||
position: absolute;
|
||||
@ -373,17 +349,6 @@
|
||||
transition: transform 1000ms cubic-bezier(0.16, 1, 0.3, 1), opacity 1000ms ease;
|
||||
}
|
||||
|
||||
/* Импульс центрального кольца при захвате нового фокуса */
|
||||
.fg-reticle.is-pulse {
|
||||
animation: fg-reticle-pulse 0.6s ease;
|
||||
}
|
||||
|
||||
@keyframes fg-reticle-pulse {
|
||||
0% { transform: scale(1); }
|
||||
40% { transform: scale(1.22); border-color: rgba(130, 235, 255, 0.9); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Панель фильтров слоёв (оверлей под шапкой) */
|
||||
.fg-filter-bar {
|
||||
position: absolute;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user