Web Dev Solutions

Catalin Mititiuc

aboutsummaryrefslogtreecommitdiff
blob: 8be3cb95fd6c54388399937d852f61524e08c58b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { default as getComputedTransformMatrix, track, getTracked } from './utils';

const minDistanceThreshold = 5;

function mainButtonPressed(e) {
  return e.button === 0;
}

function distanceBetween({ x: x1, y: y1 }, { x: x2, y: y2 }) {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

function exceedsMinDistanceThreshhold(startPt, endPt) {
  return distanceBetween(startPt, endPt) >= minDistanceThreshold;
}

function stopEventPropagationToChildren(el, type) {
  el.addEventListener(type, e => e.stopPropagation(), { capture: true, once: true });
}

function getTranslateMatrix(startPt, movePt) {
  const translateMatrix = new DOMMatrix();

  return translateMatrix.translate(movePt.x - startPt.x, movePt.y - startPt.y);
}

function getTransformMatrices(el) {
  return {
    computed: getComputedTransformMatrix(el),
    inverseScreen: el.getScreenCTM().inverse()
  };
}

function clientToSvgPt(e, inverseScreenMtx, pt = new DOMPoint()) {
  pt.x = e.clientX;
  pt.y = e.clientY;
  return pt.matrixTransform(inverseScreenMtx);
}

function setTransform(el, computedMtx, startPt, endPt) {
  const translateMtx = getTranslateMatrix(startPt, endPt);
  const transformMtx = computedMtx.multiply(translateMtx);

  groups.forEach(([z, p]) => track(z, p, transformMtx));

  el.style.transform = transformMtx;
}

const groups = getTracked(document.querySelectorAll('svg g[class] use'));

export function programmaticPan(el, from, to) {
  const matrices = getTransformMatrices(el);
  const startPt = clientToSvgPt(from, matrices.inverseScreen);
  const endPt = clientToSvgPt(to, matrices.inverseScreen);

  el.style.transition = 'transform 0.5s';
  setTransform(el, matrices.computed, startPt, endPt);
  el.addEventListener('transitionend', () => el.style.transition = '', { once: true });
}

export default function (el) {
  let matrices, startPt, movePt, isPanning;

  function pointerMove(e) {
    movePt.x = e.clientX;
    movePt.y = e.clientY;

    if (!isPanning && exceedsMinDistanceThreshhold(startPt, movePt)) {
      isPanning = true;
      startPt = clientToSvgPt(e, matrices.inverseScreen, startPt);
      stopEventPropagationToChildren(el, 'click');
    }

    if (isPanning) {
      movePt = clientToSvgPt(e, matrices.inverseScreen, movePt);
      setTransform(el, matrices.computed, startPt, movePt);
    }
  }

  return function(e) {
    if (!mainButtonPressed(e)) return;
    e.preventDefault();
    e.target.setPointerCapture(e.pointerId);

    isPanning = false;
    matrices = getTransformMatrices(el);
    startPt = new DOMPoint(e.clientX, e.clientY);
    movePt = new DOMPoint();

    this.addEventListener('pointermove', pointerMove);

    this.addEventListener(
      'pointerup',
      () => this.removeEventListener('pointermove', pointerMove),
      { once: true }
    );
  }
}