@bubo-squared/gyroview 0.5.0 → 0.6.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 +47 -0
- package/README.md +2 -2
- package/dist/index.d.ts +62 -15
- package/dist/packages/core/src/domain/view/Framing.js +8 -2
- package/dist/packages/core/src/domain/view/ViewState.js +13 -8
- package/dist/packages/core/src/domain/view/lensTilesView.js +2 -1
- package/dist/packages/core/src/domain/view/motionLookView.js +103 -0
- package/dist/packages/core/src/domain/view/normalView.js +12 -5
- package/dist/packages/core/src/domain/view/panoramaView.js +2 -1
- package/dist/packages/core/src/domain/view/screenLook.js +74 -0
- package/dist/packages/core/src/domain/view/viewModes.js +18 -1
- package/dist/packages/core/src/index.js +5 -3
- package/dist/packages/core/src/shared/math/Matrix3.js +12 -1
- package/dist/packages/player/src/browserPlayer.js +6 -4
- package/dist/packages/player/src/composition/BrowserAttitudeSensor.js +192 -0
- package/dist/packages/player/src/composition/attitudeSensor.js +18 -0
- package/dist/packages/player/src/controls/MotionLookButton.js +35 -0
- package/dist/packages/player/src/controls/controlParts.js +1 -0
- package/dist/packages/player/src/controls/controls.js +1 -1
- package/dist/packages/player/src/controls/controlsBar.js +5 -3
- package/dist/packages/player/src/controls/controlsMarkup.js +1 -0
- package/dist/packages/player/src/controls/icons.js +1 -0
- package/dist/packages/player/src/controls/messages.js +1 -0
- package/dist/packages/player/src/element/ElementLoads.js +4 -7
- package/dist/packages/player/src/element/FullscreenToggle.js +35 -0
- package/dist/packages/player/src/element/GyroViewElement.js +34 -1
- package/dist/packages/player/src/element/styles.js +1 -1
- package/dist/packages/player/src/player/AutomaticStarts.js +54 -0
- package/dist/packages/player/src/player/MotionLook.js +150 -0
- package/dist/packages/player/src/player/Player.js +46 -39
- package/dist/packages/player/src/player/PlayerEvents.js +1 -0
- package/dist/packages/player/src/player/PlayerView.js +44 -13
- package/dist/packages/player/src/player/ensureFinite.js +2 -1
- package/dist/packages/player/src/player/viewAngles.js +17 -0
- package/dist/standalone.js +129 -62
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,53 @@
|
|
|
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.1 (2026-10-05)
|
|
7
|
+
|
|
8
|
+
Fixed:
|
|
9
|
+
|
|
10
|
+
- Where the browser has no fullscreen for the element (iPhone Safari), the pinned player covers
|
|
11
|
+
the whole viewport from inside a modal, a carousel or any container with a transform, a filter
|
|
12
|
+
or containment: it is shown in the top layer, as the browser's own fullscreen is (ADR 0041).
|
|
13
|
+
It filled only that container, and the page showed around it. It also stays above the page's
|
|
14
|
+
own layers, whatever their `z-index`, and keeps none of the page's corners, `translate`,
|
|
15
|
+
`rotate` or `scale` for it.
|
|
16
|
+
|
|
17
|
+
What a page may notice:
|
|
18
|
+
|
|
19
|
+
- While pinned, the element has `popover="manual"` beside `data-fill`; a page never sets the
|
|
20
|
+
attribute itself. A popover or dialog the page opens meanwhile shows above it, as over the
|
|
21
|
+
browser's fullscreen. Where the browser has no popovers of its own (Safari before 17), the
|
|
22
|
+
element is pinned in place as before.
|
|
23
|
+
- While pinned, the element's background, colour, font and overflow are its own: a page's styles
|
|
24
|
+
for every `[popover]`, and the page's own `background`, `color` or `font-family` on
|
|
25
|
+
`gyro-view`, no longer reach it; the `--gyro-view-*` properties still theme it. A page's
|
|
26
|
+
shadow or opacity for every popover would.
|
|
27
|
+
- An element the page removes while pinned leaves the fill, as one in the browser's fullscreen
|
|
28
|
+
does. One it moves within the same task, taken out and put back before a microtask runs, stays
|
|
29
|
+
pinned.
|
|
30
|
+
|
|
31
|
+
## 0.6.0 (2026-10-05)
|
|
32
|
+
|
|
33
|
+
New:
|
|
34
|
+
|
|
35
|
+
- Motion look: on a phone or tablet, a toggle in the normal view lets the device turn the view, the
|
|
36
|
+
screen a window into the recording with its horizon level with the real one (ADR 0040). The
|
|
37
|
+
element and the player have `motionLook` (`on`, `off` or `unavailable`), `startMotionLook()`,
|
|
38
|
+
which iOS honours only from a tap's handler, and `stopMotionLook()`; `motionlookchange` reports
|
|
39
|
+
it, and the warnings `motion-look-refused` and `motion-look-needs-gesture` say why it did not
|
|
40
|
+
start.
|
|
41
|
+
|
|
42
|
+
What a page may notice:
|
|
43
|
+
|
|
44
|
+
- `PlayerEvents`, `GyroViewElementEventMap` and `WarningCode` have the new event and codes; a
|
|
45
|
+
record keyed on every event or code needs them.
|
|
46
|
+
- `view` and `viewchange` give a fresh object of `yaw`, `pitch` and `fieldOfView` each time.
|
|
47
|
+
- The iframe the snippet creates allows `accelerometer; gyroscope; magnetometer`; a hand-written
|
|
48
|
+
iframe needs them for motion look. Without them, Chromium offers no toggle; iOS offers it and
|
|
49
|
+
refuses at the first press (`motion-look-refused`).
|
|
50
|
+
- On the narrowest players with the motion look toggle shown, the Stabilization menu gives way
|
|
51
|
+
to it.
|
|
52
|
+
|
|
6
53
|
## 0.5.0 (2026-10-03)
|
|
7
54
|
|
|
8
55
|
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.
|
|
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.
|
|
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
|
|
422
|
-
* sound, loop) belong to the player and carry over from load to load; set them on the
|
|
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
|
|
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
|
|
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
|
|
796
|
-
* on the host's canvas and sounding through its audio element. What
|
|
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.
|
|
@@ -979,6 +1020,12 @@ export declare class GyroViewElement extends TypedEventElement implements LiveSe
|
|
|
979
1020
|
* Plays local files instead of the `src` attributes, until `src` or `src2` change.
|
|
980
1021
|
*/
|
|
981
1022
|
loadFiles(files: FileSource): void;
|
|
1023
|
+
/**
|
|
1024
|
+
* A move within the document disconnects and connects again at once: only an element still out
|
|
1025
|
+
* of it a microtask later was removed. It lets its recording go, and leaves the pinned fill as
|
|
1026
|
+
* the browser's fullscreen element leaves fullscreen.
|
|
1027
|
+
*/
|
|
1028
|
+
private leaveIfRemoved;
|
|
982
1029
|
/**
|
|
983
1030
|
* What the controls and the keyboard ask of the element. They live as long as the element:
|
|
984
1031
|
* their listeners sit on its own shadow tree and host, and on its player.
|
|
@@ -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,
|
|
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:
|
|
41
|
-
*
|
|
42
|
-
* rotation about x tilts the forward direction
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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 };
|