@playcanvas/web-components 0.10.0 → 0.10.1

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.
@@ -0,0 +1,122 @@
1
+ /** Covers the 0.2s opacity transition; jsdom never fires transitionend, so removal is timed. */
2
+ const REMOVAL_DELAY_MS = 250;
3
+
4
+ /**
5
+ * The slim progress bar `<pc-app>` shows while it boots and preloads. An implementation detail of
6
+ * AppElement rather than a custom element, so its shape can change without a breaking change.
7
+ *
8
+ * All styling is inline, so the library injects no stylesheet. The colors and height resolve CSS
9
+ * custom properties — `--pc-loading-bar-color`, `--pc-loading-bar-background` and
10
+ * `--pc-loading-bar-height` — so a page can theme the bar from `pc-app` or `:root`.
11
+ */
12
+ class LoadingBar {
13
+ private _track: HTMLDivElement;
14
+
15
+ private _fill: HTMLDivElement;
16
+
17
+ private _sweep: Animation | null = null;
18
+
19
+ private _removal: ReturnType<typeof setTimeout> | null = null;
20
+
21
+ /**
22
+ * Creates the bar and appends it to `parent`, starting in the indeterminate state.
23
+ * @param parent - The element to append the bar to.
24
+ */
25
+ constructor(parent: HTMLElement) {
26
+ this._track = document.createElement('div');
27
+ this._track.setAttribute('role', 'progressbar');
28
+ this._track.setAttribute('aria-label', 'Loading');
29
+ this._track.setAttribute('aria-valuemin', '0');
30
+ this._track.setAttribute('aria-valuemax', '100');
31
+ // Fixed positioning matches the canvas, which always fills the window (FILLMODE_FILL_WINDOW)
32
+ this._track.style.cssText = [
33
+ 'position: fixed',
34
+ 'top: 0',
35
+ 'left: 0',
36
+ 'width: 100%',
37
+ 'height: var(--pc-loading-bar-height, 3px)',
38
+ 'background: var(--pc-loading-bar-background, rgba(0, 0, 0, 0.1))',
39
+ 'z-index: 10000',
40
+ 'pointer-events: none',
41
+ 'opacity: 1',
42
+ 'transition: opacity 0.2s ease'
43
+ ].join('; ');
44
+
45
+ this._fill = document.createElement('div');
46
+ this._fill.style.cssText = [
47
+ 'width: 100%',
48
+ 'height: 100%',
49
+ 'transform-origin: left center',
50
+ 'transform: scaleX(0)',
51
+ 'background: var(--pc-loading-bar-color, #f60)',
52
+ 'transition: transform 0.2s ease'
53
+ ].join('; ');
54
+
55
+ this._track.appendChild(this._fill);
56
+ parent.appendChild(this._track);
57
+
58
+ // Indeterminate sweep until the first progress() call reports a real total. No
59
+ // aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
60
+ // Animations API, so the guard degrades to a static bar there rather than crashing boot.
61
+ if (typeof this._fill.animate === 'function') {
62
+ this._sweep = this._fill.animate([
63
+ { transform: 'scaleX(0.25) translateX(-100%)' },
64
+ { transform: 'scaleX(0.25) translateX(500%)' }
65
+ ], {
66
+ duration: 1000,
67
+ iterations: Infinity,
68
+ easing: 'ease-in-out'
69
+ });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Reflects preload progress, switching the bar from indeterminate to determinate on the first
75
+ * call.
76
+ * @param loaded - The number of assets that have finished loading.
77
+ * @param total - The number of assets being preloaded.
78
+ */
79
+ progress(loaded: number, total: number) {
80
+ if (this._sweep) {
81
+ this._sweep.cancel();
82
+ this._sweep = null;
83
+ }
84
+ const fraction = total === 0 ? 1 : loaded / total;
85
+ this._track.setAttribute('aria-valuenow', String(Math.round(fraction * 100)));
86
+ this._fill.style.transform = `scaleX(${fraction})`;
87
+ }
88
+
89
+ /**
90
+ * Fills the bar, fades it out and removes it. Idempotent.
91
+ */
92
+ complete() {
93
+ if (this._removal !== null) {
94
+ return;
95
+ }
96
+ if (this._sweep) {
97
+ this._sweep.cancel();
98
+ this._sweep = null;
99
+ }
100
+ this._track.setAttribute('aria-valuenow', '100');
101
+ this._fill.style.transform = 'scaleX(1)';
102
+ this._track.style.opacity = '0';
103
+ this._removal = setTimeout(() => this._track.remove(), REMOVAL_DELAY_MS);
104
+ }
105
+
106
+ /**
107
+ * Removes the bar immediately, cancelling any pending fade. Idempotent.
108
+ */
109
+ destroy() {
110
+ if (this._sweep) {
111
+ this._sweep.cancel();
112
+ this._sweep = null;
113
+ }
114
+ if (this._removal !== null) {
115
+ clearTimeout(this._removal);
116
+ this._removal = null;
117
+ }
118
+ this._track.remove();
119
+ }
120
+ }
121
+
122
+ export { LoadingBar };
package/src/material.ts CHANGED
@@ -114,8 +114,14 @@ type TextureSlot = 'aoMap' | 'diffuseMap' | 'emissiveMap' | 'glossMap' | 'height
114
114
  * created on insertion.
115
115
  *
116
116
  * The element is metal/rough by default: unlike a bare `StandardMaterial` it enables the metalness
117
- * workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR. The
118
- * `roughness` and `roughness-map` attributes are aliases for `gloss` and `gloss-map` that
117
+ * workflow, which is what the `metalness-*` attributes assume and what glTF means by PBR. It also
118
+ * defaults `metalness` to 0 rather than the engine's 1, because those two defaults have to be
119
+ * chosen together - the engine's 1 is unreachable under its own `useMetalness` of false, and with
120
+ * the workflow on it would make every material fully metallic, so `<pc-material diffuse="crimson">`
121
+ * would render as dark tinted reflections of an environment that may not exist rather than as a
122
+ * crimson surface. `metalness="1"` remains one attribute away.
123
+ *
124
+ * The `roughness` and `roughness-map` attributes are aliases for `gloss` and `gloss-map` that
119
125
  * additionally invert the gloss channel; do not mix the two families on one element.
120
126
  *
121
127
  * The two aliases are documented here rather than on an accessor, because they resolve to the
@@ -222,7 +228,7 @@ class MaterialElement extends HTMLElement {
222
228
 
223
229
  private _heightMapUv = 0;
224
230
 
225
- private _metalness = 1;
231
+ private _metalness = 0;
226
232
 
227
233
  private _metalnessMap = '';
228
234
 
@@ -2386,7 +2392,7 @@ class MaterialElement extends HTMLElement {
2386
2392
  this.heightMapUv = parseNumber(newValue, 0, name);
2387
2393
  break;
2388
2394
  case 'metalness':
2389
- this.metalness = parseNumber(newValue, 1, name);
2395
+ this.metalness = parseNumber(newValue, 0, name);
2390
2396
  break;
2391
2397
  case 'metalness-map':
2392
2398
  this.metalnessMap = newValue ?? '';
package/src/scene.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Color, Scene, Vec3 } from 'playcanvas';
2
2
 
3
- import { AppElement } from './app';
4
3
  import { AsyncElement } from './async-element';
5
4
  import { parseColor, parseEnum, parseNumber, parseVec3 } from './parse';
6
5
 
@@ -44,36 +43,68 @@ class SceneElement extends AsyncElement {
44
43
  private _scene: Scene | null = null;
45
44
 
46
45
  /**
47
- * The PlayCanvas scene instance. Available once the element is ready — await
46
+ * The PlayCanvas scene instance. `null` until the element is ready — await
48
47
  * {@link whenReady} or the element's `ready()` promise before accessing it.
49
- * @returns The scene instance.
48
+ * @returns The scene instance, or `null`.
50
49
  */
51
- get scene(): Scene {
52
- return this._scene!;
50
+ get scene(): Scene | null {
51
+ return this._scene;
53
52
  }
54
53
 
55
54
  async connectedCallback() {
56
- await this.closestApp?.ready();
55
+ const appElement = this.closestApp;
56
+ if (!appElement) {
57
+ console.warn('pc-scene must be a descendant of pc-app - scene settings not applied');
58
+ return;
59
+ }
60
+
61
+ await appElement.ready();
62
+
63
+ // The element may have been removed or re-parented while waiting for the app. Matches the
64
+ // guard in AssetElement and MaterialElement, but compares closestApp rather than
65
+ // parentElement because pc-scene resolves its app by ancestor rather than direct child.
66
+ // Without this, a scene re-parented mid-await would take its Scene from the app it started
67
+ // under while _applyGravity resolved the app it ended up under, splitting the two.
68
+ if (!this.isConnected || this.closestApp !== appElement) {
69
+ return;
70
+ }
57
71
 
58
- this._scene = this.closestApp!.app!.scene;
72
+ // The application is gone if the tree was torn down while we awaited readiness. There is
73
+ // nothing to configure and nothing the author can act on, so this stays silent.
74
+ const app = appElement.app;
75
+ if (!app) {
76
+ return;
77
+ }
78
+
79
+ this._scene = app.scene;
59
80
  this.updateSceneSettings();
60
81
 
61
82
  this._onReady();
62
83
  }
63
84
 
64
85
  updateSceneSettings() {
65
- if (this.scene) {
66
- this.scene.fog.type = this._fog;
67
- this.scene.fog.color = this._fogColor;
68
- this.scene.fog.density = this._fogDensity;
69
- this.scene.fog.start = this._fogStart;
70
- this.scene.fog.end = this._fogEnd;
71
-
72
- const appElement = this.parentElement as AppElement;
73
- appElement.app!.systems.rigidbody!.gravity.copy(this._gravity);
86
+ if (this._scene) {
87
+ this._scene.fog.type = this._fog;
88
+ this._scene.fog.color = this._fogColor;
89
+ this._scene.fog.density = this._fogDensity;
90
+ this._scene.fog.start = this._fogStart;
91
+ this._scene.fog.end = this._fogEnd;
92
+
93
+ this._applyGravity(this._gravity);
74
94
  }
75
95
  }
76
96
 
97
+ /**
98
+ * Applies gravity to the rigid body system. Resolved through `closestApp` rather than
99
+ * `parentElement` so that a `<pc-scene>` nested inside a wrapper element behaves the same as
100
+ * a direct child, matching how `connectedCallback` resolves the application.
101
+ *
102
+ * @param value - The gravity to apply.
103
+ */
104
+ private _applyGravity(value: Vec3) {
105
+ this.closestApp?.app?.systems.rigidbody?.gravity.copy(value);
106
+ }
107
+
77
108
  /**
78
109
  * Sets the fog type of the scene. Can be `none`, `linear`, `exp` or `exp2`. Defaults to
79
110
  * `none`.
@@ -176,9 +207,8 @@ class SceneElement extends AsyncElement {
176
207
  */
177
208
  set gravity(value: Vec3) {
178
209
  this._gravity = value;
179
- if (this.scene) {
180
- const appElement = this.parentElement as AppElement;
181
- appElement.app!.systems.rigidbody!.gravity.copy(value);
210
+ if (this._scene) {
211
+ this._applyGravity(value);
182
212
  }
183
213
  }
184
214