@ericchen1990/pano-view 0.1.6 → 0.2.0

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
@@ -191,7 +191,85 @@ export function ControlledExample() {
191
191
  }
192
192
  ```
193
193
 
194
- The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRotate`, and `toggleFullscreen`. Built-in input supports pointer drag, touch drag, pinch, wheel, arrow keys, `+/-`, and `0` to reset. The two auto-rotation handle methods remain supported for compatibility; prefer rendering `AutoRotate` for new code.
194
+ The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRotate`, and `toggleFullscreen`. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. 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. The two auto-rotation handle methods remain supported for compatibility; prefer rendering `AutoRotate` for new code. A default context menu (Reset view / Enter fullscreen) is also mounted — see [Context menu](#context-menu).
195
+
196
+ ## Mouse, touch, and keyboard controls
197
+
198
+ `PanoView` mounts default `MouseControls`, `TouchControls`, and `KeyboardControls` instances. Configure them through `controls` for most apps; render the components yourself only when you need to replace a channel (for example scene-switch callbacks).
199
+
200
+ ```tsx
201
+ <PanoView
202
+ controls={{
203
+ invert: false,
204
+ bouncingLimits: false,
205
+ mouse: { zoomSpeed: 0.12, wheel: true, buttons: ["left"] },
206
+ touch: { pinchZoom: true },
207
+ keyboard: { enabled: true, rotateSpeed: 90 },
208
+ }}
209
+ style={{ height: 560 }}
210
+ >
211
+ <Sphere src="/panoramas/room.webp" />
212
+ </PanoView>
213
+ ```
214
+
215
+ **Mouse** (pointer types `mouse` / `pen`): drag look and optional wheel zoom. Defaults: `rotateSpeed` `0.35`, `zoomSpeed` `0.08`, `wheel` `true`, `buttons` `["left"]`.
216
+
217
+ **Touch**: one-finger drag and optional two-finger pinch zoom (`pinchZoom`, default `true`).
218
+
219
+ **Keyboard**: hold arrows (or custom bindings) for continuous look / FOV; `0` resets; optional scene bindings. Defaults: `rotateSpeed` `60`, `zoomSpeed` `30`, `shiftMultiplier` `3`. Set `invert` to flip up/down only.
220
+
221
+ Shared all-mode options on `controls`: `enabled`, `invert` (drag direction for mouse/touch), `bouncingLimits`, `fovSpeed`, `frictionStop` (default `0.01`), plus existing damping / inertia.
222
+
223
+ ### Overriding a control channel
224
+
225
+ Set the channel to `false` and render your own component when you need callbacks or fully custom behaviour:
226
+
227
+ ```tsx
228
+ import { useState } from "react";
229
+ import {
230
+ KeyboardControls,
231
+ PanoramaScenes,
232
+ PanoView,
233
+ cycleSceneId,
234
+ type PanoramaScene,
235
+ } from "@ericchen1990/pano-view";
236
+
237
+ export function KeyboardExample({ scenes }: { scenes: PanoramaScene[] }) {
238
+ const [activeSceneId, setActiveSceneId] = useState(scenes[0]!.id);
239
+
240
+ return (
241
+ <PanoView controls={{ keyboard: false }} style={{ height: 560 }}>
242
+ <KeyboardControls
243
+ keys={{
244
+ left: ["ArrowLeft", "a"],
245
+ right: ["ArrowRight", "d"],
246
+ up: ["ArrowUp", "w"],
247
+ down: ["ArrowDown", "s"],
248
+ zoomIn: ["=", "+"],
249
+ zoomOut: ["-", "_"],
250
+ previousScene: ["[", "PageUp"],
251
+ nextScene: ["]", "PageDown"],
252
+ }}
253
+ rotateSpeed={60}
254
+ zoomSpeed={30}
255
+ onPreviousScene={() => {
256
+ const next = cycleSceneId(scenes, activeSceneId, -1);
257
+ if (next) setActiveSceneId(next);
258
+ }}
259
+ onNextScene={() => {
260
+ const next = cycleSceneId(scenes, activeSceneId, 1);
261
+ if (next) setActiveSceneId(next);
262
+ }}
263
+ />
264
+ <PanoramaScenes scenes={scenes} activeSceneId={activeSceneId} />
265
+ </PanoView>
266
+ );
267
+ }
268
+ ```
269
+
270
+ Hold movement and zoom keys for continuous motion in degrees per second (`rotateSpeed` / `zoomSpeed`). Hold Shift to multiply those rates (`shiftMultiplier`, default `3`). Scene and reset bindings fire once per press. The canvas must be focused to receive keys (click the viewer first).
271
+
272
+ Default bindings: arrows for look, `+/-` for FOV, `0` for reset, plus `[`/`PageUp` and `]`/`PageDown` for previous/next scene when callbacks are provided. The same override pattern works with `MouseControls` and `TouchControls` (`controls.mouse={false}` / `controls.touch={false}`).
195
273
 
196
274
  ## Panorama coordinate events
197
275
 
@@ -231,6 +309,165 @@ export function PlacementExample() {
231
309
  is normalized to `[-180, 180)`. Pitch is clamped to `[-90, 90]`; at either
232
310
  pole, yaw is normalized to `0` because it does not identify a unique point.
233
311
 
312
+ ## Viewer-level events (`PanoEvents`)
313
+
314
+ `PanoEvents` is the React counterpart to krpano's global `<events>` element.
315
+ Render one or more instances inside `PanoView` to subscribe to viewer-level
316
+ lifecycle and interaction callbacks. Multiple instances coexist (like named
317
+ krpano events); each tracks its own `idleTime`. For composition inside a custom
318
+ child, use `usePanoEvents` instead.
319
+
320
+ ```tsx
321
+ import {
322
+ AutoRotate,
323
+ PanoEvents,
324
+ PanoView,
325
+ Sphere,
326
+ } from "@ericchen1990/pano-view";
327
+
328
+ <PanoView style={{ height: 560 }}>
329
+ <PanoEvents
330
+ idleTime={3000}
331
+ onIdle={() => console.log("idle")}
332
+ onIdleEnd={() => console.log("active again")}
333
+ onViewSettled={(view) => console.log("settled", view)}
334
+ onViewInteractionStart={({ source }) => console.log("drag", source)}
335
+ onClick={({ position }) => console.log("pano click", position)}
336
+ onEnterFullscreen={() => console.log("fullscreen")}
337
+ onAutoRotateOneRound={() => console.log("full turn")}
338
+ />
339
+ <AutoRotate enabled />
340
+ <Sphere src="/panoramas/room.webp" />
341
+ </PanoView>;
342
+ ```
343
+
344
+ Supported callbacks (krpano names in parentheses where applicable):
345
+
346
+ - View: `onViewChange` (`onviewchange`), `onViewSettled`, `onViewInteractionStart` / `onViewInteractionEnd`
347
+ - Pointer on the panorama shell (not hotspots): `onClick`, `onDoubleClick`, `onPointerDown`, `onPointerUp`, `onPointerMove`, `onContextMenu`
348
+ - `onWheel` — mouse wheel only; touch pinch does not synthesize a wheel event
349
+ - Idle: `onIdle` / `onIdleEnd` with per-instance `idleTime` (ms, default `2000`)
350
+ - Fullscreen: `onEnterFullscreen` / `onExitFullscreen`
351
+ - `onResize` — canvas content-box size via `ResizeObserver`
352
+ - Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
353
+
354
+ `PanoView`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
355
+ `onPanoramaPointerMove` props remain supported and share the same event bus.
356
+
357
+ Resource loading and scene blending stay on their owners: use `Sphere` /
358
+ `Tile` `onLoad` / `onError` / `onLoadProgress`, and `PanoramaScenes`
359
+ `onTransitionEnd` / `onTransitionError`. This package does not mirror krpano
360
+ xml/VR/gyro/frame-render events.
361
+
362
+ ## Context menu
363
+
364
+ `PanoView` mounts a default context menu that replaces the browser menu on
365
+ right-click. Default items: **Reset view** and **Enter fullscreen** / **Exit
366
+ fullscreen** (label and icon follow the current fullscreen state), with a
367
+ separator between them.
368
+
369
+ ```tsx
370
+ <PanoView style={{ height: 560 }}>
371
+ <Sphere src="/panoramas/room.webp" />
372
+ </PanoView>
373
+ ```
374
+
375
+ Disable the default menu (restore the browser menu, or mount your own):
376
+
377
+ ```tsx
378
+ <PanoView contextMenu={false}>
379
+ <Sphere src="/panoramas/room.webp" />
380
+ </PanoView>
381
+ ```
382
+
383
+ Tune appearance while keeping the default items:
384
+
385
+ ```tsx
386
+ <PanoView contextMenu={{ appearance: { opacity: 0.92, borderRadius: 8 } }}>
387
+ <Sphere src="/panoramas/room.webp" />
388
+ </PanoView>
389
+ ```
390
+
391
+ ### Adding items without rebuilding defaults
392
+
393
+ Use `append` or `prepend` to keep the built-in Reset / Fullscreen entries and
394
+ only add your own:
395
+
396
+ ```tsx
397
+ <PanoView
398
+ contextMenu={{
399
+ append: [
400
+ "separator",
401
+ {
402
+ id: "copy",
403
+ label: "Copy yaw / pitch",
404
+ onSelect: ({ position }) => {
405
+ void navigator.clipboard.writeText(
406
+ `${position.yaw.toFixed(1)}, ${position.pitch.toFixed(1)}`,
407
+ );
408
+ },
409
+ },
410
+ ],
411
+ }}
412
+ style={{ height: 560 }}
413
+ >
414
+ <Sphere src="/panoramas/room.webp" />
415
+ </PanoView>
416
+ ```
417
+
418
+ ### Built-in presets
419
+
420
+ When you need a custom order, reuse built-in presets by id instead of
421
+ reimplementing fullscreen enter/exit state:
422
+
423
+ - `"resetView"` — resets the view
424
+ - `"fullscreen"` — Enter / Exit fullscreen with the matching icon
425
+ - `"separator"` — a horizontal rule
426
+
427
+ ```tsx
428
+ <PanoView
429
+ contextMenu={{
430
+ items: [
431
+ "resetView",
432
+ "separator",
433
+ {
434
+ id: "copy",
435
+ label: "Copy yaw / pitch",
436
+ onSelect: ({ position }) => {
437
+ void navigator.clipboard.writeText(
438
+ `${position.yaw.toFixed(1)}, ${position.pitch.toFixed(1)}`,
439
+ );
440
+ },
441
+ },
442
+ "separator",
443
+ "fullscreen",
444
+ ],
445
+ }}
446
+ style={{ height: 560 }}
447
+ >
448
+ <Sphere src="/panoramas/room.webp" />
449
+ </PanoView>
450
+ ```
451
+
452
+ Override presentation on a preset with `{ preset: "fullscreen", label: "…" }`.
453
+ For fully manual assembly outside the prop (for example a custom
454
+ `PanoContextMenu` child), call `createPanoContextMenuPresets({ reset,
455
+ toggleFullscreen, isFullscreen })` and use `.resetView` / `.fullscreen` /
456
+ `.defaults`.
457
+
458
+ Concrete items may use an `icon` React node or an `image` URL. Separators
459
+ always come from the `"separator"` preset. Appearance covers background, text
460
+ color, border, radius, shadow, background-only `opacity` (clamped from
461
+ `MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY` / `0.4` to `1`; labels and icons
462
+ stay opaque), hover/disabled colors, and icon size. For an explicit translucent
463
+ fill without using `opacity`, you can still pass `background: "rgba(...)"`.
464
+
465
+ For a fully custom DOM menu, set `contextMenu={false}` and render
466
+ `PanoContextMenu` as a child (same override pattern as `MouseControls`). Do
467
+ not mount both the default menu and a child `PanoContextMenu` at once. Avoid
468
+ combining the context menu with `controls.mouse.buttons: ["right"]` — both
469
+ claim the right mouse button.
470
+
234
471
  ## Shared hotspot contract and saved definitions
235
472
 
236
473
  Every hotspot has an `id`, optional `visible`, `interactive`, `renderOrder`,
@@ -289,10 +526,11 @@ import { ImageHotspot, PanoView, Sphere } from "@ericchen1990/pano-view";
289
526
  </PanoView>;
290
527
  ```
291
528
 
292
- `GraphicHotspot` accepts built-in `circle`, `rectangle`, and `ring` graphics,
293
- an SVG URL, or safe SVG path data with an explicit viewBox. Built-in graphics
294
- support `fill`, `stroke`, and `strokeWidth`; rectangles also support
295
- `cornerRadius` and rings support `innerRadius`.
529
+ `GraphicHotspot` accepts built-in `circle`, `triangle`, `diamond`, `star`,
530
+ `arrow`, `rectangle`, and `ring` graphics, an SVG URL, or safe SVG path data
531
+ with an explicit viewBox. Built-in graphics support `fill`, `stroke`, and
532
+ `strokeWidth`; rectangles support a relative `cornerRadius` from `0` to `0.5`,
533
+ and rings support `innerRadius`.
296
534
 
297
535
  ```tsx
298
536
  import { GraphicHotspot } from "@ericchen1990/pano-view";
@@ -314,6 +552,31 @@ import { GraphicHotspot } from "@ericchen1990/pano-view";
314
552
  />
315
553
  ```
316
554
 
555
+ Triangle, diamond, star, and arrow graphics use the same paint properties. The
556
+ triangle and arrow point upward at `rotation={0}`; use the hotspot's `rotation`
557
+ property to orient them in another direction.
558
+
559
+ ```tsx
560
+ <GraphicHotspot
561
+ id="direction"
562
+ ariaLabel="Open next scene"
563
+ position={{ yaw: 8, pitch: 2 }}
564
+ rotation={45}
565
+ width={8}
566
+ height={8}
567
+ graphic={{
568
+ kind: "arrow",
569
+ fill: "#df6b42",
570
+ stroke: "#f5fbfc",
571
+ strokeWidth: 8,
572
+ }}
573
+ />
574
+ ```
575
+
576
+ `cornerRadius` is relative to the rendered graphic's shorter side: `0` keeps
577
+ square corners and `0.5` produces the maximum rounded corners. This replaces
578
+ the previous Canvas-texture pixel interpretation.
579
+
317
580
  Set `draggable` and update the controlled position from `onPositionChange` to
318
581
  move a selected hotspot. Clickable hotspots need `ariaLabel`: PanoView creates
319
582
  an internal semantic control for Tab, Enter, and Space activation, with a
@@ -337,9 +600,10 @@ placement properties:
337
600
  - `scaleMode="fov"` (default) lets a hotspot grow when the user zooms in.
338
601
  `scaleMode="fixed"` compensates for FOV changes and keeps its screen size
339
602
  close to the size at `referenceFov` (default `75`).
340
- - `rotation` rotates the hotspot around its own normal in degrees. `scale`
341
- applies an overall positive multiplier to its angular width and height and
342
- defaults to `1`.
603
+ - `rotation` rotates the hotspot around its own normal in degrees; positive
604
+ values rotate clockwise and negative values rotate counterclockwise.
605
+ `scale` applies an overall positive multiplier to its angular width and
606
+ height and defaults to `1`.
343
607
 
344
608
  ```tsx
345
609
  <ImageHotspot