webXREnterExitUI.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { Nullable } from "../types";
  2. import { Observable } from "../Misc/observable";
  3. import { IDisposable, Scene } from "../scene";
  4. import { WebXRExperienceHelper } from "./webXRExperienceHelper";
  5. import { WebXRState, WebXRRenderTarget } from './webXRTypes';
  6. import { Tools } from '../Misc/tools';
  7. /**
  8. * Button which can be used to enter a different mode of XR
  9. */
  10. export class WebXREnterExitUIButton {
  11. /**
  12. * Creates a WebXREnterExitUIButton
  13. * @param element button element
  14. * @param sessionMode XR initialization session mode
  15. * @param referenceSpaceType the type of reference space to be used
  16. */
  17. constructor(
  18. /** button element */
  19. public element: HTMLElement,
  20. /** XR initialization options for the button */
  21. public sessionMode: XRSessionMode,
  22. /** Reference space type */
  23. public referenceSpaceType: XRReferenceSpaceType
  24. ) { }
  25. /**
  26. * Extendable function which can be used to update the button's visuals when the state changes
  27. * @param activeButton the current active button in the UI
  28. */
  29. public update(activeButton: Nullable<WebXREnterExitUIButton>) {
  30. }
  31. }
  32. /**
  33. * Options to create the webXR UI
  34. */
  35. export class WebXREnterExitUIOptions {
  36. /**
  37. * User provided buttons to enable/disable WebXR. The system will provide default if not set
  38. */
  39. customButtons?: Array<WebXREnterExitUIButton>;
  40. /**
  41. * A reference space type to use when creating the default button.
  42. * Default is local-floor
  43. */
  44. referenceSpaceType?: XRReferenceSpaceType;
  45. /**
  46. * Context to enter xr with
  47. */
  48. renderTarget?: Nullable<WebXRRenderTarget>;
  49. /**
  50. * A session mode to use when creating the default button.
  51. * Default is immersive-vr
  52. */
  53. sessionMode?: XRSessionMode;
  54. /**
  55. * A list of optional features to init the session with
  56. */
  57. optionalFeatures?: string[];
  58. }
  59. /**
  60. * UI to allow the user to enter/exit XR mode
  61. */
  62. export class WebXREnterExitUI implements IDisposable {
  63. private _activeButton: Nullable<WebXREnterExitUIButton> = null;
  64. private _buttons: Array<WebXREnterExitUIButton> = [];
  65. private _overlay: HTMLDivElement;
  66. /**
  67. * Fired every time the active button is changed.
  68. *
  69. * When xr is entered via a button that launches xr that button will be the callback parameter
  70. *
  71. * When exiting xr the callback parameter will be null)
  72. */
  73. public activeButtonChangedObservable = new Observable<Nullable<WebXREnterExitUIButton>>();
  74. /**
  75. *
  76. * @param scene babylon scene object to use
  77. * @param options (read-only) version of the options passed to this UI
  78. */
  79. private constructor(
  80. private scene: Scene,
  81. /** version of the options passed to this UI */
  82. public options: WebXREnterExitUIOptions
  83. ) {
  84. this._overlay = document.createElement("div");
  85. this._overlay.style.cssText = "z-index:11;position: absolute; right: 20px;bottom: 50px;";
  86. // if served over HTTP, warn people.
  87. // Hopefully the browsers will catch up
  88. if (typeof window !== 'undefined') {
  89. if (window.location && window.location.protocol === 'http:') {
  90. Tools.Warn('WebXR can only be served over HTTPS');
  91. }
  92. }
  93. if (options.customButtons) {
  94. this._buttons = options.customButtons;
  95. } else {
  96. const sessionMode = options.sessionMode || "immersive-vr";
  97. const referenceSpaceType = options.referenceSpaceType || "local-floor";
  98. const url = !window.SVGSVGElement ? "https://cdn.babylonjs.com/Assets/vrButton.png" : "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%222048%22%20height%3D%221152%22%20viewBox%3D%220%200%202048%201152%22%20version%3D%221.1%22%3E%3Cpath%20transform%3D%22rotate%28180%201024%2C576.0000000000001%29%22%20d%3D%22m1109%2C896q17%2C0%2030%2C-12t13%2C-30t-12.5%2C-30.5t-30.5%2C-12.5l-170%2C0q-18%2C0%20-30.5%2C12.5t-12.5%2C30.5t13%2C30t30%2C12l170%2C0zm-85%2C256q59%2C0%20132.5%2C-1.5t154.5%2C-5.5t164.5%2C-11.5t163%2C-20t150%2C-30t124.5%2C-41.5q23%2C-11%2042%2C-24t38%2C-30q27%2C-25%2041%2C-61.5t14%2C-72.5l0%2C-257q0%2C-123%20-47%2C-232t-128%2C-190t-190%2C-128t-232%2C-47l-81%2C0q-37%2C0%20-68.5%2C14t-60.5%2C34.5t-55.5%2C45t-53%2C45t-53%2C34.5t-55.5%2C14t-55.5%2C-14t-53%2C-34.5t-53%2C-45t-55.5%2C-45t-60.5%2C-34.5t-68.5%2C-14l-81%2C0q-123%2C0%20-232%2C47t-190%2C128t-128%2C190t-47%2C232l0%2C257q0%2C68%2038%2C115t97%2C73q54%2C24%20124.5%2C41.5t150%2C30t163%2C20t164.5%2C11.5t154.5%2C5.5t132.5%2C1.5zm939%2C-298q0%2C39%20-24.5%2C67t-58.5%2C42q-54%2C23%20-122%2C39.5t-143.5%2C28t-155.5%2C19t-157%2C11t-148.5%2C5t-129.5%2C1.5q-59%2C0%20-130%2C-1.5t-148%2C-5t-157%2C-11t-155.5%2C-19t-143.5%2C-28t-122%2C-39.5q-34%2C-14%20-58.5%2C-42t-24.5%2C-67l0%2C-257q0%2C-106%2040.5%2C-199t110%2C-162.5t162.5%2C-109.5t199%2C-40l81%2C0q27%2C0%2052%2C14t50%2C34.5t51%2C44.5t55.5%2C44.5t63.5%2C34.5t74%2C14t74%2C-14t63.5%2C-34.5t55.5%2C-44.5t51%2C-44.5t50%2C-34.5t52%2C-14l14%2C0q37%2C0%2070%2C0.5t64.5%2C4.5t63.5%2C12t68%2C23q71%2C30%20128.5%2C78.5t98.5%2C110t63.5%2C133.5t22.5%2C149l0%2C257z%22%20fill%3D%22white%22%20/%3E%3C/svg%3E%0A";
  99. var css = ".babylonVRicon { color: #868686; border-color: #868686; border-style: solid; margin-left: 10px; height: 50px; width: 80px; background-color: rgba(51,51,51,0.7); background-image: url(" + url + "); background-size: 80%; background-repeat:no-repeat; background-position: center; border: none; outline: none; transition: transform 0.125s ease-out } .babylonVRicon:hover { transform: scale(1.05) } .babylonVRicon:active {background-color: rgba(51,51,51,1) } .babylonVRicon:focus {background-color: rgba(51,51,51,1) }";
  100. css += ".babylonVRicon.vrdisplaypresenting { background-image: none;} .vrdisplaypresenting::after { content: \"EXIT\"} .xr-error::after { content: \"ERROR\"}";
  101. var style = document.createElement('style');
  102. style.appendChild(document.createTextNode(css));
  103. document.getElementsByTagName('head')[0].appendChild(style);
  104. var hmdBtn = document.createElement("button");
  105. hmdBtn.className = "babylonVRicon";
  106. hmdBtn.title = `${sessionMode} - ${referenceSpaceType}`;
  107. this._buttons.push(new WebXREnterExitUIButton(hmdBtn, sessionMode, referenceSpaceType));
  108. this._buttons[this._buttons.length - 1].update = function(activeButton: WebXREnterExitUIButton) {
  109. this.element.style.display = (activeButton === null || activeButton === this) ? "" : "none";
  110. hmdBtn.className = "babylonVRicon" + (activeButton === this ? " vrdisplaypresenting" : "");
  111. };
  112. this._updateButtons(null);
  113. }
  114. var renderCanvas = scene.getEngine().getInputElement();
  115. if (renderCanvas && renderCanvas.parentNode) {
  116. renderCanvas.parentNode.appendChild(this._overlay);
  117. scene.onDisposeObservable.addOnce(() => {
  118. this.dispose();
  119. });
  120. }
  121. }
  122. /**
  123. * Creates UI to allow the user to enter/exit XR mode
  124. * @param scene the scene to add the ui to
  125. * @param helper the xr experience helper to enter/exit xr with
  126. * @param options options to configure the UI
  127. * @returns the created ui
  128. */
  129. public static CreateAsync(scene: Scene, helper: WebXRExperienceHelper, options: WebXREnterExitUIOptions): Promise<WebXREnterExitUI> {
  130. var ui = new WebXREnterExitUI(scene, options);
  131. var supportedPromises = ui._buttons.map((btn) => {
  132. return helper.sessionManager.isSessionSupportedAsync(btn.sessionMode);
  133. });
  134. helper.onStateChangedObservable.add((state) => {
  135. if (state == WebXRState.NOT_IN_XR) {
  136. ui._updateButtons(null);
  137. }
  138. });
  139. return Promise.all(supportedPromises).then((results) => {
  140. results.forEach((supported, i) => {
  141. if (supported) {
  142. ui._overlay.appendChild(ui._buttons[i].element);
  143. ui._buttons[i].element.onclick = async() => {
  144. if (helper.state == WebXRState.IN_XR) {
  145. await helper.exitXRAsync();
  146. ui._updateButtons(null);
  147. } else if (helper.state == WebXRState.NOT_IN_XR) {
  148. if (options.renderTarget) {
  149. try {
  150. await helper.enterXRAsync(ui._buttons[i].sessionMode, ui._buttons[i].referenceSpaceType, options.renderTarget, {optionalFeatures: options.optionalFeatures});
  151. ui._updateButtons(ui._buttons[i]);
  152. } catch (e) {
  153. // make sure button is visible
  154. ui._updateButtons(null);
  155. const element = ui._buttons[i].element;
  156. const prevTitle = element.title;
  157. element.title = "Error entering XR session : " + prevTitle;
  158. element.classList.add("xr-error");
  159. }
  160. }
  161. }
  162. };
  163. } else {
  164. Tools.Warn(`Session mode "${ui._buttons[i].sessionMode}" not supported in browser`);
  165. }
  166. });
  167. return ui;
  168. });
  169. }
  170. /**
  171. * Disposes of the XR UI component
  172. */
  173. public dispose() {
  174. var renderCanvas = this.scene.getEngine().getInputElement();
  175. if (renderCanvas && renderCanvas.parentNode && renderCanvas.parentNode.contains(this._overlay)) {
  176. renderCanvas.parentNode.removeChild(this._overlay);
  177. }
  178. this.activeButtonChangedObservable.clear();
  179. }
  180. private _updateButtons(activeButton: Nullable<WebXREnterExitUIButton>) {
  181. this._activeButton = activeButton;
  182. this._buttons.forEach((b) => {
  183. b.update(this._activeButton);
  184. });
  185. this.activeButtonChangedObservable.notifyObservers(this._activeButton);
  186. }
  187. }