@luma.gl/core 9.3.4 → 9.3.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luma.gl/core",
3
- "version": "9.3.4",
3
+ "version": "9.3.5",
4
4
  "description": "The luma.gl core Device API",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,5 +46,5 @@
46
46
  "@probe.gl/stats": "^4.1.1",
47
47
  "@types/offscreencanvas": "^2019.7.3"
48
48
  },
49
- "gitHead": "f430daba8be25561034dca6bb0af3ea487c9d1c8"
49
+ "gitHead": "7c66d63f213836a3fc83f2ad0103d1d3dadf7633"
50
50
  }
@@ -2,22 +2,33 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ /** Options and callbacks used by {@link CanvasObserver}. */
5
6
  type CanvasObserverProps = {
7
+ /** HTML canvas element whose DOM lifecycle should be observed. */
6
8
  canvas?: HTMLCanvasElement;
9
+ /** Whether to poll for canvas position changes. */
7
10
  trackPosition: boolean;
11
+ /** ResizeObserver box type passed to `observe()`. */
12
+ resizeObserverBox: ResizeObserverBoxOptions;
13
+ /** Called with ResizeObserver entries for the observed canvas. */
8
14
  onResize: (entries: ResizeObserverEntry[]) => void;
15
+ /** Called with IntersectionObserver entries for the observed canvas. */
9
16
  onIntersection: (entries: IntersectionObserverEntry[]) => void;
17
+ /** Called when the window device pixel ratio may have changed. */
10
18
  onDevicePixelRatioChange: () => void;
19
+ /** Called while canvas position tracking is enabled. */
11
20
  onPositionChange: () => void;
12
21
  };
13
22
 
14
23
  /**
15
24
  * Internal DOM observer orchestration for HTML canvas surfaces.
16
25
  *
26
+ * @remarks
17
27
  * CanvasSurface owns the tracked state and device callback dispatch. This helper only manages
18
28
  * browser observers, timers, and polling loops, then reports events through callbacks.
19
29
  */
20
30
  export class CanvasObserver {
31
+ /** Observer options and event callbacks. */
21
32
  readonly props: CanvasObserverProps;
22
33
 
23
34
  private _resizeObserver: ResizeObserver | undefined;
@@ -28,14 +39,21 @@ export class CanvasObserver {
28
39
  private _trackPositionInterval: ReturnType<typeof setInterval> | null = null;
29
40
  private _started = false;
30
41
 
42
+ /** Whether the DOM observers and polling loops have been started. */
31
43
  get started(): boolean {
32
44
  return this._started;
33
45
  }
34
46
 
47
+ /**
48
+ * Creates an observer coordinator for one HTML canvas.
49
+ *
50
+ * @param props - Observer options and event callbacks.
51
+ */
35
52
  constructor(props: CanvasObserverProps) {
36
53
  this.props = props;
37
54
  }
38
55
 
56
+ /** Starts DOM observation and optional position polling. */
39
57
  start(): void {
40
58
  if (this._started || !this.props.canvas) {
41
59
  return;
@@ -48,8 +66,9 @@ export class CanvasObserver {
48
66
  this._resizeObserver ||= new ResizeObserver(entries => this.props.onResize(entries));
49
67
 
50
68
  this._intersectionObserver.observe(this.props.canvas);
69
+ const box = this.props.resizeObserverBox;
51
70
  try {
52
- this._resizeObserver.observe(this.props.canvas, {box: 'device-pixel-content-box'});
71
+ this._resizeObserver.observe(this.props.canvas, {box});
53
72
  } catch {
54
73
  this._resizeObserver.observe(this.props.canvas, {box: 'content-box'});
55
74
  }
@@ -61,6 +80,7 @@ export class CanvasObserver {
61
80
  }
62
81
  }
63
82
 
83
+ /** Stops DOM observation, media-query listeners, and position polling. */
64
84
  stop(): void {
65
85
  if (!this._started) {
66
86
  return;
@@ -90,6 +110,7 @@ export class CanvasObserver {
90
110
  this._intersectionObserver?.disconnect();
91
111
  }
92
112
 
113
+ /** Reports the current device pixel ratio and arms the media query for its next change. */
93
114
  private _refreshDevicePixelRatio(): void {
94
115
  if (!this._started) {
95
116
  return;
@@ -111,6 +132,11 @@ export class CanvasObserver {
111
132
  );
112
133
  }
113
134
 
135
+ /**
136
+ * Starts periodic position callbacks while the observer remains active.
137
+ *
138
+ * @param intervalMs - Poll interval in milliseconds.
139
+ */
114
140
  private _trackPosition(intervalMs: number = 100): void {
115
141
  if (this._trackPositionInterval) {
116
142
  return;
@@ -34,8 +34,8 @@ export type CanvasContextProps = {
34
34
  *
35
35
  * - `'exact'` uses `ResizeObserver.devicePixelContentBoxSize` when available to match the
36
36
  * browser's exact physical pixel coverage.
37
- * - `'css-dpr'` uses `Math.round(cssSize * devicePixelRatio)` to match overlays and external
38
- * canvases that still rely on legacy CSS-size times DPR rounding.
37
+ * - `'css-dpr'` uses `Math.floor(cssSize * devicePixelRatio)` to match overlays and external
38
+ * canvases that size their drawing buffer via implicit truncation (e.g. `canvas.width = css * dpr`).
39
39
  */
40
40
  pixelSizeSource?: 'exact' | 'css-dpr';
41
41
  /** Whether to track window resizes. */
@@ -53,6 +53,11 @@ export type MutableCanvasContextProps = {
53
53
  useDevicePixels?: boolean | number;
54
54
  };
55
55
 
56
+ type DevicePixelSize = {
57
+ devicePixelWidth: number;
58
+ devicePixelHeight: number;
59
+ };
60
+
56
61
  /**
57
62
  * Shared tracked-canvas lifecycle used by both renderable and presentation contexts.
58
63
  * - Creates a new canvas or looks up a canvas from the DOM
@@ -176,6 +181,8 @@ export abstract class CanvasSurface {
176
181
  this._canvasObserver = new CanvasObserver({
177
182
  canvas: this.htmlCanvas,
178
183
  trackPosition: this.props.trackPosition,
184
+ resizeObserverBox:
185
+ this.props.pixelSizeSource === 'css-dpr' ? 'content-box' : 'device-pixel-content-box',
179
186
  onResize: entries => this._handleResize(entries),
180
187
  onIntersection: entries => this._handleIntersection(entries),
181
188
  onDevicePixelRatioChange: () => this._observeDevicePixelRatio(),
@@ -357,11 +364,7 @@ export abstract class CanvasSurface {
357
364
 
358
365
  const oldPixelSize = this.getDevicePixelSize();
359
366
 
360
- const {devicePixelWidth, devicePixelHeight} = this._getDevicePixelSizeFromResizeEntry(entry);
361
-
362
- const [maxDevicePixelWidth, maxDevicePixelHeight] = this.getMaxDrawingBufferSize();
363
- this.devicePixelWidth = Math.max(1, Math.min(devicePixelWidth, maxDevicePixelWidth));
364
- this.devicePixelHeight = Math.max(1, Math.min(devicePixelHeight, maxDevicePixelHeight));
367
+ this._setDevicePixelSize(this._getDevicePixelSizeFromResizeEntry(entry));
365
368
 
366
369
  this._updateDrawingBufferSize();
367
370
 
@@ -389,18 +392,14 @@ export abstract class CanvasSurface {
389
392
  this.updatePosition();
390
393
  }
391
394
 
392
- protected _getDevicePixelSizeFromResizeEntry(entry: ResizeObserverEntry): {
393
- devicePixelWidth: number;
394
- devicePixelHeight: number;
395
- } {
395
+ protected _getDevicePixelSizeFromResizeEntry(entry: ResizeObserverEntry): DevicePixelSize {
396
396
  const contentBoxSize = assertDefined(entry.contentBoxSize?.[0]);
397
397
 
398
398
  if (this.props.pixelSizeSource === 'css-dpr') {
399
- const devicePixelRatio = this.getDevicePixelRatio();
400
- return {
401
- devicePixelWidth: Math.round(contentBoxSize.inlineSize * devicePixelRatio),
402
- devicePixelHeight: Math.round(contentBoxSize.blockSize * devicePixelRatio)
403
- };
399
+ return this._getDevicePixelSizeFromCSSSize(
400
+ contentBoxSize.inlineSize,
401
+ contentBoxSize.blockSize
402
+ );
404
403
  }
405
404
 
406
405
  return {
@@ -413,6 +412,20 @@ export abstract class CanvasSurface {
413
412
  };
414
413
  }
415
414
 
415
+ protected _getDevicePixelSizeFromCSSSize(cssWidth: number, cssHeight: number): DevicePixelSize {
416
+ const devicePixelRatio = this.getDevicePixelRatio();
417
+ return {
418
+ devicePixelWidth: Math.floor(cssWidth * devicePixelRatio),
419
+ devicePixelHeight: Math.floor(cssHeight * devicePixelRatio)
420
+ };
421
+ }
422
+
423
+ protected _setDevicePixelSize({devicePixelWidth, devicePixelHeight}: DevicePixelSize): void {
424
+ const [maxDevicePixelWidth, maxDevicePixelHeight] = this.getMaxDrawingBufferSize();
425
+ this.devicePixelWidth = Math.max(1, Math.min(devicePixelWidth, maxDevicePixelWidth));
426
+ this.devicePixelHeight = Math.max(1, Math.min(devicePixelHeight, maxDevicePixelHeight));
427
+ }
428
+
416
429
  _resizeDrawingBufferIfNeeded() {
417
430
  if (this._needsDrawingBufferResize) {
418
431
  this._needsDrawingBufferResize = false;
@@ -434,6 +447,15 @@ export abstract class CanvasSurface {
434
447
  const oldRatio = this.devicePixelRatio;
435
448
  this.devicePixelRatio = window.devicePixelRatio;
436
449
 
450
+ if (this.props.pixelSizeSource === 'css-dpr') {
451
+ // In css-dpr mode the ResizeObserver watches content-box, which won't fire on a pure DPR
452
+ // change (CSS size unchanged). Recalculate the drawing buffer from the new DPR here.
453
+ const oldPixelSize = this.getDevicePixelSize();
454
+ this._setDevicePixelSize(this._getDevicePixelSizeFromCSSSize(this.cssWidth, this.cssHeight));
455
+ this._updateDrawingBufferSize();
456
+ this.device.props.onResize(this as CanvasContext | PresentationContext, {oldPixelSize});
457
+ }
458
+
437
459
  this.updatePosition();
438
460
 
439
461
  this.device.props.onDevicePixelRatioChange?.(this as CanvasContext | PresentationContext, {