Связи (test 12.06): SVG-стеклянные орбы аватаров + цвет/свечение линий глубоких связей
Аватары → SVG GlassOrb (фото в стеклянной сфере, блик, rim, свечение). Линии глубоких связей (tier-2/3) — в цвете типа (друзья/семья/...), сияющие связи светятся (голубой ореол + ядро). Версия 1.2.159. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2559f1e66b
commit
652ddc9d88
@ -1,2 +1,2 @@
|
||||
client.version=1.2.158
|
||||
server.version=1.2.142
|
||||
client.version=1.2.159
|
||||
server.version=1.2.143
|
||||
|
||||
@ -419,6 +419,45 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// SVG-«стеклянный орб» для аватара (фото в стеклянной сфере ≈90% + блик + rim + свечение).
|
||||
// Уникальные id на экземпляр (иначе defs конфликтуют). Тело стекла тёмно-синее/прозрачное (без серости),
|
||||
// вторичный блик убран (был «звёздочкой»). Фолбэк: если фото не загрузилось — остаются инициалы.
|
||||
let orbSeq = 0;
|
||||
function buildGlassOrb(src, opts) {
|
||||
const o = opts || {};
|
||||
const u = 'o' + (orbSeq += 1);
|
||||
const glowOp = o.isFocus ? 0.34 : 0.28;
|
||||
const imgFilter = o.isFocus ? 'grayscale(0.9) contrast(1.04)' : 'saturate(0.85) brightness(0.97)';
|
||||
const init = String(o.initials || '').slice(0, 2);
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 100 100');
|
||||
svg.setAttribute('class', 'fg-orb-svg');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
svg.innerHTML = ''
|
||||
+ '<defs>'
|
||||
+ '<clipPath id="c' + u + '"><circle cx="50" cy="50" r="38"/></clipPath>'
|
||||
+ '<radialGradient id="sh' + u + '" cx="50%" cy="45%" r="52%"><stop offset="62%" stop-color="#bfe3ff" stop-opacity="0"/><stop offset="90%" stop-color="#dff1ff" stop-opacity="0.55"/><stop offset="100%" stop-color="#5f93b8" stop-opacity="0.25"/></radialGradient>'
|
||||
+ '<radialGradient id="sd' + u + '" cx="50%" cy="60%" r="55%"><stop offset="58%" stop-color="#000000" stop-opacity="0"/><stop offset="100%" stop-color="#02060d" stop-opacity="0.4"/></radialGradient>'
|
||||
+ '<linearGradient id="sp' + u + '" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.85"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></linearGradient>'
|
||||
+ '<filter id="so' + u + '" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="1.3"/></filter>'
|
||||
+ '<filter id="gl' + u + '" x="-70%" y="-70%" width="240%" height="240%"><feGaussianBlur stdDeviation="4.5"/></filter>'
|
||||
+ '</defs>'
|
||||
+ '<circle cx="50" cy="50" r="42" fill="#5facd4" opacity="' + glowOp + '" filter="url(#gl' + u + ')"/>'
|
||||
+ '<circle cx="50" cy="50" r="42" fill="#0a1626" opacity="0.38"/>'
|
||||
+ '<circle cx="50" cy="50" r="38" fill="#26344a"/>'
|
||||
+ (init ? '<text x="50" y="58" text-anchor="middle" fill="#cfe0ff" font-family="sans-serif" font-weight="600" font-size="22">' + init + '</text>' : '')
|
||||
+ (src ? '<image href="' + src + '" x="12" y="12" width="76" height="76" clip-path="url(#c' + u + ')" preserveAspectRatio="xMidYMid slice" style="filter:' + imgFilter + '"/>' : '')
|
||||
+ '<circle cx="50" cy="50" r="38" fill="url(#sd' + u + ')"/>'
|
||||
+ '<circle cx="50" cy="50" r="42" fill="url(#sh' + u + ')"/>'
|
||||
+ '<circle cx="50" cy="50" r="42" fill="#2f7fd0" opacity="0.07"/>'
|
||||
+ '<path d="M74 16 A42 42 0 0 1 90 61" fill="none" stroke="#9fd2f2" stroke-width="1.6" stroke-opacity="0.85" stroke-linecap="round" filter="url(#so' + u + ')"/>'
|
||||
+ '<circle cx="50" cy="50" r="42" fill="none" stroke="#a9d6f0" stroke-width="0.6" stroke-opacity="0.5"/>'
|
||||
+ '<ellipse cx="37" cy="30" rx="16" ry="8" fill="url(#sp' + u + ')" transform="rotate(-28 37 30)" filter="url(#so' + u + ')"/>';
|
||||
const im = svg.querySelector('image');
|
||||
if (im) im.addEventListener('error', () => { try { im.remove(); } catch (e) { /* останутся инициалы */ } });
|
||||
return svg;
|
||||
}
|
||||
|
||||
function buildNodeElement(src, isFocus, tier, dotOnly = false) {
|
||||
const el = document.createElement('button');
|
||||
el.type = 'button';
|
||||
@ -447,17 +486,14 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
].filter(Boolean).join(' ');
|
||||
el.dataset.nodeId = String(src.id);
|
||||
|
||||
// тестовое фото (лаборатория) — прямой URL; иначе штатный аватар (Arweave/инициалы)
|
||||
const avatar = src.photo
|
||||
? buildPhotoAvatar(src)
|
||||
: renderUserAvatar({
|
||||
login: src.login || src.name || String(src.id),
|
||||
firstName: src.name || '',
|
||||
avatar: src.avatar || null,
|
||||
size: 'node',
|
||||
title: src.name || src.login || '',
|
||||
});
|
||||
el.append(avatar);
|
||||
// Аватар = SVG-«стеклянный орб» (фото в стеклянной сфере). Хост — .node-dot (масштаб/состояния/
|
||||
// синхронизация позиций движка). Меняем ТОЛЬКО аватар; бейдж/имя/линии — как раньше.
|
||||
const photoSrc = src.photo || (src.avatar && src.avatar !== 'url_to_image' ? src.avatar : null);
|
||||
const initials = buildAvatarInitials({ login: src.login || src.name || String(src.id), firstName: src.name || '' });
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'avatar node-dot fg-orb-host';
|
||||
dot.appendChild(buildGlassOrb(photoSrc, { isFocus, initials }));
|
||||
el.append(dot);
|
||||
|
||||
// Бейдж-счётчик числа связей (заполняется в updateBadges по degreeById). Скрыт, пока 0.
|
||||
const badge = document.createElement('span');
|
||||
@ -772,11 +808,25 @@ 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 * sp).toFixed(2)}" />`);
|
||||
// 3-й уровень: тонкая нить В ЦВЕТЕ СВЯЗИ (видна при раскрытии). Сияющая — светится (ореол+ядро).
|
||||
if (pe > 0.02) {
|
||||
if (shine) {
|
||||
parts.push(`<path d="${d}" fill="none" stroke="#00e5ff" stroke-width="2.6" stroke-linecap="round" opacity="${(0.42 * pe * sp).toFixed(2)}" filter="url(#fg-plasma-blur2)" style="mix-blend-mode:screen" />`);
|
||||
parts.push(`<path d="${d}" fill="none" stroke="#dffaff" stroke-width="1.1" stroke-linecap="round" opacity="${(0.85 * pe * sp).toFixed(2)}" />`);
|
||||
} else {
|
||||
parts.push(`<path d="${d}" fill="none" stroke="${relationColor(n.relationType)}" stroke-width="0.8" stroke-linecap="round" opacity="${(0.34 * 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 * sp).toFixed(2)}" />`);
|
||||
// 2-й уровень: связь В ЦВЕТЕ ТИПА (семья/друзья/...). Сияющая связь — светящаяся линия.
|
||||
if (pe > 0.02) {
|
||||
if (shine) {
|
||||
parts.push(`<path d="${d}" fill="none" stroke="#00e5ff" stroke-width="3.2" stroke-linecap="round" opacity="${(0.46 * pe * sp).toFixed(2)}" filter="url(#fg-plasma-blur2)" style="mix-blend-mode:screen" />`);
|
||||
parts.push(`<path d="${d}" fill="none" stroke="#dffaff" stroke-width="1.3" stroke-linecap="round" opacity="${(0.9 * pe * sp).toFixed(2)}" />`);
|
||||
} else {
|
||||
parts.push(`<path d="${d}" fill="none" stroke="${relationColor(n.relationType)}" stroke-width="1.0" stroke-linecap="round" opacity="${(0.42 * pe * sp).toFixed(2)}" />`);
|
||||
}
|
||||
}
|
||||
} else if (shine || n.track || onPath) {
|
||||
// СИЯЮЩАЯ связь — плазменный композитинг: ОДИН центральный S-путь (cubic) + ТРИ наложенных слоя
|
||||
// с ОДИНАКОВЫМ d (объём из толщины+blur, не из геометрии). Слои: широкое поле / трубка / ядро.
|
||||
|
||||
@ -128,6 +128,25 @@
|
||||
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
/* SVG-«стеклянный орб» — масштабируем так, чтобы сфера (r42 = 84% SVG) ≈ диаметр узла → линии-связи
|
||||
прилипают к краю орба, как раньше. Хост .node-dot держит размер/состояния/синхронизацию позиций. */
|
||||
.fg-node .node-dot.fg-orb-host {
|
||||
position: relative;
|
||||
background: none;
|
||||
overflow: visible; /* не срезать внешнее свечение орба */
|
||||
box-shadow: none;
|
||||
}
|
||||
.fg-orb-svg {
|
||||
position: absolute;
|
||||
width: 119%;
|
||||
height: 119%;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.fg-node.is-family .node-dot {
|
||||
background: linear-gradient(165deg, #785038, #5f3e2c);
|
||||
border-color: rgba(255, 194, 143, 0.6);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user