@bubo-squared/gyroview 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +7 -2
  3. package/dist/index.d.ts +23 -2
  4. package/dist/packages/adapters/fetch/src/httpRequest.js +10 -1
  5. package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
  6. package/dist/packages/core/src/domain/view/lensTilesView.js +2 -2
  7. package/dist/packages/core/src/domain/view/magnification.js +1 -1
  8. package/dist/packages/core/src/domain/view/panoramaView.js +1 -1
  9. package/dist/packages/player/src/composition/browserPorts.js +16 -2
  10. package/dist/packages/player/src/composition/openInputs.js +2 -4
  11. package/dist/packages/player/src/composition/openRecording.js +13 -2
  12. package/dist/packages/player/src/controls/ChoiceMenu.js +16 -4
  13. package/dist/packages/player/src/controls/PictureMenus.js +22 -8
  14. package/dist/packages/player/src/controls/SeekBar.js +6 -3
  15. package/dist/packages/player/src/controls/SoundControls.js +2 -0
  16. package/dist/packages/player/src/controls/TransportButtons.js +2 -5
  17. package/dist/packages/player/src/controls/Wording.js +11 -9
  18. package/dist/packages/player/src/controls/controlParts.js +5 -3
  19. package/dist/packages/player/src/controls/controls.js +1 -1
  20. package/dist/packages/player/src/controls/controlsBar.js +4 -1
  21. package/dist/packages/player/src/controls/controlsMarkup.js +36 -21
  22. package/dist/packages/player/src/controls/icons.js +32 -8
  23. package/dist/packages/player/src/controls/messages.js +26 -4
  24. package/dist/packages/player/src/controls/sliderFill.js +16 -0
  25. package/dist/packages/player/src/element/ElementLoads.js +7 -0
  26. package/dist/packages/player/src/element/GyroViewElement.js +8 -3
  27. package/dist/packages/player/src/element/attributeNames.js +12 -1
  28. package/dist/packages/player/src/element/attributes.js +29 -7
  29. package/dist/packages/player/src/element/reflectedProperties.js +19 -1
  30. package/dist/packages/player/src/element/styles.js +1 -1
  31. package/dist/standalone.js +385 -126
  32. package/package.json +1 -1
@@ -2,7 +2,8 @@ import { STABILIZATION_MODES } from "../../../core/src/domain/motion/stabilizati
2
2
  import { VIEW_MODES } from "../../../core/src/domain/view/ViewMode.js";
3
3
  import "../../../core/src/index.js";
4
4
  import controls_default from "./controls.js";
5
- import { ICONS } from "./icons.js";
5
+ import { ICONS, STABILIZATION_ICONS, VIEW_MODE_ICONS } from "./icons.js";
6
+ import { STABILIZATION_WORDS, VIEW_MODE_WORDS } from "./messages.js";
6
7
  //#region ../../packages/player/src/controls/controlsMarkup.ts
7
8
  /**
8
9
  * The bar's choice menus, by the name their parts are found under.
@@ -20,20 +21,35 @@ function choiceMenuClasses(name) {
20
21
  popup: `${name}-menu`
21
22
  };
22
23
  }
23
- function menuItemsOf(choices) {
24
- return choices.map((choice) => `<button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-choice="${choice}"></button>`).join("");
24
+ /**
25
+ * Each choice names itself in `data-choice` and shows its icon, and the words below it fill in
26
+ * from the menu's tables: its name labels it for assistive technology and its description
27
+ * describes it. Ids need be unique only within one element's shadow tree.
28
+ */
29
+ function menuItemsOf(menu) {
30
+ return menu.choices.map((choice) => {
31
+ const id = `${menu.name}-${choice}`;
32
+ return `<button type="button" role="menuitemradio" aria-checked="false" tabindex="-1" data-choice="${choice}" aria-labelledby="${id}-name" aria-describedby="${id}-description"><span class="choice-icon">${menu.icons[choice] ?? ""}</span><span class="choice-name" id="${id}-name" data-choice-text="${menu.words.names}"></span><span class="choice-description" id="${id}-description" data-choice-text="${menu.words.descriptions}"></span></button>`;
33
+ }).join("");
25
34
  }
26
35
  /**
27
- * A button opening a popup of choices; the popup's title repeats the button's label for sight.
28
- * The words are the wording's to fill in.
36
+ * A setting button opening a popup of choices. The button shows the icon of the choice in effect,
37
+ * which its `data-choice` names, and is named by words it never shows: what it sets and the
38
+ * choice's name, which hidden elements still give through `aria-labelledby`. The popup's title
39
+ * repeats what the button sets for sight; its close button serves the popup shown over the whole
40
+ * player.
29
41
  */
30
42
  function choiceMenuOf(menu) {
31
43
  const classes = choiceMenuClasses(menu.name);
44
+ const [label, value] = [`${menu.name}-label`, `${menu.name}-value`];
32
45
  return `<div class="choice">
33
- <button class="${classes.button}" type="button" data-label="${menu.title}" aria-haspopup="menu" aria-expanded="false">${menu.icon}</button>
46
+ <button class="icon-button setting ${classes.button}" type="button" aria-haspopup="menu" aria-expanded="false" aria-labelledby="${label} ${value}"><span class="setting-icon"></span><span hidden><span id="${label}" data-text="${menu.title}"></span> <span id="${value}" data-choice-text="${menu.words.names}"></span></span></button>
34
47
  <div class="popup ${classes.popup}" hidden>
35
- <p class="popup-title" aria-hidden="true" data-text="${menu.title}"></p>
36
- <div role="menu" data-label="${menu.title}" data-choices="${menu.choices}">${menu.items}</div>
48
+ <div class="popup-header">
49
+ <p class="popup-title" aria-hidden="true" data-text="${menu.title}"></p>
50
+ <button class="icon-button popup-close" type="button" data-label="close">${ICONS.close}</button>
51
+ </div>
52
+ <div role="menu" data-label="${menu.title}">${menuItemsOf(menu)}</div>
37
53
  </div>
38
54
  </div>`;
39
55
  }
@@ -41,15 +57,15 @@ function pictureMenusMarkup() {
41
57
  return choiceMenuOf({
42
58
  name: ChoiceMenuName.Stabilization,
43
59
  title: "stabilization",
44
- icon: ICONS.stabilization,
45
- choices: "stabilizationModes",
46
- items: menuItemsOf(STABILIZATION_MODES)
60
+ choices: STABILIZATION_MODES,
61
+ icons: STABILIZATION_ICONS,
62
+ words: STABILIZATION_WORDS
47
63
  }) + choiceMenuOf({
48
64
  name: ChoiceMenuName.ViewMode,
49
65
  title: "viewMode",
50
- icon: ICONS.viewMode,
51
- choices: "viewModes",
52
- items: menuItemsOf(VIEW_MODES)
66
+ choices: VIEW_MODES,
67
+ icons: VIEW_MODE_ICONS,
68
+ words: VIEW_MODE_WORDS
53
69
  });
54
70
  }
55
71
  /**
@@ -69,15 +85,14 @@ function controlsMarkup() {
69
85
  <div class="controls" part="controls">
70
86
  <input class="seek" type="range" min="0" max="0" step="0.01" value="0" data-label="seek" />
71
87
  <div class="row">
72
- <button class="play" type="button"></button>
73
- <button class="stop" type="button" data-label="stop">${ICONS.stop}</button>
74
- <span class="time">0:00 / 0:00</span>
75
- <span class="spacer"></span>
76
- <button class="mute" type="button" data-label="mute"></button>
88
+ <button class="icon-button play" type="button"></button>
89
+ <button class="icon-button mute" type="button" data-label="mute"></button>
77
90
  <input class="volume" type="range" min="0" max="1" step="0.01" value="1" data-label="volume" />
78
- <button class="reset-view" type="button" data-label="resetView">${ICONS.resetView}</button>
91
+ <span class="time"><span class="elapsed">0:00</span> <span class="total">/ 0:00</span></span>
92
+ <span class="spacer"></span>
79
93
  ${pictureMenusMarkup()}
80
- <button class="fullscreen" type="button" data-label="fullscreen">${ICONS.fullscreen}</button>
94
+ <button class="icon-button reset-view" type="button" data-label="resetView">${ICONS.resetView}</button>
95
+ <button class="icon-button fullscreen" type="button" data-label="fullscreen">${ICONS.fullscreen}</button>
81
96
  </div>
82
97
  </div>`;
83
98
  }
@@ -2,12 +2,20 @@ import { parseMarkup } from "./parseMarkup.js";
2
2
  //#region ../../packages/player/src/controls/icons.ts
3
3
  /**
4
4
  * Wraps an icon's shapes in a 24-unit square. Shapes are stroked in `currentColor` unless they
5
- * say otherwise, so every icon takes the controls' text colour.
5
+ * say otherwise, so every icon takes the controls' text colour. The stroke is 1.8 units: a line
6
+ * 1.5 pixels wide at the 20 pixels the bar draws its icons at.
6
7
  */
7
8
  function icon(shapes) {
8
- return `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${shapes}</svg>`;
9
+ return `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${shapes}</svg>`;
9
10
  }
10
- var SPEAKER = "<path d=\"M4 9.5v5h3.5L12 19V5L7.5 9.5z\" fill=\"currentColor\"/>";
11
+ /**
12
+ * Shapes drawn on a 20-unit grid with a 1.5-unit stroke, scaled onto the 24-unit one: the stroke
13
+ * scales with them to the 1.8 units of every other icon.
14
+ */
15
+ function fromTwentyUnitGrid(shapes) {
16
+ return icon(`<g transform="scale(1.2)" stroke-width="1.5">${shapes}</g>`);
17
+ }
18
+ var SPEAKER = "<path d=\"M4 9.5v5h3.5L12 19V5L7.5 9.5z\"/>";
11
19
  /**
12
20
  * The controls' icons, drawn as inline SVG in `currentColor` only: a text glyph is left to the
13
21
  * platform's fonts, some of which draw it as a colour emoji.
@@ -15,13 +23,29 @@ var SPEAKER = "<path d=\"M4 9.5v5h3.5L12 19V5L7.5 9.5z\" fill=\"currentColor\"/>
15
23
  var ICONS = {
16
24
  play: icon("<path d=\"M8 5v14l11-7z\" fill=\"currentColor\"/>"),
17
25
  pause: icon("<rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/>"),
18
- stop: icon("<rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" rx=\"1.5\" fill=\"currentColor\" stroke=\"none\"/>"),
19
26
  sound: icon(`${SPEAKER}<path d="M15.5 9a4 4 0 0 1 0 6"/><path d="M18.5 6a8 8 0 0 1 0 12"/>`),
20
27
  muted: icon(`${SPEAKER}<path d="M16 9.5l5 5M21 9.5l-5 5"/>`),
21
28
  resetView: icon("<path d=\"M4.5 12a7.5 7.5 0 1 0 2.2-5.3\"/><path d=\"M4.5 4.5v4h4\"/>"),
22
- stabilization: icon("<rect x=\"2.5\" y=\"8.5\" width=\"19\" height=\"7\" rx=\"3.5\"/><path d=\"M8.5 8.5v7M15.5 8.5v7\"/><circle cx=\"12\" cy=\"12\" r=\"1.8\" fill=\"currentColor\" stroke=\"none\"/>"),
23
- viewMode: icon("<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><ellipse cx=\"12\" cy=\"12\" rx=\"3.5\" ry=\"8.5\"/><path d=\"M3.5 12h17\"/>"),
24
- fullscreen: icon("<path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\"/>")
29
+ fullscreen: icon("<path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\"/>"),
30
+ close: icon("<path d=\"M6 6l12 12M18 6L6 18\"/>")
31
+ };
32
+ /**
33
+ * Each stabilization mode's icon, which its menu shows beside it and its button while it is in
34
+ * effect: records, so a mode cannot be left without one.
35
+ */
36
+ var STABILIZATION_ICONS = {
37
+ off: fromTwentyUnitGrid("<path d=\"M2 11l2.5-3 3 4.5 3-6 3 5 2-2.5L18 11\"/>"),
38
+ lock: fromTwentyUnitGrid("<rect x=\"4.5\" y=\"9\" width=\"11\" height=\"8\" rx=\"1.5\"/><path d=\"M7 9V6.5a3 3 0 016 0V9\"/>"),
39
+ horizon: fromTwentyUnitGrid("<circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M3 10h14M8 13h4\"/>"),
40
+ follow: fromTwentyUnitGrid("<path d=\"M3 15c3.5 0 4-9 8.5-9 2 0 3 1 4.5 1\"/><path d=\"M14 4.5L16.5 7 14 9.5\"/>")
41
+ };
42
+ /**
43
+ * Each view mode's icon, as the stabilization modes have theirs.
44
+ */
45
+ var VIEW_MODE_ICONS = {
46
+ "raw-lenses": fromTwentyUnitGrid("<circle cx=\"6\" cy=\"10\" r=\"4.25\"/><circle cx=\"14\" cy=\"10\" r=\"4.25\"/>"),
47
+ equirectangular: fromTwentyUnitGrid("<rect x=\"2\" y=\"5\" width=\"16\" height=\"10\" rx=\"1.5\"/><path d=\"M2 10h16M7.5 5c-1.2 3.3-1.2 6.7 0 10M12.5 5c1.2 3.3 1.2 6.7 0 10\"/>"),
48
+ normal: fromTwentyUnitGrid("<path d=\"M10 16L3.2 7.2a10 10 0 0113.6 0z\"/>")
25
49
  };
26
50
  var parsedIcons = /* @__PURE__ */ new Map();
27
51
  /**
@@ -37,4 +61,4 @@ function iconNode(name) {
37
61
  return parsed.cloneNode(true);
38
62
  }
39
63
  //#endregion
40
- export { ICONS, iconNode };
64
+ export { ICONS, STABILIZATION_ICONS, VIEW_MODE_ICONS, iconNode };
@@ -1,4 +1,13 @@
1
1
  //#region ../../packages/player/src/controls/messages.ts
2
+ var STABILIZATION_WORDS = {
3
+ names: "stabilizationModes",
4
+ descriptions: "stabilizationModeDescriptions"
5
+ };
6
+ var VIEW_MODE_WORDS = {
7
+ names: "viewModes",
8
+ descriptions: "viewModeDescriptions"
9
+ };
10
+ var CHOICE_TABLES = new Set([STABILIZATION_WORDS, VIEW_MODE_WORDS].flatMap((words) => [words.names, words.descriptions]));
2
11
  var UNREACHABLE = "The video could not be loaded.";
3
12
  var UNSUPPORTED_BROWSER = "This browser cannot play this video.";
4
13
  var UNREADABLE = "This file cannot be played.";
@@ -9,7 +18,6 @@ var DEFAULT_MESSAGES = {
9
18
  loading: "Loading",
10
19
  play: "Play",
11
20
  pause: "Pause",
12
- stop: "Stop",
13
21
  seek: "Seek",
14
22
  position: "{time} of {duration}",
15
23
  mute: "Mute",
@@ -17,7 +25,8 @@ var DEFAULT_MESSAGES = {
17
25
  resetView: "Reset view",
18
26
  stabilization: "Stabilization",
19
27
  viewMode: "View",
20
- fullscreen: "Fullscreen"
28
+ fullscreen: "Fullscreen",
29
+ close: "Close"
21
30
  },
22
31
  stabilizationModes: {
23
32
  off: "Off",
@@ -25,11 +34,22 @@ var DEFAULT_MESSAGES = {
25
34
  horizon: "Horizon",
26
35
  follow: "Follow"
27
36
  },
37
+ stabilizationModeDescriptions: {
38
+ off: "Footage as the camera moved",
39
+ lock: "Orientation fixed to the world",
40
+ horizon: "Level horizon, follows heading",
41
+ follow: "Follows turns, smooths out shake"
42
+ },
28
43
  viewModes: {
29
44
  "raw-lenses": "Raw lenses",
30
45
  equirectangular: "Equirectangular",
31
46
  normal: "Normal"
32
47
  },
48
+ viewModeDescriptions: {
49
+ "raw-lenses": "Both fisheye images as recorded",
50
+ equirectangular: "The full 360° frame, unwrapped",
51
+ normal: "Standard view, drag to look around"
52
+ },
33
53
  errors: {
34
54
  "binary-out-of-bounds": UNREADABLE,
35
55
  "binary-unsafe-integer": UNREADABLE,
@@ -69,7 +89,9 @@ function messagesWith(overrides) {
69
89
  return {
70
90
  labels: tableWith(DEFAULT_MESSAGES.labels, given["labels"]),
71
91
  stabilizationModes: tableWith(DEFAULT_MESSAGES.stabilizationModes, given["stabilizationModes"]),
92
+ stabilizationModeDescriptions: tableWith(DEFAULT_MESSAGES.stabilizationModeDescriptions, given["stabilizationModeDescriptions"]),
72
93
  viewModes: tableWith(DEFAULT_MESSAGES.viewModes, given["viewModes"]),
94
+ viewModeDescriptions: tableWith(DEFAULT_MESSAGES.viewModeDescriptions, given["viewModeDescriptions"]),
73
95
  errors: tableWith(DEFAULT_MESSAGES.errors, given["errors"])
74
96
  };
75
97
  }
@@ -77,7 +99,7 @@ function isLabelName(name) {
77
99
  return name !== null && Object.hasOwn(DEFAULT_MESSAGES.labels, name);
78
100
  }
79
101
  function isChoiceTable(name) {
80
- return name === "stabilizationModes" || name === "viewModes";
102
+ return name !== null && CHOICE_TABLES.has(name);
81
103
  }
82
104
  function tableWith(defaults, overrides) {
83
105
  if (!isRecord(overrides)) return defaults;
@@ -91,4 +113,4 @@ function isRecord(value) {
91
113
  return typeof value === "object" && value !== null;
92
114
  }
93
115
  //#endregion
94
- export { DEFAULT_MESSAGES, isChoiceTable, isLabelName, messagesWith };
116
+ export { DEFAULT_MESSAGES, STABILIZATION_WORDS, VIEW_MODE_WORDS, isChoiceTable, isLabelName, messagesWith };
@@ -0,0 +1,16 @@
1
+ import { clamp } from "../../../core/src/shared/math/clamp.js";
2
+ import "../../../core/src/index.js";
3
+ //#region ../../packages/player/src/controls/sliderFill.ts
4
+ /**
5
+ * Tells the stylesheet how far a range input is filled, as a fraction in `--fill`: only Firefox
6
+ * styles the filled part of a range itself. Set through the CSSOM, which a Content Security
7
+ * Policy does not govern as it governs style attributes.
8
+ */
9
+ function showSliderFill(slider) {
10
+ const minimum = Number(slider.min);
11
+ const span = Number(slider.max) - minimum;
12
+ const fraction = span > 0 ? (Number(slider.value) - minimum) / span : 0;
13
+ slider.style.setProperty("--fill", String(clamp(fraction, 0, 1)));
14
+ }
15
+ //#endregion
16
+ export { showSliderFill };
@@ -49,6 +49,13 @@ var ElementLoads = class {
49
49
  this.files = void 0;
50
50
  this.schedule();
51
51
  }
52
+ /**
53
+ * How the recording is fetched changed: one named by URL loads again, read the new way; files
54
+ * handed in are not fetched, so they play on.
55
+ */
56
+ requestChanged() {
57
+ if (this.files === void 0) this.schedule();
58
+ }
52
59
  loadFiles(files) {
53
60
  this.files = files;
54
61
  this.schedule();
@@ -1,11 +1,11 @@
1
- import { defineBooleanProperties, defineKeywordProperties, defineStringProperties, propertyNameOf } from "./reflectedProperties.js";
1
+ import { defineBooleanProperties, defineKeywordProperties, defineNullableKeywordProperties, defineStringProperties, propertyNameOf } from "./reflectedProperties.js";
2
2
  import { ensureFinite } from "../player/ensureFinite.js";
3
3
  import { bindKeyboard } from "../controls/keyboard.js";
4
4
  import { ViewGestures } from "../controls/ViewGestures.js";
5
5
  import { togglePlayback } from "../controls/customControls.js";
6
6
  import { createBrowserPlayer } from "../browserPlayer.js";
7
- import { OBSERVED_ATTRIBUTES, PlaybackAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
8
- import { GAIN_MATCH, PRELOAD, unreadableAngleWarning, viewAfterAttribute } from "./attributes.js";
7
+ import { OBSERVED_ATTRIBUTES, PlaybackAttribute, RequestAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
8
+ import { CROSS_ORIGIN, GAIN_MATCH, PRELOAD, unreadableAngleWarning, viewAfterAttribute } from "./attributes.js";
9
9
  import { ElementLoads } from "./ElementLoads.js";
10
10
  import { FullscreenToggle } from "./FullscreenToggle.js";
11
11
  import { IdleWatcher } from "./IdleWatcher.js";
@@ -27,8 +27,10 @@ import { Wording } from "../controls/Wording.js";
27
27
  */
28
28
  var STRING_ATTRIBUTES = [...Object.values(SourceAttribute), PlaybackAttribute.Poster];
29
29
  var KEYWORD_ATTRIBUTES = [PRELOAD, GAIN_MATCH];
30
+ var NULLABLE_KEYWORD_ATTRIBUTES = [CROSS_ORIGIN];
30
31
  var BOOLEAN_ATTRIBUTES = [PlaybackAttribute.Autoplay, PlaybackAttribute.Controls];
31
32
  var SOURCE_ATTRIBUTES = Object.values(SourceAttribute);
33
+ var REQUEST_ATTRIBUTES = Object.values(RequestAttribute);
32
34
  /**
33
35
  * The properties a page may set before the element is defined, all kept for it.
34
36
  */
@@ -38,6 +40,7 @@ var PUBLIC_PROPERTIES = [
38
40
  ...KEYWORD_ATTRIBUTES.map(({ name }) => name),
39
41
  ...BOOLEAN_ATTRIBUTES
40
42
  ].map((name) => propertyNameOf(name)),
43
+ ...NULLABLE_KEYWORD_ATTRIBUTES.map(({ property }) => property),
41
44
  ...LIVE_SETTING_NAMES,
42
45
  "currentTime",
43
46
  "messages"
@@ -66,6 +69,7 @@ var GyroViewElement = class extends TypedEventElement {
66
69
  this.earlyProperties = takeEarlyProperties(this, PUBLIC_PROPERTIES);
67
70
  defineStringProperties(this, STRING_ATTRIBUTES);
68
71
  defineKeywordProperties(this, KEYWORD_ATTRIBUTES);
72
+ defineNullableKeywordProperties(this, NULLABLE_KEYWORD_ATTRIBUTES);
69
73
  defineBooleanProperties(this, BOOLEAN_ATTRIBUTES);
70
74
  const shadow = this.attachShadow({ mode: "open" });
71
75
  renderShadowTree(shadow);
@@ -158,6 +162,7 @@ var GyroViewElement = class extends TypedEventElement {
158
162
  }
159
163
  attributeChangedCallback(name, _previous, value) {
160
164
  if (SOURCE_ATTRIBUTES.includes(name)) this.loads.sourceChanged();
165
+ else if (REQUEST_ATTRIBUTES.includes(name)) this.loads.requestChanged();
161
166
  else if (VIEW_ATTRIBUTES.includes(name)) {
162
167
  this.player.setView(viewAfterAttribute(this.player.view, name, value));
163
168
  const warning = unreadableAngleWarning(name, value);
@@ -11,6 +11,16 @@ var SourceAttribute = {
11
11
  Src: "src",
12
12
  Src2: "src2"
13
13
  };
14
+ /**
15
+ * `Request` ones say how the recording is fetched; changing any of them reloads a recording
16
+ * named by URL. The iframe embed carries none of them (ADR 0027).
17
+ */
18
+ var RequestAttribute = {
19
+ /**
20
+ * `use-credentials` fetches with the visitor's cookies, as a media element's `crossorigin`
21
+ * does; `anonymous`, the default, sends them to the page's own origin only.
22
+ */
23
+ CrossOrigin: "crossorigin" };
14
24
  var ViewAttribute = {
15
25
  FieldOfView: "fov",
16
26
  Yaw: "yaw",
@@ -44,8 +54,9 @@ var PlaybackAttribute = {
44
54
  };
45
55
  var OBSERVED_ATTRIBUTES = [
46
56
  ...Object.values(SourceAttribute),
57
+ ...Object.values(RequestAttribute),
47
58
  ...Object.values(ViewAttribute),
48
59
  ...Object.values(PlaybackAttribute)
49
60
  ];
50
61
  //#endregion
51
- export { OBSERVED_ATTRIBUTES, PlaybackAttribute, SourceAttribute, ViewAttribute };
62
+ export { OBSERVED_ATTRIBUTES, PlaybackAttribute, RequestAttribute, SourceAttribute, ViewAttribute };
@@ -1,20 +1,31 @@
1
1
  import { keywordOf } from "./reflectedProperties.js";
2
- import { PlaybackAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
2
+ import { PlaybackAttribute, RequestAttribute, SourceAttribute, ViewAttribute } from "./attributeNames.js";
3
3
  //#region ../../packages/player/src/element/attributes.ts
4
4
  /**
5
5
  * The source the attributes describe, or nothing when `src` is absent. URLs resolve against
6
- * the document, as an image's would.
6
+ * the document, as an image's would, and both files of a pair are read alike.
7
7
  */
8
8
  function sourceFromAttributes(read, baseUrl) {
9
- const main = urlInputOf(read(SourceAttribute.Src), baseUrl);
9
+ const reading = readingOf(read(RequestAttribute.CrossOrigin));
10
+ const main = urlInputOf(read(SourceAttribute.Src), baseUrl, reading);
10
11
  return main && {
11
12
  main,
12
- second: urlInputOf(read(SourceAttribute.Src2), baseUrl)
13
+ second: urlInputOf(read(SourceAttribute.Src2), baseUrl, reading)
13
14
  };
14
15
  }
15
- function urlInputOf(value, baseUrl) {
16
+ function urlInputOf(value, baseUrl, reading) {
16
17
  const trimmed = value?.trim() ?? "";
17
- return trimmed === "" ? void 0 : { url: resolvedOrAsWritten(trimmed, baseUrl) };
18
+ return trimmed === "" ? void 0 : {
19
+ url: resolvedOrAsWritten(trimmed, baseUrl),
20
+ ...reading
21
+ };
22
+ }
23
+ /**
24
+ * `use-credentials` sends the visitor's cookies to any origin; anything else leaves fetch's own
25
+ * default, which sends them to the page's origin only, as `anonymous` does for a media element.
26
+ */
27
+ function readingOf(crossOrigin) {
28
+ return keywordOf(CROSS_ORIGIN, crossOrigin) === "use-credentials" ? { credentials: "include" } : {};
18
29
  }
19
30
  /**
20
31
  * The URL resolved against the document; one that does not parse goes on as written, so the
@@ -91,6 +102,17 @@ var GAIN_MATCH = {
91
102
  keywords: ["on", "off"],
92
103
  fallback: "on"
93
104
  };
105
+ /**
106
+ * `use-credentials` fetches the recording with the visitor's cookies; `anonymous`, in effect for
107
+ * an empty or unknown value, without them across origins. Absent, its property reads `null`, as
108
+ * a media element's `crossOrigin` does.
109
+ */
110
+ var CROSS_ORIGIN = {
111
+ name: RequestAttribute.CrossOrigin,
112
+ property: "crossOrigin",
113
+ keywords: ["anonymous", "use-credentials"],
114
+ fallback: "anonymous"
115
+ };
94
116
  function shouldPreload(value) {
95
117
  return keywordOf(PRELOAD, value) === "auto";
96
118
  }
@@ -98,4 +120,4 @@ function shouldMatchGains(value) {
98
120
  return keywordOf(GAIN_MATCH, value) === "on";
99
121
  }
100
122
  //#endregion
101
- export { GAIN_MATCH, PRELOAD, ignoredChoiceWarning, isBooleanAttributeSet, parseNumber, shouldMatchGains, shouldPreload, sourceFromAttributes, unreadableAngleWarning, viewAfterAttribute };
123
+ export { CROSS_ORIGIN, GAIN_MATCH, PRELOAD, ignoredChoiceWarning, isBooleanAttributeSet, parseNumber, shouldMatchGains, shouldPreload, sourceFromAttributes, unreadableAngleWarning, viewAfterAttribute };
@@ -48,6 +48,24 @@ function defineKeywordProperties(element, attributes) {
48
48
  });
49
49
  }
50
50
  /**
51
+ * Enumerated attributes whose property reads `null` while the attribute is absent and the keyword
52
+ * in effect otherwise, the fallback for an empty or unknown value, as the DOM reflects
53
+ * `crossOrigin`; it writes the attribute.
54
+ */
55
+ function defineNullableKeywordProperties(element, attributes) {
56
+ for (const attribute of attributes) Object.defineProperty(element, attribute.property, {
57
+ configurable: true,
58
+ enumerable: true,
59
+ get() {
60
+ const value = this.getAttribute(attribute.name);
61
+ return value === null ? null : keywordOf(attribute, value);
62
+ },
63
+ set(value) {
64
+ writeAttribute(this, attribute.name, value);
65
+ }
66
+ });
67
+ }
68
+ /**
51
69
  * The keyword an enumerated attribute's value names, ignoring case and spacing; the fallback for
52
70
  * an absent or unknown one.
53
71
  */
@@ -75,4 +93,4 @@ function propertyNameOf(attribute) {
75
93
  return attribute.replaceAll(HYPHENATED_LETTER, (_match, letter) => letter.toUpperCase());
76
94
  }
77
95
  //#endregion
78
- export { defineBooleanProperties, defineKeywordProperties, defineStringProperties, keywordOf, propertyNameOf, writeAttribute };
96
+ export { defineBooleanProperties, defineKeywordProperties, defineNullableKeywordProperties, defineStringProperties, keywordOf, propertyNameOf, writeAttribute };
@@ -1,4 +1,4 @@
1
1
  //#region ../../packages/player/src/element/styles.css?raw
2
- var styles_default = ":host {\n --gyro-view-accent: #4cc2ff;\n --gyro-view-controls-background: rgb(0 0 0 / 55%);\n --gyro-view-text: #f4f4f4;\n --gyro-view-font: system-ui, -apple-system, 'Segoe UI', sans-serif;\n --gyro-view-radius: 6px;\n\n display: block;\n position: relative;\n width: 100%;\n aspect-ratio: 16 / 9;\n background: #000;\n color: var(--gyro-view-text);\n font-family: var(--gyro-view-font);\n overflow: hidden;\n user-select: none;\n -webkit-user-select: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host(:focus-visible) {\n outline: 2px solid var(--gyro-view-accent);\n outline-offset: 2px;\n}\n\n/*\n * Pinned over the page where the Fullscreen API is refused (iPhone Safari). Like the browser's\n * own fullscreen rules it overrides the size, margins and place the page gave the element, and\n * the insets size it to the visible viewport, where 100vh would reach under Safari's toolbars.\n */\n:host([data-fill]) {\n position: fixed !important;\n inset: 0 !important;\n box-sizing: border-box !important;\n width: auto !important;\n height: auto !important;\n min-width: 0 !important;\n min-height: 0 !important;\n max-width: none !important;\n max-height: none !important;\n margin: 0 !important;\n transform: none !important;\n aspect-ratio: auto !important;\n z-index: 2147483000 !important;\n}\n\n:host(:fullscreen) {\n width: 100%;\n height: 100%;\n aspect-ratio: auto;\n}\n\n/* The size container the controls fit themselves to: the player's size, not the page's. Its\n size never depends on its content, which size containment requires. */\n.stage {\n position: absolute;\n inset: 0;\n container-type: size;\n}\n\ncanvas {\n display: block;\n width: 100%;\n height: 100%;\n touch-action: none;\n}\n\n.poster {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n}\n\n:host([data-has-frame]) .poster,\n:host(:not([poster])) .poster {\n display: none;\n}\n\n.overlay {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 0.5rem;\n padding: 1rem;\n text-align: center;\n background: rgb(0 0 0 / 45%);\n}\n\n:host([data-status='loading']) .overlay.loading,\n:host([data-status='buffering']) .overlay.loading,\n:host([data-status='error']) .overlay.error {\n display: flex;\n}\n\n:host([data-status='buffering']) .overlay.loading {\n background: transparent;\n pointer-events: none;\n}\n\n.spinner {\n width: 2.5rem;\n height: 2.5rem;\n border: 3px solid rgb(255 255 255 / 25%);\n border-top-color: var(--gyro-view-accent);\n border-radius: 50%;\n animation: gyro-view-spin 0.9s linear infinite;\n}\n\n@keyframes gyro-view-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* A viewer who asked for less motion still sees that it loads, three times slower. */\n@media (prefers-reduced-motion: reduce) {\n .spinner {\n animation-duration: 2.7s;\n }\n}\n\n.error-code {\n font-size: 0.8rem;\n opacity: 0.7;\n}\n";
2
+ var styles_default = ":host {\n --gyro-view-accent: #fff;\n --gyro-view-controls-background: rgb(0 0 0 / 60%);\n --gyro-view-menu-background: rgb(24 24 23 / 92%);\n --gyro-view-text: #fff;\n --gyro-view-font: system-ui, -apple-system, 'Segoe UI', sans-serif;\n --gyro-view-radius: 8px;\n\n display: block;\n position: relative;\n width: 100%;\n aspect-ratio: 16 / 9;\n background: #000;\n color: var(--gyro-view-text);\n font-family: var(--gyro-view-font);\n overflow: hidden;\n user-select: none;\n -webkit-user-select: none;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host(:focus-visible) {\n outline: 2px solid var(--gyro-view-accent);\n outline-offset: 2px;\n}\n\n/*\n * Pinned over the page where the Fullscreen API is refused (iPhone Safari). Like the browser's\n * own fullscreen rules it overrides the size, margins and place the page gave the element, and\n * the insets size it to the visible viewport, where 100vh would reach under Safari's toolbars.\n */\n:host([data-fill]) {\n position: fixed !important;\n inset: 0 !important;\n box-sizing: border-box !important;\n width: auto !important;\n height: auto !important;\n min-width: 0 !important;\n min-height: 0 !important;\n max-width: none !important;\n max-height: none !important;\n margin: 0 !important;\n transform: none !important;\n aspect-ratio: auto !important;\n z-index: 2147483000 !important;\n}\n\n:host(:fullscreen) {\n width: 100%;\n height: 100%;\n aspect-ratio: auto;\n}\n\n/* The size container the controls fit themselves to: the player's size, not the page's. Its\n size never depends on its content, which size containment requires. */\n.stage {\n position: absolute;\n inset: 0;\n container-type: size;\n}\n\ncanvas {\n display: block;\n width: 100%;\n height: 100%;\n touch-action: none;\n}\n\n.poster {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n pointer-events: none;\n}\n\n:host([data-has-frame]) .poster,\n:host(:not([poster])) .poster {\n display: none;\n}\n\n.overlay {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 0.5rem;\n padding: 1rem;\n text-align: center;\n background: rgb(0 0 0 / 45%);\n}\n\n:host([data-status='loading']) .overlay.loading,\n:host([data-status='buffering']) .overlay.loading,\n:host([data-status='error']) .overlay.error {\n display: flex;\n}\n\n:host([data-status='buffering']) .overlay.loading {\n background: transparent;\n pointer-events: none;\n}\n\n.spinner {\n width: 2.5rem;\n height: 2.5rem;\n border: 3px solid rgb(255 255 255 / 25%);\n border-top-color: var(--gyro-view-accent);\n border-radius: 50%;\n animation: gyro-view-spin 0.9s linear infinite;\n}\n\n@keyframes gyro-view-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* A viewer who asked for less motion still sees that it loads, three times slower. */\n@media (prefers-reduced-motion: reduce) {\n .spinner {\n animation-duration: 2.7s;\n }\n}\n\n.error-code {\n font-size: 0.8rem;\n opacity: 0.7;\n}\n";
3
3
  //#endregion
4
4
  export { styles_default as default };