Связи: чистка мёртвого кода в движке (lerp, неиспользуемые easing)

- Убран механизм lerpX/lerpY: координаты для отрисовки берутся из n.x/n.y, lerp нигде
  не читался кроме условия заморозки (lerpSettling). Удалены поля, advanceLerp(), EDGE_LERP
  и lerpSettling — граф засыпает чуть раньше (без визуальных изменений; проверено: frozen=true).
- Удалены неиспользуемые cubicBezier() и EASE_BLOOM (easing теперь делает CSS); easeOutCubic
  оставлен (нужен в stepTween для фолбэк-центрирования).
- Документация фичи актуализирована (убрана заметка про lerp как кандидата на чистку).
- Бамп client.version → 1.2.139.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pixel 2026-06-09 21:23:18 +03:00
parent dc96033cb1
commit e6e96c4b0d
3 changed files with 5 additions and 47 deletions

View File

@ -1,2 +1,2 @@
client.version=1.2.138 client.version=1.2.139
server.version=1.2.127 server.version=1.2.127

View File

@ -91,6 +91,5 @@
## Ограничения / на будущее ## Ограничения / на будущее
- Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи» - Многоуровневая глубина (друзья друзей мельче, 3-й уровень — точки), кластеры, «общие связи»
упираются в API (отдаёт только прямые связи) — требуют доработки сервера. упираются в API (отдаёт только прямые связи) — требуют доработки сервера.
- `lerpX/lerpY` в движке больше не используются для отрисовки — кандидат на чистку.
- Превью в простое троттлит `requestAnimationFrame` (физика не идёт между вызовами) — для замеров - Превью в простое троттлит `requestAnimationFrame` (физика не идёт между вызовами) — для замеров
прокачивать кадры; в активном табе всё работает на 60 FPS. прокачивать кадры; в активном табе всё работает на 60 FPS.

View File

@ -31,7 +31,6 @@ const FRICTION_BOOST = 0.94; // «гелевая» вязкость в пер
const BOOST_FRAMES = 42; // длительность затухающего boost'а вязкости (~700мс @60fps) const BOOST_FRAMES = 42; // длительность затухающего boost'а вязкости (~700мс @60fps)
const SLEEP_V = 0.03; // порог суммарной |vx|+|vy| для жёсткой заморозки графа const SLEEP_V = 0.03; // порог суммарной |vx|+|vy| для жёсткой заморозки графа
const INTRO_FACTOR = 0.22; // стартовый радиус пера (доля от целевого) — узлы «вылетают» из центра const INTRO_FACTOR = 0.22; // стартовый радиус пера (доля от целевого) — узлы «вылетают» из центра
const EDGE_LERP = 0.25; // догон концов линии за узлом за кадр (эффект натянутой резинки)
const PAN_FRICTION = 0.93; // трение инерционного скролла карты const PAN_FRICTION = 0.93; // трение инерционного скролла карты
const TWEEN_MS = 560; // длительность анимации центрирования (фильтр/фолбэк) const TWEEN_MS = 560; // длительность анимации центрирования (фильтр/фолбэк)
const BLOOM_MS = 900; // длительность разлёта узлов из центра (физика выключена → ноль тряски) const BLOOM_MS = 900; // длительность разлёта узлов из центра (физика выключена → ноль тряски)
@ -59,30 +58,6 @@ function easeOutCubic(t) {
return 1 - x * x * x; return 1 - x * x * x;
} }
// Решатель кубической кривой Безье (CSS cubic-bezier): прогресс x → значение y.
function cubicBezier(x1, y1, x2, y2) {
const cx = 3 * x1;
const bx = 3 * (x2 - x1) - cx;
const ax = 1 - cx - bx;
const cy = 3 * y1;
const by = 3 * (y2 - y1) - cy;
const ay = 1 - cy - by;
const sampleX = (t) => ((ax * t + bx) * t + cx) * t;
const sampleY = (t) => ((ay * t + by) * t + cy) * t;
const dX = (t) => (3 * ax * t + 2 * bx) * t + cx;
return (x) => {
let t = x;
for (let i = 0; i < 6; i += 1) {
const d = dX(t);
if (Math.abs(d) < 1e-6) break;
t -= (sampleX(t) - x) / d;
}
return sampleY(Math.max(0, Math.min(1, t)));
};
}
// Премиальная «вязкая» кривая для разлёта узлов (быстрый старт → очень мягкая посадка).
const EASE_BLOOM = cubicBezier(0.16, 1, 0.3, 1);
function relationColor(relationType) { function relationColor(relationType) {
return RELATION_COLORS[relationType] || RELATION_COLORS.contact; return RELATION_COLORS[relationType] || RELATION_COLORS.contact;
} }
@ -233,8 +208,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
ty, ty,
x: tx * INTRO_FACTOR, x: tx * INTRO_FACTOR,
y: ty * INTRO_FACTOR, y: ty * INTRO_FACTOR,
lerpX: tx * INTRO_FACTOR,
lerpY: ty * INTRO_FACTOR,
vx: 0, vx: 0,
vy: 0, vy: 0,
scale, scale,
@ -517,14 +490,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
return totalV; return totalV;
} }
// Концы линий догоняют узлы с запаздыванием (эффект резинки): lerp-позиция тянется за реальной.
function advanceLerp() {
for (const n of nodes) {
n.lerpX += (n.x - n.lerpX) * EDGE_LERP;
n.lerpY += (n.y - n.lerpY) * EDGE_LERP;
}
}
// Плавное приближение масштаба/прозрачности к целям + рост линии («прорастание»). // Плавное приближение масштаба/прозрачности к целям + рост линии («прорастание»).
function advanceVisual() { function advanceVisual() {
for (const n of nodes) { for (const n of nodes) {
@ -613,7 +578,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
const ao = a.opacity ?? 1; const ao = a.opacity ?? 1;
const bo = b.opacity ?? 1; const bo = b.opacity ?? 1;
n.opacity = ao + (bo - ao) * t; n.opacity = ao + (bo - ao) * t;
n.lerpX = n.x; n.lerpY = n.y;
if (b.grow) n.edgeGrow = raw; // линия «вытекает» по прогрессу своего узла if (b.grow) n.edgeGrow = raw; // линия «вытекает» по прогрессу своего узла
} }
const camT = ease(Math.min(1, elapsed / dur)); const camT = ease(Math.min(1, elapsed / dur));
@ -693,15 +657,13 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
} }
// Жёсткая заморозка: гасим скорости, округляем координаты до целых пикселей, // Жёсткая заморозка: гасим скорости, округляем координаты до целых пикселей,
// подтягиваем lerp и НЕ перезапускаем цикл — граф замирает намертво (без «треска»). // НЕ перезапускаем цикл — граф замирает намертво (без «треска»).
function freezeGraph() { function freezeGraph() {
for (const n of nodes) { for (const n of nodes) {
n.vx = 0; n.vx = 0;
n.vy = 0; n.vy = 0;
n.x = Math.round(n.x); n.x = Math.round(n.x);
n.y = Math.round(n.y); n.y = Math.round(n.y);
n.lerpX = n.x;
n.lerpY = n.y;
n.scale = n.targetScale; n.scale = n.targetScale;
n.opacity = n.targetOpacity; n.opacity = n.targetOpacity;
} }
@ -748,11 +710,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
advanceVisual(); // bloom/смена роли вне твина — через целевые scale/opacity advanceVisual(); // bloom/смена роли вне твина — через целевые scale/opacity
} }
advanceLerp();
renderAll(); renderAll();
const lerpSettling = nodes.some((n) => Math.abs(n.x - n.lerpX) + Math.abs(n.y - n.lerpY) > 0.5); if (tween || dragging || panActive || boost > 0 || totalV > SLEEP_V || visualSettling()) {
if (tween || dragging || panActive || boost > 0 || totalV > SLEEP_V || lerpSettling || visualSettling()) {
schedule(); schedule();
} else { } else {
freezeGraph(); // система успокоилась — замираем freezeGraph(); // система успокоилась — замираем
@ -910,7 +870,6 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
const r = dot.getBoundingClientRect(); const r = dot.getBoundingClientRect();
n.x = (r.left + r.width / 2) - sr.left - centerX - camX; n.x = (r.left + r.width / 2) - sr.left - centerX - camX;
n.y = (r.top + r.height / 2) - sr.top - centerY - camY; n.y = (r.top + r.height / 2) - sr.top - centerY - camY;
n.lerpX = n.x; n.lerpY = n.y;
// живая прозрачность из CSS-перехода — чтобы лучи гасли/проявлялись вместе с аватаркой // живая прозрачность из CSS-перехода — чтобы лучи гасли/проявлялись вместе с аватаркой
const o = parseFloat(getComputedStyle(n.el).opacity); const o = parseFloat(getComputedStyle(n.el).opacity);
if (Number.isFinite(o)) n.opacity = o; if (Number.isFinite(o)) n.opacity = o;
@ -926,7 +885,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
for (const n of nodes) { for (const n of nodes) {
n.el.style.transition = ''; n.el.style.transition = '';
const fo = (typeof n.bfo === 'number') ? n.bfo : 1; // финальная прозрачность (0 — скрыт фильтром) const fo = (typeof n.bfo === 'number') ? n.bfo : 1; // финальная прозрачность (0 — скрыт фильтром)
n.x = n.bfx; n.y = n.bfy; n.lerpX = n.x; n.lerpY = n.y; n.x = n.bfx; n.y = n.bfy;
n.scale = n.bfs; n.targetScale = n.bfs; n.opacity = fo; n.targetOpacity = fo; n.scale = n.bfs; n.targetScale = n.bfs; n.opacity = fo; n.targetOpacity = fo;
n.vx = 0; n.vy = 0; n.edgeGrow = 1; n.vx = 0; n.vy = 0; n.edgeGrow = 1;
} }
@ -1001,7 +960,7 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
// финал запоминаем для покоя; стартовое состояние держим в n.x/n.y (для первой отрисовки лучей) // финал запоминаем для покоя; стартовое состояние держим в n.x/n.y (для первой отрисовки лучей)
node.bfx = finalX; node.bfy = finalY; node.bfs = finalScale; node.bfo = 1; node.bfx = finalX; node.bfy = finalY; node.bfs = finalScale; node.bfo = 1;
node.x = fx; node.y = fy; node.lerpX = fx; node.lerpY = fy; node.x = fx; node.y = fy;
node.scale = finalScale; node.opacity = 1; node.targetScale = finalScale; node.targetOpacity = 1; node.scale = finalScale; node.opacity = 1; node.targetScale = finalScale; node.targetOpacity = 1;
node.hidden = false; node.hidden = false;
// НОВЫЙ узел (разлетается из центра) — помечаем для эффекта прорастания линии (edgeGrow=0); // НОВЫЙ узел (разлетается из центра) — помечаем для эффекта прорастания линии (edgeGrow=0);