mesh-avatar-element 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuki Shindo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,250 @@
1
- # Temporary Holding Version
1
+ # mesh-avatar-element
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Embed an animated 2D avatar with a `<mesh-avatar>` HTML element. Uses the [mesh-avatar](https://github.com/shinshin86/mesh-avatar-studio/tree/main/packages/runtime) WebGL runtime, with no framework required.
4
+
5
+ <p align="center">
6
+ <img src="https://raw.githubusercontent.com/shinshin86/mesh-avatar-studio/main/docs/images/face-tracking.gif" width="400" alt="A Mesh Avatar Studio avatar following a face through a webcam">
7
+ </p>
8
+
9
+ ## Getting started
10
+
11
+ ### CDN
12
+
13
+ Add these two lines to your page to show the Miko sample:
14
+
15
+ ```html
16
+ <script type="module" src="https://cdn.jsdelivr.net/npm/mesh-avatar-element@0/dist/standalone.js"></script>
17
+ <mesh-avatar></mesh-avatar>
18
+ ```
19
+
20
+ Without `src`, the element loads Miko from
21
+ `https://shinshin86.github.io/mesh-avatar-studio/miko.mavatar`.
22
+
23
+ ### npm
24
+
25
+ ```sh
26
+ npm install mesh-avatar-element
27
+ ```
28
+
29
+ Import once in your app's JavaScript:
30
+
31
+ ```js
32
+ import 'mesh-avatar-element';
33
+ ```
34
+
35
+ Then add `<mesh-avatar></mesh-avatar>` to your HTML.
36
+
37
+ ### Your own avatar
38
+
39
+ Open your project in [Mesh Avatar Studio](https://github.com/shinshin86/mesh-avatar-studio) and choose **Export .mavatar**. Put the exported file on your site and set `src`:
40
+
41
+ ```html
42
+ <mesh-avatar src="/avatars/character.mavatar" expression="smile"></mesh-avatar>
43
+ ```
44
+
45
+ An unpacked avatar folder URL also works. Cross-origin assets need CORS headers.
46
+
47
+ ### Size
48
+
49
+ The element defaults to `display: block; aspect-ratio: 1 / 1`. Set its width in your page's CSS; you can also override its height or aspect ratio. The background is transparent.
50
+
51
+ ```html
52
+ <style>
53
+ mesh-avatar { width: min(100%, 320px); }
54
+ </style>
55
+ <mesh-avatar></mesh-avatar>
56
+ ```
57
+
58
+ ## Attributes
59
+
60
+ | Attribute | Values | Default |
61
+ | --- | --- | --- |
62
+ | `src` | A `.mavatar` URL or avatar folder URL | [Miko on GitHub Pages](https://shinshin86.github.io/mesh-avatar-studio/miko.mavatar) |
63
+ | `fit` | `contain`, `cover` | `contain` |
64
+ | `expression` | `neutral`, `smile`, `shy`, `surprise`, `halfLidded`, `angry`, `sad`, `wink` | `neutral` |
65
+ | `emotion` | `happy`, `sad`, `angry`, `surprised`, `relaxed`, `neutral` | Unset |
66
+ | `paused` | Boolean attribute; presence pauses animation | Absent |
67
+ | `no-idle` | Boolean attribute; presence disables automatic idle motion | Absent |
68
+ | `label` | Accessible canvas label | `Avatar` |
69
+
70
+ Attributes can be changed after loading. Changing `src` or `fit` destroys the current avatar and loads a new one. After `load(source)`, changing `fit` reloads that source; changing `src` switches back to the attribute URL. A failed load leaves the element empty; listen for `error` to show a fallback. Unknown expression, emotion or fit values warn once per attribute/value pair rather than throwing.
71
+
72
+ ## Properties, methods and events
73
+
74
+ Wait for the custom element to be registered before accessing it from a separate script:
75
+
76
+ ```js
77
+ await customElements.whenDefined('mesh-avatar');
78
+ const el = document.querySelector('mesh-avatar');
79
+ await el.ready;
80
+ el.avatar.setExpression('smile');
81
+ ```
82
+
83
+ | Property or method | Description |
84
+ | --- | --- |
85
+ | `avatar` | Loaded runtime avatar, or `null`. All [runtime methods](https://github.com/shinshin86/mesh-avatar-studio/tree/main/packages/runtime#drive-the-avatar) are available through `el.avatar`. |
86
+ | `load(source)` | Loads a File, Blob, ArrayBuffer, Uint8Array, URL or URL string without changing `src`. Returns the loading promise and emits `load` or `error`. The `AvatarSource` type is re-exported from `mesh-avatar`. |
87
+ | `ready` | Promise resolving with the avatar for the current source, rejecting on loading failure. A new load after completion creates a new promise; overlapping pending loads resolve with the latest avatar. |
88
+ | `src`, `fit`, `expression`, `emotion`, `paused` | Properties reflecting their corresponding attributes. Use attributes for `no-idle` and `label`. |
89
+ | `setVoiceLevel(value)` | Mouth opening from loudness, 0–1. Retains the value until loading finishes. |
90
+ | `speak(text, options?)` | Calls `speakKana` after loading. Japanese text animates the mouth without generating audio. Options: `speed`, `loop`. |
91
+ | `play(id)` | Plays a runtime motion after loading. Available IDs are in `el.avatar.motions`. |
92
+
93
+ `speak` and `play` return promises; handle rejected promises if loading can fail.
94
+
95
+ | Event | `event.detail` | When |
96
+ | --- | --- | --- |
97
+ | `load` | `{ avatar }` | Avatar loaded and ready |
98
+ | `error` | `{ error }` | Loading failed |
99
+ | `motion` | `{ id }` (string or `null`) | The runtime reports a motion change |
100
+
101
+ Attach listeners directly to the element with `el.addEventListener('load', event => { /* … */ })`.
102
+ Moving the element between parents in the same turn preserves the avatar. Removing it destroys the avatar; reconnecting it reloads the last source, including a File passed to `load()`.
103
+
104
+ To display a `.mavatar` chosen with a file input, pass the File directly rather than a `blob:` URL:
105
+
106
+ ```html
107
+ <input id="avatar-file" type="file" accept=".mavatar">
108
+ <mesh-avatar></mesh-avatar>
109
+ <script type="module">
110
+ await customElements.whenDefined('mesh-avatar');
111
+ const el = document.querySelector('mesh-avatar');
112
+ document.querySelector('#avatar-file').onchange = async event => {
113
+ const file = event.target.files[0];
114
+ if (!file) return;
115
+ try {
116
+ await el.load(file);
117
+ } catch (error) {
118
+ console.error('Could not load avatar:', error);
119
+ }
120
+ };
121
+ </script>
122
+ ```
123
+
124
+ ## Examples
125
+
126
+ - [Basic embed](https://github.com/shinshin86/mesh-avatar-studio/blob/main/packages/element/examples/basic.html): two HTML lines and a CSS width.
127
+ - [Playground](https://github.com/shinshin86/mesh-avatar-studio/blob/main/packages/element/examples/playground.html): expressions, text and audio lip sync, microphone input and your own avatar files.
128
+ - [Interactive demo](https://shinshin86.github.io/mesh-avatar-studio/embed/) ([日本語](https://shinshin86.github.io/mesh-avatar-studio/ja/embed/)): try the controls in your browser.
129
+
130
+ Download either HTML example and open it directly, or serve it on your site. Both load the element and Miko over the internet. Microphone access depends on browser permissions and secure-context support; use HTTPS or localhost if it is unavailable when opening a file directly. Examples are in the repository and are not included in the npm package.
131
+
132
+ ## Lip sync
133
+
134
+ Each example below assumes the CDN script and a `<mesh-avatar>` element are on your page. Run JavaScript as a module.
135
+
136
+ ### Audio file or TTS audio
137
+
138
+ Replace `/voice.mp3` with an audio file or a URL returned by your TTS service. Cross-origin audio needs CORS headers. Click the button to start playback.
139
+
140
+ ```html
141
+ <button id="play-voice">Play voice</button>
142
+ <script type="module">
143
+ await customElements.whenDefined('mesh-avatar');
144
+ const el = document.querySelector('mesh-avatar');
145
+ await el.ready;
146
+ const audio = new Audio();
147
+ audio.crossOrigin = 'anonymous';
148
+ audio.src = '/voice.mp3';
149
+ const ctx = new AudioContext();
150
+ const analyser = ctx.createAnalyser();
151
+ ctx.createMediaElementSource(audio).connect(analyser);
152
+ analyser.connect(ctx.destination);
153
+ const data = new Float32Array(analyser.fftSize);
154
+ function frame() {
155
+ if (audio.paused || audio.ended) {
156
+ el.avatar.setSpeaking(false);
157
+ el.setVoiceLevel(0);
158
+ return;
159
+ }
160
+ analyser.getFloatTimeDomainData(data);
161
+ el.setVoiceLevel(Math.min(1, Math.sqrt(data.reduce((s, v) => s + v * v, 0) / data.length) * 5));
162
+ requestAnimationFrame(frame);
163
+ }
164
+ document.querySelector('#play-voice').onclick = async () => {
165
+ await ctx.resume();
166
+ await audio.play();
167
+ el.avatar.setSpeaking(true);
168
+ frame();
169
+ };
170
+ </script>
171
+ ```
172
+
173
+ ### Microphone
174
+
175
+ Start microphone capture from a user action. Microphone access requires HTTPS or localhost and the user's permission. The microphone is not connected to the speakers.
176
+
177
+ ```html
178
+ <button id="mic">Start microphone</button>
179
+ <script type="module">
180
+ await customElements.whenDefined('mesh-avatar');
181
+ const el = document.querySelector('mesh-avatar');
182
+ await el.ready;
183
+ let stop;
184
+ document.querySelector('#mic').onclick = async event => {
185
+ if (stop) {
186
+ stop();
187
+ stop = null;
188
+ event.target.textContent = 'Start microphone';
189
+ return;
190
+ }
191
+ const ctx = new AudioContext();
192
+ await ctx.resume();
193
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
194
+ const analyser = ctx.createAnalyser();
195
+ ctx.createMediaStreamSource(stream).connect(analyser);
196
+ const data = new Float32Array(analyser.fftSize);
197
+ let frameId;
198
+ el.avatar.setSpeaking(true);
199
+ const frame = () => {
200
+ analyser.getFloatTimeDomainData(data);
201
+ el.setVoiceLevel(Math.min(1, Math.sqrt(data.reduce((s, v) => s + v * v, 0) / data.length) * 5));
202
+ frameId = requestAnimationFrame(frame);
203
+ };
204
+ stop = () => {
205
+ cancelAnimationFrame(frameId);
206
+ stream.getTracks().forEach(t => t.stop());
207
+ void ctx.close();
208
+ el.avatar.setSpeaking(false);
209
+ el.setVoiceLevel(0);
210
+ };
211
+ frame();
212
+ event.target.textContent = 'Stop microphone';
213
+ };
214
+ </script>
215
+ ```
216
+
217
+ ### Text only
218
+
219
+ This makes mouth shapes from Japanese text; it does not play or synthesize a voice.
220
+
221
+ ```html
222
+ <button id="say">Animate speech</button>
223
+ <script type="module">
224
+ await customElements.whenDefined('mesh-avatar');
225
+ const el = document.querySelector('mesh-avatar');
226
+ await el.ready;
227
+ document.querySelector('#say').onclick = () => el.speak('こんにちは');
228
+ </script>
229
+ ```
230
+
231
+ ## Using the Miko sample
232
+
233
+ Without `src`, Miko loads from Mesh Avatar Studio's GitHub Pages site. To host it yourself instead, save
234
+ [the Miko archive](https://shinshin86.github.io/mesh-avatar-studio/miko.mavatar) as part of your site and point `src` at your copy.
235
+
236
+ Miko © Yuki Shindo (AITuber OnAir). Follow the [Miko character guidelines](https://miko.aituberonair.com/#terms).
237
+ Redistribution as part of an application or website is allowed; standalone distribution, asset collections and distribution primarily intended to provide the assets are not.
238
+ Do not imply official affiliation or endorsement by Miko or AITuber OnAir, or claim ownership of the character or exclusive usage rights. See also the [sample asset terms](https://github.com/shinshin86/mesh-avatar-studio/blob/main/samples/miko-qipao/MIKO_ASSET_TERMS.md).
239
+
240
+ ## Notes
241
+
242
+ - Requires WebGL2 in a browser.
243
+ - Avoid placing many avatars on one page: browsers limit simultaneous WebGL contexts.
244
+ - Animation pauses offscreen, while the tab is hidden, or when `paused` is present.
245
+ - With `prefers-reduced-motion: reduce`, hair sway is reduced.
246
+ - The npm entry point can be imported during server-side rendering, but rendering requires the browser.
247
+
248
+ ## License
249
+
250
+ MIT. Miko's artwork is subject to the character guidelines above.
@@ -0,0 +1,26 @@
1
+ import { type LiveExpression } from 'mesh-avatar';
2
+ /** Shown when the element has no `src`: the Miko sample published with the Mesh Avatar Studio site. */
3
+ export declare const DEFAULT_SRC = "https://shinshin86.github.io/mesh-avatar-studio/miko.mavatar";
4
+ export declare const EMOTIONS: readonly ["happy", "sad", "angry", "surprised", "relaxed", "neutral"];
5
+ export type Emotion = typeof EMOTIONS[number];
6
+ export type Fit = 'contain' | 'cover';
7
+ /** Returns the expression, 'neutral' when the attribute is missing, or undefined when it is unknown. */
8
+ export declare function parseExpression(value: string | null): LiveExpression | undefined;
9
+ /** Returns the emotion, null when the attribute is missing, or undefined when it is unknown. */
10
+ export declare function parseEmotion(value: string | null): Emotion | null | undefined;
11
+ /** Returns the fit, 'contain' when the attribute is missing, or undefined when it is unknown. */
12
+ export declare function parseFit(value: string | null): Fit | undefined;
13
+ /** Seconds since the previous frame, limited like the runtime's own loop. */
14
+ export declare function frameSeconds(now: number, previous: number): number;
15
+ /**
16
+ * A promise for the avatar that is loading now. A load that starts before the previous one
17
+ * finished keeps the same pending promise, so callers waiting on it get the newest avatar.
18
+ */
19
+ export declare class ReadySignal<T> {
20
+ #private;
21
+ promise: Promise<T>;
22
+ /** Call when a load starts. */
23
+ start(): void;
24
+ resolve(value: T): void;
25
+ reject(error: unknown): void;
26
+ }
@@ -0,0 +1,48 @@
1
+ import { type AvatarSource, type MeshAvatar } from 'mesh-avatar';
2
+ import { type Fit } from './attributes.js';
3
+ export { DEFAULT_SRC, EMOTIONS } from './attributes.js';
4
+ export type { Emotion, Fit } from './attributes.js';
5
+ export type { AvatarSource, MeshAvatar, LiveExpression } from 'mesh-avatar';
6
+ declare const Base: typeof HTMLElement;
7
+ /**
8
+ * `<mesh-avatar>`: shows a Mesh Avatar Studio avatar. Without `src` it shows the Miko sample.
9
+ * Animation pauses while the element is off screen, the tab is hidden or `paused` is set.
10
+ */
11
+ export declare class MeshAvatarElement extends Base {
12
+ #private;
13
+ static observedAttributes: string[];
14
+ constructor();
15
+ /** The loaded runtime avatar, or null while loading. Every `mesh-avatar` method is available on it. */
16
+ get avatar(): MeshAvatar | null;
17
+ /** Resolves with the avatar once the current source has loaded. */
18
+ get ready(): Promise<MeshAvatar>;
19
+ /** Loads a file, archive or URL without changing `src`. A changed `src` replaces this source. */
20
+ load(source: AvatarSource): Promise<MeshAvatar>;
21
+ get src(): string;
22
+ set src(value: string);
23
+ get fit(): Fit;
24
+ set fit(value: Fit);
25
+ get expression(): string;
26
+ set expression(value: string);
27
+ get emotion(): string | null;
28
+ set emotion(value: string | null);
29
+ get paused(): boolean;
30
+ set paused(value: boolean);
31
+ /** Mouth opening from the voice loudness, 0 to 1. Kept until the avatar loads. */
32
+ setVoiceLevel(value: number): void;
33
+ /** Moves the mouth to Japanese text without audio. Waits for the avatar to load. */
34
+ speak(text: string, options?: {
35
+ speed?: number;
36
+ loop?: boolean;
37
+ }): Promise<void>;
38
+ /** Plays a motion by id (see `avatar.motions`). Waits for the avatar to load. */
39
+ play(id: string): Promise<void>;
40
+ connectedCallback(): void;
41
+ disconnectedCallback(): void;
42
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
43
+ }
44
+ declare global {
45
+ interface HTMLElementTagNameMap {
46
+ 'mesh-avatar': MeshAvatarElement;
47
+ }
48
+ }
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ import { LIVE_EXPRESSIONS as o, loadMeshAvatar as h } from "mesh-avatar";
2
+ const a = "https://shinshin86.github.io/mesh-avatar-studio/miko.mavatar", c = ["happy", "sad", "angry", "surprised", "relaxed", "neutral"];
3
+ function d(i) {
4
+ return i === null || i === "" ? "neutral" : o.includes(i) ? i : void 0;
5
+ }
6
+ function l(i) {
7
+ return i === null || i === "" ? null : c.includes(i) ? i : void 0;
8
+ }
9
+ function n(i) {
10
+ return i === null || i === "" ? "contain" : i === "contain" || i === "cover" ? i : void 0;
11
+ }
12
+ function u(i, t) {
13
+ return Math.min(0.05, Math.max(1e-3, (i - t) / 1e3));
14
+ }
15
+ class f {
16
+ promise;
17
+ #s;
18
+ #t;
19
+ #e = !0;
20
+ /** Call when a load starts. */
21
+ start() {
22
+ this.#e && (this.#e = !1, this.promise = new Promise((t, s) => {
23
+ this.#s = t, this.#t = s;
24
+ }), this.promise.catch(() => {
25
+ }));
26
+ }
27
+ resolve(t) {
28
+ this.#e || (this.#e = !0, this.#s(t));
29
+ }
30
+ reject(t) {
31
+ this.#e || (this.#e = !0, this.#t(t));
32
+ }
33
+ }
34
+ const p = `
35
+ :host { display: block; position: relative; aspect-ratio: 1 / 1; }
36
+ :host([hidden]) { display: none; }
37
+ canvas { display: block; width: 100%; height: 100%; }
38
+ `, m = typeof HTMLElement > "u" ? class {
39
+ } : HTMLElement;
40
+ class b extends m {
41
+ static observedAttributes = ["src", "fit", "expression", "emotion", "paused", "no-idle", "label"];
42
+ #s;
43
+ #t = null;
44
+ #e = new f();
45
+ #o = 0;
46
+ #r = !1;
47
+ #c;
48
+ #n = 0;
49
+ #d = 0;
50
+ #p = !0;
51
+ #l = null;
52
+ #u = null;
53
+ #m = /* @__PURE__ */ new Set();
54
+ constructor() {
55
+ super();
56
+ const t = this.attachShadow({ mode: "open" }), s = document.createElement("style");
57
+ s.textContent = p, this.#s = document.createElement("canvas"), this.#s.setAttribute("role", "img"), this.#s.setAttribute("aria-label", "Avatar"), t.append(s, this.#s), this.#e.start(), this.#s.addEventListener("webglcontextlost", (e) => {
58
+ e.preventDefault(), this.#a();
59
+ }), this.#s.addEventListener("webglcontextrestored", () => this.#h());
60
+ }
61
+ /** The loaded runtime avatar, or null while loading. Every `mesh-avatar` method is available on it. */
62
+ get avatar() {
63
+ return this.#t;
64
+ }
65
+ /** Resolves with the avatar once the current source has loaded. */
66
+ get ready() {
67
+ return this.#e.promise;
68
+ }
69
+ /** Loads a file, archive or URL without changing `src`. A changed `src` replaces this source. */
70
+ load(t) {
71
+ return this.#c = t, this.#h(), this.ready;
72
+ }
73
+ get src() {
74
+ return this.getAttribute("src") ?? "";
75
+ }
76
+ set src(t) {
77
+ this.setAttribute("src", t);
78
+ }
79
+ get fit() {
80
+ return n(this.getAttribute("fit")) ?? "contain";
81
+ }
82
+ set fit(t) {
83
+ this.setAttribute("fit", t);
84
+ }
85
+ get expression() {
86
+ return this.getAttribute("expression") ?? "neutral";
87
+ }
88
+ set expression(t) {
89
+ this.setAttribute("expression", t);
90
+ }
91
+ get emotion() {
92
+ return this.getAttribute("emotion");
93
+ }
94
+ set emotion(t) {
95
+ t === null ? this.removeAttribute("emotion") : this.setAttribute("emotion", t);
96
+ }
97
+ get paused() {
98
+ return this.hasAttribute("paused");
99
+ }
100
+ set paused(t) {
101
+ this.toggleAttribute("paused", t);
102
+ }
103
+ /** Mouth opening from the voice loudness, 0 to 1. Kept until the avatar loads. */
104
+ setVoiceLevel(t) {
105
+ this.#u = t, this.#t?.setVoiceLevel(t);
106
+ }
107
+ /** Moves the mouth to Japanese text without audio. Waits for the avatar to load. */
108
+ async speak(t, s) {
109
+ (await this.ready).speakKana(t, s);
110
+ }
111
+ /** Plays a motion by id (see `avatar.motions`). Waits for the avatar to load. */
112
+ async play(t) {
113
+ (await this.ready).play(t);
114
+ }
115
+ connectedCallback() {
116
+ this.#l ??= new IntersectionObserver((t) => {
117
+ this.#p = t.at(-1)?.isIntersecting ?? !0, this.#i();
118
+ }), this.#l.observe(this), document.addEventListener("visibilitychange", this.#i), !this.#t && !this.#r ? this.#h() : this.#i();
119
+ }
120
+ disconnectedCallback() {
121
+ this.#l?.disconnect(), document.removeEventListener("visibilitychange", this.#i), this.#a(), queueMicrotask(() => {
122
+ this.isConnected || this.#v();
123
+ });
124
+ }
125
+ attributeChangedCallback(t, s, e) {
126
+ s !== e && (t === "src" || t === "fit" ? (t === "src" && (this.#c = void 0), t === "fit" && n(e) === void 0 && this.#f(t, e), this.isConnected && this.#h()) : t === "label" ? this.#s.setAttribute("aria-label", e || "Avatar") : t === "paused" ? this.#i() : this.#t && this.#b(this.#t, t));
127
+ }
128
+ #b(t, s) {
129
+ if (s === "expression") {
130
+ const e = d(this.getAttribute("expression"));
131
+ e ? t.setExpression(e) : this.#f(s, this.getAttribute(s));
132
+ } else if (s === "emotion") {
133
+ const e = l(this.getAttribute("emotion"));
134
+ e !== void 0 ? t.setEmotion(e) : this.#f(s, this.getAttribute(s));
135
+ } else s === "no-idle" && t.setAutoIdle(!this.hasAttribute("no-idle"));
136
+ }
137
+ async #h(t = this.#c ?? (this.getAttribute("src") || a)) {
138
+ const s = ++this.#o;
139
+ this.#v(!1), this.#r = !0, this.#e.start();
140
+ try {
141
+ const e = await h(this.#s, t, { manual: !0, fit: this.fit });
142
+ if (s !== this.#o) {
143
+ e.destroy();
144
+ return;
145
+ }
146
+ this.#r = !1, this.#t = e;
147
+ for (const r of ["expression", "emotion", "no-idle"])
148
+ (r !== "emotion" || this.hasAttribute(r)) && this.#b(e, r);
149
+ this.#u !== null && e.setVoiceLevel(this.#u), matchMedia("(prefers-reduced-motion: reduce)").matches && e.setSwayGain(0.5), e.onMotion((r) => this.dispatchEvent(new CustomEvent("motion", { detail: { id: r } }))), e.advance(0), this.#e.resolve(e), this.dispatchEvent(new CustomEvent("load", { detail: { avatar: e } })), this.#i();
150
+ } catch (e) {
151
+ if (s !== this.#o) return;
152
+ this.#r = !1, this.#e.reject(e), this.dispatchEvent(new CustomEvent("error", { detail: { error: e } }));
153
+ }
154
+ }
155
+ /** Destroys the avatar. Without `cancel = false` it also abandons a load in progress. */
156
+ #v(t = !0) {
157
+ t && (this.#o++, this.#r = !1), this.#a(), this.#t?.destroy(), this.#t = null;
158
+ }
159
+ #i = () => {
160
+ const t = this.#t && this.isConnected && this.#p && !document.hidden && !this.paused;
161
+ t && !this.#n ? (this.#d = performance.now(), this.#n = requestAnimationFrame(this.#g)) : t || this.#a();
162
+ };
163
+ #g = (t) => {
164
+ const s = u(t, this.#d);
165
+ this.#d = t, this.#t?.advance(s, 1 / s), this.#n = requestAnimationFrame(this.#g);
166
+ };
167
+ #a() {
168
+ cancelAnimationFrame(this.#n), this.#n = 0;
169
+ }
170
+ #f(t, s) {
171
+ const e = `${t}=${s}`;
172
+ this.#m.has(e) || (this.#m.add(e), console.warn(`<mesh-avatar> ignores ${t}="${s}".`));
173
+ }
174
+ }
175
+ typeof customElements < "u" && !customElements.get("mesh-avatar") && customElements.define("mesh-avatar", b);
176
+ export {
177
+ a as DEFAULT_SRC,
178
+ c as EMOTIONS,
179
+ b as MeshAvatarElement
180
+ };