@genome-spy/core 0.47.0 → 0.48.0

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 +1 @@
1
- {"version":3,"file":"genomeSpy.d.ts","sourceRoot":"","sources":["../../src/genomeSpy.js"],"names":[],"mappings":"AA8CA;IACI;;;;;OAKG;IAEH;;;;;OAKG;IACH,uBAJW,WAAW,qDAEX,OAAO,qBAAqB,EAAE,YAAY,EA2FpD;IAxFG,uBAA0B;IAC1B,oDAAsB;IAItB,6BAA6B;IAC7B,uBADW,CAAC,MAAM,IAAI,CAAC,EAAE,CACM;IAE/B,sCAAsC;IACtC,wCAAgB;IAEhB,iCAA4C;IAC5C,yBAAoC;IAEpC,4CAA4C;IAC5C,oBADW,QAAU,MAAM,KAAE,MAAM,EAAE,CAAC,EAAE,CACZ;IAE5B,mBAAoD;IAEpD,0BAA0B;IAC1B,aADW,WAAW,CACM;IAE5B;;;;;OAKG;IACH,qEAF0B,OAAO,CAE8B;IAE/D,2CAA2C;IAC3C,mBADW,4BAA4B,CACL;IAClC,2CAA2C;IAC3C,iBADW,4BAA4B,CACP;IAEhC,oDAAoD;IACpD,6BAAgC;IAEhC;;;OAGG;IACH,eAFU;QAAE,IAAI,EAAE,OAAO,iBAAiB,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,oBAAoB,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAEpF;IAE9B,uBAA+C;IAE/C;;;OAGG;IACH,oBAFU,IAAI,MAAM,EAAE,QAAU,aAAa,KAAE,IAAI,CAAC,EAAE,CAAC,CAEpB;IAEnC;;;;;;OAMG;IACH,yCAFkC,GAAG,KAAK,IAAI,GAEd;IAEhC;;;OAGG;IACH,kDAFkC,GAAG,KAAK,IAAI,GAEL;IAEzC,oFAAoF;IACpF,iBADW,OAAO,MAAM,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC,CAK9E;IAED,mBAAmB;IACnB,2CAAyB;IAEzB;;;;OAIG;IACH;gBAF8B,OAAO,wBAAwB,EAAE,iBAAiB;iBAAW,MAAM;OAEnE;IAE9B;;OAEG;IACH,wBAFU,WAAW,CAEkB;IA2C3C;;;OAGG;IACH,2CAFkB,MAAM,KAAK,GAAG,EAAE,QAIjC;IAED;;OAEG;IACH,+BAFW,MAAM,YAShB;IAED;;;;OAIG;IACH,sBAHW,MAAM,QACN,GAAG,EAAE,QAaf;IAED;;;;;OAKG;IACH,gBAHW,kBAAkB,YAClB,GAAG,QAQb;IAED;;;OAGG;IACH,iCAqDC;IA0DG,uBAQC;IAGD,mCAGE;IAOF,sCAEE;IAGF,iBAAyC;IAW7C;;OAEG;IACH,gBAuBC;IAED,sCA8MC;IAED;;;OAGG;IACH,UAFa,QAAQ,OAAO,CAAC,CA6B5B;IAED,4BA+IC;IAxHe,iCAAoC;IA0HpD;;;OAGG;IACH,kBAHW,MAAM,KACN,MAAM,QAsEhB;IAED;;;;;;;OAOG;IACH,oDAHuB,QAAQ,MAAM,GAAG,WAAW,GAAG,OAAO,KAAK,EAAE,cAAc,CAAC,QAYlF;IAED,sBA6CC;IAED,kBAIC;IAED,iCAOC;IAED,iCASC;IAED,qFAWC;;CACJ;;;;iCAl7BY,eAAe,GAAG,YAAY,GAAG,QAAQ,GAAG,gBAAgB;4BAjC7C,uBAAuB;4BA2BP,uBAAuB;qBAZ9C,qBAAqB;wBAIlB,yBAAyB;yCARR,yDAAyD;oBAYvD,oBAAoB;wBAdvC,qBAAqB;oBAXzB,uBAAuB;qBAStB,oBAAoB"}
1
+ {"version":3,"file":"genomeSpy.d.ts","sourceRoot":"","sources":["../../src/genomeSpy.js"],"names":[],"mappings":"AA+CA;IACI;;;;;OAKG;IAEH;;;;;OAKG;IACH,uBAJW,WAAW,qDAEX,OAAO,qBAAqB,EAAE,YAAY,EA2FpD;IAxFG,uBAA0B;IAC1B,oDAAsB;IAItB,6BAA6B;IAC7B,uBADW,CAAC,MAAM,IAAI,CAAC,EAAE,CACM;IAE/B,sCAAsC;IACtC,wCAAgB;IAEhB,iCAA4C;IAC5C,yBAAoC;IAEpC,4CAA4C;IAC5C,oBADW,QAAU,MAAM,KAAE,MAAM,EAAE,CAAC,EAAE,CACZ;IAE5B,mBAAoD;IAEpD,0BAA0B;IAC1B,aADW,WAAW,CACM;IAE5B;;;;;OAKG;IACH,qEAF0B,OAAO,CAE8B;IAE/D,2CAA2C;IAC3C,mBADW,4BAA4B,CACL;IAClC,2CAA2C;IAC3C,iBADW,4BAA4B,CACP;IAEhC,oDAAoD;IACpD,6BAAgC;IAEhC;;;OAGG;IACH,eAFU;QAAE,IAAI,EAAE,OAAO,iBAAiB,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,oBAAoB,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAEpF;IAE9B,uBAA+C;IAE/C;;;OAGG;IACH,oBAFU,IAAI,MAAM,EAAE,QAAU,aAAa,KAAE,IAAI,CAAC,EAAE,CAAC,CAEpB;IAEnC;;;;;;OAMG;IACH,yCAFkC,GAAG,KAAK,IAAI,GAEd;IAEhC;;;OAGG;IACH,kDAFkC,GAAG,KAAK,IAAI,GAEL;IAEzC,oFAAoF;IACpF,iBADW,OAAO,MAAM,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC,CAK9E;IAED,mBAAmB;IACnB,2CAAyB;IAEzB;;;;OAIG;IACH;gBAF8B,OAAO,wBAAwB,EAAE,iBAAiB;iBAAW,MAAM;OAEnE;IAE9B;;OAEG;IACH,wBAFU,WAAW,CAEkB;IA2C3C;;;OAGG;IACH,2CAFkB,MAAM,KAAK,GAAG,EAAE,QAIjC;IAED;;OAEG;IACH,+BAFW,MAAM,YAShB;IAED;;;;OAIG;IACH,sBAHW,MAAM,QACN,GAAG,EAAE,QAaf;IAED;;;;;OAKG;IACH,gBAHW,kBAAkB,YAClB,GAAG,QAQb;IAED;;;OAGG;IACH,iCAqDC;IA0DG,uBAQC;IAGD,mCAGE;IAOF,sCAEE;IAGF,iBAAyC;IAW7C;;OAEG;IACH,gBAuBC;IAED,sCA8MC;IAED;;;OAGG;IACH,UAFa,QAAQ,OAAO,CAAC,CA6B5B;IAED,4BAqJC;IA9He,iCAAoC;IAgIpD;;;OAGG;IACH,kBAHW,MAAM,KACN,MAAM,QAsEhB;IAED;;;;;;;OAOG;IACH,oDAHuB,QAAQ,MAAM,GAAG,WAAW,GAAG,OAAO,KAAK,EAAE,cAAc,CAAC,QAYlF;IAED,sBA6CC;IAED,kBAIC;IAED,iCAOC;IAED,iCASC;IAED,qFAWC;;CACJ;;;;iCAx7BY,eAAe,GAAG,YAAY,GAAG,QAAQ,GAAG,gBAAgB;4BAlC7C,uBAAuB;4BA2BP,uBAAuB;qBAZ9C,qBAAqB;wBAIlB,yBAAyB;yCARR,yDAAyD;oBAYvD,oBAAoB;wBAdvC,qBAAqB;oBAXzB,uBAAuB;qBAStB,oBAAoB"}
@@ -36,6 +36,7 @@ import { invalidatePrefix } from "./utils/propertyCacher.js";
36
36
  import { VIEW_ROOT_NAME, ViewFactory } from "./view/viewFactory.js";
37
37
  import { reconfigureScales } from "./view/scaleResolution.js";
38
38
  import createBindingInputs from "./utils/inputBinding.js";
39
+ import { isStillZooming } from "./view/zoom.js";
39
40
 
40
41
  /**
41
42
  * Events that are broadcasted to all views.
@@ -687,8 +688,10 @@ export default class GenomeSpy {
687
688
  this.tooltip.handleMouseMove(event);
688
689
  this._tooltipUpdateRequested = false;
689
690
 
690
- if (event.buttons == 0) {
691
- // Disable during dragging
691
+ // Disable picking during dragging. Also postpone picking until
692
+ // the user has stopped zooming as reading pixels from the
693
+ // picking buffer is slow and ruins smooth animations.
694
+ if (event.buttons == 0 && !isStillZooming()) {
692
695
  this.renderPickingFramebuffer();
693
696
  this._handlePicking(point.x, point.y);
694
697
  }
@@ -711,7 +714,11 @@ export default class GenomeSpy {
711
714
  this._wheelInertia.cancel();
712
715
  }
713
716
 
714
- if (event.type == "mousedown" || event.type == "mouseup") {
717
+ if (
718
+ (event.type == "mousedown" || event.type == "mouseup") &&
719
+ !isStillZooming()
720
+ ) {
721
+ // Actually, only needed when clicking on a mark
715
722
  this.renderPickingFramebuffer();
716
723
  } else if (event.type == "wheel") {
717
724
  lastWheelEvent = now;
@@ -10,9 +10,11 @@
10
10
  * @param {number} halfLife Time until half of the value is reached, in milliseconds
11
11
  * @param {number} stopAt Stop animation when the value is within this distance from the target
12
12
  * @param {number} [initialValue] Initial value
13
- * @returns {(target: number) => void} Function that activates the transition with a new target value
13
+ * @returns {((target: number) => void) & { stop: () => void}} Function that activates the transition with a new target value
14
14
  */
15
- export function makeLerpSmoother(animator: import("../utils/animator.js").default, callback: (value: number) => void, halfLife: number, stopAt: number, initialValue?: number): (target: number) => void;
15
+ export function makeLerpSmoother(animator: import("../utils/animator.js").default, callback: (value: number) => void, halfLife: number, stopAt: number, initialValue?: number): ((target: number) => void) & {
16
+ stop: () => void;
17
+ };
16
18
  export default class Animator {
17
19
  /**
18
20
  *
@@ -1 +1 @@
1
- {"version":3,"file":"animator.d.ts","sourceRoot":"","sources":["../../../src/utils/animator.js"],"names":[],"mappings":"AAoFA;;;;;;;;;;;;;GAaG;AACH,2CAPW,OAAO,sBAAsB,EAAE,OAAO,oBAC9B,MAAM,KAAK,IAAI,YACvB,MAAM,UACN,MAAM,iBACN,MAAM,YACK,MAAM,KAAK,IAAI,CAoDpC;AAlJD;IACI;;;OAGG;IACH,mCAFoB,MAAM,KAAE,IAAI,EAS/B;IANG,wBAHgB,MAAM,KAAE,IAAI,CAGS;IACrC,0BAA6B;IAC7B,eAAkB;IAElB,wCAAwC;IACxC,aADW,QAAU,MAAM,KAAE,IAAI,CAAC,EAAE,CACf;IAGzB;;;;;;;;;OASG;IACH,mCAFoB,MAAM,KAAE,IAAI,QAM/B;IAED;;OAEG;IACH,kCAFoB,MAAM,KAAE,IAAI,QAO/B;IAED;;;;OAIG;IACH,sBAoBC;IAED;;;;;OAKG;IACH,oBAFW,OAAO,iBAAiB,EAAE,iBAAiB,gBAQrD;CACJ"}
1
+ {"version":3,"file":"animator.d.ts","sourceRoot":"","sources":["../../../src/utils/animator.js"],"names":[],"mappings":"AAoFA;;;;;;;;;;;;;GAaG;AACH,2CAPW,OAAO,sBAAsB,EAAE,OAAO,oBAC9B,MAAM,KAAK,IAAI,YACvB,MAAM,UACN,MAAM,iBACN,MAAM,aACM,MAAM,KAAK,IAAI;UAAY,MAAM,IAAI;EA0D3D;AAxJD;IACI;;;OAGG;IACH,mCAFoB,MAAM,KAAE,IAAI,EAS/B;IANG,wBAHgB,MAAM,KAAE,IAAI,CAGS;IACrC,0BAA6B;IAC7B,eAAkB;IAElB,wCAAwC;IACxC,aADW,QAAU,MAAM,KAAE,IAAI,CAAC,EAAE,CACf;IAGzB;;;;;;;;;OASG;IACH,mCAFoB,MAAM,KAAE,IAAI,QAM/B;IAED;;OAEG;IACH,kCAFoB,MAAM,KAAE,IAAI,QAO/B;IAED;;;;OAIG;IACH,sBAoBC;IAED;;;;;OAKG;IACH,oBAFW,OAAO,iBAAiB,EAAE,iBAAiB,gBAQrD;CACJ"}
@@ -94,7 +94,7 @@ export default class Animator {
94
94
  * @param {number} halfLife Time until half of the value is reached, in milliseconds
95
95
  * @param {number} stopAt Stop animation when the value is within this distance from the target
96
96
  * @param {number} [initialValue] Initial value
97
- * @returns {(target: number) => void} Function that activates the transition with a new target value
97
+ * @returns {((target: number) => void) & { stop: () => void}} Function that activates the transition with a new target value
98
98
  */
99
99
  export function makeLerpSmoother(
100
100
  animator,
@@ -113,15 +113,13 @@ export function makeLerpSmoother(
113
113
  * @param {number} [timestamp]
114
114
  */
115
115
  function smoothUpdate(timestamp) {
116
- timestamp ??= +document.timeline.currentTime;
116
+ if (settled) {
117
+ return;
118
+ }
117
119
 
118
- // If settled, the animation loop may have been stopped, so we need to
119
- // wait until the next frame to get a proper time delta.
120
- const tD = settled ? 0 : timestamp - lastTimeStamp;
120
+ const tD = timestamp - lastTimeStamp;
121
121
  lastTimeStamp = timestamp;
122
122
 
123
- settled = false;
124
-
125
123
  // Lerp smoothing: https://twitter.com/FreyaHolmer/status/1757836988495847568
126
124
  current = target + (current - target) * Math.pow(2, -tD / halfLife);
127
125
 
@@ -140,10 +138,18 @@ export function makeLerpSmoother(
140
138
  /**
141
139
  * @param {number} value
142
140
  */
143
- return function setTarget(value) {
141
+ function setTarget(value) {
144
142
  target = value;
145
143
  if (settled) {
146
- smoothUpdate();
144
+ settled = false;
145
+ lastTimeStamp = +document.timeline.currentTime;
146
+ smoothUpdate(lastTimeStamp);
147
147
  }
148
+ }
149
+
150
+ setTarget.stop = () => {
151
+ settled = true;
148
152
  };
153
+
154
+ return setTarget;
149
155
  }
@@ -19,7 +19,9 @@ export default class Inertia {
19
19
  callback: (arg0: number) => void;
20
20
  targetValue: number;
21
21
  lastValue: number;
22
- smoother: (target: number) => void;
22
+ smoother: ((target: number) => void) & {
23
+ stop: () => void;
24
+ };
23
25
  cancel(): void;
24
26
  /**
25
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"inertia.d.ts","sourceRoot":"","sources":["../../../src/utils/inertia.js"],"names":[],"mappings":"AAmEA;;;GAGG;AACH,8EAiBC;AApFD;;GAEG;AACH;IACI;;;OAGG;IACH,sBAHW,OAAO,eAAe,EAAE,OAAO,aAC/B,OAAO,EAyBjB;IAtBG,0CAAwB;IACxB,kBAA0B;IAG1B,oBAAsB;IAEtB,oCAAoC;IACpC,iBADoB,MAAM,KAAE,IAAI,CACZ;IAEpB,oBAAoB;IACpB,kBAAkB;IAElB,mCASC;IAGL,eAIC;IAED;;;;OAIG;IACH,mBAHW,MAAM,mBACG,MAAM,KAAE,IAAI,QAkB/B;CACJ"}
1
+ {"version":3,"file":"inertia.d.ts","sourceRoot":"","sources":["../../../src/utils/inertia.js"],"names":[],"mappings":"AAmEA;;;GAGG;AACH,8EAiBC;AApFD;;GAEG;AACH;IACI;;;OAGG;IACH,sBAHW,OAAO,eAAe,EAAE,OAAO,aAC/B,OAAO,EAyBjB;IAtBG,0CAAwB;IACxB,kBAA0B;IAG1B,oBAAsB;IAEtB,oCAAoC;IACpC,iBADoB,MAAM,KAAE,IAAI,CACZ;IAEpB,oBAAoB;IACpB,kBAAkB;IAElB;;MASC;IAGL,eAIC;IAED;;;;OAIG;IACH,mBAHW,MAAM,mBACG,MAAM,KAAE,IAAI,QAkB/B;CACJ"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @template T
3
+ */
4
+ export default class RingBuffer<T> {
5
+ /**
6
+ * @param {number} size
7
+ */
8
+ constructor(size: number);
9
+ /** @param {T} value */
10
+ push(value: T): void;
11
+ /**
12
+ * @returns {T[]}
13
+ */
14
+ get(): T[];
15
+ get size(): number;
16
+ get length(): number;
17
+ #private;
18
+ }
19
+ //# sourceMappingURL=ringBuffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ringBuffer.d.ts","sourceRoot":"","sources":["../../../src/utils/ringBuffer.js"],"names":[],"mappings":"AAAA;;GAEG;AACH;IAQI;;OAEG;IACH,kBAFW,MAAM,EAIhB;IAED,uBAAuB;IACvB,YADY,CAAC,QAKZ;IAED;;OAEG;IACH,OAFa,CAAC,EAAE,CAOf;IAED,mBAEC;IAED,qBAEC;;CACJ"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @template T
3
+ */
4
+ export default class RingBuffer {
5
+ /** @type {T[]} */
6
+ #buffer;
7
+
8
+ #index = 0;
9
+
10
+ #length = 0;
11
+
12
+ /**
13
+ * @param {number} size
14
+ */
15
+ constructor(size) {
16
+ this.#buffer = new Array(size);
17
+ }
18
+
19
+ /** @param {T} value */
20
+ push(value) {
21
+ this.#buffer[this.#index] = value;
22
+ this.#index = (this.#index + 1) % this.size;
23
+ this.#length = Math.min(this.#length + 1, this.size);
24
+ }
25
+
26
+ /**
27
+ * @returns {T[]}
28
+ */
29
+ get() {
30
+ const b = this.#buffer;
31
+ return this.#length < this.size
32
+ ? b.slice(0, this.#length)
33
+ : b.slice(this.#index, this.size).concat(b.slice(0, this.#index));
34
+ }
35
+
36
+ get size() {
37
+ return this.#buffer.length;
38
+ }
39
+
40
+ get length() {
41
+ return this.#length;
42
+ }
43
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import RingBuffer from "./ringBuffer.js";
3
+
4
+ describe("ringBuffer", () => {
5
+ test("Empty buffer", () => {
6
+ const buffer = new RingBuffer(10);
7
+ expect(buffer.length).toBe(0);
8
+ expect(buffer.get()).toEqual([]);
9
+ });
10
+
11
+ test("Partially filled buffer", () => {
12
+ const buffer = new RingBuffer(10);
13
+ buffer.push(1);
14
+ buffer.push(2);
15
+ buffer.push(3);
16
+ expect(buffer.length).toBe(3);
17
+ expect(buffer.get()).toEqual([1, 2, 3]);
18
+ });
19
+
20
+ test("Full buffer", () => {
21
+ const buffer = new RingBuffer(3);
22
+ buffer.push(1);
23
+ buffer.push(2);
24
+ buffer.push(3);
25
+ expect(buffer.length).toBe(3);
26
+ expect(buffer.get()).toEqual([1, 2, 3]);
27
+ });
28
+
29
+ test("Overfilled buffer", () => {
30
+ const buffer = new RingBuffer(3);
31
+ buffer.push(1);
32
+ buffer.push(2);
33
+ buffer.push(3);
34
+ buffer.push(4);
35
+ buffer.push(5);
36
+ expect(buffer.length).toBe(3);
37
+ expect(buffer.get()).toEqual([3, 4, 5]);
38
+ });
39
+ });
@@ -122,7 +122,9 @@ declare class Scrollbar extends UnitView {
122
122
  scrollbarSize: number;
123
123
  scrollbarPadding: number;
124
124
  };
125
- interpolateViewportOffset: (target: number) => void;
125
+ interpolateViewportOffset: ((target: number) => void) & {
126
+ stop: () => void;
127
+ };
126
128
  get scrollOffset(): number;
127
129
  /**
128
130
  *
@@ -1 +1 @@
1
- {"version":3,"file":"gridView.d.ts","sourceRoot":"","sources":["../../../src/view/gridView.js"],"names":[],"mappings":"AAqwBA;;;GAGG;AACH,iDAHW,OAAO,iBAAiB,EAAE,cAAc,GACtC,OAAO,iBAAiB,EAAE,QAAQ,CAwB9C;AAED;;;GAGG;AACH,uDAHW,OAAO,iBAAiB,EAAE,cAAc,GACtC,OAAO,iBAAiB,EAAE,QAAQ,CA6C9C;AA2BD;;GAEG;AACH,8EAUC;AAED;;;;;GAKG;AACH,4CAJW,OAAO,uBAAuB,EAAE,OAAO,UACvC,OAAO,iBAAiB,EAAE,UAAU,YACpC,QAAQ,aAmBlB;AA33BD;;;;;;;;;;;;;;;GAeG;AACH;IA6BI;;;;;;;;;OASG;IACH,kBARW,OAAO,iBAAiB,EAAE,aAAa,WACvC,OAAO,yBAAyB,EAAE,OAAO,gBACzC,aAAa,iDAEb,MAAM,WACN,MAAM,YACN,OAAO,WAAW,EAAE,WAAW,EAoBzC;IARG,8CAAgB;IAOhB,uBAA0B;IAG9B;;OAEG;IACH,qDAIC;IAeD;;OAEG;IACH,wDAKC;IAqBD;;OAEG;IACH,8CAEC;IAED,yBAEC;IAED;;OAEG;IACH,wCAmCC;IA2OD;;;;OAIG;IAEH,gBALW,OAAO,4CAA4C,EAAE,OAAO,UAC5D,OAAO,uBAAuB,EAAE,OAAO,YACvC,OAAO,uBAAuB,EAAE,gBAAgB,QA6O1D;;CAmGJ;AAgJD;IACI;;;;OAIG;IACH,6DAHW,aAAa,UACb,MAAM,EAoFhB;IAjFG,4BAAgC;IAChC,kCAAgB;IAChB,eAAoB;IAEpB,uBAAuB;IACvB,YADW,QAAQ,CACQ;IAE3B,uBAAuB;IACvB,kBADW,QAAQ,CACc;IAEjC,mFAAmF;IACnF,MADW,QAAQ,OAAO,OAAO,iBAAiB,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAC5D;IAEd,4FAA4F;IAC5F,WADW,QAAQ,OAAO,OAAO,iBAAiB,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAC3D;IAEnB,0DAA0D;IAC1D,kEAAoB;IAEpB,uBAAuB;IACvB,OADW,QAAQ,CACG;IAEtB,wBAAwB;IACxB,QADW,SAAS,CACQ;IA4DhC,uEAcC;IAED;;OAEG;IACH,4BAkKC;IAED,uBAqBC;IAED,iCAEC;CACJ;qBAjrC0D,eAAe;sBAFpD,uBAAuB;0BAGnB,oBAAoB;qBAGzB,eAAe;yBALX,mBAAmB;AAorC5C;IAeI;;;OAGG;IACH,uBAHW,SAAS,8CA4FnB;IA/FD,uBAAmB;IAsCf;;;MAAoB;IAIpB,oDAQC;IA+CL,2BAKC;IAWD;;;;OAIG;IACH,gCAHW,SAAS,UACT,SAAS,QA8CnB;;CACJ;oBAt2CmB,qBAAqB"}
1
+ {"version":3,"file":"gridView.d.ts","sourceRoot":"","sources":["../../../src/view/gridView.js"],"names":[],"mappings":"AAswBA;;;GAGG;AACH,iDAHW,OAAO,iBAAiB,EAAE,cAAc,GACtC,OAAO,iBAAiB,EAAE,QAAQ,CAwB9C;AAED;;;GAGG;AACH,uDAHW,OAAO,iBAAiB,EAAE,cAAc,GACtC,OAAO,iBAAiB,EAAE,QAAQ,CA6C9C;AA2BD;;GAEG;AACH,8EAUC;AAED;;;;;GAKG;AACH,4CAJW,OAAO,uBAAuB,EAAE,OAAO,UACvC,OAAO,iBAAiB,EAAE,UAAU,YACpC,QAAQ,aAmBlB;AA53BD;;;;;;;;;;;;;;;GAeG;AACH;IA6BI;;;;;;;;;OASG;IACH,kBARW,OAAO,iBAAiB,EAAE,aAAa,WACvC,OAAO,yBAAyB,EAAE,OAAO,gBACzC,aAAa,iDAEb,MAAM,WACN,MAAM,YACN,OAAO,WAAW,EAAE,WAAW,EAoBzC;IARG,8CAAgB;IAOhB,uBAA0B;IAG9B;;OAEG;IACH,qDAIC;IAeD;;OAEG;IACH,wDAKC;IAqBD;;OAEG;IACH,8CAEC;IAED,yBAEC;IAED;;OAEG;IACH,wCAmCC;IA2OD;;;;OAIG;IAEH,gBALW,OAAO,4CAA4C,EAAE,OAAO,UAC5D,OAAO,uBAAuB,EAAE,OAAO,YACvC,OAAO,uBAAuB,EAAE,gBAAgB,QA6O1D;;CAoGJ;AAgJD;IACI;;;;OAIG;IACH,6DAHW,aAAa,UACb,MAAM,EAoFhB;IAjFG,4BAAgC;IAChC,kCAAgB;IAChB,eAAoB;IAEpB,uBAAuB;IACvB,YADW,QAAQ,CACQ;IAE3B,uBAAuB;IACvB,kBADW,QAAQ,CACc;IAEjC,mFAAmF;IACnF,MADW,QAAQ,OAAO,OAAO,iBAAiB,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAC5D;IAEd,4FAA4F;IAC5F,WADW,QAAQ,OAAO,OAAO,iBAAiB,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAC3D;IAEnB,0DAA0D;IAC1D,kEAAoB;IAEpB,uBAAuB;IACvB,OADW,QAAQ,CACG;IAEtB,wBAAwB;IACxB,QADW,SAAS,CACQ;IA4DhC,uEAcC;IAED;;OAEG;IACH,4BAkKC;IAED,uBAqBC;IAED,iCAEC;CACJ;qBAlrC0D,eAAe;sBAFpD,uBAAuB;0BAGnB,oBAAoB;qBAGzB,eAAe;yBALX,mBAAmB;AAqrC5C;IAeI;;;OAGG;IACH,uBAHW,SAAS,8CA4FnB;IA/FD,uBAAmB;IAsCf;;;MAAoB;IAIpB;;MAQC;IA+CL,2BAKC;IAWD;;;;OAIG;IACH,gCAHW,SAAS,UACT,SAAS,QA8CnB;;CACJ;oBAv2CmB,qBAAqB"}
@@ -16,7 +16,7 @@ import ContainerView from "./containerView.js";
16
16
  import LayerView from "./layerView.js";
17
17
  import createTitle from "./title.js";
18
18
  import UnitView from "./unitView.js";
19
- import interactionToZoom from "./zoom.js";
19
+ import { interactionToZoom } from "./zoom.js";
20
20
  import clamp from "../utils/clamp.js";
21
21
  import { makeLerpSmoother } from "../utils/animator.js";
22
22
 
@@ -712,7 +712,8 @@ export default class GridView extends ContainerView {
712
712
  pointedChild.view,
713
713
  zoomEvent
714
714
  ),
715
- this.context.getCurrentHover()
715
+ this.context.getCurrentHover(),
716
+ this.context.animator
716
717
  );
717
718
  }
718
719
  }
@@ -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,QA0IhD;;OAlLS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM"}
@@ -7,19 +7,54 @@
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") {
18
49
  event.uiEvent.preventDefault(); // TODO: Only if there was something zoomable
19
50
 
20
51
  const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
21
52
  const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
22
53
 
54
+ if (wheelEvent.deltaX === 0 && wheelEvent.deltaY === 0) {
55
+ return;
56
+ }
57
+
23
58
  let { x, y } = event.point;
24
59
 
25
60
  // Snapping to the hovered item:
@@ -28,6 +63,9 @@ export default function interactionToZoom(event, coords, handleZoom, hover) {
28
63
  // This allows the user to rapidly zoom closer without having to
29
64
  // continuously adjust the cursor position.
30
65
 
66
+ // Stop drag-to-pan inertia
67
+ smoother?.stop();
68
+
31
69
  if (hover) {
32
70
  const e = hover.mark.encoders;
33
71
  if (e.x && !e.x2 && !e.x.constantValue) {
@@ -59,31 +97,110 @@ export default function interactionToZoom(event, coords, handleZoom, hover) {
59
97
  event.type == "mousedown" &&
60
98
  /** @type {MouseEvent} */ (event.uiEvent).button === 0
61
99
  ) {
100
+ if (smoother) {
101
+ smoother.stop();
102
+ }
103
+
104
+ /** @type {RingBuffer<{point: Point, timestamp: number}>} */
105
+ const buffer = new RingBuffer(30);
106
+
62
107
  const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
63
108
  mouseEvent.preventDefault();
64
109
 
65
- let prevMouseEvent = mouseEvent;
110
+ let prevPoint = Point.fromMouseEvent(mouseEvent);
66
111
 
67
112
  const onMousemove = /** @param {MouseEvent} moveEvent */ (
68
113
  moveEvent
69
114
  ) => {
115
+ const point = Point.fromMouseEvent(moveEvent);
116
+ buffer.push({ point, timestamp: performance.now() });
117
+
118
+ const delta = point.subtract(prevPoint);
119
+
70
120
  handleZoom({
71
- x: prevMouseEvent.clientX,
72
- y: prevMouseEvent.clientY,
73
- xDelta: moveEvent.clientX - prevMouseEvent.clientX,
74
- yDelta: moveEvent.clientY - prevMouseEvent.clientY,
121
+ x: prevPoint.x,
122
+ y: prevPoint.y,
123
+ xDelta: delta.x,
124
+ yDelta: delta.y,
75
125
  zDelta: 0,
76
126
  });
77
127
 
78
- prevMouseEvent = moveEvent;
128
+ prevPoint = point;
79
129
  };
80
130
 
81
- const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
131
+ const animateInertia = () => {
132
+ const lastMillisToInclude = 160;
133
+
134
+ const now = performance.now();
135
+ const arr = buffer
136
+ .get()
137
+ .filter((p) => now - p.timestamp < lastMillisToInclude);
138
+
139
+ if (arr.length < 5 || !animator || isDecelerating(arr)) {
140
+ return;
141
+ }
142
+
143
+ const a = arr.at(-1);
144
+ const b = arr[0];
145
+
146
+ const v = a.point
147
+ .subtract(b.point)
148
+ .multiply(1 / (a.timestamp - b.timestamp));
149
+
150
+ let x = prevPoint.x;
151
+
152
+ smoother = makeLerpSmoother(
153
+ animator,
154
+ (a) => {
155
+ handleZoom({
156
+ x: a,
157
+ y: prevPoint.y,
158
+ xDelta: x - a,
159
+ yDelta: 0,
160
+ zDelta: 0,
161
+ });
162
+ x = a;
163
+ },
164
+ 150,
165
+ 0.5,
166
+ x
167
+ );
168
+
169
+ smoother(prevPoint.x - v.x * 250);
170
+ };
171
+
172
+ const onMouseup = () => {
82
173
  document.removeEventListener("mousemove", onMousemove);
83
174
  document.removeEventListener("mouseup", onMouseup);
175
+ animateInertia();
84
176
  };
85
177
 
86
178
  document.addEventListener("mouseup", onMouseup, false);
87
179
  document.addEventListener("mousemove", onMousemove, false);
88
180
  }
89
181
  }
182
+
183
+ /**
184
+ * Split the array into two vectors and compare their lengths to find out if
185
+ * the mouse movement is decelerating.
186
+ * @param {{point: Point, timestamp: number}[]} arr
187
+ */
188
+ function isDecelerating(arr) {
189
+ const mid = arr[Math.floor(arr.length / 2)];
190
+
191
+ const ap = mid.point
192
+ .subtract(arr[0].point)
193
+ .multiply(mid.timestamp - arr[0].timestamp);
194
+ const bp = arr
195
+ .at(-1)
196
+ .point.subtract(mid.point)
197
+ .multiply(arr.at(-1).timestamp - mid.timestamp);
198
+
199
+ const a = ap.length;
200
+ const b = bp.length;
201
+
202
+ // Found by trial and error
203
+ const maxRatio = 0.4;
204
+
205
+ return b / a < maxRatio;
206
+ }
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.0",
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": "64cd3335607b2608db3c85c74a4fb571302e77d0"
68
68
  }