@doki-land/live2d 0.0.22 → 0.0.24

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 CHANGED
@@ -20,6 +20,67 @@ import {
20
20
  serializeCpuProgram
21
21
  } from "@doki-land/live2d-renderer";
22
22
 
23
+ // src/expression/apply-expression.ts
24
+ function blendValue(current, target, mode, weight) {
25
+ if (weight <= 0) return current;
26
+ if (weight >= 1) {
27
+ if (mode === "Add") return current + target;
28
+ if (mode === "Multiply") return current * target;
29
+ return target;
30
+ }
31
+ const full = mode === "Add" ? current + target : mode === "Multiply" ? current * target : target;
32
+ return current + (full - current) * weight;
33
+ }
34
+ function applyExpression3Clip(clip, weight, bindings, setParameter) {
35
+ if (weight <= 0) return;
36
+ for (const p of clip.parameters) {
37
+ const binding = bindings.get(p.id);
38
+ if (!binding) continue;
39
+ setParameter(p.id, blendValue(binding.value, p.value, p.blend, weight));
40
+ }
41
+ }
42
+
43
+ // src/expression/parse-expression3.ts
44
+ var BLENDS = /* @__PURE__ */ new Set(["Add", "Multiply", "Override"]);
45
+ function parseBlend(raw) {
46
+ if (typeof raw !== "string") return "Add";
47
+ if (raw === "Overwrite" || raw === "Override") return "Override";
48
+ if (BLENDS.has(raw)) {
49
+ return raw;
50
+ }
51
+ return "Add";
52
+ }
53
+ function parseExpression3(json) {
54
+ if (!json || typeof json !== "object") {
55
+ throw new Error("@doki-land/live2d: exp3.json root must be an object");
56
+ }
57
+ const root = json;
58
+ const version = Number(root.Version ?? 3);
59
+ const paramsRaw = root.Parameters;
60
+ if (!Array.isArray(paramsRaw)) {
61
+ throw new Error("@doki-land/live2d: exp3.json missing Parameters");
62
+ }
63
+ const parameters = paramsRaw.map((item, index) => {
64
+ if (!item || typeof item !== "object") {
65
+ throw new Error(`@doki-land/live2d: Parameters[${index}] invalid`);
66
+ }
67
+ const p = item;
68
+ const id = p.Id;
69
+ const value = p.Value;
70
+ if (typeof id !== "string" || typeof value !== "number") {
71
+ throw new Error(
72
+ `@doki-land/live2d: Parameters[${index}] needs Id/Value`
73
+ );
74
+ }
75
+ return {
76
+ id,
77
+ value,
78
+ blend: parseBlend(p.Blend)
79
+ };
80
+ });
81
+ return { version, parameters };
82
+ }
83
+
23
84
  // src/facade/create-live2d.ts
24
85
  import {
25
86
  createMoc2Backend as createMoc2Backend2,
@@ -512,6 +573,116 @@ function optionalNum(v) {
512
573
  return typeof v === "number" && Number.isFinite(v) ? v : void 0;
513
574
  }
514
575
 
576
+ // src/physics/apply-physics3.ts
577
+ function applyPhysics3(clip, _deltaTimeSeconds, bindings, setParameter) {
578
+ for (const id of clip.outputParameterIds) {
579
+ const binding = bindings.get(id);
580
+ if (!binding) continue;
581
+ setParameter(id, binding.value);
582
+ }
583
+ }
584
+
585
+ // src/physics/parse-physics3.ts
586
+ function parsePhysics3(json) {
587
+ if (!json || typeof json !== "object") {
588
+ throw new Error(
589
+ "@doki-land/live2d: physics3 json root must be an object"
590
+ );
591
+ }
592
+ const root = json;
593
+ const settingsRaw = root.PhysicsSettings;
594
+ if (settingsRaw === void 0 || settingsRaw === null) {
595
+ return { settings: [], outputParameterIds: [] };
596
+ }
597
+ if (!Array.isArray(settingsRaw)) {
598
+ throw new Error("@doki-land/live2d: PhysicsSettings must be an array");
599
+ }
600
+ const settings = [];
601
+ const outputParameterIds = [];
602
+ for (let si = 0; si < settingsRaw.length; si++) {
603
+ const entry = settingsRaw[si];
604
+ if (!entry || typeof entry !== "object") {
605
+ throw new Error(
606
+ `@doki-land/live2d: PhysicsSettings[${si}] must be an object`
607
+ );
608
+ }
609
+ const rec = entry;
610
+ const id = typeof rec.Id === "string" ? rec.Id : `PhysicsSetting${si}`;
611
+ const outputsRaw = rec.Output;
612
+ const outputs = [];
613
+ if (Array.isArray(outputsRaw)) {
614
+ for (let oi = 0; oi < outputsRaw.length; oi++) {
615
+ const out = outputsRaw[oi];
616
+ if (!out || typeof out !== "object") continue;
617
+ const dest = out.Destination;
618
+ if (!dest || typeof dest !== "object") continue;
619
+ const destId = dest.Id;
620
+ if (typeof destId !== "string" || !destId) continue;
621
+ outputs.push({ destinationId: destId });
622
+ outputParameterIds.push(destId);
623
+ }
624
+ }
625
+ settings.push({ id, outputs });
626
+ }
627
+ return { settings, outputParameterIds };
628
+ }
629
+
630
+ // src/pose/apply-pose3.ts
631
+ function applyPose3Activation(clip, activatedPartId, setPartOpacity) {
632
+ for (const group of clip.groups) {
633
+ if (!group.includes(activatedPartId)) continue;
634
+ for (const partId of group) {
635
+ setPartOpacity(partId, partId === activatedPartId ? 1 : 0);
636
+ }
637
+ return;
638
+ }
639
+ }
640
+
641
+ // src/pose/parse-pose3.ts
642
+ function parsePose3(json) {
643
+ if (!json || typeof json !== "object") {
644
+ throw new Error("@doki-land/live2d: pose json root must be an object");
645
+ }
646
+ const root = json;
647
+ const groupsRaw = root.Groups;
648
+ if (!Array.isArray(groupsRaw)) {
649
+ throw new Error("@doki-land/live2d: pose json missing Groups");
650
+ }
651
+ const groups = groupsRaw.map((group, gi) => {
652
+ if (!Array.isArray(group)) {
653
+ throw new Error(`@doki-land/live2d: Groups[${gi}] must be array`);
654
+ }
655
+ return group.map((entry, ei) => {
656
+ if (!entry || typeof entry !== "object") {
657
+ throw new Error(
658
+ `@doki-land/live2d: Groups[${gi}][${ei}] invalid`
659
+ );
660
+ }
661
+ const id = entry.Id;
662
+ if (typeof id !== "string" || !id) {
663
+ throw new Error(
664
+ `@doki-land/live2d: Groups[${gi}][${ei}] missing Id`
665
+ );
666
+ }
667
+ return id;
668
+ });
669
+ });
670
+ return { groups };
671
+ }
672
+
673
+ // src/stage/hit-area.ts
674
+ function resolveHitAreaName(input) {
675
+ const { hitAreas, drawableIndex, artMeshId } = input;
676
+ const candidates = /* @__PURE__ */ new Set();
677
+ if (artMeshId) candidates.add(artMeshId);
678
+ candidates.add(`D_${drawableIndex}`);
679
+ candidates.add(`${drawableIndex}`);
680
+ for (const area of hitAreas) {
681
+ if (candidates.has(area.id)) return area.name;
682
+ }
683
+ return `drawable:${drawableIndex}`;
684
+ }
685
+
515
686
  // src/stage/actor-model-slot.ts
516
687
  var ActorModelSlot = class {
517
688
  #assets;
@@ -525,6 +696,10 @@ var ActorModelSlot = class {
525
696
  #loadGeneration = 0;
526
697
  #paramById = /* @__PURE__ */ new Map();
527
698
  #paramIndexById = /* @__PURE__ */ new Map();
699
+ #expressionCache = /* @__PURE__ */ new Map();
700
+ #activeExpression = null;
701
+ #poseClip = null;
702
+ #physicsClip = null;
528
703
  constructor(options) {
529
704
  this.#assets = options.assets;
530
705
  this.#renderer = options.renderer;
@@ -559,16 +734,8 @@ var ActorModelSlot = class {
559
734
  if (s.weight <= 0) continue;
560
735
  if (s.target === "PartOpacity") {
561
736
  if (!this.#backend.setPartOpacity) continue;
562
- if (s.weight >= 1) {
563
- this.#backend.setPartOpacity(this.#model, s.id, s.value);
564
- } else {
565
- const cur2 = 1;
566
- this.#backend.setPartOpacity(
567
- this.#model,
568
- s.id,
569
- cur2 + (s.value - cur2) * s.weight
570
- );
571
- }
737
+ const value = s.weight >= 1 ? s.value : 1 + (s.value - 1) * s.weight;
738
+ this.#setPartOpacityWithPose(s.id, value);
572
739
  continue;
573
740
  }
574
741
  if (s.target !== "Parameter" || !this.#backend.setParameter)
@@ -584,6 +751,75 @@ var ActorModelSlot = class {
584
751
  cur + (s.value - cur) * s.weight
585
752
  );
586
753
  }
754
+ this.#syncParamCacheFromBackend();
755
+ }
756
+ #setPartOpacityWithPose(partId, opacity) {
757
+ if (!this.#model || !this.#backend?.setPartOpacity) return;
758
+ if (this.#poseClip && opacity > 0) {
759
+ applyPose3Activation(this.#poseClip, partId, (id, value) => {
760
+ this.#backend?.setPartOpacity?.(this.#model, id, value);
761
+ });
762
+ return;
763
+ }
764
+ this.#backend.setPartOpacity(this.#model, partId, opacity);
765
+ }
766
+ #syncParamCacheFromBackend() {
767
+ if (!this.#model || !this.#backend?.listParameters) return;
768
+ for (const p of this.#backend.listParameters(this.#model)) {
769
+ const cached = this.#paramById.get(p.id);
770
+ if (cached) cached.value = p.value;
771
+ }
772
+ }
773
+ #tickExpression(deltaTimeSeconds) {
774
+ if (!this.#activeExpression) return;
775
+ const fadeSeconds = 0.25;
776
+ const step = deltaTimeSeconds / Math.max(1e-3, fadeSeconds);
777
+ this.#activeExpression.weight = Math.min(
778
+ 1,
779
+ this.#activeExpression.weight + step
780
+ );
781
+ }
782
+ #applyExpressionLayer() {
783
+ if (!this.#activeExpression || !this.#model || !this.#backend) return;
784
+ applyExpression3Clip(
785
+ this.#activeExpression.clip,
786
+ this.#activeExpression.weight,
787
+ this.#paramById,
788
+ (id, value) => this.#backend?.setParameter?.(this.#model, id, value)
789
+ );
790
+ this.#syncParamCacheFromBackend();
791
+ }
792
+ async #loadPoseClip() {
793
+ this.#poseClip = null;
794
+ const posePath = this.#model?.settings.pose;
795
+ if (!posePath || !this.#lease) return;
796
+ try {
797
+ const json = await this.#lease.resolver.fetchJson(posePath);
798
+ this.#poseClip = parsePose3(json);
799
+ } catch {
800
+ this.#poseClip = null;
801
+ }
802
+ }
803
+ async #loadPhysicsClip() {
804
+ this.#physicsClip = null;
805
+ const physicsPath = this.#model?.settings.physics;
806
+ if (!physicsPath || !this.#lease) return;
807
+ try {
808
+ const json = await this.#lease.resolver.fetchJson(physicsPath);
809
+ this.#physicsClip = parsePhysics3(json);
810
+ } catch {
811
+ this.#physicsClip = null;
812
+ }
813
+ }
814
+ #applyPhysicsLayer(deltaTimeSeconds) {
815
+ if (!this.#physicsClip || !this.#model || !this.#backend) return;
816
+ applyPhysics3(
817
+ this.#physicsClip,
818
+ deltaTimeSeconds,
819
+ this.#paramById,
820
+ (id, value) => this.#backend?.setParameter?.(this.#model, id, value)
821
+ );
822
+ this.#syncParamCacheFromBackend();
587
823
  }
588
824
  #rebuildParamCache() {
589
825
  this.#paramById.clear();
@@ -614,24 +850,37 @@ var ActorModelSlot = class {
614
850
  }
615
851
  return this.#paramIndexById.get(id);
616
852
  }
617
- async load(source, resolver) {
853
+ async load(source, resolver, options) {
618
854
  const gen = ++this.#loadGeneration;
855
+ const signal = options?.signal;
856
+ if (signal?.aborted) {
857
+ throw new Error("@doki-land/live2d: load cancelled");
858
+ }
859
+ const onAbort = () => {
860
+ this.#loadGeneration += 1;
861
+ };
862
+ signal?.addEventListener("abort", onAbort, { once: true });
619
863
  this.#report({
620
864
  stage: "mounting",
621
865
  progress: 0.01,
622
866
  detail: "prepare draw pass"
623
867
  });
624
868
  const drawPass = this.ensureDrawPass();
625
- const lease = await this.#assets.acquire(
626
- source,
627
- resolver,
628
- (p) => this.#report(p)
629
- );
630
- if (gen !== this.#loadGeneration) {
631
- lease.release();
632
- throw new Error("@doki-land/live2d: load cancelled");
869
+ try {
870
+ const lease = await this.#assets.acquire(
871
+ source,
872
+ resolver,
873
+ (p) => this.#report(p),
874
+ { signal }
875
+ );
876
+ if (gen !== this.#loadGeneration) {
877
+ lease.release();
878
+ throw new Error("@doki-land/live2d: load cancelled");
879
+ }
880
+ return await this.#attachLease(lease, drawPass, gen);
881
+ } finally {
882
+ signal?.removeEventListener("abort", onAbort);
633
883
  }
634
- return await this.#attachLease(lease, drawPass, gen);
635
884
  }
636
885
  async loadAsset(asset) {
637
886
  const gen = ++this.#loadGeneration;
@@ -655,6 +904,9 @@ var ActorModelSlot = class {
655
904
  }
656
905
  async #attachLease(lease, drawPass, gen) {
657
906
  this.#motionPlayer.clear();
907
+ this.#activeExpression = null;
908
+ this.#poseClip = null;
909
+ this.#physicsClip = null;
658
910
  this.#releaseLease();
659
911
  const { model, backend } = await lease.createInstance(this.#renderer);
660
912
  if (gen !== this.#loadGeneration) {
@@ -670,6 +922,8 @@ var ActorModelSlot = class {
670
922
  this.#model = model;
671
923
  this.#backend = backend;
672
924
  this.#rebuildParamCache();
925
+ await this.#loadPoseClip();
926
+ await this.#loadPhysicsClip();
673
927
  this.#report({
674
928
  stage: "ready",
675
929
  progress: 1,
@@ -688,6 +942,28 @@ var ActorModelSlot = class {
688
942
  listMotionGroups() {
689
943
  return this.#model?.settings.motionGroups ?? {};
690
944
  }
945
+ listExpressions() {
946
+ return this.#model?.settings.expressions ?? [];
947
+ }
948
+ async setExpression(name) {
949
+ if (!this.#model || !this.#lease) return false;
950
+ if (name === null) {
951
+ this.#activeExpression = null;
952
+ return true;
953
+ }
954
+ const def = this.#model.settings.expressions.find(
955
+ (item) => item.name === name
956
+ );
957
+ if (!def) return false;
958
+ let clip = this.#expressionCache.get(def.file);
959
+ if (!clip) {
960
+ const json = await this.#lease.resolver.fetchJson(def.file);
961
+ clip = parseExpression3(json);
962
+ this.#expressionCache.set(def.file, clip);
963
+ }
964
+ this.#activeExpression = { name, clip, weight: 0 };
965
+ return true;
966
+ }
691
967
  async playMotion(group, index = 0, options = {}) {
692
968
  if (!this.#model || !this.#lease) return false;
693
969
  const list = this.#model.settings.motionGroups[group];
@@ -720,6 +996,9 @@ var ActorModelSlot = class {
720
996
  update(deltaTimeSeconds) {
721
997
  if (!this.#model || !this.#backend || !this.#drawPass) return null;
722
998
  this.#applyMotionSamples(this.#motionPlayer.update(deltaTimeSeconds));
999
+ this.#tickExpression(deltaTimeSeconds);
1000
+ this.#applyExpressionLayer();
1001
+ this.#applyPhysicsLayer(deltaTimeSeconds);
723
1002
  this.#backend.updateModel(this.#model, deltaTimeSeconds);
724
1003
  return this.#backend.getDrawables(this.#model);
725
1004
  }
@@ -740,10 +1019,15 @@ var ActorModelSlot = class {
740
1019
  const s1 = (bx - ax) * (modelY - ay) - (by - ay) * (modelX - ax);
741
1020
  const s2 = (cx - bx) * (modelY - by) - (cy - by) * (modelX - bx);
742
1021
  if (s >= 0 && s1 >= 0 && s2 >= 0 || s <= 0 && s1 <= 0 && s2 <= 0) {
743
- const hitArea = this.#model.settings.hitAreas.find(
744
- (h) => h.id === `D_${d.index}` || h.id === `${d.index}`
1022
+ const artMeshId = this.#backend.getDrawableArtMeshId?.(
1023
+ this.#model,
1024
+ d.index
745
1025
  );
746
- return hitArea?.name ?? `drawable:${d.index}`;
1026
+ return resolveHitAreaName({
1027
+ hitAreas: this.#model.settings.hitAreas,
1028
+ drawableIndex: d.index,
1029
+ artMeshId
1030
+ });
747
1031
  }
748
1032
  }
749
1033
  }
@@ -752,6 +1036,9 @@ var ActorModelSlot = class {
752
1036
  destroy() {
753
1037
  this.#loadGeneration += 1;
754
1038
  this.#motionPlayer.clear();
1039
+ this.#activeExpression = null;
1040
+ this.#poseClip = null;
1041
+ this.#physicsClip = null;
755
1042
  if (this.#model && this.#backend) {
756
1043
  this.#backend.destroyModel(this.#model);
757
1044
  }
@@ -977,9 +1264,18 @@ var Live2dActorImpl = class {
977
1264
  this.#order = options?.order ?? 0;
978
1265
  this.#slot = new ActorModelSlot({
979
1266
  assets: shared.assets,
980
- renderer: shared.renderer
1267
+ renderer: shared.renderer,
1268
+ onMotionStart: (payload) => this.#onMotionStart?.(payload),
1269
+ onMotionFinish: (payload) => this.#onMotionFinish?.(payload)
981
1270
  });
982
1271
  }
1272
+ #onMotionStart = null;
1273
+ #onMotionFinish = null;
1274
+ /** Bridge MotionPlayer lifecycle into Stage/facade event buses. */
1275
+ setMotionEventHandlers(handlers) {
1276
+ this.#onMotionStart = handlers.onStart ?? null;
1277
+ this.#onMotionFinish = handlers.onFinish ?? null;
1278
+ }
983
1279
  get model() {
984
1280
  return this.#slot.model;
985
1281
  }
@@ -1016,11 +1312,11 @@ var Live2dActorImpl = class {
1016
1312
  ...patch
1017
1313
  });
1018
1314
  }
1019
- async load(source, resolver) {
1315
+ async load(source, resolver, options) {
1020
1316
  if (this.#destroyed) {
1021
1317
  throw new Error("@doki-land/live2d: actor destroyed");
1022
1318
  }
1023
- return await this.#slot.load(source, resolver);
1319
+ return await this.#slot.load(source, resolver, options);
1024
1320
  }
1025
1321
  async loadAsset(asset) {
1026
1322
  if (this.#destroyed) {
@@ -1056,6 +1352,12 @@ var Live2dActorImpl = class {
1056
1352
  listPlayingMotions() {
1057
1353
  return this.#slot.listPlayingMotions();
1058
1354
  }
1355
+ listExpressions() {
1356
+ return this.#slot.listExpressions();
1357
+ }
1358
+ setExpression(name) {
1359
+ return this.#slot.setExpression(name);
1360
+ }
1059
1361
  lookAt(stageX, stageY) {
1060
1362
  const { dragX, dragY } = stageFocusDrag(
1061
1363
  stageX,
@@ -1163,10 +1465,10 @@ function createSingleActorFacade(stage, actor, backends) {
1163
1465
  }
1164
1466
  );
1165
1467
  },
1166
- async loadModel(source, resolver) {
1468
+ async loadModel(source, resolver, options) {
1167
1469
  setPhase("loading");
1168
1470
  try {
1169
- const model = await actor.load(source, resolver);
1471
+ const model = await actor.load(source, resolver, options);
1170
1472
  lastError = null;
1171
1473
  setPhase("live");
1172
1474
  events.emit("ready", { modelId: model.id });
@@ -1212,6 +1514,12 @@ function createSingleActorFacade(stage, actor, backends) {
1212
1514
  listPlayingMotions() {
1213
1515
  return actor.listPlayingMotions();
1214
1516
  },
1517
+ listExpressions() {
1518
+ return actor.listExpressions();
1519
+ },
1520
+ setExpression(name) {
1521
+ return actor.setExpression(name);
1522
+ },
1215
1523
  async capturePng(opts = {}) {
1216
1524
  if (!canvas) {
1217
1525
  throw new Error(
@@ -1273,6 +1581,10 @@ function createSingleActorFacade(stage, actor, backends) {
1273
1581
  events.clear();
1274
1582
  }
1275
1583
  };
1584
+ actor.setMotionEventHandlers({
1585
+ onStart: (payload) => events.emit("motion:start", payload),
1586
+ onFinish: (payload) => events.emit("motion:finish", payload)
1587
+ });
1276
1588
  return runtime;
1277
1589
  }
1278
1590
 
@@ -1402,8 +1714,13 @@ var ModelAssetRegistry = class {
1402
1714
  const entry = await this.#ensureEntry(source, resolver);
1403
1715
  return new ModelAssetHandle(entry);
1404
1716
  }
1405
- async acquire(source, resolver, onProgress) {
1406
- const entry = await this.#ensureEntry(source, resolver, onProgress);
1717
+ async acquire(source, resolver, onProgress, options) {
1718
+ const entry = await this.#ensureEntry(
1719
+ source,
1720
+ resolver,
1721
+ onProgress,
1722
+ options
1723
+ );
1407
1724
  entry.refCount += 1;
1408
1725
  return this.#leaseFromEntry(entry);
1409
1726
  }
@@ -1454,13 +1771,19 @@ var ModelAssetRegistry = class {
1454
1771
  }
1455
1772
  };
1456
1773
  }
1457
- async #ensureEntry(source, resolver, onProgress) {
1774
+ async #ensureEntry(source, resolver, onProgress, options) {
1458
1775
  const key = resolveModelAssetKey(source);
1459
1776
  const existing = this.#entries.get(key);
1460
1777
  if (existing) return existing;
1461
1778
  let pending = this.#inFlight.get(key);
1462
1779
  if (!pending) {
1463
- pending = this.#compileEntry(key, source, resolver, onProgress);
1780
+ pending = this.#compileEntry(
1781
+ key,
1782
+ source,
1783
+ resolver,
1784
+ onProgress,
1785
+ options
1786
+ );
1464
1787
  this.#inFlight.set(key, pending);
1465
1788
  }
1466
1789
  try {
@@ -1471,8 +1794,12 @@ var ModelAssetRegistry = class {
1471
1794
  this.#inFlight.delete(key);
1472
1795
  }
1473
1796
  }
1474
- async #compileEntry(key, source, resolver, onProgress) {
1797
+ async #compileEntry(key, source, resolver, onProgress, options) {
1475
1798
  const notify = (payload) => onProgress?.(payload);
1799
+ const signal = options?.signal;
1800
+ if (signal?.aborted) {
1801
+ throw new Error("@doki-land/live2d: load cancelled");
1802
+ }
1476
1803
  notify({
1477
1804
  stage: "resolve",
1478
1805
  progress: 0.02,
@@ -1501,19 +1828,26 @@ var ModelAssetRegistry = class {
1501
1828
  progress: 0.05,
1502
1829
  detail: fetchUrl
1503
1830
  });
1504
- json = await fetchModelJson(fetchUrl, (u) => {
1505
- const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
1506
- notify({
1507
- stage: "settings",
1508
- progress: lerp(0.05, 0.22, ratio),
1509
- detail: fetchUrl,
1510
- bytesLoaded: u.bytesLoaded,
1511
- bytesTotal: u.bytesTotal
1512
- });
1513
- });
1831
+ json = await fetchModelJson(
1832
+ fetchUrl,
1833
+ (u) => {
1834
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
1835
+ notify({
1836
+ stage: "settings",
1837
+ progress: lerp(0.05, 0.22, ratio),
1838
+ detail: fetchUrl,
1839
+ bytesLoaded: u.bytesLoaded,
1840
+ bytesTotal: u.bytesTotal
1841
+ });
1842
+ },
1843
+ { signal }
1844
+ );
1514
1845
  baseUrl = fetchUrl;
1515
1846
  settingsUrl = fetchUrl;
1516
1847
  }
1848
+ if (signal?.aborted) {
1849
+ throw new Error("@doki-land/live2d: load cancelled");
1850
+ }
1517
1851
  const settings = normalizeModelSettings(json, settingsUrl);
1518
1852
  notify({
1519
1853
  stage: "moc",
@@ -1521,6 +1855,7 @@ var ModelAssetRegistry = class {
1521
1855
  detail: settings.moc
1522
1856
  });
1523
1857
  const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
1858
+ signal,
1524
1859
  onBytesProgress: (assetKey, u) => {
1525
1860
  const isMoc = assetKey === settings.moc;
1526
1861
  const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
@@ -1550,6 +1885,9 @@ var ModelAssetRegistry = class {
1550
1885
  detail: `decode ${settings.format}`
1551
1886
  });
1552
1887
  const mocBytes = await assetResolver.fetchBytes(settings.moc);
1888
+ if (signal?.aborted) {
1889
+ throw new Error("@doki-land/live2d: load cancelled");
1890
+ }
1553
1891
  const sharedCompile = compileSharedModelCompile(settings, mocBytes);
1554
1892
  const textures = [];
1555
1893
  if (settings.textures.length > 0) {
@@ -1573,6 +1911,9 @@ var ModelAssetRegistry = class {
1573
1911
  })
1574
1912
  );
1575
1913
  }
1914
+ if (signal?.aborted) {
1915
+ throw new Error("@doki-land/live2d: load cancelled");
1916
+ }
1576
1917
  notify({
1577
1918
  stage: "ready",
1578
1919
  progress: 1,
@@ -1620,6 +1961,7 @@ var Live2dStageImpl = class {
1620
1961
  ]);
1621
1962
  #drawScratch = new StageDrawableScratch();
1622
1963
  #sortedActors = [];
1964
+ #frameListeners = /* @__PURE__ */ new Set();
1623
1965
  #canvas = null;
1624
1966
  #initPromise = null;
1625
1967
  #rafId = null;
@@ -1717,11 +2059,20 @@ var Live2dStageImpl = class {
1717
2059
  }
1718
2060
  update(deltaTimeSeconds) {
1719
2061
  if (this.#destroyed) return;
2062
+ for (const listener of this.#frameListeners) {
2063
+ listener(deltaTimeSeconds);
2064
+ }
1720
2065
  for (const actor of this.#actors.values()) {
1721
2066
  actor.update(deltaTimeSeconds);
1722
2067
  }
1723
2068
  this.#applyPointerTracking();
1724
2069
  }
2070
+ onFrame(listener) {
2071
+ this.#frameListeners.add(listener);
2072
+ return () => {
2073
+ this.#frameListeners.delete(listener);
2074
+ };
2075
+ }
1725
2076
  render() {
1726
2077
  if (this.#destroyed || !this.#canvas) return;
1727
2078
  const sorted = this.#sortedActors;
@@ -1809,6 +2160,7 @@ var Live2dStageImpl = class {
1809
2160
  if (this.#destroyed) return;
1810
2161
  this.#destroyed = true;
1811
2162
  this.stop();
2163
+ this.#frameListeners.clear();
1812
2164
  this.#detachPointerListeners();
1813
2165
  for (const actor of this.#actors.values()) {
1814
2166
  actor.destroy();
@@ -1931,7 +2283,7 @@ function createLive2dStage(options) {
1931
2283
  }
1932
2284
 
1933
2285
  // src/facade/create-live2d.ts
1934
- function createLive2D(options = {}) {
2286
+ function createLive2d(options = {}) {
1935
2287
  const backends = options.backends ?? [
1936
2288
  createMoc2Backend2(),
1937
2289
  createMoc3Backend2()
@@ -1946,6 +2298,7 @@ function createLive2D(options = {}) {
1946
2298
  });
1947
2299
  return createSingleActorFacade(stage, actor, backends);
1948
2300
  }
2301
+ var createLive2D = createLive2d;
1949
2302
 
1950
2303
  // src/index.ts
1951
2304
  var LIVE2D_VERSION = "0.0.0";
@@ -1956,9 +2309,14 @@ export {
1956
2309
  LIVE2D_VERSION,
1957
2310
  MotionPlayer,
1958
2311
  MotionPriority,
2312
+ allocateActorId,
2313
+ applyExpression3Clip,
2314
+ applyPhysics3,
2315
+ applyPose3Activation,
1959
2316
  blendMotionLayers,
1960
2317
  createCanvas2DRenderer,
1961
2318
  createLive2D,
2319
+ createLive2d,
1962
2320
  createLive2dStage,
1963
2321
  createMoc2Backend3 as createMoc2Backend,
1964
2322
  createMoc3Backend3 as createMoc3Backend,
@@ -1973,7 +2331,11 @@ export {
1973
2331
  fingerprintSnapshot,
1974
2332
  focusParameterUpdates,
1975
2333
  parseCpuProgram,
2334
+ parseExpression3,
1976
2335
  parseMotion3,
2336
+ parsePhysics3,
2337
+ parsePose3,
2338
+ resolveHitAreaName,
1977
2339
  resolveModelSourceUrl2 as resolveModelSourceUrl,
1978
2340
  resolveNpmSpecifier,
1979
2341
  serializeCpuProgram