@doki-land/live2d 0.0.13 → 0.0.15

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
@@ -57,74 +57,6 @@ function valueFromNormalized(binding, normalized) {
57
57
  return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
58
58
  }
59
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
60
  // src/motion/evaluate-curve.ts
129
61
  function evaluateMotion3(clip, timeSeconds) {
130
62
  const t = clamp(timeSeconds, 0, clip.duration);
@@ -611,23 +543,18 @@ function optionalNum(v) {
611
543
  }
612
544
 
613
545
  // 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
546
  var ActorModelSlot = class {
618
- #backends;
547
+ #assets;
619
548
  #renderer;
620
549
  #onProgress;
621
550
  #motionPlayer;
622
- #motionCache = /* @__PURE__ */ new Map();
623
551
  #drawPass = null;
624
552
  #model = null;
625
553
  #backend = null;
626
- #textures = [];
627
- #resolver = null;
554
+ #lease = null;
628
555
  #loadGeneration = 0;
629
556
  constructor(options) {
630
- this.#backends = options.backends;
557
+ this.#assets = options.assets;
631
558
  this.#renderer = options.renderer;
632
559
  this.#onProgress = options.onProgress;
633
560
  this.#motionPlayer = new MotionPlayer({
@@ -650,12 +577,9 @@ var ActorModelSlot = class {
650
577
  #report(payload) {
651
578
  this.#onProgress?.(payload);
652
579
  }
653
- #clearTextures() {
654
- if (this.#textures.length > 0) {
655
- releaseTextureData(this.#textures);
656
- this.#textures = [];
657
- }
658
- this.#drawPass?.setTextures([]);
580
+ #releaseLease() {
581
+ this.#lease?.release();
582
+ this.#lease = null;
659
583
  }
660
584
  #applyMotionSamples(samples) {
661
585
  if (!this.#model || !this.#backend) return;
@@ -697,138 +621,59 @@ var ActorModelSlot = class {
697
621
  detail: "prepare draw pass"
698
622
  });
699
623
  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
- }
624
+ const lease = await this.#assets.acquire(
625
+ source,
626
+ resolver,
627
+ (p) => this.#report(p)
628
+ );
741
629
  if (gen !== this.#loadGeneration) {
630
+ lease.release();
742
631
  throw new Error("@doki-land/live2d: load cancelled");
743
632
  }
744
- const settings = normalizeModelSettings(json, settingsUrl);
633
+ return await this.#attachLease(lease, drawPass, gen);
634
+ }
635
+ async loadAsset(asset) {
636
+ const gen = ++this.#loadGeneration;
745
637
  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
- }
638
+ stage: "mounting",
639
+ progress: 0.01,
640
+ detail: "prepare draw pass"
772
641
  });
773
- this.#resolver = assetResolver;
774
- this.#motionPlayer.clear();
775
- this.#motionCache.clear();
776
- const backend = selectModelBackend([...this.#backends], json);
642
+ const drawPass = this.ensureDrawPass();
643
+ const lease = this.#assets.acquireExisting(asset);
644
+ if (gen !== this.#loadGeneration) {
645
+ lease.release();
646
+ throw new Error("@doki-land/live2d: load cancelled");
647
+ }
777
648
  this.#report({
778
649
  stage: "decode",
779
650
  progress: 0.85,
780
- detail: `decode ${settings.format}`
781
- });
782
- const next = await backend.createModel(settings, {
783
- renderer: this.#renderer,
784
- resolver: assetResolver
651
+ detail: `reuse ${asset.key}`
785
652
  });
653
+ return await this.#attachLease(lease, drawPass, gen);
654
+ }
655
+ async #attachLease(lease, drawPass, gen) {
656
+ this.#motionPlayer.clear();
657
+ this.#releaseLease();
658
+ const { model, backend } = await lease.createInstance(this.#renderer);
786
659
  if (gen !== this.#loadGeneration) {
787
- backend.destroyModel(next);
660
+ backend.destroyModel(model);
661
+ lease.release();
788
662
  throw new Error("@doki-land/live2d: load cancelled");
789
663
  }
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
664
  if (this.#model && this.#backend) {
822
665
  this.#backend.destroyModel(this.#model);
823
666
  }
824
- this.#model = next;
667
+ drawPass.setTextures([...lease.textures]);
668
+ this.#lease = lease;
669
+ this.#model = model;
825
670
  this.#backend = backend;
826
671
  this.#report({
827
672
  stage: "ready",
828
673
  progress: 1,
829
- detail: next.id
674
+ detail: model.id
830
675
  });
831
- return next;
676
+ return model;
832
677
  }
833
678
  setParameter(id, value) {
834
679
  if (!this.#model || !this.#backend?.setParameter) return;
@@ -842,15 +687,16 @@ var ActorModelSlot = class {
842
687
  return this.#model?.settings.motionGroups ?? {};
843
688
  }
844
689
  async playMotion(group, index = 0, options = {}) {
845
- if (!this.#model || !this.#resolver) return false;
690
+ if (!this.#model || !this.#lease) return false;
846
691
  const list = this.#model.settings.motionGroups[group];
847
692
  const def = list?.[index];
848
693
  if (!def) return false;
849
- let clip = this.#motionCache.get(def.file);
694
+ const cache = this.#lease.motionCache;
695
+ let clip = cache.get(def.file);
850
696
  if (!clip) {
851
- const json = await this.#resolver.fetchJson(def.file);
697
+ const json = await this.#lease.resolver.fetchJson(def.file);
852
698
  clip = parseMotion3(json);
853
- this.#motionCache.set(def.file, clip);
699
+ cache.set(def.file, clip);
854
700
  }
855
701
  const fadeInTime = options.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
856
702
  const fadeOutTime = options.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
@@ -904,14 +750,13 @@ var ActorModelSlot = class {
904
750
  destroy() {
905
751
  this.#loadGeneration += 1;
906
752
  this.#motionPlayer.clear();
907
- this.#motionCache.clear();
908
- this.#resolver = null;
909
753
  if (this.#model && this.#backend) {
910
754
  this.#backend.destroyModel(this.#model);
911
755
  }
912
756
  this.#model = null;
913
757
  this.#backend = null;
914
- this.#clearTextures();
758
+ this.#releaseLease();
759
+ this.#drawPass?.setTextures([]);
915
760
  this.#drawPass?.destroy();
916
761
  this.#drawPass = null;
917
762
  }
@@ -1042,7 +887,7 @@ var Live2dActorImpl = class {
1042
887
  this.#layer = options?.layer ?? "characters";
1043
888
  this.#order = options?.order ?? 0;
1044
889
  this.#slot = new ActorModelSlot({
1045
- backends: shared.backends,
890
+ assets: shared.assets,
1046
891
  renderer: shared.renderer
1047
892
  });
1048
893
  }
@@ -1088,9 +933,34 @@ var Live2dActorImpl = class {
1088
933
  }
1089
934
  return await this.#slot.load(source, resolver);
1090
935
  }
936
+ async loadAsset(asset) {
937
+ if (this.#destroyed) {
938
+ throw new Error("@doki-land/live2d: actor destroyed");
939
+ }
940
+ return await this.#slot.loadAsset(asset);
941
+ }
1091
942
  setParameter(id, value) {
1092
943
  this.#slot.setParameter(id, value);
1093
944
  }
945
+ listParameters() {
946
+ return this.#slot.listParameters();
947
+ }
948
+ listMotionGroups() {
949
+ return this.#slot.listMotionGroups();
950
+ }
951
+ playMotion(group, index, options) {
952
+ return this.#slot.playMotion(
953
+ group,
954
+ index,
955
+ options
956
+ );
957
+ }
958
+ stopMotion(opts) {
959
+ this.#slot.stopMotion(opts);
960
+ }
961
+ listPlayingMotions() {
962
+ return this.#slot.listPlayingMotions();
963
+ }
1094
964
  lookAt(stageX, stageY) {
1095
965
  const { dragX, dragY } = stageFocusDrag(
1096
966
  stageX,
@@ -1227,19 +1097,19 @@ function createSingleActorFacade(stage, actor, backends) {
1227
1097
  return hit.area;
1228
1098
  },
1229
1099
  listParameters() {
1230
- return actor.slot.listParameters();
1100
+ return actor.listParameters();
1231
1101
  },
1232
1102
  listMotionGroups() {
1233
- return actor.slot.listMotionGroups();
1103
+ return actor.listMotionGroups();
1234
1104
  },
1235
1105
  playMotion(group, index, options) {
1236
- return actor.slot.playMotion(group, index, options);
1106
+ return actor.playMotion(group, index, options);
1237
1107
  },
1238
1108
  stopMotion(opts) {
1239
- actor.slot.stopMotion(opts);
1109
+ actor.stopMotion(opts);
1240
1110
  },
1241
1111
  listPlayingMotions() {
1242
- return actor.slot.listPlayingMotions();
1112
+ return actor.listPlayingMotions();
1243
1113
  },
1244
1114
  async capturePng(opts = {}) {
1245
1115
  if (!canvas) {
@@ -1311,9 +1181,320 @@ import {
1311
1181
  createMoc3Backend,
1312
1182
  createRenderer
1313
1183
  } from "@doki-land/live2d-renderer";
1184
+
1185
+ // src/stage/model-asset-registry.ts
1186
+ import { modelSourceUrl as modelSourceUrl2 } from "@doki-land/live2d-core";
1187
+ import {
1188
+ createUrlAssetResolver,
1189
+ fetchModelJson,
1190
+ normalizeModelSettings,
1191
+ resolveModelSourceUrl
1192
+ } from "@doki-land/live2d-loader";
1193
+ import {
1194
+ compileSharedModelCompile,
1195
+ selectModelBackend
1196
+ } from "@doki-land/live2d-renderer";
1197
+
1198
+ // src/load-textures.ts
1199
+ function guessMime(path) {
1200
+ const lower = path.toLowerCase();
1201
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
1202
+ if (lower.endsWith(".webp")) return "image/webp";
1203
+ if (lower.endsWith(".gif")) return "image/gif";
1204
+ return "image/png";
1205
+ }
1206
+ async function bytesToImageBitmap(bytes, path) {
1207
+ if (typeof createImageBitmap !== "function") {
1208
+ throw new Error(
1209
+ "@doki-land/live2d: createImageBitmap is not available in this environment"
1210
+ );
1211
+ }
1212
+ const blob = new Blob([new Uint8Array(bytes)], {
1213
+ type: guessMime(path)
1214
+ });
1215
+ return createImageBitmap(blob);
1216
+ }
1217
+ async function loadTextureData(resolver, paths, options = {}) {
1218
+ const out = [];
1219
+ const total = paths.length;
1220
+ for (let i = 0; i < paths.length; i++) {
1221
+ const key = paths[i];
1222
+ options.onProgress?.({
1223
+ index: i,
1224
+ total,
1225
+ key,
1226
+ bytesLoaded: 0,
1227
+ bytesTotal: null
1228
+ });
1229
+ const bytes = await resolver.fetchBytes(key);
1230
+ options.onProgress?.({
1231
+ index: i,
1232
+ total,
1233
+ key,
1234
+ bytesLoaded: bytes.byteLength,
1235
+ bytesTotal: bytes.byteLength
1236
+ });
1237
+ const image = await bytesToImageBitmap(bytes, key);
1238
+ out.push({
1239
+ index: i,
1240
+ image,
1241
+ width: image.width,
1242
+ height: image.height
1243
+ });
1244
+ }
1245
+ return out;
1246
+ }
1247
+ function releaseTextureData(textures) {
1248
+ for (const t of textures) {
1249
+ const img = t.image;
1250
+ if (typeof ImageBitmap !== "undefined" && img instanceof ImageBitmap) {
1251
+ img.close();
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // src/stage/model-asset-key.ts
1257
+ import { modelSourceUrl } from "@doki-land/live2d-core";
1258
+ function stableJsonKey(value) {
1259
+ let h = 5381;
1260
+ const text = JSON.stringify(value);
1261
+ for (let i = 0; i < text.length; i += 1) {
1262
+ h = (Math.imul(h, 33) ^ text.charCodeAt(i)) >>> 0;
1263
+ }
1264
+ return h.toString(36);
1265
+ }
1266
+ function resolveModelAssetKey(source) {
1267
+ if (typeof source === "object" && source.kind === "json") {
1268
+ return `json:${source.baseUrl}#${stableJsonKey(source.json)}`;
1269
+ }
1270
+ return modelSourceUrl(source);
1271
+ }
1272
+
1273
+ // src/stage/model-asset-registry.ts
1274
+ function lerp(a, b, t) {
1275
+ return a + (b - a) * Math.min(1, Math.max(0, t));
1276
+ }
1277
+ var ModelAssetHandle = class {
1278
+ #entry;
1279
+ constructor(entry) {
1280
+ this.#entry = entry;
1281
+ }
1282
+ get key() {
1283
+ return this.#entry.key;
1284
+ }
1285
+ get settings() {
1286
+ return this.#entry.settings;
1287
+ }
1288
+ /** @internal */
1289
+ get entry() {
1290
+ return this.#entry;
1291
+ }
1292
+ };
1293
+ var ModelAssetRegistry = class {
1294
+ #backends;
1295
+ #entries = /* @__PURE__ */ new Map();
1296
+ #inFlight = /* @__PURE__ */ new Map();
1297
+ constructor(options) {
1298
+ this.#backends = options.backends;
1299
+ }
1300
+ async load(source, resolver) {
1301
+ const entry = await this.#ensureEntry(source, resolver);
1302
+ return new ModelAssetHandle(entry);
1303
+ }
1304
+ async acquire(source, resolver, onProgress) {
1305
+ const entry = await this.#ensureEntry(source, resolver, onProgress);
1306
+ entry.refCount += 1;
1307
+ return this.#leaseFromEntry(entry);
1308
+ }
1309
+ acquireExisting(asset) {
1310
+ const handle = asset;
1311
+ const entry = handle.entry;
1312
+ if (!this.#entries.has(entry.key) || this.#entries.get(entry.key) !== entry) {
1313
+ throw new Error(
1314
+ "@doki-land/live2d: ModelAsset does not belong to this stage"
1315
+ );
1316
+ }
1317
+ entry.refCount += 1;
1318
+ return this.#leaseFromEntry(entry);
1319
+ }
1320
+ destroy() {
1321
+ this.#inFlight.clear();
1322
+ for (const entry of this.#entries.values()) {
1323
+ releaseTextureData(entry.textures);
1324
+ entry.motionCache.clear();
1325
+ }
1326
+ this.#entries.clear();
1327
+ }
1328
+ #leaseFromEntry(entry) {
1329
+ let released = false;
1330
+ return {
1331
+ asset: new ModelAssetHandle(entry),
1332
+ motionCache: entry.motionCache,
1333
+ resolver: entry.resolver,
1334
+ textures: entry.textures,
1335
+ createInstance: async (renderer) => {
1336
+ const model = await entry.backend.createModel(entry.settings, {
1337
+ renderer,
1338
+ resolver: entry.resolver,
1339
+ sharedCompile: entry.sharedCompile
1340
+ });
1341
+ return { model, backend: entry.backend };
1342
+ },
1343
+ release: () => {
1344
+ if (released) return;
1345
+ released = true;
1346
+ entry.refCount = Math.max(0, entry.refCount - 1);
1347
+ if (entry.refCount === 0) {
1348
+ releaseTextureData(entry.textures);
1349
+ entry.textures.length = 0;
1350
+ entry.motionCache.clear();
1351
+ this.#entries.delete(entry.key);
1352
+ }
1353
+ }
1354
+ };
1355
+ }
1356
+ async #ensureEntry(source, resolver, onProgress) {
1357
+ const key = resolveModelAssetKey(source);
1358
+ const existing = this.#entries.get(key);
1359
+ if (existing) return existing;
1360
+ let pending = this.#inFlight.get(key);
1361
+ if (!pending) {
1362
+ pending = this.#compileEntry(key, source, resolver, onProgress);
1363
+ this.#inFlight.set(key, pending);
1364
+ }
1365
+ try {
1366
+ const entry = await pending;
1367
+ this.#entries.set(key, entry);
1368
+ return entry;
1369
+ } finally {
1370
+ this.#inFlight.delete(key);
1371
+ }
1372
+ }
1373
+ async #compileEntry(key, source, resolver, onProgress) {
1374
+ const notify = (payload) => onProgress?.(payload);
1375
+ notify({
1376
+ stage: "resolve",
1377
+ progress: 0.02,
1378
+ detail: "resolve source"
1379
+ });
1380
+ let json;
1381
+ let baseUrl;
1382
+ let settingsUrl;
1383
+ if (typeof source === "object" && source.kind === "json") {
1384
+ json = source.json;
1385
+ baseUrl = source.baseUrl;
1386
+ settingsUrl = source.baseUrl;
1387
+ notify({
1388
+ stage: "settings",
1389
+ progress: 0.2,
1390
+ detail: "inline settings"
1391
+ });
1392
+ } else {
1393
+ const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl2(source) : source.url;
1394
+ const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
1395
+ const fetchUrl = resolveModelSourceUrl(raw, {
1396
+ npmCdnBase: cdnBase
1397
+ });
1398
+ notify({
1399
+ stage: "settings",
1400
+ progress: 0.05,
1401
+ detail: fetchUrl
1402
+ });
1403
+ json = await fetchModelJson(fetchUrl, (u) => {
1404
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
1405
+ notify({
1406
+ stage: "settings",
1407
+ progress: lerp(0.05, 0.22, ratio),
1408
+ detail: fetchUrl,
1409
+ bytesLoaded: u.bytesLoaded,
1410
+ bytesTotal: u.bytesTotal
1411
+ });
1412
+ });
1413
+ baseUrl = fetchUrl;
1414
+ settingsUrl = fetchUrl;
1415
+ }
1416
+ const settings = normalizeModelSettings(json, settingsUrl);
1417
+ notify({
1418
+ stage: "moc",
1419
+ progress: 0.25,
1420
+ detail: settings.moc
1421
+ });
1422
+ const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
1423
+ onBytesProgress: (assetKey, u) => {
1424
+ const isMoc = assetKey === settings.moc;
1425
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
1426
+ if (isMoc) {
1427
+ notify({
1428
+ stage: "moc",
1429
+ progress: lerp(0.25, 0.8, ratio),
1430
+ detail: assetKey,
1431
+ bytesLoaded: u.bytesLoaded,
1432
+ bytesTotal: u.bytesTotal
1433
+ });
1434
+ } else {
1435
+ notify({
1436
+ stage: "textures",
1437
+ progress: lerp(0.8, 0.9, ratio),
1438
+ detail: assetKey,
1439
+ bytesLoaded: u.bytesLoaded,
1440
+ bytesTotal: u.bytesTotal
1441
+ });
1442
+ }
1443
+ }
1444
+ });
1445
+ const backend = selectModelBackend([...this.#backends], json);
1446
+ notify({
1447
+ stage: "decode",
1448
+ progress: 0.85,
1449
+ detail: `decode ${settings.format}`
1450
+ });
1451
+ const mocBytes = await assetResolver.fetchBytes(settings.moc);
1452
+ const sharedCompile = compileSharedModelCompile(settings, mocBytes);
1453
+ const textures = [];
1454
+ if (settings.textures.length > 0) {
1455
+ notify({
1456
+ stage: "textures",
1457
+ progress: 0.88,
1458
+ detail: `${settings.textures.length} textures`
1459
+ });
1460
+ textures.push(
1461
+ ...await loadTextureData(assetResolver, settings.textures, {
1462
+ onProgress: (u) => {
1463
+ const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
1464
+ notify({
1465
+ stage: "textures",
1466
+ progress: lerp(0.88, 0.96, ratio),
1467
+ detail: u.key,
1468
+ bytesLoaded: u.bytesLoaded,
1469
+ bytesTotal: u.bytesTotal
1470
+ });
1471
+ }
1472
+ })
1473
+ );
1474
+ }
1475
+ notify({
1476
+ stage: "ready",
1477
+ progress: 1,
1478
+ detail: settings.name ?? key
1479
+ });
1480
+ return {
1481
+ key,
1482
+ settings,
1483
+ backend,
1484
+ resolver: assetResolver,
1485
+ sharedCompile,
1486
+ textures,
1487
+ motionCache: /* @__PURE__ */ new Map(),
1488
+ refCount: 0
1489
+ };
1490
+ }
1491
+ };
1492
+
1493
+ // src/stage/stage.ts
1314
1494
  var Live2dStageImpl = class {
1315
1495
  #backends;
1316
1496
  #renderer;
1497
+ #assets;
1317
1498
  #updateMode;
1318
1499
  #actors = /* @__PURE__ */ new Map();
1319
1500
  #definedLayers = [
@@ -1348,8 +1529,12 @@ var Live2dStageImpl = class {
1348
1529
  createMoc3Backend()
1349
1530
  ];
1350
1531
  this.#renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
1532
+ this.#assets = new ModelAssetRegistry({ backends: this.#backends });
1351
1533
  this.#updateMode = options.updateMode ?? "auto";
1352
1534
  }
1535
+ get assets() {
1536
+ return this.#assets;
1537
+ }
1353
1538
  get actors() {
1354
1539
  return [...this.#actors.values()];
1355
1540
  }
@@ -1383,7 +1568,7 @@ var Live2dStageImpl = class {
1383
1568
  const actor = new Live2dActorImpl(options, {
1384
1569
  id,
1385
1570
  creationIndex,
1386
- backends: this.#backends,
1571
+ assets: this.#assets,
1387
1572
  renderer: this.#renderer
1388
1573
  });
1389
1574
  this.#actors.set(id, actor);
@@ -1392,6 +1577,19 @@ var Live2dStageImpl = class {
1392
1577
  }
1393
1578
  return actor;
1394
1579
  }
1580
+ getActor(id) {
1581
+ return this.#actors.get(id) ?? null;
1582
+ }
1583
+ resize(width, height) {
1584
+ if (!this.#canvas || this.#destroyed) return;
1585
+ const cssW = width ?? this.#canvas.clientWidth;
1586
+ const cssH = height ?? this.#canvas.clientHeight;
1587
+ if (cssW <= 0 || cssH <= 0) return;
1588
+ const dpr = typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
1589
+ this.#canvas.width = Math.round(cssW * dpr);
1590
+ this.#canvas.height = Math.round(cssH * dpr);
1591
+ this.#renderer.resize(cssW, cssH);
1592
+ }
1395
1593
  removeActor(actorOrId) {
1396
1594
  const id = typeof actorOrId === "string" ? actorOrId : actorOrId.id;
1397
1595
  const actor = this.#actors.get(id);
@@ -1502,6 +1700,7 @@ var Live2dStageImpl = class {
1502
1700
  actor.destroy();
1503
1701
  }
1504
1702
  this.#actors.clear();
1703
+ this.#assets.destroy();
1505
1704
  this.#renderer.destroy();
1506
1705
  this.#canvas = null;
1507
1706
  this.#initPromise = null;