refs #14349 Merge branch 'origin/14349-files-placeholders'
[arvados-workbench2.git] / src / lib / cwl-svg / behaviors / edge-panning.ts
1 import {Workflow} from "..";
2
3 export class EdgePanner {
4
5
6     /** ID of the requested animation frame for panning */
7     private panAnimationFrame: any;
8
9     private workflow: Workflow;
10
11     private movementSpeed = 10;
12     private scrollMargin  = 100;
13
14     /**
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
17      */
18     private collision = {x: 0, y: 0};
19
20     private viewportClientRect: ClientRect;
21     private panningCallback = (sdx: number, sdy: number) => {};
22
23     constructor(workflow: Workflow, config = {
24         scrollMargin: 100,
25         movementSpeed: 10
26     }) {
27         const options = Object.assign({
28             scrollMargin: 100,
29             movementSpeed: 10
30         }, config);
31
32         this.workflow      = workflow;
33         this.scrollMargin  = options.scrollMargin;
34         this.movementSpeed = options.movementSpeed;
35
36         this.viewportClientRect = this.workflow.svgRoot.getBoundingClientRect();
37     }
38
39     /**
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.
42      */
43     triggerCollisionDetection(x: number, y: number, callback: (sdx: number, sdy: number) => void) {
44         const collision      = {x: 0, y: 0};
45         this.panningCallback = callback;
46
47         let {left, right, top, bottom} = this.viewportClientRect;
48
49         left   = left + this.scrollMargin;
50         right  = right - this.scrollMargin;
51         top    = top + this.scrollMargin;
52         bottom = bottom - this.scrollMargin;
53
54         if (x < left) {
55             collision.x = x - left;
56         } else if (x > right) {
57             collision.x = x - right;
58         }
59
60         if (y < top) {
61             collision.y = y - top;
62         } else if (y > bottom) {
63             collision.y = y - bottom;
64         }
65
66         if (
67             Math.sign(collision.x) !== Math.sign(this.collision.x)
68             || Math.sign(collision.y) !== Math.sign(this.collision.y)
69         ) {
70             const previous = this.collision;
71             this.collision = collision;
72             this.onBoundaryCollisionChange(collision, previous);
73         }
74     }
75
76     /**
77      * Triggered when {@link triggerCollisionDetection} determines that collision properties have changed.
78      */
79     private onBoundaryCollisionChange(current: { x: number, y: number }, previous: { x: number, y: number }): void {
80
81         this.stop();
82
83         if (current.x === 0 && current.y === 0) {
84             return;
85         }
86
87         this.start(this.collision);
88     }
89
90     private start(direction: { x: number, y: number }) {
91
92         let startTimestamp: number | undefined;
93
94         const scale    = this.workflow.scale;
95         const matrix   = this.workflow.workflow.transform.baseVal.getItem(0).matrix;
96         const sixtyFPS = 16.6666;
97
98         const onFrame = (timestamp: number) => {
99
100             const frameDeltaTime = timestamp - (startTimestamp || timestamp);
101             startTimestamp       = timestamp;
102
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;
109                 return;
110             }
111
112             const moveX = Math.sign(direction.x) * this.movementSpeed * frameDeltaTime / sixtyFPS;
113             const moveY = Math.sign(direction.y) * this.movementSpeed * frameDeltaTime / sixtyFPS;
114
115             matrix.e -= moveX;
116             matrix.f -= moveY;
117
118             const frameDiffX = moveX / scale;
119             const frameDiffY = moveY / scale;
120
121             this.panningCallback(frameDiffX, frameDiffY);
122             this.panAnimationFrame = window.requestAnimationFrame(onFrame);
123         };
124
125         this.panAnimationFrame = window.requestAnimationFrame(onFrame);
126     }
127
128     stop() {
129         window.cancelAnimationFrame(this.panAnimationFrame);
130         this.panAnimationFrame = undefined;
131     }
132
133 }