1 import {Workflow} from "..";
3 export class EdgePanner {
6 /** ID of the requested animation frame for panning */
7 private panAnimationFrame: any;
9 private workflow: Workflow;
11 private movementSpeed = 10;
12 private scrollMargin = 100;
15 * Current state of collision on both axes, each negative if beyond top/left border,
16 * positive if beyond right/bottom, zero if inside the viewport
18 private collision = {x: 0, y: 0};
20 private viewportClientRect: ClientRect;
21 private panningCallback = (sdx: number, sdy: number) => {};
23 constructor(workflow: Workflow, config = {
27 const options = Object.assign({
32 this.workflow = workflow;
33 this.scrollMargin = options.scrollMargin;
34 this.movementSpeed = options.movementSpeed;
36 this.viewportClientRect = this.workflow.svgRoot.getBoundingClientRect();
40 * Calculates if dragged node is at or beyond the point beyond which workflow panning should be triggered.
41 * If collision state has changed, {@link onBoundaryCollisionChange} will be triggered.
43 triggerCollisionDetection(x: number, y: number, callback: (sdx: number, sdy: number) => void) {
44 const collision = {x: 0, y: 0};
45 this.panningCallback = callback;
47 let {left, right, top, bottom} = this.viewportClientRect;
49 left = left + this.scrollMargin;
50 right = right - this.scrollMargin;
51 top = top + this.scrollMargin;
52 bottom = bottom - this.scrollMargin;
55 collision.x = x - left;
56 } else if (x > right) {
57 collision.x = x - right;
61 collision.y = y - top;
62 } else if (y > bottom) {
63 collision.y = y - bottom;
67 Math.sign(collision.x) !== Math.sign(this.collision.x)
68 || Math.sign(collision.y) !== Math.sign(this.collision.y)
70 const previous = this.collision;
71 this.collision = collision;
72 this.onBoundaryCollisionChange(collision, previous);
77 * Triggered when {@link triggerCollisionDetection} determines that collision properties have changed.
79 private onBoundaryCollisionChange(current: { x: number, y: number }, previous: { x: number, y: number }): void {
83 if (current.x === 0 && current.y === 0) {
87 this.start(this.collision);
90 private start(direction: { x: number, y: number }) {
92 let startTimestamp: number | undefined;
94 const scale = this.workflow.scale;
95 const matrix = this.workflow.workflow.transform.baseVal.getItem(0).matrix;
96 const sixtyFPS = 16.6666;
98 const onFrame = (timestamp: number) => {
100 const frameDeltaTime = timestamp - (startTimestamp || timestamp);
101 startTimestamp = timestamp;
103 // We need to stop the animation at some point
104 // It should be stopped when there is no animation frame ID anymore,
105 // which means that stopScroll() was called
106 // However, don't do that if we haven't made the first move yet, which is a situation when ∆t is 0
107 if (frameDeltaTime !== 0 && !this.panAnimationFrame) {
108 startTimestamp = undefined;
112 const moveX = Math.sign(direction.x) * this.movementSpeed * frameDeltaTime / sixtyFPS;
113 const moveY = Math.sign(direction.y) * this.movementSpeed * frameDeltaTime / sixtyFPS;
118 const frameDiffX = moveX / scale;
119 const frameDiffY = moveY / scale;
121 this.panningCallback(frameDiffX, frameDiffY);
122 this.panAnimationFrame = window.requestAnimationFrame(onFrame);
125 this.panAnimationFrame = window.requestAnimationFrame(onFrame);
129 window.cancelAnimationFrame(this.panAnimationFrame);
130 this.panAnimationFrame = undefined;