@ericchen1990/pano-view 3.6.1 → 3.6.2

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/README.md CHANGED
@@ -879,7 +879,7 @@ function TourPlayer() {
879
879
 
880
880
  The two deprecated auto-rotation handle methods (`startAutoRotate`, `stopAutoRotate`) remain for compatibility. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. Render `MouseControls`, `TouchControls`, or `KeyboardControls` only to override properties (the child replaces that channel's default instance). Tune shared behaviour through `controls` (`inertia`, `invert`, `bouncingLimits`, `fovSpeed`, `frictionStop`, `rotateDamping`, `zoomDamping`, and top-level `rotateSpeed` / `zoomSpeed`). Disable a channel with `controls.mouse` / `touch` / `keyboard` set to `false`, or pass an options object (including `enabled`) to override defaults without mounting a child. Auto-rotation is off until you render `AutoRotate`. A default context menu (Reset view / Enter fullscreen) is also mounted — see [`PanoContextMenu`](#panocontextmenu).
881
881
 
882
- Mouse, pen, and single-touch drags anchor the grabbed panorama point and update rotation once per render frame, with a short time-based filter (about 10ms of following delay during steady movement) to smooth uneven input samples. The grabbed point aligns exactly once movement settles. View limits and positions near the poles can restrict tracking. The default `rotateSpeed` of `0.35` gives 1:1 tracking; other values scale pointer displacement relative to it, `0` disables rotation, and `invert` reverses direction. The separate `rotateDamping` setting does not apply during anchored drags; release inertia is retained. Pinch and wheel zoom continue to follow the target FOV smoothly.
882
+ Mouse, pen, and single-touch drags anchor the grabbed panorama point and update rotation once per render frame, with a short time-based filter (about 10ms of following delay during steady movement) to smooth uneven input samples. The grabbed point aligns once movement settles, and releasing the pointer completes that alignment immediately. Flick inertia follows the latest pointer motion, so a pause before release does not continue an earlier flick. View limits and positions near the poles can restrict tracking. The default `rotateSpeed` of `0.35` gives 1:1 tracking; other values scale pointer displacement relative to it, `0` disables rotation, and `invert` reverses direction. The separate `rotateDamping` setting does not apply during anchored drags; release inertia is retained. Pinch and wheel zoom continue to follow the target FOV smoothly.
883
883
 
884
884
  Other rotation input and zoom update a target view that the camera follows smoothly. `rotateDamping` and `zoomDamping` control that following speed in seconds^-1 (defaults: `14` and `16` respectively); smaller positive values make the camera approach the target more gradually, while `0` disables smoothing for that axis. Both values must be non-negative finite numbers. Imperative `setView()` and `reset()` remain immediate.
885
885
 
package/README.zh-CN.md CHANGED
@@ -852,7 +852,7 @@ function TourPlayer() {
852
852
 
853
853
  两个已废弃的自动旋转方法(`startAutoRotate`、`stopAutoRotate`)继续保留以兼容。鼠标、触控与键盘输入默认启用 — 普通浏览无需再渲染控件组件。仅当需要覆盖属性时渲染 `MouseControls`、`TouchControls` 或 `KeyboardControls`(子组件会替换该通道的默认实例)。通过 `controls` 调节共享行为(`inertia`、`invert`、`bouncingLimits`、`fovSpeed`、`frictionStop`、`rotateDamping`、`zoomDamping`,以及顶层的 `rotateSpeed` / `zoomSpeed`)。将 `controls.mouse` / `touch` / `keyboard` 设为 `false` 可关闭某通道,或传入选项对象(含 `enabled`)在不挂载子组件的情况下覆盖默认值。自动旋转在渲染 `AutoRotate` 之前处于关闭状态。默认右键菜单(重置视图 / 进入全屏)也会挂载 — 见 [`PanoContextMenu`](#panocontextmenu)。
854
854
 
855
- 鼠标、笔和单指拖拽会锚定按下的全景点,并在每个渲染帧更新旋转;使用短暂的时间平滑吸收输入采样的波动(匀速移动时约 10ms 跟随延迟),停住并收敛后按下点会准确对齐指针;视角边界或极点附近可能限制跟随。`rotateSpeed` 默认 `0.35` 表示 1:1 跟随,其他值按其相对倍率调整指针位移,`0` 禁止旋转;`invert` 反转方向。双指缩放和滚轮继续更新目标 FOV,相机平滑跟随。拖拽释放后保留惯性。`rotateDamping` 与 `zoomDamping` 控制跟随速度(旋转平滑不作用于锚定拖拽期间),单位为秒⁻¹(默认分别为 `14` 与 `16`);较小的正值使相机更缓慢地接近目标视角,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
855
+ 鼠标、笔和单指拖拽会锚定按下的全景点,并在每个渲染帧更新旋转;使用短暂的时间平滑吸收输入采样的波动(匀速移动时约 10ms 跟随延迟)。指针停住并收敛后,按下点会与指针对齐;松手时也会立刻完成这次对齐。甩动惯性只跟随最近一段指针运动,松手前如果已经停住,不会把更早的速度继续带出去。视角边界或极点附近可能限制跟随。`rotateSpeed` 默认 `0.35` 表示 1:1 跟随,其他值按其相对倍率调整指针位移,`0` 禁止旋转;`invert` 反转方向。双指缩放和滚轮继续更新目标 FOV,相机平滑跟随。拖拽释放后保留惯性。`rotateDamping` 与 `zoomDamping` 控制跟随速度(旋转平滑不作用于锚定拖拽期间),单位为秒⁻¹(默认分别为 `14` 与 `16`);较小的正值使相机更缓慢地接近目标视角,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
856
856
 
857
857
  画布光标默认为 `grab`,鼠标拖拽时为 `grabbing`,悬停在可交互热点上为 `pointer`,拖拽热点时为 `move`。可通过 `cursors`(`default`、`dragging`、`hotspot`、`hotspotDragging`)覆盖,或传 `cursors={false}` 保持画布光标不变。热点可设置 `cursor`,仅替换该热点的悬停光标。
858
858
 
package/dist/index.cjs CHANGED
@@ -2896,7 +2896,12 @@ var PanoramaViewRuntime = (0, import_react3.forwardRef)(function PanoramaViewRun
2896
2896
  }
2897
2897
  }
2898
2898
  markDirty();
2899
- if (applyOptions?.recordVelocity && !gyroControlsView) {
2899
+ const releaseVelocity = applyOptions?.velocity;
2900
+ if (!gyroControlsView && releaseVelocity && Number.isFinite(releaseVelocity.yaw) && Number.isFinite(releaseVelocity.pitch)) {
2901
+ yawVelocityRef.current = releaseVelocity.yaw;
2902
+ pitchVelocityRef.current = releaseVelocity.pitch;
2903
+ lastVelocitySampleAtRef.current = performance.now();
2904
+ } else if (applyOptions?.recordVelocity && !gyroControlsView) {
2900
2905
  const now = performance.now();
2901
2906
  const previousAt = lastVelocitySampleAtRef.current;
2902
2907
  const elapsed = previousAt === null ? 1 / 60 : Math.max((now - previousAt) / 1e3, 1 / 120);
@@ -3736,6 +3741,26 @@ var import_react9 = require("react");
3736
3741
  var import_three2 = require("three");
3737
3742
  var DEFAULT_POINTER_DRAG_SPEED = 0.35;
3738
3743
  var POINTER_FOLLOW_TIME_SECONDS = 0.01;
3744
+ var MIN_VELOCITY_SAMPLE_SECONDS = 1 / 120;
3745
+ function pointerReleaseVelocity(samples, releasedAt, solve, maxAgeMs = POINTER_VELOCITY_MAX_AGE_MS) {
3746
+ if (samples.length < 2) return { yaw: 0, pitch: 0 };
3747
+ const newest = samples[samples.length - 1];
3748
+ if (releasedAt < newest.at || releasedAt - newest.at > maxAgeMs) return { yaw: 0, pitch: 0 };
3749
+ let oldest = newest;
3750
+ for (let index = samples.length - 1; index >= 0; index -= 1) {
3751
+ const sample = samples[index];
3752
+ if (releasedAt - sample.at <= maxAgeMs) oldest = sample;
3753
+ else break;
3754
+ }
3755
+ if (oldest === newest) return { yaw: 0, pitch: 0 };
3756
+ const elapsed = Math.max((newest.at - oldest.at) / 1e3, MIN_VELOCITY_SAMPLE_SECONDS);
3757
+ const from = solve(oldest);
3758
+ const to = solve(newest);
3759
+ return {
3760
+ yaw: normalizeViewYaw(to.yaw - from.yaw) / elapsed,
3761
+ pitch: (to.pitch - from.pitch) / elapsed
3762
+ };
3763
+ }
3739
3764
  var PointerDragAnchor = class {
3740
3765
  constructor(start, origin, rect, projection) {
3741
3766
  this.start = start;
@@ -3793,8 +3818,11 @@ function createPointerDrag(camera, element, runtime, source) {
3793
3818
  let lastPoint;
3794
3819
  let fresh = false;
3795
3820
  let lastFrameDelta = 1 / 60;
3821
+ let samples = [];
3822
+ let gestureSpeed = DEFAULT_POINTER_DRAG_SPEED;
3823
+ let gestureInvert = false;
3796
3824
  const key = (rect) => `${rect.left},${rect.top},${rect.width},${rect.height},${camera.projectionMatrix.elements.join(",")}`;
3797
- const start = (point) => {
3825
+ const place = (point) => {
3798
3826
  const handle = runtime();
3799
3827
  displayed = { ...point };
3800
3828
  lastPoint = { ...point };
@@ -3806,69 +3834,113 @@ function createPointerDrag(camera, element, runtime, source) {
3806
3834
  projectionKey = key(rect);
3807
3835
  anchor = new PointerDragAnchor(point, handle.getView(), rect, camera.clone());
3808
3836
  };
3809
- const apply = (point, previous, speed, invert, recordVelocity) => {
3837
+ const remember = (point, at) => {
3838
+ samples.push({ x: point.x, y: point.y, at });
3839
+ const cutoff = at - POINTER_VELOCITY_MAX_AGE_MS;
3840
+ while (samples.length > 2 && samples[0].at < cutoff) samples.shift();
3841
+ };
3842
+ const apply = (point, previous, speed, invert, recordVelocity, releaseVelocity) => {
3810
3843
  const handle = runtime();
3811
3844
  if (!handle) return;
3812
3845
  const rect = element.getBoundingClientRect();
3813
3846
  if (anchor && key(rect) !== projectionKey) {
3814
3847
  const queued = pending;
3815
- start(displayed ?? previous);
3848
+ place(displayed ?? previous);
3816
3849
  pending = queued;
3817
3850
  }
3851
+ const options = {
3852
+ recordVelocity: releaseVelocity ? false : recordVelocity,
3853
+ source,
3854
+ ...releaseVelocity ? { velocity: releaseVelocity } : {}
3855
+ };
3818
3856
  if (anchor) {
3819
3857
  handle.applyViewDelta(anchor.solve(point, speed, invert), {
3820
- pointerOrigin: anchor.origin,
3821
- recordVelocity,
3822
- source
3858
+ ...options,
3859
+ pointerOrigin: anchor.origin
3823
3860
  });
3824
3861
  } else {
3825
3862
  handle.applyViewDelta({
3826
3863
  yaw: -(point.x - previous.x) * 360 * speed / Math.max(rect.width, 1) * (invert ? -1 : 1),
3827
3864
  pitch: (point.y - previous.y) * 180 * speed / Math.max(rect.height, 1) * (invert ? -1 : 1)
3828
- }, { recordVelocity, source });
3865
+ }, options);
3829
3866
  }
3830
3867
  };
3868
+ const valid = () => revision === (runtime()?.getPointerDragState().revision ?? -1) && !runtime()?.isInteractionLocked();
3869
+ const clear = () => {
3870
+ anchor = void 0;
3871
+ revision = -1;
3872
+ pending = void 0;
3873
+ displayed = void 0;
3874
+ samples = [];
3875
+ };
3831
3876
  return {
3832
- start,
3833
- valid: () => revision === (runtime()?.getPointerDragState().revision ?? -1) && !runtime()?.isInteractionLocked(),
3834
- clear: () => {
3835
- anchor = void 0;
3836
- revision = -1;
3837
- pending = void 0;
3838
- displayed = void 0;
3877
+ start(point, at = performance.now()) {
3878
+ gestureSpeed = DEFAULT_POINTER_DRAG_SPEED;
3879
+ gestureInvert = false;
3880
+ samples = [];
3881
+ remember(point, at);
3882
+ place(point);
3839
3883
  },
3884
+ valid,
3885
+ clear,
3840
3886
  suspend: () => {
3841
3887
  anchor = void 0;
3842
3888
  pending = void 0;
3843
3889
  displayed = void 0;
3890
+ samples = [];
3844
3891
  },
3845
- move(point, previous, speed, invert) {
3892
+ move(point, previous, speed, invert, at = performance.now()) {
3846
3893
  fresh = true;
3894
+ gestureSpeed = speed;
3895
+ gestureInvert = invert;
3896
+ remember(point, at);
3847
3897
  pending = { point, previous: lastPoint ?? previous, speed, invert };
3848
3898
  },
3849
- flush(deltaSeconds = lastFrameDelta, immediate = false) {
3850
- lastFrameDelta = deltaSeconds;
3899
+ flush(deltaSeconds = lastFrameDelta, immediate = false, releaseVelocity) {
3900
+ if (deltaSeconds > 0) lastFrameDelta = deltaSeconds;
3851
3901
  if (!pending) return;
3852
3902
  const sample = pending;
3853
3903
  if (!anchor || immediate) {
3854
3904
  pending = void 0;
3855
- apply(sample.point, sample.previous, sample.speed, sample.invert, fresh);
3905
+ apply(sample.point, sample.previous, sample.speed, sample.invert, fresh, releaseVelocity);
3856
3906
  fresh = false;
3857
3907
  lastPoint = sample.point;
3858
3908
  displayed = sample.point;
3859
3909
  return;
3860
3910
  }
3911
+ const dt = deltaSeconds > 0 ? deltaSeconds : lastFrameDelta;
3861
3912
  const previousDisplay = displayed ?? sample.previous;
3862
3913
  const dx = sample.point.x - previousDisplay.x;
3863
3914
  const dy = sample.point.y - previousDisplay.y;
3864
3915
  const distance = Math.hypot(dx, dy);
3865
- const ratio = distance > 0.01 ? POINTER_FOLLOW_TIME_SECONDS / (POINTER_FOLLOW_TIME_SECONDS + deltaSeconds) : 0;
3916
+ const ratio = distance > 0.01 ? POINTER_FOLLOW_TIME_SECONDS / (POINTER_FOLLOW_TIME_SECONDS + dt) : 0;
3866
3917
  const point = { x: sample.point.x - dx * ratio, y: sample.point.y - dy * ratio };
3867
3918
  apply(point, sample.previous, sample.speed, sample.invert, fresh);
3868
3919
  fresh = false;
3869
3920
  lastPoint = sample.point;
3870
3921
  displayed = point;
3871
3922
  if (ratio === 0) pending = void 0;
3923
+ },
3924
+ release() {
3925
+ const handle = runtime();
3926
+ if (!handle || !valid()) {
3927
+ clear();
3928
+ return;
3929
+ }
3930
+ const newestAt = samples[samples.length - 1]?.at ?? 0;
3931
+ const velocity = pointerReleaseVelocity(samples, Math.max(performance.now(), newestAt), (point) => {
3932
+ if (anchor) return anchor.solve(point, gestureSpeed, gestureInvert);
3933
+ const origin = samples[0] ?? point;
3934
+ const rect = element.getBoundingClientRect();
3935
+ const sign = gestureInvert ? -1 : 1;
3936
+ return {
3937
+ yaw: -(point.x - origin.x) * 360 * gestureSpeed / Math.max(rect.width, 1) * sign,
3938
+ pitch: (point.y - origin.y) * 180 * gestureSpeed / Math.max(rect.height, 1) * sign
3939
+ };
3940
+ });
3941
+ if (pending) this.flush(lastFrameDelta, true, velocity);
3942
+ else handle.applyViewDelta({}, { velocity, source });
3943
+ clear();
3872
3944
  }
3873
3945
  };
3874
3946
  }
@@ -3884,6 +3956,9 @@ var BUTTON_FLAG = {
3884
3956
  function resolveSpeed2(value, fallback) {
3885
3957
  return Number.isFinite(value) && value >= 0 ? value : fallback;
3886
3958
  }
3959
+ function pointerAt(event) {
3960
+ return event.timeStamp > 0 ? event.timeStamp : performance.now();
3961
+ }
3887
3962
  function resolveButtons(buttons) {
3888
3963
  const list = buttons && buttons.length > 0 ? buttons : ["left"];
3889
3964
  return new Set(list.map((button) => BUTTON_FLAG[button]));
@@ -3970,7 +4045,7 @@ function MouseControls({
3970
4045
  });
3971
4046
  element.setPointerCapture?.(event.pointerId);
3972
4047
  syncActive();
3973
- drag.start({ x: event.clientX, y: event.clientY });
4048
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
3974
4049
  };
3975
4050
  const onPointerMove = (event) => {
3976
4051
  if (!isMouseLike(event)) {
@@ -3991,15 +4066,15 @@ function MouseControls({
3991
4066
  };
3992
4067
  pointersRef.current.set(event.pointerId, next);
3993
4068
  const speed = resolveSpeed2(optionsRef.current.rotateSpeed, DEFAULT_ROTATE_SPEED2);
3994
- drag.move(next, previous, speed, optionsRef.current.invert);
4069
+ drag.move(next, previous, speed, optionsRef.current.invert, pointerAt(event));
3995
4070
  };
3996
4071
  const releasePointer = (event) => {
3997
4072
  if (!pointersRef.current.has(event.pointerId)) {
3998
4073
  return;
3999
4074
  }
4000
- if (event.type === "pointerup") drag.flush();
4075
+ if (event.type === "pointerup") drag.release();
4076
+ else drag.clear();
4001
4077
  pointersRef.current.delete(event.pointerId);
4002
- drag.clear();
4003
4078
  if (element.hasPointerCapture?.(event.pointerId)) {
4004
4079
  element.releasePointerCapture?.(event.pointerId);
4005
4080
  }
@@ -5642,6 +5717,9 @@ function notifyScenesHost(host) {
5642
5717
  var import_fiber8 = require("@react-three/fiber");
5643
5718
  var import_react18 = require("react");
5644
5719
  var DEFAULT_ROTATE_SPEED3 = 0.35;
5720
+ function pointerAt2(event) {
5721
+ return event.timeStamp > 0 ? event.timeStamp : performance.now();
5722
+ }
5645
5723
  function resolveSpeed3(value, fallback) {
5646
5724
  return Number.isFinite(value) && value >= 0 ? value : fallback;
5647
5725
  }
@@ -5710,7 +5788,7 @@ function TouchControls({
5710
5788
  });
5711
5789
  element.setPointerCapture?.(event.pointerId);
5712
5790
  syncActive();
5713
- drag.start({ x: event.clientX, y: event.clientY });
5791
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt2(event));
5714
5792
  if (pointersRef.current.size >= 2) {
5715
5793
  drag.suspend();
5716
5794
  const [first, second] = Array.from(pointersRef.current.values());
@@ -5757,15 +5835,16 @@ function TouchControls({
5757
5835
  return;
5758
5836
  }
5759
5837
  const speed = resolveSpeed3(optionsRef.current.rotateSpeed, DEFAULT_ROTATE_SPEED3);
5760
- drag.move(next, previous, speed, optionsRef.current.invert);
5838
+ drag.move(next, previous, speed, optionsRef.current.invert, pointerAt2(event));
5761
5839
  };
5762
5840
  const releasePointer = (event) => {
5763
5841
  if (!pointersRef.current.has(event.pointerId)) {
5764
5842
  return;
5765
5843
  }
5766
- if (event.type === "pointerup") drag.flush();
5844
+ const pinch = pointersRef.current.size >= 2;
5845
+ if (event.type === "pointerup" && !pinch) drag.release();
5846
+ else drag.clear();
5767
5847
  pointersRef.current.delete(event.pointerId);
5768
- drag.clear();
5769
5848
  if (element.hasPointerCapture?.(event.pointerId)) {
5770
5849
  element.releasePointerCapture?.(event.pointerId);
5771
5850
  }
@@ -5774,7 +5853,7 @@ function TouchControls({
5774
5853
  }
5775
5854
  syncActive(event.type !== "pointerup");
5776
5855
  const remaining = pointersRef.current.values().next().value;
5777
- if (remaining) drag.start(remaining);
5856
+ if (remaining) drag.start(remaining, pointerAt2(event));
5778
5857
  };
5779
5858
  element.addEventListener("pointerdown", onPointerDown);
5780
5859
  element.addEventListener("pointermove", onPointerMove);