@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region ../../packages/player/src/player/MotionLook.ts
|
|
2
|
+
/**
|
|
3
|
+
* The motion look setting, kept across loads like the others (ADR 0016), and what of the device
|
|
4
|
+
* it hears: its availability while a recording is drawn, and its readings while motion look is
|
|
5
|
+
* also on and the view mode follows the device. Each time the readings stop, the view is let go,
|
|
6
|
+
* so it is level again everywhere else. Nothing of the page holds the player between recordings:
|
|
7
|
+
* an element taken out of the page unloads, and can be collected.
|
|
8
|
+
*/
|
|
9
|
+
var MotionLook = class {
|
|
10
|
+
parts;
|
|
11
|
+
stateValue;
|
|
12
|
+
request;
|
|
13
|
+
/**
|
|
14
|
+
* Counts the requests `stop`, `dispose` and a change of availability overtook: an answer that
|
|
15
|
+
* comes after them changes nothing.
|
|
16
|
+
*/
|
|
17
|
+
generation = 0;
|
|
18
|
+
stopHearing;
|
|
19
|
+
stopFollowingAvailability;
|
|
20
|
+
/**
|
|
21
|
+
* The viewer refused access: the device's availability changes nothing after that.
|
|
22
|
+
*/
|
|
23
|
+
wasRefused = false;
|
|
24
|
+
constructor(parts) {
|
|
25
|
+
this.parts = parts;
|
|
26
|
+
this.stateValue = parts.sensor.availability === "available" ? "off" : "unavailable";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* As last known: the device's availability is followed while a recording is drawn.
|
|
30
|
+
*/
|
|
31
|
+
get state() {
|
|
32
|
+
return this.stateValue;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Turns motion look on, asking for access first; resolves with the state it ends in and never
|
|
36
|
+
* rejects. A second call while the first asks waits for the same answer.
|
|
37
|
+
*/
|
|
38
|
+
start() {
|
|
39
|
+
this.followAvailability();
|
|
40
|
+
if (this.stateValue !== "off") return Promise.resolve(this.stateValue);
|
|
41
|
+
this.request ??= this.ask();
|
|
42
|
+
return this.request;
|
|
43
|
+
}
|
|
44
|
+
stop() {
|
|
45
|
+
this.overtakeRequest();
|
|
46
|
+
if (this.stateValue === "on") this.change("off");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The player calls it whenever a recording or the view mode may have changed.
|
|
50
|
+
*/
|
|
51
|
+
reconsider() {
|
|
52
|
+
this.reconsiderAvailability();
|
|
53
|
+
this.reconsiderHearing();
|
|
54
|
+
}
|
|
55
|
+
dispose() {
|
|
56
|
+
this.overtakeRequest();
|
|
57
|
+
this.stopFollowingAvailability?.();
|
|
58
|
+
this.stopFollowingAvailability = void 0;
|
|
59
|
+
this.stopHearing?.();
|
|
60
|
+
this.stopHearing = void 0;
|
|
61
|
+
this.parts.sensor.dispose();
|
|
62
|
+
}
|
|
63
|
+
reconsiderAvailability() {
|
|
64
|
+
const isDrawn = this.parts.hasRecording();
|
|
65
|
+
if (isDrawn && !this.stopFollowingAvailability) {
|
|
66
|
+
this.stopFollowingAvailability = this.parts.sensor.onAvailabilityChange(() => {
|
|
67
|
+
this.followAvailability();
|
|
68
|
+
});
|
|
69
|
+
this.followAvailability();
|
|
70
|
+
} else if (!isDrawn && this.stopFollowingAvailability) {
|
|
71
|
+
this.stopFollowingAvailability();
|
|
72
|
+
this.stopFollowingAvailability = void 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hears the device exactly while motion look is on, a recording is drawn and the view mode
|
|
77
|
+
* follows the device; lets the view go whenever it stops.
|
|
78
|
+
*/
|
|
79
|
+
reconsiderHearing() {
|
|
80
|
+
const { sensor, view, hasRecording } = this.parts;
|
|
81
|
+
const shouldHear = this.stateValue === "on" && hasRecording() && view.followsDevice;
|
|
82
|
+
if (shouldHear && !this.stopHearing) this.stopHearing = sensor.listen((reading) => {
|
|
83
|
+
view.followDevice(reading);
|
|
84
|
+
});
|
|
85
|
+
else if (!shouldHear && this.stopHearing) {
|
|
86
|
+
this.stopHearing();
|
|
87
|
+
this.stopHearing = void 0;
|
|
88
|
+
view.letGo();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A device found to report its attitude after all may be turned on; one found not to, which
|
|
93
|
+
* may happen after it was turned on, can no longer be.
|
|
94
|
+
*/
|
|
95
|
+
followAvailability() {
|
|
96
|
+
const isAvailable = this.parts.sensor.availability === "available";
|
|
97
|
+
if (this.wasRefused || isAvailable === (this.stateValue !== "unavailable")) return;
|
|
98
|
+
this.overtakeRequest();
|
|
99
|
+
this.change(isAvailable ? "off" : "unavailable");
|
|
100
|
+
}
|
|
101
|
+
async ask() {
|
|
102
|
+
const generation = this.generation;
|
|
103
|
+
const access = await this.accessOrGesture();
|
|
104
|
+
if (generation !== this.generation) return this.stateValue;
|
|
105
|
+
this.request = void 0;
|
|
106
|
+
this.answer(access);
|
|
107
|
+
return this.stateValue;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A sensor's request never rejects; one that did is taken as wanting a gesture, so motion look
|
|
111
|
+
* may still be started from the next tap.
|
|
112
|
+
*/
|
|
113
|
+
async accessOrGesture() {
|
|
114
|
+
try {
|
|
115
|
+
return await this.parts.sensor.requestAccess();
|
|
116
|
+
} catch {
|
|
117
|
+
return "needs-gesture";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
answer(access) {
|
|
121
|
+
if (access === "granted") this.change("on");
|
|
122
|
+
else if (access === "denied") {
|
|
123
|
+
this.wasRefused = true;
|
|
124
|
+
this.change("unavailable");
|
|
125
|
+
this.warn("motion-look-refused", REFUSED);
|
|
126
|
+
} else this.warn("motion-look-needs-gesture", NEEDS_GESTURE);
|
|
127
|
+
}
|
|
128
|
+
overtakeRequest() {
|
|
129
|
+
this.generation += 1;
|
|
130
|
+
this.request = void 0;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Announced once the change is whole (ADR 0021): the device heard, or let go, first.
|
|
134
|
+
*/
|
|
135
|
+
change(state) {
|
|
136
|
+
this.stateValue = state;
|
|
137
|
+
this.reconsiderHearing();
|
|
138
|
+
this.parts.events.emit("motionlookchange", state);
|
|
139
|
+
}
|
|
140
|
+
warn(code, message) {
|
|
141
|
+
this.parts.events.emit("warning", {
|
|
142
|
+
code,
|
|
143
|
+
message
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var REFUSED = "motion look was refused: by the viewer, or by an iframe whose `allow` leaves out accelerometer, gyroscope and magnetometer";
|
|
148
|
+
var NEEDS_GESTURE = "motion look asks for access, which this browser grants only from a user gesture such as a tap";
|
|
149
|
+
//#endregion
|
|
150
|
+
export { MotionLook };
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { degrees } from "../../../core/src/shared/units/angle.js";
|
|
2
|
-
import { asGyroViewError
|
|
2
|
+
import { asGyroViewError } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
3
3
|
import { seconds } from "../../../core/src/shared/units/time.js";
|
|
4
4
|
import { Outbox } from "../../../core/src/shared/events/Outbox.js";
|
|
5
5
|
import { isFlowing } from "../../../core/src/domain/playback/PlayerState.js";
|
|
6
6
|
import { TypedEmitter } from "../../../core/src/shared/events/TypedEmitter.js";
|
|
7
7
|
import "../../../core/src/index.js";
|
|
8
8
|
import { ensureFinite, viewStateOf } from "./ensureFinite.js";
|
|
9
|
+
import { NO_ATTITUDE_SENSOR } from "../composition/attitudeSensor.js";
|
|
10
|
+
import { AutomaticStarts } from "./AutomaticStarts.js";
|
|
9
11
|
import { cssSizeOf } from "./DrawingBufferFit.js";
|
|
10
12
|
import { FrameLoop } from "./FrameLoop.js";
|
|
11
13
|
import { loadRecording } from "./loadRecording.js";
|
|
14
|
+
import { MotionLook } from "./MotionLook.js";
|
|
12
15
|
import { IDLE, loadingPhase, statusOf } from "./PlayerPhase.js";
|
|
13
16
|
import { PictureSettings } from "./PictureSettings.js";
|
|
14
17
|
import { PlayerSound } from "./PlayerSound.js";
|
|
18
|
+
import { anglesOf } from "./viewAngles.js";
|
|
15
19
|
import { PlayerView } from "./PlayerView.js";
|
|
16
20
|
import { SessionRelay } from "./SessionRelay.js";
|
|
17
21
|
//#region ../../packages/player/src/player/Player.ts
|
|
18
22
|
/**
|
|
19
23
|
* The player without the element, which `createBrowserPlayer` gives: it plays one recording at a
|
|
20
24
|
* time on the canvas and audio element it was handed, reports in media-element events, and keeps
|
|
21
|
-
* its settings (view, view mode, stabilization, gain matching, sound, loop) from
|
|
22
|
-
* `<gyro-view>` is a facade over it.
|
|
25
|
+
* its settings (view, view mode, motion look, stabilization, gain matching, sound, loop) from
|
|
26
|
+
* load to load. `<gyro-view>` is a facade over it.
|
|
23
27
|
*/
|
|
24
28
|
var Player = class {
|
|
25
29
|
parts;
|
|
@@ -31,6 +35,7 @@ var Player = class {
|
|
|
31
35
|
loop;
|
|
32
36
|
phase = IDLE;
|
|
33
37
|
viewing;
|
|
38
|
+
motion;
|
|
34
39
|
picture = new PictureSettings(this.outbox);
|
|
35
40
|
/**
|
|
36
41
|
* A seek asked for before a recording was ready, where the next one starts.
|
|
@@ -43,10 +48,10 @@ var Player = class {
|
|
|
43
48
|
this.announceStatus();
|
|
44
49
|
},
|
|
45
50
|
onEnded: () => {
|
|
46
|
-
this.
|
|
51
|
+
this.starts.ended();
|
|
47
52
|
}
|
|
48
53
|
});
|
|
49
|
-
|
|
54
|
+
starts = new AutomaticStarts(this.outbox, () => this.play());
|
|
50
55
|
/**
|
|
51
56
|
* Past `dispose`: a listener of the idle it announces must not load again.
|
|
52
57
|
*/
|
|
@@ -65,6 +70,12 @@ var Player = class {
|
|
|
65
70
|
this.sound = new PlayerSound(parts.host.audio, this.outbox);
|
|
66
71
|
const { canvas } = parts.host;
|
|
67
72
|
this.viewing = new PlayerView(this.outbox, () => cssSizeOf(canvas));
|
|
73
|
+
this.motion = new MotionLook({
|
|
74
|
+
sensor: parts.attitude ?? NO_ATTITUDE_SENSOR,
|
|
75
|
+
view: this.viewing,
|
|
76
|
+
events: this.outbox,
|
|
77
|
+
hasRecording: () => this.loaded !== void 0
|
|
78
|
+
});
|
|
68
79
|
}
|
|
69
80
|
get status() {
|
|
70
81
|
return statusOf(this.phase);
|
|
@@ -85,7 +96,7 @@ var Player = class {
|
|
|
85
96
|
return !isFlowing(this.loaded?.pipeline.session.state);
|
|
86
97
|
}
|
|
87
98
|
get view() {
|
|
88
|
-
return this.viewing.current;
|
|
99
|
+
return anglesOf(this.viewing.current);
|
|
89
100
|
}
|
|
90
101
|
get viewMode() {
|
|
91
102
|
return this.viewing.viewMode;
|
|
@@ -96,6 +107,13 @@ var Player = class {
|
|
|
96
107
|
get canPan() {
|
|
97
108
|
return this.viewing.canPan;
|
|
98
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether turning the device turns the normal view: `unavailable` where the device reports no
|
|
112
|
+
* attitude or access was refused.
|
|
113
|
+
*/
|
|
114
|
+
get motionLook() {
|
|
115
|
+
return this.motion.state;
|
|
116
|
+
}
|
|
99
117
|
get stabilization() {
|
|
100
118
|
return this.picture.stabilization;
|
|
101
119
|
}
|
|
@@ -103,7 +121,7 @@ var Player = class {
|
|
|
103
121
|
return this.picture.quality;
|
|
104
122
|
}
|
|
105
123
|
get isLooping() {
|
|
106
|
-
return this.
|
|
124
|
+
return this.starts.isLooping;
|
|
107
125
|
}
|
|
108
126
|
get volume() {
|
|
109
127
|
return this.sound.volume;
|
|
@@ -219,6 +237,22 @@ var Player = class {
|
|
|
219
237
|
}
|
|
220
238
|
setViewMode(mode) {
|
|
221
239
|
this.viewing.setMode(mode);
|
|
240
|
+
this.motion.reconsider();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Lets the device turn the normal view, as a window into the recording: its tilt and roll
|
|
244
|
+
* hold the pitch and keep the horizon level, drags turn the heading alone. Call it from a tap's
|
|
245
|
+
* handler: iOS asks the viewer for access there and refuses it anywhere else. Resolves with the
|
|
246
|
+
* state it ends in; a refusal is a warning, never a rejection.
|
|
247
|
+
*/
|
|
248
|
+
startMotionLook() {
|
|
249
|
+
return this.motion.start();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Gives the view back to the pointer, level, looking where it looked.
|
|
253
|
+
*/
|
|
254
|
+
stopMotionLook() {
|
|
255
|
+
this.motion.stop();
|
|
222
256
|
}
|
|
223
257
|
setStabilization(mode) {
|
|
224
258
|
this.picture.setStabilization(mode);
|
|
@@ -230,7 +264,7 @@ var Player = class {
|
|
|
230
264
|
this.picture.setQuality(quality);
|
|
231
265
|
}
|
|
232
266
|
setLooping(isLooping) {
|
|
233
|
-
this.
|
|
267
|
+
this.starts.setLooping(isLooping);
|
|
234
268
|
}
|
|
235
269
|
/**
|
|
236
270
|
* Whether the lenses' exposure is matched along the seam; kept across loads.
|
|
@@ -248,6 +282,7 @@ var Player = class {
|
|
|
248
282
|
dispose() {
|
|
249
283
|
this.isDisposed = true;
|
|
250
284
|
this.unload();
|
|
285
|
+
this.motion.dispose();
|
|
251
286
|
this.parts.host.audio.removeEventListener("ended", this.tick);
|
|
252
287
|
this.sound.dispose();
|
|
253
288
|
this.events.removeAll();
|
|
@@ -280,7 +315,7 @@ var Player = class {
|
|
|
280
315
|
this.startAtPendingTime(loaded);
|
|
281
316
|
if (options.preload !== false) loaded.pipeline.session.preload();
|
|
282
317
|
});
|
|
283
|
-
if (options.autoplay && this.loaded === loaded) await this.autoplay();
|
|
318
|
+
if (options.autoplay && this.loaded === loaded) await this.starts.autoplay();
|
|
284
319
|
}
|
|
285
320
|
startAtPendingTime(loaded) {
|
|
286
321
|
const time = this.pendingStartTime;
|
|
@@ -294,6 +329,7 @@ var Player = class {
|
|
|
294
329
|
controller
|
|
295
330
|
};
|
|
296
331
|
this.viewing.attach(loaded.pipeline.renderer);
|
|
332
|
+
this.motion.reconsider();
|
|
297
333
|
const { session } = loaded.pipeline;
|
|
298
334
|
this.picture.attach(loaded.pipeline);
|
|
299
335
|
this.relay.attach(session);
|
|
@@ -303,36 +339,6 @@ var Player = class {
|
|
|
303
339
|
this.outbox.emit("ready", loaded.opened.metadata);
|
|
304
340
|
}
|
|
305
341
|
/**
|
|
306
|
-
* A refused start leaves the recording loaded and paused: a warning, never a failed load.
|
|
307
|
-
*/
|
|
308
|
-
async autoplay() {
|
|
309
|
-
try {
|
|
310
|
-
await this.play();
|
|
311
|
-
} catch (error) {
|
|
312
|
-
this.outbox.emit("warning", hasErrorCode(error, "playback-blocked") ? {
|
|
313
|
-
code: "autoplay-blocked",
|
|
314
|
-
message: "playback waits for a user gesture"
|
|
315
|
-
} : {
|
|
316
|
-
code: "playback-failed",
|
|
317
|
-
message: `autoplay failed: ${messageOf(error)}`
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
onEnded() {
|
|
322
|
-
this.outbox.emit("ended", void 0);
|
|
323
|
-
if (this.isLoopingValue) this.replay();
|
|
324
|
-
}
|
|
325
|
-
async replay() {
|
|
326
|
-
try {
|
|
327
|
-
await this.play();
|
|
328
|
-
} catch (error) {
|
|
329
|
-
this.outbox.emit("warning", {
|
|
330
|
-
code: "playback-failed",
|
|
331
|
-
message: `the loop could not restart playback: ${messageOf(error)}`
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
342
|
* Lets go of whatever is loading or loaded, announcing nothing.
|
|
337
343
|
*/
|
|
338
344
|
release() {
|
|
@@ -342,6 +348,7 @@ var Player = class {
|
|
|
342
348
|
this.loop.stop();
|
|
343
349
|
this.relay.detach();
|
|
344
350
|
this.viewing.attach(void 0);
|
|
351
|
+
this.motion.reconsider();
|
|
345
352
|
this.picture.attach(void 0);
|
|
346
353
|
if (previous.kind !== "loaded") return;
|
|
347
354
|
previous.loaded.dispose();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { clampView, isSameView } from "../../../core/src/domain/view/ViewState.js";
|
|
2
1
|
import { DEFAULT_VIEW_MODE } from "../../../core/src/domain/view/ViewMode.js";
|
|
3
2
|
import { SCREEN_CENTRE } from "../../../core/src/domain/view/screenLayout.js";
|
|
4
3
|
import { lookAt } from "../../../core/src/domain/view/viewGestures.js";
|
|
5
4
|
import { DEFAULT_FRAMING, isSameFraming } from "../../../core/src/domain/view/Framing.js";
|
|
6
|
-
import {
|
|
5
|
+
import { followReading, withoutRoll } from "../../../core/src/domain/view/motionLookView.js";
|
|
6
|
+
import { motionLookRulesFor, viewModeRulesFor } from "../../../core/src/domain/view/viewModes.js";
|
|
7
7
|
import "../../../core/src/index.js";
|
|
8
|
+
import { anglesOf, isSameAngles } from "./viewAngles.js";
|
|
8
9
|
//#region ../../packages/player/src/player/PlayerView.ts
|
|
9
10
|
/**
|
|
10
11
|
* Every layout the player accepts has two lenses; a renderer attached says so itself.
|
|
@@ -12,9 +13,10 @@ import "../../../core/src/index.js";
|
|
|
12
13
|
var LENS_COUNT_BEFORE_A_LOAD = 2;
|
|
13
14
|
/**
|
|
14
15
|
* How the picture is framed and which view mode shows it: kept across loads, drawn by whichever
|
|
15
|
-
* renderer is attached, and the normal view announced when
|
|
16
|
-
* through the mode's rules on the viewport as it is
|
|
17
|
-
*
|
|
16
|
+
* renderer is attached, and the normal view's angles announced when they change. The viewer's
|
|
17
|
+
* gestures and a host setting the view go through the mode's rules on the viewport as it is
|
|
18
|
+
* now; while the device holds the view (motion look, ADR 0040), through the rules it follows
|
|
19
|
+
* then.
|
|
18
20
|
*/
|
|
19
21
|
var PlayerView = class {
|
|
20
22
|
events;
|
|
@@ -23,6 +25,10 @@ var PlayerView = class {
|
|
|
23
25
|
mode = DEFAULT_VIEW_MODE;
|
|
24
26
|
surface;
|
|
25
27
|
lensCount = LENS_COUNT_BEFORE_A_LOAD;
|
|
28
|
+
/**
|
|
29
|
+
* The device reading the held view follows from; none while the device does not hold it.
|
|
30
|
+
*/
|
|
31
|
+
previous;
|
|
26
32
|
constructor(events, measureViewport) {
|
|
27
33
|
this.events = events;
|
|
28
34
|
this.measureViewport = measureViewport;
|
|
@@ -40,6 +46,12 @@ var PlayerView = class {
|
|
|
40
46
|
return this.rules().canPan(this.framing);
|
|
41
47
|
}
|
|
42
48
|
/**
|
|
49
|
+
* Whether the device may turn the view in the current mode.
|
|
50
|
+
*/
|
|
51
|
+
get followsDevice() {
|
|
52
|
+
return motionLookRulesFor(this.mode) !== void 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
43
55
|
* The renderer of the loaded recording, or nothing between loads. A new renderer draws the
|
|
44
56
|
* framing as it is now, however it changed while the recording was loading; the lens tiles are
|
|
45
57
|
* measured by the lenses it draws from then on.
|
|
@@ -51,10 +63,7 @@ var PlayerView = class {
|
|
|
51
63
|
surface?.setFraming(this.framing);
|
|
52
64
|
}
|
|
53
65
|
set(view) {
|
|
54
|
-
this.frame(
|
|
55
|
-
...this.framing,
|
|
56
|
-
view: clampView(view)
|
|
57
|
-
});
|
|
66
|
+
this.frame(this.rules().place(this.framing, view));
|
|
58
67
|
}
|
|
59
68
|
setMode(mode) {
|
|
60
69
|
if (mode === this.mode) return;
|
|
@@ -63,9 +72,28 @@ var PlayerView = class {
|
|
|
63
72
|
this.events.emit("viewmodechange", mode);
|
|
64
73
|
}
|
|
65
74
|
lookAt(yaw, pitch) {
|
|
75
|
+
this.frame(this.rules().place(this.framing, lookAt(this.framing.view, yaw, pitch)));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turns the view as the device turned: from its first reading on, the device holds the view.
|
|
79
|
+
*/
|
|
80
|
+
followDevice(reading) {
|
|
81
|
+
const hold = followReading(this.framing.view, this.previous, reading);
|
|
82
|
+
this.previous = hold.reading;
|
|
66
83
|
this.frame({
|
|
67
84
|
...this.framing,
|
|
68
|
-
view:
|
|
85
|
+
view: hold.view
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The device no longer holds the view: it stays where it looks, level again.
|
|
90
|
+
*/
|
|
91
|
+
letGo() {
|
|
92
|
+
if (!this.previous) return;
|
|
93
|
+
this.previous = void 0;
|
|
94
|
+
this.frame({
|
|
95
|
+
...this.framing,
|
|
96
|
+
view: withoutRoll(this.framing.view)
|
|
69
97
|
});
|
|
70
98
|
}
|
|
71
99
|
pan(delta) {
|
|
@@ -92,12 +120,15 @@ var PlayerView = class {
|
|
|
92
120
|
reset() {
|
|
93
121
|
this.frame(this.rules().reset(this.framing));
|
|
94
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Draws any change; announces one of the angles a page sees, not a roll alone.
|
|
125
|
+
*/
|
|
95
126
|
frame(next) {
|
|
96
127
|
if (isSameFraming(next, this.framing)) return;
|
|
97
|
-
const
|
|
128
|
+
const haveAnglesChanged = !isSameAngles(next.view, this.framing.view);
|
|
98
129
|
this.framing = next;
|
|
99
130
|
this.surface?.setFraming(next);
|
|
100
|
-
if (
|
|
131
|
+
if (haveAnglesChanged) this.events.emit("viewchange", anglesOf(next.view));
|
|
101
132
|
}
|
|
102
133
|
context() {
|
|
103
134
|
return {
|
|
@@ -106,7 +137,7 @@ var PlayerView = class {
|
|
|
106
137
|
};
|
|
107
138
|
}
|
|
108
139
|
rules() {
|
|
109
|
-
return viewModeRulesFor(this.mode);
|
|
140
|
+
return (this.previous === void 0 ? void 0 : motionLookRulesFor(this.mode)) ?? viewModeRulesFor(this.mode);
|
|
110
141
|
}
|
|
111
142
|
};
|
|
112
143
|
//#endregion
|
|
@@ -10,7 +10,7 @@ function ensureFinite(value, name) {
|
|
|
10
10
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new GyroViewError("invalid-argument", `${name} must be a finite number`);
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* A view from the page, each angle checked and read as degrees.
|
|
13
|
+
* A view from the page, each angle checked and read as degrees, level.
|
|
14
14
|
*/
|
|
15
15
|
function viewStateOf(view) {
|
|
16
16
|
ensureFinite(view.yaw, "yaw");
|
|
@@ -19,6 +19,7 @@ function viewStateOf(view) {
|
|
|
19
19
|
return {
|
|
20
20
|
yaw: degrees(view.yaw),
|
|
21
21
|
pitch: degrees(view.pitch),
|
|
22
|
+
roll: degrees(0),
|
|
22
23
|
fieldOfView: degrees(view.fieldOfView)
|
|
23
24
|
};
|
|
24
25
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../../packages/player/src/player/viewAngles.ts
|
|
2
|
+
/**
|
|
3
|
+
* The view as a page sees it: a fresh object of the three angles, the roll the device gives the
|
|
4
|
+
* held view kept inside the player (ADR 0040).
|
|
5
|
+
*/
|
|
6
|
+
function anglesOf(view) {
|
|
7
|
+
return {
|
|
8
|
+
yaw: view.yaw,
|
|
9
|
+
pitch: view.pitch,
|
|
10
|
+
fieldOfView: view.fieldOfView
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function isSameAngles(a, b) {
|
|
14
|
+
return a.yaw === b.yaw && a.pitch === b.pitch && a.fieldOfView === b.fieldOfView;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { anglesOf, isSameAngles };
|