import {Workflow} from ".."; export class EdgePanner { /** ID of the requested animation frame for panning */ private panAnimationFrame: any; private workflow: Workflow; private movementSpeed = 10; private scrollMargin = 100; /** * Current state of collision on both axes, each negative if beyond top/left border, * positive if beyond right/bottom, zero if inside the viewport */ private collision = {x: 0, y: 0}; private viewportClientRect: ClientRect; private panningCallback = (sdx: number, sdy: number) => {}; constructor(workflow: Workflow, config = { scrollMargin: 100, movementSpeed: 10 }) { const options = Object.assign({ scrollMargin: 100, movementSpeed: 10 }, config); this.workflow = workflow; this.scrollMargin = options.scrollMargin; this.movementSpeed = options.movementSpeed; this.viewportClientRect = this.workflow.svgRoot.getBoundingClientRect(); } /** * Calculates if dragged node is at or beyond the point beyond which workflow panning should be triggered. * If collision state has changed, {@link onBoundaryCollisionChange} will be triggered. */ triggerCollisionDetection(x: number, y: number, callback: (sdx: number, sdy: number) => void) { const collision = {x: 0, y: 0}; this.panningCallback = callback; let {left, right, top, bottom} = this.viewportClientRect; left = left + this.scrollMargin; right = right - this.scrollMargin; top = top + this.scrollMargin; bottom = bottom - this.scrollMargin; if (x < left) { collision.x = x - left; } else if (x > right) { collision.x = x - right; } if (y < top) { collision.y = y - top; } else if (y > bottom) { collision.y = y - bottom; } if ( Math.sign(collision.x) !== Math.sign(this.collision.x) || Math.sign(collision.y) !== Math.sign(this.collision.y) ) { const previous = this.collision; this.collision = collision; this.onBoundaryCollisionChange(collision, previous); } } /** * Triggered when {@link triggerCollisionDetection} determines that collision properties have changed. */ private onBoundaryCollisionChange(current: { x: number, y: number }, previous: { x: number, y: number }): void { this.stop(); if (current.x === 0 && current.y === 0) { return; } this.start(this.collision); } private start(direction: { x: number, y: number }) { let startTimestamp: number | undefined; const scale = this.workflow.scale; const matrix = this.workflow.workflow.transform.baseVal.getItem(0).matrix; const sixtyFPS = 16.6666; const onFrame = (timestamp: number) => { const frameDeltaTime = timestamp - (startTimestamp || timestamp); startTimestamp = timestamp; // We need to stop the animation at some point // It should be stopped when there is no animation frame ID anymore, // which means that stopScroll() was called // However, don't do that if we haven't made the first move yet, which is a situation when ∆t is 0 if (frameDeltaTime !== 0 && !this.panAnimationFrame) { startTimestamp = undefined; return; } const moveX = Math.sign(direction.x) * this.movementSpeed * frameDeltaTime / sixtyFPS; const moveY = Math.sign(direction.y) * this.movementSpeed * frameDeltaTime / sixtyFPS; matrix.e -= moveX; matrix.f -= moveY; const frameDiffX = moveX / scale; const frameDiffY = moveY / scale; this.panningCallback(frameDiffX, frameDiffY); this.panAnimationFrame = window.requestAnimationFrame(onFrame); }; this.panAnimationFrame = window.requestAnimationFrame(onFrame); } stop() { window.cancelAnimationFrame(this.panAnimationFrame); this.panAnimationFrame = undefined; } }