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);
|
||||
}
|
||||
|
||||
/** 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();
|
||||
this._zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, level));
|
||||
this._vx = gx - (r.width / this._zoom) / 2;
|
||||
this._vy = gy - (r.height / this._zoom) / 2;
|
||||
this._applyTransform(true);
|
||||
if (this._onZoomChange) this._onZoomChange(this._zoom);
|
||||
const hw = r.width / 2;
|
||||
const hh = r.height / 2;
|
||||
|
||||
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 (p < 1) {
|
||||
this._zoomAnim = requestAnimationFrame(step);
|
||||
} else {
|
||||
this._zoomAnim = null;
|
||||
}
|
||||
};
|
||||
this._zoomAnim = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
zoomIn() { this.zoomTo(this._zoom * 1.25); }
|
||||
@@ -141,6 +176,7 @@ export class GraphCanvas {
|
||||
|
||||
_onWheel(e) {
|
||||
e.preventDefault();
|
||||
if (this._zoomAnim) { cancelAnimationFrame(this._zoomAnim); this._zoomAnim = null; }
|
||||
const delta = -e.deltaY * ZOOM_SENSITIVITY;
|
||||
const newZoom = this._zoom * (1 + delta);
|
||||
this.zoomTo(newZoom, e.clientX, e.clientY);
|
||||
@@ -197,6 +233,7 @@ export class GraphCanvas {
|
||||
}
|
||||
|
||||
_startPan(e) {
|
||||
if (this._zoomAnim) { cancelAnimationFrame(this._zoomAnim); this._zoomAnim = null; }
|
||||
this._panning = true;
|
||||
this._panPending = false;
|
||||
this._panStart = { x: e.clientX, y: e.clientY };
|
||||
|
||||
@@ -286,9 +286,14 @@ function _watchForNewEntity() {
|
||||
loadGraphEditor().then(() => {
|
||||
const node = _nodeMap?.get(newId);
|
||||
if (node && _canvas) {
|
||||
// Animate zoom + pan together in one transition
|
||||
_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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user