@doki-land/live2d 0.0.18 → 0.0.19
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 +11 -1
- package/dist/index.js +141 -27
- package/package.json +4 -4
- package/src/stage/actor-model-slot.ts +38 -4
- package/src/stage/actor.ts +9 -1
- package/src/stage/assets/focus.ts +26 -2
- package/src/stage/single-facade.ts +9 -0
- package/src/stage/stage.ts +21 -6
- package/src/stage/transform.ts +87 -20
package/dist/index.d.ts
CHANGED
|
@@ -226,6 +226,9 @@ declare class ActorModelSlot {
|
|
|
226
226
|
get model(): InternalModel | null;
|
|
227
227
|
get drawPass(): ModelDrawPass | null;
|
|
228
228
|
ensureDrawPass(): ModelDrawPass;
|
|
229
|
+
/** Load-time stable id → binding map (binding.value updated on setParameter). */
|
|
230
|
+
parameterMap(): ReadonlyMap<string, ParameterBinding>;
|
|
231
|
+
resolveParameter(id: string): number | undefined;
|
|
229
232
|
load(source: ModelSource, resolver?: AssetResolver): Promise<InternalModel>;
|
|
230
233
|
loadAsset(asset: ModelAsset): Promise<InternalModel>;
|
|
231
234
|
setParameter(id: string, value: number): void;
|
|
@@ -274,6 +277,8 @@ declare class Live2dActorImpl implements Live2dActor {
|
|
|
274
277
|
loadAsset(asset: ModelAsset): Promise<InternalModel>;
|
|
275
278
|
setParameter(id: string, value: number): void;
|
|
276
279
|
listParameters(): readonly _doki_land_live2d_renderer.ParameterBinding[];
|
|
280
|
+
parameterMap(): ReadonlyMap<string, _doki_land_live2d_renderer.ParameterBinding>;
|
|
281
|
+
resolveParameter(id: string): number | undefined;
|
|
277
282
|
listMotionGroups(): Record<string, readonly _doki_land_live2d_core.MotionDefinition[]>;
|
|
278
283
|
playMotion(group: string, index?: number, options?: PlayMotionActorOptions): Promise<boolean>;
|
|
279
284
|
stopMotion(opts?: {
|
|
@@ -343,6 +348,9 @@ interface Live2DRuntime extends Live2DSession {
|
|
|
343
348
|
readonly actor: Live2dActorImpl;
|
|
344
349
|
setParameter(id: string, value: number): void;
|
|
345
350
|
listParameters(): readonly ParameterBinding[];
|
|
351
|
+
/** Stable load-time parameter map for focus / hosts (no per-frame rebuild). */
|
|
352
|
+
parameterMap(): ReadonlyMap<string, ParameterBinding>;
|
|
353
|
+
resolveParameter(id: string): number | undefined;
|
|
346
354
|
hitTest(x: number, y: number): string | null;
|
|
347
355
|
listMotionGroups(): Record<string, readonly _doki_land_live2d_core.MotionDefinition[]>;
|
|
348
356
|
playMotion(group: string, index?: number, options?: PlayMotionOptions): Promise<boolean>;
|
|
@@ -372,8 +380,10 @@ declare function createLive2D(options?: CreateLive2DOptions): Live2DRuntime;
|
|
|
372
380
|
* ANGLE_X/Y use the full declared range, ANGLE_Z combines both axes, and body
|
|
373
381
|
* and eye parameters receive the corresponding normalized axis. Parameters not
|
|
374
382
|
* declared by a model are skipped.
|
|
383
|
+
*
|
|
384
|
+
* Prefer a stable `Map` from load-time bindings to avoid per-frame Map rebuild.
|
|
375
385
|
*/
|
|
376
|
-
declare function focusParameterUpdates(parameters: readonly ParameterBinding[], dragX: number, dragY: number): Array<{
|
|
386
|
+
declare function focusParameterUpdates(parameters: readonly ParameterBinding[] | ReadonlyMap<string, ParameterBinding>, dragX: number, dragY: number): Array<{
|
|
377
387
|
id: string;
|
|
378
388
|
value: number;
|
|
379
389
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -523,6 +523,8 @@ var ActorModelSlot = class {
|
|
|
523
523
|
#backend = null;
|
|
524
524
|
#lease = null;
|
|
525
525
|
#loadGeneration = 0;
|
|
526
|
+
#paramById = /* @__PURE__ */ new Map();
|
|
527
|
+
#paramIndexById = /* @__PURE__ */ new Map();
|
|
526
528
|
constructor(options) {
|
|
527
529
|
this.#assets = options.assets;
|
|
528
530
|
this.#renderer = options.renderer;
|
|
@@ -575,7 +577,7 @@ var ActorModelSlot = class {
|
|
|
575
577
|
this.#backend.setParameter(this.#model, s.id, s.value);
|
|
576
578
|
continue;
|
|
577
579
|
}
|
|
578
|
-
const cur = this.#
|
|
580
|
+
const cur = this.#paramById.get(s.id)?.value ?? s.value;
|
|
579
581
|
this.#backend.setParameter(
|
|
580
582
|
this.#model,
|
|
581
583
|
s.id,
|
|
@@ -583,6 +585,35 @@ var ActorModelSlot = class {
|
|
|
583
585
|
);
|
|
584
586
|
}
|
|
585
587
|
}
|
|
588
|
+
#rebuildParamCache() {
|
|
589
|
+
this.#paramById.clear();
|
|
590
|
+
this.#paramIndexById.clear();
|
|
591
|
+
if (!this.#model || !this.#backend?.listParameters) return;
|
|
592
|
+
const list = this.#backend.listParameters(this.#model);
|
|
593
|
+
for (let i = 0; i < list.length; i++) {
|
|
594
|
+
const p = list[i];
|
|
595
|
+
this.#paramById.set(p.id, p);
|
|
596
|
+
const resolved = this.#backend.resolveParameter?.(
|
|
597
|
+
this.#model,
|
|
598
|
+
p.id
|
|
599
|
+
);
|
|
600
|
+
this.#paramIndexById.set(p.id, resolved ?? i);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
#clearParamCache() {
|
|
604
|
+
this.#paramById.clear();
|
|
605
|
+
this.#paramIndexById.clear();
|
|
606
|
+
}
|
|
607
|
+
/** Load-time stable id → binding map (binding.value updated on setParameter). */
|
|
608
|
+
parameterMap() {
|
|
609
|
+
return this.#paramById;
|
|
610
|
+
}
|
|
611
|
+
resolveParameter(id) {
|
|
612
|
+
if (this.#model && this.#backend?.resolveParameter) {
|
|
613
|
+
return this.#backend.resolveParameter(this.#model, id);
|
|
614
|
+
}
|
|
615
|
+
return this.#paramIndexById.get(id);
|
|
616
|
+
}
|
|
586
617
|
async load(source, resolver) {
|
|
587
618
|
const gen = ++this.#loadGeneration;
|
|
588
619
|
this.#report({
|
|
@@ -638,6 +669,7 @@ var ActorModelSlot = class {
|
|
|
638
669
|
this.#lease = lease;
|
|
639
670
|
this.#model = model;
|
|
640
671
|
this.#backend = backend;
|
|
672
|
+
this.#rebuildParamCache();
|
|
641
673
|
this.#report({
|
|
642
674
|
stage: "ready",
|
|
643
675
|
progress: 1,
|
|
@@ -725,6 +757,7 @@ var ActorModelSlot = class {
|
|
|
725
757
|
}
|
|
726
758
|
this.#model = null;
|
|
727
759
|
this.#backend = null;
|
|
760
|
+
this.#clearParamCache();
|
|
728
761
|
this.#releaseLease();
|
|
729
762
|
this.#drawPass?.setTextures([]);
|
|
730
763
|
this.#drawPass?.destroy();
|
|
@@ -734,7 +767,12 @@ var ActorModelSlot = class {
|
|
|
734
767
|
|
|
735
768
|
// src/stage/assets/focus.ts
|
|
736
769
|
function focusParameterUpdates(parameters, dragX, dragY) {
|
|
737
|
-
const byId =
|
|
770
|
+
const byId = parameters instanceof Map || isParamMap(parameters) ? parameters : new Map(
|
|
771
|
+
parameters.map((p) => [
|
|
772
|
+
p.id,
|
|
773
|
+
p
|
|
774
|
+
])
|
|
775
|
+
);
|
|
738
776
|
const x = clampUnit(dragX);
|
|
739
777
|
const y = clampUnit(dragY);
|
|
740
778
|
const out = [];
|
|
@@ -752,6 +790,9 @@ function focusParameterUpdates(parameters, dragX, dragY) {
|
|
|
752
790
|
set("PARAM_EYE_BALL_Y", y);
|
|
753
791
|
return out;
|
|
754
792
|
}
|
|
793
|
+
function isParamMap(value) {
|
|
794
|
+
return typeof value === "object" && value !== null && typeof value.get === "function" && typeof value.keys === "function" && !Array.isArray(value);
|
|
795
|
+
}
|
|
755
796
|
function clampUnit(n) {
|
|
756
797
|
if (n > 1) return 1;
|
|
757
798
|
if (n < -1) return -1;
|
|
@@ -843,26 +884,74 @@ function compareActorsForDraw(a, b, definedLayers) {
|
|
|
843
884
|
function compareActorsForHit(a, b, definedLayers) {
|
|
844
885
|
return compareActorsForDraw(b, a, definedLayers);
|
|
845
886
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
|
|
887
|
+
var StageDrawableScratch = class {
|
|
888
|
+
#meshes = [];
|
|
889
|
+
#view = [];
|
|
890
|
+
transform(drawables, transform, opacity) {
|
|
891
|
+
const alpha = Math.min(1, Math.max(0, opacity));
|
|
892
|
+
const n = drawables.length;
|
|
893
|
+
while (this.#meshes.length < n) {
|
|
894
|
+
this.#meshes.push(createScratchMesh());
|
|
895
|
+
}
|
|
896
|
+
if (this.#view.length !== n) {
|
|
897
|
+
this.#view = this.#meshes.slice(0, n);
|
|
898
|
+
}
|
|
899
|
+
for (let i = 0; i < n; i++) {
|
|
900
|
+
const src = drawables[i];
|
|
901
|
+
const dst = this.#meshes[i];
|
|
902
|
+
copyMeshMeta(src, dst);
|
|
903
|
+
if (!src.visible || alpha <= 0) {
|
|
904
|
+
dst.visible = false;
|
|
905
|
+
dst.opacity = 0;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const len = src.vertexPositions.length;
|
|
909
|
+
let pos = dst.vertexPositions;
|
|
910
|
+
if (pos.length !== len) {
|
|
911
|
+
pos = new Float32Array(len);
|
|
912
|
+
dst.vertexPositions = pos;
|
|
913
|
+
}
|
|
914
|
+
for (let j = 0; j < len; j += 2) {
|
|
915
|
+
const { stageX, stageY } = modelNdcToStage(
|
|
916
|
+
src.vertexPositions[j],
|
|
917
|
+
src.vertexPositions[j + 1],
|
|
918
|
+
transform
|
|
919
|
+
);
|
|
920
|
+
pos[j] = stageX * 2 - 1;
|
|
921
|
+
pos[j + 1] = 1 - stageY * 2;
|
|
922
|
+
}
|
|
923
|
+
dst.opacity = src.opacity * alpha;
|
|
924
|
+
dst.visible = true;
|
|
859
925
|
}
|
|
860
|
-
return
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
926
|
+
return this.#view;
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
function createScratchMesh() {
|
|
930
|
+
return {
|
|
931
|
+
index: 0,
|
|
932
|
+
textureIndex: 0,
|
|
933
|
+
vertexPositions: new Float32Array(0),
|
|
934
|
+
uvs: new Float32Array(0),
|
|
935
|
+
indices: new Uint16Array(0),
|
|
936
|
+
opacity: 1,
|
|
937
|
+
blendMode: 0,
|
|
938
|
+
invertedMask: false,
|
|
939
|
+
renderOrder: 0,
|
|
940
|
+
dynamicFlag: true,
|
|
941
|
+
maskIndices: [],
|
|
942
|
+
visible: true
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function copyMeshMeta(src, dst) {
|
|
946
|
+
dst.index = src.index;
|
|
947
|
+
dst.textureIndex = src.textureIndex;
|
|
948
|
+
dst.uvs = src.uvs;
|
|
949
|
+
dst.indices = src.indices;
|
|
950
|
+
dst.blendMode = src.blendMode;
|
|
951
|
+
dst.invertedMask = src.invertedMask;
|
|
952
|
+
dst.renderOrder = src.renderOrder;
|
|
953
|
+
dst.dynamicFlag = src.dynamicFlag;
|
|
954
|
+
dst.maskIndices = src.maskIndices;
|
|
866
955
|
}
|
|
867
956
|
|
|
868
957
|
// src/stage/actor.ts
|
|
@@ -945,6 +1034,12 @@ var Live2dActorImpl = class {
|
|
|
945
1034
|
listParameters() {
|
|
946
1035
|
return this.#slot.listParameters();
|
|
947
1036
|
}
|
|
1037
|
+
parameterMap() {
|
|
1038
|
+
return this.#slot.parameterMap();
|
|
1039
|
+
}
|
|
1040
|
+
resolveParameter(id) {
|
|
1041
|
+
return this.#slot.resolveParameter(id);
|
|
1042
|
+
}
|
|
948
1043
|
listMotionGroups() {
|
|
949
1044
|
return this.#slot.listMotionGroups();
|
|
950
1045
|
}
|
|
@@ -968,7 +1063,7 @@ var Live2dActorImpl = class {
|
|
|
968
1063
|
this.#transform
|
|
969
1064
|
);
|
|
970
1065
|
for (const u of focusParameterUpdates(
|
|
971
|
-
this.#slot.
|
|
1066
|
+
this.#slot.parameterMap(),
|
|
972
1067
|
dragX,
|
|
973
1068
|
dragY
|
|
974
1069
|
)) {
|
|
@@ -1099,6 +1194,12 @@ function createSingleActorFacade(stage, actor, backends) {
|
|
|
1099
1194
|
listParameters() {
|
|
1100
1195
|
return actor.listParameters();
|
|
1101
1196
|
},
|
|
1197
|
+
parameterMap() {
|
|
1198
|
+
return actor.parameterMap();
|
|
1199
|
+
},
|
|
1200
|
+
resolveParameter(id) {
|
|
1201
|
+
return actor.resolveParameter(id);
|
|
1202
|
+
},
|
|
1102
1203
|
listMotionGroups() {
|
|
1103
1204
|
return actor.listMotionGroups();
|
|
1104
1205
|
},
|
|
@@ -1491,6 +1592,14 @@ var ModelAssetRegistry = class {
|
|
|
1491
1592
|
};
|
|
1492
1593
|
|
|
1493
1594
|
// src/stage/stage.ts
|
|
1595
|
+
var IDENTITY_MAT4 = /* @__PURE__ */ (() => {
|
|
1596
|
+
const m = new Float32Array(16);
|
|
1597
|
+
m[0] = 1;
|
|
1598
|
+
m[5] = 1;
|
|
1599
|
+
m[10] = 1;
|
|
1600
|
+
m[15] = 1;
|
|
1601
|
+
return m;
|
|
1602
|
+
})();
|
|
1494
1603
|
var Live2dStageImpl = class {
|
|
1495
1604
|
#backends;
|
|
1496
1605
|
#renderer;
|
|
@@ -1509,6 +1618,8 @@ var Live2dStageImpl = class {
|
|
|
1509
1618
|
["pointermove", /* @__PURE__ */ new Set()],
|
|
1510
1619
|
["pointerup", /* @__PURE__ */ new Set()]
|
|
1511
1620
|
]);
|
|
1621
|
+
#drawScratch = new StageDrawableScratch();
|
|
1622
|
+
#sortedActors = [];
|
|
1512
1623
|
#canvas = null;
|
|
1513
1624
|
#initPromise = null;
|
|
1514
1625
|
#rafId = null;
|
|
@@ -1613,21 +1724,24 @@ var Live2dStageImpl = class {
|
|
|
1613
1724
|
}
|
|
1614
1725
|
render() {
|
|
1615
1726
|
if (this.#destroyed || !this.#canvas) return;
|
|
1616
|
-
const sorted =
|
|
1617
|
-
|
|
1618
|
-
)
|
|
1727
|
+
const sorted = this.#sortedActors;
|
|
1728
|
+
sorted.length = 0;
|
|
1729
|
+
for (const actor of this.#actors.values()) {
|
|
1730
|
+
sorted.push(actor);
|
|
1731
|
+
}
|
|
1732
|
+
sorted.sort((a, b) => compareActorsForDraw(a, b, this.#definedLayers));
|
|
1619
1733
|
this.#renderer.beginFrame();
|
|
1620
1734
|
for (const actor of sorted) {
|
|
1621
1735
|
if (!actor.visible || actor.opacity <= 0) continue;
|
|
1622
1736
|
const drawables = actor.lastDrawables;
|
|
1623
1737
|
const pass = actor.slot.drawPass;
|
|
1624
1738
|
if (!drawables || !pass) continue;
|
|
1625
|
-
const placed =
|
|
1739
|
+
const placed = this.#drawScratch.transform(
|
|
1626
1740
|
drawables,
|
|
1627
1741
|
actor.getTransform(),
|
|
1628
1742
|
actor.opacity
|
|
1629
1743
|
);
|
|
1630
|
-
pass.draw(placed,
|
|
1744
|
+
pass.draw(placed, IDENTITY_MAT4);
|
|
1631
1745
|
}
|
|
1632
1746
|
this.#renderer.endFrame();
|
|
1633
1747
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doki-land/live2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Live2D in the browser — load moc2/moc3 models, Stage + multi-actor, motion; WebGPU/WebGL2/Canvas2D. Main entry for live2d.ts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"test": "vitest run --passWithNoTests"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@doki-land/live2d-core": "0.0.
|
|
63
|
-
"@doki-land/live2d-loader": "0.0.
|
|
64
|
-
"@doki-land/live2d-renderer": "0.0.
|
|
62
|
+
"@doki-land/live2d-core": "0.0.19",
|
|
63
|
+
"@doki-land/live2d-loader": "0.0.19",
|
|
64
|
+
"@doki-land/live2d-renderer": "0.0.19"
|
|
65
65
|
},
|
|
66
66
|
"sideEffects": false
|
|
67
67
|
}
|
|
@@ -52,6 +52,8 @@ export class ActorModelSlot {
|
|
|
52
52
|
#backend: ModelBackend | null = null;
|
|
53
53
|
#lease: ModelAssetLease | null = null;
|
|
54
54
|
#loadGeneration = 0;
|
|
55
|
+
readonly #paramById = new Map<string, ParameterBinding>();
|
|
56
|
+
readonly #paramIndexById = new Map<string, number>();
|
|
55
57
|
|
|
56
58
|
constructor(options: ActorModelSlotOptions) {
|
|
57
59
|
this.#assets = options.assets;
|
|
@@ -111,10 +113,7 @@ export class ActorModelSlot {
|
|
|
111
113
|
this.#backend.setParameter(this.#model, s.id, s.value);
|
|
112
114
|
continue;
|
|
113
115
|
}
|
|
114
|
-
const cur =
|
|
115
|
-
this.#backend
|
|
116
|
-
.listParameters?.(this.#model)
|
|
117
|
-
.find((p) => p.id === s.id)?.value ?? s.value;
|
|
116
|
+
const cur = this.#paramById.get(s.id)?.value ?? s.value;
|
|
118
117
|
this.#backend.setParameter(
|
|
119
118
|
this.#model,
|
|
120
119
|
s.id,
|
|
@@ -123,6 +122,39 @@ export class ActorModelSlot {
|
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
124
|
|
|
125
|
+
#rebuildParamCache(): void {
|
|
126
|
+
this.#paramById.clear();
|
|
127
|
+
this.#paramIndexById.clear();
|
|
128
|
+
if (!this.#model || !this.#backend?.listParameters) return;
|
|
129
|
+
const list = this.#backend.listParameters(this.#model);
|
|
130
|
+
for (let i = 0; i < list.length; i++) {
|
|
131
|
+
const p = list[i]!;
|
|
132
|
+
this.#paramById.set(p.id, p);
|
|
133
|
+
const resolved = this.#backend.resolveParameter?.(
|
|
134
|
+
this.#model,
|
|
135
|
+
p.id,
|
|
136
|
+
);
|
|
137
|
+
this.#paramIndexById.set(p.id, resolved ?? i);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#clearParamCache(): void {
|
|
142
|
+
this.#paramById.clear();
|
|
143
|
+
this.#paramIndexById.clear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Load-time stable id → binding map (binding.value updated on setParameter). */
|
|
147
|
+
parameterMap(): ReadonlyMap<string, ParameterBinding> {
|
|
148
|
+
return this.#paramById;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
resolveParameter(id: string): number | undefined {
|
|
152
|
+
if (this.#model && this.#backend?.resolveParameter) {
|
|
153
|
+
return this.#backend.resolveParameter(this.#model, id);
|
|
154
|
+
}
|
|
155
|
+
return this.#paramIndexById.get(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
126
158
|
async load(
|
|
127
159
|
source: ModelSource,
|
|
128
160
|
resolver?: AssetResolver,
|
|
@@ -193,6 +225,7 @@ export class ActorModelSlot {
|
|
|
193
225
|
this.#lease = lease;
|
|
194
226
|
this.#model = model;
|
|
195
227
|
this.#backend = backend;
|
|
228
|
+
this.#rebuildParamCache();
|
|
196
229
|
this.#report({
|
|
197
230
|
stage: "ready",
|
|
198
231
|
progress: 1,
|
|
@@ -317,6 +350,7 @@ export class ActorModelSlot {
|
|
|
317
350
|
}
|
|
318
351
|
this.#model = null;
|
|
319
352
|
this.#backend = null;
|
|
353
|
+
this.#clearParamCache();
|
|
320
354
|
this.#releaseLease();
|
|
321
355
|
this.#drawPass?.setTextures([]);
|
|
322
356
|
this.#drawPass?.destroy();
|
package/src/stage/actor.ts
CHANGED
|
@@ -128,6 +128,14 @@ export class Live2dActorImpl implements Live2dActor {
|
|
|
128
128
|
return this.#slot.listParameters();
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
parameterMap() {
|
|
132
|
+
return this.#slot.parameterMap();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resolveParameter(id: string) {
|
|
136
|
+
return this.#slot.resolveParameter(id);
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
listMotionGroups() {
|
|
132
140
|
return this.#slot.listMotionGroups();
|
|
133
141
|
}
|
|
@@ -159,7 +167,7 @@ export class Live2dActorImpl implements Live2dActor {
|
|
|
159
167
|
this.#transform,
|
|
160
168
|
);
|
|
161
169
|
for (const u of focusParameterUpdates(
|
|
162
|
-
this.#slot.
|
|
170
|
+
this.#slot.parameterMap(),
|
|
163
171
|
dragX,
|
|
164
172
|
dragY,
|
|
165
173
|
)) {
|
|
@@ -6,13 +6,25 @@ import type { ParameterBinding } from "@doki-land/live2d-renderer";
|
|
|
6
6
|
* ANGLE_X/Y use the full declared range, ANGLE_Z combines both axes, and body
|
|
7
7
|
* and eye parameters receive the corresponding normalized axis. Parameters not
|
|
8
8
|
* declared by a model are skipped.
|
|
9
|
+
*
|
|
10
|
+
* Prefer a stable `Map` from load-time bindings to avoid per-frame Map rebuild.
|
|
9
11
|
*/
|
|
10
12
|
export function focusParameterUpdates(
|
|
11
|
-
parameters:
|
|
13
|
+
parameters:
|
|
14
|
+
| readonly ParameterBinding[]
|
|
15
|
+
| ReadonlyMap<string, ParameterBinding>,
|
|
12
16
|
dragX: number,
|
|
13
17
|
dragY: number,
|
|
14
18
|
): Array<{ id: string; value: number }> {
|
|
15
|
-
const byId =
|
|
19
|
+
const byId =
|
|
20
|
+
parameters instanceof Map || isParamMap(parameters)
|
|
21
|
+
? parameters
|
|
22
|
+
: new Map(
|
|
23
|
+
(parameters as readonly ParameterBinding[]).map((p) => [
|
|
24
|
+
p.id,
|
|
25
|
+
p,
|
|
26
|
+
]),
|
|
27
|
+
);
|
|
16
28
|
const x = clampUnit(dragX);
|
|
17
29
|
const y = clampUnit(dragY);
|
|
18
30
|
const out: Array<{ id: string; value: number }> = [];
|
|
@@ -35,6 +47,18 @@ export function focusParameterUpdates(
|
|
|
35
47
|
return out;
|
|
36
48
|
}
|
|
37
49
|
|
|
50
|
+
function isParamMap(
|
|
51
|
+
value: unknown,
|
|
52
|
+
): value is ReadonlyMap<string, ParameterBinding> {
|
|
53
|
+
return (
|
|
54
|
+
typeof value === "object" &&
|
|
55
|
+
value !== null &&
|
|
56
|
+
typeof (value as Map<string, unknown>).get === "function" &&
|
|
57
|
+
typeof (value as Map<string, unknown>).keys === "function" &&
|
|
58
|
+
!Array.isArray(value)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
function clampUnit(n: number): number {
|
|
39
63
|
if (n > 1) return 1;
|
|
40
64
|
if (n < -1) return -1;
|
|
@@ -33,6 +33,9 @@ export interface Live2DRuntime extends Live2DSession {
|
|
|
33
33
|
|
|
34
34
|
setParameter(id: string, value: number): void;
|
|
35
35
|
listParameters(): readonly ParameterBinding[];
|
|
36
|
+
/** Stable load-time parameter map for focus / hosts (no per-frame rebuild). */
|
|
37
|
+
parameterMap(): ReadonlyMap<string, ParameterBinding>;
|
|
38
|
+
resolveParameter(id: string): number | undefined;
|
|
36
39
|
hitTest(x: number, y: number): string | null;
|
|
37
40
|
listMotionGroups(): Record<
|
|
38
41
|
string,
|
|
@@ -141,6 +144,12 @@ export function createSingleActorFacade(
|
|
|
141
144
|
listParameters() {
|
|
142
145
|
return actor.listParameters();
|
|
143
146
|
},
|
|
147
|
+
parameterMap() {
|
|
148
|
+
return actor.parameterMap();
|
|
149
|
+
},
|
|
150
|
+
resolveParameter(id) {
|
|
151
|
+
return actor.resolveParameter(id);
|
|
152
|
+
},
|
|
144
153
|
listMotionGroups() {
|
|
145
154
|
return actor.listMotionGroups();
|
|
146
155
|
},
|
package/src/stage/stage.ts
CHANGED
|
@@ -23,9 +23,19 @@ import {
|
|
|
23
23
|
clientToStage,
|
|
24
24
|
compareActorsForDraw,
|
|
25
25
|
compareActorsForHit,
|
|
26
|
-
|
|
26
|
+
StageDrawableScratch,
|
|
27
27
|
} from "./transform.js";
|
|
28
28
|
|
|
29
|
+
/** Resident identity model matrix — backends currently ignore it; never alloc per frame. */
|
|
30
|
+
export const IDENTITY_MAT4 = /* @__PURE__ */ (() => {
|
|
31
|
+
const m = new Float32Array(16);
|
|
32
|
+
m[0] = 1;
|
|
33
|
+
m[5] = 1;
|
|
34
|
+
m[10] = 1;
|
|
35
|
+
m[15] = 1;
|
|
36
|
+
return m;
|
|
37
|
+
})();
|
|
38
|
+
|
|
29
39
|
export interface CreateLive2dStageFullOptions extends CreateLive2dStageOptions {
|
|
30
40
|
backends?: ModelBackend[];
|
|
31
41
|
renderer?: Renderer;
|
|
@@ -55,6 +65,8 @@ export class Live2dStageImpl implements Live2dStage {
|
|
|
55
65
|
["pointermove", new Set()],
|
|
56
66
|
["pointerup", new Set()],
|
|
57
67
|
]);
|
|
68
|
+
readonly #drawScratch = new StageDrawableScratch();
|
|
69
|
+
readonly #sortedActors: Live2dActorImpl[] = [];
|
|
58
70
|
|
|
59
71
|
#canvas: HTMLCanvasElement | null = null;
|
|
60
72
|
#initPromise: Promise<void> | null = null;
|
|
@@ -178,9 +190,12 @@ export class Live2dStageImpl implements Live2dStage {
|
|
|
178
190
|
|
|
179
191
|
render(): void {
|
|
180
192
|
if (this.#destroyed || !this.#canvas) return;
|
|
181
|
-
const sorted =
|
|
182
|
-
|
|
183
|
-
)
|
|
193
|
+
const sorted = this.#sortedActors;
|
|
194
|
+
sorted.length = 0;
|
|
195
|
+
for (const actor of this.#actors.values()) {
|
|
196
|
+
sorted.push(actor);
|
|
197
|
+
}
|
|
198
|
+
sorted.sort((a, b) => compareActorsForDraw(a, b, this.#definedLayers));
|
|
184
199
|
|
|
185
200
|
this.#renderer.beginFrame();
|
|
186
201
|
for (const actor of sorted) {
|
|
@@ -188,12 +203,12 @@ export class Live2dStageImpl implements Live2dStage {
|
|
|
188
203
|
const drawables = actor.lastDrawables;
|
|
189
204
|
const pass = actor.slot.drawPass;
|
|
190
205
|
if (!drawables || !pass) continue;
|
|
191
|
-
const placed =
|
|
206
|
+
const placed = this.#drawScratch.transform(
|
|
192
207
|
drawables,
|
|
193
208
|
actor.getTransform(),
|
|
194
209
|
actor.opacity,
|
|
195
210
|
);
|
|
196
|
-
pass.draw(placed,
|
|
211
|
+
pass.draw(placed, IDENTITY_MAT4);
|
|
197
212
|
}
|
|
198
213
|
this.#renderer.endFrame();
|
|
199
214
|
}
|
package/src/stage/transform.ts
CHANGED
|
@@ -133,29 +133,96 @@ export function compareActorsForHit<
|
|
|
133
133
|
return compareActorsForDraw(b, a, definedLayers);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Resident scratch for {@link transformDrawablesForStage}: stable mesh objects
|
|
138
|
+
* and vertex buffers (no per-frame `map` / `{...d}` / `new Float32Array`).
|
|
139
|
+
* Safe to reuse across actors when each transformed list is drawn before the
|
|
140
|
+
* next `transform` call.
|
|
141
|
+
*/
|
|
142
|
+
export class StageDrawableScratch {
|
|
143
|
+
readonly #meshes: DrawableMesh[] = [];
|
|
144
|
+
#view: DrawableMesh[] = [];
|
|
145
|
+
|
|
146
|
+
transform(
|
|
147
|
+
drawables: readonly DrawableMesh[],
|
|
148
|
+
transform: ActorTransform,
|
|
149
|
+
opacity: number,
|
|
150
|
+
): readonly DrawableMesh[] {
|
|
151
|
+
const alpha = Math.min(1, Math.max(0, opacity));
|
|
152
|
+
const n = drawables.length;
|
|
153
|
+
while (this.#meshes.length < n) {
|
|
154
|
+
this.#meshes.push(createScratchMesh());
|
|
155
|
+
}
|
|
156
|
+
if (this.#view.length !== n) {
|
|
157
|
+
this.#view = this.#meshes.slice(0, n);
|
|
158
|
+
}
|
|
159
|
+
for (let i = 0; i < n; i++) {
|
|
160
|
+
const src = drawables[i]!;
|
|
161
|
+
const dst = this.#meshes[i]!;
|
|
162
|
+
copyMeshMeta(src, dst);
|
|
163
|
+
if (!src.visible || alpha <= 0) {
|
|
164
|
+
dst.visible = false;
|
|
165
|
+
dst.opacity = 0;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const len = src.vertexPositions.length;
|
|
169
|
+
let pos = dst.vertexPositions;
|
|
170
|
+
if (pos.length !== len) {
|
|
171
|
+
pos = new Float32Array(len);
|
|
172
|
+
dst.vertexPositions = pos;
|
|
173
|
+
}
|
|
174
|
+
for (let j = 0; j < len; j += 2) {
|
|
175
|
+
const { stageX, stageY } = modelNdcToStage(
|
|
176
|
+
src.vertexPositions[j]!,
|
|
177
|
+
src.vertexPositions[j + 1]!,
|
|
178
|
+
transform,
|
|
179
|
+
);
|
|
180
|
+
pos[j] = stageX * 2 - 1;
|
|
181
|
+
pos[j + 1] = 1 - stageY * 2;
|
|
182
|
+
}
|
|
183
|
+
dst.opacity = src.opacity * alpha;
|
|
184
|
+
dst.visible = true;
|
|
185
|
+
}
|
|
186
|
+
return this.#view;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createScratchMesh(): DrawableMesh {
|
|
191
|
+
return {
|
|
192
|
+
index: 0,
|
|
193
|
+
textureIndex: 0,
|
|
194
|
+
vertexPositions: new Float32Array(0),
|
|
195
|
+
uvs: new Float32Array(0),
|
|
196
|
+
indices: new Uint16Array(0),
|
|
197
|
+
opacity: 1,
|
|
198
|
+
blendMode: 0,
|
|
199
|
+
invertedMask: false,
|
|
200
|
+
renderOrder: 0,
|
|
201
|
+
dynamicFlag: true,
|
|
202
|
+
maskIndices: [],
|
|
203
|
+
visible: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function copyMeshMeta(src: DrawableMesh, dst: DrawableMesh): void {
|
|
208
|
+
dst.index = src.index;
|
|
209
|
+
dst.textureIndex = src.textureIndex;
|
|
210
|
+
dst.uvs = src.uvs;
|
|
211
|
+
dst.indices = src.indices;
|
|
212
|
+
dst.blendMode = src.blendMode;
|
|
213
|
+
dst.invertedMask = src.invertedMask;
|
|
214
|
+
dst.renderOrder = src.renderOrder;
|
|
215
|
+
dst.dynamicFlag = src.dynamicFlag;
|
|
216
|
+
dst.maskIndices = src.maskIndices;
|
|
217
|
+
}
|
|
218
|
+
|
|
136
219
|
/** Bake actor stage placement into drawable vertices (renderer stays single-pass). */
|
|
137
220
|
export function transformDrawablesForStage(
|
|
138
221
|
drawables: readonly DrawableMesh[],
|
|
139
222
|
transform: ActorTransform,
|
|
140
223
|
opacity: number,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const pos = new Float32Array(d.vertexPositions.length);
|
|
146
|
-
for (let i = 0; i < pos.length; i += 2) {
|
|
147
|
-
const { stageX, stageY } = modelNdcToStage(
|
|
148
|
-
d.vertexPositions[i]!,
|
|
149
|
-
d.vertexPositions[i + 1]!,
|
|
150
|
-
transform,
|
|
151
|
-
);
|
|
152
|
-
pos[i] = stageX * 2 - 1;
|
|
153
|
-
pos[i + 1] = 1 - stageY * 2;
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
...d,
|
|
157
|
-
vertexPositions: pos,
|
|
158
|
-
opacity: d.opacity * alpha,
|
|
159
|
-
};
|
|
160
|
-
});
|
|
224
|
+
scratch?: StageDrawableScratch,
|
|
225
|
+
): readonly DrawableMesh[] {
|
|
226
|
+
const pool = scratch ?? new StageDrawableScratch();
|
|
227
|
+
return pool.transform(drawables, transform, opacity);
|
|
161
228
|
}
|