@displayxr/inline3d 1.2.0 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ entry points (`.`, `./three`) are frozen for 1.x, while the **scene subpaths** (
5
5
  `./splat`, `./model`) are a preview tier whose options may change in any release. Entries below say
6
6
  which tier they touch, because that is what tells you whether an upgrade can move your pixels.
7
7
 
8
+ ## 1.2.1 — 2026-08-25
9
+
10
+ ### Fixed
11
+
12
+ - **`./viewer` wheel zoom is proportional and eased.** It scaled by the delta's SIGN only — a flat
13
+ 8% step per wheel event — which is about right for one mouse notch and badly wrong for a
14
+ trackpad, where a single two-finger flick emits dozens of small events. Measured: a 20-event
15
+ flick reached **5.3x** and a longer swipe hit the 6x clamp, on gestures the user reads as gentle.
16
+
17
+ The handler now scales by magnitude, normalises `deltaMode` (Chrome reports pixels; Firefox
18
+ reports LINES for a mouse wheel, so identical hardware was ~2x more sensitive in one browser),
19
+ clamps per event against OS pointer-acceleration spikes, and applies the zoom as `exp()` so equal
20
+ deltas give equal ratios in both directions — `1 + d` and `1 - d` are not inverses, and the
21
+ asymmetry was felt as zooming out being weaker than zooming in. Zoom then eases toward its target
22
+ on the same damping curve yaw and pitch already used, so a notch glides instead of stepping.
23
+
24
+ Same gestures after: trackpad flick **1.13x**, mouse 3 notches **1.35x**, and Chrome and Firefox
25
+ now agree per notch. *(preview tier — no API change)*
26
+
8
27
  ## 1.2.0 — 2026-08-20
9
28
 
10
29
  ### Added
@@ -219,6 +219,7 @@ export class SceneViewer {
219
219
 
220
220
  this._fitScale = 1;
221
221
  this._zoom = 1;
222
+ this._targetZoom = 1;
222
223
  this._yaw = 0;
223
224
  this._pitch = 0;
224
225
  this._targetYaw = 0;
@@ -346,7 +347,7 @@ export class SceneViewer {
346
347
  if (pitch !== undefined) {
347
348
  this._targetPitch = this._pitch = clamp(pitch, this.pitchLimit[0], this.pitchLimit[1]);
348
349
  }
349
- if (zoom !== undefined) this._zoom = clamp(zoom, 0.2, 6);
350
+ if (zoom !== undefined) this._targetZoom = this._zoom = clamp(zoom, ZOOM_MIN, ZOOM_MAX);
350
351
  this._applyTransform();
351
352
  }
352
353
 
@@ -682,6 +683,14 @@ export class SceneViewer {
682
683
  const k = dt > 0 ? 1 - Math.pow(0.001, dt) : 1;
683
684
  this._yaw += (this._targetYaw - this._yaw) * k;
684
685
  this._pitch += (this._targetPitch - this._pitch) * k;
686
+ // Zoom eases on the same curve. Multiplicatively, because zoom is a ratio: approaching 2x
687
+ // linearly spends most of its time near the start and then lurches, while a ratio approach
688
+ // covers equal PERCEPTUAL steps per frame.
689
+ if (Math.abs(this._targetZoom - this._zoom) > 1e-4) {
690
+ this._zoom *= Math.pow(this._targetZoom / this._zoom, k);
691
+ } else {
692
+ this._zoom = this._targetZoom;
693
+ }
685
694
  this._applyTransform();
686
695
  }
687
696
 
@@ -725,9 +734,29 @@ export class SceneViewer {
725
734
  };
726
735
  this._onWheel = (ev) => {
727
736
  ev.preventDefault();
728
- this._zoom = clamp(this._zoom * (ev.deltaY > 0 ? 0.92 : 1.087), 0.2, 6);
737
+ // Scale by the delta's MAGNITUDE, not just its sign. The previous version applied a fixed
738
+ // 8% step per event, which is roughly right for one mouse notch and badly wrong for a
739
+ // trackpad: a two-finger flick emits dozens of small events, so an 8% step compounded per
740
+ // event sent the subject to the clamp on a gesture the user read as gentle.
741
+ //
742
+ // deltaMode has to be normalised first or the same code means different things per browser:
743
+ // Chrome reports pixels, Firefox reports LINES for a mouse wheel (deltaY 3, not 100), and
744
+ // a page-mode device would otherwise be ~200x more sensitive than a trackpad.
745
+ let px = ev.deltaY;
746
+ if (ev.deltaMode === 1) px *= WHEEL_LINE_PX;
747
+ else if (ev.deltaMode === 2) px *= WHEEL_PAGE_PX;
748
+ // OS pointer acceleration can spike a single event past 500px. Clamping per event keeps one
749
+ // hard flick from teleporting the subject while leaving the gesture's total travel intact,
750
+ // since the events keep coming.
751
+ px = clamp(px, -WHEEL_MAX_PX, WHEEL_MAX_PX);
752
+
753
+ // exp() rather than a multiply-add: zoom is a ratio, so equal deltas should give equal
754
+ // ratios in both directions. `1 + d` and `1 - d` are not inverses, and the asymmetry is
755
+ // felt as zooming out being weaker than zooming in.
756
+ this._targetZoom = clamp(this._targetZoom * Math.exp(-px * ZOOM_PER_PX), ZOOM_MIN, ZOOM_MAX);
729
757
  this._lastInput = now();
730
- this._applyTransform();
758
+ // No _applyTransform() here: _tick() eases toward the target and applies it, which is what
759
+ // makes a wheel notch glide instead of step.
731
760
  };
732
761
 
733
762
  el.style.touchAction = 'none'; // or the browser eats the drag as a scroll
@@ -749,6 +778,24 @@ export class SceneViewer {
749
778
  }
750
779
  }
751
780
 
781
+ /**
782
+ * Wheel-zoom tuning.
783
+ *
784
+ * ZOOM_PER_PX is set so one ordinary mouse notch (~100 px in Chrome) is about a 10% step, which
785
+ * puts a trackpad's 1-10 px events at a fraction of a percent each — small enough that the easing
786
+ * reads as continuous rather than as a stack of jumps.
787
+ */
788
+ // A deltaMode-1 "line" is sized to match a wheel DETENT, not a line of text. Firefox reports a
789
+ // notch as deltaY 3 in lines where Chrome reports it as ~100 in pixels, so 33 makes one physical
790
+ // notch feel the same in both; 16 (a text line) would make Firefox roughly half as responsive as
791
+ // Chrome for identical hardware.
792
+ const WHEEL_LINE_PX = 33;
793
+ const WHEEL_PAGE_PX = 400; // a "page" in deltaMode 2; rare, but it must not be unbounded
794
+ const WHEEL_MAX_PX = 120; // per-event ceiling, against OS pointer acceleration spikes
795
+ const ZOOM_PER_PX = 0.001;
796
+ const ZOOM_MIN = 0.2;
797
+ const ZOOM_MAX = 6;
798
+
752
799
  function now() {
753
800
  return typeof performance !== 'undefined' ? performance.now() : Date.now();
754
801
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",