Fix zoomToPoint animation to smoothly fly-to target node
Interpolate both view center and zoom level together using rAF instead of CSS transitions, so the target node smoothly slides to screen center while zooming in simultaneously. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,14 +105,49 @@ export class GraphCanvas {
|
|||||||
if (this._onZoomChange) this._onZoomChange(this._zoom);
|
if (this._onZoomChange) this._onZoomChange(this._zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set zoom and center on a graph-space point in one animated step. */
|
/**
|
||||||
zoomToPoint(level, gx, gy) {
|
* Animate zoom + pan to center on a graph-space point.
|
||||||
|
* Interpolates the view center in graph-space so the target smoothly
|
||||||
|
* slides to screen center while zoom changes simultaneously.
|
||||||
|
*/
|
||||||
|
zoomToPoint(level, gx, gy, duration = 500) {
|
||||||
|
if (this._zoomAnim) cancelAnimationFrame(this._zoomAnim);
|
||||||
|
|
||||||
const r = this.svg.getBoundingClientRect();
|
const r = this.svg.getBoundingClientRect();
|
||||||
this._zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, level));
|
const hw = r.width / 2;
|
||||||
this._vx = gx - (r.width / this._zoom) / 2;
|
const hh = r.height / 2;
|
||||||
this._vy = gy - (r.height / this._zoom) / 2;
|
|
||||||
this._applyTransform(true);
|
const startZoom = this._zoom;
|
||||||
|
const targetZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, level));
|
||||||
|
// Current view center in graph-space
|
||||||
|
const startCx = this._vx + hw / startZoom;
|
||||||
|
const startCy = this._vy + hh / startZoom;
|
||||||
|
const t0 = performance.now();
|
||||||
|
|
||||||
|
const step = (now) => {
|
||||||
|
const elapsed = now - t0;
|
||||||
|
const p = Math.min(elapsed / duration, 1);
|
||||||
|
// Ease-in-out cubic
|
||||||
|
const t = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
|
||||||
|
|
||||||
|
// Interpolate zoom and view center together
|
||||||
|
this._zoom = startZoom + (targetZoom - startZoom) * t;
|
||||||
|
const cx = startCx + (gx - startCx) * t;
|
||||||
|
const cy = startCy + (gy - startCy) * t;
|
||||||
|
// Convert center back to vx/vy for current zoom
|
||||||
|
this._vx = cx - hw / this._zoom;
|
||||||
|
this._vy = cy - hh / this._zoom;
|
||||||
|
|
||||||
|
this._applyTransform(false);
|
||||||
if (this._onZoomChange) this._onZoomChange(this._zoom);
|
if (this._onZoomChange) this._onZoomChange(this._zoom);
|
||||||
|
|
||||||
|
if (p < 1) {
|
||||||
|
this._zoomAnim = requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
this._zoomAnim = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._zoomAnim = requestAnimationFrame(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
zoomIn() { this.zoomTo(this._zoom * 1.25); }
|
zoomIn() { this.zoomTo(this._zoom * 1.25); }
|
||||||
@@ -141,6 +176,7 @@ export class GraphCanvas {
|
|||||||
|
|
||||||
_onWheel(e) {
|
_onWheel(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (this._zoomAnim) { cancelAnimationFrame(this._zoomAnim); this._zoomAnim = null; }
|
||||||
const delta = -e.deltaY * ZOOM_SENSITIVITY;
|
const delta = -e.deltaY * ZOOM_SENSITIVITY;
|
||||||
const newZoom = this._zoom * (1 + delta);
|
const newZoom = this._zoom * (1 + delta);
|
||||||
this.zoomTo(newZoom, e.clientX, e.clientY);
|
this.zoomTo(newZoom, e.clientX, e.clientY);
|
||||||
@@ -197,6 +233,7 @@ export class GraphCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_startPan(e) {
|
_startPan(e) {
|
||||||
|
if (this._zoomAnim) { cancelAnimationFrame(this._zoomAnim); this._zoomAnim = null; }
|
||||||
this._panning = true;
|
this._panning = true;
|
||||||
this._panPending = false;
|
this._panPending = false;
|
||||||
this._panStart = { x: e.clientX, y: e.clientY };
|
this._panStart = { x: e.clientX, y: e.clientY };
|
||||||
|
|||||||
@@ -286,9 +286,14 @@ function _watchForNewEntity() {
|
|||||||
loadGraphEditor().then(() => {
|
loadGraphEditor().then(() => {
|
||||||
const node = _nodeMap?.get(newId);
|
const node = _nodeMap?.get(newId);
|
||||||
if (node && _canvas) {
|
if (node && _canvas) {
|
||||||
|
// Animate zoom + pan together in one transition
|
||||||
_canvas.zoomToPoint(1.5, node.x + node.width / 2, node.y + node.height / 2);
|
_canvas.zoomToPoint(1.5, node.x + node.width / 2, node.y + node.height / 2);
|
||||||
}
|
}
|
||||||
_navigateToNode(newId);
|
// Highlight the node and its chain (without re-panning)
|
||||||
|
const nodeGroup = document.querySelector('.graph-nodes');
|
||||||
|
if (nodeGroup) { highlightNode(nodeGroup, newId); setTimeout(() => highlightNode(nodeGroup, null), 3000); }
|
||||||
|
const edgeGroup = document.querySelector('.graph-edges');
|
||||||
|
if (edgeGroup && _edges) { highlightChain(edgeGroup, newId, _edges); setTimeout(() => clearEdgeHighlights(edgeGroup), 5000); }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user