@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.d.ts +128 -11
- package/dist/index.js +406 -44
- package/package.json +4 -4
- package/src/expression/apply-expression.ts +41 -0
- package/src/expression/index.ts +7 -0
- package/src/expression/parse-expression3.ts +45 -0
- package/src/expression/types.ts +13 -0
- package/src/facade/create-live2d.ts +10 -3
- package/src/index.ts +32 -4
- package/src/physics/apply-physics3.ts +26 -0
- package/src/physics/index.ts +7 -0
- package/src/physics/parse-physics3.ts +52 -0
- package/src/physics/types.ts +20 -0
- package/src/pose/apply-pose3.ts +19 -0
- package/src/pose/index.ts +3 -0
- package/src/pose/parse-pose3.ts +33 -0
- package/src/pose/types.ts +4 -0
- package/src/stage/actor-model-slot.ts +172 -21
- package/src/stage/actor.ts +36 -1
- package/src/stage/hit-area.ts +24 -0
- package/src/stage/index.ts +5 -1
- package/src/stage/model-asset-registry.ts +49 -15
- package/src/stage/single-facade.ts +27 -7
- package/src/stage/stage.ts +12 -0
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
|
-
|
|
563
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
744
|
-
|
|
1022
|
+
const artMeshId = this.#backend.getDrawableArtMeshId?.(
|
|
1023
|
+
this.#model,
|
|
1024
|
+
d.index
|
|
745
1025
|
);
|
|
746
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
|
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
|