@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/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doki-land/live2d",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Live2D
|
|
3
|
+
"version": "0.0.15",
|
|
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",
|
|
7
7
|
"author": "Doki Land",
|
|
8
8
|
"homepage": "https://github.com/doki-land/live2d.ts",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/doki-land/live2d.ts.git"
|
|
11
|
+
"url": "git+https://github.com/doki-land/live2d.ts.git",
|
|
12
|
+
"directory": "projects/live2d"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"live2d",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"dist",
|
|
29
|
-
"src"
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
30
32
|
],
|
|
31
33
|
"publishConfig": {
|
|
32
34
|
"access": "public",
|
|
@@ -57,9 +59,9 @@
|
|
|
57
59
|
"test": "vitest run --passWithNoTests"
|
|
58
60
|
},
|
|
59
61
|
"dependencies": {
|
|
60
|
-
"@doki-land/live2d-core": "0.0.
|
|
61
|
-
"@doki-land/live2d-loader": "0.0.
|
|
62
|
-
"@doki-land/live2d-renderer": "0.0.
|
|
62
|
+
"@doki-land/live2d-core": "0.0.15",
|
|
63
|
+
"@doki-land/live2d-loader": "0.0.15",
|
|
64
|
+
"@doki-land/live2d-renderer": "0.0.15"
|
|
63
65
|
},
|
|
64
66
|
"sideEffects": false
|
|
65
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export type {
|
|
10
10
|
ActorHit,
|
|
11
|
+
ActorInstance,
|
|
11
12
|
ActorTransform,
|
|
12
13
|
AssetResolver,
|
|
13
14
|
CreateActorOptions,
|
|
@@ -18,13 +19,16 @@ export type {
|
|
|
18
19
|
Live2DSession,
|
|
19
20
|
Live2dActor,
|
|
20
21
|
Live2dStage,
|
|
22
|
+
Live2dStageAssets,
|
|
21
23
|
LoadProgress,
|
|
22
24
|
LoadProgressStage,
|
|
25
|
+
ModelAsset,
|
|
23
26
|
ModelFormat,
|
|
24
27
|
ModelInstance,
|
|
25
28
|
ModelProgram,
|
|
26
29
|
ModelSettings,
|
|
27
30
|
ModelSource,
|
|
31
|
+
PlayMotionActorOptions,
|
|
28
32
|
PointerTrackingMode,
|
|
29
33
|
PointerTrackingPolicy,
|
|
30
34
|
SessionPhase,
|
|
@@ -2,38 +2,30 @@ import type {
|
|
|
2
2
|
AssetResolver,
|
|
3
3
|
InternalModel,
|
|
4
4
|
LoadProgress,
|
|
5
|
+
ModelAsset,
|
|
5
6
|
ModelSource,
|
|
6
7
|
} from "@doki-land/live2d-core";
|
|
7
|
-
import { modelSourceUrl } from "@doki-land/live2d-core";
|
|
8
|
-
import {
|
|
9
|
-
createUrlAssetResolver,
|
|
10
|
-
fetchModelJson,
|
|
11
|
-
normalizeModelSettings,
|
|
12
|
-
resolveModelSourceUrl,
|
|
13
|
-
} from "@doki-land/live2d-loader";
|
|
14
8
|
import type {
|
|
15
9
|
DrawableMesh,
|
|
16
10
|
ModelBackend,
|
|
17
11
|
ParameterBinding,
|
|
18
12
|
Renderer,
|
|
19
|
-
TextureData,
|
|
20
13
|
} from "@doki-land/live2d-renderer";
|
|
21
|
-
import { selectModelBackend } from "@doki-land/live2d-renderer";
|
|
22
|
-
import { loadTextureData, releaseTextureData } from "../load-textures.js";
|
|
23
14
|
import {
|
|
24
|
-
type Motion3Clip,
|
|
25
15
|
MotionPlayer,
|
|
26
16
|
MotionPriority,
|
|
27
17
|
type PlayMotionOptions,
|
|
28
18
|
parseMotion3,
|
|
29
19
|
} from "../motion/index.js";
|
|
20
|
+
import type {
|
|
21
|
+
ModelAssetLease,
|
|
22
|
+
ModelAssetRegistry,
|
|
23
|
+
} from "./model-asset-registry.js";
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
return a + (b - a) * Math.min(1, Math.max(0, t));
|
|
33
|
-
}
|
|
25
|
+
type ModelDrawPass = ReturnType<Renderer["createModelDrawPass"]>;
|
|
34
26
|
|
|
35
27
|
export interface ActorModelSlotOptions {
|
|
36
|
-
|
|
28
|
+
assets: ModelAssetRegistry;
|
|
37
29
|
renderer: Renderer;
|
|
38
30
|
onProgress?: (payload: LoadProgress) => void;
|
|
39
31
|
onMotionStart?: (payload: {
|
|
@@ -50,21 +42,19 @@ export interface ActorModelSlotOptions {
|
|
|
50
42
|
|
|
51
43
|
/** One loaded model + draw pass owned by an actor (not a renderer). */
|
|
52
44
|
export class ActorModelSlot {
|
|
53
|
-
readonly #
|
|
45
|
+
readonly #assets: ModelAssetRegistry;
|
|
54
46
|
readonly #renderer: Renderer;
|
|
55
47
|
readonly #onProgress?: (payload: LoadProgress) => void;
|
|
56
48
|
readonly #motionPlayer: MotionPlayer;
|
|
57
|
-
readonly #motionCache = new Map<string, Motion3Clip>();
|
|
58
49
|
|
|
59
|
-
#drawPass:
|
|
50
|
+
#drawPass: ModelDrawPass | null = null;
|
|
60
51
|
#model: InternalModel | null = null;
|
|
61
52
|
#backend: ModelBackend | null = null;
|
|
62
|
-
#
|
|
63
|
-
#resolver: AssetResolver | null = null;
|
|
53
|
+
#lease: ModelAssetLease | null = null;
|
|
64
54
|
#loadGeneration = 0;
|
|
65
55
|
|
|
66
56
|
constructor(options: ActorModelSlotOptions) {
|
|
67
|
-
this.#
|
|
57
|
+
this.#assets = options.assets;
|
|
68
58
|
this.#renderer = options.renderer;
|
|
69
59
|
this.#onProgress = options.onProgress;
|
|
70
60
|
this.#motionPlayer = new MotionPlayer({
|
|
@@ -77,11 +67,11 @@ export class ActorModelSlot {
|
|
|
77
67
|
return this.#model;
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
get drawPass():
|
|
70
|
+
get drawPass(): ModelDrawPass | null {
|
|
81
71
|
return this.#drawPass;
|
|
82
72
|
}
|
|
83
73
|
|
|
84
|
-
ensureDrawPass():
|
|
74
|
+
ensureDrawPass(): ModelDrawPass {
|
|
85
75
|
if (!this.#drawPass) {
|
|
86
76
|
this.#drawPass = this.#renderer.createModelDrawPass();
|
|
87
77
|
}
|
|
@@ -92,12 +82,9 @@ export class ActorModelSlot {
|
|
|
92
82
|
this.#onProgress?.(payload);
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.#textures = [];
|
|
99
|
-
}
|
|
100
|
-
this.#drawPass?.setTextures([]);
|
|
85
|
+
#releaseLease(): void {
|
|
86
|
+
this.#lease?.release();
|
|
87
|
+
this.#lease = null;
|
|
101
88
|
}
|
|
102
89
|
|
|
103
90
|
#applyMotionSamples(samples: ReturnType<MotionPlayer["update"]>): void {
|
|
@@ -148,161 +135,70 @@ export class ActorModelSlot {
|
|
|
148
135
|
});
|
|
149
136
|
const drawPass = this.ensureDrawPass();
|
|
150
137
|
|
|
151
|
-
this.#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
detail: "resolve source",
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
let json: unknown;
|
|
158
|
-
let baseUrl: string;
|
|
159
|
-
let settingsUrl: string;
|
|
160
|
-
|
|
161
|
-
if (typeof source === "object" && source.kind === "json") {
|
|
162
|
-
json = source.json;
|
|
163
|
-
baseUrl = source.baseUrl;
|
|
164
|
-
settingsUrl = source.baseUrl;
|
|
165
|
-
this.#report({
|
|
166
|
-
stage: "settings",
|
|
167
|
-
progress: 0.2,
|
|
168
|
-
detail: "inline settings",
|
|
169
|
-
});
|
|
170
|
-
} else {
|
|
171
|
-
const raw =
|
|
172
|
-
typeof source === "string"
|
|
173
|
-
? source
|
|
174
|
-
: source.kind === "npm"
|
|
175
|
-
? modelSourceUrl(source)
|
|
176
|
-
: source.url;
|
|
177
|
-
const cdnBase =
|
|
178
|
-
typeof source === "object" && source.kind === "npm"
|
|
179
|
-
? source.cdnBase
|
|
180
|
-
: undefined;
|
|
181
|
-
const fetchUrl = resolveModelSourceUrl(raw, {
|
|
182
|
-
npmCdnBase: cdnBase,
|
|
183
|
-
});
|
|
184
|
-
this.#report({
|
|
185
|
-
stage: "settings",
|
|
186
|
-
progress: 0.05,
|
|
187
|
-
detail: fetchUrl,
|
|
188
|
-
});
|
|
189
|
-
json = await fetchModelJson(fetchUrl, (u) => {
|
|
190
|
-
const ratio =
|
|
191
|
-
u.bytesTotal && u.bytesTotal > 0
|
|
192
|
-
? u.bytesLoaded / u.bytesTotal
|
|
193
|
-
: 0;
|
|
194
|
-
this.#report({
|
|
195
|
-
stage: "settings",
|
|
196
|
-
progress: lerp(0.05, 0.22, ratio),
|
|
197
|
-
detail: fetchUrl,
|
|
198
|
-
bytesLoaded: u.bytesLoaded,
|
|
199
|
-
bytesTotal: u.bytesTotal,
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
baseUrl = fetchUrl;
|
|
203
|
-
settingsUrl = fetchUrl;
|
|
204
|
-
}
|
|
138
|
+
const lease = await this.#assets.acquire(source, resolver, (p) =>
|
|
139
|
+
this.#report(p),
|
|
140
|
+
);
|
|
205
141
|
if (gen !== this.#loadGeneration) {
|
|
142
|
+
lease.release();
|
|
206
143
|
throw new Error("@doki-land/live2d: load cancelled");
|
|
207
144
|
}
|
|
208
145
|
|
|
209
|
-
|
|
146
|
+
return await this.#attachLease(lease, drawPass, gen);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async loadAsset(asset: ModelAsset): Promise<InternalModel> {
|
|
150
|
+
const gen = ++this.#loadGeneration;
|
|
210
151
|
this.#report({
|
|
211
|
-
stage: "
|
|
212
|
-
progress: 0.
|
|
213
|
-
detail:
|
|
152
|
+
stage: "mounting",
|
|
153
|
+
progress: 0.01,
|
|
154
|
+
detail: "prepare draw pass",
|
|
214
155
|
});
|
|
156
|
+
const drawPass = this.ensureDrawPass();
|
|
215
157
|
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const ratio =
|
|
222
|
-
u.bytesTotal && u.bytesTotal > 0
|
|
223
|
-
? u.bytesLoaded / u.bytesTotal
|
|
224
|
-
: 0;
|
|
225
|
-
if (isMoc) {
|
|
226
|
-
this.#report({
|
|
227
|
-
stage: "moc",
|
|
228
|
-
progress: lerp(0.25, 0.8, ratio),
|
|
229
|
-
detail: key,
|
|
230
|
-
bytesLoaded: u.bytesLoaded,
|
|
231
|
-
bytesTotal: u.bytesTotal,
|
|
232
|
-
});
|
|
233
|
-
} else {
|
|
234
|
-
this.#report({
|
|
235
|
-
stage: "textures",
|
|
236
|
-
progress: lerp(0.8, 0.9, ratio),
|
|
237
|
-
detail: key,
|
|
238
|
-
bytesLoaded: u.bytesLoaded,
|
|
239
|
-
bytesTotal: u.bytesTotal,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
this.#resolver = assetResolver;
|
|
245
|
-
this.#motionPlayer.clear();
|
|
246
|
-
this.#motionCache.clear();
|
|
158
|
+
const lease = this.#assets.acquireExisting(asset);
|
|
159
|
+
if (gen !== this.#loadGeneration) {
|
|
160
|
+
lease.release();
|
|
161
|
+
throw new Error("@doki-land/live2d: load cancelled");
|
|
162
|
+
}
|
|
247
163
|
|
|
248
|
-
const backend = selectModelBackend([...this.#backends], json);
|
|
249
164
|
this.#report({
|
|
250
165
|
stage: "decode",
|
|
251
166
|
progress: 0.85,
|
|
252
|
-
detail: `
|
|
253
|
-
});
|
|
254
|
-
const next = await backend.createModel(settings, {
|
|
255
|
-
renderer: this.#renderer,
|
|
256
|
-
resolver: assetResolver,
|
|
167
|
+
detail: `reuse ${asset.key}`,
|
|
257
168
|
});
|
|
169
|
+
|
|
170
|
+
return await this.#attachLease(lease, drawPass, gen);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async #attachLease(
|
|
174
|
+
lease: ModelAssetLease,
|
|
175
|
+
drawPass: ModelDrawPass,
|
|
176
|
+
gen: number,
|
|
177
|
+
): Promise<InternalModel> {
|
|
178
|
+
this.#motionPlayer.clear();
|
|
179
|
+
this.#releaseLease();
|
|
180
|
+
|
|
181
|
+
const { model, backend } = await lease.createInstance(this.#renderer);
|
|
258
182
|
if (gen !== this.#loadGeneration) {
|
|
259
|
-
backend.destroyModel(
|
|
183
|
+
backend.destroyModel(model);
|
|
184
|
+
lease.release();
|
|
260
185
|
throw new Error("@doki-land/live2d: load cancelled");
|
|
261
186
|
}
|
|
262
187
|
|
|
263
|
-
this.#clearTextures();
|
|
264
|
-
if (settings.textures.length > 0) {
|
|
265
|
-
this.#report({
|
|
266
|
-
stage: "textures",
|
|
267
|
-
progress: 0.88,
|
|
268
|
-
detail: `${settings.textures.length} textures`,
|
|
269
|
-
});
|
|
270
|
-
const textures = await loadTextureData(
|
|
271
|
-
assetResolver,
|
|
272
|
-
settings.textures,
|
|
273
|
-
{
|
|
274
|
-
onProgress: (u) => {
|
|
275
|
-
const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
|
|
276
|
-
this.#report({
|
|
277
|
-
stage: "textures",
|
|
278
|
-
progress: lerp(0.88, 0.96, ratio),
|
|
279
|
-
detail: u.key,
|
|
280
|
-
bytesLoaded: u.bytesLoaded,
|
|
281
|
-
bytesTotal: u.bytesTotal,
|
|
282
|
-
});
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
);
|
|
286
|
-
if (gen !== this.#loadGeneration) {
|
|
287
|
-
releaseTextureData(textures);
|
|
288
|
-
backend.destroyModel(next);
|
|
289
|
-
throw new Error("@doki-land/live2d: load cancelled");
|
|
290
|
-
}
|
|
291
|
-
this.#textures = textures;
|
|
292
|
-
drawPass.setTextures(textures);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
188
|
if (this.#model && this.#backend) {
|
|
296
189
|
this.#backend.destroyModel(this.#model);
|
|
297
190
|
}
|
|
298
|
-
|
|
191
|
+
|
|
192
|
+
drawPass.setTextures([...lease.textures]);
|
|
193
|
+
this.#lease = lease;
|
|
194
|
+
this.#model = model;
|
|
299
195
|
this.#backend = backend;
|
|
300
196
|
this.#report({
|
|
301
197
|
stage: "ready",
|
|
302
198
|
progress: 1,
|
|
303
|
-
detail:
|
|
199
|
+
detail: model.id,
|
|
304
200
|
});
|
|
305
|
-
return
|
|
201
|
+
return model;
|
|
306
202
|
}
|
|
307
203
|
|
|
308
204
|
setParameter(id: string, value: number): void {
|
|
@@ -327,16 +223,17 @@ export class ActorModelSlot {
|
|
|
327
223
|
index = 0,
|
|
328
224
|
options: PlayMotionOptions = {},
|
|
329
225
|
): Promise<boolean> {
|
|
330
|
-
if (!this.#model || !this.#
|
|
226
|
+
if (!this.#model || !this.#lease) return false;
|
|
331
227
|
const list = this.#model.settings.motionGroups[group];
|
|
332
228
|
const def = list?.[index];
|
|
333
229
|
if (!def) return false;
|
|
334
230
|
|
|
335
|
-
|
|
231
|
+
const cache = this.#lease.motionCache;
|
|
232
|
+
let clip = cache.get(def.file);
|
|
336
233
|
if (!clip) {
|
|
337
|
-
const json = await this.#resolver.fetchJson(def.file);
|
|
234
|
+
const json = await this.#lease.resolver.fetchJson(def.file);
|
|
338
235
|
clip = parseMotion3(json);
|
|
339
|
-
|
|
236
|
+
cache.set(def.file, clip);
|
|
340
237
|
}
|
|
341
238
|
|
|
342
239
|
const fadeInTime =
|
|
@@ -415,14 +312,13 @@ export class ActorModelSlot {
|
|
|
415
312
|
destroy(): void {
|
|
416
313
|
this.#loadGeneration += 1;
|
|
417
314
|
this.#motionPlayer.clear();
|
|
418
|
-
this.#motionCache.clear();
|
|
419
|
-
this.#resolver = null;
|
|
420
315
|
if (this.#model && this.#backend) {
|
|
421
316
|
this.#backend.destroyModel(this.#model);
|
|
422
317
|
}
|
|
423
318
|
this.#model = null;
|
|
424
319
|
this.#backend = null;
|
|
425
|
-
this.#
|
|
320
|
+
this.#releaseLease();
|
|
321
|
+
this.#drawPass?.setTextures([]);
|
|
426
322
|
this.#drawPass?.destroy();
|
|
427
323
|
this.#drawPass = null;
|
|
428
324
|
}
|
package/src/stage/actor.ts
CHANGED
|
@@ -4,14 +4,14 @@ import type {
|
|
|
4
4
|
CreateActorOptions,
|
|
5
5
|
InternalModel,
|
|
6
6
|
Live2dActor,
|
|
7
|
+
ModelAsset,
|
|
8
|
+
PlayMotionActorOptions,
|
|
7
9
|
} from "@doki-land/live2d-core";
|
|
8
|
-
import type {
|
|
9
|
-
DrawableMesh,
|
|
10
|
-
ModelBackend,
|
|
11
|
-
Renderer,
|
|
12
|
-
} from "@doki-land/live2d-renderer";
|
|
10
|
+
import type { DrawableMesh, Renderer } from "@doki-land/live2d-renderer";
|
|
13
11
|
import { focusParameterUpdates } from "../focus.js";
|
|
12
|
+
import type { PlayMotionOptions } from "../motion/index.js";
|
|
14
13
|
import { ActorModelSlot } from "./actor-model-slot.js";
|
|
14
|
+
import type { ModelAssetRegistry } from "./model-asset-registry.js";
|
|
15
15
|
import {
|
|
16
16
|
resolveActorTransform,
|
|
17
17
|
stageFocusDrag,
|
|
@@ -23,7 +23,7 @@ let nextActorId = 0;
|
|
|
23
23
|
export interface Live2dActorImplOptions {
|
|
24
24
|
id: string;
|
|
25
25
|
creationIndex: number;
|
|
26
|
-
|
|
26
|
+
assets: ModelAssetRegistry;
|
|
27
27
|
renderer: Renderer;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -51,7 +51,7 @@ export class Live2dActorImpl implements Live2dActor {
|
|
|
51
51
|
this.#layer = options?.layer ?? "characters";
|
|
52
52
|
this.#order = options?.order ?? 0;
|
|
53
53
|
this.#slot = new ActorModelSlot({
|
|
54
|
-
|
|
54
|
+
assets: shared.assets,
|
|
55
55
|
renderer: shared.renderer,
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -113,10 +113,45 @@ export class Live2dActorImpl implements Live2dActor {
|
|
|
113
113
|
return await this.#slot.load(source, resolver);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
async loadAsset(asset: ModelAsset): Promise<InternalModel> {
|
|
117
|
+
if (this.#destroyed) {
|
|
118
|
+
throw new Error("@doki-land/live2d: actor destroyed");
|
|
119
|
+
}
|
|
120
|
+
return await this.#slot.loadAsset(asset);
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
setParameter(id: string, value: number): void {
|
|
117
124
|
this.#slot.setParameter(id, value);
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
listParameters() {
|
|
128
|
+
return this.#slot.listParameters();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
listMotionGroups() {
|
|
132
|
+
return this.#slot.listMotionGroups();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
playMotion(
|
|
136
|
+
group: string,
|
|
137
|
+
index?: number,
|
|
138
|
+
options?: PlayMotionActorOptions,
|
|
139
|
+
) {
|
|
140
|
+
return this.#slot.playMotion(
|
|
141
|
+
group,
|
|
142
|
+
index,
|
|
143
|
+
options as PlayMotionOptions | undefined,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stopMotion(opts?: { fade?: boolean; slot?: string }) {
|
|
148
|
+
this.#slot.stopMotion(opts);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
listPlayingMotions() {
|
|
152
|
+
return this.#slot.listPlayingMotions();
|
|
153
|
+
}
|
|
154
|
+
|
|
120
155
|
lookAt(stageX: number, stageY: number): void {
|
|
121
156
|
const { dragX, dragY } = stageFocusDrag(
|
|
122
157
|
stageX,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ModelSource } from "@doki-land/live2d-core";
|
|
2
|
+
import { modelSourceUrl } from "@doki-land/live2d-core";
|
|
3
|
+
|
|
4
|
+
function stableJsonKey(value: unknown): string {
|
|
5
|
+
let h = 5381;
|
|
6
|
+
const text = JSON.stringify(value);
|
|
7
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
8
|
+
h = (Math.imul(h, 33) ^ text.charCodeAt(i)) >>> 0;
|
|
9
|
+
}
|
|
10
|
+
return h.toString(36);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Stable cache key for stage-level {@link ModelAsset} sharing. */
|
|
14
|
+
export function resolveModelAssetKey(source: ModelSource): string {
|
|
15
|
+
if (typeof source === "object" && source.kind === "json") {
|
|
16
|
+
return `json:${source.baseUrl}#${stableJsonKey(source.json)}`;
|
|
17
|
+
}
|
|
18
|
+
return modelSourceUrl(source);
|
|
19
|
+
}
|