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.

stats-panel.ts 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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. * stats-panel.ts
  20. * Stats panel used for development purposes
  21. */
  22. import { SpeedySize } from 'speedy-vision/types/core/speedy-size';
  23. import { Settings, PowerPreference } from '../core/settings';
  24. import { Viewport } from '../core/viewport';
  25. import { Tracker } from '../trackers/tracker';
  26. import { Source } from '../sources/source';
  27. import { Nullable } from '../utils/utils';
  28. import { AR } from '../main';
  29. /** Update interval, in ms */
  30. const UPDATE_INTERVAL = 500;
  31. /** Icons for different power profiles */
  32. const POWER_ICON: { readonly [P in PowerPreference]: string } = Object.freeze({
  33. 'default': '',
  34. 'low-power': '&#x1F50B',
  35. 'high-performance': '&#x26A1'
  36. });
  37. /** Button icons (atlas) */
  38. const BUTTON_ICONS = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAYAAAB3AH1ZAAAAVUlEQVRIS2NkGGDAOMD2M4w6YDQE8IbAfyBgBAJSEipIDy712MzCaTiyQdRwBC4zsDoAmy8ocQQ+vRgOIDUI8UUPMVFIUvySkhaIVTvqgNEQGPAQAABSNiARgz5LggAAAABJRU5ErkJggg==';
  39. /**
  40. * Stats panel used for development purposes
  41. */
  42. export class StatsPanel
  43. {
  44. /** A container for the panel */
  45. private readonly _container: HTMLDivElement;
  46. /** Time of last update, in milliseconds */
  47. private _lastUpdate: DOMHighResTimeStamp;
  48. /**
  49. * Constructor
  50. */
  51. constructor()
  52. {
  53. this._container = this._createContainer();
  54. this._lastUpdate = 0;
  55. }
  56. /**
  57. * Initialize the panel
  58. * @param parent parent node
  59. * @param isVisible
  60. */
  61. init(parent: Node, isVisible: boolean): void
  62. {
  63. parent.appendChild(this._container);
  64. this._container.hidden = !isVisible;
  65. }
  66. /**
  67. * Release the panel
  68. */
  69. release(): void
  70. {
  71. this._container.remove();
  72. }
  73. /**
  74. * A method to be called in the update loop
  75. * @param time current time in ms
  76. * @param sources the sources of media linked to the session
  77. * @param trackers the trackers attached to the session
  78. * @param viewport the viewport
  79. * @param gpu GPU cycles per second
  80. * @param fps frames per second
  81. */
  82. update(time: DOMHighResTimeStamp, sources: Source[], trackers: Tracker[], viewport: Viewport, gpu: number, fps: number): void
  83. {
  84. if(time >= this._lastUpdate + UPDATE_INTERVAL) {
  85. this._lastUpdate = time;
  86. this._update(sources, trackers, viewport, fps, gpu);
  87. }
  88. }
  89. /**
  90. * Update the contents of the panel
  91. * @param sources the sources of media linked to the session
  92. * @param trackers the trackers attached to the session
  93. * @param viewport the viewport
  94. * @param fps frames per second
  95. * @param gpu GPU cycles per second
  96. */
  97. private _update(sources: Source[], trackers: Tracker[], viewport: Viewport, fps: number, gpu: number): void
  98. {
  99. // all sanitized
  100. const lfps = this._label('_ar_fps');
  101. if(lfps !== null) {
  102. lfps.style.color = this._color(fps);
  103. lfps.innerText = String(fps);
  104. }
  105. const lgpu = this._label('_ar_gpu');
  106. if(lgpu !== null) {
  107. lgpu.style.color = this._color(gpu);
  108. lgpu.innerText = String(gpu);
  109. }
  110. const lpower = this._label('_ar_power');
  111. if(lpower !== null)
  112. lpower.innerHTML = POWER_ICON[Settings.powerPreference];
  113. const lin = this._label('_ar_in');
  114. if(lin !== null) {
  115. const sourceStats = sources.map(source => source._stats).join(', ');
  116. lin.innerText = sourceStats;
  117. }
  118. const lout = this._label('_ar_out');
  119. if(lout !== null) {
  120. const trackerStats = trackers.map(tracker => tracker._stats).join(', ');
  121. lout.innerText = trackerStats;
  122. }
  123. const lview = this._label('_ar_view');
  124. if(lview !== null) {
  125. const size = viewport.virtualSize;
  126. lview.innerText = `${size.width}x${size.height} rendering`;
  127. }
  128. }
  129. /**
  130. * Get a label of the panel
  131. * @param className
  132. * @returns the HTML element, or null if it doesn't exist
  133. */
  134. private _label(className: string): Nullable<HTMLElement>
  135. {
  136. return this._container.getElementsByClassName(className).item(0) as Nullable<HTMLElement>;
  137. }
  138. /**
  139. * Associate a color to a frequency number
  140. * @param f frequency given in cycles per second
  141. * @returns colorized number (HTML)
  142. */
  143. private _color(f: number): string
  144. {
  145. const GREEN = '#0f0', YELLOW = '#ff0', RED = '#f33';
  146. const color3 = f >= 50 ? GREEN : (f >= 30 ? YELLOW : RED);
  147. const color2 = f >= 30 ? GREEN : RED;
  148. const color = Settings.powerPreference != 'low-power' ? color3 : color2;
  149. return color;
  150. }
  151. /**
  152. * Create the container for the panel
  153. * @returns a container
  154. */
  155. private _createContainer(): HTMLDivElement
  156. {
  157. const container = document.createElement('div');
  158. container.style.position = 'absolute';
  159. container.style.left = container.style.top = '0px';
  160. container.style.zIndex = '1000000';
  161. container.style.padding = '0px';
  162. container.appendChild(this._createTitle());
  163. container.appendChild(this._createContent());
  164. return container;
  165. }
  166. /**
  167. * Create a title
  168. * @returns a title
  169. */
  170. private _createTitle(): HTMLElement
  171. {
  172. const title = document.createElement('div');
  173. const button = document.createElement('button');
  174. title.style.display = 'flex';
  175. title.style.backgroundColor = '#7e56c2';
  176. title.style.color = 'white';
  177. title.style.fontFamily = 'monospace';
  178. title.style.fontSize = '14px';
  179. title.style.fontWeight = 'bold';
  180. title.style.paddingRight = '4px';
  181. title.innerText = 'encantar.js ' + AR.version;
  182. button.style.width = '18px';
  183. button.style.height = '18px';
  184. button.style.marginRight = '4px';
  185. button.style.backgroundColor = '#7e56c2';
  186. button.style.backgroundImage = 'url(' + BUTTON_ICONS + ')';
  187. button.style.backgroundRepeat = 'no-repeat';
  188. button.style.backgroundPosition = '0 0';
  189. button.style.borderWidth = '2px';
  190. button.style.borderColor = '#b588fb #46346a #46346a #b588fb';
  191. title.insertBefore(button, title.firstChild);
  192. button.addEventListener('click', () => {
  193. const container = title.parentNode;
  194. const details = container && container.querySelector<HTMLElement>('._ar_details');
  195. if(!details)
  196. return;
  197. details.hidden = !details.hidden;
  198. button.style.backgroundPosition = details.hidden ? '0 0 ' : '-16px 0';
  199. });
  200. return title;
  201. }
  202. /**
  203. * Create a content container
  204. * @returns a content container
  205. */
  206. private _createContent(): HTMLElement
  207. {
  208. const content = document.createElement('div');
  209. const details = document.createElement('div');
  210. content.style.backgroundColor = 'rgba(0,0,0,0.5)';
  211. content.style.color = 'white';
  212. content.style.fontFamily = 'monospace';
  213. content.style.fontSize = '14px';
  214. content.style.padding = '2px';
  215. content.style.whiteSpace = 'pre-line';
  216. details.classList.add('_ar_details');
  217. details.hidden = true;
  218. // all sanitized
  219. const append = (div: HTMLDivElement, html: string): void => div.insertAdjacentHTML('beforeend', html);
  220. append(content, 'FPS: <span class="_ar_fps"></span> | ');
  221. append(content, 'GPU: <span class="_ar_gpu"></span> ');
  222. append(content, '<span class="_ar_power"></span>');
  223. append(details, 'IN: <span class="_ar_in"></span><br>');
  224. append(details, 'OUT: <span class="_ar_out"></span><br>');
  225. append(details, 'VIEW: <span class="_ar_view"></span>');
  226. // done!
  227. content.appendChild(details);
  228. return content;
  229. }
  230. }