@doki-land/live2d 0.0.14 → 0.0.16

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,6 +933,12 @@ 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
  }
@@ -1330,9 +1181,320 @@ import {
1330
1181
  createMoc3Backend,
1331
1182
  createRenderer
1332
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
1333
1494
  var Live2dStageImpl = class {
1334
1495
  #backends;
1335
1496
  #renderer;
1497
+ #assets;
1336
1498
  #updateMode;
1337
1499
  #actors = /* @__PURE__ */ new Map();
1338
1500
  #definedLayers = [
@@ -1367,8 +1529,12 @@ var Live2dStageImpl = class {
1367
1529
  createMoc3Backend()
1368
1530
  ];
1369
1531
  this.#renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
1532
+ this.#assets = new ModelAssetRegistry({ backends: this.#backends });
1370
1533
  this.#updateMode = options.updateMode ?? "auto";
1371
1534
  }
1535
+ get assets() {
1536
+ return this.#assets;
1537
+ }
1372
1538
  get actors() {
1373
1539
  return [...this.#actors.values()];
1374
1540
  }
@@ -1402,7 +1568,7 @@ var Live2dStageImpl = class {
1402
1568
  const actor = new Live2dActorImpl(options, {
1403
1569
  id,
1404
1570
  creationIndex,
1405
- backends: this.#backends,
1571
+ assets: this.#assets,
1406
1572
  renderer: this.#renderer
1407
1573
  });
1408
1574
  this.#actors.set(id, actor);
@@ -1534,6 +1700,7 @@ var Live2dStageImpl = class {
1534
1700
  actor.destroy();
1535
1701
  }
1536
1702
  this.#actors.clear();
1703
+ this.#assets.destroy();
1537
1704
  this.#renderer.destroy();
1538
1705
  this.#canvas = null;
1539
1706
  this.#initPromise = null;