Связи: финальный вид сияющих связей — плазма (поле+трубка+ядро, screen-blend) + цвет связи у обычных
Промежуточные итерации линий схлопнуты в один коммит. Убран мёртвый фильтр fg-plasma-turb. Лаборатория (/network-view/lab) и автотесты сохранены. Версия 1.2.158. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
519bce6b78
commit
2559f1e66b
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.150
|
client.version=1.2.158
|
||||||
server.version=1.2.134
|
server.version=1.2.142
|
||||||
|
|||||||
@ -145,6 +145,22 @@
|
|||||||
- **Бейдж числа связей** — `.fg-node-badge` (число из `degreeById`, обновляется в `updateBadges`).
|
- **Бейдж числа связей** — `.fg-node-badge` (число из `degreeById`, обновляется в `updateBadges`).
|
||||||
- **Цветовые кластеры** — мягкая аура узла по типу связи (CSS `is-family/friend/business/contact`).
|
- **Цветовые кластеры** — мягкая аура узла по типу связи (CSS `is-family/friend/business/contact`).
|
||||||
|
|
||||||
|
**Линии-«жгуты» (партия 4, по референсу — плазменный композитинг):**
|
||||||
|
- **Сияющие** — ОДИН центральный S-путь (cubic Bézier) + ТРИ наложенных слоя с ОДИНАКОВЫМ `d` (объём
|
||||||
|
из толщины+размытия, НЕ из геометрии — никаких расходящихся линий):
|
||||||
|
Настоящий НЕОН (видимый ореол вокруг яркого ядра; поле/трубка в `mix-blend-mode: screen` — свет
|
||||||
|
складывается аддитивно с тёмным фоном, а в пересечениях у центра ярче — энергохаб):
|
||||||
|
- `.fg-plasma-flare` — плазменное облако: 16px, `#00bfff`, opacity 0.42, **`feGaussianBlur` stdDev=6**, screen (+ «дыхание» 3.6с);
|
||||||
|
- `.fg-plasma-tube` — направляющий свет: 6px, `#00e5ff`, opacity 0.85, **`feGaussianBlur` stdDev=2**, screen;
|
||||||
|
- `.fg-plasma-core` — ядро: 2px, `#dffaff` (светло-голубо-белое), opacity 1, без размытия.
|
||||||
|
Толщина/насыщенность подогнаны под референс (толстая яркая голубая плазма, гладкие края).
|
||||||
|
S-волна спокойная/изящная (amp до 13px). Размытие — именно SVG-фильтры (`#fg-plasma-blur6/2`), т.к.
|
||||||
|
CSS-`filter` на `<path>` в части мобильных WebView не применяется (отсюда был «плоский»/«канатный» вид).
|
||||||
|
⚠️ Это НЕ Canvas-движок (не библиотека force-graph): связи — реальные SVG `<path>`, фильтры применяются.
|
||||||
|
Прозрачность слоёв inline (× spotlight/глубину). Тяжёлый blur только у сияющих (их мало) — перф.
|
||||||
|
- **Не-сияущие** — мягкое свечение **в цвете связи** (семья/друзья/бизнес/контакт): широкая
|
||||||
|
полупрозрачная подложка + тонкое ядро, без SVG-blur (дёшево). «Похоже, но тише».
|
||||||
|
|
||||||
**Фишки (партия 3, лаборатория):**
|
**Фишки (партия 3, лаборатория):**
|
||||||
- **Общие связи** — среди друзей человека один помечен как «общий» (он и твой друг тоже): золотой
|
- **Общие связи** — среди друзей человека один помечен как «общий» (он и твой друг тоже): золотой
|
||||||
ободок + ★ (CSS `is-common`; в лаб-генерации `addDeepLevels` подставляет узнаваемого друга Ивана).
|
ободок + ★ (CSS `is-common`; в лаб-генерации `addDeepLevels` подставляет узнаваемого друга Ивана).
|
||||||
|
|||||||
@ -109,8 +109,13 @@ function ensureShineFilter() {
|
|||||||
svg.setAttribute('width', '0');
|
svg.setAttribute('width', '0');
|
||||||
svg.setAttribute('height', '0');
|
svg.setAttribute('height', '0');
|
||||||
svg.style.position = 'absolute';
|
svg.style.position = 'absolute';
|
||||||
svg.innerHTML = '<defs><filter id="fg-shine-glow" x="-120%" y="-120%" width="340%" height="340%" '
|
// + SVG-фильтры размытия для плазменных линий (feGaussianBlur надёжнее CSS-filter на <path> в WebView).
|
||||||
+ 'color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="3.4"/></filter></defs>';
|
// Широкий регион фильтра (userSpaceOnUse-подобный запас в %), чтобы размытие не срезалось по bbox пути.
|
||||||
|
svg.innerHTML = '<defs>'
|
||||||
|
+ '<filter id="fg-shine-glow" x="-120%" y="-120%" width="340%" height="340%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="3.4"/></filter>'
|
||||||
|
+ '<filter id="fg-plasma-blur6" x="-100%" y="-200%" width="300%" height="500%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="6"/></filter>'
|
||||||
|
+ '<filter id="fg-plasma-blur2" x="-60%" y="-120%" width="220%" height="340%" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2"/></filter>'
|
||||||
|
+ '</defs>';
|
||||||
document.body.appendChild(svg);
|
document.body.appendChild(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,27 +778,33 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
// 2-й уровень: матовая паутинка, проявляется по мере раскрытия родителя (локальный bloom)
|
// 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 * sp).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 || onPath) {
|
} else if (shine || n.track || onPath) {
|
||||||
// glow (размытый, приглушённый) + core (тонкий, чёткий). Используется для сияющих, «трека
|
// СИЯЮЩАЯ связь — плазменный композитинг: ОДИН центральный S-путь (cubic) + ТРИ наложенных слоя
|
||||||
// прохождения» (n.track) И нити-крошки пути погружения (onPath) — путь назад к Ивану горит ярко.
|
// с ОДИНАКОВЫМ d (объём из толщины+blur, не из геометрии). Слои: широкое поле / трубка / ядро.
|
||||||
const cOpVal = nodeOpacity * sp;
|
const pnx = -uy;
|
||||||
const gOp = (0.4 * cOpVal).toFixed(2);
|
const pny = ux; // перпендикуляр к хорде
|
||||||
const cOpAttr = cOpVal < 0.995 ? ` opacity="${cOpVal.toFixed(2)}"` : '';
|
const amp = Math.min(13, 5 + segLen0 * 0.05); // амплитуда S — спокойная изящная волна (∝ длине)
|
||||||
parts.push(`<path class="fg-edge-glow" d="${d}"${dashAttr} opacity="${gOp}" />`);
|
const bowX = desX - mx;
|
||||||
parts.push(`<path class="fg-edge-core" d="${d}"${dashAttr}${cOpAttr} />`);
|
const bowY = desY - my; // вектор изящной дуги + пан-стретч — сохраняем
|
||||||
|
// единственная S-кривая: контрольная точка на 1/3 смещена +amp, на 2/3 — −amp (плавная волна)
|
||||||
|
const c1x = x1 + (x2 - x1) / 3 + bowX + pnx * amp; const c1y = y1 + (y2 - y1) / 3 + bowY + pny * amp;
|
||||||
|
const c2x = x1 + 2 * (x2 - x1) / 3 + bowX - pnx * amp; const c2y = y1 + 2 * (y2 - y1) / 3 + bowY - pny * amp;
|
||||||
|
const dS = `M${x1.toFixed(1)} ${y1.toFixed(1)} C${c1x.toFixed(1)} ${c1y.toFixed(1)} ${c2x.toFixed(1)} ${c2y.toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`;
|
||||||
|
const base = nodeOpacity * sp; // затемнение от spotlight/глубины
|
||||||
|
// 3 слоя на ОДНОМ пути dS. Видимый НЕОН: яркое ядро + чёткий голубой ореол (поле+трубка в режиме
|
||||||
|
// screen — свет складывается аддитивно с фоном). Прозрачности подняты, чтобы не было «белого каната».
|
||||||
|
parts.push(`<path class="fg-plasma-flare" d="${dS}"${dashAttr} opacity="${(0.42 * base).toFixed(3)}" />`);
|
||||||
|
parts.push(`<path class="fg-plasma-tube" d="${dS}"${dashAttr} opacity="${(0.85 * base).toFixed(3)}" />`);
|
||||||
|
parts.push(`<path class="fg-plasma-core" d="${dS}"${dashAttr} opacity="${(1 * base).toFixed(3)}" />`);
|
||||||
} else {
|
} else {
|
||||||
// обычная: тонкая дуга, градиент 0.42 (центр) → 0.07 (аватарка) — глубокий уход в прозрачность,
|
// ОБЫЧНАЯ связь — мягкий светящийся жгут В ЦВЕТЕ СВЯЗИ (семья тёплый / друзья синий /
|
||||||
// чтобы матовые связи не спорили с сияющими.
|
// бизнес фиолет): тоньше и тише сияющих. Без SVG-blur (дёшево): широкая подложка + тонкое ядро.
|
||||||
const gid = `fg-grad-${gi}`;
|
const col = relationColor(n.relationType);
|
||||||
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 opVal = nodeOpacity * sp;
|
const opVal = nodeOpacity * sp;
|
||||||
const op = opVal < 0.995 ? ` opacity="${opVal.toFixed(2)}"` : '';
|
const haloOp = (0.22 * opVal).toFixed(2);
|
||||||
parts.push(`<path d="${d}" fill="none" stroke="url(#${gid})" stroke-width="${sw}" stroke-linecap="round"${op}${dashAttr} />`);
|
const coreOp = (0.7 * opVal).toFixed(2);
|
||||||
|
const sw = (2.6 + n.strength * 1.4).toFixed(2); // мягкая подложка-«свечение» в цвете связи
|
||||||
|
parts.push(`<path d="${d}" fill="none" stroke="${col}" stroke-width="${sw}" stroke-linecap="round" opacity="${haloOp}"${dashAttr} />`);
|
||||||
|
parts.push(`<path d="${d}" fill="none" stroke="${col}" stroke-width="1.2" stroke-linecap="round" opacity="${coreOp}"${dashAttr} />`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
edgesSvg.innerHTML = `<defs>${defs.join('')}</defs>${parts.join('')}`;
|
edgesSvg.innerHTML = `<defs>${defs.join('')}</defs>${parts.join('')}`;
|
||||||
|
|||||||
@ -124,6 +124,17 @@ export async function runNetworkSelfTest(graph, deepChipEl) {
|
|||||||
check('E2 выход: зум вернулся (~1)', Math.abs(s.zoom - 1) < 0.05, `zoom=${s.zoom}`);
|
check('E2 выход: зум вернулся (~1)', Math.abs(s.zoom - 1) < 0.05, `zoom=${s.zoom}`);
|
||||||
check('E3 выход: погружение снято', s.diveTargetId === null, `dive=${s.diveTargetId}`);
|
check('E3 выход: погружение снято', s.diveTargetId === null, `dive=${s.diveTargetId}`);
|
||||||
|
|
||||||
|
// === Тест K: сияющие линии — плазма из 3 слоёв на ОДНОМ S-пути (одинаковый d) ===
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const flare = document.querySelectorAll('.fg-plasma-flare');
|
||||||
|
const tube = document.querySelectorAll('.fg-plasma-tube');
|
||||||
|
const core = document.querySelectorAll('.fg-plasma-core');
|
||||||
|
const equalLayers = flare.length >= 1 && flare.length === tube.length && tube.length === core.length;
|
||||||
|
const sameD = flare[0] && flare[0].getAttribute('d') === tube[0].getAttribute('d')
|
||||||
|
&& tube[0].getAttribute('d') === core[0].getAttribute('d');
|
||||||
|
check('K1 плазма: 3 слоя на ОДНОМ S-пути', equalLayers && !!sameD, `поле:${flare.length} трубка:${tube.length} ядро:${core.length} sameD:${!!sameD}`);
|
||||||
|
}
|
||||||
|
|
||||||
return finish(results);
|
return finish(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,38 +60,43 @@
|
|||||||
transition: opacity 420ms ease; /* плавное появление линий при перестройке */
|
transition: opacity 420ms ease; /* плавное появление линий при перестройке */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Сияющая связь = двухслойный неоновый «световод» (Neon Layering): изящно, но объёмно (как OLED).
|
/* Сияющая связь = плазменный композитинг (3 слоя на ОДНОМ S-пути, см. renderEdges).
|
||||||
GLOW — широкий размытый ореол неонового оттенка под линией; CORE — тонкий чёткий светлый контур. */
|
Настоящий НЕОН: яркое светлое ядро + ВИДИМЫЙ голубой ореол вокруг. Слои поля/трубки идут в режиме
|
||||||
.fg-edge-glow {
|
mix-blend-mode: screen → свет складывается аддитивно с тёмным фоном (как реальное свечение), а в точках
|
||||||
|
пересечения нитей у центра — ярче (энергетический хаб). Прозрачность слоёв — inline (×spotlight/глубину). */
|
||||||
|
.fg-plasma-flare { /* нижний: широкое насыщенное голубое плазменное свечение (по референсу) */
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: rgba(110, 225, 255, 1);
|
stroke: #00bfff; /* глубокий голубой */
|
||||||
stroke-width: 4;
|
stroke-width: 16;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
filter: blur(2px); /* мягкое объёмное свечение вокруг нити */
|
filter: url(#fg-plasma-blur6); /* мягкое объёмное свечение (гладкие края — как на референсе) */
|
||||||
/* синхро-пульс: нить «дышит» толщиной/размытием в том же ритме (3.6с), что и ободок сияющего узла —
|
mix-blend-mode: screen; /* аддитивное свечение поверх тёмного фона */
|
||||||
в покое SVG не перерисовывается, поэтому все нити стартуют синхронно и пульсируют вместе. */
|
/* синхро-«дыхание» поля толщиной в такт ободку сияющего узла (3.6с); прозрачность не трогаем (inline) */
|
||||||
animation: fg-edge-pulse 3.6s ease-in-out infinite;
|
animation: fg-plasma-breath 3.6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.fg-edge-core {
|
.fg-plasma-tube { /* средний: яркая толстая неоновая трубка */
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: #e0f7fc; /* ультра-светлый голубой — чёткий контур луча */
|
stroke: #00e5ff; /* яркий циан */
|
||||||
stroke-width: 1.5;
|
stroke-width: 6;
|
||||||
|
stroke-linecap: round;
|
||||||
|
filter: url(#fg-plasma-blur2); /* SVG feGaussianBlur stdDeviation=2 */
|
||||||
|
mix-blend-mode: screen; /* аддитивное свечение */
|
||||||
|
}
|
||||||
|
.fg-plasma-core { /* верхний: яркое чёткое ядро (светло-голубо-белое) */
|
||||||
|
fill: none;
|
||||||
|
stroke: #dffaff; /* светло-голубо-белое — «жидкое» ядро */
|
||||||
|
stroke-width: 2;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
animation: fg-edge-core-pulse 3.6s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* пульс «световода» в такт дыханию сияющего ободка (та же длительность 3.6с) */
|
/* мягкое «дыхание» плазменного облака толщиной, синхронно с пульсом сияющего ободка (3.6с) */
|
||||||
@keyframes fg-edge-pulse {
|
@keyframes fg-plasma-breath {
|
||||||
0%, 100% { stroke-width: 3.4; filter: blur(1.6px); }
|
0%, 100% { stroke-width: 14; }
|
||||||
50% { stroke-width: 5.2; filter: blur(2.8px); }
|
50% { stroke-width: 19; }
|
||||||
}
|
|
||||||
@keyframes fg-edge-core-pulse {
|
|
||||||
0%, 100% { stroke-width: 1.3; }
|
|
||||||
50% { stroke-width: 1.9; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.fg-edge-glow, .fg-edge-core { animation: none; }
|
.fg-plasma-flare { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.fg-node {
|
.fg-node {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user