Add radix cwl graph visualization
[arvados-workbench2.git] / src / lib / cwl-svg / behaviors / edge-panning.ts
diff --git a/src/lib/cwl-svg/behaviors/edge-panning.ts b/src/lib/cwl-svg/behaviors/edge-panning.ts
new file mode 100644 (file)
index 0000000..7bd983c
--- /dev/null
@@ -0,0 +1,133 @@
+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;
+    }
+
+}