@doki-land/live2d-element 0.0.23 → 0.0.25
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 +12 -3
- package/dist/index.d.ts +48 -5
- package/dist/index.js +307 -34
- package/package.json +2 -2
- package/src/define.ts +28 -4
- package/src/index.ts +16 -9
- package/src/live2d-element.ts +222 -32
- package/src/live2d-widget-element.ts +117 -0
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# `@doki-land/live2d-element`
|
|
2
2
|
|
|
3
|
-
Official `<live-2d>` Custom
|
|
3
|
+
Official `<live-2d>` / `<live-2d-widget>` Custom Elements — thin hosts over
|
|
4
|
+
`@doki-land/live2d` with Stage-owned RAF.
|
|
4
5
|
|
|
5
6
|
## Status (Developer Preview)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Pure TypeScript `customElements.define`. Zero VMZ runtime dependency.
|
|
9
|
+
Primary TS symbols use the `Live2d*` / `createLive2d*` spelling (see design `00` §4.4).
|
|
8
10
|
|
|
9
|
-
HTML Custom Elements require a hyphen;
|
|
11
|
+
HTML Custom Elements require a hyphen; tags are **`live-2d`** and **`live-2d-widget`**.
|
|
10
12
|
|
|
11
13
|
## Install
|
|
12
14
|
|
|
@@ -26,12 +28,19 @@ pnpm add @doki-land/live2d-element
|
|
|
26
28
|
width="320"
|
|
27
29
|
height="320"
|
|
28
30
|
autoplay
|
|
31
|
+
interactive
|
|
32
|
+
tracking="pointer"
|
|
29
33
|
></live-2d>
|
|
30
34
|
<script type="module">
|
|
31
35
|
const el = document.querySelector("live-2d");
|
|
32
36
|
el.addEventListener("live2d-ready", () => console.log("ready"));
|
|
37
|
+
el.addEventListener("live2d-hit", (e) => console.log(e.detail));
|
|
33
38
|
el.addEventListener("live2d-error", (e) => console.error(e.detail));
|
|
39
|
+
// Methods: loadModel / playMotion / setExpression / lookAt / pause / resume
|
|
34
40
|
</script>
|
|
35
41
|
```
|
|
36
42
|
|
|
43
|
+
`<live-2d-widget>` nests an inner `<live-2d>` and forwards model/size attributes
|
|
44
|
+
(product-shell chrome POC; no second Stage clock).
|
|
45
|
+
|
|
37
46
|
See `fixtures/index.html` for a native HTML dogfood page.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Live2dRuntime, PlayMotionOptions } from '@doki-land/live2d';
|
|
2
2
|
|
|
3
3
|
/** Idempotent `customElements.define('live-2d', …)`. */
|
|
4
4
|
declare function defineLive2dElement(): void;
|
|
5
|
+
/** Idempotent `customElements.define('live-2d-widget', …)`. */
|
|
6
|
+
declare function defineLive2dWidgetElement(): void;
|
|
7
|
+
/** Register both official CE tags. */
|
|
8
|
+
declare function defineLive2dElements(): void;
|
|
5
9
|
|
|
6
10
|
declare const LIVE2D_ELEMENT_TAG: "live-2d";
|
|
7
11
|
type Live2dElementRenderer = "auto" | "webgpu" | "webgl2" | "canvas2d";
|
|
12
|
+
type Live2dElementTracking = "pointer" | "none";
|
|
8
13
|
/**
|
|
9
|
-
* Thin `<live-2d>` host: attribute →
|
|
14
|
+
* Thin `<live-2d>` host: attribute → createLive2d → Stage RAF → live2d-* events.
|
|
10
15
|
*/
|
|
11
16
|
declare class Live2dElement extends HTMLElement {
|
|
12
17
|
#private;
|
|
@@ -21,12 +26,50 @@ declare class Live2dElement extends HTMLElement {
|
|
|
21
26
|
set height(value: number);
|
|
22
27
|
get autoplay(): boolean;
|
|
23
28
|
set autoplay(value: boolean);
|
|
24
|
-
get
|
|
29
|
+
get interactive(): boolean;
|
|
30
|
+
set interactive(value: boolean);
|
|
31
|
+
get tracking(): Live2dElementTracking;
|
|
32
|
+
set tracking(value: Live2dElementTracking);
|
|
33
|
+
get runtime(): Live2dRuntime | null;
|
|
25
34
|
/** Imperative reload (also used when `model` attribute changes). */
|
|
26
|
-
loadModel(source?: string
|
|
35
|
+
loadModel(source?: string, opts?: {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
playMotion(group: string, index?: number, options?: PlayMotionOptions): Promise<boolean>;
|
|
39
|
+
setExpression(name: string | null): Promise<boolean>;
|
|
40
|
+
/** Stage-normalized look-at in [-1, 1] client-mapped coords (x right, y up). */
|
|
41
|
+
lookAt(x: number, y: number): void;
|
|
42
|
+
pause(): void;
|
|
43
|
+
resume(): void;
|
|
27
44
|
connectedCallback(): void;
|
|
28
45
|
disconnectedCallback(): void;
|
|
29
46
|
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
declare const LIVE2D_WIDGET_ELEMENT_TAG: "live-2d-widget";
|
|
50
|
+
/**
|
|
51
|
+
* Thin `<live-2d-widget>` product-shell CE (v0.0.24 POC).
|
|
52
|
+
*
|
|
53
|
+
* Owns chrome layout only; delegates model/RAF/hit to an inner `<live-2d>`.
|
|
54
|
+
* Does not create a second Stage clock.
|
|
55
|
+
*/
|
|
56
|
+
declare class Live2dWidgetElement extends HTMLElement {
|
|
57
|
+
#private;
|
|
58
|
+
static get observedAttributes(): string[];
|
|
59
|
+
get model(): string;
|
|
60
|
+
set model(value: string);
|
|
61
|
+
get renderer(): string;
|
|
62
|
+
set renderer(value: string);
|
|
63
|
+
get width(): number;
|
|
64
|
+
set width(value: number);
|
|
65
|
+
get height(): number;
|
|
66
|
+
set height(value: number);
|
|
67
|
+
get autoplay(): boolean;
|
|
68
|
+
set autoplay(value: boolean);
|
|
69
|
+
/** Inner `<live-2d>` when present. */
|
|
70
|
+
get actor(): Live2dElement | null;
|
|
71
|
+
connectedCallback(): void;
|
|
72
|
+
attributeChangedCallback(): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { LIVE2D_ELEMENT_TAG, LIVE2D_WIDGET_ELEMENT_TAG, Live2dElement, type Live2dElementRenderer, type Live2dElementTracking, Live2dWidgetElement, defineLive2dElement, defineLive2dElements, defineLive2dWidgetElement };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// src/live2d-element.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createLive2d
|
|
4
|
+
} from "@doki-land/live2d";
|
|
3
5
|
var LIVE2D_ELEMENT_TAG = "live-2d";
|
|
4
|
-
var OBSERVED = [
|
|
6
|
+
var OBSERVED = [
|
|
7
|
+
"model",
|
|
8
|
+
"renderer",
|
|
9
|
+
"width",
|
|
10
|
+
"height",
|
|
11
|
+
"autoplay",
|
|
12
|
+
"interactive",
|
|
13
|
+
"tracking"
|
|
14
|
+
];
|
|
5
15
|
function preferFromRenderer(renderer) {
|
|
6
16
|
if (renderer === "webgpu") return ["webgpu", "webgl2", "canvas2d"];
|
|
7
17
|
if (renderer === "webgl2") return ["webgl2", "canvas2d", "webgpu"];
|
|
@@ -27,7 +37,11 @@ var Live2dElement = class extends HTMLElement {
|
|
|
27
37
|
#width = 320;
|
|
28
38
|
#height = 320;
|
|
29
39
|
#autoplay = true;
|
|
40
|
+
#interactive = false;
|
|
41
|
+
#tracking = "none";
|
|
30
42
|
#connected = false;
|
|
43
|
+
#boundPointer = null;
|
|
44
|
+
#unsubs = [];
|
|
31
45
|
get model() {
|
|
32
46
|
return this.#model;
|
|
33
47
|
}
|
|
@@ -61,15 +75,52 @@ var Live2dElement = class extends HTMLElement {
|
|
|
61
75
|
if (value) this.setAttribute("autoplay", "");
|
|
62
76
|
else this.removeAttribute("autoplay");
|
|
63
77
|
}
|
|
78
|
+
get interactive() {
|
|
79
|
+
return this.#interactive;
|
|
80
|
+
}
|
|
81
|
+
set interactive(value) {
|
|
82
|
+
if (value) this.setAttribute("interactive", "");
|
|
83
|
+
else this.removeAttribute("interactive");
|
|
84
|
+
}
|
|
85
|
+
get tracking() {
|
|
86
|
+
return this.#tracking;
|
|
87
|
+
}
|
|
88
|
+
set tracking(value) {
|
|
89
|
+
this.setAttribute("tracking", value === "pointer" ? "pointer" : "none");
|
|
90
|
+
}
|
|
64
91
|
get runtime() {
|
|
65
92
|
return this.#runtime;
|
|
66
93
|
}
|
|
67
94
|
/** Imperative reload (also used when `model` attribute changes). */
|
|
68
|
-
async loadModel(source) {
|
|
95
|
+
async loadModel(source, opts) {
|
|
69
96
|
if (source !== void 0) {
|
|
70
97
|
this.model = String(source);
|
|
71
98
|
}
|
|
72
|
-
await this.#boot();
|
|
99
|
+
await this.#boot(opts?.signal);
|
|
100
|
+
}
|
|
101
|
+
playMotion(group, index, options) {
|
|
102
|
+
return this.#runtime?.playMotion(group, index, options) ?? Promise.resolve(false);
|
|
103
|
+
}
|
|
104
|
+
setExpression(name) {
|
|
105
|
+
return this.#runtime?.setExpression(name) ?? Promise.resolve(false);
|
|
106
|
+
}
|
|
107
|
+
/** Stage-normalized look-at in [-1, 1] client-mapped coords (x right, y up). */
|
|
108
|
+
lookAt(x, y) {
|
|
109
|
+
const stageX = (Number(x) + 1) / 2;
|
|
110
|
+
const stageY = (1 - Number(y)) / 2;
|
|
111
|
+
this.#runtime?.actor.lookAt(stageX, stageY);
|
|
112
|
+
}
|
|
113
|
+
pause() {
|
|
114
|
+
this.#runtime?.stage.pause();
|
|
115
|
+
}
|
|
116
|
+
resume() {
|
|
117
|
+
this.#runtime?.stage.resume();
|
|
118
|
+
if (this.#autoplay) {
|
|
119
|
+
try {
|
|
120
|
+
this.#runtime?.stage.start();
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
73
124
|
}
|
|
74
125
|
connectedCallback() {
|
|
75
126
|
this.#connected = true;
|
|
@@ -109,6 +160,16 @@ var Live2dElement = class extends HTMLElement {
|
|
|
109
160
|
if (this.#autoplay) this.#runtime.stage.start();
|
|
110
161
|
else this.#runtime.stage.pause();
|
|
111
162
|
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (name === "interactive") {
|
|
166
|
+
this.#interactive = parseBoolAttr(this, "interactive", false);
|
|
167
|
+
this.#syncPointer();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (name === "tracking") {
|
|
171
|
+
this.#tracking = value === "pointer" ? "pointer" : "none";
|
|
172
|
+
this.#syncPointer();
|
|
112
173
|
}
|
|
113
174
|
}
|
|
114
175
|
#readAttributes() {
|
|
@@ -131,6 +192,8 @@ var Live2dElement = class extends HTMLElement {
|
|
|
131
192
|
);
|
|
132
193
|
}
|
|
133
194
|
this.#autoplay = parseBoolAttr(this, "autoplay", true);
|
|
195
|
+
this.#interactive = parseBoolAttr(this, "interactive", false);
|
|
196
|
+
this.#tracking = this.getAttribute("tracking") === "pointer" ? "pointer" : "none";
|
|
134
197
|
}
|
|
135
198
|
#ensureCanvas() {
|
|
136
199
|
if (this.#canvas?.isConnected) return this.#canvas;
|
|
@@ -153,7 +216,62 @@ var Live2dElement = class extends HTMLElement {
|
|
|
153
216
|
this.style.display = this.style.display || "inline-block";
|
|
154
217
|
this.#runtime?.stage.resize?.(this.#width, this.#height);
|
|
155
218
|
}
|
|
219
|
+
#clearUnsubs() {
|
|
220
|
+
for (const off of this.#unsubs) {
|
|
221
|
+
try {
|
|
222
|
+
off();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
this.#unsubs = [];
|
|
227
|
+
}
|
|
228
|
+
#detachPointer() {
|
|
229
|
+
if (this.#canvas && this.#boundPointer) {
|
|
230
|
+
this.#canvas.removeEventListener("pointerdown", this.#boundPointer);
|
|
231
|
+
this.#canvas.removeEventListener("pointermove", this.#boundPointer);
|
|
232
|
+
}
|
|
233
|
+
this.#boundPointer = null;
|
|
234
|
+
}
|
|
235
|
+
#syncPointer() {
|
|
236
|
+
this.#detachPointer();
|
|
237
|
+
const canvas = this.#canvas;
|
|
238
|
+
const runtime = this.#runtime;
|
|
239
|
+
if (!canvas || !runtime) return;
|
|
240
|
+
if (!this.#interactive && this.#tracking !== "pointer") return;
|
|
241
|
+
this.#boundPointer = (event) => {
|
|
242
|
+
const rect = canvas.getBoundingClientRect();
|
|
243
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
244
|
+
const nx = (event.clientX - rect.left) / rect.width * 2 - 1;
|
|
245
|
+
const ny = 1 - (event.clientY - rect.top) / rect.height * 2;
|
|
246
|
+
if (this.#tracking === "pointer" || event.type === "pointermove") {
|
|
247
|
+
this.lookAt(nx, ny);
|
|
248
|
+
}
|
|
249
|
+
if (this.#interactive && event.type === "pointerdown") {
|
|
250
|
+
const area = runtime.hitTest(nx, ny);
|
|
251
|
+
this.dispatchEvent(
|
|
252
|
+
new CustomEvent("live2d-hit", {
|
|
253
|
+
bubbles: true,
|
|
254
|
+
composed: true,
|
|
255
|
+
detail: {
|
|
256
|
+
area,
|
|
257
|
+
modelX: nx,
|
|
258
|
+
modelY: ny,
|
|
259
|
+
originalEvent: event
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
if (this.#interactive) {
|
|
266
|
+
canvas.addEventListener("pointerdown", this.#boundPointer);
|
|
267
|
+
}
|
|
268
|
+
if (this.#tracking === "pointer") {
|
|
269
|
+
canvas.addEventListener("pointermove", this.#boundPointer);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
156
272
|
#destroyRuntime() {
|
|
273
|
+
this.#clearUnsubs();
|
|
274
|
+
this.#detachPointer();
|
|
157
275
|
try {
|
|
158
276
|
this.#runtime?.stage.stop?.();
|
|
159
277
|
} catch {
|
|
@@ -163,17 +281,26 @@ var Live2dElement = class extends HTMLElement {
|
|
|
163
281
|
this.removeAttribute("data-phase");
|
|
164
282
|
this.removeAttribute("aria-busy");
|
|
165
283
|
}
|
|
166
|
-
async #boot() {
|
|
284
|
+
async #boot(signal) {
|
|
167
285
|
if (!this.#connected) return;
|
|
168
286
|
const model = this.#model.trim();
|
|
169
287
|
if (!model) return;
|
|
170
288
|
const gen = ++this.#mountGen;
|
|
171
289
|
this.#destroyRuntime();
|
|
290
|
+
if (signal?.aborted) {
|
|
291
|
+
this.#emitError(signal.reason ?? new Error("aborted"));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const onAbort = () => {
|
|
295
|
+
this.#mountGen += 1;
|
|
296
|
+
this.#destroyRuntime();
|
|
297
|
+
};
|
|
298
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
172
299
|
const canvas = this.#ensureCanvas();
|
|
173
300
|
this.setAttribute("data-phase", "loading");
|
|
174
301
|
this.setAttribute("aria-busy", "true");
|
|
175
302
|
try {
|
|
176
|
-
const runtime =
|
|
303
|
+
const runtime = createLive2d({
|
|
177
304
|
prefer: preferFromRenderer(this.#renderer),
|
|
178
305
|
updateMode: "auto"
|
|
179
306
|
});
|
|
@@ -181,38 +308,70 @@ var Live2dElement = class extends HTMLElement {
|
|
|
181
308
|
runtime.destroy();
|
|
182
309
|
return;
|
|
183
310
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
311
|
+
this.#unsubs.push(
|
|
312
|
+
runtime.events.on("ready", () => {
|
|
313
|
+
if (gen !== this.#mountGen) return;
|
|
314
|
+
this.setAttribute("data-phase", "live");
|
|
315
|
+
this.setAttribute("aria-busy", "false");
|
|
316
|
+
if (this.#autoplay) {
|
|
317
|
+
try {
|
|
318
|
+
runtime.stage.start();
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
192
321
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
322
|
+
this.#syncPointer();
|
|
323
|
+
this.dispatchEvent(
|
|
324
|
+
new CustomEvent("live2d-ready", {
|
|
325
|
+
bubbles: true,
|
|
326
|
+
composed: true,
|
|
327
|
+
detail: { model }
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
this.#unsubs.push(
|
|
333
|
+
runtime.events.on("error", (payload) => {
|
|
334
|
+
if (gen !== this.#mountGen) return;
|
|
335
|
+
this.#emitError(payload?.error ?? "load error");
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
this.#unsubs.push(
|
|
339
|
+
runtime.events.on("motion:start", (payload) => {
|
|
340
|
+
if (gen !== this.#mountGen) return;
|
|
341
|
+
this.dispatchEvent(
|
|
342
|
+
new CustomEvent("live2d-motion-start", {
|
|
343
|
+
bubbles: true,
|
|
344
|
+
composed: true,
|
|
345
|
+
detail: payload
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
this.#unsubs.push(
|
|
351
|
+
runtime.events.on("motion:finish", (payload) => {
|
|
352
|
+
if (gen !== this.#mountGen) return;
|
|
353
|
+
this.dispatchEvent(
|
|
354
|
+
new CustomEvent("live2d-motion-finish", {
|
|
355
|
+
bubbles: true,
|
|
356
|
+
composed: true,
|
|
357
|
+
detail: payload
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
})
|
|
361
|
+
);
|
|
206
362
|
runtime.mount(canvas);
|
|
207
|
-
await runtime.loadModel(model);
|
|
363
|
+
await runtime.loadModel(model, void 0, { signal });
|
|
208
364
|
if (gen !== this.#mountGen) {
|
|
209
365
|
runtime.destroy();
|
|
210
366
|
return;
|
|
211
367
|
}
|
|
212
368
|
this.#runtime = runtime;
|
|
369
|
+
this.#syncPointer();
|
|
213
370
|
} catch (err) {
|
|
214
371
|
if (gen !== this.#mountGen) return;
|
|
215
372
|
this.#emitError(err);
|
|
373
|
+
} finally {
|
|
374
|
+
signal?.removeEventListener("abort", onAbort);
|
|
216
375
|
}
|
|
217
376
|
}
|
|
218
377
|
#emitError(error) {
|
|
@@ -231,23 +390,137 @@ var Live2dElement = class extends HTMLElement {
|
|
|
231
390
|
}
|
|
232
391
|
};
|
|
233
392
|
|
|
393
|
+
// src/live2d-widget-element.ts
|
|
394
|
+
var LIVE2D_WIDGET_ELEMENT_TAG = "live-2d-widget";
|
|
395
|
+
var OBSERVED2 = ["model", "renderer", "width", "height", "autoplay"];
|
|
396
|
+
var Live2dWidgetElement = class extends HTMLElement {
|
|
397
|
+
static get observedAttributes() {
|
|
398
|
+
return [...OBSERVED2];
|
|
399
|
+
}
|
|
400
|
+
#host = null;
|
|
401
|
+
#actor = null;
|
|
402
|
+
get model() {
|
|
403
|
+
return this.getAttribute("model") ?? "";
|
|
404
|
+
}
|
|
405
|
+
set model(value) {
|
|
406
|
+
const next = String(value ?? "");
|
|
407
|
+
if (next) this.setAttribute("model", next);
|
|
408
|
+
else this.removeAttribute("model");
|
|
409
|
+
}
|
|
410
|
+
get renderer() {
|
|
411
|
+
return this.getAttribute("renderer") ?? "auto";
|
|
412
|
+
}
|
|
413
|
+
set renderer(value) {
|
|
414
|
+
this.setAttribute("renderer", value || "auto");
|
|
415
|
+
}
|
|
416
|
+
get width() {
|
|
417
|
+
return Math.max(1, Number(this.getAttribute("width")) || 320);
|
|
418
|
+
}
|
|
419
|
+
set width(value) {
|
|
420
|
+
this.setAttribute("width", String(Math.max(1, Number(value) || 320)));
|
|
421
|
+
}
|
|
422
|
+
get height() {
|
|
423
|
+
return Math.max(1, Number(this.getAttribute("height")) || 320);
|
|
424
|
+
}
|
|
425
|
+
set height(value) {
|
|
426
|
+
this.setAttribute("height", String(Math.max(1, Number(value) || 320)));
|
|
427
|
+
}
|
|
428
|
+
get autoplay() {
|
|
429
|
+
if (!this.hasAttribute("autoplay")) return true;
|
|
430
|
+
const v = this.getAttribute("autoplay");
|
|
431
|
+
if (v === null || v === "" || v === "autoplay") return true;
|
|
432
|
+
return v !== "false" && v !== "0";
|
|
433
|
+
}
|
|
434
|
+
set autoplay(value) {
|
|
435
|
+
if (value) this.setAttribute("autoplay", "");
|
|
436
|
+
else this.removeAttribute("autoplay");
|
|
437
|
+
}
|
|
438
|
+
/** Inner `<live-2d>` when present. */
|
|
439
|
+
get actor() {
|
|
440
|
+
return this.#actor;
|
|
441
|
+
}
|
|
442
|
+
connectedCallback() {
|
|
443
|
+
defineLive2dElement();
|
|
444
|
+
this.#ensureDom();
|
|
445
|
+
this.#syncActorFromAttributes();
|
|
446
|
+
}
|
|
447
|
+
attributeChangedCallback() {
|
|
448
|
+
if (!this.isConnected) return;
|
|
449
|
+
this.#syncActorFromAttributes();
|
|
450
|
+
}
|
|
451
|
+
#ensureDom() {
|
|
452
|
+
if (this.#host?.isConnected) return;
|
|
453
|
+
let actor = this.querySelector(
|
|
454
|
+
LIVE2D_ELEMENT_TAG
|
|
455
|
+
);
|
|
456
|
+
if (!actor) {
|
|
457
|
+
actor = document.createElement(LIVE2D_ELEMENT_TAG);
|
|
458
|
+
} else {
|
|
459
|
+
actor.remove();
|
|
460
|
+
}
|
|
461
|
+
this.replaceChildren();
|
|
462
|
+
const host = document.createElement("div");
|
|
463
|
+
host.setAttribute("part", "chrome");
|
|
464
|
+
host.className = "live2d-widget-chrome";
|
|
465
|
+
host.appendChild(actor);
|
|
466
|
+
this.appendChild(host);
|
|
467
|
+
this.#host = host;
|
|
468
|
+
this.#actor = actor;
|
|
469
|
+
}
|
|
470
|
+
#syncActorFromAttributes() {
|
|
471
|
+
this.#ensureDom();
|
|
472
|
+
const actor = this.#actor;
|
|
473
|
+
if (!actor) return;
|
|
474
|
+
if (this.hasAttribute("model")) {
|
|
475
|
+
actor.setAttribute("model", this.model);
|
|
476
|
+
}
|
|
477
|
+
if (this.hasAttribute("renderer")) {
|
|
478
|
+
actor.setAttribute("renderer", this.renderer);
|
|
479
|
+
}
|
|
480
|
+
actor.setAttribute("width", String(this.width));
|
|
481
|
+
actor.setAttribute("height", String(this.height));
|
|
482
|
+
if (this.autoplay) actor.setAttribute("autoplay", "");
|
|
483
|
+
else actor.removeAttribute("autoplay");
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
234
487
|
// src/define.ts
|
|
235
|
-
var
|
|
488
|
+
var elementDefined = false;
|
|
489
|
+
var widgetDefined = false;
|
|
236
490
|
function defineLive2dElement() {
|
|
237
|
-
if (
|
|
491
|
+
if (elementDefined) return;
|
|
238
492
|
if (typeof customElements === "undefined") return;
|
|
239
493
|
if (customElements.get(LIVE2D_ELEMENT_TAG)) {
|
|
240
|
-
|
|
494
|
+
elementDefined = true;
|
|
241
495
|
return;
|
|
242
496
|
}
|
|
243
497
|
customElements.define(LIVE2D_ELEMENT_TAG, Live2dElement);
|
|
244
|
-
|
|
498
|
+
elementDefined = true;
|
|
499
|
+
}
|
|
500
|
+
function defineLive2dWidgetElement() {
|
|
501
|
+
defineLive2dElement();
|
|
502
|
+
if (widgetDefined) return;
|
|
503
|
+
if (typeof customElements === "undefined") return;
|
|
504
|
+
if (customElements.get(LIVE2D_WIDGET_ELEMENT_TAG)) {
|
|
505
|
+
widgetDefined = true;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
customElements.define(LIVE2D_WIDGET_ELEMENT_TAG, Live2dWidgetElement);
|
|
509
|
+
widgetDefined = true;
|
|
510
|
+
}
|
|
511
|
+
function defineLive2dElements() {
|
|
512
|
+
defineLive2dElement();
|
|
513
|
+
defineLive2dWidgetElement();
|
|
245
514
|
}
|
|
246
515
|
|
|
247
516
|
// src/index.ts
|
|
248
|
-
|
|
517
|
+
defineLive2dElements();
|
|
249
518
|
export {
|
|
250
519
|
LIVE2D_ELEMENT_TAG,
|
|
520
|
+
LIVE2D_WIDGET_ELEMENT_TAG,
|
|
251
521
|
Live2dElement,
|
|
252
|
-
|
|
522
|
+
Live2dWidgetElement,
|
|
523
|
+
defineLive2dElement,
|
|
524
|
+
defineLive2dElements,
|
|
525
|
+
defineLive2dWidgetElement
|
|
253
526
|
};
|
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.25",
|
|
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.25"
|
|
49
49
|
},
|
|
50
50
|
"sideEffects": [
|
|
51
51
|
"./src/index.ts",
|
package/src/define.ts
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { LIVE2D_ELEMENT_TAG, Live2dElement } from "./live2d-element.js";
|
|
2
|
+
import {
|
|
3
|
+
LIVE2D_WIDGET_ELEMENT_TAG,
|
|
4
|
+
Live2dWidgetElement,
|
|
5
|
+
} from "./live2d-widget-element.js";
|
|
2
6
|
|
|
3
|
-
let
|
|
7
|
+
let elementDefined = false;
|
|
8
|
+
let widgetDefined = false;
|
|
4
9
|
|
|
5
10
|
/** Idempotent `customElements.define('live-2d', …)`. */
|
|
6
11
|
export function defineLive2dElement(): void {
|
|
7
|
-
if (
|
|
12
|
+
if (elementDefined) return;
|
|
8
13
|
if (typeof customElements === "undefined") return;
|
|
9
14
|
if (customElements.get(LIVE2D_ELEMENT_TAG)) {
|
|
10
|
-
|
|
15
|
+
elementDefined = true;
|
|
11
16
|
return;
|
|
12
17
|
}
|
|
13
18
|
customElements.define(LIVE2D_ELEMENT_TAG, Live2dElement);
|
|
14
|
-
|
|
19
|
+
elementDefined = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Idempotent `customElements.define('live-2d-widget', …)`. */
|
|
23
|
+
export function defineLive2dWidgetElement(): void {
|
|
24
|
+
defineLive2dElement();
|
|
25
|
+
if (widgetDefined) return;
|
|
26
|
+
if (typeof customElements === "undefined") return;
|
|
27
|
+
if (customElements.get(LIVE2D_WIDGET_ELEMENT_TAG)) {
|
|
28
|
+
widgetDefined = true;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
customElements.define(LIVE2D_WIDGET_ELEMENT_TAG, Live2dWidgetElement);
|
|
32
|
+
widgetDefined = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Register both official CE tags. */
|
|
36
|
+
export function defineLive2dElements(): void {
|
|
37
|
+
defineLive2dElement();
|
|
38
|
+
defineLive2dWidgetElement();
|
|
15
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@doki-land/live2d-element` — official `<live-2d>`
|
|
2
|
+
* `@doki-land/live2d-element` — official `<live-2d>` / `<live-2d-widget>` CEs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* when that CLI target ships in published `@vmz/vmz`; until then this
|
|
8
|
-
* package is the cross-ecosystem ABI skeleton.
|
|
4
|
+
* Pure TypeScript CE wrapping `@doki-land/live2d` (`createLive2d` + Stage-owned
|
|
5
|
+
* RAF). Zero VMZ runtime dependency. Design `01` still targets eventual
|
|
6
|
+
* `vmz build --target custom-element` when that CLI target ships.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
defineLive2dElement,
|
|
11
|
+
defineLive2dElements,
|
|
12
|
+
defineLive2dWidgetElement,
|
|
13
|
+
} from "./define.js";
|
|
12
14
|
export {
|
|
13
15
|
LIVE2D_ELEMENT_TAG,
|
|
14
16
|
Live2dElement,
|
|
15
17
|
type Live2dElementRenderer,
|
|
18
|
+
type Live2dElementTracking,
|
|
16
19
|
} from "./live2d-element.js";
|
|
20
|
+
export {
|
|
21
|
+
LIVE2D_WIDGET_ELEMENT_TAG,
|
|
22
|
+
Live2dWidgetElement,
|
|
23
|
+
} from "./live2d-widget-element.js";
|
|
17
24
|
|
|
18
|
-
import {
|
|
25
|
+
import { defineLive2dElements } from "./define.js";
|
|
19
26
|
|
|
20
27
|
/** Side-effect registration for `<script type="module">` consumers. */
|
|
21
|
-
|
|
28
|
+
defineLive2dElements();
|
package/src/live2d-element.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createLive2d,
|
|
3
|
+
type Live2dRuntime,
|
|
4
|
+
type PlayMotionOptions,
|
|
5
|
+
} from "@doki-land/live2d";
|
|
2
6
|
|
|
3
7
|
export const LIVE2D_ELEMENT_TAG = "live-2d" as const;
|
|
4
8
|
|
|
5
9
|
export type Live2dElementRenderer = "auto" | "webgpu" | "webgl2" | "canvas2d";
|
|
10
|
+
export type Live2dElementTracking = "pointer" | "none";
|
|
6
11
|
|
|
7
|
-
const OBSERVED = [
|
|
12
|
+
const OBSERVED = [
|
|
13
|
+
"model",
|
|
14
|
+
"renderer",
|
|
15
|
+
"width",
|
|
16
|
+
"height",
|
|
17
|
+
"autoplay",
|
|
18
|
+
"interactive",
|
|
19
|
+
"tracking",
|
|
20
|
+
] as const;
|
|
8
21
|
|
|
9
22
|
function preferFromRenderer(
|
|
10
23
|
renderer: Live2dElementRenderer,
|
|
@@ -28,14 +41,14 @@ function parseBoolAttr(
|
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
|
-
* Thin `<live-2d>` host: attribute →
|
|
44
|
+
* Thin `<live-2d>` host: attribute → createLive2d → Stage RAF → live2d-* events.
|
|
32
45
|
*/
|
|
33
46
|
export class Live2dElement extends HTMLElement {
|
|
34
47
|
static get observedAttributes(): string[] {
|
|
35
48
|
return [...OBSERVED];
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
#runtime:
|
|
51
|
+
#runtime: Live2dRuntime | null = null;
|
|
39
52
|
#canvas: HTMLCanvasElement | null = null;
|
|
40
53
|
#mountGen = 0;
|
|
41
54
|
#model = "";
|
|
@@ -43,7 +56,11 @@ export class Live2dElement extends HTMLElement {
|
|
|
43
56
|
#width = 320;
|
|
44
57
|
#height = 320;
|
|
45
58
|
#autoplay = true;
|
|
59
|
+
#interactive = false;
|
|
60
|
+
#tracking: Live2dElementTracking = "none";
|
|
46
61
|
#connected = false;
|
|
62
|
+
#boundPointer: ((event: PointerEvent) => void) | null = null;
|
|
63
|
+
#unsubs: Array<() => void> = [];
|
|
47
64
|
|
|
48
65
|
get model(): string {
|
|
49
66
|
return this.#model;
|
|
@@ -83,16 +100,71 @@ export class Live2dElement extends HTMLElement {
|
|
|
83
100
|
else this.removeAttribute("autoplay");
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
get
|
|
103
|
+
get interactive(): boolean {
|
|
104
|
+
return this.#interactive;
|
|
105
|
+
}
|
|
106
|
+
set interactive(value: boolean) {
|
|
107
|
+
if (value) this.setAttribute("interactive", "");
|
|
108
|
+
else this.removeAttribute("interactive");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get tracking(): Live2dElementTracking {
|
|
112
|
+
return this.#tracking;
|
|
113
|
+
}
|
|
114
|
+
set tracking(value: Live2dElementTracking) {
|
|
115
|
+
this.setAttribute("tracking", value === "pointer" ? "pointer" : "none");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get runtime(): Live2dRuntime | null {
|
|
87
119
|
return this.#runtime;
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
/** Imperative reload (also used when `model` attribute changes). */
|
|
91
|
-
async loadModel(
|
|
123
|
+
async loadModel(
|
|
124
|
+
source?: string,
|
|
125
|
+
opts?: { signal?: AbortSignal },
|
|
126
|
+
): Promise<void> {
|
|
92
127
|
if (source !== undefined) {
|
|
93
128
|
this.model = String(source);
|
|
94
129
|
}
|
|
95
|
-
await this.#boot();
|
|
130
|
+
await this.#boot(opts?.signal);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
playMotion(
|
|
134
|
+
group: string,
|
|
135
|
+
index?: number,
|
|
136
|
+
options?: PlayMotionOptions,
|
|
137
|
+
): Promise<boolean> {
|
|
138
|
+
return (
|
|
139
|
+
this.#runtime?.playMotion(group, index, options) ??
|
|
140
|
+
Promise.resolve(false)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setExpression(name: string | null): Promise<boolean> {
|
|
145
|
+
return this.#runtime?.setExpression(name) ?? Promise.resolve(false);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Stage-normalized look-at in [-1, 1] client-mapped coords (x right, y up). */
|
|
149
|
+
lookAt(x: number, y: number): void {
|
|
150
|
+
const stageX = (Number(x) + 1) / 2;
|
|
151
|
+
const stageY = (1 - Number(y)) / 2;
|
|
152
|
+
this.#runtime?.actor.lookAt(stageX, stageY);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pause(): void {
|
|
156
|
+
this.#runtime?.stage.pause();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resume(): void {
|
|
160
|
+
this.#runtime?.stage.resume();
|
|
161
|
+
if (this.#autoplay) {
|
|
162
|
+
try {
|
|
163
|
+
this.#runtime?.stage.start();
|
|
164
|
+
} catch {
|
|
165
|
+
/* already running */
|
|
166
|
+
}
|
|
167
|
+
}
|
|
96
168
|
}
|
|
97
169
|
|
|
98
170
|
connectedCallback(): void {
|
|
@@ -139,6 +211,16 @@ export class Live2dElement extends HTMLElement {
|
|
|
139
211
|
if (this.#autoplay) this.#runtime.stage.start();
|
|
140
212
|
else this.#runtime.stage.pause();
|
|
141
213
|
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (name === "interactive") {
|
|
217
|
+
this.#interactive = parseBoolAttr(this, "interactive", false);
|
|
218
|
+
this.#syncPointer();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (name === "tracking") {
|
|
222
|
+
this.#tracking = value === "pointer" ? "pointer" : "none";
|
|
223
|
+
this.#syncPointer();
|
|
142
224
|
}
|
|
143
225
|
}
|
|
144
226
|
|
|
@@ -163,6 +245,9 @@ export class Live2dElement extends HTMLElement {
|
|
|
163
245
|
);
|
|
164
246
|
}
|
|
165
247
|
this.#autoplay = parseBoolAttr(this, "autoplay", true);
|
|
248
|
+
this.#interactive = parseBoolAttr(this, "interactive", false);
|
|
249
|
+
this.#tracking =
|
|
250
|
+
this.getAttribute("tracking") === "pointer" ? "pointer" : "none";
|
|
166
251
|
}
|
|
167
252
|
|
|
168
253
|
#ensureCanvas(): HTMLCanvasElement {
|
|
@@ -191,7 +276,67 @@ export class Live2dElement extends HTMLElement {
|
|
|
191
276
|
this.#runtime?.stage.resize?.(this.#width, this.#height);
|
|
192
277
|
}
|
|
193
278
|
|
|
279
|
+
#clearUnsubs(): void {
|
|
280
|
+
for (const off of this.#unsubs) {
|
|
281
|
+
try {
|
|
282
|
+
off();
|
|
283
|
+
} catch {
|
|
284
|
+
/* ignore */
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
this.#unsubs = [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#detachPointer(): void {
|
|
291
|
+
if (this.#canvas && this.#boundPointer) {
|
|
292
|
+
this.#canvas.removeEventListener("pointerdown", this.#boundPointer);
|
|
293
|
+
this.#canvas.removeEventListener("pointermove", this.#boundPointer);
|
|
294
|
+
}
|
|
295
|
+
this.#boundPointer = null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#syncPointer(): void {
|
|
299
|
+
this.#detachPointer();
|
|
300
|
+
const canvas = this.#canvas;
|
|
301
|
+
const runtime = this.#runtime;
|
|
302
|
+
if (!canvas || !runtime) return;
|
|
303
|
+
if (!this.#interactive && this.#tracking !== "pointer") return;
|
|
304
|
+
|
|
305
|
+
this.#boundPointer = (event: PointerEvent) => {
|
|
306
|
+
const rect = canvas.getBoundingClientRect();
|
|
307
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
308
|
+
const nx = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
309
|
+
const ny = 1 - ((event.clientY - rect.top) / rect.height) * 2;
|
|
310
|
+
if (this.#tracking === "pointer" || event.type === "pointermove") {
|
|
311
|
+
this.lookAt(nx, ny);
|
|
312
|
+
}
|
|
313
|
+
if (this.#interactive && event.type === "pointerdown") {
|
|
314
|
+
const area = runtime.hitTest(nx, ny);
|
|
315
|
+
this.dispatchEvent(
|
|
316
|
+
new CustomEvent("live2d-hit", {
|
|
317
|
+
bubbles: true,
|
|
318
|
+
composed: true,
|
|
319
|
+
detail: {
|
|
320
|
+
area,
|
|
321
|
+
modelX: nx,
|
|
322
|
+
modelY: ny,
|
|
323
|
+
originalEvent: event,
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
if (this.#interactive) {
|
|
330
|
+
canvas.addEventListener("pointerdown", this.#boundPointer);
|
|
331
|
+
}
|
|
332
|
+
if (this.#tracking === "pointer") {
|
|
333
|
+
canvas.addEventListener("pointermove", this.#boundPointer);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
194
337
|
#destroyRuntime(): void {
|
|
338
|
+
this.#clearUnsubs();
|
|
339
|
+
this.#detachPointer();
|
|
195
340
|
try {
|
|
196
341
|
this.#runtime?.stage.stop?.();
|
|
197
342
|
} catch {
|
|
@@ -203,7 +348,7 @@ export class Live2dElement extends HTMLElement {
|
|
|
203
348
|
this.removeAttribute("aria-busy");
|
|
204
349
|
}
|
|
205
350
|
|
|
206
|
-
async #boot(): Promise<void> {
|
|
351
|
+
async #boot(signal?: AbortSignal): Promise<void> {
|
|
207
352
|
if (!this.#connected) return;
|
|
208
353
|
const model = this.#model.trim();
|
|
209
354
|
if (!model) return;
|
|
@@ -211,12 +356,23 @@ export class Live2dElement extends HTMLElement {
|
|
|
211
356
|
const gen = ++this.#mountGen;
|
|
212
357
|
this.#destroyRuntime();
|
|
213
358
|
|
|
359
|
+
if (signal?.aborted) {
|
|
360
|
+
this.#emitError(signal.reason ?? new Error("aborted"));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const onAbort = () => {
|
|
365
|
+
this.#mountGen += 1;
|
|
366
|
+
this.#destroyRuntime();
|
|
367
|
+
};
|
|
368
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
369
|
+
|
|
214
370
|
const canvas = this.#ensureCanvas();
|
|
215
371
|
this.setAttribute("data-phase", "loading");
|
|
216
372
|
this.setAttribute("aria-busy", "true");
|
|
217
373
|
|
|
218
374
|
try {
|
|
219
|
-
const runtime =
|
|
375
|
+
const runtime = createLive2d({
|
|
220
376
|
prefer: preferFromRenderer(this.#renderer),
|
|
221
377
|
updateMode: "auto",
|
|
222
378
|
});
|
|
@@ -225,41 +381,75 @@ export class Live2dElement extends HTMLElement {
|
|
|
225
381
|
return;
|
|
226
382
|
}
|
|
227
383
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
384
|
+
this.#unsubs.push(
|
|
385
|
+
runtime.events.on("ready", () => {
|
|
386
|
+
if (gen !== this.#mountGen) return;
|
|
387
|
+
this.setAttribute("data-phase", "live");
|
|
388
|
+
this.setAttribute("aria-busy", "false");
|
|
389
|
+
if (this.#autoplay) {
|
|
390
|
+
try {
|
|
391
|
+
runtime.stage.start();
|
|
392
|
+
} catch {
|
|
393
|
+
/* already running */
|
|
394
|
+
}
|
|
237
395
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
396
|
+
this.#syncPointer();
|
|
397
|
+
this.dispatchEvent(
|
|
398
|
+
new CustomEvent("live2d-ready", {
|
|
399
|
+
bubbles: true,
|
|
400
|
+
composed: true,
|
|
401
|
+
detail: { model },
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
247
406
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
407
|
+
this.#unsubs.push(
|
|
408
|
+
runtime.events.on("error", (payload) => {
|
|
409
|
+
if (gen !== this.#mountGen) return;
|
|
410
|
+
this.#emitError(payload?.error ?? "load error");
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
this.#unsubs.push(
|
|
415
|
+
runtime.events.on("motion:start", (payload) => {
|
|
416
|
+
if (gen !== this.#mountGen) return;
|
|
417
|
+
this.dispatchEvent(
|
|
418
|
+
new CustomEvent("live2d-motion-start", {
|
|
419
|
+
bubbles: true,
|
|
420
|
+
composed: true,
|
|
421
|
+
detail: payload,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
this.#unsubs.push(
|
|
428
|
+
runtime.events.on("motion:finish", (payload) => {
|
|
429
|
+
if (gen !== this.#mountGen) return;
|
|
430
|
+
this.dispatchEvent(
|
|
431
|
+
new CustomEvent("live2d-motion-finish", {
|
|
432
|
+
bubbles: true,
|
|
433
|
+
composed: true,
|
|
434
|
+
detail: payload,
|
|
435
|
+
}),
|
|
436
|
+
);
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
252
439
|
|
|
253
440
|
runtime.mount(canvas);
|
|
254
|
-
await runtime.loadModel(model);
|
|
441
|
+
await runtime.loadModel(model, undefined, { signal });
|
|
255
442
|
if (gen !== this.#mountGen) {
|
|
256
443
|
runtime.destroy();
|
|
257
444
|
return;
|
|
258
445
|
}
|
|
259
446
|
this.#runtime = runtime;
|
|
447
|
+
this.#syncPointer();
|
|
260
448
|
} catch (err) {
|
|
261
449
|
if (gen !== this.#mountGen) return;
|
|
262
450
|
this.#emitError(err);
|
|
451
|
+
} finally {
|
|
452
|
+
signal?.removeEventListener("abort", onAbort);
|
|
263
453
|
}
|
|
264
454
|
}
|
|
265
455
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { defineLive2dElement } from "./define.js";
|
|
2
|
+
import { LIVE2D_ELEMENT_TAG, type Live2dElement } from "./live2d-element.js";
|
|
3
|
+
|
|
4
|
+
export const LIVE2D_WIDGET_ELEMENT_TAG = "live-2d-widget" as const;
|
|
5
|
+
|
|
6
|
+
const OBSERVED = ["model", "renderer", "width", "height", "autoplay"] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thin `<live-2d-widget>` product-shell CE (v0.0.24 POC).
|
|
10
|
+
*
|
|
11
|
+
* Owns chrome layout only; delegates model/RAF/hit to an inner `<live-2d>`.
|
|
12
|
+
* Does not create a second Stage clock.
|
|
13
|
+
*/
|
|
14
|
+
export class Live2dWidgetElement extends HTMLElement {
|
|
15
|
+
static get observedAttributes(): string[] {
|
|
16
|
+
return [...OBSERVED];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#host: HTMLElement | null = null;
|
|
20
|
+
#actor: Live2dElement | null = null;
|
|
21
|
+
|
|
22
|
+
get model(): string {
|
|
23
|
+
return this.getAttribute("model") ?? "";
|
|
24
|
+
}
|
|
25
|
+
set model(value: string) {
|
|
26
|
+
const next = String(value ?? "");
|
|
27
|
+
if (next) this.setAttribute("model", next);
|
|
28
|
+
else this.removeAttribute("model");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get renderer(): string {
|
|
32
|
+
return this.getAttribute("renderer") ?? "auto";
|
|
33
|
+
}
|
|
34
|
+
set renderer(value: string) {
|
|
35
|
+
this.setAttribute("renderer", value || "auto");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get width(): number {
|
|
39
|
+
return Math.max(1, Number(this.getAttribute("width")) || 320);
|
|
40
|
+
}
|
|
41
|
+
set width(value: number) {
|
|
42
|
+
this.setAttribute("width", String(Math.max(1, Number(value) || 320)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get height(): number {
|
|
46
|
+
return Math.max(1, Number(this.getAttribute("height")) || 320);
|
|
47
|
+
}
|
|
48
|
+
set height(value: number) {
|
|
49
|
+
this.setAttribute("height", String(Math.max(1, Number(value) || 320)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get autoplay(): boolean {
|
|
53
|
+
if (!this.hasAttribute("autoplay")) return true;
|
|
54
|
+
const v = this.getAttribute("autoplay");
|
|
55
|
+
if (v === null || v === "" || v === "autoplay") return true;
|
|
56
|
+
return v !== "false" && v !== "0";
|
|
57
|
+
}
|
|
58
|
+
set autoplay(value: boolean) {
|
|
59
|
+
if (value) this.setAttribute("autoplay", "");
|
|
60
|
+
else this.removeAttribute("autoplay");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Inner `<live-2d>` when present. */
|
|
64
|
+
get actor(): Live2dElement | null {
|
|
65
|
+
return this.#actor;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
connectedCallback(): void {
|
|
69
|
+
defineLive2dElement();
|
|
70
|
+
this.#ensureDom();
|
|
71
|
+
this.#syncActorFromAttributes();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
attributeChangedCallback(): void {
|
|
75
|
+
if (!this.isConnected) return;
|
|
76
|
+
this.#syncActorFromAttributes();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#ensureDom(): void {
|
|
80
|
+
if (this.#host?.isConnected) return;
|
|
81
|
+
|
|
82
|
+
let actor = this.querySelector(
|
|
83
|
+
LIVE2D_ELEMENT_TAG,
|
|
84
|
+
) as Live2dElement | null;
|
|
85
|
+
if (!actor) {
|
|
86
|
+
actor = document.createElement(LIVE2D_ELEMENT_TAG) as Live2dElement;
|
|
87
|
+
} else {
|
|
88
|
+
actor.remove();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.replaceChildren();
|
|
92
|
+
const host = document.createElement("div");
|
|
93
|
+
host.setAttribute("part", "chrome");
|
|
94
|
+
host.className = "live2d-widget-chrome";
|
|
95
|
+
host.appendChild(actor);
|
|
96
|
+
this.appendChild(host);
|
|
97
|
+
this.#host = host;
|
|
98
|
+
this.#actor = actor;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#syncActorFromAttributes(): void {
|
|
102
|
+
this.#ensureDom();
|
|
103
|
+
const actor = this.#actor;
|
|
104
|
+
if (!actor) return;
|
|
105
|
+
|
|
106
|
+
if (this.hasAttribute("model")) {
|
|
107
|
+
actor.setAttribute("model", this.model);
|
|
108
|
+
}
|
|
109
|
+
if (this.hasAttribute("renderer")) {
|
|
110
|
+
actor.setAttribute("renderer", this.renderer);
|
|
111
|
+
}
|
|
112
|
+
actor.setAttribute("width", String(this.width));
|
|
113
|
+
actor.setAttribute("height", String(this.height));
|
|
114
|
+
if (this.autoplay) actor.setAttribute("autoplay", "");
|
|
115
|
+
else actor.removeAttribute("autoplay");
|
|
116
|
+
}
|
|
117
|
+
}
|