@genome-spy/core 0.47.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,8 @@
1
1
  export default class Point {
2
+ /**
3
+ * @param {MouseEvent} event
4
+ */
5
+ static fromMouseEvent(event: MouseEvent): Point;
2
6
  /**
3
7
  *
4
8
  * @param {number} x
@@ -8,7 +12,19 @@ export default class Point {
8
12
  /** @readonly */ readonly x: number;
9
13
  /** @readonly */ readonly y: number;
10
14
  /**
11
- *
15
+ * @param {Point} point
16
+ */
17
+ subtract(point: Point): Point;
18
+ /**
19
+ * @param {Point} point
20
+ */
21
+ add(point: Point): Point;
22
+ /**
23
+ * @param {number} scalar
24
+ */
25
+ multiply(scalar: number): Point;
26
+ get length(): number;
27
+ /**
12
28
  * @param {Point} point
13
29
  */
14
30
  equals(point: Point): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"point.d.ts","sourceRoot":"","sources":["../../../../src/view/layout/point.js"],"names":[],"mappings":"AAAA;IACI;;;;OAIG;IACH,eAHW,MAAM,KACN,MAAM,EAKhB;IAFG,gBAAgB,CAAC,mBAAU;IAC3B,gBAAgB,CAAC,mBAAU;IAG/B;;;OAGG;IACH,cAFW,KAAK,WAQf;CACJ"}
1
+ {"version":3,"file":"point.d.ts","sourceRoot":"","sources":["../../../../src/view/layout/point.js"],"names":[],"mappings":"AAIA;IACI;;OAEG;IACH,6BAFW,UAAU,SAIpB;IAED;;;;OAIG;IACH,eAHW,MAAM,KACN,MAAM,EAKhB;IAFG,gBAAgB,CAAC,mBAAU;IAC3B,gBAAgB,CAAC,mBAAU;IAG/B;;OAEG;IACH,gBAFW,KAAK,SAIf;IAED;;OAEG;IACH,WAFW,KAAK,SAIf;IAED;;OAEG;IACH,iBAFW,MAAM,SAIhB;IAED,qBAEC;IAED;;OAEG;IACH,cAFW,KAAK,WAQf;CACJ"}
@@ -1,4 +1,15 @@
1
+ /*
2
+ * Hmm. This looks quite a bit like a two-dimensional vector.
3
+ * Maybe we should use a vector instead?
4
+ */
1
5
  export default class Point {
6
+ /**
7
+ * @param {MouseEvent} event
8
+ */
9
+ static fromMouseEvent(event) {
10
+ return new Point(event.clientX, event.clientY);
11
+ }
12
+
2
13
  /**
3
14
  *
4
15
  * @param {number} x
@@ -10,7 +21,31 @@ export default class Point {
10
21
  }
11
22
 
12
23
  /**
13
- *
24
+ * @param {Point} point
25
+ */
26
+ subtract(point) {
27
+ return new Point(this.x - point.x, this.y - point.y);
28
+ }
29
+
30
+ /**
31
+ * @param {Point} point
32
+ */
33
+ add(point) {
34
+ return new Point(this.x - point.x, this.y - point.y);
35
+ }
36
+
37
+ /**
38
+ * @param {number} scalar
39
+ */
40
+ multiply(scalar) {
41
+ return new Point(this.x * scalar, this.y * scalar);
42
+ }
43
+
44
+ get length() {
45
+ return Math.sqrt(this.x ** 2 + this.y ** 2);
46
+ }
47
+
48
+ /**
14
49
  * @param {Point} point
15
50
  */
16
51
  equals(point) {
@@ -1,18 +1,12 @@
1
- /**
2
- * @typedef {object} ZoomEvent
3
- * @prop {number} x
4
- * @prop {number} y
5
- * @prop {number} xDelta
6
- * @prop {number} yDelta
7
- * @prop {number} zDelta
8
- */
1
+ export function isStillZooming(): boolean;
9
2
  /**
10
3
  * @param {import("../utils/interactionEvent.js").default} event
11
4
  * @param {import("./layout/rectangle.js").default} coords
12
5
  * @param {(zoomEvent: ZoomEvent) => void} handleZoom
13
6
  * @param {import("../types/viewContext.js").Hover} [hover]
7
+ * @param {import("../utils/animator.js").default} [animator]
14
8
  */
15
- export default function interactionToZoom(event: import("../utils/interactionEvent.js").default, coords: import("./layout/rectangle.js").default, handleZoom: (zoomEvent: ZoomEvent) => void, hover?: import("../types/viewContext.js").Hover): void;
9
+ export function interactionToZoom(event: import("../utils/interactionEvent.js").default, coords: import("./layout/rectangle.js").default, handleZoom: (zoomEvent: ZoomEvent) => void, hover?: import("../types/viewContext.js").Hover, animator?: import("../utils/animator.js").default): void;
16
10
  export type ZoomEvent = {
17
11
  x: number;
18
12
  y: number;
@@ -1 +1 @@
1
- {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../../src/view/zoom.js"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,iDALW,OAAO,8BAA8B,EAAE,OAAO,UAC9C,OAAO,uBAAuB,EAAE,OAAO,0BAC3B,SAAS,KAAK,IAAI,UAC9B,OAAO,yBAAyB,EAAE,KAAK,QA2EjD;;OAtFS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM"}
1
+ {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../../src/view/zoom.js"],"names":[],"mappings":"AAkBA,0CAGC;AAgBD;;;;;;GAMG;AACH,yCANW,OAAO,8BAA8B,EAAE,OAAO,UAC9C,OAAO,uBAAuB,EAAE,OAAO,0BAC3B,SAAS,KAAK,IAAI,UAC9B,OAAO,yBAAyB,EAAE,KAAK,aACvC,OAAO,sBAAsB,EAAE,OAAO,QAkJhD;;OA1LS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM"}
@@ -7,19 +7,60 @@
7
7
  * @prop {number} zDelta
8
8
  */
9
9
 
10
+ import { makeLerpSmoother } from "../utils/animator.js";
11
+ import RingBuffer from "../utils/ringBuffer.js";
12
+ import Point from "./layout/point.js";
13
+
14
+ /** @type {ReturnType<typeof makeLerpSmoother>} */
15
+ let smoother;
16
+
17
+ let lastTimestamp = 0;
18
+
19
+ export function isStillZooming() {
20
+ const delta = performance.now() - lastTimestamp;
21
+ return delta < 50;
22
+ }
23
+
24
+ /**
25
+ *
26
+ * @param {T} fn
27
+ * @returns {T}
28
+ * @template {Function} T
29
+ */
30
+ function recordTimeStamp(fn) {
31
+ // @ts-ignore
32
+ return function (...args) {
33
+ lastTimestamp = performance.now();
34
+ fn(...args);
35
+ };
36
+ }
37
+
10
38
  /**
11
39
  * @param {import("../utils/interactionEvent.js").default} event
12
40
  * @param {import("./layout/rectangle.js").default} coords
13
41
  * @param {(zoomEvent: ZoomEvent) => void} handleZoom
14
42
  * @param {import("../types/viewContext.js").Hover} [hover]
43
+ * @param {import("../utils/animator.js").default} [animator]
15
44
  */
16
- export default function interactionToZoom(event, coords, handleZoom, hover) {
45
+ export function interactionToZoom(event, coords, handleZoom, hover, animator) {
46
+ handleZoom = recordTimeStamp(handleZoom);
47
+
17
48
  if (event.type == "wheel") {
49
+ // TODO: Wheel-zoom inertia should probably be moved here and the faked wheel
50
+ // events in genomeSpy.js and inertia.js should be retired.
51
+
18
52
  event.uiEvent.preventDefault(); // TODO: Only if there was something zoomable
19
53
 
20
54
  const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
21
55
  const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
22
56
 
57
+ if (!wheelEvent.deltaX && !wheelEvent.deltaY) {
58
+ return;
59
+ }
60
+
61
+ // Stop drag-to-pan inertia
62
+ smoother?.stop();
63
+
23
64
  let { x, y } = event.point;
24
65
 
25
66
  // Snapping to the hovered item:
@@ -59,31 +100,115 @@ export default function interactionToZoom(event, coords, handleZoom, hover) {
59
100
  event.type == "mousedown" &&
60
101
  /** @type {MouseEvent} */ (event.uiEvent).button === 0
61
102
  ) {
103
+ if (smoother) {
104
+ smoother.stop();
105
+ }
106
+
107
+ /** @type {RingBuffer<{point: Point, timestamp: number}>} */
108
+ const eventBuffer = new RingBuffer(30);
109
+
62
110
  const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
63
111
  mouseEvent.preventDefault();
64
112
 
65
- let prevMouseEvent = mouseEvent;
113
+ let prevPoint = Point.fromMouseEvent(mouseEvent);
66
114
 
67
115
  const onMousemove = /** @param {MouseEvent} moveEvent */ (
68
116
  moveEvent
69
117
  ) => {
118
+ const point = Point.fromMouseEvent(moveEvent);
119
+ eventBuffer.push({ point, timestamp: performance.now() });
120
+
121
+ const delta = point.subtract(prevPoint);
122
+
70
123
  handleZoom({
71
- x: prevMouseEvent.clientX,
72
- y: prevMouseEvent.clientY,
73
- xDelta: moveEvent.clientX - prevMouseEvent.clientX,
74
- yDelta: moveEvent.clientY - prevMouseEvent.clientY,
124
+ x: prevPoint.x,
125
+ y: prevPoint.y,
126
+ xDelta: delta.x,
127
+ yDelta: delta.y,
75
128
  zDelta: 0,
76
129
  });
77
130
 
78
- prevMouseEvent = moveEvent;
131
+ prevPoint = point;
79
132
  };
80
133
 
81
- const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
134
+ const animateInertia = () => {
135
+ const lastMillisToInclude = 160;
136
+
137
+ const now = performance.now();
138
+ const arr = eventBuffer
139
+ .get()
140
+ .filter((p) => now - p.timestamp < lastMillisToInclude);
141
+
142
+ if (arr.length < 5 || !animator || isDecelerating(arr)) {
143
+ return;
144
+ }
145
+
146
+ const a = arr.at(-1);
147
+ const b = arr[0];
148
+
149
+ const v = a.point
150
+ .subtract(b.point)
151
+ .multiply(1 / (a.timestamp - b.timestamp));
152
+
153
+ let x = prevPoint.x;
154
+ let y = prevPoint.y;
155
+
156
+ smoother = makeLerpSmoother(
157
+ animator,
158
+ (p) => {
159
+ handleZoom({
160
+ x: p.x,
161
+ y: p.y,
162
+ xDelta: x - p.x,
163
+ yDelta: y - p.y,
164
+ zDelta: 0,
165
+ });
166
+ x = p.x;
167
+ y = p.y;
168
+ },
169
+ 150,
170
+ 0.5,
171
+ { x, y }
172
+ );
173
+
174
+ smoother({
175
+ x: prevPoint.x - v.x * 250,
176
+ y: prevPoint.y - v.y * 250,
177
+ });
178
+ };
179
+
180
+ const onMouseup = () => {
82
181
  document.removeEventListener("mousemove", onMousemove);
83
182
  document.removeEventListener("mouseup", onMouseup);
183
+ animateInertia();
84
184
  };
85
185
 
86
186
  document.addEventListener("mouseup", onMouseup, false);
87
187
  document.addEventListener("mousemove", onMousemove, false);
88
188
  }
89
189
  }
190
+
191
+ /**
192
+ * Split the array into two vectors and compare their lengths to find out if
193
+ * the mouse movement is decelerating.
194
+ * @param {{point: Point, timestamp: number}[]} arr
195
+ */
196
+ function isDecelerating(arr) {
197
+ const mid = arr[Math.floor(arr.length / 2)];
198
+
199
+ const ap = mid.point
200
+ .subtract(arr[0].point)
201
+ .multiply(mid.timestamp - arr[0].timestamp);
202
+ const bp = arr
203
+ .at(-1)
204
+ .point.subtract(mid.point)
205
+ .multiply(arr.at(-1).timestamp - mid.timestamp);
206
+
207
+ const a = ap.length;
208
+ const b = bp.length;
209
+
210
+ // Found by trial and error
211
+ const maxRatio = 0.4;
212
+
213
+ return b / a < maxRatio;
214
+ }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "contributors": [],
9
9
  "license": "MIT",
10
- "version": "0.47.0",
10
+ "version": "0.48.1",
11
11
  "jsdelivr": "dist/bundle/index.js",
12
12
  "unpkg": "dist/bundle/index.js",
13
13
  "browser": "dist/bundle/index.js",
@@ -64,5 +64,5 @@
64
64
  "vega-scale": "^7.3.1",
65
65
  "vega-util": "^1.17.2"
66
66
  },
67
- "gitHead": "4c4aabc561aa5c61a200b7c4eaaf0d2cbec623b6"
67
+ "gitHead": "f2e023ce43cf18091cb81140f5be1f59887271df"
68
68
  }