mesh-avatar-element 0.1.0 → 0.1.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.
- package/README.md +3 -1
- package/dist/index.js +36 -28
- package/dist/standalone.js +36 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
For React and Next.js, use [mesh-avatar-react](https://github.com/shinshin86/mesh-avatar-studio/tree/main/packages/react).
|
|
6
|
+
|
|
5
7
|
<p align="center">
|
|
6
8
|
<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
9
|
</p>
|
|
@@ -67,7 +69,7 @@ The element defaults to `display: block; aspect-ratio: 1 / 1`. Set its width in
|
|
|
67
69
|
| `no-idle` | Boolean attribute; presence disables automatic idle motion | Absent |
|
|
68
70
|
| `label` | Accessible canvas label | `Avatar` |
|
|
69
71
|
|
|
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.
|
|
72
|
+
Attributes can be changed after loading. Connection and `src` / `fit` changes in the same task are combined into one load in a microtask; `load(source)` starts immediately. 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
73
|
|
|
72
74
|
## Properties, methods and events
|
|
73
75
|
|
package/dist/index.js
CHANGED
|
@@ -42,21 +42,22 @@ class b extends m {
|
|
|
42
42
|
#s;
|
|
43
43
|
#t = null;
|
|
44
44
|
#e = new f();
|
|
45
|
-
#
|
|
45
|
+
#h = 0;
|
|
46
46
|
#r = !1;
|
|
47
|
+
#n = null;
|
|
47
48
|
#c;
|
|
48
|
-
#
|
|
49
|
+
#o = 0;
|
|
49
50
|
#d = 0;
|
|
50
|
-
#
|
|
51
|
+
#m = !0;
|
|
51
52
|
#l = null;
|
|
52
53
|
#u = null;
|
|
53
|
-
#
|
|
54
|
+
#b = /* @__PURE__ */ new Set();
|
|
54
55
|
constructor() {
|
|
55
56
|
super();
|
|
56
57
|
const t = this.attachShadow({ mode: "open" }), s = document.createElement("style");
|
|
57
58
|
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
59
|
e.preventDefault(), this.#a();
|
|
59
|
-
}), this.#s.addEventListener("webglcontextrestored", () => this.#
|
|
60
|
+
}), this.#s.addEventListener("webglcontextrestored", () => this.#f());
|
|
60
61
|
}
|
|
61
62
|
/** The loaded runtime avatar, or null while loading. Every `mesh-avatar` method is available on it. */
|
|
62
63
|
get avatar() {
|
|
@@ -68,7 +69,7 @@ class b extends m {
|
|
|
68
69
|
}
|
|
69
70
|
/** Loads a file, archive or URL without changing `src`. A changed `src` replaces this source. */
|
|
70
71
|
load(t) {
|
|
71
|
-
return this.#c = t, this.#
|
|
72
|
+
return this.#n = null, this.#c = t, this.#f(), this.ready;
|
|
72
73
|
}
|
|
73
74
|
get src() {
|
|
74
75
|
return this.getAttribute("src") ?? "";
|
|
@@ -114,62 +115,69 @@ class b extends m {
|
|
|
114
115
|
}
|
|
115
116
|
connectedCallback() {
|
|
116
117
|
this.#l ??= new IntersectionObserver((t) => {
|
|
117
|
-
this.#
|
|
118
|
-
}), this.#l.observe(this), document.addEventListener("visibilitychange", this.#i), !this.#t && !this.#r ? this.#
|
|
118
|
+
this.#m = t.at(-1)?.isIntersecting ?? !0, this.#i();
|
|
119
|
+
}), this.#l.observe(this), document.addEventListener("visibilitychange", this.#i), !this.#t && !this.#r ? this.#g() : this.#i();
|
|
119
120
|
}
|
|
120
121
|
disconnectedCallback() {
|
|
121
122
|
this.#l?.disconnect(), document.removeEventListener("visibilitychange", this.#i), this.#a(), queueMicrotask(() => {
|
|
122
|
-
this.isConnected || this.#
|
|
123
|
+
this.isConnected || this.#A();
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
attributeChangedCallback(t, s, e) {
|
|
126
|
-
s !== e && (t === "src" || t === "fit" ? (t === "src" && (this.#c = void 0), t === "fit" && n(e) === void 0 && this.#
|
|
127
|
+
s !== e && (t === "src" || t === "fit" ? (t === "src" && (this.#c = void 0), t === "fit" && n(e) === void 0 && this.#p(t, e), this.isConnected && this.#g()) : t === "label" ? this.#s.setAttribute("aria-label", e || "Avatar") : t === "paused" ? this.#i() : this.#t && this.#v(this.#t, t));
|
|
127
128
|
}
|
|
128
|
-
#
|
|
129
|
+
#v(t, s) {
|
|
129
130
|
if (s === "expression") {
|
|
130
131
|
const e = d(this.getAttribute("expression"));
|
|
131
|
-
e ? t.setExpression(e) : this.#
|
|
132
|
+
e ? t.setExpression(e) : this.#p(s, this.getAttribute(s));
|
|
132
133
|
} else if (s === "emotion") {
|
|
133
134
|
const e = l(this.getAttribute("emotion"));
|
|
134
|
-
e !== void 0 ? t.setEmotion(e) : this.#
|
|
135
|
+
e !== void 0 ? t.setEmotion(e) : this.#p(s, this.getAttribute(s));
|
|
135
136
|
} else s === "no-idle" && t.setAutoIdle(!this.hasAttribute("no-idle"));
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
#g() {
|
|
139
|
+
if (this.#e.start(), this.#n) return;
|
|
140
|
+
const t = this.#n = {};
|
|
141
|
+
queueMicrotask(() => {
|
|
142
|
+
this.#n === t && (this.#n = null, this.isConnected && this.#f());
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async #f(t = this.#c ?? (this.getAttribute("src") || a)) {
|
|
146
|
+
const s = ++this.#h;
|
|
147
|
+
this.#A(!1), this.#r = !0, this.#e.start();
|
|
140
148
|
try {
|
|
141
149
|
const e = await h(this.#s, t, { manual: !0, fit: this.fit });
|
|
142
|
-
if (s !== this.#
|
|
150
|
+
if (s !== this.#h) {
|
|
143
151
|
e.destroy();
|
|
144
152
|
return;
|
|
145
153
|
}
|
|
146
154
|
this.#r = !1, this.#t = e;
|
|
147
155
|
for (const r of ["expression", "emotion", "no-idle"])
|
|
148
|
-
(r !== "emotion" || this.hasAttribute(r)) && this.#
|
|
156
|
+
(r !== "emotion" || this.hasAttribute(r)) && this.#v(e, r);
|
|
149
157
|
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
158
|
} catch (e) {
|
|
151
|
-
if (s !== this.#
|
|
159
|
+
if (s !== this.#h) return;
|
|
152
160
|
this.#r = !1, this.#e.reject(e), this.dispatchEvent(new CustomEvent("error", { detail: { error: e } }));
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
/** Destroys the avatar. Without `cancel = false` it also abandons a load in progress. */
|
|
156
|
-
#
|
|
157
|
-
t && (this.#
|
|
164
|
+
#A(t = !0) {
|
|
165
|
+
t && (this.#h++, this.#r = !1), this.#a(), this.#t?.destroy(), this.#t = null;
|
|
158
166
|
}
|
|
159
167
|
#i = () => {
|
|
160
|
-
const t = this.#t && this.isConnected && this.#
|
|
161
|
-
t && !this.#
|
|
168
|
+
const t = this.#t && this.isConnected && this.#m && !document.hidden && !this.paused;
|
|
169
|
+
t && !this.#o ? (this.#d = performance.now(), this.#o = requestAnimationFrame(this.#y)) : t || this.#a();
|
|
162
170
|
};
|
|
163
|
-
#
|
|
171
|
+
#y = (t) => {
|
|
164
172
|
const s = u(t, this.#d);
|
|
165
|
-
this.#d = t, this.#t?.advance(s, 1 / s), this.#
|
|
173
|
+
this.#d = t, this.#t?.advance(s, 1 / s), this.#o = requestAnimationFrame(this.#y);
|
|
166
174
|
};
|
|
167
175
|
#a() {
|
|
168
|
-
cancelAnimationFrame(this.#
|
|
176
|
+
cancelAnimationFrame(this.#o), this.#o = 0;
|
|
169
177
|
}
|
|
170
|
-
#
|
|
178
|
+
#p(t, s) {
|
|
171
179
|
const e = `${t}=${s}`;
|
|
172
|
-
this.#
|
|
180
|
+
this.#b.has(e) || (this.#b.add(e), console.warn(`<mesh-avatar> ignores ${t}="${s}".`));
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
typeof customElements < "u" && !customElements.get("mesh-avatar") && customElements.define("mesh-avatar", b);
|
package/dist/standalone.js
CHANGED
|
@@ -2658,21 +2658,22 @@ class lr extends or {
|
|
|
2658
2658
|
#a;
|
|
2659
2659
|
#e = null;
|
|
2660
2660
|
#t = new ir();
|
|
2661
|
-
#
|
|
2661
|
+
#o = 0;
|
|
2662
2662
|
#n = !1;
|
|
2663
|
+
#i = null;
|
|
2663
2664
|
#h;
|
|
2664
|
-
#
|
|
2665
|
+
#s = 0;
|
|
2665
2666
|
#u = 0;
|
|
2666
|
-
#
|
|
2667
|
+
#p = !0;
|
|
2667
2668
|
#c = null;
|
|
2668
2669
|
#f = null;
|
|
2669
|
-
#
|
|
2670
|
+
#g = /* @__PURE__ */ new Set();
|
|
2670
2671
|
constructor() {
|
|
2671
2672
|
super();
|
|
2672
2673
|
const e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
|
|
2673
2674
|
t.textContent = sr, this.#a = document.createElement("canvas"), this.#a.setAttribute("role", "img"), this.#a.setAttribute("aria-label", "Avatar"), e.append(t, this.#a), this.#t.start(), this.#a.addEventListener("webglcontextlost", (r) => {
|
|
2674
2675
|
r.preventDefault(), this.#l();
|
|
2675
|
-
}), this.#a.addEventListener("webglcontextrestored", () => this.#
|
|
2676
|
+
}), this.#a.addEventListener("webglcontextrestored", () => this.#m());
|
|
2676
2677
|
}
|
|
2677
2678
|
/** The loaded runtime avatar, or null while loading. Every `mesh-avatar` method is available on it. */
|
|
2678
2679
|
get avatar() {
|
|
@@ -2684,7 +2685,7 @@ class lr extends or {
|
|
|
2684
2685
|
}
|
|
2685
2686
|
/** Loads a file, archive or URL without changing `src`. A changed `src` replaces this source. */
|
|
2686
2687
|
load(e) {
|
|
2687
|
-
return this.#h = e, this.#
|
|
2688
|
+
return this.#i = null, this.#h = e, this.#m(), this.ready;
|
|
2688
2689
|
}
|
|
2689
2690
|
get src() {
|
|
2690
2691
|
return this.getAttribute("src") ?? "";
|
|
@@ -2730,62 +2731,69 @@ class lr extends or {
|
|
|
2730
2731
|
}
|
|
2731
2732
|
connectedCallback() {
|
|
2732
2733
|
this.#c ??= new IntersectionObserver((e) => {
|
|
2733
|
-
this.#
|
|
2734
|
-
}), this.#c.observe(this), document.addEventListener("visibilitychange", this.#r), !this.#e && !this.#n ? this.#
|
|
2734
|
+
this.#p = e.at(-1)?.isIntersecting ?? !0, this.#r();
|
|
2735
|
+
}), this.#c.observe(this), document.addEventListener("visibilitychange", this.#r), !this.#e && !this.#n ? this.#b() : this.#r();
|
|
2735
2736
|
}
|
|
2736
2737
|
disconnectedCallback() {
|
|
2737
2738
|
this.#c?.disconnect(), document.removeEventListener("visibilitychange", this.#r), this.#l(), queueMicrotask(() => {
|
|
2738
|
-
this.isConnected || this.#
|
|
2739
|
+
this.isConnected || this.#v();
|
|
2739
2740
|
});
|
|
2740
2741
|
}
|
|
2741
2742
|
attributeChangedCallback(e, t, r) {
|
|
2742
|
-
t !== r && (e === "src" || e === "fit" ? (e === "src" && (this.#h = void 0), e === "fit" && Tt(r) === void 0 && this.#
|
|
2743
|
+
t !== r && (e === "src" || e === "fit" ? (e === "src" && (this.#h = void 0), e === "fit" && Tt(r) === void 0 && this.#d(e, r), this.isConnected && this.#b()) : e === "label" ? this.#a.setAttribute("aria-label", r || "Avatar") : e === "paused" ? this.#r() : this.#e && this.#y(this.#e, e));
|
|
2743
2744
|
}
|
|
2744
|
-
#
|
|
2745
|
+
#y(e, t) {
|
|
2745
2746
|
if (t === "expression") {
|
|
2746
2747
|
const r = ar(this.getAttribute("expression"));
|
|
2747
|
-
r ? e.setExpression(r) : this.#
|
|
2748
|
+
r ? e.setExpression(r) : this.#d(t, this.getAttribute(t));
|
|
2748
2749
|
} else if (t === "emotion") {
|
|
2749
2750
|
const r = rr(this.getAttribute("emotion"));
|
|
2750
|
-
r !== void 0 ? e.setEmotion(r) : this.#
|
|
2751
|
+
r !== void 0 ? e.setEmotion(r) : this.#d(t, this.getAttribute(t));
|
|
2751
2752
|
} else t === "no-idle" && e.setAutoIdle(!this.hasAttribute("no-idle"));
|
|
2752
2753
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2754
|
+
#b() {
|
|
2755
|
+
if (this.#t.start(), this.#i) return;
|
|
2756
|
+
const e = this.#i = {};
|
|
2757
|
+
queueMicrotask(() => {
|
|
2758
|
+
this.#i === e && (this.#i = null, this.isConnected && this.#m());
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
async #m(e = this.#h ?? (this.getAttribute("src") || er)) {
|
|
2762
|
+
const t = ++this.#o;
|
|
2763
|
+
this.#v(!1), this.#n = !0, this.#t.start();
|
|
2756
2764
|
try {
|
|
2757
2765
|
const r = await Qa(this.#a, e, { manual: !0, fit: this.fit });
|
|
2758
|
-
if (t !== this.#
|
|
2766
|
+
if (t !== this.#o) {
|
|
2759
2767
|
r.destroy();
|
|
2760
2768
|
return;
|
|
2761
2769
|
}
|
|
2762
2770
|
this.#n = !1, this.#e = r;
|
|
2763
2771
|
for (const i of ["expression", "emotion", "no-idle"])
|
|
2764
|
-
(i !== "emotion" || this.hasAttribute(i)) && this.#
|
|
2772
|
+
(i !== "emotion" || this.hasAttribute(i)) && this.#y(r, i);
|
|
2765
2773
|
this.#f !== null && r.setVoiceLevel(this.#f), matchMedia("(prefers-reduced-motion: reduce)").matches && r.setSwayGain(0.5), r.onMotion((i) => this.dispatchEvent(new CustomEvent("motion", { detail: { id: i } }))), r.advance(0), this.#t.resolve(r), this.dispatchEvent(new CustomEvent("load", { detail: { avatar: r } })), this.#r();
|
|
2766
2774
|
} catch (r) {
|
|
2767
|
-
if (t !== this.#
|
|
2775
|
+
if (t !== this.#o) return;
|
|
2768
2776
|
this.#n = !1, this.#t.reject(r), this.dispatchEvent(new CustomEvent("error", { detail: { error: r } }));
|
|
2769
2777
|
}
|
|
2770
2778
|
}
|
|
2771
2779
|
/** Destroys the avatar. Without `cancel = false` it also abandons a load in progress. */
|
|
2772
|
-
#
|
|
2773
|
-
e && (this.#
|
|
2780
|
+
#v(e = !0) {
|
|
2781
|
+
e && (this.#o++, this.#n = !1), this.#l(), this.#e?.destroy(), this.#e = null;
|
|
2774
2782
|
}
|
|
2775
2783
|
#r = () => {
|
|
2776
|
-
const e = this.#e && this.isConnected && this.#
|
|
2777
|
-
e && !this.#
|
|
2784
|
+
const e = this.#e && this.isConnected && this.#p && !document.hidden && !this.paused;
|
|
2785
|
+
e && !this.#s ? (this.#u = performance.now(), this.#s = requestAnimationFrame(this.#x)) : e || this.#l();
|
|
2778
2786
|
};
|
|
2779
|
-
#
|
|
2787
|
+
#x = (e) => {
|
|
2780
2788
|
const t = nr(e, this.#u);
|
|
2781
|
-
this.#u = e, this.#e?.advance(t, 1 / t), this.#
|
|
2789
|
+
this.#u = e, this.#e?.advance(t, 1 / t), this.#s = requestAnimationFrame(this.#x);
|
|
2782
2790
|
};
|
|
2783
2791
|
#l() {
|
|
2784
|
-
cancelAnimationFrame(this.#
|
|
2792
|
+
cancelAnimationFrame(this.#s), this.#s = 0;
|
|
2785
2793
|
}
|
|
2786
|
-
#
|
|
2794
|
+
#d(e, t) {
|
|
2787
2795
|
const r = `${e}=${t}`;
|
|
2788
|
-
this.#
|
|
2796
|
+
this.#g.has(r) || (this.#g.add(r), console.warn(`<mesh-avatar> ignores ${e}="${t}".`));
|
|
2789
2797
|
}
|
|
2790
2798
|
}
|
|
2791
2799
|
typeof customElements < "u" && !customElements.get("mesh-avatar") && customElements.define("mesh-avatar", lr);
|