@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
@@ -1,25 +1,29 @@
1
1
  import { degrees } from "../../../core/src/shared/units/angle.js";
2
- import { asGyroViewError, hasErrorCode, messageOf } from "../../../core/src/shared/errors/GyroViewError.js";
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 load to load.
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.onEnded();
51
+ this.starts.ended();
47
52
  }
48
53
  });
49
- isLoopingValue = false;
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.isLoopingValue;
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.isLoopingValue = isLooping;
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();
@@ -18,6 +18,7 @@ var PLAYER_EVENT_NAMES = keysOf({
18
18
  frame: true,
19
19
  viewchange: true,
20
20
  viewmodechange: true,
21
+ motionlookchange: true,
21
22
  stabilizationchange: true,
22
23
  qualitychange: true,
23
24
  volumechange: true,
@@ -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 { viewModeRulesFor } from "../../../core/src/domain/view/viewModes.js";
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 it changes. The viewer's gestures go
16
- * through the mode's rules on the viewport as it is now; a host setting the view directly is
17
- * obeyed as it is.
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: lookAt(this.framing.view, yaw, pitch)
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 hasViewChanged = !isSameView(next.view, this.framing.view);
128
+ const haveAnglesChanged = !isSameAngles(next.view, this.framing.view);
98
129
  this.framing = next;
99
130
  this.surface?.setFraming(next);
100
- if (hasViewChanged) this.events.emit("viewchange", next.view);
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 };