@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 +4 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +115 -7
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/live2d-element.ts +143 -8
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`
|
|
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`
|
|
135
|
+
/** Imperative reload (also used when `model` / `source` changes). */
|
|
95
136
|
async loadModel(source, opts) {
|
|
96
137
|
if (source !== void 0) {
|
|
97
|
-
|
|
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
|
|
287
|
-
if (!
|
|
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:
|
|
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(
|
|
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.
|
|
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.
|
|
48
|
+
"@doki-land/live2d": "0.0.26"
|
|
49
49
|
},
|
|
50
50
|
"sideEffects": [
|
|
51
51
|
"./src/index.ts",
|
package/src/index.ts
CHANGED
package/src/live2d-element.ts
CHANGED
|
@@ -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`
|
|
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
|
-
|
|
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
|
|
354
|
-
if (!
|
|
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:
|
|
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(
|
|
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;
|