feat(ui): add animated canvas panning when locating nodes

Replace instant canvas jumps with smooth animated transitions using
ease-in-out-cubic easing. The animateCanvasToNode method calculates the
target offset based on node position and canvas dimensions, then smoothly
interpolates the canvas offset over 380ms. Includes proper cleanup of
pending animation frames and fallback handling for older ComfyUI versions.
This commit is contained in:
Dariusz L
2026-05-10 06:35:52 +02:00
parent 9783a1641d
commit 25760bb7d8
+65 -6
View File
@@ -32,6 +32,7 @@ class LinkerManagerDialog extends ComfyDialog {
this._dragging = false;
this._dragStart = null;
this._analysisProgressToken = null;
this._locateAnimationFrame = null;
// Inject global styles for the redesigned UI
this.injectStyles();
@@ -5547,11 +5548,14 @@ class LinkerManagerDialog extends ComfyDialog {
return;
}
// Focus on the node in the canvas
if (app.canvas && typeof app.canvas.centerOnNode === 'function') {
// Modern ComfyUI versions
app.canvas.centerOnNode(node);
} else if (app.graph._nodes && app.graph._nodes.get(nodeId)) {
let locatedWithAnimation = false;
// Focus on the node in the canvas with animated panning when possible
if (app.canvas?.ds && app.canvas?.canvas) {
locatedWithAnimation = this.animateCanvasToNode(node);
}
if (!locatedWithAnimation && app.graph._nodes && app.graph._nodes.get(nodeId)) {
// Alternative method for older versions
const canvasNode = app.graph._nodes.get(nodeId);
if (canvasNode && canvasNode.setSelected && canvasNode.graph) {
@@ -5559,9 +5563,12 @@ class LinkerManagerDialog extends ComfyDialog {
// Scroll to node
app.canvas.scrollToNode(canvasNode);
}
} else if (app.ui && app.ui.nodeGraph && typeof app.ui.nodeGraph.scrollToNode === 'function') {
} else if (!locatedWithAnimation && app.ui && app.ui.nodeGraph && typeof app.ui.nodeGraph.scrollToNode === 'function') {
// Alternative for other versions
app.ui.nodeGraph.scrollToNode(node);
} else if (!locatedWithAnimation && app.canvas && typeof app.canvas.centerOnNode === 'function') {
// Final fallback for versions where animation path is unavailable
app.canvas.centerOnNode(node);
}
// Also try to flash/select the node
@@ -5582,6 +5589,58 @@ class LinkerManagerDialog extends ComfyDialog {
}
}
animateCanvasToNode(node) {
const canvas = app?.canvas;
const ds = canvas?.ds;
const htmlCanvas = canvas?.canvas;
if (!canvas || !ds || !htmlCanvas) return false;
const rect = node.boundingRect || [node.pos?.[0] || 0, node.pos?.[1] || 0, node.size?.[0] || 0, node.size?.[1] || 0];
const centerX = rect[0] + (rect[2] || 0) / 2;
const centerY = rect[1] + (rect[3] || 0) / 2;
const scale = Number.isFinite(ds.scale) && ds.scale > 0 ? ds.scale : 1;
const dpr = window.devicePixelRatio || 1;
const viewWidth = (htmlCanvas.width || htmlCanvas.clientWidth || 0) / (scale * dpr);
const viewHeight = (htmlCanvas.height || htmlCanvas.clientHeight || 0) / (scale * dpr);
if (!viewWidth || !viewHeight) return false;
const targetX = -centerX + (viewWidth / 2);
const targetY = -centerY + (viewHeight / 2);
const startX = Number.isFinite(ds.offset?.[0]) ? ds.offset[0] : targetX;
const startY = Number.isFinite(ds.offset?.[1]) ? ds.offset[1] : targetY;
if (this._locateAnimationFrame) {
cancelAnimationFrame(this._locateAnimationFrame);
this._locateAnimationFrame = null;
}
const durationMs = 380;
const startTime = performance.now();
const easeInOutCubic = (t) => t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
const tick = (now) => {
const progress = Math.min(1, (now - startTime) / durationMs);
const eased = easeInOutCubic(progress);
ds.offset[0] = startX + ((targetX - startX) * eased);
ds.offset[1] = startY + ((targetY - startY) * eased);
canvas.setDirty?.(true, true);
app.graph?.setDirtyCanvas?.(true, true);
if (progress < 1) {
this._locateAnimationFrame = requestAnimationFrame(tick);
} else {
this._locateAnimationFrame = null;
}
};
this._locateAnimationFrame = requestAnimationFrame(tick);
return true;
}
/**
* Reconnect active downloads to their new progress div elements after UI refresh
*/