@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/README.md +180 -1
- package/dist/index.d.ts +72 -7
- package/dist/index.js +407 -208
- package/package.json +9 -7
- package/src/index.ts +4 -0
- package/src/stage/actor-model-slot.ts +63 -167
- package/src/stage/actor.ts +42 -7
- package/src/stage/model-asset-key.ts +19 -0
- package/src/stage/model-asset-registry.ts +337 -0
- package/src/stage/single-facade.ts +5 -5
- package/src/stage/stage.ts +27 -1
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
|
-
#
|
|
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
|
-
#
|
|
627
|
-
#resolver = null;
|
|
554
|
+
#lease = null;
|
|
628
555
|
#loadGeneration = 0;
|
|
629
556
|
constructor(options) {
|
|
630
|
-
this.#
|
|
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
|
-
#
|
|
654
|
-
|
|
655
|
-
|
|
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.#
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
633
|
+
return await this.#attachLease(lease, drawPass, gen);
|
|
634
|
+
}
|
|
635
|
+
async loadAsset(asset) {
|
|
636
|
+
const gen = ++this.#loadGeneration;
|
|
745
637
|
this.#report({
|
|
746
|
-
stage: "
|
|
747
|
-
progress: 0.
|
|
748
|
-
detail:
|
|
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
|
-
|
|
774
|
-
this.#
|
|
775
|
-
this.#
|
|
776
|
-
|
|
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: `
|
|
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(
|
|
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
|
-
|
|
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:
|
|
674
|
+
detail: model.id
|
|
830
675
|
});
|
|
831
|
-
return
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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.
|
|
1100
|
+
return actor.listParameters();
|
|
1231
1101
|
},
|
|
1232
1102
|
listMotionGroups() {
|
|
1233
|
-
return actor.
|
|
1103
|
+
return actor.listMotionGroups();
|
|
1234
1104
|
},
|
|
1235
1105
|
playMotion(group, index, options) {
|
|
1236
|
-
return actor.
|
|
1106
|
+
return actor.playMotion(group, index, options);
|
|
1237
1107
|
},
|
|
1238
1108
|
stopMotion(opts) {
|
|
1239
|
-
actor.
|
|
1109
|
+
actor.stopMotion(opts);
|
|
1240
1110
|
},
|
|
1241
1111
|
listPlayingMotions() {
|
|
1242
|
-
return actor.
|
|
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
|
-
|
|
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;
|