SHA256
Улучшить аватары и граф связей
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.12.17
|
client.version=1.12.18
|
||||||
server.version=1.10.7
|
server.version=1.10.7
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function renderAvatar({
|
|||||||
title = '',
|
title = '',
|
||||||
alt = 'Аватар',
|
alt = 'Аватар',
|
||||||
glow = false,
|
glow = false,
|
||||||
|
official = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
const classes = new Set(['avatar', 'avatar-image', 'avatar-framed']);
|
const classes = new Set(['avatar', 'avatar-image', 'avatar-framed']);
|
||||||
@@ -52,6 +53,15 @@ export function renderAvatar({
|
|||||||
fallback.textContent = String(initials || '?').trim().slice(0, 2).toUpperCase() || '?';
|
fallback.textContent = String(initials || '?').trim().slice(0, 2).toUpperCase() || '?';
|
||||||
wrap.append(fallback);
|
wrap.append(fallback);
|
||||||
|
|
||||||
|
if (official) {
|
||||||
|
const badge = document.createElement('img');
|
||||||
|
badge.className = 'avatar-official-badge';
|
||||||
|
badge.src = '/assets/shine-official-badge.svg?v=2';
|
||||||
|
badge.alt = '';
|
||||||
|
badge.setAttribute('aria-hidden', 'true');
|
||||||
|
wrap.append(badge);
|
||||||
|
}
|
||||||
|
|
||||||
const txId = String(avatar?.ar || '').trim();
|
const txId = String(avatar?.ar || '').trim();
|
||||||
if (!validateArweaveTxId(txId)) {
|
if (!validateArweaveTxId(txId)) {
|
||||||
return wrap;
|
return wrap;
|
||||||
@@ -146,6 +156,7 @@ export function renderUserAvatar({
|
|||||||
className = '',
|
className = '',
|
||||||
title = '',
|
title = '',
|
||||||
glow = false,
|
glow = false,
|
||||||
|
official = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return renderAvatar({
|
return renderAvatar({
|
||||||
initials: buildAvatarInitials({ login, firstName, lastName }),
|
initials: buildAvatarInitials({ login, firstName, lastName }),
|
||||||
@@ -155,5 +166,6 @@ export function renderUserAvatar({
|
|||||||
title,
|
title,
|
||||||
alt: 'Аватар',
|
alt: 'Аватар',
|
||||||
glow,
|
glow,
|
||||||
|
official,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -709,14 +709,23 @@ function buildStableHistoryEngineModel(history, historyDepth = HISTORY_MAX_PREVI
|
|||||||
const centerKey = normKey(snap?.centerLogin);
|
const centerKey = normKey(snap?.centerLogin);
|
||||||
const snapNodes = Array.isArray(snap?.engineModel?.nodes) ? snap.engineModel.nodes : [];
|
const snapNodes = Array.isArray(snap?.engineModel?.nodes) ? snap.engineModel.nodes : [];
|
||||||
const ownCenter = snapNodes.find((node) => normKey(node?.id) === centerKey);
|
const ownCenter = snapNodes.find((node) => normKey(node?.id) === centerKey);
|
||||||
if (ownCenter) centerNodeByKey.set(centerKey, { ...ownCenter, isHistoryCenter: true, keepVisible: true, tier: 1 });
|
if (ownCenter) centerNodeByKey.set(centerKey, {
|
||||||
|
...ownCenter,
|
||||||
|
isHistoryCenter: true,
|
||||||
|
clusterOwnerId: centerKey,
|
||||||
|
keepVisible: true,
|
||||||
|
tier: 1,
|
||||||
|
});
|
||||||
|
|
||||||
snapNodes.forEach((rawNode) => {
|
snapNodes.forEach((rawNode) => {
|
||||||
const key = normKey(rawNode?.id);
|
const key = normKey(rawNode?.id);
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
// Исторические центры сохраняют позицию собственного кластера. Все остальные общие узлы
|
// Исторические центры сохраняют позицию собственного кластера. Все остальные общие узлы
|
||||||
// принадлежат самому свежему кластеру, где встретились, и поэтому «переезжают» туда без дубля.
|
// принадлежат самому свежему кластеру, где встретились, и поэтому «переезжают» туда без дубля.
|
||||||
if (!centerKeys.has(key) || key === latestCenterKey || key === centerKey) nodeMap.set(key, { ...rawNode, id: key });
|
if (!centerKeys.has(key) || key === latestCenterKey || key === centerKey) {
|
||||||
|
// Общий узел принадлежит последнему кластеру, возле которого он был перерисован.
|
||||||
|
nodeMap.set(key, { ...rawNode, id: key, clusterOwnerId: centerKey });
|
||||||
|
}
|
||||||
|
|
||||||
if (key === centerKey) return;
|
if (key === centerKey) return;
|
||||||
let parents = Array.isArray(rawNode?.edgeParents) ? rawNode.edgeParents : [];
|
let parents = Array.isArray(rawNode?.edgeParents) ? rawNode.edgeParents : [];
|
||||||
@@ -755,9 +764,11 @@ function buildStableHistoryEngineModel(history, historyDepth = HISTORY_MAX_PREVI
|
|||||||
id: key,
|
id: key,
|
||||||
login: node?.login || node?.id || key,
|
login: node?.login || node?.id || key,
|
||||||
tier: centerKeys.has(key) ? 1 : Math.max(1, Number(node?.tier) || 1),
|
tier: centerKeys.has(key) ? 1 : Math.max(1, Number(node?.tier) || 1),
|
||||||
|
isHistoryCenter: centerKeys.has(key),
|
||||||
fixedLayout: true,
|
fixedLayout: true,
|
||||||
keepVisible: centerKeys.has(key) || Boolean(node?.keepVisible),
|
keepVisible: centerKeys.has(key) || Boolean(node?.keepVisible),
|
||||||
alwaysVisible: (Number(node?.tier) || 1) >= 2 ? true : Boolean(node?.alwaysVisible),
|
alwaysVisible: (Number(node?.tier) || 1) >= 2 ? true : Boolean(node?.alwaysVisible),
|
||||||
|
clusterOwnerId: normKey(node?.clusterOwnerId || key),
|
||||||
edgeParents: edgeParentsByChild.get(key) || [],
|
edgeParents: edgeParentsByChild.get(key) || [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1098,11 +1109,15 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
window.setTimeout(() => inputEl.focus(), 0);
|
window.setTimeout(() => inputEl.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistManualNodePosition(nodeId, point) {
|
function persistManualNodePositions(rows) {
|
||||||
const key = normKey(nodeId);
|
const positions = new Map();
|
||||||
const x = Number(point?.x);
|
(Array.isArray(rows) ? rows : []).forEach((row) => {
|
||||||
const y = Number(point?.y);
|
const key = normKey(row?.id || row?.login);
|
||||||
if (!key || !Number.isFinite(x) || !Number.isFinite(y)) return;
|
const x = Number(row?.x);
|
||||||
|
const y = Number(row?.y);
|
||||||
|
if (key && Number.isFinite(x) && Number.isFinite(y)) positions.set(key, { x, y });
|
||||||
|
});
|
||||||
|
if (!positions.size) return;
|
||||||
|
|
||||||
// Координаты исторических snapshot'ов уже находятся в общей world-системе. Обновляем все
|
// Координаты исторических snapshot'ов уже находятся в общей world-системе. Обновляем все
|
||||||
// упоминания пользователя, чтобы следующий setModel/filter/history render не откатил ручной drag.
|
// упоминания пользователя, чтобы следующий setModel/filter/history render не откатил ручной drag.
|
||||||
@@ -1110,12 +1125,13 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
const model = cloneEngineModel(snapshot?.engineModel || { focusId: '', nodes: [] });
|
const model = cloneEngineModel(snapshot?.engineModel || { focusId: '', nodes: [] });
|
||||||
let changed = false;
|
let changed = false;
|
||||||
model.nodes = model.nodes.map((node) => {
|
model.nodes = model.nodes.map((node) => {
|
||||||
if (normKey(node?.id) !== key) return node;
|
const point = positions.get(normKey(node?.id));
|
||||||
|
if (!point) return node;
|
||||||
changed = true;
|
changed = true;
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
layoutX: x,
|
layoutX: point.x,
|
||||||
layoutY: y,
|
layoutY: point.y,
|
||||||
fixedLayout: true,
|
fixedLayout: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1149,10 +1165,19 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
}
|
}
|
||||||
void load(node.login, { pushHistory: true, transitionAngle, transitionX, transitionY });
|
void load(node.login, { pushHistory: true, transitionAngle, transitionX, transitionY });
|
||||||
},
|
},
|
||||||
// тап по центру — полноценный профиль
|
// Короткий тап по текущему центру сразу открывает его меню.
|
||||||
onCenterTap: (node) => {
|
onCenterTap: (node) => {
|
||||||
const routeTo = profileInfoRoute(node.login);
|
const rect = node?.el?.getBoundingClientRect?.();
|
||||||
if (routeTo) navigate(routeTo);
|
openNodeMenu({
|
||||||
|
login: normalizeLogin(node?.login),
|
||||||
|
displayName: String(node?.name || '').trim(),
|
||||||
|
relationType: node?.relationType,
|
||||||
|
point: rect ? { x: rect.left + rect.width / 2, y: rect.top, rect } : undefined,
|
||||||
|
actions: [
|
||||||
|
{ label: 'Профиль', onClick: () => { const r = profileInfoRoute(node?.login); if (r) navigate(r); } },
|
||||||
|
{ label: 'Написать', onClick: () => navigate(`chat/${encodeURIComponent(normalizeLogin(node?.login))}`) },
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// долгое нажатие — контекстное меню (вне масштабируемого холста)
|
// долгое нажатие — контекстное меню (вне масштабируемого холста)
|
||||||
onNodeLongPress: (node, point) => {
|
onNodeLongPress: (node, point) => {
|
||||||
@@ -1171,7 +1196,12 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
// Drag периферийного аватара — ручная правка текущей карты. Движок уже двигает DOM/рёбра
|
// Drag периферийного аватара — ручная правка текущей карты. Движок уже двигает DOM/рёбра
|
||||||
// в реальном времени; здесь только сохраняем итоговую world-позицию в историю/X2 snapshot.
|
// в реальном времени; здесь только сохраняем итоговую world-позицию в историю/X2 snapshot.
|
||||||
onNodeMoveEnd: (node, point) => {
|
onNodeMoveEnd: (node, point) => {
|
||||||
persistManualNodePosition(node?.id || node?.login, point);
|
persistManualNodePositions([{ id: node?.id || node?.login, ...point }]);
|
||||||
|
},
|
||||||
|
// Центр и узлы, которые в последний раз были нарисованы возле него,
|
||||||
|
// сохраняются одной пачкой, чтобы общий узел не вернулся в старый кластер при rebuild.
|
||||||
|
onGroupMoveEnd: (movedNodes) => {
|
||||||
|
persistManualNodePositions(movedNodes);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,13 +219,14 @@ function hash01(str) {
|
|||||||
* @param {Object} opts
|
* @param {Object} opts
|
||||||
* @param {HTMLElement} opts.stage - контейнер сцены (position: relative/absolute, overflow hidden)
|
* @param {HTMLElement} opts.stage - контейнер сцены (position: relative/absolute, overflow hidden)
|
||||||
* @param {Object} opts.model - нормализованная модель { focusId, nodes[] }
|
* @param {Object} opts.model - нормализованная модель { focusId, nodes[] }
|
||||||
* @param {Function} [opts.onCenterTap] - тап по центральному узлу (node) => void
|
* @param {Function} [opts.onCenterTap] - тап по текущему центральному узлу (node) => void
|
||||||
* @param {Function} [opts.onNodeTap] - тап по периферийному узлу (node) => void (вызывается ДО центрирования)
|
* @param {Function} [opts.onNodeTap] - тап по периферийному узлу (node) => void (вызывается ДО центрирования)
|
||||||
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
||||||
* @param {Function} [opts.onNodeMoveEnd] - ручное перемещение периферийного узла (node, {x,y}) => void
|
* @param {Function} [opts.onNodeMoveEnd] - ручное перемещение периферийного узла (node, {x,y}) => void
|
||||||
|
* @param {Function} [opts.onGroupMoveEnd] - перемещение исторического кластера (nodes[]) => void
|
||||||
* @returns {{ destroy: Function, recenter: Function, setModel: Function, getFocusNode: Function }}
|
* @returns {{ destroy: Function, recenter: Function, setModel: Function, getFocusNode: Function }}
|
||||||
*/
|
*/
|
||||||
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeMoveEnd, onNodeHover, onDiveChange } = {}) {
|
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeMoveEnd, onGroupMoveEnd, onNodeHover, onDiveChange } = {}) {
|
||||||
// Слои DOM
|
// Слои DOM
|
||||||
const edgesSvg = document.createElementNS(SVGNS, 'svg');
|
const edgesSvg = document.createElementNS(SVGNS, 'svg');
|
||||||
edgesSvg.setAttribute('class', 'fg-edges');
|
edgesSvg.setAttribute('class', 'fg-edges');
|
||||||
@@ -658,6 +659,8 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
node.relationType = src.relationType;
|
node.relationType = src.relationType;
|
||||||
node.shining = Boolean(src.shining);
|
node.shining = Boolean(src.shining);
|
||||||
node.official = Boolean(src.official);
|
node.official = Boolean(src.official);
|
||||||
|
node.isHistoryCenter = Boolean(src.isHistoryCenter);
|
||||||
|
node.clusterOwnerId = String(src.clusterOwnerId || src.id || '');
|
||||||
node.keepVisible = Boolean(src.keepVisible);
|
node.keepVisible = Boolean(src.keepVisible);
|
||||||
node.alwaysVisible = Boolean(src.alwaysVisible);
|
node.alwaysVisible = Boolean(src.alwaysVisible);
|
||||||
node.edgeParents = Array.isArray(src.edgeParents) ? src.edgeParents.map((row) => ({ ...row })) : [];
|
node.edgeParents = Array.isArray(src.edgeParents) ? src.edgeParents.map((row) => ({ ...row })) : [];
|
||||||
@@ -1472,6 +1475,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
let nodeDragActive = false;
|
let nodeDragActive = false;
|
||||||
let nodeDragStartX = 0;
|
let nodeDragStartX = 0;
|
||||||
let nodeDragStartY = 0;
|
let nodeDragStartY = 0;
|
||||||
|
let groupDragActive = false;
|
||||||
|
let groupDragNodes = [];
|
||||||
|
let groupDragStarts = new Map();
|
||||||
let longTimer = 0;
|
let longTimer = 0;
|
||||||
let longFired = false;
|
let longFired = false;
|
||||||
const activePointers = new Map(); // id → {x, y}: для щипкового зума двумя пальцами
|
const activePointers = new Map(); // id → {x, y}: для щипкового зума двумя пальцами
|
||||||
@@ -1602,8 +1608,16 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
if (downNodeEl) { downNodeEl.classList.add('is-pressed'); haptic(6); } // тактильный «клик» вдавливания
|
if (downNodeEl) { downNodeEl.classList.add('is-pressed'); haptic(6); } // тактильный «клик» вдавливания
|
||||||
downNode = nodeFromEvent(ev);
|
downNode = nodeFromEvent(ev);
|
||||||
nodeDragActive = false;
|
nodeDragActive = false;
|
||||||
|
groupDragActive = false;
|
||||||
|
groupDragNodes = [];
|
||||||
|
groupDragStarts = new Map();
|
||||||
nodeDragStartX = Number(downNode?.x) || 0;
|
nodeDragStartX = Number(downNode?.x) || 0;
|
||||||
nodeDragStartY = Number(downNode?.y) || 0;
|
nodeDragStartY = Number(downNode?.y) || 0;
|
||||||
|
if (downNode?.isHistoryCenter) {
|
||||||
|
const ownerId = String(downNode.clusterOwnerId || downNode.id);
|
||||||
|
groupDragNodes = nodes.filter((node) => String(node.clusterOwnerId || node.id) === ownerId);
|
||||||
|
groupDragStarts = new Map(groupDragNodes.map((node) => [String(node.id), { x: Number(node.x) || 0, y: Number(node.y) || 0 }]));
|
||||||
|
}
|
||||||
// касание пальцем по узлу = «наведение» (превью ветки), как ховер мышью; мышь обслуживают over/out
|
// касание пальцем по узлу = «наведение» (превью ветки), как ховер мышью; мышь обслуживают over/out
|
||||||
if (downNode && ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(downNode, true);
|
if (downNode && ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(downNode, true);
|
||||||
if (downNode && typeof onNodeLongPress === 'function') {
|
if (downNode && typeof onNodeLongPress === 'function') {
|
||||||
@@ -1655,15 +1669,34 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
moved = true;
|
moved = true;
|
||||||
if (longTimer) { window.clearTimeout(longTimer); longTimer = 0; }
|
if (longTimer) { window.clearTimeout(longTimer); longTimer = 0; }
|
||||||
if (downNodeEl) downNodeEl.classList.remove('is-pressed'); // это drag/pan, а не нажатие
|
if (downNodeEl) downNodeEl.classList.remove('is-pressed'); // это drag/pan, а не нажатие
|
||||||
// Не центральный аватар перетаскивается сам. Пустой фон или центральный узел продолжают панорамировать карту.
|
// Исторический центр тащит свой кластер; обычный узел — только себя; пустой фон — всю карту.
|
||||||
nodeDragActive = Boolean(downNode && !downNode.isFocus);
|
groupDragActive = Boolean(downNode?.isHistoryCenter && groupDragNodes.length);
|
||||||
if (nodeDragActive && cssBloom) endCssBloom();
|
nodeDragActive = Boolean(downNode && !groupDragActive);
|
||||||
|
if ((nodeDragActive || groupDragActive) && cssBloom) endCssBloom();
|
||||||
// палец «съехал» с узла — снимаем временный ховер-превью (касанием), если он был
|
// палец «съехал» с узла — снимаем временный ховер-превью (касанием), если он был
|
||||||
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
||||||
camTargetX = null; camTargetY = null; // свайп отменяет доводчик камеры (приоритет жеста)
|
camTargetX = null; camTargetY = null; // свайп отменяет доводчик камеры (приоритет жеста)
|
||||||
cancelTween(); // жест прерывает анимацию центрирования
|
cancelTween(); // жест прерывает анимацию центрирования
|
||||||
dragging = true;
|
dragging = true;
|
||||||
}
|
}
|
||||||
|
if (moved && groupDragActive) {
|
||||||
|
const zx = dx / Math.max(0.001, zoom);
|
||||||
|
const zy = dy / Math.max(0.001, zoom);
|
||||||
|
groupDragNodes.forEach((node) => {
|
||||||
|
const start = groupDragStarts.get(String(node.id));
|
||||||
|
if (!start) return;
|
||||||
|
const nx = start.x + zx;
|
||||||
|
const ny = start.y + zy;
|
||||||
|
node.x = nx; node.y = ny;
|
||||||
|
node.tx = nx; node.ty = ny;
|
||||||
|
node.bfx = nx; node.bfy = ny;
|
||||||
|
node.vx = 0; node.vy = 0;
|
||||||
|
node.fixedLayout = true;
|
||||||
|
});
|
||||||
|
renderNodes();
|
||||||
|
renderEdges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (moved && nodeDragActive && downNode) {
|
if (moved && nodeDragActive && downNode) {
|
||||||
// dx/dy приходят в экранных пикселях, координаты узла живут в world-space — делим на текущий zoom.
|
// dx/dy приходят в экранных пикселях, координаты узла живут в world-space — делим на текущий zoom.
|
||||||
const nx = nodeDragStartX + dx / Math.max(0.001, zoom);
|
const nx = nodeDragStartX + dx / Math.max(0.001, zoom);
|
||||||
@@ -1708,9 +1741,11 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
const wasMoved = moved;
|
const wasMoved = moved;
|
||||||
const wasLong = longFired;
|
const wasLong = longFired;
|
||||||
const movedNode = nodeDragActive ? downNode : null;
|
const movedNode = nodeDragActive ? downNode : null;
|
||||||
|
const movedGroup = groupDragActive ? [...groupDragNodes] : [];
|
||||||
pointerId = null;
|
pointerId = null;
|
||||||
dragging = false;
|
dragging = false;
|
||||||
nodeDragActive = false;
|
nodeDragActive = false;
|
||||||
|
groupDragActive = false;
|
||||||
// касание: убрали палец — снимаем временный ховер-превью (фиксацию ниже делает тап через onNodeTap)
|
// касание: убрали палец — снимаем временный ховер-превью (фиксацию ниже делает тап через onNodeTap)
|
||||||
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
||||||
|
|
||||||
@@ -1719,6 +1754,11 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
// Передаём окончательную world-позицию наружу, чтобы ручная правка пережила следующий setModel/history render.
|
// Передаём окончательную world-позицию наружу, чтобы ручная правка пережила следующий setModel/history render.
|
||||||
if (typeof onNodeMoveEnd === 'function') onNodeMoveEnd(movedNode, { x: movedNode.x, y: movedNode.y });
|
if (typeof onNodeMoveEnd === 'function') onNodeMoveEnd(movedNode, { x: movedNode.x, y: movedNode.y });
|
||||||
renderEdges();
|
renderEdges();
|
||||||
|
} else if (wasMoved && movedGroup.length) {
|
||||||
|
if (typeof onGroupMoveEnd === 'function') {
|
||||||
|
onGroupMoveEnd(movedGroup.map((node) => ({ id: node.id, login: node.login, x: node.x, y: node.y })));
|
||||||
|
}
|
||||||
|
renderEdges();
|
||||||
} else if (wasMoved) {
|
} else if (wasMoved) {
|
||||||
// после pan даём физике чуть устаканиться и уснуть
|
// после pan даём физике чуть устаканиться и уснуть
|
||||||
wake();
|
wake();
|
||||||
@@ -1866,8 +1906,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
|||||||
}
|
}
|
||||||
|
|
||||||
// финальные координаты разлёта (детерминированная орбита с джиттером — в node.tx/ty)
|
// финальные координаты разлёта (детерминированная орбита с джиттером — в node.tx/ty)
|
||||||
const finalX = node.isFocus ? 0 : node.tx;
|
// Ручно сдвинутый текущий центр не должен прыгать обратно в (0,0) при setModel.
|
||||||
const finalY = node.isFocus ? 0 : node.ty;
|
const finalX = node.tx;
|
||||||
|
const finalY = node.ty;
|
||||||
const finalScale = node.targetScale; // масштаб уже по уровню (focus / tier-1 / tier-2 0.5 / tier-3 точка)
|
const finalScale = node.targetScale; // масштаб уже по уровню (focus / tier-1 / tier-2 0.5 / tier-3 точка)
|
||||||
const finalOp = node.targetOpacity; // прозрачность по уровню (tier-2 ~0.4, tier-3 ~0.9, иначе 1)
|
const finalOp = node.targetOpacity; // прозрачность по уровню (tier-2 ~0.4, tier-3 ~0.9, иначе 1)
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export function render({ navigate, chrome }) {
|
|||||||
size: 'xl',
|
size: 'xl',
|
||||||
className: 'user-profile-hero-avatar',
|
className: 'user-profile-hero-avatar',
|
||||||
glow: shining,
|
glow: shining,
|
||||||
|
official,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
status.textContent = '';
|
status.textContent = '';
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
size: 'xl',
|
size: 'xl',
|
||||||
className: 'user-profile-hero-avatar',
|
className: 'user-profile-hero-avatar',
|
||||||
glow: shining,
|
glow: shining,
|
||||||
|
official,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
addMenu = body.querySelector('.user-profile-add-menu');
|
addMenu = body.querySelector('.user-profile-add-menu');
|
||||||
|
|||||||
@@ -269,3 +269,23 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Единый знак официального пользователя. Левый и нижний края зафиксированы,
|
||||||
|
а сам знак растёт вправо и вверх вместе с аватаром. */
|
||||||
|
.avatar.avatar-image.avatar-framed > .avatar-official-badge {
|
||||||
|
position: absolute;
|
||||||
|
left: -2%;
|
||||||
|
bottom: -1%;
|
||||||
|
width: 32%;
|
||||||
|
height: 32%;
|
||||||
|
min-width: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: center;
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 4;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.42));
|
||||||
|
}
|
||||||
|
|||||||
@@ -800,7 +800,10 @@
|
|||||||
|
|
||||||
.user-profile-screen .user-profile-hero-avatar.avatar-glow::before {
|
.user-profile-screen .user-profile-hero-avatar.avatar-glow::before {
|
||||||
inset: -12%;
|
inset: -12%;
|
||||||
opacity: 0.5;
|
background: radial-gradient(circle, rgba(140, 240, 255, 0.5) 0%, rgba(130, 235, 255, 0.18) 46%, rgba(130, 235, 255, 0) 72%);
|
||||||
|
filter: blur(3.4px);
|
||||||
|
transform: scale(1.07);
|
||||||
|
opacity: 0.91;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -210,10 +210,10 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: -2%;
|
left: -2%;
|
||||||
bottom: -1%;
|
bottom: -1%;
|
||||||
width: 16%;
|
width: 32%;
|
||||||
height: 16%;
|
height: 32%;
|
||||||
min-width: 9px;
|
min-width: 18px;
|
||||||
min-height: 9px;
|
min-height: 18px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
Reference in New Issue
Block a user