@doki-land/live2d 0.0.9 → 0.0.11

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.js ADDED
@@ -0,0 +1,1068 @@
1
+ // src/index.ts
2
+ import { 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 createMoc2Backend2,
11
+ createMoc3Backend as createMoc3Backend2,
12
+ createQuadProgram,
13
+ createRenderer as createRenderer2,
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 { EventEmitter, modelSourceUrl } from "@doki-land/live2d-core";
25
+ import {
26
+ createUrlAssetResolver,
27
+ fetchModelJson,
28
+ normalizeModelSettings,
29
+ resolveModelSourceUrl
30
+ } from "@doki-land/live2d-loader";
31
+ import {
32
+ createMoc2Backend,
33
+ createMoc3Backend,
34
+ createRenderer,
35
+ selectModelBackend
36
+ } from "@doki-land/live2d-renderer";
37
+
38
+ // src/load-textures.ts
39
+ function guessMime(path) {
40
+ const lower = path.toLowerCase();
41
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
42
+ if (lower.endsWith(".webp")) return "image/webp";
43
+ if (lower.endsWith(".gif")) return "image/gif";
44
+ return "image/png";
45
+ }
46
+ async function bytesToImageBitmap(bytes, path) {
47
+ if (typeof createImageBitmap !== "function") {
48
+ throw new Error(
49
+ "@doki-land/live2d: createImageBitmap is not available in this environment"
50
+ );
51
+ }
52
+ const blob = new Blob([new Uint8Array(bytes)], {
53
+ type: guessMime(path)
54
+ });
55
+ return createImageBitmap(blob);
56
+ }
57
+ async function loadTextureData(resolver, paths, options = {}) {
58
+ const out = [];
59
+ const total = paths.length;
60
+ for (let i = 0; i < paths.length; i++) {
61
+ const key = paths[i];
62
+ options.onProgress?.({
63
+ index: i,
64
+ total,
65
+ key,
66
+ bytesLoaded: 0,
67
+ bytesTotal: null
68
+ });
69
+ const bytes = await resolver.fetchBytes(key);
70
+ options.onProgress?.({
71
+ index: i,
72
+ total,
73
+ key,
74
+ bytesLoaded: bytes.byteLength,
75
+ bytesTotal: bytes.byteLength
76
+ });
77
+ const image = await bytesToImageBitmap(bytes, key);
78
+ out.push({
79
+ index: i,
80
+ image,
81
+ width: image.width,
82
+ height: image.height
83
+ });
84
+ }
85
+ return out;
86
+ }
87
+ function releaseTextureData(textures) {
88
+ for (const t of textures) {
89
+ const img = t.image;
90
+ if (typeof ImageBitmap !== "undefined" && img instanceof ImageBitmap) {
91
+ img.close();
92
+ }
93
+ }
94
+ }
95
+
96
+ // src/motion/evaluate-curve.ts
97
+ function evaluateMotion3(clip, timeSeconds) {
98
+ const t = clamp(timeSeconds, 0, clip.duration);
99
+ const out = [];
100
+ for (const curve of clip.curves) {
101
+ out.push({
102
+ target: curve.target,
103
+ id: curve.id,
104
+ value: evaluateCurve(curve, t, clip.areBeziersRestricted)
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+ function evaluateCurve(curve, timeSeconds, areBeziersRestricted) {
110
+ const segs = curve.segments;
111
+ if (segs.length === 0) return 0;
112
+ if (timeSeconds <= segs[0].p0.time) return segs[0].p0.value;
113
+ const last = segs[segs.length - 1];
114
+ if (timeSeconds >= last.p3.time) return last.p3.value;
115
+ for (let i = 0; i < segs.length; i += 1) {
116
+ const seg = segs[i];
117
+ const isLast = i === segs.length - 1;
118
+ if (timeSeconds < seg.p3.time || isLast && timeSeconds <= seg.p3.time) {
119
+ return evaluateSegment(seg, timeSeconds, areBeziersRestricted);
120
+ }
121
+ }
122
+ return last.p3.value;
123
+ }
124
+ function evaluateSegment(seg, time, areBeziersRestricted) {
125
+ const { p0, p3 } = seg;
126
+ switch (seg.kind) {
127
+ case "linear": {
128
+ const span = p3.time - p0.time;
129
+ if (span <= 0) return p3.value;
130
+ const u = (time - p0.time) / span;
131
+ return p0.value + (p3.value - p0.value) * u;
132
+ }
133
+ case "stepped":
134
+ return p0.value;
135
+ case "inverseStepped":
136
+ return p3.value;
137
+ case "bezier": {
138
+ const p1 = seg.p1;
139
+ const p2 = seg.p2;
140
+ if (areBeziersRestricted) {
141
+ const span = p3.time - p0.time;
142
+ if (span <= 0) return p3.value;
143
+ const u2 = (time - p0.time) / span;
144
+ return cubic(p0.value, p1.value, p2.value, p3.value, u2);
145
+ }
146
+ const u = solveBezierTime(p0.time, p1.time, p2.time, p3.time, time);
147
+ return cubic(p0.value, p1.value, p2.value, p3.value, u);
148
+ }
149
+ default:
150
+ return p3.value;
151
+ }
152
+ }
153
+ function cubic(a, b, c, d, t) {
154
+ const u = 1 - t;
155
+ return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;
156
+ }
157
+ function solveBezierTime(t0, t1, t2, t3, target) {
158
+ let lo = 0;
159
+ let hi = 1;
160
+ for (let i = 0; i < 20; i += 1) {
161
+ const mid = (lo + hi) * 0.5;
162
+ const x = cubic(t0, t1, t2, t3, mid);
163
+ if (x < target) lo = mid;
164
+ else hi = mid;
165
+ }
166
+ return (lo + hi) * 0.5;
167
+ }
168
+ function clamp(n, min, max) {
169
+ if (n < min) return min;
170
+ if (n > max) return max;
171
+ return n;
172
+ }
173
+
174
+ // src/motion/types.ts
175
+ var MotionPriority = {
176
+ none: 0,
177
+ idle: 1,
178
+ normal: 2,
179
+ force: 3
180
+ };
181
+
182
+ // src/motion/motion-player.ts
183
+ var MotionPlayer = class {
184
+ #slots = /* @__PURE__ */ new Map();
185
+ #queues = /* @__PURE__ */ new Map();
186
+ #handlers;
187
+ constructor(handlers = {}) {
188
+ this.#handlers = handlers;
189
+ }
190
+ get isPlaying() {
191
+ return this.#slots.size > 0;
192
+ }
193
+ listPlaying() {
194
+ return [...this.#slots.values()].map((a) => ({
195
+ slot: a.slot,
196
+ group: a.group,
197
+ index: a.index,
198
+ time: a.time,
199
+ priority: a.priority
200
+ }));
201
+ }
202
+ /** @deprecated Prefer {@link listPlaying}; returns highest-priority slot. */
203
+ get current() {
204
+ const list = [...this.listPlaying()];
205
+ if (!list.length) return null;
206
+ list.sort((a, b) => b.priority - a.priority);
207
+ const top = list[0];
208
+ return {
209
+ group: top.group,
210
+ index: top.index,
211
+ time: top.time,
212
+ priority: top.priority
213
+ };
214
+ }
215
+ /**
216
+ * Start a clip on a slot. Returns false if rejected by priority
217
+ * (and not queued).
218
+ */
219
+ start(group, index, clip, options = {}) {
220
+ const priority = options.priority ?? MotionPriority.normal;
221
+ const slot = options.slot ?? `priority:${priority}`;
222
+ const existing = this.#slots.get(slot);
223
+ if (existing && priority < existing.priority) {
224
+ if (options.queue) {
225
+ this.#enqueue(slot, { group, index, clip, options });
226
+ return true;
227
+ }
228
+ return false;
229
+ }
230
+ if (existing && options.queue && !existing.fadingOut) {
231
+ this.#enqueue(slot, { group, index, clip, options });
232
+ return true;
233
+ }
234
+ if (existing) {
235
+ if (existing.fadeOutTime > 0 && !existing.fadingOut) {
236
+ existing.fadingOut = true;
237
+ existing.fadeOutElapsed = 0;
238
+ this.#enqueueFront(slot, { group, index, clip, options });
239
+ return true;
240
+ }
241
+ this.#finish(existing, false);
242
+ }
243
+ this.#slots.set(
244
+ slot,
245
+ this.#createActive(slot, group, index, clip, options)
246
+ );
247
+ return true;
248
+ }
249
+ /** Fade out (default) or hard-stop; `slot` omits → all slots. */
250
+ stop(fade = true, slot) {
251
+ if (slot !== void 0) {
252
+ const a = this.#slots.get(slot);
253
+ if (!a) return;
254
+ this.#stopOne(a, fade);
255
+ return;
256
+ }
257
+ for (const a of [...this.#slots.values()]) {
258
+ this.#stopOne(a, fade);
259
+ }
260
+ }
261
+ clear() {
262
+ this.#slots.clear();
263
+ this.#queues.clear();
264
+ }
265
+ /**
266
+ * Advance all slots and return blended samples (weight baked in; apply as absolute).
267
+ */
268
+ update(deltaTimeSeconds) {
269
+ const dt = Math.max(0, deltaTimeSeconds);
270
+ const layerSamples = [];
271
+ for (const a of [...this.#slots.values()]) {
272
+ const samples = this.#tick(a, dt);
273
+ if (samples) {
274
+ layerSamples.push({ priority: a.priority, samples });
275
+ }
276
+ }
277
+ layerSamples.sort((a, b) => a.priority - b.priority);
278
+ return blendMotionLayers(layerSamples);
279
+ }
280
+ #tick(a, dt) {
281
+ if (!a.started) {
282
+ a.started = true;
283
+ this.#handlers.onStart?.({
284
+ group: a.group,
285
+ index: a.index,
286
+ slot: a.slot
287
+ });
288
+ }
289
+ a.time += dt;
290
+ if (a.fadingOut) {
291
+ a.fadeOutElapsed += dt;
292
+ if (a.fadeOutElapsed >= a.fadeOutTime) {
293
+ this.#finish(a, true);
294
+ return null;
295
+ }
296
+ } else if (!a.loop && a.time >= a.clip.duration) {
297
+ if (a.fadeOutTime > 0) {
298
+ a.fadingOut = true;
299
+ a.fadeOutElapsed = 0;
300
+ } else {
301
+ const samples = this.#sample(a, a.clip.duration, 1);
302
+ this.#finish(a, true);
303
+ return samples;
304
+ }
305
+ }
306
+ let playTime = a.time;
307
+ if (a.loop && a.clip.duration > 0) {
308
+ playTime = a.time % a.clip.duration;
309
+ } else {
310
+ playTime = Math.min(playTime, a.clip.duration);
311
+ }
312
+ this.#emitEvents(a, playTime);
313
+ return this.#sample(a, playTime, this.#fadeWeight(a));
314
+ }
315
+ #createActive(slot, group, index, clip, options) {
316
+ const fadeIn = options.fadeInTime ?? (clip.fadeInTime > 0 ? clip.fadeInTime : 0);
317
+ const fadeOut = options.fadeOutTime ?? (clip.fadeOutTime > 0 ? clip.fadeOutTime : 0);
318
+ return {
319
+ slot,
320
+ group,
321
+ index,
322
+ clip,
323
+ priority: options.priority ?? MotionPriority.normal,
324
+ loop: options.loop ?? clip.loop,
325
+ fadeInTime: Math.max(0, fadeIn),
326
+ fadeOutTime: Math.max(0, fadeOut),
327
+ time: 0,
328
+ fadingOut: false,
329
+ fadeOutElapsed: 0,
330
+ lastEventIndex: -1,
331
+ started: false
332
+ };
333
+ }
334
+ #enqueue(slot, item) {
335
+ const q = this.#queues.get(slot) ?? [];
336
+ q.push(item);
337
+ this.#queues.set(slot, q);
338
+ }
339
+ #enqueueFront(slot, item) {
340
+ const q = this.#queues.get(slot) ?? [];
341
+ q.unshift(item);
342
+ this.#queues.set(slot, q);
343
+ }
344
+ #stopOne(a, fade) {
345
+ if (!fade || a.fadeOutTime <= 0) {
346
+ this.#finish(a, true);
347
+ return;
348
+ }
349
+ a.fadingOut = true;
350
+ a.fadeOutElapsed = 0;
351
+ }
352
+ #finish(a, promoteQueue) {
353
+ if (this.#slots.get(a.slot) !== a) return;
354
+ this.#slots.delete(a.slot);
355
+ this.#handlers.onFinish?.({
356
+ group: a.group,
357
+ index: a.index,
358
+ slot: a.slot
359
+ });
360
+ if (!promoteQueue) return;
361
+ const q = this.#queues.get(a.slot);
362
+ const next = q?.shift();
363
+ if (next) {
364
+ this.#slots.set(
365
+ a.slot,
366
+ this.#createActive(
367
+ a.slot,
368
+ next.group,
369
+ next.index,
370
+ next.clip,
371
+ next.options
372
+ )
373
+ );
374
+ }
375
+ }
376
+ #sample(a, playTime, weight) {
377
+ const values = evaluateMotion3(a.clip, playTime);
378
+ return values.map((v) => ({
379
+ target: v.target,
380
+ id: v.id,
381
+ value: v.value,
382
+ weight
383
+ }));
384
+ }
385
+ #fadeWeight(a) {
386
+ let w = 1;
387
+ if (a.fadeInTime > 0 && a.time < a.fadeInTime) {
388
+ w = sineEase(a.time / a.fadeInTime);
389
+ }
390
+ if (a.fadingOut && a.fadeOutTime > 0) {
391
+ const u = 1 - a.fadeOutElapsed / a.fadeOutTime;
392
+ w *= sineEase(Math.max(0, u));
393
+ }
394
+ return w;
395
+ }
396
+ #emitEvents(a, playTime) {
397
+ const events = a.clip.userData;
398
+ for (let i = a.lastEventIndex + 1; i < events.length; i += 1) {
399
+ const e = events[i];
400
+ if (e.time > playTime) break;
401
+ a.lastEventIndex = i;
402
+ this.#handlers.onEvent?.({
403
+ group: a.group,
404
+ index: a.index,
405
+ slot: a.slot,
406
+ time: e.time,
407
+ value: e.value
408
+ });
409
+ }
410
+ if (a.loop && a.clip.duration > 0) {
411
+ const prevMod = (a.time - 1e-6) % a.clip.duration + (a.time - 1e-6 < 0 ? a.clip.duration : 0);
412
+ if (playTime < prevMod - 1e-4) {
413
+ a.lastEventIndex = -1;
414
+ }
415
+ }
416
+ }
417
+ };
418
+ function blendMotionLayers(layers) {
419
+ const map = /* @__PURE__ */ new Map();
420
+ for (const layer of layers) {
421
+ for (const s of layer.samples) {
422
+ const key = `${s.target}\0${s.id}`;
423
+ const w = Math.min(1, Math.max(0, s.weight));
424
+ const prev = map.get(key);
425
+ if (!prev) {
426
+ map.set(key, {
427
+ target: s.target,
428
+ id: s.id,
429
+ value: s.value,
430
+ weight: w
431
+ });
432
+ } else {
433
+ prev.value = prev.value + (s.value - prev.value) * w;
434
+ prev.weight = Math.min(1, prev.weight + w * (1 - prev.weight));
435
+ }
436
+ }
437
+ }
438
+ return [...map.values()];
439
+ }
440
+ function sineEase(t) {
441
+ const x = Math.min(1, Math.max(0, t));
442
+ return 0.5 - 0.5 * Math.cos(x * Math.PI);
443
+ }
444
+
445
+ // src/motion/parse-motion3.ts
446
+ var SEGMENT_KIND = {
447
+ 0: "linear",
448
+ 1: "bezier",
449
+ 2: "stepped",
450
+ 3: "inverseStepped"
451
+ };
452
+ function parseMotion3(json) {
453
+ if (!json || typeof json !== "object") {
454
+ throw new Error(
455
+ "@doki-land/live2d: motion3.json root must be an object"
456
+ );
457
+ }
458
+ const root = json;
459
+ const version = Number(root.Version ?? 3);
460
+ const meta = root.Meta;
461
+ if (!meta || typeof meta !== "object") {
462
+ throw new Error("@doki-land/live2d: motion3.json missing Meta");
463
+ }
464
+ const m = meta;
465
+ const duration = num(m.Duration, "Meta.Duration");
466
+ const fps = num(m.Fps, "Meta.Fps");
467
+ const loop = m.Loop === true;
468
+ const areBeziersRestricted = m.AreBeziersRestricted !== false;
469
+ const fadeInTime = optionalNum(m.FadeInTime) ?? 0;
470
+ const fadeOutTime = optionalNum(m.FadeOutTime) ?? 0;
471
+ const curvesRaw = root.Curves;
472
+ if (!Array.isArray(curvesRaw)) {
473
+ throw new Error("@doki-land/live2d: motion3.json missing Curves");
474
+ }
475
+ const curves = curvesRaw.map((c, i) => parseCurve(c, i));
476
+ const userData = [];
477
+ if (Array.isArray(root.UserData)) {
478
+ for (const item of root.UserData) {
479
+ if (!item || typeof item !== "object") continue;
480
+ const u = item;
481
+ if (typeof u.Time === "number" && typeof u.Value === "string") {
482
+ userData.push({ time: u.Time, value: u.Value });
483
+ }
484
+ }
485
+ userData.sort((a, b) => a.time - b.time);
486
+ }
487
+ return {
488
+ version,
489
+ duration,
490
+ fps,
491
+ loop,
492
+ areBeziersRestricted,
493
+ fadeInTime,
494
+ fadeOutTime,
495
+ curves,
496
+ userData
497
+ };
498
+ }
499
+ function parseCurve(raw, index) {
500
+ if (!raw || typeof raw !== "object") {
501
+ throw new Error(`@doki-land/live2d: Curves[${index}] invalid`);
502
+ }
503
+ const c = raw;
504
+ const target = c.Target;
505
+ const id = c.Id;
506
+ if (typeof target !== "string" || typeof id !== "string") {
507
+ throw new Error(`@doki-land/live2d: Curves[${index}] needs Target/Id`);
508
+ }
509
+ if (target !== "Parameter" && target !== "PartOpacity" && target !== "Model") {
510
+ throw new Error(
511
+ `@doki-land/live2d: Curves[${index}] unknown Target ${target}`
512
+ );
513
+ }
514
+ const segmentsFlat = c.Segments;
515
+ if (!Array.isArray(segmentsFlat) || segmentsFlat.length < 2) {
516
+ throw new Error(`@doki-land/live2d: Curves[${index}] empty Segments`);
517
+ }
518
+ const numbers = segmentsFlat.map((n, j) => {
519
+ if (typeof n !== "number" || !Number.isFinite(n)) {
520
+ throw new Error(
521
+ `@doki-land/live2d: Curves[${index}].Segments[${j}] not a number`
522
+ );
523
+ }
524
+ return n;
525
+ });
526
+ return {
527
+ target,
528
+ id,
529
+ fadeInTime: optionalNum(c.FadeInTime),
530
+ fadeOutTime: optionalNum(c.FadeOutTime),
531
+ segments: parseSegments(numbers, index)
532
+ };
533
+ }
534
+ function parseSegments(flat, curveIndex) {
535
+ let i = 0;
536
+ const p0 = { time: flat[i++], value: flat[i++] };
537
+ const out = [];
538
+ let prev = p0;
539
+ while (i < flat.length) {
540
+ const kindId = flat[i++];
541
+ const kind = SEGMENT_KIND[kindId];
542
+ if (!kind) {
543
+ throw new Error(
544
+ `@doki-land/live2d: Curves[${curveIndex}] unknown segment ${kindId}`
545
+ );
546
+ }
547
+ if (kind === "bezier") {
548
+ if (i + 5 >= flat.length) {
549
+ throw new Error(
550
+ `@doki-land/live2d: Curves[${curveIndex}] truncated bezier`
551
+ );
552
+ }
553
+ const p1 = { time: flat[i++], value: flat[i++] };
554
+ const p2 = { time: flat[i++], value: flat[i++] };
555
+ const p3 = { time: flat[i++], value: flat[i++] };
556
+ out.push({ kind, p0: prev, p1, p2, p3 });
557
+ prev = p3;
558
+ } else {
559
+ if (i + 1 >= flat.length) {
560
+ throw new Error(
561
+ `@doki-land/live2d: Curves[${curveIndex}] truncated ${kind}`
562
+ );
563
+ }
564
+ const p3 = { time: flat[i++], value: flat[i++] };
565
+ out.push({ kind, p0: prev, p3 });
566
+ prev = p3;
567
+ }
568
+ }
569
+ return out;
570
+ }
571
+ function num(v, label) {
572
+ if (typeof v !== "number" || !Number.isFinite(v)) {
573
+ throw new Error(`@doki-land/live2d: motion3 ${label} must be a number`);
574
+ }
575
+ return v;
576
+ }
577
+ function optionalNum(v) {
578
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
579
+ }
580
+
581
+ // src/create-live2d.ts
582
+ function lerp(a, b, t) {
583
+ return a + (b - a) * Math.min(1, Math.max(0, t));
584
+ }
585
+ function nowMs() {
586
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
587
+ }
588
+ function createLive2D(options = {}) {
589
+ const backends = options.backends ?? [
590
+ createMoc2Backend(),
591
+ createMoc3Backend()
592
+ ];
593
+ const renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
594
+ const events = new EventEmitter();
595
+ let canvas = null;
596
+ let model = null;
597
+ let activeBackend = null;
598
+ let drawPass = null;
599
+ let loadedTextures = [];
600
+ let initPromise = null;
601
+ let phase = "idle";
602
+ let lastError = null;
603
+ let generation = 0;
604
+ let loadGeneration = 0;
605
+ let fpsSmooth = 0;
606
+ let activeResolver = null;
607
+ const motionCache = /* @__PURE__ */ new Map();
608
+ const motionPlayer = new MotionPlayer({
609
+ onStart: ({ group, index, slot }) => events.emit("motion:start", { group, index, slot }),
610
+ onFinish: ({ group, index, slot }) => events.emit("motion:finish", { group, index, slot })
611
+ });
612
+ const applyMotionSamples = (samples) => {
613
+ if (!model || !activeBackend) return;
614
+ for (const s of samples) {
615
+ if (s.weight <= 0) continue;
616
+ if (s.target === "PartOpacity") {
617
+ if (!activeBackend.setPartOpacity) continue;
618
+ if (s.weight >= 1) {
619
+ activeBackend.setPartOpacity(model, s.id, s.value);
620
+ } else {
621
+ const cur2 = 1;
622
+ activeBackend.setPartOpacity(
623
+ model,
624
+ s.id,
625
+ cur2 + (s.value - cur2) * s.weight
626
+ );
627
+ }
628
+ continue;
629
+ }
630
+ if (s.target !== "Parameter" || !activeBackend.setParameter)
631
+ continue;
632
+ if (s.weight >= 1) {
633
+ activeBackend.setParameter(model, s.id, s.value);
634
+ continue;
635
+ }
636
+ const cur = activeBackend.listParameters?.(model).find((p) => p.id === s.id)?.value ?? s.value;
637
+ activeBackend.setParameter(
638
+ model,
639
+ s.id,
640
+ cur + (s.value - cur) * s.weight
641
+ );
642
+ }
643
+ };
644
+ const clearTextures = () => {
645
+ if (loadedTextures.length > 0) {
646
+ releaseTextureData(loadedTextures);
647
+ loadedTextures = [];
648
+ }
649
+ drawPass?.setTextures([]);
650
+ };
651
+ const setPhase = (next) => {
652
+ phase = next;
653
+ events.emit("phase", { phase, generation });
654
+ };
655
+ const report = (payload) => {
656
+ events.emit("progress", payload);
657
+ };
658
+ const state = () => ({
659
+ phase,
660
+ lastError,
661
+ generation
662
+ });
663
+ const ensureInitialized = async () => {
664
+ if (!canvas) {
665
+ throw new Error(
666
+ "@doki-land/live2d: call mount(canvas) before loadModel"
667
+ );
668
+ }
669
+ if (!initPromise) {
670
+ setPhase("mounting");
671
+ const gen = generation;
672
+ initPromise = renderer.initialize(canvas).then(() => {
673
+ if (gen !== generation) return;
674
+ drawPass = renderer.createModelDrawPass();
675
+ setPhase("ready");
676
+ }).catch((err) => {
677
+ lastError = err;
678
+ setPhase("error");
679
+ events.emit("error", { error: err });
680
+ throw err;
681
+ });
682
+ }
683
+ await initPromise;
684
+ };
685
+ const runtime = {
686
+ events,
687
+ backends,
688
+ renderer,
689
+ get model() {
690
+ return model;
691
+ },
692
+ get state() {
693
+ return state();
694
+ },
695
+ mount(target) {
696
+ generation += 1;
697
+ canvas = target;
698
+ initPromise = null;
699
+ clearTextures();
700
+ drawPass?.destroy();
701
+ drawPass = null;
702
+ setPhase("idle");
703
+ void ensureInitialized();
704
+ },
705
+ async loadModel(source, resolver) {
706
+ const gen = ++loadGeneration;
707
+ setPhase("loading");
708
+ report({
709
+ stage: "mounting",
710
+ progress: 0.01,
711
+ detail: "initialize renderer"
712
+ });
713
+ await ensureInitialized();
714
+ if (gen !== loadGeneration) {
715
+ throw new Error("@doki-land/live2d: load cancelled");
716
+ }
717
+ report({
718
+ stage: "resolve",
719
+ progress: 0.02,
720
+ detail: "resolve source"
721
+ });
722
+ try {
723
+ let json;
724
+ let baseUrl;
725
+ let settingsUrl;
726
+ if (typeof source === "object" && source.kind === "json") {
727
+ json = source.json;
728
+ baseUrl = source.baseUrl;
729
+ settingsUrl = source.baseUrl;
730
+ report({
731
+ stage: "settings",
732
+ progress: 0.2,
733
+ detail: "inline settings"
734
+ });
735
+ } else {
736
+ const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl(source) : source.url;
737
+ const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
738
+ const fetchUrl = resolveModelSourceUrl(raw, {
739
+ npmCdnBase: cdnBase
740
+ });
741
+ report({
742
+ stage: "settings",
743
+ progress: 0.05,
744
+ detail: fetchUrl
745
+ });
746
+ json = await fetchModelJson(fetchUrl, (u) => {
747
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
748
+ report({
749
+ stage: "settings",
750
+ progress: lerp(0.05, 0.22, ratio),
751
+ detail: fetchUrl,
752
+ bytesLoaded: u.bytesLoaded,
753
+ bytesTotal: u.bytesTotal
754
+ });
755
+ });
756
+ baseUrl = fetchUrl;
757
+ settingsUrl = fetchUrl;
758
+ }
759
+ if (gen !== loadGeneration) {
760
+ throw new Error("@doki-land/live2d: load cancelled");
761
+ }
762
+ const settings = normalizeModelSettings(json, settingsUrl);
763
+ report({
764
+ stage: "moc",
765
+ progress: 0.25,
766
+ detail: settings.moc
767
+ });
768
+ const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
769
+ onBytesProgress: (key, u) => {
770
+ const isMoc = key === settings.moc;
771
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
772
+ if (isMoc) {
773
+ report({
774
+ stage: "moc",
775
+ progress: lerp(0.25, 0.8, ratio),
776
+ detail: key,
777
+ bytesLoaded: u.bytesLoaded,
778
+ bytesTotal: u.bytesTotal
779
+ });
780
+ } else {
781
+ report({
782
+ stage: "textures",
783
+ progress: lerp(0.8, 0.9, ratio),
784
+ detail: key,
785
+ bytesLoaded: u.bytesLoaded,
786
+ bytesTotal: u.bytesTotal
787
+ });
788
+ }
789
+ }
790
+ });
791
+ activeResolver = assetResolver;
792
+ motionPlayer.clear();
793
+ motionCache.clear();
794
+ const backend = selectModelBackend(backends, json);
795
+ report({
796
+ stage: "decode",
797
+ progress: 0.85,
798
+ detail: `decode ${settings.format}`
799
+ });
800
+ const next = await backend.createModel(settings, {
801
+ renderer,
802
+ resolver: assetResolver
803
+ });
804
+ if (gen !== loadGeneration) {
805
+ backend.destroyModel(next);
806
+ throw new Error("@doki-land/live2d: load cancelled");
807
+ }
808
+ clearTextures();
809
+ if (settings.textures.length > 0 && drawPass) {
810
+ report({
811
+ stage: "textures",
812
+ progress: 0.88,
813
+ detail: `${settings.textures.length} textures`
814
+ });
815
+ const textures = await loadTextureData(
816
+ assetResolver,
817
+ settings.textures,
818
+ {
819
+ onProgress: (u) => {
820
+ const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
821
+ report({
822
+ stage: "textures",
823
+ progress: lerp(0.88, 0.96, ratio),
824
+ detail: u.key,
825
+ bytesLoaded: u.bytesLoaded,
826
+ bytesTotal: u.bytesTotal
827
+ });
828
+ }
829
+ }
830
+ );
831
+ if (gen !== loadGeneration) {
832
+ releaseTextureData(textures);
833
+ backend.destroyModel(next);
834
+ throw new Error("@doki-land/live2d: load cancelled");
835
+ }
836
+ loadedTextures = textures;
837
+ drawPass.setTextures(textures);
838
+ }
839
+ if (model && activeBackend) {
840
+ activeBackend.destroyModel(model);
841
+ }
842
+ model = next;
843
+ activeBackend = backend;
844
+ lastError = null;
845
+ setPhase("live");
846
+ report({
847
+ stage: "ready",
848
+ progress: 1,
849
+ detail: model.id
850
+ });
851
+ events.emit("ready", { modelId: model.id });
852
+ return model;
853
+ } catch (err) {
854
+ lastError = err;
855
+ setPhase("error");
856
+ events.emit("error", { error: err });
857
+ throw err;
858
+ }
859
+ },
860
+ captureFrame() {
861
+ if (!model || !activeBackend?.captureFrame) return null;
862
+ return activeBackend.captureFrame(model);
863
+ },
864
+ setParameter(id, value) {
865
+ if (!model || !activeBackend?.setParameter) return;
866
+ activeBackend.setParameter(model, id, value);
867
+ },
868
+ hitTest(x, y) {
869
+ if (!model || !activeBackend) return null;
870
+ const drawables = activeBackend.getDrawables(model);
871
+ for (let n = drawables.length - 1; n >= 0; n -= 1) {
872
+ const d = drawables[n];
873
+ if (!d.visible || d.opacity <= 0) continue;
874
+ const p = d.vertexPositions;
875
+ const idx = d.indices;
876
+ for (let i = 0; i + 2 < idx.length; i += 3) {
877
+ const a = idx[i] * 2, b = idx[i + 1] * 2, c = idx[i + 2] * 2;
878
+ const ax = p[a], ay = p[a + 1];
879
+ const bx = p[b], by = p[b + 1];
880
+ const cx = p[c], cy = p[c + 1];
881
+ const s = (ax - cx) * (y - cy) - (ay - cy) * (x - cx);
882
+ const s1 = (bx - ax) * (y - ay) - (by - ay) * (x - ax);
883
+ const s2 = (cx - bx) * (y - by) - (cy - by) * (x - bx);
884
+ if (s >= 0 && s1 >= 0 && s2 >= 0 || s <= 0 && s1 <= 0 && s2 <= 0) {
885
+ return `drawable:${d.index}`;
886
+ }
887
+ }
888
+ }
889
+ return null;
890
+ },
891
+ listParameters() {
892
+ if (!model || !activeBackend?.listParameters) return [];
893
+ return activeBackend.listParameters(model);
894
+ },
895
+ listMotionGroups() {
896
+ return model?.settings.motionGroups ?? {};
897
+ },
898
+ async playMotion(group, index = 0, options2 = {}) {
899
+ if (!model || !activeResolver) return false;
900
+ const list = model.settings.motionGroups[group];
901
+ const def = list?.[index];
902
+ if (!def) return false;
903
+ let clip = motionCache.get(def.file);
904
+ if (!clip) {
905
+ const json = await activeResolver.fetchJson(def.file);
906
+ clip = parseMotion3(json);
907
+ motionCache.set(def.file, clip);
908
+ }
909
+ const fadeInTime = options2.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
910
+ const fadeOutTime = options2.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
911
+ return motionPlayer.start(group, index, clip, {
912
+ priority: options2.priority ?? MotionPriority.normal,
913
+ slot: options2.slot,
914
+ queue: options2.queue,
915
+ loop: options2.loop,
916
+ fadeInTime,
917
+ fadeOutTime
918
+ });
919
+ },
920
+ stopMotion(opts) {
921
+ motionPlayer.stop(opts?.fade !== false, opts?.slot);
922
+ },
923
+ listPlayingMotions() {
924
+ return motionPlayer.listPlaying();
925
+ },
926
+ async capturePng(opts = {}) {
927
+ if (!canvas) {
928
+ throw new Error(
929
+ "@doki-land/live2d: mount(canvas) before capturePng"
930
+ );
931
+ }
932
+ if (phase === "live" && model && activeBackend && drawPass) {
933
+ runtime.update(0);
934
+ }
935
+ const mime = opts.mimeType ?? "image/png";
936
+ return await new Promise((resolve, reject) => {
937
+ canvas.toBlob(
938
+ (blob) => {
939
+ if (blob) resolve(blob);
940
+ else
941
+ reject(
942
+ new Error(
943
+ "@doki-land/live2d: canvas.toBlob returned null"
944
+ )
945
+ );
946
+ },
947
+ mime,
948
+ opts.quality
949
+ );
950
+ });
951
+ },
952
+ update(deltaTimeSeconds) {
953
+ if (!model || !activeBackend || !drawPass) return;
954
+ if (phase !== "live") return;
955
+ const t0 = nowMs();
956
+ applyMotionSamples(motionPlayer.update(deltaTimeSeconds));
957
+ activeBackend.updateModel(model, deltaTimeSeconds);
958
+ const drawables = activeBackend.getDrawables(model);
959
+ const t1 = nowMs();
960
+ renderer.beginFrame();
961
+ drawPass.draw(drawables, new Float32Array(16));
962
+ renderer.endFrame();
963
+ const t2 = nowMs();
964
+ let vertexCount = 0;
965
+ let indexCount = 0;
966
+ for (const d of drawables) {
967
+ vertexCount += d.vertexPositions.length / 2;
968
+ indexCount += d.indices.length;
969
+ }
970
+ const frameMs = t2 - t0;
971
+ const evaluateMs = t1 - t0;
972
+ const drawMs = t2 - t1;
973
+ const fps = deltaTimeSeconds > 0 ? 1 / deltaTimeSeconds : 0;
974
+ fpsSmooth = fpsSmooth <= 0 ? fps : fpsSmooth * 0.85 + fps * 0.15;
975
+ events.emit("profile", {
976
+ fps,
977
+ fpsSmooth,
978
+ frameMs,
979
+ evaluateMs,
980
+ drawMs,
981
+ drawableCount: drawables.length,
982
+ vertexCount,
983
+ indexCount
984
+ });
985
+ },
986
+ destroy() {
987
+ loadGeneration += 1;
988
+ generation += 1;
989
+ motionPlayer.clear();
990
+ motionCache.clear();
991
+ activeResolver = null;
992
+ if (model && activeBackend) {
993
+ activeBackend.destroyModel(model);
994
+ }
995
+ model = null;
996
+ activeBackend = null;
997
+ clearTextures();
998
+ drawPass?.destroy();
999
+ drawPass = null;
1000
+ initPromise = null;
1001
+ renderer.destroy();
1002
+ canvas = null;
1003
+ setPhase("destroyed");
1004
+ events.clear();
1005
+ }
1006
+ };
1007
+ return runtime;
1008
+ }
1009
+
1010
+ // src/focus.ts
1011
+ function focusParameterUpdates(parameters, dragX, dragY) {
1012
+ const byId = new Map(parameters.map((p) => [p.id, p]));
1013
+ const x = clampUnit(dragX);
1014
+ const y = clampUnit(dragY);
1015
+ const out = [];
1016
+ const set = (id, normalized) => {
1017
+ const binding = byId.get(id);
1018
+ if (!binding) return;
1019
+ out.push({ id, value: valueFromNormalized(binding, normalized) });
1020
+ };
1021
+ set("PARAM_ANGLE_X", x);
1022
+ set("PARAM_ANGLE_Y", y);
1023
+ set("PARAM_ANGLE_Z", clampUnit(x * y * -1));
1024
+ set("PARAM_BODY_ANGLE_X", x);
1025
+ set("PARAM_BODY_ANGLE_Y", y);
1026
+ set("PARAM_EYE_BALL_X", x);
1027
+ set("PARAM_EYE_BALL_Y", y);
1028
+ return out;
1029
+ }
1030
+ function clampUnit(n) {
1031
+ if (n > 1) return 1;
1032
+ if (n < -1) return -1;
1033
+ return n;
1034
+ }
1035
+ function valueFromNormalized(binding, normalized) {
1036
+ const n = clampUnit(normalized);
1037
+ return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
1038
+ }
1039
+
1040
+ // src/index.ts
1041
+ var LIVE2D_VERSION = "0.0.0";
1042
+ export {
1043
+ DEFAULT_NPM_CDN,
1044
+ EventEmitter2 as EventEmitter,
1045
+ LIVE2D_VERSION,
1046
+ MotionPlayer,
1047
+ MotionPriority,
1048
+ blendMotionLayers,
1049
+ createCanvas2DRenderer,
1050
+ createLive2D,
1051
+ createMoc2Backend2 as createMoc2Backend,
1052
+ createMoc3Backend2 as createMoc3Backend,
1053
+ createQuadProgram,
1054
+ createRenderer2 as createRenderer,
1055
+ createWebGl2Renderer,
1056
+ createWebGpuRenderer,
1057
+ decodeMoc3,
1058
+ evaluateCurve,
1059
+ evaluateFrame,
1060
+ evaluateMotion3,
1061
+ fingerprintSnapshot,
1062
+ focusParameterUpdates,
1063
+ parseCpuProgram,
1064
+ parseMotion3,
1065
+ resolveModelSourceUrl2 as resolveModelSourceUrl,
1066
+ resolveNpmSpecifier,
1067
+ serializeCpuProgram
1068
+ };