You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

viewport.ts 28KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. /*
  2. * encantar.js
  3. * GPU-accelerated Augmented Reality for the web
  4. * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Lesser General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. *
  19. * viewport.ts
  20. * Viewport
  21. */
  22. import AR from '../main';
  23. import Speedy from 'speedy-vision';
  24. import { SpeedySize } from 'speedy-vision/types/core/speedy-size';
  25. import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
  26. import { SessionMode } from './session';
  27. import { HUD, HUDContainer } from './hud';
  28. import { FullscreenButton } from '../ui/fullscreen-button';
  29. import { Vector2 } from '../geometry/vector2';
  30. import { Resolution } from '../utils/resolution';
  31. import { Nullable } from '../utils/utils';
  32. import { Utils } from '../utils/utils';
  33. import { AREvent, AREventTarget, AREventListener } from '../utils/ar-events';
  34. import { IllegalArgumentError, IllegalOperationError, NotSupportedError, AccessDeniedError } from '../utils/errors';
  35. /** Viewport container */
  36. export type ViewportContainer = HTMLDivElement;
  37. /** We admit that the size of the drawing buffer of the background canvas of the viewport may change over time */
  38. type ViewportSizeGetter = () => SpeedySize;
  39. /** All possible event types emitted by a Viewport */
  40. type ViewportEventType = 'resize' | 'fullscreenchange';
  41. /** An event emitted by a Viewport */
  42. class ViewportEvent extends AREvent<ViewportEventType> { }
  43. /** Viewport event target */
  44. class ViewportEventTarget extends AREventTarget<ViewportEventType> { }
  45. /** Viewport style (immersive mode) */
  46. type ViewportStyle = 'best-fit' | 'stretch' | 'inline';
  47. /**
  48. * Viewport constructor settings
  49. */
  50. export interface ViewportSettings
  51. {
  52. /** Viewport container */
  53. container: Nullable<ViewportContainer>;
  54. /** HUD container */
  55. hudContainer?: Nullable<HUDContainer>;
  56. /** Resolution of the canvas on which the virtual scene will be drawn */
  57. resolution?: Resolution;
  58. /** Viewport style */
  59. style?: ViewportStyle;
  60. /** An existing <canvas> on which the virtual scene will be drawn */
  61. canvas?: Nullable<HTMLCanvasElement>;
  62. /** Whether or not to include the built-in fullscreen button */
  63. fullscreenUI?: boolean;
  64. }
  65. /** Default viewport constructor settings */
  66. const DEFAULT_VIEWPORT_SETTINGS: Readonly<Required<ViewportSettings>> = {
  67. container: null,
  68. hudContainer: null,
  69. resolution: 'lg',
  70. style: 'best-fit',
  71. canvas: null,
  72. fullscreenUI: true,
  73. };
  74. /** Base z-index of the children of the viewport container */
  75. const BASE_ZINDEX = 0;
  76. /** Z-index of the background canvas */
  77. const BACKGROUND_ZINDEX = BASE_ZINDEX + 0;
  78. /** Z-index of the foreground canvas */
  79. const FOREGROUND_ZINDEX = BASE_ZINDEX + 1;
  80. /** Z-index of the HUD */
  81. const HUD_ZINDEX = BASE_ZINDEX + 2;
  82. /**
  83. * Helper class to work with the containers of the viewport
  84. */
  85. class ViewportContainers
  86. {
  87. /** The viewport container */
  88. private readonly _container: ViewportContainer;
  89. /** A direct child of the viewport container */
  90. private readonly _subContainer: HTMLDivElement;
  91. /**
  92. * Constructor
  93. * @param container viewport container
  94. */
  95. constructor(container: Nullable<ViewportContainer>)
  96. {
  97. // validate
  98. if(container == null)
  99. throw new IllegalArgumentError('Unspecified viewport container');
  100. else if(!(container instanceof HTMLElement))
  101. throw new IllegalArgumentError('Invalid viewport container');
  102. // store the viewport container
  103. this._container = container;
  104. // create the sub-container
  105. this._subContainer = document.createElement('div') as HTMLDivElement;
  106. container.appendChild(this._subContainer);
  107. }
  108. /**
  109. * The viewport container
  110. */
  111. get container(): ViewportContainer
  112. {
  113. return this._container;
  114. }
  115. /**
  116. * The sub-container
  117. */
  118. get subContainer(): HTMLDivElement
  119. {
  120. return this._subContainer;
  121. }
  122. /**
  123. * Initialize
  124. */
  125. init(): void
  126. {
  127. this._container.style.touchAction = 'none';
  128. this._container.style.backgroundColor = 'black';
  129. }
  130. /**
  131. * Release
  132. */
  133. release(): void
  134. {
  135. this._container.style.removeProperty('background-color');
  136. this._container.style.removeProperty('touch-action');
  137. }
  138. }
  139. /**
  140. * Helper class to work with the canvases of the viewport
  141. */
  142. class ViewportCanvases
  143. {
  144. /** A canvas used to render the physical scene */
  145. private readonly _backgroundCanvas: HTMLCanvasElement;
  146. /** A canvas used to render the virtual scene */
  147. private readonly _foregroundCanvas: HTMLCanvasElement;
  148. /** Original CSS of the foreground canvas */
  149. private readonly _originalCSSTextOfForegroundCanvas: string;
  150. /**
  151. * Constructor
  152. * @param parent container for the canvases
  153. * @param initialSize initial size of the canvases
  154. * @param fgCanvas optional existing foreground canvas
  155. */
  156. constructor(parent: HTMLElement, initialSize: SpeedySize, fgCanvas: Nullable<HTMLCanvasElement> = null)
  157. {
  158. if(fgCanvas !== null && !(fgCanvas instanceof HTMLCanvasElement))
  159. throw new IllegalArgumentError('Not a canvas: ' + fgCanvas);
  160. this._originalCSSTextOfForegroundCanvas = fgCanvas ? fgCanvas.style.cssText : '';
  161. this._foregroundCanvas = this._styleCanvas(
  162. fgCanvas || this._createCanvas(initialSize),
  163. FOREGROUND_ZINDEX
  164. );
  165. this._foregroundCanvas.style.background = 'transparent';
  166. this._backgroundCanvas = this._styleCanvas(
  167. this._createCanvas(initialSize),
  168. BACKGROUND_ZINDEX
  169. );
  170. this._backgroundCanvas.hidden = true;
  171. this._foregroundCanvas.hidden = true;
  172. const engineInfo = 'encantar.js ' + AR.version;
  173. this._backgroundCanvas.dataset.arEngine = engineInfo;
  174. this._foregroundCanvas.dataset.arEngine = engineInfo;
  175. parent.appendChild(this._backgroundCanvas);
  176. parent.appendChild(this._foregroundCanvas);
  177. }
  178. /**
  179. * The background canvas
  180. */
  181. get backgroundCanvas(): HTMLCanvasElement
  182. {
  183. return this._backgroundCanvas;
  184. }
  185. /**
  186. * The foreground canvas
  187. */
  188. get foregroundCanvas(): HTMLCanvasElement
  189. {
  190. return this._foregroundCanvas;
  191. }
  192. /**
  193. * Initialize
  194. */
  195. init(): void
  196. {
  197. this._backgroundCanvas.hidden = false;
  198. this._foregroundCanvas.hidden = false;
  199. }
  200. /**
  201. * Release
  202. */
  203. release(): void
  204. {
  205. this._backgroundCanvas.hidden = true;
  206. this._foregroundCanvas.hidden = true;
  207. this._backgroundCanvas.style.cssText = '';
  208. this._foregroundCanvas.style.cssText = this._originalCSSTextOfForegroundCanvas;
  209. }
  210. /**
  211. * Create a canvas
  212. * @param size size of the drawing buffer
  213. * @returns a new canvas
  214. */
  215. private _createCanvas(size: SpeedySize): HTMLCanvasElement
  216. {
  217. const canvas = document.createElement('canvas') as HTMLCanvasElement;
  218. canvas.width = size.width;
  219. canvas.height = size.height;
  220. return canvas;
  221. }
  222. /**
  223. * Add suitable CSS rules to a canvas
  224. * @param canvas
  225. * @param zIndex
  226. * @returns canvas
  227. */
  228. private _styleCanvas(canvas: HTMLCanvasElement, zIndex: number): HTMLCanvasElement
  229. {
  230. canvas.style.position = 'absolute';
  231. canvas.style.left = '0px';
  232. canvas.style.top = '0px';
  233. canvas.style.width = '100%';
  234. canvas.style.height = '100%';
  235. canvas.style.zIndex = String(zIndex);
  236. return canvas;
  237. }
  238. }
  239. /**
  240. * Fullscreen utilities
  241. */
  242. class ViewportFullscreenHelper
  243. {
  244. /** The viewport */
  245. private readonly _viewport: Viewport;
  246. /** The container to be put in fullscreen */
  247. private readonly _container: HTMLElement;
  248. /** Event handler */
  249. private _boundEventHandler: EventListener;
  250. /**
  251. * Constructor
  252. * @param viewport Viewport
  253. */
  254. constructor(viewport: Viewport)
  255. {
  256. this._viewport = viewport;
  257. this._container = viewport.container;
  258. this._boundEventHandler = this._triggerEvent.bind(this);
  259. }
  260. /**
  261. * Initialize
  262. */
  263. init(): void
  264. {
  265. this._container.addEventListener('fullscreenchange', this._boundEventHandler);
  266. }
  267. /**
  268. * Release
  269. */
  270. release(): void
  271. {
  272. this._container.removeEventListener('fullscreenchange', this._boundEventHandler);
  273. }
  274. /**
  275. * Make a request to the user agent so that the viewport container is
  276. * displayed in fullscreen mode. The container must be a compatible element[1]
  277. * and the user must interact with the page in order to comply with browser
  278. * policies[2]. In case of error, the returned promise is rejected.
  279. * [1] https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#compatible_elements
  280. * [2] https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#security
  281. * @returns promise
  282. */
  283. request(): SpeedyPromise<void>
  284. {
  285. const container = this._container;
  286. // fallback for older WebKit versions
  287. if(container.requestFullscreen === undefined) {
  288. if((container as any).webkitRequestFullscreen === undefined)
  289. return Speedy.Promise.reject(new NotSupportedError());
  290. else if(!(document as any).webkitFullscreenEnabled)
  291. return Speedy.Promise.reject(new AccessDeniedError());
  292. // webkitRequestFullscreen() does not return a value
  293. (container as any).webkitRequestFullscreen();
  294. return new Speedy.Promise<void>((resolve, reject) => {
  295. setTimeout(() => {
  296. if(container === (document as any).webkitFullscreenElement) {
  297. Utils.log('Entering fullscreen mode...');
  298. resolve();
  299. }
  300. else
  301. reject(new TypeError());
  302. }, 100);
  303. });
  304. }
  305. // check if the fullscreen mode is available
  306. if(!document.fullscreenEnabled)
  307. return Speedy.Promise.reject(new AccessDeniedError());
  308. // request fullscreen
  309. return new Speedy.Promise<void>((resolve, reject) => {
  310. container.requestFullscreen({
  311. navigationUI: 'hide'
  312. }).then(() => {
  313. Utils.log('Entering fullscreen mode...');
  314. resolve();
  315. }, reject);
  316. });
  317. }
  318. /**
  319. * Exit fullscreen mode
  320. * @returns promise
  321. */
  322. exit(): SpeedyPromise<void>
  323. {
  324. // fallback for older WebKit versions
  325. if(document.exitFullscreen === undefined) {
  326. const doc = document as any;
  327. if(doc.webkitExitFullscreen === undefined)
  328. return Speedy.Promise.reject(new NotSupportedError());
  329. else if(doc.webkitFullscreenElement === null)
  330. return Speedy.Promise.reject(new IllegalOperationError('Not in fullscreen mode'));
  331. // webkitExitFullscreen() does not return a value
  332. doc.webkitExitFullscreen();
  333. return new Speedy.Promise<void>((resolve, reject) => {
  334. setTimeout(() => {
  335. if(doc.webkitFullscreenElement === null) {
  336. Utils.log('Exiting fullscreen mode...');
  337. resolve();
  338. }
  339. else
  340. reject(new TypeError());
  341. }, 100);
  342. });
  343. }
  344. // error if not in fullscreen mode
  345. if(document.fullscreenElement === null)
  346. return Speedy.Promise.reject(new IllegalOperationError('Not in fullscreen mode'));
  347. // exit fullscreen
  348. return new Speedy.Promise<void>((resolve, reject) => {
  349. document.exitFullscreen().then(() => {
  350. Utils.log('Exiting fullscreen mode...');
  351. resolve();
  352. }, reject);
  353. });
  354. }
  355. /**
  356. * Is the fullscreen mode available in this platform?
  357. * @returns true if the fullscreen mode is available in this platform
  358. */
  359. isAvailable(): boolean
  360. {
  361. return document.fullscreenEnabled ||
  362. !!((document as any).webkitFullscreenEnabled);
  363. }
  364. /**
  365. * Is the container currently being displayed in fullscreen mode?
  366. * @returns true if the container is currently being displayed in fullscreen mode
  367. */
  368. isActivated(): boolean
  369. {
  370. if(document.fullscreenElement !== undefined)
  371. return document.fullscreenElement === this._container;
  372. else if((document as any).webkitFullscreenElement !== undefined)
  373. return (document as any).webkitFullscreenElement === this._container;
  374. else
  375. return false;
  376. }
  377. /**
  378. * Trigger a fullscreenchange event
  379. */
  380. _triggerEvent(): void
  381. {
  382. const event = new ViewportEvent('fullscreenchange');
  383. this._viewport.dispatchEvent(event);
  384. }
  385. }
  386. /**
  387. * Helper class to resize the viewport
  388. */
  389. class ViewportResizer
  390. {
  391. /** the viewport to be resized */
  392. private readonly _viewport: Viewport;
  393. /** a helper */
  394. private _timeout: Nullable<ReturnType<typeof setTimeout>>;
  395. /** bound resize method */
  396. private readonly _resize: () => void;
  397. /** bound event trigger */
  398. private readonly _triggerResize: () => void;
  399. /** resize strategy */
  400. private _resizeStrategy: ViewportResizeStrategy;
  401. /**
  402. * Constructor
  403. * @param viewport the viewport to be resized
  404. */
  405. constructor(viewport: Viewport)
  406. {
  407. this._viewport = viewport;
  408. this._timeout = null;
  409. this._resize = this._onResize.bind(this);
  410. this._triggerResize = this.triggerResize.bind(this);
  411. this._resizeStrategy = new InlineResizeStrategy();
  412. // initial setup
  413. // (the size is yet unknown)
  414. this._viewport.addEventListener('resize', this._resize);
  415. this.triggerResize(0);
  416. }
  417. /**
  418. * Initialize
  419. */
  420. init(): void
  421. {
  422. // Configure the resize listener. We want the viewport to adjust itself
  423. // if the phone/screen is resized or changes orientation
  424. window.addEventListener('resize', this._triggerResize); // a delay is welcome
  425. // handle changes of orientation
  426. // (is this needed? we already listen to resize events)
  427. if(screen.orientation !== undefined)
  428. screen.orientation.addEventListener('change', this._triggerResize);
  429. else
  430. window.addEventListener('orientationchange', this._triggerResize); // deprecated
  431. // trigger a resize to setup the sizes / the CSS
  432. this.triggerResize(0);
  433. }
  434. /**
  435. * Release
  436. */
  437. release(): void
  438. {
  439. if(screen.orientation !== undefined)
  440. screen.orientation.removeEventListener('change', this._triggerResize);
  441. else
  442. window.removeEventListener('orientationchange', this._triggerResize);
  443. window.removeEventListener('resize', this._triggerResize);
  444. this._viewport.removeEventListener('resize', this._resize);
  445. this._resizeStrategy.clear(this._viewport);
  446. }
  447. /**
  448. * Trigger a resize event after a delay
  449. * @param delay in milliseconds
  450. */
  451. triggerResize(delay: number = 100): void
  452. {
  453. const event = new ViewportEvent('resize');
  454. if(delay <= 0) {
  455. this._viewport.dispatchEvent(event);
  456. return;
  457. }
  458. if(this._timeout !== null)
  459. clearTimeout(this._timeout);
  460. this._timeout = setTimeout(() => {
  461. this._timeout = null;
  462. this._viewport.dispatchEvent(event);
  463. }, delay);
  464. }
  465. /**
  466. * Change the resize strategy
  467. * @param strategy new strategy
  468. */
  469. setStrategy(strategy: ViewportResizeStrategy): void
  470. {
  471. this._resizeStrategy.clear(this._viewport);
  472. this._resizeStrategy = strategy;
  473. this.triggerResize(0);
  474. }
  475. /**
  476. * Change the resize strategy
  477. * @param strategyName name of the new strategy
  478. */
  479. setStrategyByName(strategyName: ViewportStyle): void
  480. {
  481. switch(strategyName) {
  482. case 'best-fit':
  483. this.setStrategy(new BestFitResizeStrategy());
  484. break;
  485. case 'stretch':
  486. this.setStrategy(new StretchResizeStrategy());
  487. break;
  488. case 'inline':
  489. this.setStrategy(new InlineResizeStrategy());
  490. break;
  491. default:
  492. throw new IllegalArgumentError('Invalid viewport style: ' + strategyName);
  493. }
  494. }
  495. /**
  496. * Resize callback
  497. */
  498. private _onResize(): void
  499. {
  500. const viewport = this._viewport;
  501. // Resize the drawing buffer of the foreground canvas, so that it
  502. // matches the desired resolution, as well as the aspect ratio of the
  503. // background canvas
  504. const foregroundCanvas = viewport.canvas;
  505. const virtualSize = viewport.virtualSize;
  506. foregroundCanvas.width = virtualSize.width;
  507. foregroundCanvas.height = virtualSize.height;
  508. // Resize the drawing buffer of the background canvas
  509. const backgroundCanvas = viewport._backgroundCanvas;
  510. const realSize = viewport._realSize;
  511. backgroundCanvas.width = realSize.width;
  512. backgroundCanvas.height = realSize.height;
  513. // Call strategy
  514. this._resizeStrategy.resize(viewport);
  515. }
  516. }
  517. /**
  518. * Resize strategies
  519. */
  520. abstract class ViewportResizeStrategy
  521. {
  522. /**
  523. * Resize the viewport
  524. * @param viewport
  525. */
  526. abstract resize(viewport: Viewport): void;
  527. /**
  528. * Clear CSS rules
  529. * @param viewport
  530. */
  531. clear(viewport: Viewport): void
  532. {
  533. viewport.container.style.cssText = '';
  534. viewport._subContainer.style.cssText = '';
  535. }
  536. }
  537. /**
  538. * Inline viewport: it follows the typical flow of a web page
  539. */
  540. class InlineResizeStrategy extends ViewportResizeStrategy
  541. {
  542. /**
  543. * Resize the viewport
  544. * @param viewport
  545. */
  546. resize(viewport: Viewport): void
  547. {
  548. const container = viewport.container;
  549. const subContainer = viewport._subContainer;
  550. const virtualSize = viewport.virtualSize;
  551. container.style.display = 'inline-block'; // fixes a potential issue of the viewport not showing up
  552. container.style.position = 'relative';
  553. container.style.left = '0px';
  554. container.style.top = '0px';
  555. container.style.width = virtualSize.width + 'px';
  556. container.style.height = virtualSize.height + 'px';
  557. subContainer.style.position = 'absolute';
  558. subContainer.style.left = '0px';
  559. subContainer.style.top = '0px';
  560. subContainer.style.width = '100%';
  561. subContainer.style.height = '100%';
  562. }
  563. }
  564. /**
  565. * Immersive viewport: it occupies the entire page
  566. */
  567. abstract class ImmersiveResizeStrategy extends ViewportResizeStrategy
  568. {
  569. /**
  570. * Resize the viewport
  571. * @param viewport
  572. */
  573. resize(viewport: Viewport): void
  574. {
  575. const CONTAINER_ZINDEX = 1000000000;
  576. const container = viewport.container;
  577. container.style.position = 'fixed';
  578. container.style.left = '0px';
  579. container.style.top = '0px';
  580. container.style.width = '100vw';
  581. container.style.height = '100vh';
  582. container.style.zIndex = String(CONTAINER_ZINDEX);
  583. }
  584. }
  585. /**
  586. * Immersive viewport with best-fit style: it occupies the entire page and
  587. * preserves the aspect ratio of the media
  588. */
  589. class BestFitResizeStrategy extends ImmersiveResizeStrategy
  590. {
  591. /**
  592. * Resize the viewport
  593. * @param viewport
  594. */
  595. resize(viewport: Viewport): void
  596. {
  597. const subContainer = viewport._subContainer;
  598. const windowAspectRatio = window.innerWidth / window.innerHeight;
  599. const viewportAspectRatio = viewport._realSize.width / viewport._realSize.height;
  600. let width = 1, height = 1, left = '0px', top = '0px';
  601. if(viewportAspectRatio <= windowAspectRatio) {
  602. height = window.innerHeight;
  603. width = Math.round(height * viewportAspectRatio);
  604. width -= width % 2;
  605. left = `calc(50% - ${width >>> 1}px)`;
  606. }
  607. else {
  608. width = window.innerWidth;
  609. height = Math.round(width / viewportAspectRatio);
  610. height -= height % 2;
  611. top = `calc(50% - ${height >>> 1}px)`;
  612. }
  613. subContainer.style.position = 'absolute';
  614. subContainer.style.left = left;
  615. subContainer.style.top = top;
  616. subContainer.style.width = width + 'px';
  617. subContainer.style.height = height + 'px';
  618. super.resize(viewport);
  619. }
  620. }
  621. /**
  622. * Immersive viewport with stretch style: it occupies the entire page and
  623. * fully stretches the media
  624. */
  625. class StretchResizeStrategy extends ImmersiveResizeStrategy
  626. {
  627. /**
  628. * Resize the viewport
  629. * @param viewport
  630. */
  631. resize(viewport: Viewport): void
  632. {
  633. const subContainer = viewport._subContainer;
  634. subContainer.style.position = 'absolute';
  635. subContainer.style.left = '0px';
  636. subContainer.style.top = '0px';
  637. subContainer.style.width = window.innerWidth + 'px';
  638. subContainer.style.height = window.innerHeight + 'px';
  639. super.resize(viewport);
  640. }
  641. }
  642. /**
  643. * Viewport
  644. */
  645. export class Viewport extends ViewportEventTarget
  646. {
  647. /** Viewport resolution (controls the size of the drawing buffer of the foreground canvas) */
  648. private readonly _resolution: Resolution;
  649. /** The containers */
  650. private readonly _containers: ViewportContainers;
  651. /** An overlay displayed in front of the augmented scene */
  652. private readonly _hud: HUD;
  653. /** Viewport style */
  654. private _style: ViewportStyle;
  655. /** The canvases of the viewport */
  656. private readonly _canvases: ViewportCanvases;
  657. /** Resize helper */
  658. private readonly _resizer: ViewportResizer;
  659. /** The current size of the underlying SpeedyMedia */
  660. private _mediaSize: ViewportSizeGetter;
  661. /** Fullscreen utilities */
  662. private readonly _fullscreen: ViewportFullscreenHelper;
  663. /** Built-in fullscreen button */
  664. private readonly _fullscreenButton: Nullable<FullscreenButton>;
  665. /**
  666. * Constructor
  667. * @param viewportSettings
  668. */
  669. constructor(viewportSettings: ViewportSettings)
  670. {
  671. const settings = Object.assign({}, DEFAULT_VIEWPORT_SETTINGS, viewportSettings);
  672. super();
  673. const guessedAspectRatio = window.innerWidth / window.innerHeight;
  674. const initialSize = Utils.resolution(settings.resolution, guessedAspectRatio);
  675. this._mediaSize = () => initialSize;
  676. this._resolution = settings.resolution;
  677. this._style = settings.style;
  678. this._containers = new ViewportContainers(settings.container);
  679. this._hud = new HUD(this._subContainer, settings.hudContainer);
  680. this._canvases = new ViewportCanvases(this._subContainer, initialSize, settings.canvas);
  681. this._resizer = new ViewportResizer(this);
  682. this._resizer.setStrategyByName(this._style);
  683. this._fullscreen = new ViewportFullscreenHelper(this);
  684. this._fullscreenButton = null;
  685. if(settings.fullscreenUI && this.fullscreenAvailable)
  686. this._fullscreenButton = new FullscreenButton(this);
  687. }
  688. /**
  689. * Viewport container
  690. */
  691. get container(): ViewportContainer
  692. {
  693. return this._containers.container;
  694. }
  695. /**
  696. * Viewport style
  697. */
  698. get style(): ViewportStyle
  699. {
  700. return this._style;
  701. }
  702. /**
  703. * Set viewport style
  704. */
  705. /*
  706. set style(value: ViewportStyle)
  707. {
  708. // note: the viewport style is independent of the session mode!
  709. if(value !== this._style) {
  710. this._resizer.setStrategyByName(value);
  711. this._style = value;
  712. }
  713. }
  714. */
  715. /**
  716. * HUD
  717. */
  718. get hud(): HUD
  719. {
  720. return this._hud;
  721. }
  722. /**
  723. * Resolution of the virtual scene
  724. */
  725. get resolution(): Resolution
  726. {
  727. return this._resolution;
  728. }
  729. /**
  730. * Size in pixels of the drawing buffer of the canvas
  731. * on which the virtual scene will be drawn
  732. */
  733. get virtualSize(): SpeedySize
  734. {
  735. const size = this._realSize;
  736. const aspectRatio = size.width / size.height;
  737. return Utils.resolution(this._resolution, aspectRatio);
  738. }
  739. /**
  740. * Is the viewport currently being displayed in fullscreen mode?
  741. */
  742. get fullscreen(): boolean
  743. {
  744. return this._fullscreen.isActivated();
  745. }
  746. /**
  747. * Is the fullscreen mode available in this platform?
  748. */
  749. get fullscreenAvailable(): boolean
  750. {
  751. return this._fullscreen.isAvailable();
  752. }
  753. /**
  754. * The canvas on which the virtual scene will be drawn
  755. */
  756. get canvas(): HTMLCanvasElement
  757. {
  758. return this._canvases.foregroundCanvas;
  759. }
  760. /**
  761. * The canvas on which the physical scene will be drawn
  762. * @internal
  763. */
  764. get _backgroundCanvas(): HTMLCanvasElement
  765. {
  766. return this._canvases.backgroundCanvas;
  767. }
  768. /**
  769. * Size of the drawing buffer of the background canvas, in pixels
  770. * @internal
  771. */
  772. get _realSize(): SpeedySize
  773. {
  774. return this._mediaSize();
  775. }
  776. /**
  777. * Sub-container of the viewport container
  778. * @internal
  779. */
  780. get _subContainer(): HTMLDivElement
  781. {
  782. return this._containers.subContainer;
  783. }
  784. /**
  785. * Request fullscreen mode
  786. * @returns promise
  787. */
  788. requestFullscreen(): SpeedyPromise<void>
  789. {
  790. return this._fullscreen.request();
  791. }
  792. /**
  793. * Exit fullscreen mode
  794. * @returns promise
  795. */
  796. exitFullscreen(): SpeedyPromise<void>
  797. {
  798. return this._fullscreen.exit();
  799. }
  800. /**
  801. * Convert a position given in normalized units to a corresponding pixel
  802. * position in canvas space. Normalized units range from -1 to +1. The
  803. * center of the canvas is at (0,0). The top right corner is at (1,1).
  804. * The bottom left corner is at (-1,-1).
  805. * @param position in normalized units
  806. * @returns an equivalent pixel position in canvas space
  807. */
  808. convertToPixels(position: Vector2): Vector2
  809. {
  810. const canvas = this.canvas;
  811. const x = 0.5 * (1 + position.x) * canvas.width;
  812. const y = -0.5 * (1 + position.y) * canvas.height;
  813. return new Vector2(x, y);
  814. }
  815. /**
  816. * Initialize the viewport (when the session starts)
  817. * @param getMediaSize
  818. * @param sessionMode
  819. * @internal
  820. */
  821. _init(getMediaSize: ViewportSizeGetter, sessionMode: SessionMode): void
  822. {
  823. // validate if the viewport style matches the session mode
  824. if(sessionMode == 'immersive') {
  825. if(this._style != 'best-fit' && this._style != 'stretch') {
  826. Utils.warning(`Invalid viewport style \"${this._style}\" for the \"${sessionMode}\" mode`);
  827. this._style = 'best-fit';
  828. this._resizer.setStrategyByName(this._style);
  829. }
  830. }
  831. else if(sessionMode == 'inline') {
  832. if(this._style != 'inline') {
  833. Utils.warning(`Invalid viewport style \"${this._style}\" for the \"${sessionMode}\" mode`);
  834. this._style = 'inline';
  835. this._resizer.setStrategyByName(this._style);
  836. }
  837. }
  838. // set the media size getter
  839. this._mediaSize = getMediaSize;
  840. // initialize the components
  841. this._containers.init();
  842. this._hud._init(HUD_ZINDEX);
  843. this._canvases.init();
  844. this._resizer.init();
  845. this._fullscreen.init();
  846. this._fullscreenButton?.init();
  847. }
  848. /**
  849. * Release the viewport (when the session ends)
  850. * @internal
  851. */
  852. _release(): void
  853. {
  854. this._fullscreenButton?.release();
  855. this._fullscreen.release();
  856. this._resizer.release();
  857. this._canvases.release();
  858. this._hud._release();
  859. this._containers.release();
  860. }
  861. }