[Refactor] Move animate code to DragAndScale (#745)

This commit is contained in:
filtered
2025-03-11 22:31:21 +11:00
committed by GitHub
parent 58011e77b6
commit 35827b8494
2 changed files with 95 additions and 83 deletions

View File

@@ -1,10 +1,21 @@
import type { Point, Rect, Rect32 } from "./interfaces"
import type { Point, ReadOnlyRect, Rect, Rect32 } from "./interfaces"
import { EaseFunction } from "./litegraph"
export interface DragAndScaleState {
offset: Point
scale: number
}
export type AnimationOptions = {
/** Duration of the animation in milliseconds. */
duration?: number
/** Relative target zoom level. 1 means the view is fit exactly on the bounding box. */
zoom?: number
/** The animation easing function (curve) */
easing?: EaseFunction
}
export class DragAndScale {
/**
* The state of this DragAndScale instance.
@@ -19,7 +30,7 @@ export class DragAndScale {
min_scale: number
enabled: boolean
last_mouse: Point
element?: HTMLCanvasElement
element: HTMLCanvasElement
visible_area: Rect32
dragging?: boolean
viewport?: Rect
@@ -42,7 +53,7 @@ export class DragAndScale {
this.state.scale = value
}
constructor(element?: HTMLCanvasElement) {
constructor(element: HTMLCanvasElement) {
this.state = {
offset: new Float32Array([0, 0]),
scale: 1,
@@ -144,6 +155,80 @@ export class DragAndScale {
this.changeScale(this.scale * value, zooming_center)
}
/**
* Starts an animation to fit the view around the specified selection of nodes.
* @param bounds The bounds to animate the view to, defined by a rectangle.
*/
animateToBounds(
bounds: ReadOnlyRect,
setDirty: () => void,
{
duration = 350,
zoom = 0.75,
easing = EaseFunction.EASE_IN_OUT_QUAD,
}: AnimationOptions = {},
) {
const easeFunctions = {
linear: (t: number) => t,
easeInQuad: (t: number) => t * t,
easeOutQuad: (t: number) => t * (2 - t),
easeInOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
}
const easeFunction = easeFunctions[easing] ?? easeFunctions.linear
const startTimestamp = performance.now()
const startX = this.offset[0]
const startY = this.offset[1]
const startX2 = startX - (this.element.width / this.scale)
const startY2 = startY - (this.element.height / this.scale)
const startScale = this.scale
const cw = this.element.width / window.devicePixelRatio
const ch = this.element.height / window.devicePixelRatio
let targetScale = startScale
if (zoom > 0) {
const targetScaleX = (zoom * cw) / Math.max(bounds[2], 300)
const targetScaleY = (zoom * ch) / Math.max(bounds[3], 300)
// Choose the smaller scale to ensure the node fits into the viewport
// Ensure we don't go over the max scale
targetScale = Math.min(targetScaleX, targetScaleY, this.max_scale)
}
const targetX = -bounds[0] - bounds[2] * 0.5 + (cw * 0.5) / targetScale
const targetY = -bounds[1] - bounds[3] * 0.5 + (ch * 0.5) / targetScale
const targetX2 = targetX - (Math.max(bounds[2], 300) / zoom)
const targetY2 = targetY - (Math.max(bounds[3], 300) / zoom)
const animate = (timestamp: number) => {
const elapsed = timestamp - startTimestamp
const progress = Math.min(elapsed / duration, 1)
const easedProgress = easeFunction(progress)
const currentX = startX + (targetX - startX) * easedProgress
const currentY = startY + (targetY - startY) * easedProgress
this.offset[0] = currentX
this.offset[1] = currentY
if (zoom > 0) {
const currentX2 = startX2 + ((targetX2 - startX2) * easedProgress)
const currentY2 = startY2 + ((targetY2 - startY2) * easedProgress)
const currentWidth = Math.abs(currentX2 - currentX)
const currentHeight = Math.abs(currentY2 - currentY)
this.scale = Math.min(this.element.width / currentWidth, this.element.height / currentHeight)
}
setDirty()
if (progress < 1) {
animationId = requestAnimationFrame(animate)
} else {
cancelAnimationFrame(animationId)
}
}
let animationId = requestAnimationFrame(animate)
}
reset(): void {
this.scale = 1
this.offset[0] = 0

View File

@@ -40,7 +40,7 @@ import { LinkConnector } from "@/canvas/LinkConnector"
import { isOverNodeInput, isOverNodeOutput } from "./canvas/measureSlots"
import { CanvasPointer } from "./CanvasPointer"
import { DragAndScale } from "./DragAndScale"
import { type AnimationOptions, DragAndScale } from "./DragAndScale"
import { strokeShape } from "./draw"
import { NullGraphError } from "./infrastructure/NullGraphError"
import { LGraphGroup } from "./LGraphGroup"
@@ -63,7 +63,6 @@ import { Reroute, type RerouteId } from "./Reroute"
import { stringOrEmpty } from "./strings"
import {
CanvasItem,
EaseFunction,
LGraphEventMode,
LinkDirection,
LinkMarkerShape,
@@ -608,7 +607,7 @@ export class LGraphCanvas implements ConnectionColorContext {
// throw ("No graph assigned");
this.background_image = LGraphCanvas.DEFAULT_BACKGROUND_IMAGE
this.ds = new DragAndScale()
this.ds = new DragAndScale(canvas)
this.pointer = new CanvasPointer(canvas)
// @deprecated Workaround: Keep until connecting_links is removed.
@@ -7240,73 +7239,9 @@ export class LGraphCanvas implements ConnectionColorContext {
* Starts an animation to fit the view around the specified selection of nodes.
* @param bounds The bounds to animate the view to, defined by a rectangle.
*/
animateToBounds(
bounds: ReadOnlyRect,
{
duration = 350,
zoom = 0.75,
easing = EaseFunction.EASE_IN_OUT_QUAD,
}: AnimationOptions = {},
) {
const easeFunctions = {
linear: (t: number) => t,
easeInQuad: (t: number) => t * t,
easeOutQuad: (t: number) => t * (2 - t),
easeInOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
}
const easeFunction = easeFunctions[easing] ?? easeFunctions.linear
const startTimestamp = performance.now()
const startX = this.ds.offset[0]
const startY = this.ds.offset[1]
const startX2 = startX - (this.canvas.width / this.ds.scale)
const startY2 = startY - (this.canvas.height / this.ds.scale)
const startScale = this.ds.scale
const cw = this.canvas.width / window.devicePixelRatio
const ch = this.canvas.height / window.devicePixelRatio
let targetScale = startScale
if (zoom > 0) {
const targetScaleX = (zoom * cw) / Math.max(bounds[2], 300)
const targetScaleY = (zoom * ch) / Math.max(bounds[3], 300)
// Choose the smaller scale to ensure the node fits into the viewport
// Ensure we don't go over the max scale
targetScale = Math.min(targetScaleX, targetScaleY, this.ds.max_scale)
}
const targetX = -bounds[0] - bounds[2] * 0.5 + (cw * 0.5) / targetScale
const targetY = -bounds[1] - bounds[3] * 0.5 + (ch * 0.5) / targetScale
const targetX2 = targetX - (Math.max(bounds[2], 300) / zoom)
const targetY2 = targetY - (Math.max(bounds[3], 300) / zoom)
const animate = (timestamp: number) => {
const elapsed = timestamp - startTimestamp
const progress = Math.min(elapsed / duration, 1)
const easedProgress = easeFunction(progress)
const currentX = startX + (targetX - startX) * easedProgress
const currentY = startY + (targetY - startY) * easedProgress
this.ds.offset[0] = currentX
this.ds.offset[1] = currentY
if (zoom > 0) {
const currentX2 = startX2 + ((targetX2 - startX2) * easedProgress)
const currentY2 = startY2 + ((targetY2 - startY2) * easedProgress)
const currentWidth = Math.abs(currentX2 - currentX)
const currentHeight = Math.abs(currentY2 - currentY)
this.ds.scale = Math.min(this.canvas.width / currentWidth, this.canvas.height / currentHeight)
}
this.setDirty(true, true)
if (progress < 1) {
animationId = requestAnimationFrame(animate)
} else {
cancelAnimationFrame(animationId)
}
}
let animationId = requestAnimationFrame(animate)
animateToBounds(bounds: ReadOnlyRect, options: AnimationOptions = {}) {
const setDirty = () => this.setDirty(true, true)
this.ds.animateToBounds(bounds, setDirty, options)
}
/**
@@ -7320,15 +7255,7 @@ export class LGraphCanvas implements ConnectionColorContext {
const bounds = createBounds(items)
if (!bounds) throw new TypeError("Attempted to fit to view but could not calculate bounds.")
this.animateToBounds(bounds, options)
const setDirty = () => this.setDirty(true, true)
this.ds.animateToBounds(bounds, setDirty, options)
}
}
export type AnimationOptions = {
/** Duration of the animation in milliseconds. */
duration?: number
/** Relative target zoom level. 1 means the view is fit exactly on the bounding box. */
zoom?: number
/** The animation easing function (curve) */
easing?: EaseFunction
}