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 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944
  1. /*
  2. * MARTINS.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 Speedy from 'speedy-vision';
  23. import { SpeedySize } from 'speedy-vision/types/core/speedy-size';
  24. import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
  25. import { Nullable } from '../utils/utils';
  26. import { Resolution } from './resolution';
  27. import { Utils } from '../utils/utils';
  28. import { IllegalArgumentError, IllegalOperationError, NotSupportedError, AccessDeniedError } from '../utils/errors';
  29. import { HUD, HUDContainer } from './hud';
  30. import { AREvent, AREventTarget, AREventListener } from '../utils/ar-events';
  31. /** Viewport container */
  32. export type ViewportContainer = HTMLDivElement;
  33. /** We admit that the size of the drawing buffer of the background canvas of the viewport may change over time */
  34. type ViewportSizeGetter = () => SpeedySize;
  35. /** All possible event types emitted by a Viewport */
  36. type ViewportEventType = 'resize';
  37. /** An event emitted by a Viewport */
  38. class ViewportEvent extends AREvent<ViewportEventType> { }
  39. /** Viewport event target */
  40. class ViewportEventTarget extends AREventTarget<ViewportEventType> { }
  41. /** Viewport style (immersive mode) */
  42. type ViewportStyle = 'best-fit' | 'stretch' | 'inline';
  43. /**
  44. * Viewport interface
  45. */
  46. export interface Viewport extends ViewportEventTarget
  47. {
  48. /** Resolution of the virtual scene */
  49. readonly resolution: Resolution;
  50. /** Viewport container */
  51. readonly container: ViewportContainer;
  52. /** Viewport style */
  53. style: ViewportStyle;
  54. /** HUD */
  55. readonly hud: HUD;
  56. /** Fullscreen mode */
  57. readonly fullscreen: boolean;
  58. /** Canvas on which the virtual scene will be drawn */
  59. readonly canvas: HTMLCanvasElement;
  60. /** Size of the drawing buffer of the foreground canvas */
  61. readonly virtualSize: SpeedySize;
  62. /** Request fullscreen mode */
  63. requestFullscreen(): SpeedyPromise<void>;
  64. /** Exit fullscreen mode */
  65. exitFullscreen(): SpeedyPromise<void>;
  66. /** Is the fullscreen mode available? */
  67. isFullscreenAvailable(): boolean;
  68. /** Canvas on which the physical scene will be drawn @internal */
  69. readonly _backgroundCanvas: HTMLCanvasElement;
  70. /** Size of the drawing buffer of the background canvas @internal */
  71. readonly _realSize: SpeedySize;
  72. /** Initialize the viewport @internal */
  73. _init(): void;
  74. /** Release the viewport @internal */
  75. _release(): void;
  76. }
  77. /**
  78. * Viewport constructor settings
  79. */
  80. export interface ViewportSettings
  81. {
  82. /** Viewport container */
  83. container: Nullable<ViewportContainer>;
  84. /** HUD container (must be a direct child of container) */
  85. hudContainer?: Nullable<HUDContainer>;
  86. /** Resolution of the canvas on which the virtual scene will be drawn */
  87. resolution?: Resolution;
  88. /** Viewport style */
  89. style?: ViewportStyle;
  90. /** An existing <canvas> on which the virtual scene will be drawn */
  91. canvas?: Nullable<HTMLCanvasElement>;
  92. }
  93. /** Default viewport constructor settings */
  94. const DEFAULT_VIEWPORT_SETTINGS: Readonly<Required<ViewportSettings>> = {
  95. container: null,
  96. hudContainer: null,
  97. resolution: 'lg',
  98. style: 'best-fit',
  99. canvas: null,
  100. };
  101. /** Z-index of the viewport container */
  102. const CONTAINER_ZINDEX = 1000000000;
  103. /** Base z-index of the children of the viewport container */
  104. const BASE_ZINDEX = 0;
  105. /** Z-index of the background canvas */
  106. const BACKGROUND_ZINDEX = BASE_ZINDEX + 0;
  107. /** Z-index of the foreground canvas */
  108. const FOREGROUND_ZINDEX = BASE_ZINDEX + 1;
  109. /** Z-index of the HUD */
  110. const HUD_ZINDEX = BASE_ZINDEX + 2;
  111. /** Default viewport width, in pixels */
  112. const DEFAULT_VIEWPORT_WIDTH = 300;
  113. /** Default viewport height, in pixels */
  114. const DEFAULT_VIEWPORT_HEIGHT = 150;
  115. /**
  116. * Viewport
  117. */
  118. export class BaseViewport extends ViewportEventTarget implements Viewport
  119. {
  120. /** Viewport resolution (controls the size of the drawing buffer of the foreground canvas) */
  121. private readonly _resolution: Resolution;
  122. /** Viewport container */
  123. protected readonly _container: ViewportContainer;
  124. /** An overlay displayed in front of the augmented scene */
  125. protected readonly _hud: HUD;
  126. /** Viewport style */
  127. protected _style: ViewportStyle;
  128. /** Internal canvas used to render the physical scene */
  129. private readonly __backgroundCanvas: HTMLCanvasElement;
  130. /** A canvas used to render the virtual scene */
  131. protected readonly _foregroundCanvas: HTMLCanvasElement;
  132. /** Original parent of the foreground canvas, if it's imported from somewhere */
  133. private readonly _parentOfImportedForegroundCanvas: Nullable<Node>;
  134. /**
  135. * Constructor
  136. * @param viewportSettings
  137. */
  138. constructor(viewportSettings: ViewportSettings)
  139. {
  140. super();
  141. const settings = Object.assign({}, DEFAULT_VIEWPORT_SETTINGS, viewportSettings);
  142. const size = Speedy.Size(DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT);
  143. // validate settings
  144. if(settings.container == null)
  145. throw new IllegalArgumentError('Unspecified viewport container');
  146. else if(!(settings.container instanceof HTMLElement))
  147. throw new IllegalArgumentError('Invalid viewport container');
  148. // initialize attributes
  149. this._resolution = settings.resolution;
  150. this._container = settings.container;
  151. this._hud = new HUD(settings.container, settings.hudContainer);
  152. // make this more elegant?
  153. // need to initialize this._style and validate settings.style
  154. this._style = DEFAULT_VIEWPORT_SETTINGS.style;
  155. this.style = settings.style;
  156. // create the background canvas
  157. this.__backgroundCanvas = this._createBackgroundCanvas(this._container, size);
  158. // create the foreground canvas
  159. if(settings.canvas == null) {
  160. this._foregroundCanvas = this._createForegroundCanvas(this._container, size);
  161. this._parentOfImportedForegroundCanvas = null;
  162. }
  163. else {
  164. this._foregroundCanvas = settings.canvas;
  165. this._parentOfImportedForegroundCanvas = settings.canvas.parentNode;
  166. }
  167. }
  168. /**
  169. * Make a request to the user agent so that the viewport container is
  170. * displayed in fullscreen mode. The container must be a compatible element[1]
  171. * and the user must interact with the page in order to comply with browser
  172. * policies[2]. In case of error, the returned promise is rejected.
  173. * [1] https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#compatible_elements
  174. * [2] https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen#security
  175. */
  176. requestFullscreen(): SpeedyPromise<void>
  177. {
  178. const container = this._container;
  179. // fallback for older WebKit versions
  180. if(container.requestFullscreen === undefined) {
  181. if((container as any).webkitRequestFullscreen === undefined)
  182. return Speedy.Promise.reject(new NotSupportedError());
  183. else if(!(document as any).webkitFullscreenEnabled)
  184. return Speedy.Promise.reject(new AccessDeniedError());
  185. // webkitRequestFullscreen() does not return a value
  186. (container as any).webkitRequestFullscreen();
  187. return new Speedy.Promise<void>((resolve, reject) => {
  188. setTimeout(() => {
  189. if(container === (document as any).webkitFullscreenElement)
  190. resolve();
  191. else
  192. reject(new TypeError());
  193. }, 100);
  194. });
  195. }
  196. // check if the fullscreen mode is available
  197. if(!document.fullscreenEnabled)
  198. return Speedy.Promise.reject(new AccessDeniedError());
  199. // request fullscreen
  200. return new Speedy.Promise<void>((resolve, reject) => {
  201. container.requestFullscreen({
  202. navigationUI: 'hide'
  203. }).then(resolve, reject);
  204. });
  205. }
  206. /**
  207. * Exit fullscreen mode
  208. */
  209. exitFullscreen(): SpeedyPromise<void>
  210. {
  211. // fallback for older WebKit versions
  212. if(document.exitFullscreen === undefined) {
  213. const doc = document as any;
  214. if(doc.webkitExitFullscreen === undefined)
  215. return Speedy.Promise.reject(new NotSupportedError());
  216. else if(doc.webkitFullscreenElement === null)
  217. return Speedy.Promise.reject(new IllegalOperationError('Not in fullscreen mode'));
  218. // webkitExitFullscreen() does not return a value
  219. doc.webkitExitFullscreen();
  220. return new Speedy.Promise<void>((resolve, reject) => {
  221. setTimeout(() => {
  222. if(doc.webkitFullscreenElement === null)
  223. resolve();
  224. else
  225. reject(new TypeError());
  226. }, 100);
  227. });
  228. }
  229. // exit fullscreen
  230. return new Speedy.Promise<void>((resolve, reject) => {
  231. document.exitFullscreen().then(resolve, reject);
  232. });
  233. }
  234. /** Is the fullscreen mode available? */
  235. isFullscreenAvailable(): boolean
  236. {
  237. return document.fullscreenEnabled ||
  238. !!((document as any).webkitFullscreenEnabled);
  239. }
  240. /**
  241. * True if the viewport is being displayed in fullscreen mode
  242. */
  243. get fullscreen(): boolean
  244. {
  245. if(document.fullscreenElement !== undefined)
  246. return document.fullscreenElement === this._container;
  247. else if((document as any).webkitFullscreenElement !== undefined)
  248. return (document as any).webkitFullscreenElement === this._container;
  249. else
  250. return false;
  251. }
  252. /**
  253. * Viewport container
  254. */
  255. get container(): ViewportContainer
  256. {
  257. return this._container;
  258. }
  259. /**
  260. * Viewport style
  261. */
  262. get style(): ViewportStyle
  263. {
  264. return this._style;
  265. }
  266. /**
  267. * Set viewport style
  268. */
  269. set style(value: ViewportStyle)
  270. {
  271. if(value != 'best-fit' && value != 'stretch' && value != 'inline')
  272. throw new IllegalArgumentError('Invalid viewport style: ' + value);
  273. const changed = (value != this._style);
  274. this._style = value;
  275. if(changed) {
  276. const event = new ViewportEvent('resize');
  277. this.dispatchEvent(event);
  278. }
  279. }
  280. /**
  281. * HUD
  282. */
  283. get hud(): HUD
  284. {
  285. return this._hud;
  286. }
  287. /**
  288. * Resolution of the virtual scene
  289. */
  290. get resolution(): Resolution
  291. {
  292. return this._resolution;
  293. }
  294. /**
  295. * Size in pixels of the drawing buffer of the canvas
  296. * on which the virtual scene will be drawn
  297. */
  298. get virtualSize(): SpeedySize
  299. {
  300. const aspectRatio = this._backgroundCanvas.width / this._backgroundCanvas.height;
  301. return Utils.resolution(this._resolution, aspectRatio);
  302. }
  303. /**
  304. * The canvas on which the virtual scene will be drawn
  305. */
  306. get canvas(): HTMLCanvasElement
  307. {
  308. return this._foregroundCanvas;
  309. }
  310. /**
  311. * The canvas on which the physical scene will be drawn
  312. * @internal
  313. */
  314. get _backgroundCanvas(): HTMLCanvasElement
  315. {
  316. return this.__backgroundCanvas;
  317. }
  318. /**
  319. * Size of the drawing buffer of the background canvas, in pixels
  320. * @internal
  321. */
  322. get _realSize(): SpeedySize
  323. {
  324. throw new IllegalOperationError();
  325. }
  326. /**
  327. * Initialize the viewport (when the session starts)
  328. * @internal
  329. */
  330. _init(): void
  331. {
  332. // import foreground canvas
  333. if(this._parentOfImportedForegroundCanvas != null) {
  334. const size = Speedy.Size(DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT);
  335. this._importForegroundCanvas(this._foregroundCanvas, this._container, size);
  336. }
  337. // setup CSS
  338. this._container.style.touchAction = 'none';
  339. this._container.style.backgroundColor = 'black';
  340. this._container.style.zIndex = String(CONTAINER_ZINDEX);
  341. // initialize the HUD
  342. this._hud._init(HUD_ZINDEX);
  343. this._hud.visible = true;
  344. }
  345. /**
  346. * Release the viewport (when the session starts)
  347. * @internal
  348. */
  349. _release(): void
  350. {
  351. // release the HUD
  352. this._hud._release();
  353. // reset the CSS
  354. this._container.style.touchAction = 'auto';
  355. // restore imported canvas
  356. if(this._parentOfImportedForegroundCanvas != null)
  357. this._restoreImportedForegroundCanvas();
  358. }
  359. /**
  360. * Create a canvas and attach it to another HTML element
  361. * @param parent parent container
  362. * @param size size of the drawing buffer
  363. * @returns a new canvas as a child of parent
  364. */
  365. private _createCanvas(parent: HTMLElement, size: SpeedySize): HTMLCanvasElement
  366. {
  367. const canvas = document.createElement('canvas') as HTMLCanvasElement;
  368. canvas.width = size.width;
  369. canvas.height = size.height;
  370. parent.appendChild(canvas);
  371. return canvas;
  372. }
  373. /**
  374. * Create the background canvas
  375. * @param parent parent container
  376. * @param size size of the drawing buffer
  377. * @returns a new canvas as a child of parent
  378. */
  379. private _createBackgroundCanvas(parent: ViewportContainer, size: SpeedySize): HTMLCanvasElement
  380. {
  381. const canvas = this._createCanvas(parent, size);
  382. return this._styleCanvas(canvas, BACKGROUND_ZINDEX);
  383. }
  384. /**
  385. * Create the foreground canvas
  386. * @param parent parent container
  387. * @param size size of the drawing buffer
  388. * @returns a new canvas as a child of parent
  389. */
  390. private _createForegroundCanvas(parent: ViewportContainer, size: SpeedySize): HTMLCanvasElement
  391. {
  392. const canvas = this._createCanvas(parent, size);
  393. return this._styleCanvas(canvas, FOREGROUND_ZINDEX);
  394. }
  395. /**
  396. * Import an existing foreground canvas to the viewport
  397. * @param canvas existing canvas
  398. * @param parent parent container
  399. * @param size size of the drawing buffer
  400. * @returns the input canvas
  401. */
  402. private _importForegroundCanvas(canvas: HTMLCanvasElement, parent: ViewportContainer, size: SpeedySize): HTMLCanvasElement
  403. {
  404. if(!(canvas instanceof HTMLCanvasElement))
  405. throw new IllegalArgumentError('Not a canvas: ' + canvas);
  406. // borrow the canvas; add it as a child of the viewport container
  407. canvas.remove();
  408. parent.appendChild(canvas);
  409. canvas.width = size.width;
  410. canvas.height = size.height;
  411. canvas.dataset.cssText = canvas.style.cssText; // save CSS
  412. canvas.style.cssText = ''; // clear CSS
  413. this._styleCanvas(canvas, FOREGROUND_ZINDEX);
  414. return canvas;
  415. }
  416. /**
  417. * Restore a previously imported foreground canvas to its original parent
  418. */
  419. private _restoreImportedForegroundCanvas(): void
  420. {
  421. // not an imported canvas; nothing to do
  422. if(this._parentOfImportedForegroundCanvas == null)
  423. throw new IllegalOperationError();
  424. const canvas = this._foregroundCanvas;
  425. canvas.style.cssText = canvas.dataset.cssText || ''; // restore CSS
  426. canvas.remove();
  427. this._parentOfImportedForegroundCanvas.appendChild(canvas);
  428. }
  429. /**
  430. * Add suitable CSS rules to a canvas
  431. * @param canvas
  432. * @param canvasType
  433. * @returns canvas
  434. */
  435. private _styleCanvas(canvas: HTMLCanvasElement, zIndex: number): HTMLCanvasElement
  436. {
  437. canvas.style.position = 'absolute';
  438. canvas.style.left = '0px';
  439. canvas.style.top = '0px';
  440. canvas.style.width = '100%';
  441. canvas.style.height = '100%';
  442. canvas.style.zIndex = String(zIndex);
  443. return canvas;
  444. }
  445. }
  446. /**
  447. * Viewport decorator
  448. */
  449. abstract class ViewportDecorator extends ViewportEventTarget implements Viewport
  450. {
  451. /** The decorated viewport */
  452. private _base: Viewport;
  453. /** Size getter (the size of the viewport may change over time) */
  454. private _getSize: ViewportSizeGetter;
  455. /**
  456. * Constructor
  457. * @param base to be decorated
  458. * @param getSize size getter
  459. */
  460. constructor(base: Viewport, getSize: ViewportSizeGetter)
  461. {
  462. super();
  463. this._base = base;
  464. this._getSize = getSize;
  465. }
  466. /**
  467. * Viewport container
  468. */
  469. get container(): ViewportContainer
  470. {
  471. return this._base.container;
  472. }
  473. /**
  474. * Viewport style
  475. */
  476. get style(): ViewportStyle
  477. {
  478. return this._base.style;
  479. }
  480. /**
  481. * Set viewport style
  482. */
  483. set style(value: ViewportStyle)
  484. {
  485. this._base.style = value;
  486. }
  487. /**
  488. * HUD
  489. */
  490. get hud(): HUD
  491. {
  492. return this._base.hud;
  493. }
  494. /**
  495. * Fullscreen mode
  496. */
  497. get fullscreen(): boolean
  498. {
  499. return this._base.fullscreen;
  500. }
  501. /**
  502. * Resolution of the virtual scene
  503. */
  504. get resolution(): Resolution
  505. {
  506. return this._base.resolution;
  507. }
  508. /**
  509. * Size in pixels of the drawing buffer of the canvas
  510. * on which the virtual scene will be drawn
  511. */
  512. get virtualSize(): SpeedySize
  513. {
  514. return this._base.virtualSize;
  515. }
  516. /**
  517. * The canvas on which the virtual scene will be drawn
  518. */
  519. get canvas(): HTMLCanvasElement
  520. {
  521. return this._base.canvas;
  522. }
  523. /**
  524. * Request fullscreen mode
  525. */
  526. requestFullscreen(): SpeedyPromise<void>
  527. {
  528. return this._base.requestFullscreen();
  529. }
  530. /**
  531. * Exit fullscreen mode
  532. */
  533. exitFullscreen(): SpeedyPromise<void>
  534. {
  535. return this._base.exitFullscreen();
  536. }
  537. /**
  538. * Is the fullscreen mode available?
  539. */
  540. isFullscreenAvailable(): boolean
  541. {
  542. return this._base.isFullscreenAvailable();
  543. }
  544. /**
  545. * Background canvas
  546. * @internal
  547. */
  548. get _backgroundCanvas(): HTMLCanvasElement
  549. {
  550. return this._base._backgroundCanvas;
  551. }
  552. /**
  553. * Size of the drawing buffer of the background canvas, in pixels
  554. * @internal
  555. */
  556. get _realSize(): SpeedySize
  557. {
  558. return this._getSize();
  559. }
  560. /**
  561. * Initialize the viewport
  562. * @internal
  563. */
  564. _init(): void
  565. {
  566. this._base._init();
  567. }
  568. /**
  569. * Release the viewport
  570. * @internal
  571. */
  572. _release(): void
  573. {
  574. this._base._release();
  575. }
  576. /**
  577. * Add event listener
  578. * @param type event type
  579. * @param callback
  580. */
  581. addEventListener(type: ViewportEventType, callback: AREventListener): void
  582. {
  583. this._base.addEventListener(type, callback);
  584. }
  585. /**
  586. * Remove event listener
  587. * @param type event type
  588. * @param callback
  589. */
  590. removeEventListener(type: ViewportEventType, callback: AREventListener): void
  591. {
  592. this._base.removeEventListener(type, callback);
  593. }
  594. /**
  595. * Synchronously trigger an event
  596. * @param event
  597. * @returns same value as a standard event target
  598. * @internal
  599. */
  600. dispatchEvent(event: ViewportEvent): boolean
  601. {
  602. return this._base.dispatchEvent(event);
  603. }
  604. }
  605. /**
  606. * A viewport that watches for page resizes
  607. */
  608. abstract class ResizableViewport extends ViewportDecorator
  609. {
  610. /** is this viewport subject to being resized? */
  611. private _active: boolean;
  612. /**
  613. * Constructor
  614. * @param base to be decorated
  615. * @param getSize size getter
  616. */
  617. constructor(base: BaseViewport, getSize: ViewportSizeGetter)
  618. {
  619. super(base, getSize);
  620. this._active = false;
  621. this.addEventListener('resize', this._onResize.bind(this));
  622. }
  623. /**
  624. * Initialize the viewport
  625. * @internal
  626. */
  627. _init(): void
  628. {
  629. super._init();
  630. this._active = true;
  631. // Configure the resize listener. We want the viewport
  632. // to adjust itself if the phone/screen is resized or
  633. // changes orientation
  634. let timeout: Nullable<ReturnType<typeof setTimeout>> = null;
  635. const onWindowResize = () => {
  636. if(!this._active) {
  637. window.removeEventListener('resize', onWindowResize);
  638. return;
  639. }
  640. if(timeout !== null)
  641. clearTimeout(timeout);
  642. timeout = setTimeout(() => {
  643. timeout = null;
  644. this._resize();
  645. }, 50);
  646. };
  647. window.addEventListener('resize', onWindowResize);
  648. // handle changes of orientation
  649. // (is this needed? we already listen to resize events)
  650. if(screen.orientation !== undefined)
  651. screen.orientation.addEventListener('change', this._resize.bind(this));
  652. else
  653. window.addEventListener('orientationchange', this._resize.bind(this)); // deprecated
  654. // trigger a resize to setup the sizes / the CSS
  655. this._resize();
  656. }
  657. /**
  658. * Release the viewport
  659. * @internal
  660. */
  661. _release(): void
  662. {
  663. if(screen.orientation !== undefined)
  664. screen.orientation.removeEventListener('change', this._resize);
  665. else
  666. window.removeEventListener('orientationchange', this._resize); // deprecated
  667. this._active = false;
  668. super._release();
  669. }
  670. /**
  671. * Trigger a resize event
  672. */
  673. private _resize(): void
  674. {
  675. const event = new ViewportEvent('resize');
  676. this.dispatchEvent(event);
  677. }
  678. /**
  679. * Function to be called when the viewport is resized
  680. */
  681. protected _onResize(): void
  682. {
  683. // Resize the drawing buffer of the foreground canvas, so that it
  684. // matches the desired resolution, as well as the aspect ratio of the
  685. // background canvas
  686. const foregroundCanvas = this.canvas;
  687. const virtualSize = this.virtualSize;
  688. foregroundCanvas.width = virtualSize.width;
  689. foregroundCanvas.height = virtualSize.height;
  690. // Resize the drawing buffer of the background canvas
  691. const backgroundCanvas = this._backgroundCanvas;
  692. const realSize = this._realSize;
  693. backgroundCanvas.width = realSize.width;
  694. backgroundCanvas.height = realSize.height;
  695. }
  696. }
  697. /**
  698. * Immersive viewport: it occupies the entire page
  699. */
  700. export class ImmersiveViewport extends ResizableViewport
  701. {
  702. /**
  703. * Release the viewport
  704. * @internal
  705. */
  706. _release(): void
  707. {
  708. this.canvas.remove();
  709. this._backgroundCanvas.remove();
  710. this.hud.visible = false;
  711. this.container.style.cssText = ''; // reset CSS
  712. super._release();
  713. }
  714. /**
  715. * Resize the immersive viewport, so that it occupies the entire page.
  716. * We respect the aspect ratio of the source media
  717. */
  718. protected _onResize(): void
  719. {
  720. super._onResize();
  721. const container = this.container;
  722. container.style.position = 'fixed';
  723. if(this.style == 'best-fit') {
  724. // cover the page while maintaining the aspect ratio
  725. let viewportWidth = 0, viewportHeight = 0;
  726. const windowAspectRatio = window.innerWidth / window.innerHeight;
  727. const viewportAspectRatio = this._realSize.width / this._realSize.height;
  728. if(viewportAspectRatio <= windowAspectRatio) {
  729. viewportHeight = window.innerHeight;
  730. viewportWidth = (viewportHeight * viewportAspectRatio) | 0;
  731. }
  732. else {
  733. viewportWidth = window.innerWidth;
  734. viewportHeight = (viewportWidth / viewportAspectRatio) | 0;
  735. }
  736. container.style.left = `calc(50% - ${(viewportWidth+1) >>> 1}px)`;
  737. container.style.top = `calc(50% - ${(viewportHeight+1) >>> 1}px)`;
  738. container.style.width = viewportWidth + 'px';
  739. container.style.height = viewportHeight + 'px';
  740. }
  741. else if(this.style == 'stretch') {
  742. // stretch to cover the entire page
  743. container.style.left = '0px';
  744. container.style.top = '0px';
  745. container.style.width = window.innerWidth + 'px';
  746. container.style.height = window.innerHeight + 'px';
  747. }
  748. else
  749. throw new IllegalOperationError('Invalid immersive viewport style: ' + this.style);
  750. }
  751. }
  752. /**
  753. * Inline viewport: it follows the typical flow of a web page
  754. */
  755. export class InlineViewport extends ResizableViewport
  756. {
  757. /**
  758. * Initialize the viewport
  759. * @internal
  760. */
  761. _init(): void
  762. {
  763. super._init();
  764. this.style = 'inline';
  765. }
  766. /**
  767. * Release the viewport
  768. * @internal
  769. */
  770. _release(): void
  771. {
  772. this.container.style.cssText = ''; // reset CSS
  773. super._release();
  774. }
  775. /**
  776. * Resize the inline viewport
  777. * (we still take orientation changes into account)
  778. */
  779. protected _onResize(): void
  780. {
  781. super._onResize();
  782. const container = this.container;
  783. container.style.position = 'relative';
  784. if(this.style == 'inline') {
  785. container.style.left = '0px';
  786. container.style.top = '0px';
  787. container.style.width = this.virtualSize.width + 'px';
  788. container.style.height = this.virtualSize.height + 'px';
  789. }
  790. else
  791. throw new IllegalOperationError('Invalid inline viewport style: ' + this.style);
  792. }
  793. }