Связи: двухслойные линии-световоды, живой фон и стеклянные фильтры
- Сияющие связи — двухслойный неоновый «световод»: размытый glow (4px, blur 2px, opacity 0.4) + тонкий чёткий core (1.5px, #e0f7fc). Объёмное OLED-свечение, линия остаётся изящной. Оба слоя растут синхронно (общий dashoffset). - Обычные линии — тоньше (1.0–1.2px) и глубокий уход в прозрачность (0.42 → 0.07), чтобы матовые связи не спорили с сияющими. - Живой фон-«небула»: глубокое размытое сине-голубое облако под центром, медленная пульсация радиуса/яркости + переливы индиго↔ультрамарин (hue-rotate, 7с). - Стеклянные чипы фильтров (frosted glass): rgba(255,255,255,0.03) + backdrop blur(12px) + граница 0.5px solid rgba(255,255,255,0.1); активный подсвечен сине-голубым. - Бамп client.version → 1.2.138; документация фичи обновлена. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ee6bf4380
commit
dc96033cb1
@ -1,2 +1,2 @@
|
||||
client.version=1.2.137
|
||||
client.version=1.2.138
|
||||
server.version=1.2.127
|
||||
|
||||
@ -42,10 +42,13 @@
|
||||
органичного покачивания; после фильтра физика НЕ включается (фиксация на равномерных углах).
|
||||
- **Жёсткая заморозка (kill-switch):** когда сумма |vx|+|vy| < 0.03 — скорости обнуляются, координаты
|
||||
округляются, `cancelAnimationFrame` (sleep). Нет «треска», батарея не страдает.
|
||||
- **Линии:** SVG `<path> Q` (квадратичные Безье) — тонкие изящные дуги (`stroke-width ~1.3–2.2`),
|
||||
градиент неон-центр → цвет роли. Изгиб реагирует на скорость.
|
||||
- **Сияющие связи:** линия к «сияющему» узлу — ярче (градиент в неон) и МОНОЛИТНО светится статичной
|
||||
тенью `filter: drop-shadow(...)` (тот же приём, что у ободка аватарки). Без бегущих импульсов.
|
||||
- **Обычные линии:** SVG `<path> Q` (квадратичные Безье) — тонкие матовые дуги (`stroke-width ~1.0–1.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), чтобы сцена не
|
||||
@ -53,6 +56,11 @@
|
||||
- **Фильтры слоёв (Все / Семья / Друзья / Сияющие):** 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`);
|
||||
|
||||
@ -53,8 +53,6 @@ const RELATION_COLORS = {
|
||||
// Неоновый цвет центра — из него «вытекает» градиент каждой связи к цвету периферийного узла.
|
||||
const FOCUS_NEON = 'rgba(140, 240, 255, 0.95)';
|
||||
|
||||
// Яркий неон сияния — в него уходит градиент связи к «сияющему» узлу (совпадает со свечением аватарки).
|
||||
const SHINE_EDGE_NEON = 'rgba(150, 245, 255, 0.95)';
|
||||
|
||||
function easeOutCubic(t) {
|
||||
const x = 1 - t;
|
||||
@ -416,26 +414,14 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
const desY = my + ux * baseBow + invY * lag;
|
||||
const cpx = 2 * desX - mx; // CP так, чтобы середина Q-кривой попала в desired
|
||||
const cpy = 2 * desY - my;
|
||||
// ТОНКАЯ изящная дуга: одинарная квадратичная кривая Безье, лёгкий градиентный штрих.
|
||||
// Обычная связь — неон у центра → цвет роли у узла. Связь к «СИЯЮЩЕМУ» — ярче, уходит в
|
||||
// неон сияния и МОНОЛИТНО светится (статичный drop-shadow через класс .fg-edge-shine).
|
||||
// Связь рисуем по статусу узла:
|
||||
// • обычная — одна тонкая (1.0–1.2px) матовая дуга, градиент с ГЛУБОКИМ уходом в прозрачность;
|
||||
// • СИЯЮЩАЯ — двухслойный неоновый «световод»: широкий размытый glow (под) + тонкий чёткий
|
||||
// core 1.5px (над) → изящно, но с объёмным OLED-свечением (см. .fg-edge-glow / .fg-edge-core).
|
||||
const shine = Boolean(n.shining) && !n.hidden;
|
||||
const gid = `fg-grad-${gi}`;
|
||||
gi += 1;
|
||||
const tipColor = shine ? SHINE_EDGE_NEON : relationColor(n.relationType);
|
||||
const baseStop = shine ? 0.85 : 0.5;
|
||||
const tipStop = shine ? 0.7 : 0.14;
|
||||
defs.push(
|
||||
`<linearGradient id="${gid}" gradientUnits="userSpaceOnUse" x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}">`
|
||||
+ `<stop offset="0" stop-color="${FOCUS_NEON}" stop-opacity="${baseStop}"/>`
|
||||
+ `<stop offset="1" stop-color="${tipColor}" stop-opacity="${tipStop}"/></linearGradient>`
|
||||
);
|
||||
const sw = (shine ? 1.7 + n.strength * 0.8 : 1.3 + n.strength * 0.9).toFixed(2); // тонко
|
||||
const d = `M${x1.toFixed(1)} ${y1.toFixed(1)} Q${cpx.toFixed(1)} ${cpy.toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
||||
// прозрачность луча = живая прозрачность узла (гаснет вместе с узлом при фильтре/уходе)
|
||||
const op = nodeOpacity < 0.995 ? ` opacity="${nodeOpacity.toFixed(2)}"` : '';
|
||||
// ПРОРАСТАНИЕ из центра: dasharray = длина пути, dashoffset уводим от длины к 0 по мере
|
||||
// разлёта (growP = доля пройденного узлом пути центр→орбита) → линия «вытягивается» из центра.
|
||||
// ПРОРАСТАНИЕ из центра: dasharray = длина пути, dashoffset от длины к 0 по мере разлёта узла
|
||||
// (growP = доля пройденного узлом пути центр→орбита) → линия «вытягивается» из центра.
|
||||
let dashAttr = '';
|
||||
if (growing) {
|
||||
const finalD = Math.hypot(n.bfx, n.bfy) || 1;
|
||||
@ -444,8 +430,26 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
const L = (Math.hypot(cpx - x1, cpy - y1) + Math.hypot(x2 - cpx, y2 - cpy) + Math.hypot(x2 - x1, y2 - y1)) / 2;
|
||||
dashAttr = ` stroke-dasharray="${L.toFixed(1)}" stroke-dashoffset="${(L * (1 - growP)).toFixed(1)}"`;
|
||||
}
|
||||
const cls = shine ? ' class="fg-edge-shine"' : ''; // монолитное неоновое свечение (drop-shadow)
|
||||
parts.push(`<path${cls} d="${d}" fill="none" stroke="url(#${gid})" stroke-width="${sw}" stroke-linecap="round"${op}${dashAttr} />`);
|
||||
if (shine) {
|
||||
// glow (размытый, приглушённый) + core (тонкий, чёткий) — растут синхронно (общий dashAttr)
|
||||
const gOp = (0.4 * nodeOpacity).toFixed(2);
|
||||
const cOpAttr = nodeOpacity < 0.995 ? ` opacity="${nodeOpacity.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 {
|
||||
// обычная: тонкая дуга, градиент 0.42 (центр) → 0.07 (аватарка) — глубокий уход в прозрачность,
|
||||
// чтобы матовые связи не спорили с сияющими.
|
||||
const gid = `fg-grad-${gi}`;
|
||||
gi += 1;
|
||||
defs.push(
|
||||
`<linearGradient id="${gid}" gradientUnits="userSpaceOnUse" x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}">`
|
||||
+ `<stop offset="0" stop-color="${FOCUS_NEON}" stop-opacity="0.42"/>`
|
||||
+ `<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)}"` : '';
|
||||
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('')}`;
|
||||
}
|
||||
|
||||
@ -16,6 +16,29 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Живой фон-«небула»: глубокое размытое сине-голубое облако света строго под центральным узлом.
|
||||
Медленно «дышит» (радиус/яркость) и переливается индиго↔ультрамарин (hue-rotate) за 7с.
|
||||
Чистый CSS на компоновщике — создаёт ощущение живой светящейся среды, не будит rAF-цикл. */
|
||||
.fg-stage::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle 320px at 50% 47%, rgba(80, 150, 255, 0.30) 0%, rgba(60, 100, 220, 0.15) 42%, rgba(40, 70, 170, 0) 72%);
|
||||
filter: blur(80px);
|
||||
pointer-events: none;
|
||||
z-index: 0; /* строго под линиями (z:0, но раньше по порядку) и узлами (z:1) */
|
||||
animation: fg-nebula 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fg-nebula {
|
||||
0%, 100% { opacity: 0.70; filter: blur(80px) hue-rotate(-12deg); }
|
||||
50% { opacity: 1.00; filter: blur(96px) hue-rotate(16deg); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fg-stage::before { animation: none; }
|
||||
}
|
||||
|
||||
.fg-world {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@ -37,10 +60,20 @@
|
||||
transition: opacity 420ms ease; /* плавное появление линий при перестройке */
|
||||
}
|
||||
|
||||
/* Связь к «сияющему» узлу — МОНОЛИТНО светится мягким неоном (тот же приём, что у ободка аватарки):
|
||||
статичная двухслойная тень через drop-shadow. Никакой динамики, бегущих точек и пульсаций. */
|
||||
.fg-edge-shine {
|
||||
filter: drop-shadow(0 0 3px rgba(140, 235, 255, 0.75)) drop-shadow(0 0 6px rgba(110, 225, 255, 0.4));
|
||||
/* Сияющая связь = двухслойный неоновый «световод» (Neon Layering): изящно, но объёмно (как OLED).
|
||||
GLOW — широкий размытый ореол неонового оттенка под линией; CORE — тонкий чёткий светлый контур. */
|
||||
.fg-edge-glow {
|
||||
fill: none;
|
||||
stroke: rgba(110, 225, 255, 1);
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
filter: blur(2px); /* мягкое объёмное свечение вокруг нити */
|
||||
}
|
||||
.fg-edge-core {
|
||||
fill: none;
|
||||
stroke: #e0f7fc; /* ультра-светлый голубой — чёткий контур луча */
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.fg-node {
|
||||
@ -294,26 +327,30 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Стеклянные табы — тонкие пластины матового стекла (frosted glass) */
|
||||
.fg-filter-chip {
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgba(166, 196, 245, 0.28);
|
||||
background: rgba(10, 20, 37, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
color: #cfe0ff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 7px 13px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
|
||||
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.08); /* лёгкий стеклянный блик сверху */
|
||||
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
/* Активный таб — то же стекло, но подсвеченное сине-голубым (в тон неону графа) */
|
||||
.fg-filter-chip.is-active {
|
||||
background: linear-gradient(130deg, rgba(61, 196, 223, 0.92), rgba(58, 95, 142, 0.92));
|
||||
border-color: rgba(180, 230, 255, 0.85);
|
||||
color: #061119;
|
||||
background: rgba(125, 215, 255, 0.16);
|
||||
border-color: rgba(160, 230, 255, 0.55);
|
||||
color: #eaf7ff;
|
||||
box-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.12), 0 0 14px rgba(110, 210, 255, 0.28);
|
||||
}
|
||||
|
||||
/* Контекстное меню узла (долгое нажатие) — в #modal-root, поверх всего, не масштабируется */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user