@playcanvas/web-components 0.10.0 → 0.11.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.
package/src/app.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  CameraComponent,
5
5
  createGraphicsDevice,
6
6
  ElementInput,
7
+ Entity,
7
8
  FILLMODE_FILL_WINDOW,
8
9
  GraphNode,
9
10
  Keyboard,
@@ -69,15 +70,21 @@ import {
69
70
  import { AssetElement } from './asset';
70
71
  import { AsyncElement } from './async-element';
71
72
  import { EntityElement } from './entity';
73
+ import { LoadingBar } from './loading-bar';
72
74
  import { MaterialElement } from './material';
73
75
  import { ModuleElement } from './module';
74
- import { parseBool, parseEnum } from './parse';
76
+ import { parseBool, parseEnum, parseNumber } from './parse';
75
77
 
76
78
  /**
77
79
  * The AppElement interface provides properties and methods for manipulating
78
80
  * {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-app/ | `<pc-app>`} elements.
79
81
  * The AppElement interface also inherits the properties and methods of the
80
82
  * {@link HTMLElement} interface.
83
+ *
84
+ * @fires {ProgressEvent} progress - Fired while the application preloads its assets. `loaded` and
85
+ * `total` are asset counts, not bytes, and an asset that fails to load still counts as loaded.
86
+ * Fired at least once per boot, and the final event always has `loaded` equal to `total`. Does
87
+ * not bubble.
81
88
  */
82
89
  class AppElement extends AsyncElement {
83
90
  /**
@@ -91,14 +98,32 @@ class AppElement extends AsyncElement {
91
98
 
92
99
  private _antialias = true;
93
100
 
94
- private _depth = true;
101
+ private _depthBuffer = true;
95
102
 
96
- private _stencil = true;
103
+ private _stencilBuffer = true;
97
104
 
98
- private _highResolution = true;
105
+ private _maxPixelRatio = Infinity;
106
+
107
+ private _loadingBar = true;
108
+
109
+ /**
110
+ * Set once the graphics options above have been handed to `createGraphicsDevice`, after which
111
+ * writing any of them changes nothing. Guards the warning in {@link _warnIfBooted}, and is
112
+ * cleared on disconnect so a re-connected element boots from its current attributes.
113
+ */
114
+ private _optionsLocked = false;
115
+
116
+ private _bar: LoadingBar | null = null;
99
117
 
100
118
  private _hierarchyReady = false;
101
119
 
120
+ /**
121
+ * The elements backing this application's entities, keyed by the entity itself. Registered
122
+ * by EntityElement at creation and removed when an entity is destroyed, this joins engine
123
+ * scene nodes back to their owning elements by identity - never by name.
124
+ */
125
+ private _entityElements = new Map<GraphNode, EntityElement>();
126
+
102
127
  private _picker: Picker | null = null;
103
128
 
104
129
  private _hasPointerListeners: { [key: string]: boolean } = {
@@ -111,6 +136,9 @@ class AppElement extends AsyncElement {
111
136
 
112
137
  private _hoveredEntity: EntityElement | null = null;
113
138
 
139
+ // Identifies the newest in-flight hover pick, so out-of-order results can be discarded
140
+ private _pickToken = 0;
141
+
114
142
  private _pointerHandlers: { [key: string]: EventListener | null } = {
115
143
  pointermove: null,
116
144
  pointerdown: null,
@@ -119,13 +147,27 @@ class AppElement extends AsyncElement {
119
147
 
120
148
  private _app: AppBase | null = null;
121
149
 
150
+ private _loadProgress = 0;
151
+
122
152
  /**
123
- * The PlayCanvas application instance. Available once the element is ready await
124
- * {@link whenReady} or the element's `ready()` promise before accessing it.
125
- * @returns The application instance.
153
+ * The PlayCanvas application instance. `null` until the element is ready, and again once it
154
+ * has been removed from the document — await {@link whenReady} or the element's `ready()`
155
+ * promise before accessing it.
156
+ * @returns The application instance, or `null`.
126
157
  */
127
- get app(): AppBase {
128
- return this._app!;
158
+ get app(): AppBase | null {
159
+ return this._app;
160
+ }
161
+
162
+ /**
163
+ * The asset preload progress of the application, as a fraction from 0 to 1. It is 0 until
164
+ * preloading begins (and again once the element has been removed from the document), and 1
165
+ * once preloading has finished — including when there was nothing to preload. Read this to
166
+ * initialize a loading UI; subsequent updates arrive via the `progress` event.
167
+ * @returns The preload progress.
168
+ */
169
+ get loadProgress(): number {
170
+ return this._loadProgress;
129
171
  }
130
172
 
131
173
  /**
@@ -141,6 +183,12 @@ class AppElement extends AsyncElement {
141
183
  }
142
184
 
143
185
  async connectedCallback() {
186
+ // Created before the first await, so the bar is visible while modules and the graphics
187
+ // device are created, and exists before any disconnect could need to clean it up
188
+ if (this._loadingBar && !this._bar) {
189
+ this._bar = new LoadingBar(this);
190
+ }
191
+
144
192
  // Get all pc-module elements that are direct children of the pc-app element
145
193
  const moduleElements = this.querySelectorAll<ModuleElement>(':scope > pc-module');
146
194
 
@@ -159,15 +207,21 @@ class AppElement extends AsyncElement {
159
207
  };
160
208
  const deviceTypes = backendToDeviceTypes[this._backend] || [];
161
209
 
210
+ this._optionsLocked = true;
211
+
162
212
  const device = await createGraphicsDevice(this._canvas, {
163
213
  // @ts-ignore - alpha needs to be documented
164
214
  alpha: this._alpha,
165
215
  antialias: this._antialias,
166
- depth: this._depth,
216
+ depth: this._depthBuffer,
167
217
  deviceTypes: deviceTypes,
168
- stencil: this._stencil
218
+ stencil: this._stencilBuffer
169
219
  });
170
- device.maxPixelRatio = this._highResolution ? window.devicePixelRatio : 1;
220
+
221
+ // Assigned rather than resolved to a number here: the engine caps against the live
222
+ // window.devicePixelRatio on every resize, so an uncapped Infinity keeps following the
223
+ // display when a window moves between monitors of differing density.
224
+ device.maxPixelRatio = this._maxPixelRatio;
171
225
 
172
226
  const createOptions = new AppOptions();
173
227
  createOptions.graphicsDevice = device;
@@ -234,11 +288,12 @@ class AppElement extends AsyncElement {
234
288
  createOptions.batchManager = BatchManager;
235
289
  createOptions.xr = XrManager;
236
290
 
237
- this._app = new AppBase(this._canvas);
238
- this.app.init(createOptions);
291
+ const app = new AppBase(this._canvas);
292
+ this._app = app;
293
+ app.init(createOptions);
239
294
 
240
- this.app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
241
- this.app.setCanvasResolution(RESOLUTION_AUTO);
295
+ app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
296
+ app.setCanvasResolution(RESOLUTION_AUTO);
242
297
 
243
298
  this._pickerCreate();
244
299
 
@@ -248,7 +303,7 @@ class AppElement extends AsyncElement {
248
303
  assetElement.createAsset();
249
304
  const asset = assetElement.asset;
250
305
  if (asset) {
251
- this.app!.assets.add(asset);
306
+ app.assets.add(asset);
252
307
  }
253
308
  });
254
309
 
@@ -261,20 +316,46 @@ class AppElement extends AsyncElement {
261
316
  // Create all entities
262
317
  const entityElements = this.querySelectorAll<EntityElement>('pc-entity');
263
318
  Array.from(entityElements).forEach((entityElement) => {
264
- entityElement.createEntity(this.app!);
319
+ entityElement.createEntity(app);
265
320
  });
266
321
 
267
322
  // Build hierarchy
268
323
  entityElements.forEach((entityElement) => {
269
- entityElement.buildHierarchy(this.app!);
324
+ entityElement.buildHierarchy(app);
270
325
  });
271
326
 
272
327
  this._hierarchyReady = true;
273
328
 
329
+ // Forward the engine's preload lifecycle as DOM ProgressEvents on this element. The
330
+ // listener must be attached before preload() is called: an asset that is already loaded
331
+ // ticks synchronously inside it.
332
+ const total = app.assets.list({ preload: true }).length;
333
+ let loaded = 0;
334
+ const onPreloadProgress = () => {
335
+ loaded += 1;
336
+ this._loadProgress = loaded / total;
337
+ this._bar?.progress(loaded, total);
338
+ this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded, total }));
339
+ };
340
+ app.on('preload:progress', onPreloadProgress);
341
+
342
+ this._loadProgress = total === 0 ? 1 : 0;
343
+ this._bar?.progress(0, total);
344
+ this.dispatchEvent(new ProgressEvent('progress', { lengthComputable: true, loaded: 0, total }));
345
+
274
346
  // Load assets before starting the application
275
- this.app.preload(() => {
347
+ app.preload(() => {
348
+ // Scope the counter to this preload pass, so a later app.preload() call by user code
349
+ // cannot push `loaded` past `total`
350
+ app.off('preload:progress', onPreloadProgress);
351
+ this._loadProgress = 1;
352
+
276
353
  // Start the application
277
- this.app!.start();
354
+ app.start();
355
+
356
+ // Dismiss the bar only once a frame has actually rendered; ready fires before the
357
+ // first rAF tick
358
+ app.once('frameend', () => this._bar?.complete());
278
359
 
279
360
  // Handle window resize to keep the canvas responsive
280
361
  window.addEventListener('resize', this._onWindowResize);
@@ -284,13 +365,19 @@ class AppElement extends AsyncElement {
284
365
  }
285
366
 
286
367
  disconnectedCallback() {
368
+ this._optionsLocked = false;
287
369
  this._pickerDestroy();
288
370
 
289
- // Clean up the application
290
- if (this.app) {
291
- this.app.destroy();
371
+ // Clean up the application. Destroying it destroys every entity, whose destroy hooks
372
+ // unregister them - clear() covers any entity the engine no longer reached.
373
+ if (this._app) {
374
+ this._app.destroy();
292
375
  this._app = null;
293
376
  }
377
+ this._entityElements.clear();
378
+ this._loadProgress = 0;
379
+ this._bar?.destroy();
380
+ this._bar = null;
294
381
 
295
382
  // Remove event listeners
296
383
  window.removeEventListener('resize', this._onWindowResize);
@@ -312,10 +399,18 @@ class AppElement extends AsyncElement {
312
399
  const { width, height } = this.app!.graphicsDevice;
313
400
  this._picker = new Picker(this.app!, width, height);
314
401
 
315
- // Create bound handlers but don't attach them yet
316
- this._pointerHandlers.pointermove = this._onPointerMove.bind(this) as EventListener;
317
- this._pointerHandlers.pointerdown = this._onPointerDown.bind(this) as EventListener;
318
- this._pointerHandlers.pointerup = this._onPointerUp.bind(this) as EventListener;
402
+ // Create bound handlers but don't attach them yet. The handlers pick asynchronously, so
403
+ // each is wrapped to discard the promise - a listener must not return one, and nothing
404
+ // awaits the result.
405
+ const listener = (handler: (event: PointerEvent) => Promise<void>): EventListener => {
406
+ return (event: Event) => {
407
+ handler.call(this, event as PointerEvent);
408
+ };
409
+ };
410
+
411
+ this._pointerHandlers.pointermove = listener(this._onPointerMove);
412
+ this._pointerHandlers.pointerdown = listener(this._onPointerDown);
413
+ this._pointerHandlers.pointerup = listener(this._onPointerUp);
319
414
 
320
415
  // Listen for pointer listeners being added/removed
321
416
  ['pointermove', 'pointerdown', 'pointerup', 'pointerenter', 'pointerleave'].forEach((type) => {
@@ -356,6 +451,79 @@ class AppElement extends AsyncElement {
356
451
  };
357
452
  }
358
453
 
454
+ /**
455
+ * Registers the element that created an entity. Called by EntityElement when it creates its
456
+ * entity.
457
+ *
458
+ * @param entity - The entity.
459
+ * @param element - The element that created it.
460
+ * @ignore
461
+ */
462
+ _registerEntityElement(entity: Entity, element: EntityElement) {
463
+ this._entityElements.set(entity, element);
464
+ }
465
+
466
+ /**
467
+ * Removes the registration for a destroyed entity. Called by EntityElement.
468
+ *
469
+ * @param entity - The entity.
470
+ * @ignore
471
+ */
472
+ _unregisterEntityElement(entity: Entity) {
473
+ this._entityElements.delete(entity);
474
+ }
475
+
476
+ /**
477
+ * Returns the `<pc-entity>` element whose backing entity is `entity`, or `null` if the
478
+ * entity was not created by an element of this application - for example, a node inside a
479
+ * model's instantiated hierarchy, or an entity created through the engine API.
480
+ *
481
+ * @param entity - The entity to look up.
482
+ * @returns The element backing the entity, or `null`.
483
+ */
484
+ elementFromEntity(entity: Entity): EntityElement | null {
485
+ return this._entityElements.get(entity) ?? null;
486
+ }
487
+
488
+ /**
489
+ * Resolves the element that owns a picked node: the nearest node up the parent chain -
490
+ * starting with the node itself - that was created by a `<pc-entity>` of this application.
491
+ * A hit inside a model's instantiated hierarchy therefore resolves to the element hosting
492
+ * the model.
493
+ *
494
+ * @param node - The picked node, or `null`.
495
+ * @returns The owning element, or `null`.
496
+ */
497
+ private _elementFromNode(node: GraphNode | null): EntityElement | null {
498
+ while (node !== null) {
499
+ const element = this._entityElements.get(node);
500
+ if (element) {
501
+ return element;
502
+ }
503
+ node = node.parent;
504
+ }
505
+ return null;
506
+ }
507
+
508
+ /**
509
+ * Like {@link _elementFromNode}, but skips elements without a listener for `type`, so a hit
510
+ * on an unlistened child still reaches a listening ancestor.
511
+ *
512
+ * @param node - The picked node, or `null`.
513
+ * @param type - The pointer event type a listener is required for.
514
+ * @returns The nearest listening element, or `null`.
515
+ */
516
+ private _elementWithListener(node: GraphNode | null, type: string): EntityElement | null {
517
+ while (node !== null) {
518
+ const element = this._entityElements.get(node);
519
+ if (element?.hasListeners(type)) {
520
+ return element;
521
+ }
522
+ node = node.parent;
523
+ }
524
+ return null;
525
+ }
526
+
359
527
  // New helper to convert CSS coordinates to canvas (picker) coordinates
360
528
  private _getPickerCoordinates(event: PointerEvent): { x: number, y: number } {
361
529
  // Get the canvas' bounding rectangle in CSS pixels.
@@ -369,32 +537,44 @@ class AppElement extends AsyncElement {
369
537
  return { x, y };
370
538
  }
371
539
 
372
- _onPointerMove(event: PointerEvent) {
373
- if (!this._picker || !this.app) return;
374
-
540
+ /**
541
+ * Picks the scene under the pointer and returns the graph node that was hit, or `null`.
542
+ *
543
+ * The read back is asynchronous because the synchronous {@link Picker.getSelection} is not
544
+ * supported on WebGPU, where it returns an empty selection rather than failing - which
545
+ * silently disabled every `onpointer*` handler once WebGPU became the resolved backend. The
546
+ * async variant works on both backends and does not block the main thread on a GPU read.
547
+ *
548
+ * @param event - The pointer event to pick under.
549
+ * @returns The graph node under the pointer, or `null` if nothing was hit.
550
+ */
551
+ private async _pickNode(event: PointerEvent): Promise<GraphNode | null> {
375
552
  const camera = this.app!.root.findComponent('camera') as CameraComponent;
376
- if (!camera) return;
553
+ if (!camera) return null;
377
554
 
378
- // Use the helper to convert event coordinates into canvas/picker coordinates.
379
555
  const { x, y } = this._getPickerCoordinates(event);
380
556
 
381
- this._picker.prepare(camera, this.app!.scene);
382
- const selection = this._picker.getSelection(x, y);
383
-
384
- // Get the currently hovered entity by walking up the hierarchy
385
- let newHoverEntity: EntityElement | null = null;
386
- if (selection.length > 0) {
387
- const item = selection[0];
388
- let currentNode: GraphNode | null = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
389
- while (currentNode !== null) {
390
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
391
- if (entityElement) {
392
- newHoverEntity = entityElement;
393
- break;
394
- }
395
- currentNode = currentNode.parent;
396
- }
397
- }
557
+ this._picker!.prepare(camera, this.app!.scene);
558
+ const selection = await this._picker!.getSelectionAsync(x, y);
559
+ if (selection.length === 0) return null;
560
+
561
+ const item = selection[0];
562
+ return item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
563
+ }
564
+
565
+ async _onPointerMove(event: PointerEvent) {
566
+ if (!this._picker || !this.app) return;
567
+
568
+ // Moves arrive faster than a pick resolves, so results can land out of order. Only the
569
+ // newest pick may update the hover state - an older one describes a pointer position the
570
+ // user has already left.
571
+ const token = ++this._pickToken;
572
+ const node = await this._pickNode(event);
573
+ if (token !== this._pickToken || !this._picker) return;
574
+
575
+ // The hovered element is the nearest one up the node's parent chain, listening or not -
576
+ // dispatch is gated per event type below
577
+ const newHoverEntity = this._elementFromNode(node);
398
578
 
399
579
  // Handle enter/leave events
400
580
  if (this._hoveredEntity !== newHoverEntity) {
@@ -415,51 +595,27 @@ class AppElement extends AsyncElement {
415
595
  }
416
596
  }
417
597
 
418
- _onPointerDown(event: PointerEvent) {
598
+ async _onPointerDown(event: PointerEvent) {
419
599
  if (!this._picker || !this.app) return;
420
600
 
421
- const camera = this.app!.root.findComponent('camera') as CameraComponent;
422
- if (!camera) return;
423
-
424
- // Convert the event's pointer coordinates
425
- const { x, y } = this._getPickerCoordinates(event);
601
+ const node = await this._pickNode(event);
602
+ if (!this._picker) return; // the element disconnected while the pick was in flight
426
603
 
427
- this._picker.prepare(camera, this.app!.scene);
428
- const selection = this._picker.getSelection(x, y);
429
-
430
- if (selection.length > 0) {
431
- const item = selection[0];
432
- let currentNode: GraphNode | null = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
433
- while (currentNode !== null) {
434
- const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement;
435
- if (entityElement && entityElement.hasListeners('pointerdown')) {
436
- entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
437
- break;
438
- }
439
- currentNode = currentNode.parent;
440
- }
604
+ const entityElement = this._elementWithListener(node, 'pointerdown');
605
+ if (entityElement) {
606
+ entityElement.dispatchEvent(new PointerEvent('pointerdown', event));
441
607
  }
442
608
  }
443
609
 
444
- _onPointerUp(event: PointerEvent) {
610
+ async _onPointerUp(event: PointerEvent) {
445
611
  if (!this._picker || !this.app) return;
446
612
 
447
- const camera = this.app!.root.findComponent('camera') as CameraComponent;
448
- if (!camera) return;
449
-
450
- // Convert CSS coordinates to picker coordinates
451
- const { x, y } = this._getPickerCoordinates(event);
452
-
453
- this._picker.prepare(camera, this.app!.scene);
454
- const selection = this._picker.getSelection(x, y);
613
+ const node = await this._pickNode(event);
614
+ if (!this._picker) return; // the element disconnected while the pick was in flight
455
615
 
456
- if (selection.length > 0) {
457
- const item = selection[0];
458
- const node = item instanceof MeshInstance ? item.node : (item as GSplatComponent).entity;
459
- const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement;
460
- if (entityElement && entityElement.hasListeners('pointerup')) {
461
- entityElement.dispatchEvent(new PointerEvent('pointerup', event));
462
- }
616
+ const entityElement = this._elementWithListener(node, 'pointerup');
617
+ if (entityElement) {
618
+ entityElement.dispatchEvent(new PointerEvent('pointerup', event));
463
619
  }
464
620
  }
465
621
 
@@ -496,15 +652,30 @@ class AppElement extends AsyncElement {
496
652
  }
497
653
 
498
654
  /**
499
- * Sets the alpha flag.
655
+ * Warns that a graphics option was written too late to have any effect. These options are read
656
+ * once, when the element connects and creates its graphics device, so a later write updates
657
+ * only the element's own property - silently, without this.
658
+ *
659
+ * @param name - The name of the option, as its attribute.
660
+ */
661
+ private _warnIfBooted(name: string) {
662
+ if (this._optionsLocked) {
663
+ console.warn(`Attribute '${name}' on <pc-app> is only read when the application boots, so this change has no effect. Set it before the element is connected, or remove and re-insert the element to reboot with the new value.`);
664
+ }
665
+ }
666
+
667
+ /**
668
+ * Sets whether the frame buffer has an alpha channel, which is what lets the page show through
669
+ * wherever the scene has not drawn. Read only when the application boots.
500
670
  * @param value - The alpha flag.
501
671
  */
502
672
  set alpha(value: boolean) {
673
+ this._warnIfBooted('alpha');
503
674
  this._alpha = value;
504
675
  }
505
676
 
506
677
  /**
507
- * Gets the alpha flag.
678
+ * Gets whether the frame buffer has an alpha channel.
508
679
  * @returns The alpha flag.
509
680
  */
510
681
  get alpha() {
@@ -512,15 +683,16 @@ class AppElement extends AsyncElement {
512
683
  }
513
684
 
514
685
  /**
515
- * Sets the antialias flag.
686
+ * Sets whether the frame buffer is anti-aliased. Read only when the application boots.
516
687
  * @param value - The antialias flag.
517
688
  */
518
689
  set antialias(value: boolean) {
690
+ this._warnIfBooted('antialias');
519
691
  this._antialias = value;
520
692
  }
521
693
 
522
694
  /**
523
- * Gets the antialias flag.
695
+ * Gets whether the frame buffer is anti-aliased.
524
696
  * @returns The antialias flag.
525
697
  */
526
698
  get antialias() {
@@ -529,10 +701,11 @@ class AppElement extends AsyncElement {
529
701
 
530
702
  /**
531
703
  * Sets the graphics backend. Defaults to 'webgpu', which falls back to 'webgl2' if WebGPU
532
- * is not supported by the browser.
704
+ * is not supported by the browser. Read only when the application boots.
533
705
  * @param value - The graphics backend ('webgpu', 'webgl2', or 'null').
534
706
  */
535
707
  set backend(value: 'webgpu' | 'webgl2' | 'null') {
708
+ this._warnIfBooted('backend');
536
709
  this._backend = value;
537
710
  }
538
711
 
@@ -545,19 +718,21 @@ class AppElement extends AsyncElement {
545
718
  }
546
719
 
547
720
  /**
548
- * Sets the depth flag.
549
- * @param value - The depth flag.
721
+ * Sets whether the frame buffer has a depth buffer, which the renderer needs to resolve which
722
+ * surface is nearest the camera. Read only when the application boots.
723
+ * @param value - The depth buffer flag.
550
724
  */
551
- set depth(value: boolean) {
552
- this._depth = value;
725
+ set depthBuffer(value: boolean) {
726
+ this._warnIfBooted('depth-buffer');
727
+ this._depthBuffer = value;
553
728
  }
554
729
 
555
730
  /**
556
- * Gets the depth flag.
557
- * @returns The depth flag.
731
+ * Gets whether the frame buffer has a depth buffer.
732
+ * @returns The depth buffer flag.
558
733
  */
559
- get depth() {
560
- return this._depth;
734
+ get depthBuffer() {
735
+ return this._depthBuffer;
561
736
  }
562
737
 
563
738
  /**
@@ -570,43 +745,74 @@ class AppElement extends AsyncElement {
570
745
  }
571
746
 
572
747
  /**
573
- * Sets the high resolution flag. When true, the application will render at the device's
574
- * physical resolution. When false, the application will render at CSS resolution.
575
- * @param value - The high resolution flag.
748
+ * Sets whether the application shows its built-in loading bar while it boots and preloads its
749
+ * assets. Enabled by default; setting `false` removes the bar immediately, while setting
750
+ * `true` has no effect until the element is next connected. The bar can be themed with the
751
+ * CSS custom properties `--pc-loading-bar-color`, `--pc-loading-bar-background` and
752
+ * `--pc-loading-bar-height`.
753
+ * @param value - The loading bar flag.
754
+ */
755
+ set loadingBar(value: boolean) {
756
+ this._loadingBar = value;
757
+ if (!value && this._bar) {
758
+ this._bar.destroy();
759
+ this._bar = null;
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Gets whether the application shows its built-in loading bar while it boots and preloads
765
+ * its assets.
766
+ * @returns The loading bar flag.
576
767
  */
577
- set highResolution(value: boolean) {
578
- this._highResolution = value;
768
+ get loadingBar() {
769
+ return this._loadingBar;
770
+ }
771
+
772
+ /**
773
+ * Sets the cap on the pixel ratio the application renders at. The canvas is sized by the
774
+ * smaller of this value and the display's own device pixel ratio, so the default of `Infinity`
775
+ * renders at full physical resolution, `1` renders at CSS resolution, and an intermediate
776
+ * value such as `2` keeps a dense display sharp without paying for every one of its pixels.
777
+ * Must be greater than 0. Unlike the other graphics options, this applies immediately.
778
+ * @param value - The maximum pixel ratio.
779
+ */
780
+ set maxPixelRatio(value: number) {
781
+ this._maxPixelRatio = value;
579
782
  if (this.app) {
580
- this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
783
+ this.app.graphicsDevice.maxPixelRatio = value;
784
+ this.app.resizeCanvas();
581
785
  }
582
786
  }
583
787
 
584
788
  /**
585
- * Gets the high resolution flag.
586
- * @returns The high resolution flag.
789
+ * Gets the cap on the pixel ratio the application renders at.
790
+ * @returns The maximum pixel ratio.
587
791
  */
588
- get highResolution() {
589
- return this._highResolution;
792
+ get maxPixelRatio() {
793
+ return this._maxPixelRatio;
590
794
  }
591
795
 
592
796
  /**
593
- * Sets the stencil flag.
594
- * @param value - The stencil flag.
797
+ * Sets whether the frame buffer has a stencil buffer, which stencil-based effects and UI
798
+ * masking need. Read only when the application boots.
799
+ * @param value - The stencil buffer flag.
595
800
  */
596
- set stencil(value: boolean) {
597
- this._stencil = value;
801
+ set stencilBuffer(value: boolean) {
802
+ this._warnIfBooted('stencil-buffer');
803
+ this._stencilBuffer = value;
598
804
  }
599
805
 
600
806
  /**
601
- * Gets the stencil flag.
602
- * @returns The stencil flag.
807
+ * Gets whether the frame buffer has a stencil buffer.
808
+ * @returns The stencil buffer flag.
603
809
  */
604
- get stencil() {
605
- return this._stencil;
810
+ get stencilBuffer() {
811
+ return this._stencilBuffer;
606
812
  }
607
813
 
608
814
  static get observedAttributes() {
609
- return ['alpha', 'antialias', 'backend', 'depth', 'stencil', 'high-resolution'];
815
+ return ['alpha', 'antialias', 'backend', 'depth-buffer', 'loading-bar', 'max-pixel-ratio', 'stencil-buffer'];
610
816
  }
611
817
 
612
818
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -620,14 +826,17 @@ class AppElement extends AsyncElement {
620
826
  case 'backend':
621
827
  this.backend = parseEnum(newValue, ['webgpu', 'webgl2', 'null'], 'webgpu', name);
622
828
  break;
623
- case 'depth':
624
- this.depth = parseBool(newValue, true);
829
+ case 'depth-buffer':
830
+ this.depthBuffer = parseBool(newValue, true);
831
+ break;
832
+ case 'loading-bar':
833
+ this.loadingBar = parseBool(newValue, true);
625
834
  break;
626
- case 'high-resolution':
627
- this.highResolution = parseBool(newValue, true);
835
+ case 'max-pixel-ratio':
836
+ this.maxPixelRatio = parseNumber(newValue, Infinity, name);
628
837
  break;
629
- case 'stencil':
630
- this.stencil = parseBool(newValue, true);
838
+ case 'stencil-buffer':
839
+ this.stencilBuffer = parseBool(newValue, true);
631
840
  break;
632
841
  }
633
842
  }