ts-maps 0.3.5 → 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
@@ -11796,7 +11796,7 @@ function evaluateFilterLegacy(filter, feature) {
11796
11796
  const key3 = rest[0];
11797
11797
  return typeof key3 === "string" && !(key3 in feature.properties);
11798
11798
  }
11799
- const left = resolveOperand(rest[0], feature);
11799
+ const left = resolveLegacyKey(rest[0], feature);
11800
11800
  if (op === "==" || op === "!=") {
11801
11801
  const right = resolveOperand(rest[1], feature);
11802
11802
  return op === "==" ? left === right : left !== right;
@@ -11817,6 +11817,19 @@ function evaluateFilterLegacy(filter, feature) {
11817
11817
  }
11818
11818
  return true;
11819
11819
  }
11820
+ function resolveLegacyKey(node, feature) {
11821
+ if (Array.isArray(node))
11822
+ return resolveOperand(node, feature);
11823
+ if (node === "$type") {
11824
+ const t = feature.type;
11825
+ return t === 1 ? "Point" : t === 2 ? "LineString" : t === 3 ? "Polygon" : "Unknown";
11826
+ }
11827
+ if (node === "$id")
11828
+ return feature.id ?? null;
11829
+ if (typeof node === "string")
11830
+ return feature.properties[node] ?? null;
11831
+ return node;
11832
+ }
11820
11833
  function resolveOperand(node, feature) {
11821
11834
  if (Array.isArray(node)) {
11822
11835
  if (node[0] === "get" && typeof node[1] === "string")
@@ -23577,50 +23590,113 @@ init_DomEvent();
23577
23590
  init_Map();
23578
23591
  TsMap.mergeOptions({
23579
23592
  scrollWheelZoom: true,
23580
- wheelDebounceTime: 40,
23581
- wheelPxPerZoomLevel: 60
23593
+ wheelPxPerZoomLevel: 60,
23594
+ wheelSmoothing: 0.13,
23595
+ wheelDebounceTime: 40
23582
23596
  });
23583
23597
 
23584
23598
  class ScrollWheelZoomHandler extends Handler {
23585
- _delta = 0;
23586
- _startTime = null;
23587
23599
  addHooks() {
23588
23600
  on(this._map._container, "wheel", this._onWheelScroll, this);
23589
- this._delta = 0;
23601
+ this._targetZoom = null;
23602
+ this._active = false;
23603
+ this._lastFrameTime = 0;
23590
23604
  }
23591
23605
  removeHooks() {
23592
23606
  off(this._map._container, "wheel", this._onWheelScroll, this);
23593
- clearTimeout(this._timer);
23607
+ this._stopFrames();
23608
+ this._targetZoom = null;
23609
+ this._active = false;
23594
23610
  }
23595
23611
  _onWheelScroll(e) {
23612
+ const map = this._map;
23596
23613
  const delta = getWheelDelta(e);
23597
- const debounce = this._map.options.wheelDebounceTime;
23598
- this._delta += delta;
23599
- this._lastMousePos = this._map.pointerEventToContainerPoint(e);
23600
- if (!this._startTime)
23601
- this._startTime = Date.now();
23602
- const left = Math.max(debounce - (Date.now() - this._startTime), 0);
23603
- clearTimeout(this._timer);
23604
- this._timer = setTimeout(this._performZoom.bind(this), left);
23605
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();
23606
23636
  }
23607
- _performZoom() {
23637
+ _applyZoom(zoom) {
23608
23638
  const map = this._map;
23609
- const zoom = map.getZoom();
23610
- const snap = map.options.zoomSnap ?? 0;
23611
- map._stop();
23612
- const d2 = this._delta / (map.options.wheelPxPerZoomLevel * 4);
23613
- const d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2;
23614
- const d4 = snap ? Math.ceil(d3 / snap) * snap : d3;
23615
- const delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom;
23616
- this._delta = 0;
23617
- this._startTime = null;
23618
- if (!delta)
23639
+ const anchor = this._anchor;
23640
+ const anchorLatLng = this._anchorLatLng;
23641
+ if (!anchor || !anchorLatLng) {
23642
+ map._move(map.getCenter(), zoom);
23619
23643
  return;
23620
- if (map.options.scrollWheelZoom === "center")
23621
- map.setZoom(zoom + delta);
23622
- else
23623
- 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);
23624
23700
  }
23625
23701
  }
23626
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.5",
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",