ts-maps 0.3.6 → 0.3.7

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.
@@ -35,6 +35,9 @@ export declare interface MapOptions {
35
35
  theme?: 'light' | 'dark' | 'auto'
36
36
  style?: any
37
37
  zoomDelta?: number
38
+ wheelPxPerZoomLevel?: number
39
+ wheelSmoothing?: number
40
+ wheelDebounceTime?: number
38
41
  trackResize?: boolean
39
42
  [key: string]: any
40
43
  }
@@ -1,12 +1,52 @@
1
1
  import { Handler } from '../../core/Handler';
2
+ import type { LatLng } from '../../geo/LatLng';
2
3
  import type { Point } from '../../geometry/Point';
4
+ /**
5
+ * Continuous scroll-wheel zoom.
6
+ *
7
+ * The previous implementation was Leaflet's: accumulate wheel delta, wait
8
+ * 40ms for the gesture to stop, then animate one discrete `setZoom` step. That
9
+ * is three separate problems stacked on each other — the map does not move
10
+ * while you are actually scrolling, it then moves in a jump you did not ask
11
+ * for, and a trackpad's continuous stream of small deltas is chopped into a
12
+ * sequence of those jumps. Next to Google Maps or Apple Maps it reads as
13
+ * broken rather than as slow.
14
+ *
15
+ * What every other slippy map does instead, and what this does:
16
+ *
17
+ * - Wheel events push a TARGET zoom. They never animate anything themselves,
18
+ * so a fast scroll and a slow one differ in how far they go, not in how
19
+ * many animations they queue.
20
+ * - A single rAF loop eases the live zoom toward that target and stops when
21
+ * it arrives. Scrolling again just moves the target; the loop is already
22
+ * running and simply keeps going. There is nothing to cancel and nothing
23
+ * to queue.
24
+ * - The point under the cursor stays under the cursor, recomputed every
25
+ * frame from the anchor the gesture started at.
26
+ *
27
+ * ## Trackpads
28
+ *
29
+ * A pinch on a macOS trackpad arrives as a `wheel` event with `ctrlKey` set —
30
+ * the browser's way of reporting a zoom gesture, and nothing to do with the
31
+ * Control key. Those deltas are much finer than a mouse notch, so applying the
32
+ * mouse's px-per-level to them makes a pinch cover half the zoom range. They
33
+ * get their own scale, and no easing: a pinch is a direct manipulation, and
34
+ * anything that lags behind the fingers feels broken.
35
+ */
3
36
  export declare class ScrollWheelZoomHandler extends Handler {
4
- _delta: number;
5
- _startTime: number | null;
6
- _timer?: ReturnType<typeof setTimeout>;
7
- _lastMousePos?: Point;
37
+ _targetZoom: number | null;
38
+ _anchor?: Point;
39
+ _anchorLatLng?: LatLng;
40
+ _frame?: number;
41
+ _lastFrameTime: number;
42
+ _active: boolean;
8
43
  addHooks(): void;
9
44
  removeHooks(): void;
10
- _onWheelScroll(e: any): void;
11
- _performZoom(): void;
45
+ _onWheelScroll(e: WheelEvent): void;
46
+ _applyZoom(zoom: number): void;
47
+ _startFrames(): void;
48
+ _stopFrames(): void;
49
+ _tick(timestamp: number): void;
50
+ _scheduleSettle(): void;
51
+ _settle(): void;
12
52
  }
package/dist/index.js CHANGED
@@ -23590,50 +23590,113 @@ init_DomEvent();
23590
23590
  init_Map();
23591
23591
  TsMap.mergeOptions({
23592
23592
  scrollWheelZoom: true,
23593
- wheelDebounceTime: 40,
23594
- wheelPxPerZoomLevel: 60
23593
+ wheelPxPerZoomLevel: 60,
23594
+ wheelSmoothing: 0.13,
23595
+ wheelDebounceTime: 40
23595
23596
  });
23596
23597
 
23597
23598
  class ScrollWheelZoomHandler extends Handler {
23598
- _delta = 0;
23599
- _startTime = null;
23600
23599
  addHooks() {
23601
23600
  on(this._map._container, "wheel", this._onWheelScroll, this);
23602
- this._delta = 0;
23601
+ this._targetZoom = null;
23602
+ this._active = false;
23603
+ this._lastFrameTime = 0;
23603
23604
  }
23604
23605
  removeHooks() {
23605
23606
  off(this._map._container, "wheel", this._onWheelScroll, this);
23606
- clearTimeout(this._timer);
23607
+ this._stopFrames();
23608
+ this._targetZoom = null;
23609
+ this._active = false;
23607
23610
  }
23608
23611
  _onWheelScroll(e) {
23612
+ const map = this._map;
23609
23613
  const delta = getWheelDelta(e);
23610
- const debounce = this._map.options.wheelDebounceTime;
23611
- this._delta += delta;
23612
- this._lastMousePos = this._map.pointerEventToContainerPoint(e);
23613
- if (!this._startTime)
23614
- this._startTime = Date.now();
23615
- const left = Math.max(debounce - (Date.now() - this._startTime), 0);
23616
- clearTimeout(this._timer);
23617
- this._timer = setTimeout(this._performZoom.bind(this), left);
23618
23614
  stop(e);
23615
+ if (!delta)
23616
+ return;
23617
+ const pinch = e.ctrlKey === true;
23618
+ const pxPerLevel = map.options.wheelPxPerZoomLevel;
23619
+ const step = delta / (pinch ? pxPerLevel * 2 : pxPerLevel);
23620
+ if (!this._active) {
23621
+ this._anchor = map.pointerEventToContainerPoint(e);
23622
+ this._anchorLatLng = map.containerPointToLatLng(this._anchor);
23623
+ this._targetZoom = map.getZoom();
23624
+ this._active = true;
23625
+ map._stop();
23626
+ map._moveStart(true, false);
23627
+ }
23628
+ this._targetZoom = map._limitZoom(this._targetZoom + step);
23629
+ if (pinch) {
23630
+ this._applyZoom(this._targetZoom);
23631
+ this._scheduleSettle();
23632
+ return;
23633
+ }
23634
+ if (this._frame === undefined)
23635
+ this._startFrames();
23619
23636
  }
23620
- _performZoom() {
23637
+ _applyZoom(zoom) {
23621
23638
  const map = this._map;
23622
- const zoom = map.getZoom();
23623
- const snap = map.options.zoomSnap ?? 0;
23624
- map._stop();
23625
- const d2 = this._delta / (map.options.wheelPxPerZoomLevel * 4);
23626
- const d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2;
23627
- const d4 = snap ? Math.ceil(d3 / snap) * snap : d3;
23628
- const delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom;
23629
- this._delta = 0;
23630
- this._startTime = null;
23631
- if (!delta)
23639
+ const anchor = this._anchor;
23640
+ const anchorLatLng = this._anchorLatLng;
23641
+ if (!anchor || !anchorLatLng) {
23642
+ map._move(map.getCenter(), zoom);
23632
23643
  return;
23633
- if (map.options.scrollWheelZoom === "center")
23634
- map.setZoom(zoom + delta);
23635
- else
23636
- map.setZoomAround(this._lastMousePos, zoom + delta);
23644
+ }
23645
+ const viewHalf = map.getSize().divideBy(2);
23646
+ const offset = anchor.subtract(viewHalf);
23647
+ const center = map.unproject(map.project(anchorLatLng, zoom).subtract(offset), zoom);
23648
+ map._move(center, zoom, { round: false });
23649
+ }
23650
+ _startFrames() {
23651
+ this._lastFrameTime = 0;
23652
+ this._frame = requestAnimationFrame((ts) => this._tick(ts));
23653
+ }
23654
+ _stopFrames() {
23655
+ if (this._frame !== undefined) {
23656
+ cancelAnimationFrame(this._frame);
23657
+ this._frame = undefined;
23658
+ }
23659
+ }
23660
+ _tick(timestamp) {
23661
+ const map = this._map;
23662
+ const target = this._targetZoom;
23663
+ if (target === null) {
23664
+ this._frame = undefined;
23665
+ return;
23666
+ }
23667
+ const dt = this._lastFrameTime ? Math.min((timestamp - this._lastFrameTime) / 1000, 0.1) : 1 / 60;
23668
+ this._lastFrameTime = timestamp;
23669
+ const smoothing = Math.max(0.01, map.options.wheelSmoothing);
23670
+ const t = 1 - Math.exp(-dt / smoothing);
23671
+ const current = map.getZoom();
23672
+ const remaining = target - current;
23673
+ if (Math.abs(remaining) < 0.001) {
23674
+ this._applyZoom(target);
23675
+ this._frame = undefined;
23676
+ this._settle();
23677
+ return;
23678
+ }
23679
+ this._applyZoom(current + remaining * t);
23680
+ this._frame = requestAnimationFrame((ts) => this._tick(ts));
23681
+ }
23682
+ _scheduleSettle() {
23683
+ this._stopFrames();
23684
+ this._frame = requestAnimationFrame(() => {
23685
+ this._frame = undefined;
23686
+ setTimeout(() => {
23687
+ if (this._frame === undefined && this._active)
23688
+ this._settle();
23689
+ }, 120);
23690
+ });
23691
+ }
23692
+ _settle() {
23693
+ if (!this._active)
23694
+ return;
23695
+ this._active = false;
23696
+ this._targetZoom = null;
23697
+ this._anchor = undefined;
23698
+ this._anchorLatLng = undefined;
23699
+ this._map._moveEnd(true);
23637
23700
  }
23638
23701
  }
23639
23702
  TsMap.addInitHook("addHandler", "scrollWheelZoom", ScrollWheelZoomHandler);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ts-maps",
3
3
  "type": "module",
4
- "version": "0.3.6",
4
+ "version": "0.3.7",
5
5
  "description": "A modern vector map library.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",