@bubo-squared/gyroview 0.5.0 → 0.6.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -2
  3. package/dist/index.d.ts +56 -15
  4. package/dist/packages/core/src/domain/view/Framing.js +8 -2
  5. package/dist/packages/core/src/domain/view/ViewState.js +13 -8
  6. package/dist/packages/core/src/domain/view/lensTilesView.js +2 -1
  7. package/dist/packages/core/src/domain/view/motionLookView.js +103 -0
  8. package/dist/packages/core/src/domain/view/normalView.js +12 -5
  9. package/dist/packages/core/src/domain/view/panoramaView.js +2 -1
  10. package/dist/packages/core/src/domain/view/screenLook.js +74 -0
  11. package/dist/packages/core/src/domain/view/viewModes.js +18 -1
  12. package/dist/packages/core/src/index.js +5 -3
  13. package/dist/packages/core/src/shared/math/Matrix3.js +12 -1
  14. package/dist/packages/player/src/browserPlayer.js +6 -4
  15. package/dist/packages/player/src/composition/BrowserAttitudeSensor.js +192 -0
  16. package/dist/packages/player/src/composition/attitudeSensor.js +18 -0
  17. package/dist/packages/player/src/controls/MotionLookButton.js +35 -0
  18. package/dist/packages/player/src/controls/controlParts.js +1 -0
  19. package/dist/packages/player/src/controls/controls.js +1 -1
  20. package/dist/packages/player/src/controls/controlsBar.js +5 -3
  21. package/dist/packages/player/src/controls/controlsMarkup.js +1 -0
  22. package/dist/packages/player/src/controls/icons.js +1 -0
  23. package/dist/packages/player/src/controls/messages.js +1 -0
  24. package/dist/packages/player/src/element/GyroViewElement.js +21 -0
  25. package/dist/packages/player/src/player/AutomaticStarts.js +54 -0
  26. package/dist/packages/player/src/player/MotionLook.js +150 -0
  27. package/dist/packages/player/src/player/Player.js +46 -39
  28. package/dist/packages/player/src/player/PlayerEvents.js +1 -0
  29. package/dist/packages/player/src/player/PlayerView.js +44 -13
  30. package/dist/packages/player/src/player/ensureFinite.js +2 -1
  31. package/dist/packages/player/src/player/viewAngles.js +17 -0
  32. package/dist/standalone.js +115 -62
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  What changed for a page using the package, newest first. Until 1.0, a minor version may change
4
4
  the API.
5
5
 
6
+ ## 0.6.0 (2026-10-05)
7
+
8
+ New:
9
+
10
+ - Motion look: on a phone or tablet, a toggle in the normal view lets the device turn the view, the
11
+ screen a window into the recording with its horizon level with the real one (ADR 0040). The
12
+ element and the player have `motionLook` (`on`, `off` or `unavailable`), `startMotionLook()`,
13
+ which iOS honours only from a tap's handler, and `stopMotionLook()`; `motionlookchange` reports
14
+ it, and the warnings `motion-look-refused` and `motion-look-needs-gesture` say why it did not
15
+ start.
16
+
17
+ What a page may notice:
18
+
19
+ - `PlayerEvents`, `GyroViewElementEventMap` and `WarningCode` have the new event and codes; a
20
+ record keyed on every event or code needs them.
21
+ - `view` and `viewchange` give a fresh object of `yaw`, `pitch` and `fieldOfView` each time.
22
+ - The iframe the snippet creates allows `accelerometer; gyroscope; magnetometer`; a hand-written
23
+ iframe needs them for motion look. Without them, Chromium offers no toggle; iOS offers it and
24
+ refuses at the first press (`motion-look-refused`).
25
+ - On the narrowest players with the motion look toggle shown, the Stabilization menu gives way
26
+ to it.
27
+
6
28
  ## 0.5.0 (2026-10-03)
7
29
 
8
30
  New:
package/README.md CHANGED
@@ -45,10 +45,10 @@ A page without a bundler loads the standalone file, which has Three.js and media
45
45
  ```html
46
46
  <script
47
47
  type="module"
48
- src="https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.5/dist/standalone.js"
48
+ src="https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.6/dist/standalone.js"
49
49
  ></script>
50
50
  <script type="module">
51
- import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.5/dist/standalone.js';
51
+ import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/gyroview@0.6/dist/standalone.js';
52
52
  </script>
53
53
  ```
54
54
 
package/dist/index.d.ts CHANGED
@@ -418,8 +418,9 @@ export interface ViewAngles {
418
418
  readonly fieldOfView: number;
419
419
  }
420
420
  /**
421
- * What concerns one load only. The settings (view, view mode, stabilization, gain matching,
422
- * sound, loop) belong to the player and carry over from load to load; set them on the player.
421
+ * What concerns one load only. The settings (view, view mode, motion look, stabilization, gain
422
+ * matching, sound, loop) belong to the player and carry over from load to load; set them on the
423
+ * player.
423
424
  */
424
425
  export interface LoadOptions {
425
426
  /**
@@ -436,6 +437,11 @@ export interface LoadOptions {
436
437
  * The session's states plus the two only the player knows: nothing loaded, and loading.
437
438
  */
438
439
  export type PlayerStatus = "idle" | "loading" | PlayerState;
440
+ /**
441
+ * Motion look as it stands: `unavailable` where the device reports no attitude or access was
442
+ * refused, `off`, or `on`, the device turning the normal view (ADR 0040).
443
+ */
444
+ export type MotionLookState = "unavailable" | "off" | "on";
439
445
  /**
440
446
  * The sound as it is now: volume from 0 to 1, and whether it is muted.
441
447
  */
@@ -453,8 +459,12 @@ export interface SoundLevel {
453
459
  * - `playback-failed`: a start (a press, autoplay, the loop) or the seek bar's preview failed.
454
460
  * - `ignored-attribute`: an attribute's value names nothing the element knows; the setting stays.
455
461
  * - `refused-property`: a property set before the element was defined was refused.
462
+ * - `motion-look-refused`: the viewer, or an iframe's `allow`, refused access to the device's
463
+ * attitude; motion look is unavailable.
464
+ * - `motion-look-needs-gesture`: motion look was started outside a user gesture, where iOS does
465
+ * not ask; it stays off.
456
466
  */
457
- export type WarningCode = "recording-degraded" | "no-sound" | "autoplay-blocked" | "playback-failed" | "ignored-attribute" | "refused-property";
467
+ export type WarningCode = "recording-degraded" | "no-sound" | "autoplay-blocked" | "playback-failed" | "ignored-attribute" | "refused-property" | "motion-look-refused" | "motion-look-needs-gesture";
458
468
  export interface PlayerWarning {
459
469
  readonly code: WarningCode;
460
470
  /**
@@ -488,6 +498,7 @@ export interface PlayerEvents {
488
498
  readonly frame: number;
489
499
  readonly viewchange: ViewAngles;
490
500
  readonly viewmodechange: ViewMode;
501
+ readonly motionlookchange: MotionLookState;
491
502
  readonly stabilizationchange: StabilizationMode;
492
503
  /**
493
504
  * The picture quality changed, to the one named.
@@ -506,8 +517,8 @@ export interface PlayerEvents {
506
517
  /**
507
518
  * The player without the element, which `createBrowserPlayer` gives: it plays one recording at a
508
519
  * time on the canvas and audio element it was handed, reports in media-element events, and keeps
509
- * its settings (view, view mode, stabilization, gain matching, sound, loop) from load to load.
510
- * `<gyro-view>` is a facade over it.
520
+ * its settings (view, view mode, motion look, stabilization, gain matching, sound, loop) from
521
+ * load to load. `<gyro-view>` is a facade over it.
511
522
  */
512
523
  export declare class Player {
513
524
  private readonly parts;
@@ -519,6 +530,7 @@ export declare class Player {
519
530
  private readonly loop;
520
531
  private phase;
521
532
  private readonly viewing;
533
+ private readonly motion;
522
534
  private readonly picture;
523
535
  /**
524
536
  * A seek asked for before a recording was ready, where the next one starts.
@@ -526,7 +538,7 @@ export declare class Player {
526
538
  private pendingStartTime;
527
539
  private readonly sound;
528
540
  private readonly relay;
529
- private isLoopingValue;
541
+ private readonly starts;
530
542
  /**
531
543
  * Past `dispose`: a listener of the idle it announces must not load again.
532
544
  */
@@ -549,6 +561,11 @@ export declare class Player {
549
561
  * Whether a drag moves the picture as it is framed now; the view mode decides.
550
562
  */
551
563
  get canPan(): boolean;
564
+ /**
565
+ * Whether turning the device turns the normal view: `unavailable` where the device reports no
566
+ * attitude or access was refused.
567
+ */
568
+ get motionLook(): MotionLookState;
552
569
  get stabilization(): StabilizationMode;
553
570
  get quality(): PictureQuality;
554
571
  get isLooping(): boolean;
@@ -604,6 +621,17 @@ export declare class Player {
604
621
  zoom(steps: number, focus?: ScreenPoint): void;
605
622
  resetView(): void;
606
623
  setViewMode(mode: ViewMode): void;
624
+ /**
625
+ * Lets the device turn the normal view, as a window into the recording: its tilt and roll
626
+ * hold the pitch and keep the horizon level, drags turn the heading alone. Call it from a tap's
627
+ * handler: iOS asks the viewer for access there and refuses it anywhere else. Resolves with the
628
+ * state it ends in; a refusal is a warning, never a rejection.
629
+ */
630
+ startMotionLook(): Promise<MotionLookState>;
631
+ /**
632
+ * Gives the view back to the pointer, level, looking where it looked.
633
+ */
634
+ stopMotionLook(): void;
607
635
  setStabilization(mode: StabilizationMode): void;
608
636
  /**
609
637
  * How finely the lens images are read and how many device pixels are drawn; kept across loads.
@@ -626,12 +654,6 @@ export declare class Player {
626
654
  private complete;
627
655
  private startAtPendingTime;
628
656
  private attach;
629
- /**
630
- * A refused start leaves the recording loaded and paused: a warning, never a failed load.
631
- */
632
- private autoplay;
633
- private onEnded;
634
- private replay;
635
657
  /**
636
658
  * Lets go of whatever is loading or loaded, announcing nothing.
637
659
  */
@@ -664,6 +686,10 @@ export interface GyroViewLabels {
664
686
  readonly mute: string;
665
687
  readonly volume: string;
666
688
  readonly resetView: string;
689
+ /**
690
+ * The toggle that lets the device's own turns turn the normal view.
691
+ */
692
+ readonly motionLook: string;
667
693
  readonly stabilization: string;
668
694
  readonly viewMode: string;
669
695
  readonly fullscreen: string;
@@ -792,9 +818,9 @@ export interface BrowserPortsOptions {
792
818
  readonly probeTimeoutMs?: number;
793
819
  }
794
820
  /**
795
- * A player composed for a browser: HTTP, blob and WebCodecs ports and the GPU pipeline, drawing
796
- * on the host's canvas and sounding through its audio element. What `<gyro-view>` plays with,
797
- * and where a page that wants the player without the element starts.
821
+ * A player composed for a browser: HTTP, blob and WebCodecs ports, the GPU pipeline and the
822
+ * device's attitude, drawing on the host's canvas and sounding through its audio element. What
823
+ * `<gyro-view>` plays with, and where a page that wants the player without the element starts.
798
824
  */
799
825
  export declare function createBrowserPlayer(host: PipelineHost, options?: BrowserPortsOptions): Player;
800
826
  export interface InspectOptions extends Pick<BrowserPortsOptions, "http"> {
@@ -903,6 +929,11 @@ export declare class GyroViewElement extends TypedEventElement implements LiveSe
903
929
  * Where the normal view looks: yaw and pitch in degrees, and the horizontal field of view.
904
930
  */
905
931
  get view(): ViewAngles;
932
+ /**
933
+ * Whether turning the device turns the normal view: `on`, `off`, or `unavailable` where the
934
+ * device reports no attitude or access was refused.
935
+ */
936
+ get motionLook(): MotionLookState;
906
937
  /**
907
938
  * Every word the element shows or says to assistive technology. Setting it replaces any of
908
939
  * them, table by table (`labels`, `stabilizationModes`, `viewModes`, `errors`); the others keep
@@ -949,6 +980,16 @@ export declare class GyroViewElement extends TypedEventElement implements LiveSe
949
980
  * when none is given.
950
981
  */
951
982
  zoom(steps: number, focus?: ScreenPoint): void;
983
+ /**
984
+ * Lets the device turn the normal view, as a window into the recording. Call it from a tap's
985
+ * handler: iOS asks the viewer for access there and refuses it anywhere else. Resolves with the
986
+ * state it ends in (`motionLook`); a refusal is a `warning`, never a rejection.
987
+ */
988
+ startMotionLook(): Promise<MotionLookState>;
989
+ /**
990
+ * Gives the view back to the pointer, level, looking where it looked.
991
+ */
992
+ stopMotionLook(): void;
952
993
  /**
953
994
  * Stabilizes the picture in `mode` (`off`, `lock`, `horizon`, `follow`); any other value,
954
995
  * an unset one included, is refused.
@@ -1,4 +1,4 @@
1
- import { DEFAULT_VIEW, isSameView } from "./ViewState.js";
1
+ import { DEFAULT_VIEW, clampView, isSameView } from "./ViewState.js";
2
2
  import { FITTED, isSameMagnification } from "./magnification.js";
3
3
  //#region ../../packages/core/src/domain/view/Framing.ts
4
4
  var DEFAULT_FRAMING = {
@@ -19,6 +19,12 @@ function withView(framing, view) {
19
19
  };
20
20
  }
21
21
  /**
22
+ * The framing with a page's view taken as it is given, within the sphere.
23
+ */
24
+ function withPlacedView(framing, view) {
25
+ return withView(framing, clampView(view));
26
+ }
27
+ /**
22
28
  * The framing with the lens tiles' magnification replaced, the other parts kept.
23
29
  */
24
30
  function withLenses(framing, lenses) {
@@ -28,4 +34,4 @@ function withLenses(framing, lenses) {
28
34
  };
29
35
  }
30
36
  //#endregion
31
- export { DEFAULT_FRAMING, isSameFraming, withLenses, withView };
37
+ export { DEFAULT_FRAMING, isSameFraming, withLenses, withPlacedView, withView };
@@ -1,7 +1,7 @@
1
1
  import { degrees, degreesToRadians, wrapHalfTurn } from "../../shared/units/angle.js";
2
2
  import { ensureInvariant } from "../../shared/errors/GyroViewError.js";
3
3
  import { clamp } from "../../shared/math/clamp.js";
4
- import { multiplyMatrices, rotationAboutX, rotationAboutY } from "../../shared/math/Matrix3.js";
4
+ import { multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ } from "../../shared/math/Matrix3.js";
5
5
  //#region ../../packages/core/src/domain/view/ViewState.ts
6
6
  /**
7
7
  * From a short telephoto to a wide angle: past 120 degrees a rectilinear view stretches its
@@ -14,35 +14,40 @@ var MAX_PITCH_DEGREES = 90;
14
14
  var DEFAULT_VIEW = {
15
15
  yaw: degrees(0),
16
16
  pitch: degrees(0),
17
+ roll: degrees(0),
17
18
  fieldOfView: degrees(DEFAULT_FIELD_OF_VIEW_DEGREES)
18
19
  };
19
20
  /**
20
- * Keeps the view inside the sphere: yaw wrapped to (-180, 180], pitch within the poles, field
21
- * of view within its bounds. Non-finite angles are a programming error.
21
+ * Keeps the view inside the sphere: yaw and roll wrapped to (-180, 180], pitch within the poles,
22
+ * field of view within its bounds. Non-finite angles are a programming error.
22
23
  */
23
24
  function clampView(view) {
24
25
  ensureInvariant([
25
26
  view.yaw,
26
27
  view.pitch,
28
+ view.roll,
27
29
  view.fieldOfView
28
30
  ].every((angle) => Number.isFinite(angle)), "view angles must be finite");
29
31
  return {
30
32
  ...view,
31
33
  yaw: wrapHalfTurn(view.yaw),
34
+ roll: wrapHalfTurn(view.roll),
32
35
  pitch: degrees(clamp(view.pitch, -90, MAX_PITCH_DEGREES)),
33
36
  fieldOfView: degrees(clamp(view.fieldOfView, NARROWEST_DEGREES, WIDEST_DEGREES))
34
37
  };
35
38
  }
36
39
  function isSameView(a, b) {
37
- return a.yaw === b.yaw && a.pitch === b.pitch && a.fieldOfView === b.fieldOfView;
40
+ return a.yaw === b.yaw && a.pitch === b.pitch && a.roll === b.roll && a.fieldOfView === b.fieldOfView;
38
41
  }
39
42
  /**
40
- * Turns view-space directions (z forward) into camera body directions: pitch about the body's
41
- * lateral axis first, then yaw about its vertical axis. Body y points down, so a positive
42
- * rotation about x tilts the forward direction upwards.
43
+ * Turns view-space directions (x right, y down, z forward) into camera body directions: roll
44
+ * about the line of sight first, then pitch about the body's lateral axis, then yaw about its
45
+ * vertical axis. Body y points down, so a positive rotation about x tilts the forward direction
46
+ * upwards, and one about z turns the screen's right toward its bottom: clockwise.
43
47
  */
44
48
  function viewRotation(view) {
45
- return multiplyMatrices(rotationAboutY(degreesToRadians(view.yaw)), rotationAboutX(degreesToRadians(view.pitch)));
49
+ const pointed = multiplyMatrices(rotationAboutY(degreesToRadians(view.yaw)), rotationAboutX(degreesToRadians(view.pitch)));
50
+ return multiplyMatrices(pointed, rotationAboutZ(degreesToRadians(view.roll)));
46
51
  }
47
52
  //#endregion
48
53
  export { DEFAULT_VIEW, clampView, isSameView, viewRotation };
@@ -1,6 +1,6 @@
1
1
  import { aspectOf, boundsOf, lensTiles, shiftOf } from "./screenLayout.js";
2
2
  import { FITTED, clampMagnification, magnifiedArea, magnifyAt, panMagnification } from "./magnification.js";
3
- import { withLenses } from "./Framing.js";
3
+ import { withLenses, withPlacedView } from "./Framing.js";
4
4
  //#region ../../packages/core/src/domain/view/lensTilesView.ts
5
5
  /**
6
6
  * An arrow's angle moves zoomed tiles as a drag across the viewport would move them, a 90-degree
@@ -54,6 +54,7 @@ var LENS_TILES_VIEW = {
54
54
  turn: turnLenses,
55
55
  zoom: (framing, zoom, context) => withLenses(framing, magnifyAt(fittedTiles(context).bounds, framing.lenses, zoom)),
56
56
  reset: (framing) => withLenses(framing, FITTED),
57
+ place: withPlacedView,
57
58
  picture: (framing, context) => ({
58
59
  kind: "lens-tiles",
59
60
  tiles: magnifiedTiles(framing, context)
@@ -0,0 +1,103 @@
1
+ import { degrees, degreesToRadians, wrapHalfTurn } from "../../shared/units/angle.js";
2
+ import { milliseconds } from "../../shared/units/time.js";
3
+ import { angleBetweenRotations } from "../../shared/math/Matrix3.js";
4
+ import { DEFAULT_VIEW, clampView, viewRotation } from "./ViewState.js";
5
+ import { SCREEN_CENTRE, aspectOf } from "./screenLayout.js";
6
+ import { lookAt, panView, zoomViewAt } from "./viewGestures.js";
7
+ import { withView } from "./Framing.js";
8
+ import { rectilinearPicture } from "./normalView.js";
9
+ //#region ../../packages/core/src/domain/view/motionLookView.ts
10
+ var READING_GAP = milliseconds(500);
11
+ var UNSEEN_TURN = degreesToRadians(degrees(.01));
12
+ /**
13
+ * Where a reading turns a view the device holds. The yaw only turns by as much as the device
14
+ * turned since the reading it continues, so a drag, Reset view or a page's yaw move the heading
15
+ * and the device carries on from there; after a gap, and at the first reading, the yaw stays.
16
+ * The pitch and roll are the device's, level with the real horizon. A turn too small to see
17
+ * leaves the view as it is and keeps counting from the look last shown, at the new reading's
18
+ * time: a phone at rest draws nothing, and a slow drift lands in full once it shows.
19
+ */
20
+ function followReading(view, previous, reading) {
21
+ const { pitch, roll } = reading.look;
22
+ if (!isOneMovement(previous, reading)) return {
23
+ view: clampView({
24
+ ...view,
25
+ pitch,
26
+ roll
27
+ }),
28
+ reading
29
+ };
30
+ const yaw = degrees(view.yaw + wrapHalfTurn(degrees(reading.look.yaw - previous.look.yaw)));
31
+ const turned = clampView({
32
+ ...view,
33
+ yaw,
34
+ pitch,
35
+ roll
36
+ });
37
+ return isUnseenTurn(view, turned) ? {
38
+ view,
39
+ reading: {
40
+ look: previous.look,
41
+ at: reading.at
42
+ }
43
+ } : {
44
+ view: turned,
45
+ reading
46
+ };
47
+ }
48
+ function isOneMovement(previous, reading) {
49
+ return previous !== void 0 && reading.at - previous.at <= READING_GAP;
50
+ }
51
+ function isUnseenTurn(view, turned) {
52
+ return angleBetweenRotations(viewRotation(view), viewRotation(turned)) < UNSEEN_TURN;
53
+ }
54
+ /**
55
+ * The view once the device lets go of it: level, looking where it looked.
56
+ */
57
+ function withoutRoll(view) {
58
+ return {
59
+ ...view,
60
+ roll: degrees(0)
61
+ };
62
+ }
63
+ /**
64
+ * The normal view while the device holds it (ADR 0040): the device gives the pitch and the roll,
65
+ * so drags and the arrows turn the heading alone, a zoom narrows about the centre, and a page's
66
+ * view sets the heading and the zoom.
67
+ */
68
+ var MOTION_LOOK_VIEW = {
69
+ isStabilized: true,
70
+ canPan: () => true,
71
+ pan: (framing, delta, { viewport }) => withView(framing, panView(framing.view, {
72
+ x: delta.x,
73
+ y: 0
74
+ }, viewport.width)),
75
+ turn: (framing, turn) => {
76
+ const { view } = framing;
77
+ return withView(framing, lookAt(view, degrees(view.yaw + turn.yaw), view.pitch));
78
+ },
79
+ zoom: (framing, zoom, { viewport }) => withView(framing, zoomViewAt(framing.view, {
80
+ steps: zoom.steps,
81
+ focus: SCREEN_CENTRE
82
+ }, aspectOf(viewport))),
83
+ reset: (framing) => withHeading(framing, {
84
+ ...framing.view,
85
+ yaw: degrees(0),
86
+ fieldOfView: DEFAULT_VIEW.fieldOfView
87
+ }),
88
+ place: withHeading,
89
+ picture: rectilinearPicture
90
+ };
91
+ /**
92
+ * The held view with the yaw and field of view of `view`, the device's pitch and roll kept.
93
+ */
94
+ function withHeading(framing, view) {
95
+ const { yaw, fieldOfView } = view;
96
+ return withView(framing, clampView({
97
+ ...framing.view,
98
+ yaw,
99
+ fieldOfView
100
+ }));
101
+ }
102
+ //#endregion
103
+ export { MOTION_LOOK_VIEW, followReading, withoutRoll };
@@ -2,7 +2,7 @@ import { degrees } from "../../shared/units/angle.js";
2
2
  import { DEFAULT_VIEW, viewRotation } from "./ViewState.js";
3
3
  import { WHOLE_SCREEN, aspectOf } from "./screenLayout.js";
4
4
  import { lookAt, panView, zoomViewAt } from "./viewGestures.js";
5
- import { withView } from "./Framing.js";
5
+ import { withPlacedView, withView } from "./Framing.js";
6
6
  //#region ../../packages/core/src/domain/view/normalView.ts
7
7
  /**
8
8
  * A rectilinear window into the stitched sphere over the whole viewport, turned by drags and
@@ -18,12 +18,19 @@ var NORMAL_VIEW = {
18
18
  },
19
19
  zoom: (framing, zoom, { viewport }) => withView(framing, zoomViewAt(framing.view, zoom, aspectOf(viewport))),
20
20
  reset: (framing) => withView(framing, DEFAULT_VIEW),
21
- picture: ({ view }) => ({
21
+ place: withPlacedView,
22
+ picture: rectilinearPicture
23
+ };
24
+ /**
25
+ * The view's rectilinear picture over the whole viewport, turned by its whole rotation.
26
+ */
27
+ function rectilinearPicture({ view }) {
28
+ return {
22
29
  kind: "rectilinear",
23
30
  rotation: viewRotation(view),
24
31
  fieldOfView: view.fieldOfView,
25
32
  area: WHOLE_SCREEN
26
- })
27
- };
33
+ };
34
+ }
28
35
  //#endregion
29
- export { NORMAL_VIEW };
36
+ export { NORMAL_VIEW, rectilinearPicture };
@@ -3,7 +3,7 @@ import { rotationAboutY } from "../../shared/math/Matrix3.js";
3
3
  import { HALF, aspectOf, fittedRectangle, pictureAt, shiftOf } from "./screenLayout.js";
4
4
  import { lookAt } from "./viewGestures.js";
5
5
  import { FITTED, clampMagnification, magnifiedArea, magnifyAt, panMagnification, pictureShiftOf } from "./magnification.js";
6
- import { withView } from "./Framing.js";
6
+ import { withPlacedView, withView } from "./Framing.js";
7
7
  //#region ../../packages/core/src/domain/view/panoramaView.ts
8
8
  /**
9
9
  * An equirectangular picture spans a full turn across for a half turn from top to bottom.
@@ -86,6 +86,7 @@ var PANORAMA_VIEW = {
86
86
  ...withView(framing, lookAt(framing.view, degrees(0), framing.view.pitch)),
87
87
  panorama: FITTED
88
88
  }),
89
+ place: withPlacedView,
89
90
  picture: (framing, context) => {
90
91
  const { fitted, panorama } = shownPanorama(framing, context);
91
92
  return {
@@ -0,0 +1,74 @@
1
+ import { degrees, degreesToRadians, radians, radiansToDegrees } from "../../shared/units/angle.js";
2
+ import { clamp } from "../../shared/math/clamp.js";
3
+ import { multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ } from "../../shared/math/Matrix3.js";
4
+ //#region ../../packages/core/src/domain/view/screenLook.ts
5
+ /**
6
+ * View space (x right, y down, z into the screen) into the screen's frame (x right, y up, z out
7
+ * of the screen): the viewer looks through the screen, out of its back.
8
+ */
9
+ var VIEW_TO_SCREEN = [
10
+ 1,
11
+ 0,
12
+ 0,
13
+ 0,
14
+ -1,
15
+ 0,
16
+ 0,
17
+ 0,
18
+ -1
19
+ ];
20
+ /**
21
+ * The browser's world (x east, y north, z up) into the view's (x right, y down, z forward), north
22
+ * standing for forward: the yaw only counts changes, so any horizontal direction would do.
23
+ */
24
+ var WORLD_TO_VIEW = [
25
+ 1,
26
+ 0,
27
+ 0,
28
+ 0,
29
+ 0,
30
+ -1,
31
+ 0,
32
+ 1,
33
+ 0
34
+ ];
35
+ /**
36
+ * Below this cosine of the pitch the screen looks straight up or down, where only the sum or the
37
+ * difference of yaw and roll is defined, and each one alone would be read from rounding noise:
38
+ * the look then takes the turn as yaw alone, within a millionth of a radian of the screen.
39
+ */
40
+ var POLE_COSINE = 1e-6;
41
+ /**
42
+ * The rotation that turns view-space directions into the view's world for a device held this
43
+ * way, as `viewRotation` turns them for a view.
44
+ */
45
+ function screenRotationOf(attitude) {
46
+ const device = multiplyMatrices(multiplyMatrices(rotationAbout(rotationAboutZ, attitude.alpha), rotationAbout(rotationAboutX, attitude.beta)), rotationAbout(rotationAboutY, attitude.gamma));
47
+ const screen = multiplyMatrices(rotationAbout(rotationAboutZ, degrees(-attitude.screenAngle)), VIEW_TO_SCREEN);
48
+ return multiplyMatrices(multiplyMatrices(WORLD_TO_VIEW, device), screen);
49
+ }
50
+ /**
51
+ * The yaw, pitch and roll whose `viewRotation` is the screen's rotation: the rotation
52
+ * `Ry(yaw) Rx(pitch) Rz(roll)` read back from its entries.
53
+ */
54
+ function screenLookOf(attitude) {
55
+ const [m00, , m02, m10, m11, m12, m20, , m22] = screenRotationOf(attitude);
56
+ const pitch = angleOf(Math.asin(-clamp(m12, -1, 1)));
57
+ return Math.hypot(m10, m11) < POLE_COSINE ? {
58
+ yaw: angleOf(Math.atan2(-m20, m00)),
59
+ pitch,
60
+ roll: degrees(0)
61
+ } : {
62
+ yaw: angleOf(Math.atan2(m02, m22)),
63
+ pitch,
64
+ roll: angleOf(Math.atan2(m10, m11))
65
+ };
66
+ }
67
+ function rotationAbout(rotation, angle) {
68
+ return rotation(degreesToRadians(angle));
69
+ }
70
+ function angleOf(value) {
71
+ return radiansToDegrees(radians(value));
72
+ }
73
+ //#endregion
74
+ export { screenLookOf, screenRotationOf };
@@ -1,5 +1,6 @@
1
1
  import { LENS_TILES_VIEW } from "./lensTilesView.js";
2
2
  import { NORMAL_VIEW } from "./normalView.js";
3
+ import { MOTION_LOOK_VIEW } from "./motionLookView.js";
3
4
  import { PANORAMA_VIEW } from "./panoramaView.js";
4
5
  //#region ../../packages/core/src/domain/view/viewModes.ts
5
6
  var RULES = {
@@ -10,5 +11,21 @@ var RULES = {
10
11
  function viewModeRulesFor(mode) {
11
12
  return RULES[mode];
12
13
  }
14
+ /**
15
+ * Motion look turns the normal view alone: the flat pictures show the whole sphere or the raw
16
+ * lenses, which a phone held up as a window has nothing to turn.
17
+ */
18
+ var MOTION_LOOK_RULES = {
19
+ normal: MOTION_LOOK_VIEW,
20
+ equirectangular: void 0,
21
+ "raw-lenses": void 0
22
+ };
23
+ /**
24
+ * The rules of a mode while the device holds the view, or none for a mode the device does not
25
+ * turn.
26
+ */
27
+ function motionLookRulesFor(mode) {
28
+ return MOTION_LOOK_RULES[mode];
29
+ }
13
30
  //#endregion
14
- export { viewModeRulesFor };
31
+ export { motionLookRulesFor, viewModeRulesFor };
@@ -33,7 +33,7 @@ import { DEFAULT_STABILIZATION_MODE, STABILIZATION_MODES } from "./domain/motion
33
33
  import { stabilizerFor } from "./domain/motion/stabilization/stabilizers.js";
34
34
  import "./domain/optics/lensPose.js";
35
35
  import "./domain/optics/gainMatch.js";
36
- import { clampView, isSameView } from "./domain/view/ViewState.js";
36
+ import "./domain/view/ViewState.js";
37
37
  import { DEFAULT_PICTURE_QUALITY, PICTURE_QUALITIES, pixelRatioCapOf } from "./domain/view/PictureQuality.js";
38
38
  import { DEFAULT_VIEW_MODE, VIEW_MODES } from "./domain/view/ViewMode.js";
39
39
  import { SCREEN_CENTRE, aspectOf } from "./domain/view/screenLayout.js";
@@ -41,7 +41,9 @@ import { aspectOfArea, planeHalfExtentOf } from "./domain/view/rectilinear.js";
41
41
  import { lookAt, zoomStepsForPinch } from "./domain/view/viewGestures.js";
42
42
  import "./domain/view/magnification.js";
43
43
  import { DEFAULT_FRAMING, isSameFraming } from "./domain/view/Framing.js";
44
- import { viewModeRulesFor } from "./domain/view/viewModes.js";
44
+ import { followReading, withoutRoll } from "./domain/view/motionLookView.js";
45
+ import { motionLookRulesFor, viewModeRulesFor } from "./domain/view/viewModes.js";
46
+ import { screenLookOf } from "./domain/view/screenLook.js";
45
47
  import "./domain/stitching/seamRing.js";
46
48
  import { buildStitchingSetup, lensFrameOrder } from "./domain/stitching/StitchingSetup.js";
47
49
  import { StabilizingFrameSink } from "./application/stabilization/StabilizingFrameSink.js";
@@ -71,4 +73,4 @@ import { WallClock } from "./application/playback/WallClock.js";
71
73
  import { isFlowing } from "./domain/playback/PlayerState.js";
72
74
  import { TypedEmitter } from "./shared/events/TypedEmitter.js";
73
75
  import { PlaybackSession } from "./application/playback/PlaybackSession.js";
74
- export { BT709_LUMINANCE, ByteRange, COLOUR_PRIMARIES, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, HLG_OETF, IDENTITY_MATRIX3, ITERATION_END, MATRIX_COEFFICIENTS, MEI_TERM_CAPACITY, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TRANSFER_CHARACTERISTICS, TypedEmitter, UPRIGHT_MOUNTING, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, displayConversionsOf, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, hevcCodecStringOf, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, matrixCorrectionOf, messageOf, microseconds, microsecondsToSeconds, milliseconds, namedOrUnspecified, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
76
+ export { BT709_LUMINANCE, ByteRange, COLOUR_PRIMARIES, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, HLG_OETF, IDENTITY_MATRIX3, ITERATION_END, MATRIX_COEFFICIENTS, MEI_TERM_CAPACITY, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TRANSFER_CHARACTERISTICS, TypedEmitter, UPRIGHT_MOUNTING, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, degrees, detectLensLayout, displayConversionsOf, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, followReading, hasErrorCode, hevcCodecStringOf, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, matrixCorrectionOf, messageOf, microseconds, microsecondsToSeconds, milliseconds, motionLookRulesFor, namedOrUnspecified, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, screenLookOf, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, withoutRoll, zoomStepsForPinch };
@@ -1,3 +1,5 @@
1
+ import { radians } from "../units/angle.js";
2
+ import { clamp } from "./clamp.js";
1
3
  //#region ../../packages/core/src/shared/math/Matrix3.ts
2
4
  var IDENTITY_MATRIX3 = [
3
5
  1,
@@ -106,5 +108,14 @@ function determinantOf(m) {
106
108
  const [m00, m01, m02, m10, m11, m12, m20, m21, m22] = m;
107
109
  return m00 * (m11 * m22 - m12 * m21) - m01 * (m10 * m22 - m12 * m20) + m02 * (m10 * m21 - m11 * m20);
108
110
  }
111
+ /**
112
+ * How far rotation `b` turns from rotation `a`: the angle of `aᵀ b`, from its trace
113
+ * (`1 + 2 cos θ`), which is the sum of the two matrices' entrywise products; clamped against
114
+ * rounding.
115
+ */
116
+ function angleBetweenRotations(a, b) {
117
+ const trace = a.reduce((sum, entry, index) => sum + entry * (b[index] ?? 0), 0);
118
+ return radians(Math.acos(clamp((trace - 1) / 2, -1, 1)));
119
+ }
109
120
  //#endregion
110
- export { IDENTITY_MATRIX3, determinantOf, multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ, transformVector, transposeMatrix };
121
+ export { IDENTITY_MATRIX3, angleBetweenRotations, determinantOf, multiplyMatrices, rotationAboutX, rotationAboutY, rotationAboutZ, transformVector, transposeMatrix };
@@ -1,17 +1,19 @@
1
+ import { BrowserAttitudeSensor } from "./composition/BrowserAttitudeSensor.js";
1
2
  import { browserPorts } from "./composition/browserPorts.js";
2
3
  import { buildPipeline } from "./composition/buildPipeline.js";
3
4
  import { Player } from "./player/Player.js";
4
5
  //#region ../../packages/player/src/browserPlayer.ts
5
6
  /**
6
- * A player composed for a browser: HTTP, blob and WebCodecs ports and the GPU pipeline, drawing
7
- * on the host's canvas and sounding through its audio element. What `<gyro-view>` plays with,
8
- * and where a page that wants the player without the element starts.
7
+ * A player composed for a browser: HTTP, blob and WebCodecs ports, the GPU pipeline and the
8
+ * device's attitude, drawing on the host's canvas and sounding through its audio element. What
9
+ * `<gyro-view>` plays with, and where a page that wants the player without the element starts.
9
10
  */
10
11
  function createBrowserPlayer(host, options) {
11
12
  return new Player({
12
13
  host,
13
14
  ports: browserPorts(options),
14
- pipelines: buildPipeline
15
+ pipelines: buildPipeline,
16
+ attitude: new BrowserAttitudeSensor()
15
17
  });
16
18
  }
17
19
  //#endregion