@doki-land/live2d-element 0.0.25 → 0.0.26

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
@@ -28,6 +28,7 @@ pnpm add @doki-land/live2d-element
28
28
  width="320"
29
29
  height="320"
30
30
  autoplay
31
+ autosway
31
32
  interactive
32
33
  tracking="pointer"
33
34
  ></live-2d>
@@ -35,7 +36,10 @@ pnpm add @doki-land/live2d-element
35
36
  const el = document.querySelector("live-2d");
36
37
  el.addEventListener("live2d-ready", () => console.log("ready"));
37
38
  el.addEventListener("live2d-hit", (e) => console.log(e.detail));
39
+ el.addEventListener("live2d-progress", (e) => console.log(e.detail));
40
+ el.addEventListener("live2d-profile", (e) => console.log(e.detail));
38
41
  el.addEventListener("live2d-error", (e) => console.error(e.detail));
42
+ // Properties: source / renderOptions (complex ModelSource + prefer order)
39
43
  // Methods: loadModel / playMotion / setExpression / lookAt / pause / resume
40
44
  </script>
41
45
  ```
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Live2dRuntime, PlayMotionOptions } from '@doki-land/live2d';
1
+ import { ModelSource, RendererKind, Live2dRuntime, PlayMotionOptions } from '@doki-land/live2d';
2
2
 
3
3
  /** Idempotent `customElements.define('live-2d', …)`. */
4
4
  declare function defineLive2dElement(): void;
@@ -10,6 +10,9 @@ declare function defineLive2dElements(): void;
10
10
  declare const LIVE2D_ELEMENT_TAG: "live-2d";
11
11
  type Live2dElementRenderer = "auto" | "webgpu" | "webgl2" | "canvas2d";
12
12
  type Live2dElementTracking = "pointer" | "none";
13
+ interface Live2dElementRenderOptions {
14
+ prefer?: RendererKind[];
15
+ }
13
16
  /**
14
17
  * Thin `<live-2d>` host: attribute → createLive2d → Stage RAF → live2d-* events.
15
18
  */
@@ -18,6 +21,11 @@ declare class Live2dElement extends HTMLElement {
18
21
  static get observedAttributes(): string[];
19
22
  get model(): string;
20
23
  set model(value: string);
24
+ /** Structured model source (property-only; not reflected to attributes). */
25
+ get source(): ModelSource | null;
26
+ set source(value: ModelSource | null);
27
+ get renderOptions(): Live2dElementRenderOptions | null;
28
+ set renderOptions(value: Live2dElementRenderOptions | null);
21
29
  get renderer(): Live2dElementRenderer;
22
30
  set renderer(value: Live2dElementRenderer);
23
31
  get width(): number;
@@ -26,13 +34,15 @@ declare class Live2dElement extends HTMLElement {
26
34
  set height(value: number);
27
35
  get autoplay(): boolean;
28
36
  set autoplay(value: boolean);
37
+ get autosway(): boolean;
38
+ set autosway(value: boolean);
29
39
  get interactive(): boolean;
30
40
  set interactive(value: boolean);
31
41
  get tracking(): Live2dElementTracking;
32
42
  set tracking(value: Live2dElementTracking);
33
43
  get runtime(): Live2dRuntime | null;
34
- /** Imperative reload (also used when `model` attribute changes). */
35
- loadModel(source?: string, opts?: {
44
+ /** Imperative reload (also used when `model` / `source` changes). */
45
+ loadModel(source?: ModelSource | string, opts?: {
36
46
  signal?: AbortSignal;
37
47
  }): Promise<void>;
38
48
  playMotion(group: string, index?: number, options?: PlayMotionOptions): Promise<boolean>;
@@ -72,4 +82,4 @@ declare class Live2dWidgetElement extends HTMLElement {
72
82
  attributeChangedCallback(): void;
73
83
  }
74
84
 
75
- export { LIVE2D_ELEMENT_TAG, LIVE2D_WIDGET_ELEMENT_TAG, Live2dElement, type Live2dElementRenderer, type Live2dElementTracking, Live2dWidgetElement, defineLive2dElement, defineLive2dElements, defineLive2dWidgetElement };
85
+ export { LIVE2D_ELEMENT_TAG, LIVE2D_WIDGET_ELEMENT_TAG, Live2dElement, type Live2dElementRenderOptions, type Live2dElementRenderer, type Live2dElementTracking, Live2dWidgetElement, defineLive2dElement, defineLive2dElements, defineLive2dWidgetElement };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ var OBSERVED = [
9
9
  "width",
10
10
  "height",
11
11
  "autoplay",
12
+ "autosway",
12
13
  "interactive",
13
14
  "tracking"
14
15
  ];
@@ -25,6 +26,18 @@ function parseBoolAttr(el, name, fallback) {
25
26
  if (v === "false" || v === "0") return false;
26
27
  return true;
27
28
  }
29
+ function sourceLabel(source) {
30
+ if (typeof source === "string") return source;
31
+ if (source.kind === "url") return source.url;
32
+ if (source.kind === "npm") {
33
+ const path = source.path.replace(/^\/+/, "");
34
+ return `npm:${source.package}/${path}`;
35
+ }
36
+ return source.baseUrl;
37
+ }
38
+ function swayAngleX(binding, normalized) {
39
+ return normalized >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * normalized : binding.defaultValue + (binding.defaultValue - binding.min) * normalized;
40
+ }
28
41
  var Live2dElement = class extends HTMLElement {
29
42
  static get observedAttributes() {
30
43
  return [...OBSERVED];
@@ -33,23 +46,44 @@ var Live2dElement = class extends HTMLElement {
33
46
  #canvas = null;
34
47
  #mountGen = 0;
35
48
  #model = "";
49
+ #sourceProperty = null;
50
+ #renderOptions = null;
36
51
  #renderer = "auto";
37
52
  #width = 320;
38
53
  #height = 320;
39
54
  #autoplay = true;
55
+ #autosway = false;
40
56
  #interactive = false;
41
57
  #tracking = "none";
42
58
  #connected = false;
43
59
  #boundPointer = null;
44
60
  #unsubs = [];
61
+ #unsubFrame = null;
62
+ #swayPhase = 0;
45
63
  get model() {
46
64
  return this.#model;
47
65
  }
48
66
  set model(value) {
49
67
  const next = String(value ?? "");
68
+ this.#sourceProperty = null;
50
69
  if (next) this.setAttribute("model", next);
51
70
  else this.removeAttribute("model");
52
71
  }
72
+ /** Structured model source (property-only; not reflected to attributes). */
73
+ get source() {
74
+ return this.#sourceProperty;
75
+ }
76
+ set source(value) {
77
+ this.#sourceProperty = value;
78
+ if (this.#connected) void this.#boot();
79
+ }
80
+ get renderOptions() {
81
+ return this.#renderOptions;
82
+ }
83
+ set renderOptions(value) {
84
+ this.#renderOptions = value;
85
+ if (this.#connected && this.#resolveLoadSource()) void this.#boot();
86
+ }
53
87
  get renderer() {
54
88
  return this.#renderer;
55
89
  }
@@ -75,6 +109,13 @@ var Live2dElement = class extends HTMLElement {
75
109
  if (value) this.setAttribute("autoplay", "");
76
110
  else this.removeAttribute("autoplay");
77
111
  }
112
+ get autosway() {
113
+ return this.#autosway;
114
+ }
115
+ set autosway(value) {
116
+ if (value) this.setAttribute("autosway", "");
117
+ else this.removeAttribute("autosway");
118
+ }
78
119
  get interactive() {
79
120
  return this.#interactive;
80
121
  }
@@ -91,10 +132,14 @@ var Live2dElement = class extends HTMLElement {
91
132
  get runtime() {
92
133
  return this.#runtime;
93
134
  }
94
- /** Imperative reload (also used when `model` attribute changes). */
135
+ /** Imperative reload (also used when `model` / `source` changes). */
95
136
  async loadModel(source, opts) {
96
137
  if (source !== void 0) {
97
- this.model = String(source);
138
+ if (typeof source === "string") {
139
+ this.model = source;
140
+ } else {
141
+ this.#sourceProperty = source;
142
+ }
98
143
  }
99
144
  await this.#boot(opts?.signal);
100
145
  }
@@ -136,6 +181,7 @@ var Live2dElement = class extends HTMLElement {
136
181
  attributeChangedCallback(name, _old, value) {
137
182
  if (name === "model") {
138
183
  this.#model = value ?? "";
184
+ this.#sourceProperty = null;
139
185
  if (this.#connected) void this.#boot();
140
186
  return;
141
187
  }
@@ -162,6 +208,11 @@ var Live2dElement = class extends HTMLElement {
162
208
  }
163
209
  return;
164
210
  }
211
+ if (name === "autosway") {
212
+ this.#autosway = parseBoolAttr(this, "autosway", false);
213
+ this.#syncAutosway(this.#runtime, this.#mountGen);
214
+ return;
215
+ }
165
216
  if (name === "interactive") {
166
217
  this.#interactive = parseBoolAttr(this, "interactive", false);
167
218
  this.#syncPointer();
@@ -192,9 +243,20 @@ var Live2dElement = class extends HTMLElement {
192
243
  );
193
244
  }
194
245
  this.#autoplay = parseBoolAttr(this, "autoplay", true);
246
+ this.#autosway = parseBoolAttr(this, "autosway", false);
195
247
  this.#interactive = parseBoolAttr(this, "interactive", false);
196
248
  this.#tracking = this.getAttribute("tracking") === "pointer" ? "pointer" : "none";
197
249
  }
250
+ #resolveLoadSource() {
251
+ if (this.#sourceProperty != null) return this.#sourceProperty;
252
+ const model = this.#model.trim();
253
+ return model ? model : null;
254
+ }
255
+ #resolvePrefer() {
256
+ const prefer = this.#renderOptions?.prefer;
257
+ if (prefer?.length) return [...prefer];
258
+ return preferFromRenderer(this.#renderer);
259
+ }
198
260
  #ensureCanvas() {
199
261
  if (this.#canvas?.isConnected) return this.#canvas;
200
262
  this.replaceChildren();
@@ -232,6 +294,23 @@ var Live2dElement = class extends HTMLElement {
232
294
  }
233
295
  this.#boundPointer = null;
234
296
  }
297
+ #syncAutosway(runtime, gen) {
298
+ this.#unsubFrame?.();
299
+ this.#unsubFrame = null;
300
+ if (!runtime || !this.#autosway) return;
301
+ this.#swayPhase = 0;
302
+ this.#unsubFrame = runtime.stage.onFrame((dt) => {
303
+ if (gen !== this.#mountGen) return;
304
+ this.#swayPhase += dt;
305
+ const binding = runtime.actor.parameterMap().get("PARAM_ANGLE_X");
306
+ if (!binding) return;
307
+ const normalized = Math.sin(this.#swayPhase) * 0.25;
308
+ runtime.setParameter(
309
+ "PARAM_ANGLE_X",
310
+ swayAngleX(binding, normalized)
311
+ );
312
+ });
313
+ }
235
314
  #syncPointer() {
236
315
  this.#detachPointer();
237
316
  const canvas = this.#canvas;
@@ -271,6 +350,8 @@ var Live2dElement = class extends HTMLElement {
271
350
  }
272
351
  #destroyRuntime() {
273
352
  this.#clearUnsubs();
353
+ this.#unsubFrame?.();
354
+ this.#unsubFrame = null;
274
355
  this.#detachPointer();
275
356
  try {
276
357
  this.#runtime?.stage.stop?.();
@@ -283,8 +364,8 @@ var Live2dElement = class extends HTMLElement {
283
364
  }
284
365
  async #boot(signal) {
285
366
  if (!this.#connected) return;
286
- const model = this.#model.trim();
287
- if (!model) return;
367
+ const source = this.#resolveLoadSource();
368
+ if (!source) return;
288
369
  const gen = ++this.#mountGen;
289
370
  this.#destroyRuntime();
290
371
  if (signal?.aborted) {
@@ -297,11 +378,12 @@ var Live2dElement = class extends HTMLElement {
297
378
  };
298
379
  signal?.addEventListener("abort", onAbort, { once: true });
299
380
  const canvas = this.#ensureCanvas();
381
+ const sourceLabelText = sourceLabel(source);
300
382
  this.setAttribute("data-phase", "loading");
301
383
  this.setAttribute("aria-busy", "true");
302
384
  try {
303
385
  const runtime = createLive2d({
304
- prefer: preferFromRenderer(this.#renderer),
386
+ prefer: this.#resolvePrefer(),
305
387
  updateMode: "auto"
306
388
  });
307
389
  if (gen !== this.#mountGen) {
@@ -319,12 +401,13 @@ var Live2dElement = class extends HTMLElement {
319
401
  } catch {
320
402
  }
321
403
  }
404
+ this.#syncAutosway(runtime, gen);
322
405
  this.#syncPointer();
323
406
  this.dispatchEvent(
324
407
  new CustomEvent("live2d-ready", {
325
408
  bubbles: true,
326
409
  composed: true,
327
- detail: { model }
410
+ detail: { model: sourceLabelText }
328
411
  })
329
412
  );
330
413
  })
@@ -335,6 +418,30 @@ var Live2dElement = class extends HTMLElement {
335
418
  this.#emitError(payload?.error ?? "load error");
336
419
  })
337
420
  );
421
+ this.#unsubs.push(
422
+ runtime.events.on("progress", (payload) => {
423
+ if (gen !== this.#mountGen) return;
424
+ this.dispatchEvent(
425
+ new CustomEvent("live2d-progress", {
426
+ bubbles: true,
427
+ composed: true,
428
+ detail: payload
429
+ })
430
+ );
431
+ })
432
+ );
433
+ this.#unsubs.push(
434
+ runtime.events.on("profile", (payload) => {
435
+ if (gen !== this.#mountGen) return;
436
+ this.dispatchEvent(
437
+ new CustomEvent("live2d-profile", {
438
+ bubbles: true,
439
+ composed: true,
440
+ detail: payload
441
+ })
442
+ );
443
+ })
444
+ );
338
445
  this.#unsubs.push(
339
446
  runtime.events.on("motion:start", (payload) => {
340
447
  if (gen !== this.#mountGen) return;
@@ -360,12 +467,13 @@ var Live2dElement = class extends HTMLElement {
360
467
  })
361
468
  );
362
469
  runtime.mount(canvas);
363
- await runtime.loadModel(model, void 0, { signal });
470
+ await runtime.loadModel(source, void 0, { signal });
364
471
  if (gen !== this.#mountGen) {
365
472
  runtime.destroy();
366
473
  return;
367
474
  }
368
475
  this.#runtime = runtime;
476
+ this.#syncAutosway(runtime, gen);
369
477
  this.#syncPointer();
370
478
  } catch (err) {
371
479
  if (gen !== this.#mountGen) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doki-land/live2d-element",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "Official <live-2d> Custom Element — thin host over @doki-land/live2d (Stage-owned RAF).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "test": "vitest run --passWithNoTests"
46
46
  },
47
47
  "dependencies": {
48
- "@doki-land/live2d": "0.0.25"
48
+ "@doki-land/live2d": "0.0.26"
49
49
  },
50
50
  "sideEffects": [
51
51
  "./src/index.ts",
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  LIVE2D_ELEMENT_TAG,
16
16
  Live2dElement,
17
17
  type Live2dElementRenderer,
18
+ type Live2dElementRenderOptions,
18
19
  type Live2dElementTracking,
19
20
  } from "./live2d-element.js";
20
21
  export {
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  createLive2d,
3
3
  type Live2dRuntime,
4
+ type ModelSource,
4
5
  type PlayMotionOptions,
6
+ type RendererKind,
5
7
  } from "@doki-land/live2d";
6
8
 
7
9
  export const LIVE2D_ELEMENT_TAG = "live-2d" as const;
@@ -9,12 +11,17 @@ export const LIVE2D_ELEMENT_TAG = "live-2d" as const;
9
11
  export type Live2dElementRenderer = "auto" | "webgpu" | "webgl2" | "canvas2d";
10
12
  export type Live2dElementTracking = "pointer" | "none";
11
13
 
14
+ export interface Live2dElementRenderOptions {
15
+ prefer?: RendererKind[];
16
+ }
17
+
12
18
  const OBSERVED = [
13
19
  "model",
14
20
  "renderer",
15
21
  "width",
16
22
  "height",
17
23
  "autoplay",
24
+ "autosway",
18
25
  "interactive",
19
26
  "tracking",
20
27
  ] as const;
@@ -40,6 +47,31 @@ function parseBoolAttr(
40
47
  return true;
41
48
  }
42
49
 
50
+ function sourceLabel(source: ModelSource): string {
51
+ if (typeof source === "string") return source;
52
+ if (source.kind === "url") return source.url;
53
+ if (source.kind === "npm") {
54
+ const path = source.path.replace(/^\/+/, "");
55
+ return `npm:${source.package}/${path}`;
56
+ }
57
+ return source.baseUrl;
58
+ }
59
+
60
+ function swayAngleX(
61
+ binding: {
62
+ defaultValue: number;
63
+ min: number;
64
+ max: number;
65
+ },
66
+ normalized: number,
67
+ ): number {
68
+ return normalized >= 0
69
+ ? binding.defaultValue +
70
+ (binding.max - binding.defaultValue) * normalized
71
+ : binding.defaultValue +
72
+ (binding.defaultValue - binding.min) * normalized;
73
+ }
74
+
43
75
  /**
44
76
  * Thin `<live-2d>` host: attribute → createLive2d → Stage RAF → live2d-* events.
45
77
  */
@@ -52,25 +84,48 @@ export class Live2dElement extends HTMLElement {
52
84
  #canvas: HTMLCanvasElement | null = null;
53
85
  #mountGen = 0;
54
86
  #model = "";
87
+ #sourceProperty: ModelSource | null = null;
88
+ #renderOptions: Live2dElementRenderOptions | null = null;
55
89
  #renderer: Live2dElementRenderer = "auto";
56
90
  #width = 320;
57
91
  #height = 320;
58
92
  #autoplay = true;
93
+ #autosway = false;
59
94
  #interactive = false;
60
95
  #tracking: Live2dElementTracking = "none";
61
96
  #connected = false;
62
97
  #boundPointer: ((event: PointerEvent) => void) | null = null;
63
98
  #unsubs: Array<() => void> = [];
99
+ #unsubFrame: (() => void) | null = null;
100
+ #swayPhase = 0;
64
101
 
65
102
  get model(): string {
66
103
  return this.#model;
67
104
  }
68
105
  set model(value: string) {
69
106
  const next = String(value ?? "");
107
+ this.#sourceProperty = null;
70
108
  if (next) this.setAttribute("model", next);
71
109
  else this.removeAttribute("model");
72
110
  }
73
111
 
112
+ /** Structured model source (property-only; not reflected to attributes). */
113
+ get source(): ModelSource | null {
114
+ return this.#sourceProperty;
115
+ }
116
+ set source(value: ModelSource | null) {
117
+ this.#sourceProperty = value;
118
+ if (this.#connected) void this.#boot();
119
+ }
120
+
121
+ get renderOptions(): Live2dElementRenderOptions | null {
122
+ return this.#renderOptions;
123
+ }
124
+ set renderOptions(value: Live2dElementRenderOptions | null) {
125
+ this.#renderOptions = value;
126
+ if (this.#connected && this.#resolveLoadSource()) void this.#boot();
127
+ }
128
+
74
129
  get renderer(): Live2dElementRenderer {
75
130
  return this.#renderer;
76
131
  }
@@ -100,6 +155,14 @@ export class Live2dElement extends HTMLElement {
100
155
  else this.removeAttribute("autoplay");
101
156
  }
102
157
 
158
+ get autosway(): boolean {
159
+ return this.#autosway;
160
+ }
161
+ set autosway(value: boolean) {
162
+ if (value) this.setAttribute("autosway", "");
163
+ else this.removeAttribute("autosway");
164
+ }
165
+
103
166
  get interactive(): boolean {
104
167
  return this.#interactive;
105
168
  }
@@ -119,13 +182,17 @@ export class Live2dElement extends HTMLElement {
119
182
  return this.#runtime;
120
183
  }
121
184
 
122
- /** Imperative reload (also used when `model` attribute changes). */
185
+ /** Imperative reload (also used when `model` / `source` changes). */
123
186
  async loadModel(
124
- source?: string,
187
+ source?: ModelSource | string,
125
188
  opts?: { signal?: AbortSignal },
126
189
  ): Promise<void> {
127
190
  if (source !== undefined) {
128
- this.model = String(source);
191
+ if (typeof source === "string") {
192
+ this.model = source;
193
+ } else {
194
+ this.#sourceProperty = source;
195
+ }
129
196
  }
130
197
  await this.#boot(opts?.signal);
131
198
  }
@@ -187,6 +254,7 @@ export class Live2dElement extends HTMLElement {
187
254
  ): void {
188
255
  if (name === "model") {
189
256
  this.#model = value ?? "";
257
+ this.#sourceProperty = null;
190
258
  if (this.#connected) void this.#boot();
191
259
  return;
192
260
  }
@@ -213,6 +281,11 @@ export class Live2dElement extends HTMLElement {
213
281
  }
214
282
  return;
215
283
  }
284
+ if (name === "autosway") {
285
+ this.#autosway = parseBoolAttr(this, "autosway", false);
286
+ this.#syncAutosway(this.#runtime, this.#mountGen);
287
+ return;
288
+ }
216
289
  if (name === "interactive") {
217
290
  this.#interactive = parseBoolAttr(this, "interactive", false);
218
291
  this.#syncPointer();
@@ -245,11 +318,24 @@ export class Live2dElement extends HTMLElement {
245
318
  );
246
319
  }
247
320
  this.#autoplay = parseBoolAttr(this, "autoplay", true);
321
+ this.#autosway = parseBoolAttr(this, "autosway", false);
248
322
  this.#interactive = parseBoolAttr(this, "interactive", false);
249
323
  this.#tracking =
250
324
  this.getAttribute("tracking") === "pointer" ? "pointer" : "none";
251
325
  }
252
326
 
327
+ #resolveLoadSource(): ModelSource | null {
328
+ if (this.#sourceProperty != null) return this.#sourceProperty;
329
+ const model = this.#model.trim();
330
+ return model ? model : null;
331
+ }
332
+
333
+ #resolvePrefer(): RendererKind[] {
334
+ const prefer = this.#renderOptions?.prefer;
335
+ if (prefer?.length) return [...prefer];
336
+ return preferFromRenderer(this.#renderer);
337
+ }
338
+
253
339
  #ensureCanvas(): HTMLCanvasElement {
254
340
  if (this.#canvas?.isConnected) return this.#canvas;
255
341
  this.replaceChildren();
@@ -295,6 +381,24 @@ export class Live2dElement extends HTMLElement {
295
381
  this.#boundPointer = null;
296
382
  }
297
383
 
384
+ #syncAutosway(runtime: Live2dRuntime | null, gen: number): void {
385
+ this.#unsubFrame?.();
386
+ this.#unsubFrame = null;
387
+ if (!runtime || !this.#autosway) return;
388
+ this.#swayPhase = 0;
389
+ this.#unsubFrame = runtime.stage.onFrame((dt) => {
390
+ if (gen !== this.#mountGen) return;
391
+ this.#swayPhase += dt;
392
+ const binding = runtime.actor.parameterMap().get("PARAM_ANGLE_X");
393
+ if (!binding) return;
394
+ const normalized = Math.sin(this.#swayPhase) * 0.25;
395
+ runtime.setParameter(
396
+ "PARAM_ANGLE_X",
397
+ swayAngleX(binding, normalized),
398
+ );
399
+ });
400
+ }
401
+
298
402
  #syncPointer(): void {
299
403
  this.#detachPointer();
300
404
  const canvas = this.#canvas;
@@ -336,6 +440,8 @@ export class Live2dElement extends HTMLElement {
336
440
 
337
441
  #destroyRuntime(): void {
338
442
  this.#clearUnsubs();
443
+ this.#unsubFrame?.();
444
+ this.#unsubFrame = null;
339
445
  this.#detachPointer();
340
446
  try {
341
447
  this.#runtime?.stage.stop?.();
@@ -350,8 +456,8 @@ export class Live2dElement extends HTMLElement {
350
456
 
351
457
  async #boot(signal?: AbortSignal): Promise<void> {
352
458
  if (!this.#connected) return;
353
- const model = this.#model.trim();
354
- if (!model) return;
459
+ const source = this.#resolveLoadSource();
460
+ if (!source) return;
355
461
 
356
462
  const gen = ++this.#mountGen;
357
463
  this.#destroyRuntime();
@@ -368,12 +474,13 @@ export class Live2dElement extends HTMLElement {
368
474
  signal?.addEventListener("abort", onAbort, { once: true });
369
475
 
370
476
  const canvas = this.#ensureCanvas();
477
+ const sourceLabelText = sourceLabel(source);
371
478
  this.setAttribute("data-phase", "loading");
372
479
  this.setAttribute("aria-busy", "true");
373
480
 
374
481
  try {
375
482
  const runtime = createLive2d({
376
- prefer: preferFromRenderer(this.#renderer),
483
+ prefer: this.#resolvePrefer(),
377
484
  updateMode: "auto",
378
485
  });
379
486
  if (gen !== this.#mountGen) {
@@ -393,12 +500,13 @@ export class Live2dElement extends HTMLElement {
393
500
  /* already running */
394
501
  }
395
502
  }
503
+ this.#syncAutosway(runtime, gen);
396
504
  this.#syncPointer();
397
505
  this.dispatchEvent(
398
506
  new CustomEvent("live2d-ready", {
399
507
  bubbles: true,
400
508
  composed: true,
401
- detail: { model },
509
+ detail: { model: sourceLabelText },
402
510
  }),
403
511
  );
404
512
  }),
@@ -411,6 +519,32 @@ export class Live2dElement extends HTMLElement {
411
519
  }),
412
520
  );
413
521
 
522
+ this.#unsubs.push(
523
+ runtime.events.on("progress", (payload) => {
524
+ if (gen !== this.#mountGen) return;
525
+ this.dispatchEvent(
526
+ new CustomEvent("live2d-progress", {
527
+ bubbles: true,
528
+ composed: true,
529
+ detail: payload,
530
+ }),
531
+ );
532
+ }),
533
+ );
534
+
535
+ this.#unsubs.push(
536
+ runtime.events.on("profile", (payload) => {
537
+ if (gen !== this.#mountGen) return;
538
+ this.dispatchEvent(
539
+ new CustomEvent("live2d-profile", {
540
+ bubbles: true,
541
+ composed: true,
542
+ detail: payload,
543
+ }),
544
+ );
545
+ }),
546
+ );
547
+
414
548
  this.#unsubs.push(
415
549
  runtime.events.on("motion:start", (payload) => {
416
550
  if (gen !== this.#mountGen) return;
@@ -438,12 +572,13 @@ export class Live2dElement extends HTMLElement {
438
572
  );
439
573
 
440
574
  runtime.mount(canvas);
441
- await runtime.loadModel(model, undefined, { signal });
575
+ await runtime.loadModel(source, undefined, { signal });
442
576
  if (gen !== this.#mountGen) {
443
577
  runtime.destroy();
444
578
  return;
445
579
  }
446
580
  this.#runtime = runtime;
581
+ this.#syncAutosway(runtime, gen);
447
582
  this.#syncPointer();
448
583
  } catch (err) {
449
584
  if (gen !== this.#mountGen) return;