@doki-land/live2d-widget 0.0.22 → 0.0.23
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/dist/index.d.ts +43 -17
- package/dist/index.js +100 -68
- package/package.json +4 -4
- package/src/index.ts +6 -0
- package/src/shell/widget.ts +144 -92
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Live2dStage, Live2dActor, RendererKind, Live2DRuntime } from '@doki-land/live2d';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Optional page chrome for `@doki-land/live2d-widget`:
|
|
@@ -40,42 +40,68 @@ declare function mountChrome(options: {
|
|
|
40
40
|
getCanvas: () => HTMLCanvasElement | null;
|
|
41
41
|
}): ChromeSession | null;
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/** Composed widget over an existing Stage + Actor (no second RAF). */
|
|
44
|
+
interface ComposedWidgetOptions {
|
|
45
|
+
target: string | HTMLElement;
|
|
46
|
+
stage: Live2dStage;
|
|
47
|
+
actor: Live2dActor;
|
|
48
|
+
width?: number;
|
|
49
|
+
height?: number;
|
|
50
|
+
/** Drive PARAM_ANGLE_X with a sine while the stage loop runs. Default true. */
|
|
51
|
+
autoSway?: boolean;
|
|
52
|
+
/** Start Stage-owned RAF after mount. Default true. */
|
|
53
|
+
autoplay?: boolean;
|
|
54
|
+
chrome?: boolean | WidgetChromeOptions;
|
|
55
|
+
onHit?: (payload: {
|
|
56
|
+
area: string;
|
|
57
|
+
x: number;
|
|
58
|
+
y: number;
|
|
59
|
+
}) => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Legacy bootstrap that still creates its own Stage + Actor.
|
|
63
|
+
* Prefer composing `stage` + `actor` yourself and calling `createLive2dWidget`.
|
|
64
|
+
*/
|
|
65
|
+
interface LegacyWidgetOptions {
|
|
45
66
|
target: string | HTMLElement;
|
|
46
|
-
/** Initial model URL (model.json / model3.json / npm:…). */
|
|
47
67
|
model?: string;
|
|
48
68
|
width?: number;
|
|
49
69
|
height?: number;
|
|
50
|
-
|
|
70
|
+
prefer?: RendererKind[];
|
|
51
71
|
autoSway?: boolean;
|
|
52
|
-
/** Start the RAF update loop after mount. Default true. */
|
|
53
72
|
autoplay?: boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Optional tips bubble + toolbar (hitokoto / photo / quit).
|
|
56
|
-
* Pass `true` for defaults, or a config object.
|
|
57
|
-
*/
|
|
58
73
|
chrome?: boolean | WidgetChromeOptions;
|
|
59
|
-
/** Fired when canvas hit-test finds a drawable. */
|
|
60
74
|
onHit?: (payload: {
|
|
61
75
|
area: string;
|
|
62
76
|
x: number;
|
|
63
77
|
y: number;
|
|
64
78
|
}) => void;
|
|
65
79
|
}
|
|
80
|
+
type WidgetOptions = ComposedWidgetOptions | LegacyWidgetOptions;
|
|
66
81
|
/**
|
|
67
|
-
* Page widget shell over
|
|
68
|
-
*
|
|
82
|
+
* Page widget shell — product chrome over Stage + Actor.
|
|
83
|
+
* Does not own renderer selection or a private RAF loop.
|
|
69
84
|
*/
|
|
70
|
-
declare class
|
|
85
|
+
declare class Live2dWidget {
|
|
71
86
|
#private;
|
|
72
87
|
mount(options: WidgetOptions): Promise<void>;
|
|
73
88
|
destroy(): void;
|
|
89
|
+
get stage(): Live2dStage | null;
|
|
90
|
+
get actor(): Live2dActor | null;
|
|
91
|
+
/** Legacy accessor when mounted via `createLive2D`. */
|
|
74
92
|
getRuntime(): Live2DRuntime | null;
|
|
75
|
-
/** Show a tips bubble when chrome tips are enabled. */
|
|
76
93
|
showMessage(text: string | string[], timeoutMs?: number, priority?: number): void;
|
|
77
94
|
}
|
|
78
|
-
|
|
95
|
+
/** @deprecated Use `Live2dWidget` — alias kept for hexo / legacy imports. */
|
|
96
|
+
declare const Live2DWidget: typeof Live2dWidget;
|
|
97
|
+
declare function createLive2dWidget(options: ComposedWidgetOptions): Promise<Live2dWidget>;
|
|
98
|
+
declare function mountWidget(options: WidgetOptions): Promise<Live2dWidget>;
|
|
99
|
+
/** Convenience bootstrap: Stage + default Actor + widget chrome. */
|
|
100
|
+
declare function mountWidgetWithStage(options: LegacyWidgetOptions): Promise<{
|
|
101
|
+
widget: Live2dWidget;
|
|
102
|
+
stage: Live2dStage;
|
|
103
|
+
actor: Live2dActor;
|
|
104
|
+
}>;
|
|
79
105
|
|
|
80
106
|
/**
|
|
81
107
|
* `@doki-land/live2d-widget` — browser embed helper.
|
|
@@ -83,4 +109,4 @@ declare function mountWidget(options: WidgetOptions): Promise<Live2DWidget>;
|
|
|
83
109
|
|
|
84
110
|
declare const LIVE2D_WIDGET_VERSION: "0.0.0";
|
|
85
111
|
|
|
86
|
-
export { DEFAULT_WELCOME, LIVE2D_WIDGET_VERSION, Live2DWidget, type WidgetChromeOptions, type WidgetOptions, type WidgetToolId, createTipMessage, ensureChromeStyles, mountChrome, mountWidget };
|
|
112
|
+
export { type ComposedWidgetOptions, DEFAULT_WELCOME, LIVE2D_WIDGET_VERSION, type LegacyWidgetOptions, Live2DWidget, Live2dWidget, type WidgetChromeOptions, type WidgetOptions, type WidgetToolId, createLive2dWidget, createTipMessage, ensureChromeStyles, mountChrome, mountWidget, mountWidgetWithStage };
|
package/dist/index.js
CHANGED
|
@@ -222,16 +222,24 @@ function mountChrome(options) {
|
|
|
222
222
|
|
|
223
223
|
// src/shell/widget.ts
|
|
224
224
|
import {
|
|
225
|
+
allocateActorId,
|
|
225
226
|
createLive2D,
|
|
227
|
+
createLive2dStage,
|
|
228
|
+
createRenderer,
|
|
226
229
|
focusParameterUpdates
|
|
227
230
|
} from "@doki-land/live2d";
|
|
228
|
-
|
|
231
|
+
function isLegacyOptions(options) {
|
|
232
|
+
return !("stage" in options && "actor" in options);
|
|
233
|
+
}
|
|
234
|
+
var Live2dWidget = class {
|
|
229
235
|
#canvas = null;
|
|
236
|
+
#stage = null;
|
|
237
|
+
#actor = null;
|
|
230
238
|
#runtime = null;
|
|
231
239
|
#chrome = null;
|
|
232
|
-
#
|
|
233
|
-
#lastTs = 0;
|
|
240
|
+
#unsubFrame = null;
|
|
234
241
|
#autoSway = true;
|
|
242
|
+
#swayPhase = 0;
|
|
235
243
|
#onHit;
|
|
236
244
|
async mount(options) {
|
|
237
245
|
const host = typeof options.target === "string" ? document.querySelector(options.target) : options.target;
|
|
@@ -249,15 +257,29 @@ var Live2DWidget = class {
|
|
|
249
257
|
canvas.style.cssText = "display:block;width:100%;height:auto;pointer-events:auto;touch-action:none;background:transparent;cursor:grab;";
|
|
250
258
|
canvas.addEventListener("pointermove", this.#onPointerMove);
|
|
251
259
|
canvas.addEventListener("pointerdown", this.#onPointerDown);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
let stage;
|
|
261
|
+
let actor;
|
|
262
|
+
if (isLegacyOptions(options)) {
|
|
263
|
+
const prefer = normalizePrefer(options.prefer);
|
|
264
|
+
const runtime = createLive2D({
|
|
265
|
+
prefer,
|
|
266
|
+
updateMode: "auto"
|
|
267
|
+
});
|
|
268
|
+
await runtime.mount(canvas);
|
|
269
|
+
if (options.model) {
|
|
270
|
+
await runtime.loadModel(options.model);
|
|
271
|
+
}
|
|
272
|
+
this.#runtime = runtime;
|
|
273
|
+
stage = runtime.stage;
|
|
274
|
+
actor = runtime.actor;
|
|
275
|
+
} else {
|
|
276
|
+
stage = options.stage;
|
|
277
|
+
actor = options.actor;
|
|
278
|
+
await stage.mount(canvas);
|
|
279
|
+
}
|
|
259
280
|
this.#canvas = canvas;
|
|
260
|
-
this.#
|
|
281
|
+
this.#stage = stage;
|
|
282
|
+
this.#actor = actor;
|
|
261
283
|
this.#autoSway = options.autoSway !== false;
|
|
262
284
|
this.#onHit = options.onHit;
|
|
263
285
|
this.#chrome = mountChrome({
|
|
@@ -269,17 +291,26 @@ var Live2DWidget = class {
|
|
|
269
291
|
if (!this.#chrome) {
|
|
270
292
|
host.replaceChildren(canvas);
|
|
271
293
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
294
|
+
if (this.#autoSway) {
|
|
295
|
+
this.#unsubFrame = stage.onFrame((dt) => {
|
|
296
|
+
this.#swayPhase += dt;
|
|
297
|
+
const binding = actor.parameterMap().get("PARAM_ANGLE_X");
|
|
298
|
+
if (!binding) return;
|
|
299
|
+
const normalized = Math.sin(this.#swayPhase) * 0.25;
|
|
300
|
+
const value = normalized >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * normalized : binding.defaultValue + (binding.defaultValue - binding.min) * normalized;
|
|
301
|
+
actor.setParameter("PARAM_ANGLE_X", value);
|
|
302
|
+
});
|
|
274
303
|
}
|
|
275
304
|
if (options.autoplay !== false) {
|
|
276
|
-
|
|
305
|
+
stage.start();
|
|
277
306
|
} else {
|
|
278
|
-
|
|
307
|
+
stage.update(0);
|
|
308
|
+
stage.render();
|
|
279
309
|
}
|
|
280
310
|
}
|
|
281
311
|
destroy() {
|
|
282
|
-
this.#
|
|
312
|
+
this.#unsubFrame?.();
|
|
313
|
+
this.#unsubFrame = null;
|
|
283
314
|
if (this.#canvas) {
|
|
284
315
|
this.#canvas.removeEventListener(
|
|
285
316
|
"pointermove",
|
|
@@ -292,24 +323,31 @@ var Live2DWidget = class {
|
|
|
292
323
|
}
|
|
293
324
|
this.#chrome?.destroy();
|
|
294
325
|
this.#chrome = null;
|
|
295
|
-
this.#runtime
|
|
326
|
+
if (this.#runtime) {
|
|
327
|
+
this.#runtime.destroy();
|
|
328
|
+
} else {
|
|
329
|
+
this.#stage?.stop();
|
|
330
|
+
}
|
|
296
331
|
this.#canvas?.remove();
|
|
297
|
-
this.#runtime = null;
|
|
298
332
|
this.#canvas = null;
|
|
333
|
+
this.#stage = null;
|
|
334
|
+
this.#actor = null;
|
|
335
|
+
this.#runtime = null;
|
|
299
336
|
this.#onHit = void 0;
|
|
300
337
|
}
|
|
338
|
+
get stage() {
|
|
339
|
+
return this.#stage;
|
|
340
|
+
}
|
|
341
|
+
get actor() {
|
|
342
|
+
return this.#actor;
|
|
343
|
+
}
|
|
344
|
+
/** Legacy accessor when mounted via `createLive2D`. */
|
|
301
345
|
getRuntime() {
|
|
302
346
|
return this.#runtime;
|
|
303
347
|
}
|
|
304
|
-
/** Show a tips bubble when chrome tips are enabled. */
|
|
305
348
|
showMessage(text, timeoutMs, priority) {
|
|
306
349
|
this.#chrome?.tips?.show(text, timeoutMs, priority);
|
|
307
350
|
}
|
|
308
|
-
#parameterFromNormalized(id, normalized) {
|
|
309
|
-
const binding = this.#runtime?.parameterMap().get(id);
|
|
310
|
-
if (!binding) return normalized;
|
|
311
|
-
return normalized >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * normalized : binding.defaultValue + (binding.defaultValue - binding.min) * normalized;
|
|
312
|
-
}
|
|
313
351
|
#modelPoint(event) {
|
|
314
352
|
const canvas = this.#canvas;
|
|
315
353
|
if (!canvas) return null;
|
|
@@ -322,69 +360,60 @@ var Live2DWidget = class {
|
|
|
322
360
|
}
|
|
323
361
|
#onPointerMove = (event) => {
|
|
324
362
|
const p = this.#modelPoint(event);
|
|
325
|
-
const
|
|
326
|
-
if (!p || !
|
|
327
|
-
this.#autoSwayPausedByPointer = true;
|
|
363
|
+
const actor = this.#actor;
|
|
364
|
+
if (!p || !actor) return;
|
|
328
365
|
for (const { id, value } of focusParameterUpdates(
|
|
329
|
-
|
|
366
|
+
actor.parameterMap(),
|
|
330
367
|
p.x,
|
|
331
368
|
p.y
|
|
332
369
|
)) {
|
|
333
|
-
|
|
370
|
+
actor.setParameter(id, value);
|
|
334
371
|
}
|
|
335
372
|
};
|
|
336
373
|
#onPointerDown = (event) => {
|
|
337
374
|
const p = this.#modelPoint(event);
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
375
|
+
const stage = this.#stage;
|
|
376
|
+
const actor = this.#actor;
|
|
377
|
+
if (!p || !stage || !actor) return;
|
|
378
|
+
const stageX = (p.x + 1) / 2;
|
|
379
|
+
const stageY = (1 - p.y) / 2;
|
|
380
|
+
const hit = stage.hitTest(stageX, stageY);
|
|
381
|
+
if (!hit || hit.actorId !== actor.id) return;
|
|
382
|
+
this.#onHit?.({ area: hit.area, x: p.x, y: p.y });
|
|
383
|
+
this.#chrome?.tips?.show("\u78B0\u5230\u6211\u5566\uFF5E", 2500, 4);
|
|
345
384
|
};
|
|
346
|
-
#autoSwayPausedByPointer = false;
|
|
347
|
-
#startLoop() {
|
|
348
|
-
this.#stopLoop();
|
|
349
|
-
const tick = (ts) => {
|
|
350
|
-
const runtime = this.#runtime;
|
|
351
|
-
if (!runtime) return;
|
|
352
|
-
const dt = this.#lastTs ? (ts - this.#lastTs) / 1e3 : 0;
|
|
353
|
-
this.#lastTs = ts;
|
|
354
|
-
if (this.#autoSway && !this.#autoSwayPausedByPointer) {
|
|
355
|
-
runtime.setParameter(
|
|
356
|
-
"PARAM_ANGLE_X",
|
|
357
|
-
this.#parameterFromNormalized(
|
|
358
|
-
"PARAM_ANGLE_X",
|
|
359
|
-
Math.sin(ts / 1e3) * 0.25
|
|
360
|
-
)
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
if (this.#autoSwayPausedByPointer) {
|
|
364
|
-
this.#autoSwayPausedByPointer = false;
|
|
365
|
-
}
|
|
366
|
-
runtime.update(dt);
|
|
367
|
-
this.#raf = requestAnimationFrame(tick);
|
|
368
|
-
};
|
|
369
|
-
this.#raf = requestAnimationFrame(tick);
|
|
370
|
-
}
|
|
371
|
-
#stopLoop() {
|
|
372
|
-
if (this.#raf) cancelAnimationFrame(this.#raf);
|
|
373
|
-
this.#raf = 0;
|
|
374
|
-
this.#lastTs = 0;
|
|
375
|
-
}
|
|
376
385
|
};
|
|
386
|
+
var Live2DWidget = Live2dWidget;
|
|
377
387
|
function normalizePrefer(prefer) {
|
|
378
388
|
if (!prefer?.length) return void 0;
|
|
379
389
|
const allowed = /* @__PURE__ */ new Set(["webgpu", "webgl2", "canvas2d"]);
|
|
380
390
|
const out = prefer.filter((k) => allowed.has(k));
|
|
381
391
|
return out.length ? out : void 0;
|
|
382
392
|
}
|
|
393
|
+
async function createLive2dWidget(options) {
|
|
394
|
+
const widget = new Live2dWidget();
|
|
395
|
+
await widget.mount(options);
|
|
396
|
+
return widget;
|
|
397
|
+
}
|
|
383
398
|
async function mountWidget(options) {
|
|
384
|
-
const widget = new
|
|
399
|
+
const widget = new Live2dWidget();
|
|
385
400
|
await widget.mount(options);
|
|
386
401
|
return widget;
|
|
387
402
|
}
|
|
403
|
+
async function mountWidgetWithStage(options) {
|
|
404
|
+
const prefer = normalizePrefer(options.prefer);
|
|
405
|
+
const stage = createLive2dStage({
|
|
406
|
+
renderer: createRenderer({ prefer }),
|
|
407
|
+
updateMode: "auto"
|
|
408
|
+
});
|
|
409
|
+
const actor = stage.createActor({ id: allocateActorId("widget") });
|
|
410
|
+
const widget = new Live2dWidget();
|
|
411
|
+
await widget.mount({ ...options, stage, actor });
|
|
412
|
+
if (options.model) {
|
|
413
|
+
await actor.load(options.model);
|
|
414
|
+
}
|
|
415
|
+
return { widget, stage, actor };
|
|
416
|
+
}
|
|
388
417
|
|
|
389
418
|
// src/index.ts
|
|
390
419
|
var LIVE2D_WIDGET_VERSION = "0.0.0";
|
|
@@ -392,8 +421,11 @@ export {
|
|
|
392
421
|
DEFAULT_WELCOME,
|
|
393
422
|
LIVE2D_WIDGET_VERSION,
|
|
394
423
|
Live2DWidget,
|
|
424
|
+
Live2dWidget,
|
|
425
|
+
createLive2dWidget,
|
|
395
426
|
createTipMessage,
|
|
396
427
|
ensureChromeStyles,
|
|
397
428
|
mountChrome,
|
|
398
|
-
mountWidget
|
|
429
|
+
mountWidget,
|
|
430
|
+
mountWidgetWithStage
|
|
399
431
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doki-land/live2d-widget",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Live2D page widget —
|
|
3
|
+
"version": "0.0.23",
|
|
4
|
+
"description": "Live2D page widget — product chrome over Stage + Actor (no private RAF); built on @doki-land/live2d.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/doki-land/live2d.ts",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsup src/index.ts --format esm --dts",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"test": "vitest run
|
|
42
|
+
"test": "vitest run"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@doki-land/live2d": "0.0.
|
|
45
|
+
"@doki-land/live2d": "0.0.23"
|
|
46
46
|
},
|
|
47
47
|
"sideEffects": false
|
|
48
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,8 +9,14 @@ export {
|
|
|
9
9
|
mountChrome,
|
|
10
10
|
} from "./chrome/chrome.js";
|
|
11
11
|
export {
|
|
12
|
+
type ComposedWidgetOptions,
|
|
13
|
+
createLive2dWidget,
|
|
14
|
+
type LegacyWidgetOptions,
|
|
15
|
+
/** @deprecated Use `Live2dWidget`. */
|
|
12
16
|
Live2DWidget,
|
|
17
|
+
Live2dWidget,
|
|
13
18
|
mountWidget,
|
|
19
|
+
mountWidgetWithStage,
|
|
14
20
|
type WidgetChromeOptions,
|
|
15
21
|
type WidgetOptions,
|
|
16
22
|
type WidgetToolId,
|
package/src/shell/widget.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
allocateActorId,
|
|
3
3
|
createLive2D,
|
|
4
|
+
createLive2dStage,
|
|
5
|
+
createRenderer,
|
|
4
6
|
focusParameterUpdates,
|
|
5
7
|
type Live2DRuntime,
|
|
8
|
+
type Live2dActor,
|
|
9
|
+
type Live2dStage,
|
|
6
10
|
type RendererKind,
|
|
7
11
|
} from "@doki-land/live2d";
|
|
8
12
|
import {
|
|
@@ -13,38 +17,59 @@ import {
|
|
|
13
17
|
|
|
14
18
|
export type { WidgetChromeOptions, WidgetToolId } from "../chrome/chrome.js";
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
/** Composed widget over an existing Stage + Actor (no second RAF). */
|
|
21
|
+
export interface ComposedWidgetOptions {
|
|
22
|
+
target: string | HTMLElement;
|
|
23
|
+
stage: Live2dStage;
|
|
24
|
+
actor: Live2dActor;
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
/** Drive PARAM_ANGLE_X with a sine while the stage loop runs. Default true. */
|
|
28
|
+
autoSway?: boolean;
|
|
29
|
+
/** Start Stage-owned RAF after mount. Default true. */
|
|
30
|
+
autoplay?: boolean;
|
|
31
|
+
chrome?: boolean | WidgetChromeOptions;
|
|
32
|
+
onHit?: (payload: { area: string; x: number; y: number }) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Legacy bootstrap that still creates its own Stage + Actor.
|
|
37
|
+
* Prefer composing `stage` + `actor` yourself and calling `createLive2dWidget`.
|
|
38
|
+
*/
|
|
39
|
+
export interface LegacyWidgetOptions {
|
|
18
40
|
target: string | HTMLElement;
|
|
19
|
-
/** Initial model URL (model.json / model3.json / npm:…). */
|
|
20
41
|
model?: string;
|
|
21
42
|
width?: number;
|
|
22
43
|
height?: number;
|
|
23
|
-
|
|
44
|
+
prefer?: RendererKind[];
|
|
24
45
|
autoSway?: boolean;
|
|
25
|
-
/** Start the RAF update loop after mount. Default true. */
|
|
26
46
|
autoplay?: boolean;
|
|
27
|
-
/**
|
|
28
|
-
* Optional tips bubble + toolbar (hitokoto / photo / quit).
|
|
29
|
-
* Pass `true` for defaults, or a config object.
|
|
30
|
-
*/
|
|
31
47
|
chrome?: boolean | WidgetChromeOptions;
|
|
32
|
-
/** Fired when canvas hit-test finds a drawable. */
|
|
33
48
|
onHit?: (payload: { area: string; x: number; y: number }) => void;
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
export type WidgetOptions = ComposedWidgetOptions | LegacyWidgetOptions;
|
|
52
|
+
|
|
53
|
+
function isLegacyOptions(
|
|
54
|
+
options: WidgetOptions,
|
|
55
|
+
): options is LegacyWidgetOptions {
|
|
56
|
+
return !("stage" in options && "actor" in options);
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
/**
|
|
37
|
-
* Page widget shell over
|
|
38
|
-
*
|
|
60
|
+
* Page widget shell — product chrome over Stage + Actor.
|
|
61
|
+
* Does not own renderer selection or a private RAF loop.
|
|
39
62
|
*/
|
|
40
|
-
export class
|
|
63
|
+
export class Live2dWidget {
|
|
41
64
|
#canvas: HTMLCanvasElement | null = null;
|
|
65
|
+
#stage: Live2dStage | null = null;
|
|
66
|
+
#actor: Live2dActor | null = null;
|
|
42
67
|
#runtime: Live2DRuntime | null = null;
|
|
43
68
|
#chrome: ChromeSession | null = null;
|
|
44
|
-
#
|
|
45
|
-
#lastTs = 0;
|
|
69
|
+
#unsubFrame: (() => void) | null = null;
|
|
46
70
|
#autoSway = true;
|
|
47
|
-
#
|
|
71
|
+
#swayPhase = 0;
|
|
72
|
+
#onHit: ComposedWidgetOptions["onHit"];
|
|
48
73
|
|
|
49
74
|
async mount(options: WidgetOptions): Promise<void> {
|
|
50
75
|
const host =
|
|
@@ -69,16 +94,30 @@ export class Live2DWidget {
|
|
|
69
94
|
canvas.addEventListener("pointermove", this.#onPointerMove);
|
|
70
95
|
canvas.addEventListener("pointerdown", this.#onPointerDown);
|
|
71
96
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
let stage: Live2dStage;
|
|
98
|
+
let actor: Live2dActor;
|
|
99
|
+
if (isLegacyOptions(options)) {
|
|
100
|
+
const prefer = normalizePrefer(options.prefer);
|
|
101
|
+
const runtime = createLive2D({
|
|
102
|
+
prefer,
|
|
103
|
+
updateMode: "auto",
|
|
104
|
+
});
|
|
105
|
+
await runtime.mount(canvas);
|
|
106
|
+
if (options.model) {
|
|
107
|
+
await runtime.loadModel(options.model);
|
|
108
|
+
}
|
|
109
|
+
this.#runtime = runtime;
|
|
110
|
+
stage = runtime.stage;
|
|
111
|
+
actor = runtime.actor;
|
|
112
|
+
} else {
|
|
113
|
+
stage = options.stage;
|
|
114
|
+
actor = options.actor;
|
|
115
|
+
await stage.mount(canvas);
|
|
116
|
+
}
|
|
79
117
|
|
|
80
118
|
this.#canvas = canvas;
|
|
81
|
-
this.#
|
|
119
|
+
this.#stage = stage;
|
|
120
|
+
this.#actor = actor;
|
|
82
121
|
this.#autoSway = options.autoSway !== false;
|
|
83
122
|
this.#onHit = options.onHit;
|
|
84
123
|
this.#chrome = mountChrome({
|
|
@@ -91,19 +130,33 @@ export class Live2DWidget {
|
|
|
91
130
|
host.replaceChildren(canvas);
|
|
92
131
|
}
|
|
93
132
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
133
|
+
if (this.#autoSway) {
|
|
134
|
+
this.#unsubFrame = stage.onFrame((dt) => {
|
|
135
|
+
this.#swayPhase += dt;
|
|
136
|
+
const binding = actor.parameterMap().get("PARAM_ANGLE_X");
|
|
137
|
+
if (!binding) return;
|
|
138
|
+
const normalized = Math.sin(this.#swayPhase) * 0.25;
|
|
139
|
+
const value =
|
|
140
|
+
normalized >= 0
|
|
141
|
+
? binding.defaultValue +
|
|
142
|
+
(binding.max - binding.defaultValue) * normalized
|
|
143
|
+
: binding.defaultValue +
|
|
144
|
+
(binding.defaultValue - binding.min) * normalized;
|
|
145
|
+
actor.setParameter("PARAM_ANGLE_X", value);
|
|
146
|
+
});
|
|
96
147
|
}
|
|
97
148
|
|
|
98
149
|
if (options.autoplay !== false) {
|
|
99
|
-
|
|
150
|
+
stage.start();
|
|
100
151
|
} else {
|
|
101
|
-
|
|
152
|
+
stage.update(0);
|
|
153
|
+
stage.render();
|
|
102
154
|
}
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
destroy(): void {
|
|
106
|
-
this.#
|
|
158
|
+
this.#unsubFrame?.();
|
|
159
|
+
this.#unsubFrame = null;
|
|
107
160
|
if (this.#canvas) {
|
|
108
161
|
this.#canvas.removeEventListener(
|
|
109
162
|
"pointermove",
|
|
@@ -116,18 +169,32 @@ export class Live2DWidget {
|
|
|
116
169
|
}
|
|
117
170
|
this.#chrome?.destroy();
|
|
118
171
|
this.#chrome = null;
|
|
119
|
-
this.#runtime
|
|
172
|
+
if (this.#runtime) {
|
|
173
|
+
this.#runtime.destroy();
|
|
174
|
+
} else {
|
|
175
|
+
this.#stage?.stop();
|
|
176
|
+
}
|
|
120
177
|
this.#canvas?.remove();
|
|
121
|
-
this.#runtime = null;
|
|
122
178
|
this.#canvas = null;
|
|
179
|
+
this.#stage = null;
|
|
180
|
+
this.#actor = null;
|
|
181
|
+
this.#runtime = null;
|
|
123
182
|
this.#onHit = undefined;
|
|
124
183
|
}
|
|
125
184
|
|
|
185
|
+
get stage(): Live2dStage | null {
|
|
186
|
+
return this.#stage;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get actor(): Live2dActor | null {
|
|
190
|
+
return this.#actor;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Legacy accessor when mounted via `createLive2D`. */
|
|
126
194
|
getRuntime(): Live2DRuntime | null {
|
|
127
195
|
return this.#runtime;
|
|
128
196
|
}
|
|
129
197
|
|
|
130
|
-
/** Show a tips bubble when chrome tips are enabled. */
|
|
131
198
|
showMessage(
|
|
132
199
|
text: string | string[],
|
|
133
200
|
timeoutMs?: number,
|
|
@@ -136,16 +203,6 @@ export class Live2DWidget {
|
|
|
136
203
|
this.#chrome?.tips?.show(text, timeoutMs, priority);
|
|
137
204
|
}
|
|
138
205
|
|
|
139
|
-
#parameterFromNormalized(id: string, normalized: number): number {
|
|
140
|
-
const binding = this.#runtime?.parameterMap().get(id);
|
|
141
|
-
if (!binding) return normalized;
|
|
142
|
-
return normalized >= 0
|
|
143
|
-
? binding.defaultValue +
|
|
144
|
-
(binding.max - binding.defaultValue) * normalized
|
|
145
|
-
: binding.defaultValue +
|
|
146
|
-
(binding.defaultValue - binding.min) * normalized;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
206
|
#modelPoint(event: PointerEvent): { x: number; y: number } | null {
|
|
150
207
|
const canvas = this.#canvas;
|
|
151
208
|
if (!canvas) return null;
|
|
@@ -159,65 +216,34 @@ export class Live2DWidget {
|
|
|
159
216
|
|
|
160
217
|
#onPointerMove = (event: PointerEvent): void => {
|
|
161
218
|
const p = this.#modelPoint(event);
|
|
162
|
-
const
|
|
163
|
-
if (!p || !
|
|
164
|
-
// Pointer tracking overrides auto-sway for ANGLE_X while moving.
|
|
165
|
-
this.#autoSwayPausedByPointer = true;
|
|
219
|
+
const actor = this.#actor;
|
|
220
|
+
if (!p || !actor) return;
|
|
166
221
|
for (const { id, value } of focusParameterUpdates(
|
|
167
|
-
|
|
222
|
+
actor.parameterMap(),
|
|
168
223
|
p.x,
|
|
169
224
|
p.y,
|
|
170
225
|
)) {
|
|
171
|
-
|
|
226
|
+
actor.setParameter(id, value);
|
|
172
227
|
}
|
|
173
228
|
};
|
|
174
229
|
|
|
175
230
|
#onPointerDown = (event: PointerEvent): void => {
|
|
176
231
|
const p = this.#modelPoint(event);
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
232
|
+
const stage = this.#stage;
|
|
233
|
+
const actor = this.#actor;
|
|
234
|
+
if (!p || !stage || !actor) return;
|
|
235
|
+
const stageX = (p.x + 1) / 2;
|
|
236
|
+
const stageY = (1 - p.y) / 2;
|
|
237
|
+
const hit = stage.hitTest(stageX, stageY);
|
|
238
|
+
if (!hit || hit.actorId !== actor.id) return;
|
|
239
|
+
this.#onHit?.({ area: hit.area, x: p.x, y: p.y });
|
|
240
|
+
this.#chrome?.tips?.show("碰到我啦~", 2500, 4);
|
|
184
241
|
};
|
|
185
|
-
|
|
186
|
-
#autoSwayPausedByPointer = false;
|
|
187
|
-
|
|
188
|
-
#startLoop(): void {
|
|
189
|
-
this.#stopLoop();
|
|
190
|
-
const tick = (ts: number) => {
|
|
191
|
-
const runtime = this.#runtime;
|
|
192
|
-
if (!runtime) return;
|
|
193
|
-
const dt = this.#lastTs ? (ts - this.#lastTs) / 1000 : 0;
|
|
194
|
-
this.#lastTs = ts;
|
|
195
|
-
if (this.#autoSway && !this.#autoSwayPausedByPointer) {
|
|
196
|
-
runtime.setParameter(
|
|
197
|
-
"PARAM_ANGLE_X",
|
|
198
|
-
this.#parameterFromNormalized(
|
|
199
|
-
"PARAM_ANGLE_X",
|
|
200
|
-
Math.sin(ts / 1000) * 0.25,
|
|
201
|
-
),
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
// Resume sway shortly after the last pointer sample.
|
|
205
|
-
if (this.#autoSwayPausedByPointer) {
|
|
206
|
-
this.#autoSwayPausedByPointer = false;
|
|
207
|
-
}
|
|
208
|
-
runtime.update(dt);
|
|
209
|
-
this.#raf = requestAnimationFrame(tick);
|
|
210
|
-
};
|
|
211
|
-
this.#raf = requestAnimationFrame(tick);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
#stopLoop(): void {
|
|
215
|
-
if (this.#raf) cancelAnimationFrame(this.#raf);
|
|
216
|
-
this.#raf = 0;
|
|
217
|
-
this.#lastTs = 0;
|
|
218
|
-
}
|
|
219
242
|
}
|
|
220
243
|
|
|
244
|
+
/** @deprecated Use `Live2dWidget` — alias kept for hexo / legacy imports. */
|
|
245
|
+
export const Live2DWidget = Live2dWidget;
|
|
246
|
+
|
|
221
247
|
function normalizePrefer(
|
|
222
248
|
prefer: RendererKind[] | undefined,
|
|
223
249
|
): RendererKind[] | undefined {
|
|
@@ -227,10 +253,36 @@ function normalizePrefer(
|
|
|
227
253
|
return out.length ? out : undefined;
|
|
228
254
|
}
|
|
229
255
|
|
|
256
|
+
export async function createLive2dWidget(
|
|
257
|
+
options: ComposedWidgetOptions,
|
|
258
|
+
): Promise<Live2dWidget> {
|
|
259
|
+
const widget = new Live2dWidget();
|
|
260
|
+
await widget.mount(options);
|
|
261
|
+
return widget;
|
|
262
|
+
}
|
|
263
|
+
|
|
230
264
|
export async function mountWidget(
|
|
231
265
|
options: WidgetOptions,
|
|
232
|
-
): Promise<
|
|
233
|
-
const widget = new
|
|
266
|
+
): Promise<Live2dWidget> {
|
|
267
|
+
const widget = new Live2dWidget();
|
|
234
268
|
await widget.mount(options);
|
|
235
269
|
return widget;
|
|
236
270
|
}
|
|
271
|
+
|
|
272
|
+
/** Convenience bootstrap: Stage + default Actor + widget chrome. */
|
|
273
|
+
export async function mountWidgetWithStage(
|
|
274
|
+
options: LegacyWidgetOptions,
|
|
275
|
+
): Promise<{ widget: Live2dWidget; stage: Live2dStage; actor: Live2dActor }> {
|
|
276
|
+
const prefer = normalizePrefer(options.prefer);
|
|
277
|
+
const stage = createLive2dStage({
|
|
278
|
+
renderer: createRenderer({ prefer }),
|
|
279
|
+
updateMode: "auto",
|
|
280
|
+
});
|
|
281
|
+
const actor = stage.createActor({ id: allocateActorId("widget") });
|
|
282
|
+
const widget = new Live2dWidget();
|
|
283
|
+
await widget.mount({ ...options, stage, actor });
|
|
284
|
+
if (options.model) {
|
|
285
|
+
await actor.load(options.model);
|
|
286
|
+
}
|
|
287
|
+
return { widget, stage, actor };
|
|
288
|
+
}
|