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 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
- #o = 0;
45
+ #h = 0;
46
46
  #r = !1;
47
+ #n = null;
47
48
  #c;
48
- #n = 0;
49
+ #o = 0;
49
50
  #d = 0;
50
- #p = !0;
51
+ #m = !0;
51
52
  #l = null;
52
53
  #u = null;
53
- #m = /* @__PURE__ */ new Set();
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.#h());
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.#h(), this.ready;
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.#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();
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.#v();
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.#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
+ 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
- #b(t, s) {
129
+ #v(t, s) {
129
130
  if (s === "expression") {
130
131
  const e = d(this.getAttribute("expression"));
131
- e ? t.setExpression(e) : this.#f(s, this.getAttribute(s));
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.#f(s, this.getAttribute(s));
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
- async #h(t = this.#c ?? (this.getAttribute("src") || a)) {
138
- const s = ++this.#o;
139
- this.#v(!1), this.#r = !0, this.#e.start();
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.#o) {
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.#b(e, r);
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.#o) return;
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
- #v(t = !0) {
157
- t && (this.#o++, this.#r = !1), this.#a(), this.#t?.destroy(), this.#t = null;
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.#p && !document.hidden && !this.paused;
161
- t && !this.#n ? (this.#d = performance.now(), this.#n = requestAnimationFrame(this.#g)) : t || this.#a();
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
- #g = (t) => {
171
+ #y = (t) => {
164
172
  const s = u(t, this.#d);
165
- this.#d = t, this.#t?.advance(s, 1 / s), this.#n = requestAnimationFrame(this.#g);
173
+ this.#d = t, this.#t?.advance(s, 1 / s), this.#o = requestAnimationFrame(this.#y);
166
174
  };
167
175
  #a() {
168
- cancelAnimationFrame(this.#n), this.#n = 0;
176
+ cancelAnimationFrame(this.#o), this.#o = 0;
169
177
  }
170
- #f(t, s) {
178
+ #p(t, s) {
171
179
  const e = `${t}=${s}`;
172
- this.#m.has(e) || (this.#m.add(e), console.warn(`<mesh-avatar> ignores ${t}="${s}".`));
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);
@@ -2658,21 +2658,22 @@ class lr extends or {
2658
2658
  #a;
2659
2659
  #e = null;
2660
2660
  #t = new ir();
2661
- #s = 0;
2661
+ #o = 0;
2662
2662
  #n = !1;
2663
+ #i = null;
2663
2664
  #h;
2664
- #i = 0;
2665
+ #s = 0;
2665
2666
  #u = 0;
2666
- #d = !0;
2667
+ #p = !0;
2667
2668
  #c = null;
2668
2669
  #f = null;
2669
- #p = /* @__PURE__ */ new Set();
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.#o());
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.#o(), this.ready;
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.#d = e.at(-1)?.isIntersecting ?? !0, this.#r();
2734
- }), this.#c.observe(this), document.addEventListener("visibilitychange", this.#r), !this.#e && !this.#n ? this.#o() : this.#r();
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.#y();
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.#m(e, r), this.isConnected && this.#o()) : e === "label" ? this.#a.setAttribute("aria-label", r || "Avatar") : e === "paused" ? this.#r() : this.#e && this.#g(this.#e, e));
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
- #g(e, t) {
2745
+ #y(e, t) {
2745
2746
  if (t === "expression") {
2746
2747
  const r = ar(this.getAttribute("expression"));
2747
- r ? e.setExpression(r) : this.#m(t, this.getAttribute(t));
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.#m(t, this.getAttribute(t));
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
- async #o(e = this.#h ?? (this.getAttribute("src") || er)) {
2754
- const t = ++this.#s;
2755
- this.#y(!1), this.#n = !0, this.#t.start();
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.#s) {
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.#g(r, i);
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.#s) return;
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
- #y(e = !0) {
2773
- e && (this.#s++, this.#n = !1), this.#l(), this.#e?.destroy(), this.#e = null;
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.#d && !document.hidden && !this.paused;
2777
- e && !this.#i ? (this.#u = performance.now(), this.#i = requestAnimationFrame(this.#b)) : e || this.#l();
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
- #b = (e) => {
2787
+ #x = (e) => {
2780
2788
  const t = nr(e, this.#u);
2781
- this.#u = e, this.#e?.advance(t, 1 / t), this.#i = requestAnimationFrame(this.#b);
2789
+ this.#u = e, this.#e?.advance(t, 1 / t), this.#s = requestAnimationFrame(this.#x);
2782
2790
  };
2783
2791
  #l() {
2784
- cancelAnimationFrame(this.#i), this.#i = 0;
2792
+ cancelAnimationFrame(this.#s), this.#s = 0;
2785
2793
  }
2786
- #m(e, t) {
2794
+ #d(e, t) {
2787
2795
  const r = `${e}=${t}`;
2788
- this.#p.has(r) || (this.#p.add(r), console.warn(`<mesh-avatar> ignores ${e}="${t}".`));
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mesh-avatar-element",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A <mesh-avatar> custom element: put a Mesh Avatar Studio avatar on any web page with one tag.",