@doki-land/live2d 0.0.10 → 0.0.12

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.
Files changed (47) hide show
  1. package/README.md +3 -0
  2. package/dist/index.d.ts +326 -0
  3. package/dist/index.js +1667 -0
  4. package/dist/reexports/core.d.ts +3 -0
  5. package/dist/reexports/core.js +2 -0
  6. package/dist/reexports/loader.d.ts +1 -0
  7. package/dist/reexports/loader.js +2 -0
  8. package/dist/reexports/renderer.d.ts +1 -0
  9. package/dist/reexports/renderer.js +2 -0
  10. package/package.json +61 -43
  11. package/src/create-live2d.ts +39 -0
  12. package/src/focus.ts +53 -0
  13. package/src/index.ts +78 -187
  14. package/src/load-textures.ts +85 -0
  15. package/src/motion/evaluate-curve.ts +119 -0
  16. package/src/motion/index.ts +20 -0
  17. package/src/motion/motion-player.ts +389 -0
  18. package/src/motion/parse-motion3.ts +164 -0
  19. package/src/motion/types.ts +89 -0
  20. package/src/reexports/core.ts +1 -0
  21. package/src/reexports/loader.ts +1 -0
  22. package/src/reexports/renderer.ts +1 -0
  23. package/src/stage/actor-model-slot.ts +429 -0
  24. package/src/stage/actor.ts +188 -0
  25. package/src/stage/index.ts +18 -0
  26. package/src/stage/single-facade.ts +221 -0
  27. package/src/stage/stage.ts +388 -0
  28. package/src/stage/transform.ts +161 -0
  29. package/dist/l2d.umd.js +0 -1208
  30. package/dist/l2d.umd.js.map +0 -1
  31. package/dist/live2d.css +0 -1
  32. package/lib/cubism2.d.ts +0 -179
  33. package/lib/cubism2.min.js +0 -2
  34. package/lib/cubism5.d.ts +0 -367
  35. package/lib/cubism5.min.js +0 -10
  36. package/lib/index.d.ts +0 -7
  37. package/readme.md +0 -60
  38. package/src/fs/index.ts +0 -93
  39. package/src/helper/index.ts +0 -25
  40. package/src/icons/icons.ts +0 -39
  41. package/src/icons/style.css +0 -44
  42. package/src/icons/switch-character.svg +0 -1
  43. package/src/icons/switch-costume.svg +0 -1
  44. package/src/types/Live2dOptions.ts +0 -56
  45. package/src/types/ModelOptions.ts +0 -40
  46. package/src/types/Resolve.ts +0 -16
  47. package/src/types/index.ts +0 -3
package/dist/index.js ADDED
@@ -0,0 +1,1667 @@
1
+ // src/index.ts
2
+ import { DEFAULT_ACTOR_TRANSFORM as DEFAULT_ACTOR_TRANSFORM2, EventEmitter as EventEmitter2 } from "@doki-land/live2d-core";
3
+ import {
4
+ DEFAULT_NPM_CDN,
5
+ resolveModelSourceUrl as resolveModelSourceUrl2,
6
+ resolveNpmSpecifier
7
+ } from "@doki-land/live2d-loader";
8
+ import {
9
+ createCanvas2DRenderer,
10
+ createMoc2Backend as createMoc2Backend3,
11
+ createMoc3Backend as createMoc3Backend3,
12
+ createQuadProgram,
13
+ createRenderer as createRenderer3,
14
+ createWebGl2Renderer,
15
+ createWebGpuRenderer,
16
+ decodeMoc3,
17
+ evaluateFrame,
18
+ fingerprintSnapshot,
19
+ parseCpuProgram,
20
+ serializeCpuProgram
21
+ } from "@doki-land/live2d-renderer";
22
+
23
+ // src/create-live2d.ts
24
+ import {
25
+ createMoc2Backend as createMoc2Backend2,
26
+ createMoc3Backend as createMoc3Backend2,
27
+ createRenderer as createRenderer2
28
+ } from "@doki-land/live2d-renderer";
29
+
30
+ // src/focus.ts
31
+ function focusParameterUpdates(parameters, dragX, dragY) {
32
+ const byId = new Map(parameters.map((p) => [p.id, p]));
33
+ const x = clampUnit(dragX);
34
+ const y = clampUnit(dragY);
35
+ const out = [];
36
+ const set = (id, normalized) => {
37
+ const binding = byId.get(id);
38
+ if (!binding) return;
39
+ out.push({ id, value: valueFromNormalized(binding, normalized) });
40
+ };
41
+ set("PARAM_ANGLE_X", x);
42
+ set("PARAM_ANGLE_Y", y);
43
+ set("PARAM_ANGLE_Z", clampUnit(x * y * -1));
44
+ set("PARAM_BODY_ANGLE_X", x);
45
+ set("PARAM_BODY_ANGLE_Y", y);
46
+ set("PARAM_EYE_BALL_X", x);
47
+ set("PARAM_EYE_BALL_Y", y);
48
+ return out;
49
+ }
50
+ function clampUnit(n) {
51
+ if (n > 1) return 1;
52
+ if (n < -1) return -1;
53
+ return n;
54
+ }
55
+ function valueFromNormalized(binding, normalized) {
56
+ const n = clampUnit(normalized);
57
+ return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
58
+ }
59
+
60
+ // src/stage/actor-model-slot.ts
61
+ import { modelSourceUrl } from "@doki-land/live2d-core";
62
+ import {
63
+ createUrlAssetResolver,
64
+ fetchModelJson,
65
+ normalizeModelSettings,
66
+ resolveModelSourceUrl
67
+ } from "@doki-land/live2d-loader";
68
+ import { selectModelBackend } from "@doki-land/live2d-renderer";
69
+
70
+ // src/load-textures.ts
71
+ function guessMime(path) {
72
+ const lower = path.toLowerCase();
73
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
74
+ if (lower.endsWith(".webp")) return "image/webp";
75
+ if (lower.endsWith(".gif")) return "image/gif";
76
+ return "image/png";
77
+ }
78
+ async function bytesToImageBitmap(bytes, path) {
79
+ if (typeof createImageBitmap !== "function") {
80
+ throw new Error(
81
+ "@doki-land/live2d: createImageBitmap is not available in this environment"
82
+ );
83
+ }
84
+ const blob = new Blob([new Uint8Array(bytes)], {
85
+ type: guessMime(path)
86
+ });
87
+ return createImageBitmap(blob);
88
+ }
89
+ async function loadTextureData(resolver, paths, options = {}) {
90
+ const out = [];
91
+ const total = paths.length;
92
+ for (let i = 0; i < paths.length; i++) {
93
+ const key = paths[i];
94
+ options.onProgress?.({
95
+ index: i,
96
+ total,
97
+ key,
98
+ bytesLoaded: 0,
99
+ bytesTotal: null
100
+ });
101
+ const bytes = await resolver.fetchBytes(key);
102
+ options.onProgress?.({
103
+ index: i,
104
+ total,
105
+ key,
106
+ bytesLoaded: bytes.byteLength,
107
+ bytesTotal: bytes.byteLength
108
+ });
109
+ const image = await bytesToImageBitmap(bytes, key);
110
+ out.push({
111
+ index: i,
112
+ image,
113
+ width: image.width,
114
+ height: image.height
115
+ });
116
+ }
117
+ return out;
118
+ }
119
+ function releaseTextureData(textures) {
120
+ for (const t of textures) {
121
+ const img = t.image;
122
+ if (typeof ImageBitmap !== "undefined" && img instanceof ImageBitmap) {
123
+ img.close();
124
+ }
125
+ }
126
+ }
127
+
128
+ // src/motion/evaluate-curve.ts
129
+ function evaluateMotion3(clip, timeSeconds) {
130
+ const t = clamp(timeSeconds, 0, clip.duration);
131
+ const out = [];
132
+ for (const curve of clip.curves) {
133
+ out.push({
134
+ target: curve.target,
135
+ id: curve.id,
136
+ value: evaluateCurve(curve, t, clip.areBeziersRestricted)
137
+ });
138
+ }
139
+ return out;
140
+ }
141
+ function evaluateCurve(curve, timeSeconds, areBeziersRestricted) {
142
+ const segs = curve.segments;
143
+ if (segs.length === 0) return 0;
144
+ if (timeSeconds <= segs[0].p0.time) return segs[0].p0.value;
145
+ const last = segs[segs.length - 1];
146
+ if (timeSeconds >= last.p3.time) return last.p3.value;
147
+ for (let i = 0; i < segs.length; i += 1) {
148
+ const seg = segs[i];
149
+ const isLast = i === segs.length - 1;
150
+ if (timeSeconds < seg.p3.time || isLast && timeSeconds <= seg.p3.time) {
151
+ return evaluateSegment(seg, timeSeconds, areBeziersRestricted);
152
+ }
153
+ }
154
+ return last.p3.value;
155
+ }
156
+ function evaluateSegment(seg, time, areBeziersRestricted) {
157
+ const { p0, p3 } = seg;
158
+ switch (seg.kind) {
159
+ case "linear": {
160
+ const span = p3.time - p0.time;
161
+ if (span <= 0) return p3.value;
162
+ const u = (time - p0.time) / span;
163
+ return p0.value + (p3.value - p0.value) * u;
164
+ }
165
+ case "stepped":
166
+ return p0.value;
167
+ case "inverseStepped":
168
+ return p3.value;
169
+ case "bezier": {
170
+ const p1 = seg.p1;
171
+ const p2 = seg.p2;
172
+ if (areBeziersRestricted) {
173
+ const span = p3.time - p0.time;
174
+ if (span <= 0) return p3.value;
175
+ const u2 = (time - p0.time) / span;
176
+ return cubic(p0.value, p1.value, p2.value, p3.value, u2);
177
+ }
178
+ const u = solveBezierTime(p0.time, p1.time, p2.time, p3.time, time);
179
+ return cubic(p0.value, p1.value, p2.value, p3.value, u);
180
+ }
181
+ default:
182
+ return p3.value;
183
+ }
184
+ }
185
+ function cubic(a, b, c, d, t) {
186
+ const u = 1 - t;
187
+ return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;
188
+ }
189
+ function solveBezierTime(t0, t1, t2, t3, target) {
190
+ let lo = 0;
191
+ let hi = 1;
192
+ for (let i = 0; i < 20; i += 1) {
193
+ const mid = (lo + hi) * 0.5;
194
+ const x = cubic(t0, t1, t2, t3, mid);
195
+ if (x < target) lo = mid;
196
+ else hi = mid;
197
+ }
198
+ return (lo + hi) * 0.5;
199
+ }
200
+ function clamp(n, min, max) {
201
+ if (n < min) return min;
202
+ if (n > max) return max;
203
+ return n;
204
+ }
205
+
206
+ // src/motion/types.ts
207
+ var MotionPriority = {
208
+ none: 0,
209
+ idle: 1,
210
+ normal: 2,
211
+ force: 3
212
+ };
213
+
214
+ // src/motion/motion-player.ts
215
+ var MotionPlayer = class {
216
+ #slots = /* @__PURE__ */ new Map();
217
+ #queues = /* @__PURE__ */ new Map();
218
+ #handlers;
219
+ constructor(handlers = {}) {
220
+ this.#handlers = handlers;
221
+ }
222
+ get isPlaying() {
223
+ return this.#slots.size > 0;
224
+ }
225
+ listPlaying() {
226
+ return [...this.#slots.values()].map((a) => ({
227
+ slot: a.slot,
228
+ group: a.group,
229
+ index: a.index,
230
+ time: a.time,
231
+ priority: a.priority
232
+ }));
233
+ }
234
+ /** @deprecated Prefer {@link listPlaying}; returns highest-priority slot. */
235
+ get current() {
236
+ const list = [...this.listPlaying()];
237
+ if (!list.length) return null;
238
+ list.sort((a, b) => b.priority - a.priority);
239
+ const top = list[0];
240
+ return {
241
+ group: top.group,
242
+ index: top.index,
243
+ time: top.time,
244
+ priority: top.priority
245
+ };
246
+ }
247
+ /**
248
+ * Start a clip on a slot. Returns false if rejected by priority
249
+ * (and not queued).
250
+ */
251
+ start(group, index, clip, options = {}) {
252
+ const priority = options.priority ?? MotionPriority.normal;
253
+ const slot = options.slot ?? `priority:${priority}`;
254
+ const existing = this.#slots.get(slot);
255
+ if (existing && priority < existing.priority) {
256
+ if (options.queue) {
257
+ this.#enqueue(slot, { group, index, clip, options });
258
+ return true;
259
+ }
260
+ return false;
261
+ }
262
+ if (existing && options.queue && !existing.fadingOut) {
263
+ this.#enqueue(slot, { group, index, clip, options });
264
+ return true;
265
+ }
266
+ if (existing) {
267
+ if (existing.fadeOutTime > 0 && !existing.fadingOut) {
268
+ existing.fadingOut = true;
269
+ existing.fadeOutElapsed = 0;
270
+ this.#enqueueFront(slot, { group, index, clip, options });
271
+ return true;
272
+ }
273
+ this.#finish(existing, false);
274
+ }
275
+ this.#slots.set(
276
+ slot,
277
+ this.#createActive(slot, group, index, clip, options)
278
+ );
279
+ return true;
280
+ }
281
+ /** Fade out (default) or hard-stop; `slot` omits → all slots. */
282
+ stop(fade = true, slot) {
283
+ if (slot !== void 0) {
284
+ const a = this.#slots.get(slot);
285
+ if (!a) return;
286
+ this.#stopOne(a, fade);
287
+ return;
288
+ }
289
+ for (const a of [...this.#slots.values()]) {
290
+ this.#stopOne(a, fade);
291
+ }
292
+ }
293
+ clear() {
294
+ this.#slots.clear();
295
+ this.#queues.clear();
296
+ }
297
+ /**
298
+ * Advance all slots and return blended samples (weight baked in; apply as absolute).
299
+ */
300
+ update(deltaTimeSeconds) {
301
+ const dt = Math.max(0, deltaTimeSeconds);
302
+ const layerSamples = [];
303
+ for (const a of [...this.#slots.values()]) {
304
+ const samples = this.#tick(a, dt);
305
+ if (samples) {
306
+ layerSamples.push({ priority: a.priority, samples });
307
+ }
308
+ }
309
+ layerSamples.sort((a, b) => a.priority - b.priority);
310
+ return blendMotionLayers(layerSamples);
311
+ }
312
+ #tick(a, dt) {
313
+ if (!a.started) {
314
+ a.started = true;
315
+ this.#handlers.onStart?.({
316
+ group: a.group,
317
+ index: a.index,
318
+ slot: a.slot
319
+ });
320
+ }
321
+ a.time += dt;
322
+ if (a.fadingOut) {
323
+ a.fadeOutElapsed += dt;
324
+ if (a.fadeOutElapsed >= a.fadeOutTime) {
325
+ this.#finish(a, true);
326
+ return null;
327
+ }
328
+ } else if (!a.loop && a.time >= a.clip.duration) {
329
+ if (a.fadeOutTime > 0) {
330
+ a.fadingOut = true;
331
+ a.fadeOutElapsed = 0;
332
+ } else {
333
+ const samples = this.#sample(a, a.clip.duration, 1);
334
+ this.#finish(a, true);
335
+ return samples;
336
+ }
337
+ }
338
+ let playTime = a.time;
339
+ if (a.loop && a.clip.duration > 0) {
340
+ playTime = a.time % a.clip.duration;
341
+ } else {
342
+ playTime = Math.min(playTime, a.clip.duration);
343
+ }
344
+ this.#emitEvents(a, playTime);
345
+ return this.#sample(a, playTime, this.#fadeWeight(a));
346
+ }
347
+ #createActive(slot, group, index, clip, options) {
348
+ const fadeIn = options.fadeInTime ?? (clip.fadeInTime > 0 ? clip.fadeInTime : 0);
349
+ const fadeOut = options.fadeOutTime ?? (clip.fadeOutTime > 0 ? clip.fadeOutTime : 0);
350
+ return {
351
+ slot,
352
+ group,
353
+ index,
354
+ clip,
355
+ priority: options.priority ?? MotionPriority.normal,
356
+ loop: options.loop ?? clip.loop,
357
+ fadeInTime: Math.max(0, fadeIn),
358
+ fadeOutTime: Math.max(0, fadeOut),
359
+ time: 0,
360
+ fadingOut: false,
361
+ fadeOutElapsed: 0,
362
+ lastEventIndex: -1,
363
+ started: false
364
+ };
365
+ }
366
+ #enqueue(slot, item) {
367
+ const q = this.#queues.get(slot) ?? [];
368
+ q.push(item);
369
+ this.#queues.set(slot, q);
370
+ }
371
+ #enqueueFront(slot, item) {
372
+ const q = this.#queues.get(slot) ?? [];
373
+ q.unshift(item);
374
+ this.#queues.set(slot, q);
375
+ }
376
+ #stopOne(a, fade) {
377
+ if (!fade || a.fadeOutTime <= 0) {
378
+ this.#finish(a, true);
379
+ return;
380
+ }
381
+ a.fadingOut = true;
382
+ a.fadeOutElapsed = 0;
383
+ }
384
+ #finish(a, promoteQueue) {
385
+ if (this.#slots.get(a.slot) !== a) return;
386
+ this.#slots.delete(a.slot);
387
+ this.#handlers.onFinish?.({
388
+ group: a.group,
389
+ index: a.index,
390
+ slot: a.slot
391
+ });
392
+ if (!promoteQueue) return;
393
+ const q = this.#queues.get(a.slot);
394
+ const next = q?.shift();
395
+ if (next) {
396
+ this.#slots.set(
397
+ a.slot,
398
+ this.#createActive(
399
+ a.slot,
400
+ next.group,
401
+ next.index,
402
+ next.clip,
403
+ next.options
404
+ )
405
+ );
406
+ }
407
+ }
408
+ #sample(a, playTime, weight) {
409
+ const values = evaluateMotion3(a.clip, playTime);
410
+ return values.map((v) => ({
411
+ target: v.target,
412
+ id: v.id,
413
+ value: v.value,
414
+ weight
415
+ }));
416
+ }
417
+ #fadeWeight(a) {
418
+ let w = 1;
419
+ if (a.fadeInTime > 0 && a.time < a.fadeInTime) {
420
+ w = sineEase(a.time / a.fadeInTime);
421
+ }
422
+ if (a.fadingOut && a.fadeOutTime > 0) {
423
+ const u = 1 - a.fadeOutElapsed / a.fadeOutTime;
424
+ w *= sineEase(Math.max(0, u));
425
+ }
426
+ return w;
427
+ }
428
+ #emitEvents(a, playTime) {
429
+ const events = a.clip.userData;
430
+ for (let i = a.lastEventIndex + 1; i < events.length; i += 1) {
431
+ const e = events[i];
432
+ if (e.time > playTime) break;
433
+ a.lastEventIndex = i;
434
+ this.#handlers.onEvent?.({
435
+ group: a.group,
436
+ index: a.index,
437
+ slot: a.slot,
438
+ time: e.time,
439
+ value: e.value
440
+ });
441
+ }
442
+ if (a.loop && a.clip.duration > 0) {
443
+ const prevMod = (a.time - 1e-6) % a.clip.duration + (a.time - 1e-6 < 0 ? a.clip.duration : 0);
444
+ if (playTime < prevMod - 1e-4) {
445
+ a.lastEventIndex = -1;
446
+ }
447
+ }
448
+ }
449
+ };
450
+ function blendMotionLayers(layers) {
451
+ const map = /* @__PURE__ */ new Map();
452
+ for (const layer of layers) {
453
+ for (const s of layer.samples) {
454
+ const key = `${s.target}\0${s.id}`;
455
+ const w = Math.min(1, Math.max(0, s.weight));
456
+ const prev = map.get(key);
457
+ if (!prev) {
458
+ map.set(key, {
459
+ target: s.target,
460
+ id: s.id,
461
+ value: s.value,
462
+ weight: w
463
+ });
464
+ } else {
465
+ prev.value = prev.value + (s.value - prev.value) * w;
466
+ prev.weight = Math.min(1, prev.weight + w * (1 - prev.weight));
467
+ }
468
+ }
469
+ }
470
+ return [...map.values()];
471
+ }
472
+ function sineEase(t) {
473
+ const x = Math.min(1, Math.max(0, t));
474
+ return 0.5 - 0.5 * Math.cos(x * Math.PI);
475
+ }
476
+
477
+ // src/motion/parse-motion3.ts
478
+ var SEGMENT_KIND = {
479
+ 0: "linear",
480
+ 1: "bezier",
481
+ 2: "stepped",
482
+ 3: "inverseStepped"
483
+ };
484
+ function parseMotion3(json) {
485
+ if (!json || typeof json !== "object") {
486
+ throw new Error(
487
+ "@doki-land/live2d: motion3.json root must be an object"
488
+ );
489
+ }
490
+ const root = json;
491
+ const version = Number(root.Version ?? 3);
492
+ const meta = root.Meta;
493
+ if (!meta || typeof meta !== "object") {
494
+ throw new Error("@doki-land/live2d: motion3.json missing Meta");
495
+ }
496
+ const m = meta;
497
+ const duration = num(m.Duration, "Meta.Duration");
498
+ const fps = num(m.Fps, "Meta.Fps");
499
+ const loop = m.Loop === true;
500
+ const areBeziersRestricted = m.AreBeziersRestricted !== false;
501
+ const fadeInTime = optionalNum(m.FadeInTime) ?? 0;
502
+ const fadeOutTime = optionalNum(m.FadeOutTime) ?? 0;
503
+ const curvesRaw = root.Curves;
504
+ if (!Array.isArray(curvesRaw)) {
505
+ throw new Error("@doki-land/live2d: motion3.json missing Curves");
506
+ }
507
+ const curves = curvesRaw.map((c, i) => parseCurve(c, i));
508
+ const userData = [];
509
+ if (Array.isArray(root.UserData)) {
510
+ for (const item of root.UserData) {
511
+ if (!item || typeof item !== "object") continue;
512
+ const u = item;
513
+ if (typeof u.Time === "number" && typeof u.Value === "string") {
514
+ userData.push({ time: u.Time, value: u.Value });
515
+ }
516
+ }
517
+ userData.sort((a, b) => a.time - b.time);
518
+ }
519
+ return {
520
+ version,
521
+ duration,
522
+ fps,
523
+ loop,
524
+ areBeziersRestricted,
525
+ fadeInTime,
526
+ fadeOutTime,
527
+ curves,
528
+ userData
529
+ };
530
+ }
531
+ function parseCurve(raw, index) {
532
+ if (!raw || typeof raw !== "object") {
533
+ throw new Error(`@doki-land/live2d: Curves[${index}] invalid`);
534
+ }
535
+ const c = raw;
536
+ const target = c.Target;
537
+ const id = c.Id;
538
+ if (typeof target !== "string" || typeof id !== "string") {
539
+ throw new Error(`@doki-land/live2d: Curves[${index}] needs Target/Id`);
540
+ }
541
+ if (target !== "Parameter" && target !== "PartOpacity" && target !== "Model") {
542
+ throw new Error(
543
+ `@doki-land/live2d: Curves[${index}] unknown Target ${target}`
544
+ );
545
+ }
546
+ const segmentsFlat = c.Segments;
547
+ if (!Array.isArray(segmentsFlat) || segmentsFlat.length < 2) {
548
+ throw new Error(`@doki-land/live2d: Curves[${index}] empty Segments`);
549
+ }
550
+ const numbers = segmentsFlat.map((n, j) => {
551
+ if (typeof n !== "number" || !Number.isFinite(n)) {
552
+ throw new Error(
553
+ `@doki-land/live2d: Curves[${index}].Segments[${j}] not a number`
554
+ );
555
+ }
556
+ return n;
557
+ });
558
+ return {
559
+ target,
560
+ id,
561
+ fadeInTime: optionalNum(c.FadeInTime),
562
+ fadeOutTime: optionalNum(c.FadeOutTime),
563
+ segments: parseSegments(numbers, index)
564
+ };
565
+ }
566
+ function parseSegments(flat, curveIndex) {
567
+ let i = 0;
568
+ const p0 = { time: flat[i++], value: flat[i++] };
569
+ const out = [];
570
+ let prev = p0;
571
+ while (i < flat.length) {
572
+ const kindId = flat[i++];
573
+ const kind = SEGMENT_KIND[kindId];
574
+ if (!kind) {
575
+ throw new Error(
576
+ `@doki-land/live2d: Curves[${curveIndex}] unknown segment ${kindId}`
577
+ );
578
+ }
579
+ if (kind === "bezier") {
580
+ if (i + 5 >= flat.length) {
581
+ throw new Error(
582
+ `@doki-land/live2d: Curves[${curveIndex}] truncated bezier`
583
+ );
584
+ }
585
+ const p1 = { time: flat[i++], value: flat[i++] };
586
+ const p2 = { time: flat[i++], value: flat[i++] };
587
+ const p3 = { time: flat[i++], value: flat[i++] };
588
+ out.push({ kind, p0: prev, p1, p2, p3 });
589
+ prev = p3;
590
+ } else {
591
+ if (i + 1 >= flat.length) {
592
+ throw new Error(
593
+ `@doki-land/live2d: Curves[${curveIndex}] truncated ${kind}`
594
+ );
595
+ }
596
+ const p3 = { time: flat[i++], value: flat[i++] };
597
+ out.push({ kind, p0: prev, p3 });
598
+ prev = p3;
599
+ }
600
+ }
601
+ return out;
602
+ }
603
+ function num(v, label) {
604
+ if (typeof v !== "number" || !Number.isFinite(v)) {
605
+ throw new Error(`@doki-land/live2d: motion3 ${label} must be a number`);
606
+ }
607
+ return v;
608
+ }
609
+ function optionalNum(v) {
610
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
611
+ }
612
+
613
+ // src/stage/actor-model-slot.ts
614
+ function lerp(a, b, t) {
615
+ return a + (b - a) * Math.min(1, Math.max(0, t));
616
+ }
617
+ var ActorModelSlot = class {
618
+ #backends;
619
+ #renderer;
620
+ #onProgress;
621
+ #motionPlayer;
622
+ #motionCache = /* @__PURE__ */ new Map();
623
+ #drawPass = null;
624
+ #model = null;
625
+ #backend = null;
626
+ #textures = [];
627
+ #resolver = null;
628
+ #loadGeneration = 0;
629
+ constructor(options) {
630
+ this.#backends = options.backends;
631
+ this.#renderer = options.renderer;
632
+ this.#onProgress = options.onProgress;
633
+ this.#motionPlayer = new MotionPlayer({
634
+ onStart: options.onMotionStart,
635
+ onFinish: options.onMotionFinish
636
+ });
637
+ }
638
+ get model() {
639
+ return this.#model;
640
+ }
641
+ get drawPass() {
642
+ return this.#drawPass;
643
+ }
644
+ ensureDrawPass() {
645
+ if (!this.#drawPass) {
646
+ this.#drawPass = this.#renderer.createModelDrawPass();
647
+ }
648
+ return this.#drawPass;
649
+ }
650
+ #report(payload) {
651
+ this.#onProgress?.(payload);
652
+ }
653
+ #clearTextures() {
654
+ if (this.#textures.length > 0) {
655
+ releaseTextureData(this.#textures);
656
+ this.#textures = [];
657
+ }
658
+ this.#drawPass?.setTextures([]);
659
+ }
660
+ #applyMotionSamples(samples) {
661
+ if (!this.#model || !this.#backend) return;
662
+ for (const s of samples) {
663
+ if (s.weight <= 0) continue;
664
+ if (s.target === "PartOpacity") {
665
+ if (!this.#backend.setPartOpacity) continue;
666
+ if (s.weight >= 1) {
667
+ this.#backend.setPartOpacity(this.#model, s.id, s.value);
668
+ } else {
669
+ const cur2 = 1;
670
+ this.#backend.setPartOpacity(
671
+ this.#model,
672
+ s.id,
673
+ cur2 + (s.value - cur2) * s.weight
674
+ );
675
+ }
676
+ continue;
677
+ }
678
+ if (s.target !== "Parameter" || !this.#backend.setParameter)
679
+ continue;
680
+ if (s.weight >= 1) {
681
+ this.#backend.setParameter(this.#model, s.id, s.value);
682
+ continue;
683
+ }
684
+ const cur = this.#backend.listParameters?.(this.#model).find((p) => p.id === s.id)?.value ?? s.value;
685
+ this.#backend.setParameter(
686
+ this.#model,
687
+ s.id,
688
+ cur + (s.value - cur) * s.weight
689
+ );
690
+ }
691
+ }
692
+ async load(source, resolver) {
693
+ const gen = ++this.#loadGeneration;
694
+ this.#report({
695
+ stage: "mounting",
696
+ progress: 0.01,
697
+ detail: "prepare draw pass"
698
+ });
699
+ const drawPass = this.ensureDrawPass();
700
+ this.#report({
701
+ stage: "resolve",
702
+ progress: 0.02,
703
+ detail: "resolve source"
704
+ });
705
+ let json;
706
+ let baseUrl;
707
+ let settingsUrl;
708
+ if (typeof source === "object" && source.kind === "json") {
709
+ json = source.json;
710
+ baseUrl = source.baseUrl;
711
+ settingsUrl = source.baseUrl;
712
+ this.#report({
713
+ stage: "settings",
714
+ progress: 0.2,
715
+ detail: "inline settings"
716
+ });
717
+ } else {
718
+ const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl(source) : source.url;
719
+ const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
720
+ const fetchUrl = resolveModelSourceUrl(raw, {
721
+ npmCdnBase: cdnBase
722
+ });
723
+ this.#report({
724
+ stage: "settings",
725
+ progress: 0.05,
726
+ detail: fetchUrl
727
+ });
728
+ json = await fetchModelJson(fetchUrl, (u) => {
729
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
730
+ this.#report({
731
+ stage: "settings",
732
+ progress: lerp(0.05, 0.22, ratio),
733
+ detail: fetchUrl,
734
+ bytesLoaded: u.bytesLoaded,
735
+ bytesTotal: u.bytesTotal
736
+ });
737
+ });
738
+ baseUrl = fetchUrl;
739
+ settingsUrl = fetchUrl;
740
+ }
741
+ if (gen !== this.#loadGeneration) {
742
+ throw new Error("@doki-land/live2d: load cancelled");
743
+ }
744
+ const settings = normalizeModelSettings(json, settingsUrl);
745
+ this.#report({
746
+ stage: "moc",
747
+ progress: 0.25,
748
+ detail: settings.moc
749
+ });
750
+ const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
751
+ onBytesProgress: (key, u) => {
752
+ const isMoc = key === settings.moc;
753
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
754
+ if (isMoc) {
755
+ this.#report({
756
+ stage: "moc",
757
+ progress: lerp(0.25, 0.8, ratio),
758
+ detail: key,
759
+ bytesLoaded: u.bytesLoaded,
760
+ bytesTotal: u.bytesTotal
761
+ });
762
+ } else {
763
+ this.#report({
764
+ stage: "textures",
765
+ progress: lerp(0.8, 0.9, ratio),
766
+ detail: key,
767
+ bytesLoaded: u.bytesLoaded,
768
+ bytesTotal: u.bytesTotal
769
+ });
770
+ }
771
+ }
772
+ });
773
+ this.#resolver = assetResolver;
774
+ this.#motionPlayer.clear();
775
+ this.#motionCache.clear();
776
+ const backend = selectModelBackend([...this.#backends], json);
777
+ this.#report({
778
+ stage: "decode",
779
+ progress: 0.85,
780
+ detail: `decode ${settings.format}`
781
+ });
782
+ const next = await backend.createModel(settings, {
783
+ renderer: this.#renderer,
784
+ resolver: assetResolver
785
+ });
786
+ if (gen !== this.#loadGeneration) {
787
+ backend.destroyModel(next);
788
+ throw new Error("@doki-land/live2d: load cancelled");
789
+ }
790
+ this.#clearTextures();
791
+ if (settings.textures.length > 0) {
792
+ this.#report({
793
+ stage: "textures",
794
+ progress: 0.88,
795
+ detail: `${settings.textures.length} textures`
796
+ });
797
+ const textures = await loadTextureData(
798
+ assetResolver,
799
+ settings.textures,
800
+ {
801
+ onProgress: (u) => {
802
+ const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
803
+ this.#report({
804
+ stage: "textures",
805
+ progress: lerp(0.88, 0.96, ratio),
806
+ detail: u.key,
807
+ bytesLoaded: u.bytesLoaded,
808
+ bytesTotal: u.bytesTotal
809
+ });
810
+ }
811
+ }
812
+ );
813
+ if (gen !== this.#loadGeneration) {
814
+ releaseTextureData(textures);
815
+ backend.destroyModel(next);
816
+ throw new Error("@doki-land/live2d: load cancelled");
817
+ }
818
+ this.#textures = textures;
819
+ drawPass.setTextures(textures);
820
+ }
821
+ if (this.#model && this.#backend) {
822
+ this.#backend.destroyModel(this.#model);
823
+ }
824
+ this.#model = next;
825
+ this.#backend = backend;
826
+ this.#report({
827
+ stage: "ready",
828
+ progress: 1,
829
+ detail: next.id
830
+ });
831
+ return next;
832
+ }
833
+ setParameter(id, value) {
834
+ if (!this.#model || !this.#backend?.setParameter) return;
835
+ this.#backend.setParameter(this.#model, id, value);
836
+ }
837
+ listParameters() {
838
+ if (!this.#model || !this.#backend?.listParameters) return [];
839
+ return this.#backend.listParameters(this.#model);
840
+ }
841
+ listMotionGroups() {
842
+ return this.#model?.settings.motionGroups ?? {};
843
+ }
844
+ async playMotion(group, index = 0, options = {}) {
845
+ if (!this.#model || !this.#resolver) return false;
846
+ const list = this.#model.settings.motionGroups[group];
847
+ const def = list?.[index];
848
+ if (!def) return false;
849
+ let clip = this.#motionCache.get(def.file);
850
+ if (!clip) {
851
+ const json = await this.#resolver.fetchJson(def.file);
852
+ clip = parseMotion3(json);
853
+ this.#motionCache.set(def.file, clip);
854
+ }
855
+ const fadeInTime = options.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
856
+ const fadeOutTime = options.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
857
+ return this.#motionPlayer.start(group, index, clip, {
858
+ priority: options.priority ?? MotionPriority.normal,
859
+ slot: options.slot,
860
+ queue: options.queue,
861
+ loop: options.loop,
862
+ fadeInTime,
863
+ fadeOutTime
864
+ });
865
+ }
866
+ stopMotion(opts) {
867
+ this.#motionPlayer.stop(opts?.fade !== false, opts?.slot);
868
+ }
869
+ listPlayingMotions() {
870
+ return this.#motionPlayer.listPlaying();
871
+ }
872
+ update(deltaTimeSeconds) {
873
+ if (!this.#model || !this.#backend || !this.#drawPass) return null;
874
+ this.#applyMotionSamples(this.#motionPlayer.update(deltaTimeSeconds));
875
+ this.#backend.updateModel(this.#model, deltaTimeSeconds);
876
+ return this.#backend.getDrawables(this.#model);
877
+ }
878
+ hitTestModelCoords(modelX, modelY) {
879
+ if (!this.#model || !this.#backend) return null;
880
+ const drawables = this.#backend.getDrawables(this.#model);
881
+ for (let n = drawables.length - 1; n >= 0; n -= 1) {
882
+ const d = drawables[n];
883
+ if (!d.visible || d.opacity <= 0) continue;
884
+ const p = d.vertexPositions;
885
+ const idx = d.indices;
886
+ for (let i = 0; i + 2 < idx.length; i += 3) {
887
+ const a = idx[i] * 2, b = idx[i + 1] * 2, c = idx[i + 2] * 2;
888
+ const ax = p[a], ay = p[a + 1];
889
+ const bx = p[b], by = p[b + 1];
890
+ const cx = p[c], cy = p[c + 1];
891
+ const s = (ax - cx) * (modelY - cy) - (ay - cy) * (modelX - cx);
892
+ const s1 = (bx - ax) * (modelY - ay) - (by - ay) * (modelX - ax);
893
+ const s2 = (cx - bx) * (modelY - by) - (cy - by) * (modelX - bx);
894
+ if (s >= 0 && s1 >= 0 && s2 >= 0 || s <= 0 && s1 <= 0 && s2 <= 0) {
895
+ const hitArea = this.#model.settings.hitAreas.find(
896
+ (h) => h.id === `D_${d.index}` || h.id === `${d.index}`
897
+ );
898
+ return hitArea?.name ?? `drawable:${d.index}`;
899
+ }
900
+ }
901
+ }
902
+ return null;
903
+ }
904
+ destroy() {
905
+ this.#loadGeneration += 1;
906
+ this.#motionPlayer.clear();
907
+ this.#motionCache.clear();
908
+ this.#resolver = null;
909
+ if (this.#model && this.#backend) {
910
+ this.#backend.destroyModel(this.#model);
911
+ }
912
+ this.#model = null;
913
+ this.#backend = null;
914
+ this.#clearTextures();
915
+ this.#drawPass?.destroy();
916
+ this.#drawPass = null;
917
+ }
918
+ };
919
+
920
+ // src/stage/transform.ts
921
+ import { DEFAULT_ACTOR_TRANSFORM } from "@doki-land/live2d-core";
922
+ function resolveActorTransform(patch) {
923
+ const base = DEFAULT_ACTOR_TRANSFORM;
924
+ const scale = patch?.scale ?? base.scale ?? 1;
925
+ return {
926
+ x: patch?.x ?? base.x,
927
+ y: patch?.y ?? base.y,
928
+ scale,
929
+ scaleX: patch?.scaleX ?? patch?.scale ?? scale,
930
+ scaleY: patch?.scaleY ?? patch?.scale ?? scale,
931
+ rotation: patch?.rotation ?? base.rotation ?? 0,
932
+ anchorX: patch?.anchorX ?? base.anchorX ?? 0.5,
933
+ anchorY: patch?.anchorY ?? base.anchorY ?? 1
934
+ };
935
+ }
936
+ function modelNdcToStage(modelX, modelY, transform) {
937
+ const scaleX = transform.scaleX ?? transform.scale ?? 1;
938
+ const scaleY = transform.scaleY ?? transform.scale ?? 1;
939
+ const anchorX = transform.anchorX ?? 0.5;
940
+ const anchorY = transform.anchorY ?? 1;
941
+ const rotation = transform.rotation ?? 0;
942
+ const anchorModelX = -1 + anchorX * 2;
943
+ const anchorModelY = 1 - anchorY * 2;
944
+ let localX = (modelX - anchorModelX) * scaleX * 0.5;
945
+ let localY = (anchorModelY - modelY) * scaleY * 0.5;
946
+ if (rotation !== 0) {
947
+ const c = Math.cos(rotation);
948
+ const s = Math.sin(rotation);
949
+ const rx = localX * c - localY * s;
950
+ const ry = localX * s + localY * c;
951
+ localX = rx;
952
+ localY = ry;
953
+ }
954
+ return {
955
+ stageX: transform.x + localX,
956
+ stageY: transform.y + localY
957
+ };
958
+ }
959
+ function stageToModelNdc(stageX, stageY, transform) {
960
+ const scaleX = transform.scaleX ?? transform.scale ?? 1;
961
+ const scaleY = transform.scaleY ?? transform.scale ?? 1;
962
+ const anchorX = transform.anchorX ?? 0.5;
963
+ const anchorY = transform.anchorY ?? 1;
964
+ const anchorModelX = -1 + anchorX * 2;
965
+ const anchorModelY = 1 - anchorY * 2;
966
+ const localX = (stageX - transform.x) / (scaleX * 0.5);
967
+ const localY = (stageY - transform.y) / (scaleY * 0.5);
968
+ return {
969
+ modelX: localX + anchorModelX,
970
+ modelY: anchorModelY - localY
971
+ };
972
+ }
973
+ function clientToStage(clientX, clientY, canvas) {
974
+ const rect = canvas.getBoundingClientRect();
975
+ const x = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;
976
+ const y = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
977
+ return {
978
+ stageX: Math.min(1, Math.max(0, x)),
979
+ stageY: Math.min(1, Math.max(0, y))
980
+ };
981
+ }
982
+ function stageFocusDrag(stageX, stageY, transform) {
983
+ const dx = stageX - transform.x;
984
+ const dy = transform.y - stageY;
985
+ const dragX = Math.min(1, Math.max(-1, dx * 2));
986
+ const dragY = Math.min(1, Math.max(-1, dy * 2));
987
+ return { dragX, dragY };
988
+ }
989
+ function layerOrderIndex(layer, definedLayers) {
990
+ const idx = definedLayers.indexOf(layer);
991
+ return idx >= 0 ? idx : definedLayers.length;
992
+ }
993
+ function compareActorsForDraw(a, b, definedLayers) {
994
+ const la = layerOrderIndex(a.layer, definedLayers);
995
+ const lb = layerOrderIndex(b.layer, definedLayers);
996
+ return la - lb || a.order - b.order || a.creationIndex - b.creationIndex;
997
+ }
998
+ function compareActorsForHit(a, b, definedLayers) {
999
+ return compareActorsForDraw(b, a, definedLayers);
1000
+ }
1001
+ function transformDrawablesForStage(drawables, transform, opacity) {
1002
+ const alpha = Math.min(1, Math.max(0, opacity));
1003
+ return drawables.map((d) => {
1004
+ if (!d.visible || alpha <= 0) return { ...d, visible: false };
1005
+ const pos = new Float32Array(d.vertexPositions.length);
1006
+ for (let i = 0; i < pos.length; i += 2) {
1007
+ const { stageX, stageY } = modelNdcToStage(
1008
+ d.vertexPositions[i],
1009
+ d.vertexPositions[i + 1],
1010
+ transform
1011
+ );
1012
+ pos[i] = stageX * 2 - 1;
1013
+ pos[i + 1] = 1 - stageY * 2;
1014
+ }
1015
+ return {
1016
+ ...d,
1017
+ vertexPositions: pos,
1018
+ opacity: d.opacity * alpha
1019
+ };
1020
+ });
1021
+ }
1022
+
1023
+ // src/stage/actor.ts
1024
+ var nextActorId = 0;
1025
+ var Live2dActorImpl = class {
1026
+ id;
1027
+ creationIndex;
1028
+ #transform;
1029
+ #visible = true;
1030
+ #opacity = 1;
1031
+ #layer = "characters";
1032
+ #order = 0;
1033
+ #destroyed = false;
1034
+ #lastDrawables = null;
1035
+ #slot;
1036
+ constructor(options, shared) {
1037
+ this.id = options?.id ?? shared.id;
1038
+ this.creationIndex = shared.creationIndex;
1039
+ this.#transform = resolveActorTransform(options?.transform);
1040
+ this.#visible = options?.visible ?? true;
1041
+ this.#opacity = options?.opacity ?? 1;
1042
+ this.#layer = options?.layer ?? "characters";
1043
+ this.#order = options?.order ?? 0;
1044
+ this.#slot = new ActorModelSlot({
1045
+ backends: shared.backends,
1046
+ renderer: shared.renderer
1047
+ });
1048
+ }
1049
+ get model() {
1050
+ return this.#slot.model;
1051
+ }
1052
+ get visible() {
1053
+ return this.#visible;
1054
+ }
1055
+ set visible(value) {
1056
+ this.#visible = value;
1057
+ }
1058
+ get opacity() {
1059
+ return this.#opacity;
1060
+ }
1061
+ set opacity(value) {
1062
+ this.#opacity = Math.min(1, Math.max(0, value));
1063
+ }
1064
+ get layer() {
1065
+ return this.#layer;
1066
+ }
1067
+ set layer(value) {
1068
+ this.#layer = value;
1069
+ }
1070
+ get order() {
1071
+ return this.#order;
1072
+ }
1073
+ set order(value) {
1074
+ this.#order = value;
1075
+ }
1076
+ getTransform() {
1077
+ return { ...this.#transform };
1078
+ }
1079
+ setTransform(patch) {
1080
+ this.#transform = resolveActorTransform({
1081
+ ...this.#transform,
1082
+ ...patch
1083
+ });
1084
+ }
1085
+ async load(source, resolver) {
1086
+ if (this.#destroyed) {
1087
+ throw new Error("@doki-land/live2d: actor destroyed");
1088
+ }
1089
+ return await this.#slot.load(source, resolver);
1090
+ }
1091
+ setParameter(id, value) {
1092
+ this.#slot.setParameter(id, value);
1093
+ }
1094
+ lookAt(stageX, stageY) {
1095
+ const { dragX, dragY } = stageFocusDrag(
1096
+ stageX,
1097
+ stageY,
1098
+ this.#transform
1099
+ );
1100
+ for (const u of focusParameterUpdates(
1101
+ this.#slot.listParameters(),
1102
+ dragX,
1103
+ dragY
1104
+ )) {
1105
+ this.#slot.setParameter(u.id, u.value);
1106
+ }
1107
+ }
1108
+ /** Internal: evaluate motion/physics and cache drawables for render. */
1109
+ update(deltaTimeSeconds) {
1110
+ if (!this.#visible || this.#opacity <= 0) {
1111
+ this.#lastDrawables = null;
1112
+ return null;
1113
+ }
1114
+ this.#lastDrawables = this.#slot.update(deltaTimeSeconds);
1115
+ return this.#lastDrawables;
1116
+ }
1117
+ get lastDrawables() {
1118
+ return this.#lastDrawables;
1119
+ }
1120
+ get slot() {
1121
+ return this.#slot;
1122
+ }
1123
+ hitTestStage(stageX, stageY) {
1124
+ if (!this.#visible || this.#opacity <= 0 || !this.#slot.model)
1125
+ return null;
1126
+ const { modelX, modelY } = stageToModelNdc(
1127
+ stageX,
1128
+ stageY,
1129
+ this.#transform
1130
+ );
1131
+ const area = this.#slot.hitTestModelCoords(modelX, modelY);
1132
+ if (!area) return null;
1133
+ const drawableMatch = /^drawable:(\d+)$/.exec(area);
1134
+ return {
1135
+ actorId: this.id,
1136
+ area,
1137
+ drawableIndex: drawableMatch ? Number(drawableMatch[1]) : -1,
1138
+ stageX,
1139
+ stageY,
1140
+ localX: modelX,
1141
+ localY: modelY
1142
+ };
1143
+ }
1144
+ destroy() {
1145
+ if (this.#destroyed) return;
1146
+ this.#destroyed = true;
1147
+ this.#slot.destroy();
1148
+ }
1149
+ };
1150
+ function allocateActorId(prefix = "actor") {
1151
+ nextActorId += 1;
1152
+ return `${prefix}-${nextActorId}`;
1153
+ }
1154
+
1155
+ // src/stage/single-facade.ts
1156
+ import { EventEmitter } from "@doki-land/live2d-core";
1157
+ function nowMs() {
1158
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1159
+ }
1160
+ function createSingleActorFacade(stage, actor, backends) {
1161
+ const events = new EventEmitter();
1162
+ let canvas = null;
1163
+ let phase = "idle";
1164
+ let lastError = null;
1165
+ let generation = 0;
1166
+ let fpsSmooth = 0;
1167
+ const setPhase = (next) => {
1168
+ phase = next;
1169
+ events.emit("phase", { phase, generation });
1170
+ };
1171
+ const state = () => ({
1172
+ phase,
1173
+ lastError,
1174
+ generation
1175
+ });
1176
+ const runtime = {
1177
+ events,
1178
+ backends,
1179
+ renderer: stage.renderer,
1180
+ stage,
1181
+ actor,
1182
+ get model() {
1183
+ return actor.model;
1184
+ },
1185
+ get state() {
1186
+ return state();
1187
+ },
1188
+ mount(target) {
1189
+ generation += 1;
1190
+ canvas = target;
1191
+ setPhase("mounting");
1192
+ void stage.mount(target).then(
1193
+ () => setPhase(actor.model ? "live" : "ready"),
1194
+ (err) => {
1195
+ lastError = err;
1196
+ setPhase("error");
1197
+ events.emit("error", { error: err });
1198
+ }
1199
+ );
1200
+ },
1201
+ async loadModel(source, resolver) {
1202
+ setPhase("loading");
1203
+ try {
1204
+ const model = await actor.load(source, resolver);
1205
+ lastError = null;
1206
+ setPhase("live");
1207
+ events.emit("ready", { modelId: model.id });
1208
+ return model;
1209
+ } catch (err) {
1210
+ lastError = err;
1211
+ setPhase("error");
1212
+ events.emit("error", { error: err });
1213
+ throw err;
1214
+ }
1215
+ },
1216
+ captureFrame() {
1217
+ return null;
1218
+ },
1219
+ setParameter(id, value) {
1220
+ actor.setParameter(id, value);
1221
+ },
1222
+ hitTest(x, y) {
1223
+ const stageX = (x + 1) / 2;
1224
+ const stageY = (1 - y) / 2;
1225
+ const hit = stage.hitTest(stageX, stageY);
1226
+ if (!hit || hit.actorId !== actor.id) return null;
1227
+ return hit.area;
1228
+ },
1229
+ listParameters() {
1230
+ return actor.slot.listParameters();
1231
+ },
1232
+ listMotionGroups() {
1233
+ return actor.slot.listMotionGroups();
1234
+ },
1235
+ playMotion(group, index, options) {
1236
+ return actor.slot.playMotion(group, index, options);
1237
+ },
1238
+ stopMotion(opts) {
1239
+ actor.slot.stopMotion(opts);
1240
+ },
1241
+ listPlayingMotions() {
1242
+ return actor.slot.listPlayingMotions();
1243
+ },
1244
+ async capturePng(opts = {}) {
1245
+ if (!canvas) {
1246
+ throw new Error(
1247
+ "@doki-land/live2d: mount(canvas) before capturePng"
1248
+ );
1249
+ }
1250
+ if (phase === "live") {
1251
+ runtime.update(0);
1252
+ }
1253
+ const mime = opts.mimeType ?? "image/png";
1254
+ return await new Promise((resolve, reject) => {
1255
+ canvas.toBlob(
1256
+ (blob) => {
1257
+ if (blob) resolve(blob);
1258
+ else
1259
+ reject(
1260
+ new Error(
1261
+ "@doki-land/live2d: canvas.toBlob returned null"
1262
+ )
1263
+ );
1264
+ },
1265
+ mime,
1266
+ opts.quality
1267
+ );
1268
+ });
1269
+ },
1270
+ update(deltaTimeSeconds) {
1271
+ if (phase !== "live" && phase !== "ready") return;
1272
+ const t0 = nowMs();
1273
+ stage.update(deltaTimeSeconds);
1274
+ stage.render();
1275
+ const t1 = nowMs();
1276
+ const drawables = actor.lastDrawables ?? [];
1277
+ let vertexCount = 0;
1278
+ let indexCount = 0;
1279
+ for (const d of drawables) {
1280
+ vertexCount += d.vertexPositions.length / 2;
1281
+ indexCount += d.indices.length;
1282
+ }
1283
+ const frameMs = t1 - t0;
1284
+ const fps = deltaTimeSeconds > 0 ? 1 / deltaTimeSeconds : 0;
1285
+ fpsSmooth = fpsSmooth <= 0 ? fps : fpsSmooth * 0.85 + fps * 0.15;
1286
+ events.emit("profile", {
1287
+ fps,
1288
+ fpsSmooth,
1289
+ frameMs,
1290
+ evaluateMs: frameMs,
1291
+ drawMs: 0,
1292
+ drawableCount: drawables.length,
1293
+ vertexCount,
1294
+ indexCount
1295
+ });
1296
+ },
1297
+ destroy() {
1298
+ generation += 1;
1299
+ stage.destroy();
1300
+ canvas = null;
1301
+ setPhase("destroyed");
1302
+ events.clear();
1303
+ }
1304
+ };
1305
+ return runtime;
1306
+ }
1307
+
1308
+ // src/stage/stage.ts
1309
+ import {
1310
+ createMoc2Backend,
1311
+ createMoc3Backend,
1312
+ createRenderer
1313
+ } from "@doki-land/live2d-renderer";
1314
+ var Live2dStageImpl = class {
1315
+ #backends;
1316
+ #renderer;
1317
+ #updateMode;
1318
+ #actors = /* @__PURE__ */ new Map();
1319
+ #definedLayers = [
1320
+ "background",
1321
+ "characters-back",
1322
+ "characters",
1323
+ "characters-front",
1324
+ "effects"
1325
+ ];
1326
+ #pointerListeners = /* @__PURE__ */ new Map([
1327
+ ["pointerdown", /* @__PURE__ */ new Set()],
1328
+ ["pointermove", /* @__PURE__ */ new Set()],
1329
+ ["pointerup", /* @__PURE__ */ new Set()]
1330
+ ]);
1331
+ #canvas = null;
1332
+ #initPromise = null;
1333
+ #rafId = null;
1334
+ #running = false;
1335
+ #paused = false;
1336
+ #lastFrameMs = 0;
1337
+ #creationCounter = 0;
1338
+ #focusedActorId = null;
1339
+ #lastPointer = null;
1340
+ #pointerTracking = { mode: "focused" };
1341
+ #boundPointerDown;
1342
+ #boundPointerMove;
1343
+ #boundPointerUp;
1344
+ #destroyed = false;
1345
+ constructor(options = {}) {
1346
+ this.#backends = options.backends ?? [
1347
+ createMoc2Backend(),
1348
+ createMoc3Backend()
1349
+ ];
1350
+ this.#renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
1351
+ this.#updateMode = options.updateMode ?? "auto";
1352
+ }
1353
+ get actors() {
1354
+ return [...this.#actors.values()];
1355
+ }
1356
+ get pointerTracking() {
1357
+ return this.#pointerTracking;
1358
+ }
1359
+ set pointerTracking(policy) {
1360
+ this.#pointerTracking = policy;
1361
+ }
1362
+ get renderer() {
1363
+ return this.#renderer;
1364
+ }
1365
+ async mount(canvas) {
1366
+ if (this.#destroyed) {
1367
+ throw new Error("@doki-land/live2d: stage destroyed");
1368
+ }
1369
+ this.#canvas = canvas;
1370
+ this.#initPromise = this.#renderer.initialize(canvas);
1371
+ await this.#initPromise;
1372
+ this.#attachPointerListeners(canvas);
1373
+ }
1374
+ createActor(options) {
1375
+ if (this.#destroyed) {
1376
+ throw new Error("@doki-land/live2d: stage destroyed");
1377
+ }
1378
+ const id = options?.id ?? allocateActorId();
1379
+ if (this.#actors.has(id)) {
1380
+ throw new Error(`@doki-land/live2d: duplicate actor id "${id}"`);
1381
+ }
1382
+ const creationIndex = this.#creationCounter++;
1383
+ const actor = new Live2dActorImpl(options, {
1384
+ id,
1385
+ creationIndex,
1386
+ backends: this.#backends,
1387
+ renderer: this.#renderer
1388
+ });
1389
+ this.#actors.set(id, actor);
1390
+ if (!this.#focusedActorId) {
1391
+ this.#focusedActorId = id;
1392
+ }
1393
+ return actor;
1394
+ }
1395
+ removeActor(actorOrId) {
1396
+ const id = typeof actorOrId === "string" ? actorOrId : actorOrId.id;
1397
+ const actor = this.#actors.get(id);
1398
+ if (!actor) return;
1399
+ actor.destroy();
1400
+ this.#actors.delete(id);
1401
+ if (this.#focusedActorId === id) {
1402
+ this.#focusedActorId = this.#actors.keys().next().value ?? null;
1403
+ }
1404
+ }
1405
+ defineLayers(layers) {
1406
+ this.#definedLayers.length = 0;
1407
+ this.#definedLayers.push(...layers);
1408
+ }
1409
+ update(deltaTimeSeconds) {
1410
+ if (this.#destroyed) return;
1411
+ for (const actor of this.#actors.values()) {
1412
+ actor.update(deltaTimeSeconds);
1413
+ }
1414
+ this.#applyPointerTracking();
1415
+ }
1416
+ render() {
1417
+ if (this.#destroyed || !this.#canvas) return;
1418
+ const sorted = [...this.#actors.values()].sort(
1419
+ (a, b) => compareActorsForDraw(a, b, this.#definedLayers)
1420
+ );
1421
+ this.#renderer.beginFrame();
1422
+ for (const actor of sorted) {
1423
+ if (!actor.visible || actor.opacity <= 0) continue;
1424
+ const drawables = actor.lastDrawables;
1425
+ const pass = actor.slot.drawPass;
1426
+ if (!drawables || !pass) continue;
1427
+ const placed = transformDrawablesForStage(
1428
+ drawables,
1429
+ actor.getTransform(),
1430
+ actor.opacity
1431
+ );
1432
+ pass.draw(placed, new Float32Array(16));
1433
+ }
1434
+ this.#renderer.endFrame();
1435
+ }
1436
+ start() {
1437
+ if (this.#updateMode === "manual") {
1438
+ throw new Error(
1439
+ "@doki-land/live2d: start() is not available when updateMode is manual"
1440
+ );
1441
+ }
1442
+ if (this.#running) return;
1443
+ this.#running = true;
1444
+ this.#paused = false;
1445
+ this.#lastFrameMs = nowMs2();
1446
+ const tick = () => {
1447
+ if (!this.#running) return;
1448
+ if (!this.#paused) {
1449
+ const t = nowMs2();
1450
+ const dt = Math.min(0.1, (t - this.#lastFrameMs) / 1e3);
1451
+ this.#lastFrameMs = t;
1452
+ this.update(dt);
1453
+ this.render();
1454
+ }
1455
+ this.#rafId = requestAnimationFrame(tick);
1456
+ };
1457
+ this.#rafId = requestAnimationFrame(tick);
1458
+ }
1459
+ pause() {
1460
+ this.#paused = true;
1461
+ }
1462
+ resume() {
1463
+ this.#paused = false;
1464
+ this.#lastFrameMs = nowMs2();
1465
+ }
1466
+ stop() {
1467
+ this.#running = false;
1468
+ this.#paused = false;
1469
+ if (this.#rafId !== null) {
1470
+ cancelAnimationFrame(this.#rafId);
1471
+ this.#rafId = null;
1472
+ }
1473
+ }
1474
+ hitTest(stageX, stageY) {
1475
+ const hits = this.hitTestAll(stageX, stageY);
1476
+ return hits[0] ?? null;
1477
+ }
1478
+ hitTestAll(stageX, stageY) {
1479
+ const sorted = [...this.#actors.values()].sort(
1480
+ (a, b) => compareActorsForHit(a, b, this.#definedLayers)
1481
+ );
1482
+ const hits = [];
1483
+ for (const actor of sorted) {
1484
+ const partial = actor.hitTestStage(stageX, stageY);
1485
+ if (!partial) continue;
1486
+ hits.push({ ...partial, actor });
1487
+ }
1488
+ return hits;
1489
+ }
1490
+ addEventListener(type, listener) {
1491
+ this.#pointerListeners.get(type)?.add(listener);
1492
+ }
1493
+ removeEventListener(type, listener) {
1494
+ this.#pointerListeners.get(type)?.delete(listener);
1495
+ }
1496
+ destroy() {
1497
+ if (this.#destroyed) return;
1498
+ this.#destroyed = true;
1499
+ this.stop();
1500
+ this.#detachPointerListeners();
1501
+ for (const actor of this.#actors.values()) {
1502
+ actor.destroy();
1503
+ }
1504
+ this.#actors.clear();
1505
+ this.#renderer.destroy();
1506
+ this.#canvas = null;
1507
+ this.#initPromise = null;
1508
+ for (const set of this.#pointerListeners.values()) {
1509
+ set.clear();
1510
+ }
1511
+ }
1512
+ #attachPointerListeners(canvas) {
1513
+ this.#detachPointerListeners();
1514
+ this.#boundPointerDown = (e) => this.#onPointer("pointerdown", e);
1515
+ this.#boundPointerMove = (e) => this.#onPointer("pointermove", e);
1516
+ this.#boundPointerUp = (e) => this.#onPointer("pointerup", e);
1517
+ canvas.addEventListener("pointerdown", this.#boundPointerDown);
1518
+ canvas.addEventListener("pointermove", this.#boundPointerMove);
1519
+ canvas.addEventListener("pointerup", this.#boundPointerUp);
1520
+ }
1521
+ #detachPointerListeners() {
1522
+ if (!this.#canvas) return;
1523
+ if (this.#boundPointerDown) {
1524
+ this.#canvas.removeEventListener(
1525
+ "pointerdown",
1526
+ this.#boundPointerDown
1527
+ );
1528
+ }
1529
+ if (this.#boundPointerMove) {
1530
+ this.#canvas.removeEventListener(
1531
+ "pointermove",
1532
+ this.#boundPointerMove
1533
+ );
1534
+ }
1535
+ if (this.#boundPointerUp) {
1536
+ this.#canvas.removeEventListener("pointerup", this.#boundPointerUp);
1537
+ }
1538
+ this.#boundPointerDown = void 0;
1539
+ this.#boundPointerMove = void 0;
1540
+ this.#boundPointerUp = void 0;
1541
+ }
1542
+ #onPointer(type, event) {
1543
+ if (!this.#canvas) return;
1544
+ const { stageX, stageY } = clientToStage(
1545
+ event.clientX,
1546
+ event.clientY,
1547
+ this.#canvas
1548
+ );
1549
+ this.#lastPointer = { stageX, stageY };
1550
+ const hit = this.hitTest(stageX, stageY);
1551
+ if (type === "pointerdown" && hit) {
1552
+ this.#focusedActorId = hit.actorId;
1553
+ }
1554
+ const payload = {
1555
+ actor: hit?.actor ?? null,
1556
+ actorId: hit?.actorId ?? null,
1557
+ area: hit?.area ?? null,
1558
+ stageX,
1559
+ stageY,
1560
+ clientX: event.clientX,
1561
+ clientY: event.clientY,
1562
+ hit
1563
+ };
1564
+ for (const listener of this.#pointerListeners.get(type) ?? []) {
1565
+ listener(payload);
1566
+ }
1567
+ }
1568
+ #applyPointerTracking() {
1569
+ if (!this.#canvas || !this.#lastPointer) return;
1570
+ const { stageX, stageY } = this.#lastPointer;
1571
+ const mode = this.#pointerTracking.mode;
1572
+ if (mode === "none") return;
1573
+ if (mode === "all") {
1574
+ for (const actor of this.#actors.values()) {
1575
+ actor.lookAt(stageX, stageY);
1576
+ }
1577
+ return;
1578
+ }
1579
+ if (mode === "custom" && this.#pointerTracking.targetActorId) {
1580
+ const actor = this.#actors.get(this.#pointerTracking.targetActorId);
1581
+ actor?.lookAt(stageX, stageY);
1582
+ return;
1583
+ }
1584
+ if (mode === "hovered") {
1585
+ for (const actor of this.#actors.values()) {
1586
+ if (actor.hitTestStage(stageX, stageY)) {
1587
+ actor.lookAt(stageX, stageY);
1588
+ }
1589
+ }
1590
+ return;
1591
+ }
1592
+ if (mode === "nearest") {
1593
+ let best = null;
1594
+ let bestDist = Number.POSITIVE_INFINITY;
1595
+ for (const actor of this.#actors.values()) {
1596
+ const t = actor.getTransform();
1597
+ const dx = t.x - stageX;
1598
+ const dy = t.y - stageY;
1599
+ const dist = dx * dx + dy * dy;
1600
+ if (dist < bestDist) {
1601
+ bestDist = dist;
1602
+ best = actor;
1603
+ }
1604
+ }
1605
+ best?.lookAt(stageX, stageY);
1606
+ return;
1607
+ }
1608
+ if (mode === "focused" && this.#focusedActorId) {
1609
+ this.#actors.get(this.#focusedActorId)?.lookAt(stageX, stageY);
1610
+ }
1611
+ }
1612
+ };
1613
+ function nowMs2() {
1614
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1615
+ }
1616
+ function createLive2dStage(options) {
1617
+ return new Live2dStageImpl(options);
1618
+ }
1619
+
1620
+ // src/create-live2d.ts
1621
+ function createLive2D(options = {}) {
1622
+ const backends = options.backends ?? [
1623
+ createMoc2Backend2(),
1624
+ createMoc3Backend2()
1625
+ ];
1626
+ const stage = createLive2dStage({
1627
+ backends,
1628
+ renderer: options.renderer ?? createRenderer2({ prefer: options.prefer }),
1629
+ updateMode: options.updateMode ?? "manual"
1630
+ });
1631
+ const actor = stage.createActor({
1632
+ id: allocateActorId("default")
1633
+ });
1634
+ return createSingleActorFacade(stage, actor, backends);
1635
+ }
1636
+
1637
+ // src/index.ts
1638
+ var LIVE2D_VERSION = "0.0.0";
1639
+ export {
1640
+ DEFAULT_ACTOR_TRANSFORM2 as DEFAULT_ACTOR_TRANSFORM,
1641
+ DEFAULT_NPM_CDN,
1642
+ EventEmitter2 as EventEmitter,
1643
+ LIVE2D_VERSION,
1644
+ MotionPlayer,
1645
+ MotionPriority,
1646
+ blendMotionLayers,
1647
+ createCanvas2DRenderer,
1648
+ createLive2D,
1649
+ createLive2dStage,
1650
+ createMoc2Backend3 as createMoc2Backend,
1651
+ createMoc3Backend3 as createMoc3Backend,
1652
+ createQuadProgram,
1653
+ createRenderer3 as createRenderer,
1654
+ createWebGl2Renderer,
1655
+ createWebGpuRenderer,
1656
+ decodeMoc3,
1657
+ evaluateCurve,
1658
+ evaluateFrame,
1659
+ evaluateMotion3,
1660
+ fingerprintSnapshot,
1661
+ focusParameterUpdates,
1662
+ parseCpuProgram,
1663
+ parseMotion3,
1664
+ resolveModelSourceUrl2 as resolveModelSourceUrl,
1665
+ resolveNpmSpecifier,
1666
+ serializeCpuProgram
1667
+ };