@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/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@doki-land/live2d",
3
- "version": "0.0.13",
4
- "description": "Live2D TypeScript librarypublic facade for Doki Land",
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.13",
61
- "@doki-land/live2d-loader": "0.0.13",
62
- "@doki-land/live2d-renderer": "0.0.13"
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
- function lerp(a: number, b: number, t: number): number {
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
- backends: readonly ModelBackend[];
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 #backends: readonly ModelBackend[];
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: ReturnType<Renderer["createModelDrawPass"]> | null = null;
50
+ #drawPass: ModelDrawPass | null = null;
60
51
  #model: InternalModel | null = null;
61
52
  #backend: ModelBackend | null = null;
62
- #textures: TextureData[] = [];
63
- #resolver: AssetResolver | null = null;
53
+ #lease: ModelAssetLease | null = null;
64
54
  #loadGeneration = 0;
65
55
 
66
56
  constructor(options: ActorModelSlotOptions) {
67
- this.#backends = options.backends;
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(): ReturnType<Renderer["createModelDrawPass"]> | null {
70
+ get drawPass(): ModelDrawPass | null {
81
71
  return this.#drawPass;
82
72
  }
83
73
 
84
- ensureDrawPass(): ReturnType<Renderer["createModelDrawPass"]> {
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
- #clearTextures(): void {
96
- if (this.#textures.length > 0) {
97
- releaseTextureData(this.#textures);
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.#report({
152
- stage: "resolve",
153
- progress: 0.02,
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
- const settings = normalizeModelSettings(json, settingsUrl);
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: "moc",
212
- progress: 0.25,
213
- detail: settings.moc,
152
+ stage: "mounting",
153
+ progress: 0.01,
154
+ detail: "prepare draw pass",
214
155
  });
156
+ const drawPass = this.ensureDrawPass();
215
157
 
216
- const assetResolver =
217
- resolver ??
218
- createUrlAssetResolver(baseUrl, {
219
- onBytesProgress: (key, u) => {
220
- const isMoc = key === settings.moc;
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: `decode ${settings.format}`,
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(next);
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
- this.#model = next;
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: next.id,
199
+ detail: model.id,
304
200
  });
305
- return next;
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.#resolver) return false;
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
- let clip = this.#motionCache.get(def.file);
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
- this.#motionCache.set(def.file, clip);
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.#clearTextures();
320
+ this.#releaseLease();
321
+ this.#drawPass?.setTextures([]);
426
322
  this.#drawPass?.destroy();
427
323
  this.#drawPass = null;
428
324
  }
@@ -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
- backends: readonly ModelBackend[];
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
- backends: shared.backends,
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
+ }