orb: единый PNG-оверлей на все узлы + ретайр вектора
- glass_overlay_faithful.png в assets; орбы = фото (низ, круглая маска 78%) + стеклянный PNG (верх, бокс 119% от node-dot, контакт линий от ORB_R). - PNG-оверлей применён ко ВСЕМ полным орбам (центр + спутники); tier-3 точки без изменений. - ретайр мёртвого векторного стекла: удалён buildGlassOrb (+orbSeq) и CSS .fg-orb-svg, снят остаточный border .node-dot (синее кольцо) у PNG-хостов. Версия 1.2.163. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e3bebff618
commit
69f0fdf120
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.162
|
client.version=1.2.163
|
||||||
server.version=1.2.144
|
server.version=1.2.144
|
||||||
|
|||||||
BIN
shine-UI/assets/glass_overlay_faithful.png
Normal file
BIN
shine-UI/assets/glass_overlay_faithful.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 461 KiB |
@ -424,45 +424,37 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG-«стеклянный орб» для аватара (фото в стеклянной сфере ≈90% + блик + rim + свечение).
|
// Векторный SVG-орб (buildGlassOrb) ретайрнут 13.06 — все орбы рисует buildPngOrb (PNG-оверлей).
|
||||||
// Уникальные id на экземпляр (иначе defs конфликтуют). Тело стекла тёмно-синее/прозрачное (без серости),
|
|
||||||
// вторичный блик убран (был «звёздочкой»). Фолбэк: если фото не загрузилось — остаются инициалы.
|
// A/B-вариант (ветка glass-png-overlay): орб = фото + запечённый стеклянный PNG поверх.
|
||||||
let orbSeq = 0;
|
// Слой 1 — фото круглой маской ~78% от бокса оверлея (сидит внутри кромки); слой 2 — glass_overlay.png
|
||||||
function buildGlassOrb(src, opts) {
|
// на весь бокс (альфа уже в PNG). Кодовый glow не рисуем — у картинки своё свечение запечено (нет двойного).
|
||||||
|
const GLASS_OVERLAY_SRC = '/assets/glass_overlay_faithful.png';
|
||||||
|
function buildPngOrb(src, opts) {
|
||||||
const o = opts || {};
|
const o = opts || {};
|
||||||
const u = 'o' + (orbSeq += 1);
|
const wrap = document.createElement('div');
|
||||||
const glowOp = o.isFocus ? 0.34 : 0.28;
|
wrap.className = 'fg-pngorb';
|
||||||
const glowSpread = o.isFocus ? 7 : 4.5; // центр — шире/мягче ореол (рычаг 1); спутники без изменений
|
function makeInit() {
|
||||||
const imgFilter = o.isFocus ? 'grayscale(0.9) contrast(1.04)' : 'saturate(0.85) brightness(0.97)';
|
const d = document.createElement('div');
|
||||||
const init = String(o.initials || '').slice(0, 2);
|
d.className = 'fg-pngorb-photo fg-pngorb-init';
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
d.textContent = String(o.initials || '').slice(0, 2);
|
||||||
svg.setAttribute('viewBox', '0 0 100 100');
|
return d;
|
||||||
svg.setAttribute('class', 'fg-orb-svg');
|
}
|
||||||
svg.setAttribute('aria-hidden', 'true');
|
let photo;
|
||||||
svg.innerHTML = ''
|
if (src) {
|
||||||
+ '<defs>'
|
photo = document.createElement('img');
|
||||||
+ '<clipPath id="c' + u + '"><circle cx="50" cy="50" r="38"/></clipPath>'
|
photo.className = 'fg-pngorb-photo';
|
||||||
+ '<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.5"/><stop offset="100%" stop-color="#5f93b8" stop-opacity="0.18"/></radialGradient>'
|
photo.src = src; photo.alt = '';
|
||||||
+ '<radialGradient id="sd' + u + '" cx="50%" cy="60%" r="58%"><stop offset="72%" stop-color="#000000" stop-opacity="0"/><stop offset="100%" stop-color="#02060d" stop-opacity="0.22"/></radialGradient>'
|
photo.addEventListener('error', () => { try { photo.replaceWith(makeInit()); } catch (e) { /* fallback */ } });
|
||||||
+ '<linearGradient id="sp' + u + '" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#ffffff" stop-opacity="0.5"/><stop offset="100%" stop-color="#ffffff" stop-opacity="0"/></linearGradient>'
|
} else {
|
||||||
+ '<filter id="so' + u + '" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="1.3"/></filter>'
|
photo = makeInit();
|
||||||
+ '<filter id="sf' + u + '" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="3.2"/></filter>'
|
}
|
||||||
+ '<filter id="gl' + u + '" x="-70%" y="-70%" width="240%" height="240%"><feGaussianBlur stdDeviation="' + glowSpread + '"/></filter>'
|
const glass = document.createElement('img');
|
||||||
+ '</defs>'
|
glass.className = 'fg-pngorb-glass';
|
||||||
+ '<circle cx="50" cy="50" r="42" fill="#5facd4" opacity="' + glowOp + '" filter="url(#gl' + u + ')"/>'
|
glass.src = GLASS_OVERLAY_SRC; glass.alt = '';
|
||||||
+ '<circle cx="50" cy="50" r="42" fill="#0a1626" opacity="0.3"/>'
|
glass.setAttribute('aria-hidden', 'true');
|
||||||
+ '<circle cx="50" cy="50" r="38" fill="#26344a"/>'
|
wrap.append(photo, glass);
|
||||||
+ (init ? '<text x="50" y="58" text-anchor="middle" fill="#cfe0ff" font-family="sans-serif" font-weight="600" font-size="22">' + init + '</text>' : '')
|
return wrap;
|
||||||
+ (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.55" stroke-linecap="round" filter="url(#so' + u + ')"/>'
|
|
||||||
+ '<circle cx="50" cy="50" r="42" fill="none" stroke="#a9d6f0" stroke-width="0.9" stroke-opacity="0.2" filter="url(#so' + u + ')"/>'
|
|
||||||
+ '<ellipse cx="40" cy="31" rx="24" ry="12" fill="url(#sp' + u + ')" transform="rotate(-28 40 31)" filter="url(#sf' + 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) {
|
function buildNodeElement(src, isFocus, tier, dotOnly = false) {
|
||||||
@ -499,7 +491,8 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
const initials = buildAvatarInitials({ login: src.login || src.name || String(src.id), firstName: src.name || '' });
|
const initials = buildAvatarInitials({ login: src.login || src.name || String(src.id), firstName: src.name || '' });
|
||||||
const dot = document.createElement('div');
|
const dot = document.createElement('div');
|
||||||
dot.className = 'avatar node-dot fg-orb-host';
|
dot.className = 'avatar node-dot fg-orb-host';
|
||||||
dot.appendChild(buildGlassOrb(photoSrc, { isFocus, initials }));
|
// Единый PNG-оверлей на ВСЕХ полных орбах (фокус + спутники). tier-3 точки (dotOnly) сюда не идут.
|
||||||
|
dot.appendChild(buildPngOrb(photoSrc, { isFocus, initials }));
|
||||||
el.append(dot);
|
el.append(dot);
|
||||||
|
|
||||||
// Бейдж-счётчик числа связей (заполняется в updateBadges по degreeById). Скрыт, пока 0.
|
// Бейдж-счётчик числа связей (заполняется в updateBadges по degreeById). Скрыт, пока 0.
|
||||||
|
|||||||
@ -137,15 +137,45 @@
|
|||||||
overflow: visible; /* не срезать внешнее свечение орба */
|
overflow: visible; /* не срезать внешнее свечение орба */
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.fg-orb-svg {
|
/* A/B PNG-оверлей орба (ветка glass-png-overlay): фото снизу + запечённый стеклянный PNG сверху.
|
||||||
|
Бокс = 119% от .node-dot (как .fg-orb-svg → сфера ≈ кромке node-dot, контакт линий от ORB_R сохраняется). */
|
||||||
|
/* Специфичность `.fg-orb-host …` бьёт глобальное `.node-dot img` (иначе оно гасит opacity→0
|
||||||
|
и форсит размер 100%). Поэтому opacity/размеры/радиус задаём здесь явно. */
|
||||||
|
/* Кромку даёт сам glass_overlay.png → убираем остаточный border .node-dot (синее кольцо
|
||||||
|
старого векторного орба). Только у PNG-хоста; вектор и свечение/box-shadow не трогаем. */
|
||||||
|
.fg-orb-host:has(.fg-pngorb) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.fg-orb-host .fg-pngorb {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 119%;
|
left: 50%; top: 50%;
|
||||||
height: 119%;
|
width: 119%; height: 119%;
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.fg-orb-host .fg-pngorb-glass {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: 0;
|
||||||
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
overflow: visible;
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.fg-orb-host .fg-pngorb-photo {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
width: 78%; height: 78%; /* ~78% от бокса оверлея — сидит внутри стеклянной кромки */
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.fg-orb-host .fg-pngorb-init {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: #26344a; color: #cfe0ff; font-weight: 600; font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fg-node.is-family .node-dot {
|
.fg-node.is-family .node-dot {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user