@doki-land/live2d 0.0.11 → 0.0.13
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 +1 -1
- package/dist/index.d.ts +118 -23
- package/dist/index.js +938 -339
- package/package.json +4 -4
- package/src/create-live2d.ts +23 -575
- package/src/index.ts +15 -2
- package/src/stage/actor-model-slot.ts +429 -0
- package/src/stage/actor.ts +188 -0
- package/src/stage/index.ts +18 -0
- package/src/stage/single-facade.ts +221 -0
- package/src/stage/stage.ts +388 -0
- package/src/stage/transform.ts +161 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doki-land/live2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "Live2D TypeScript library — public facade for Doki Land",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"test": "vitest run --passWithNoTests"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@doki-land/live2d-core": "0.0.
|
|
61
|
-
"@doki-land/live2d-loader": "0.0.
|
|
62
|
-
"@doki-land/live2d-renderer": "0.0.
|
|
60
|
+
"@doki-land/live2d-core": "0.0.13",
|
|
61
|
+
"@doki-land/live2d-loader": "0.0.13",
|
|
62
|
+
"@doki-land/live2d-renderer": "0.0.13"
|
|
63
63
|
},
|
|
64
64
|
"sideEffects": false
|
|
65
65
|
}
|
package/src/create-live2d.ts
CHANGED
|
@@ -1,591 +1,39 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AssetResolver,
|
|
3
|
-
FrameSnapshot,
|
|
4
|
-
InternalModel,
|
|
5
|
-
Live2DSession,
|
|
6
|
-
LoadProgress,
|
|
7
|
-
ModelSource,
|
|
8
|
-
MotionDefinition,
|
|
9
|
-
SessionPhase,
|
|
10
|
-
SessionState,
|
|
11
|
-
} from "@doki-land/live2d-core";
|
|
12
|
-
import { EventEmitter, modelSourceUrl } from "@doki-land/live2d-core";
|
|
13
|
-
import {
|
|
14
|
-
createUrlAssetResolver,
|
|
15
|
-
fetchModelJson,
|
|
16
|
-
normalizeModelSettings,
|
|
17
|
-
resolveModelSourceUrl,
|
|
18
|
-
} from "@doki-land/live2d-loader";
|
|
19
|
-
import type {
|
|
20
|
-
ModelBackend,
|
|
21
|
-
ParameterBinding,
|
|
22
|
-
Renderer,
|
|
23
|
-
RendererKind,
|
|
24
|
-
TextureData,
|
|
25
|
-
} from "@doki-land/live2d-renderer";
|
|
26
1
|
import {
|
|
27
2
|
createMoc2Backend,
|
|
28
3
|
createMoc3Backend,
|
|
29
4
|
createRenderer,
|
|
30
|
-
selectModelBackend,
|
|
31
5
|
} from "@doki-land/live2d-renderer";
|
|
32
|
-
import {
|
|
6
|
+
import { allocateActorId } from "./stage/actor.js";
|
|
33
7
|
import {
|
|
34
|
-
type
|
|
35
|
-
|
|
8
|
+
type CreateLive2DOptions,
|
|
9
|
+
createSingleActorFacade,
|
|
10
|
+
type Live2DRuntime,
|
|
11
|
+
} from "./stage/single-facade.js";
|
|
12
|
+
import { createLive2dStage } from "./stage/stage.js";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
CreateLive2DOptions,
|
|
16
|
+
Live2DRuntime,
|
|
17
|
+
} from "./stage/single-facade.js";
|
|
18
|
+
export {
|
|
36
19
|
MotionPriority,
|
|
37
20
|
type PlayMotionOptions,
|
|
38
|
-
|
|
39
|
-
} from "./motion/index.js";
|
|
40
|
-
|
|
41
|
-
export type { PlayMotionOptions } from "./motion/index.js";
|
|
42
|
-
export { MotionPriority } from "./motion/index.js";
|
|
43
|
-
|
|
44
|
-
export interface CreateLive2DOptions {
|
|
45
|
-
backends?: ModelBackend[];
|
|
46
|
-
/** Defaults to `createRenderer({ prefer })` (async fallback on initialize). */
|
|
47
|
-
renderer?: Renderer;
|
|
48
|
-
/**
|
|
49
|
-
* Renderer try order when `renderer` is omitted.
|
|
50
|
-
* Default: `["webgpu", "webgl2", "canvas2d"]`.
|
|
51
|
-
*/
|
|
52
|
-
prefer?: RendererKind[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface Live2DRuntime extends Live2DSession {
|
|
56
|
-
readonly renderer: Renderer;
|
|
57
|
-
readonly backends: readonly ModelBackend[];
|
|
58
|
-
|
|
59
|
-
setParameter(id: string, value: number): void;
|
|
60
|
-
|
|
61
|
-
listParameters(): readonly ParameterBinding[];
|
|
62
|
-
|
|
63
|
-
/** Hit-test the current model in normalized canvas coordinates (-1..1). */
|
|
64
|
-
hitTest(x: number, y: number): string | null;
|
|
65
|
-
|
|
66
|
-
/** Motion groups from the loaded model settings. */
|
|
67
|
-
listMotionGroups(): Record<string, readonly MotionDefinition[]>;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Load and play a motion from `settings.motionGroups[group][index]`.
|
|
71
|
-
* Returns false if priority rejects or the entry is missing.
|
|
72
|
-
*/
|
|
73
|
-
playMotion(
|
|
74
|
-
group: string,
|
|
75
|
-
index?: number,
|
|
76
|
-
options?: PlayMotionOptions,
|
|
77
|
-
): Promise<boolean>;
|
|
21
|
+
} from "./stage/single-facade.js";
|
|
78
22
|
|
|
79
|
-
|
|
80
|
-
stopMotion(opts?: { fade?: boolean; slot?: string }): void;
|
|
81
|
-
|
|
82
|
-
/** Active motion slots (idle + tap can both appear). */
|
|
83
|
-
listPlayingMotions(): ReadonlyArray<{
|
|
84
|
-
slot: string;
|
|
85
|
-
group: string;
|
|
86
|
-
index: number;
|
|
87
|
-
time: number;
|
|
88
|
-
priority: number;
|
|
89
|
-
}>;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Draw one frame and encode the canvas as PNG.
|
|
93
|
-
* Works for Canvas2D and WebGL2 (`preserveDrawingBuffer`).
|
|
94
|
-
*/
|
|
95
|
-
capturePng(opts?: {
|
|
96
|
-
mimeType?: "image/png";
|
|
97
|
-
quality?: number;
|
|
98
|
-
}): Promise<Blob>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function lerp(a: number, b: number, t: number): number {
|
|
102
|
-
return a + (b - a) * Math.min(1, Math.max(0, t));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function nowMs(): number {
|
|
106
|
-
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Wire moc backends and a renderer into one session. */
|
|
23
|
+
/** Wire moc backends and a renderer into one single-actor session. */
|
|
110
24
|
export function createLive2D(options: CreateLive2DOptions = {}): Live2DRuntime {
|
|
111
25
|
const backends = options.backends ?? [
|
|
112
26
|
createMoc2Backend(),
|
|
113
27
|
createMoc3Backend(),
|
|
114
28
|
];
|
|
115
|
-
const
|
|
116
|
-
options.renderer ?? createRenderer({ prefer: options.prefer });
|
|
117
|
-
const events = new EventEmitter();
|
|
118
|
-
|
|
119
|
-
let canvas: HTMLCanvasElement | null = null;
|
|
120
|
-
let model: InternalModel | null = null;
|
|
121
|
-
let activeBackend: ModelBackend | null = null;
|
|
122
|
-
let drawPass: ReturnType<Renderer["createModelDrawPass"]> | null = null;
|
|
123
|
-
let loadedTextures: TextureData[] = [];
|
|
124
|
-
let initPromise: Promise<void> | null = null;
|
|
125
|
-
let phase: SessionPhase = "idle";
|
|
126
|
-
let lastError: unknown | null = null;
|
|
127
|
-
let generation = 0;
|
|
128
|
-
let loadGeneration = 0;
|
|
129
|
-
let fpsSmooth = 0;
|
|
130
|
-
let activeResolver: AssetResolver | null = null;
|
|
131
|
-
const motionCache = new Map<string, Motion3Clip>();
|
|
132
|
-
const motionPlayer = new MotionPlayer({
|
|
133
|
-
onStart: ({ group, index, slot }) =>
|
|
134
|
-
events.emit("motion:start", { group, index, slot }),
|
|
135
|
-
onFinish: ({ group, index, slot }) =>
|
|
136
|
-
events.emit("motion:finish", { group, index, slot }),
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const applyMotionSamples = (
|
|
140
|
-
samples: ReturnType<MotionPlayer["update"]>,
|
|
141
|
-
): void => {
|
|
142
|
-
if (!model || !activeBackend) return;
|
|
143
|
-
for (const s of samples) {
|
|
144
|
-
if (s.weight <= 0) continue;
|
|
145
|
-
if (s.target === "PartOpacity") {
|
|
146
|
-
if (!activeBackend.setPartOpacity) continue;
|
|
147
|
-
if (s.weight >= 1) {
|
|
148
|
-
activeBackend.setPartOpacity(model, s.id, s.value);
|
|
149
|
-
} else {
|
|
150
|
-
// Soft blend toward motion opacity from full visibility.
|
|
151
|
-
const cur = 1;
|
|
152
|
-
activeBackend.setPartOpacity(
|
|
153
|
-
model,
|
|
154
|
-
s.id,
|
|
155
|
-
cur + (s.value - cur) * s.weight,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (s.target !== "Parameter" || !activeBackend.setParameter)
|
|
161
|
-
continue;
|
|
162
|
-
if (s.weight >= 1) {
|
|
163
|
-
activeBackend.setParameter(model, s.id, s.value);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const cur =
|
|
167
|
-
activeBackend.listParameters?.(model).find((p) => p.id === s.id)
|
|
168
|
-
?.value ?? s.value;
|
|
169
|
-
activeBackend.setParameter(
|
|
170
|
-
model,
|
|
171
|
-
s.id,
|
|
172
|
-
cur + (s.value - cur) * s.weight,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const clearTextures = () => {
|
|
178
|
-
if (loadedTextures.length > 0) {
|
|
179
|
-
releaseTextureData(loadedTextures);
|
|
180
|
-
loadedTextures = [];
|
|
181
|
-
}
|
|
182
|
-
drawPass?.setTextures([]);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const setPhase = (next: SessionPhase) => {
|
|
186
|
-
phase = next;
|
|
187
|
-
events.emit("phase", { phase, generation });
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const report = (payload: LoadProgress) => {
|
|
191
|
-
events.emit("progress", payload);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const state = (): SessionState => ({
|
|
195
|
-
phase,
|
|
196
|
-
lastError,
|
|
197
|
-
generation,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const ensureInitialized = async (): Promise<void> => {
|
|
201
|
-
if (!canvas) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
"@doki-land/live2d: call mount(canvas) before loadModel",
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
if (!initPromise) {
|
|
207
|
-
setPhase("mounting");
|
|
208
|
-
const gen = generation;
|
|
209
|
-
initPromise = renderer
|
|
210
|
-
.initialize(canvas)
|
|
211
|
-
.then(() => {
|
|
212
|
-
if (gen !== generation) return;
|
|
213
|
-
drawPass = renderer.createModelDrawPass();
|
|
214
|
-
setPhase("ready");
|
|
215
|
-
})
|
|
216
|
-
.catch((err) => {
|
|
217
|
-
lastError = err;
|
|
218
|
-
setPhase("error");
|
|
219
|
-
events.emit("error", { error: err });
|
|
220
|
-
throw err;
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
await initPromise;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const runtime: Live2DRuntime = {
|
|
227
|
-
events,
|
|
29
|
+
const stage = createLive2dStage({
|
|
228
30
|
backends,
|
|
229
|
-
renderer
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
generation += 1;
|
|
238
|
-
canvas = target;
|
|
239
|
-
initPromise = null;
|
|
240
|
-
clearTextures();
|
|
241
|
-
drawPass?.destroy();
|
|
242
|
-
drawPass = null;
|
|
243
|
-
setPhase("idle");
|
|
244
|
-
void ensureInitialized();
|
|
245
|
-
},
|
|
246
|
-
async loadModel(source: ModelSource, resolver?: AssetResolver) {
|
|
247
|
-
const gen = ++loadGeneration;
|
|
248
|
-
setPhase("loading");
|
|
249
|
-
report({
|
|
250
|
-
stage: "mounting",
|
|
251
|
-
progress: 0.01,
|
|
252
|
-
detail: "initialize renderer",
|
|
253
|
-
});
|
|
254
|
-
await ensureInitialized();
|
|
255
|
-
if (gen !== loadGeneration) {
|
|
256
|
-
throw new Error("@doki-land/live2d: load cancelled");
|
|
257
|
-
}
|
|
258
|
-
report({
|
|
259
|
-
stage: "resolve",
|
|
260
|
-
progress: 0.02,
|
|
261
|
-
detail: "resolve source",
|
|
262
|
-
});
|
|
263
|
-
try {
|
|
264
|
-
let json: unknown;
|
|
265
|
-
let baseUrl: string;
|
|
266
|
-
let settingsUrl: string;
|
|
267
|
-
|
|
268
|
-
if (typeof source === "object" && source.kind === "json") {
|
|
269
|
-
json = source.json;
|
|
270
|
-
baseUrl = source.baseUrl;
|
|
271
|
-
settingsUrl = source.baseUrl;
|
|
272
|
-
report({
|
|
273
|
-
stage: "settings",
|
|
274
|
-
progress: 0.2,
|
|
275
|
-
detail: "inline settings",
|
|
276
|
-
});
|
|
277
|
-
} else {
|
|
278
|
-
const raw =
|
|
279
|
-
typeof source === "string"
|
|
280
|
-
? source
|
|
281
|
-
: source.kind === "npm"
|
|
282
|
-
? modelSourceUrl(source)
|
|
283
|
-
: source.url;
|
|
284
|
-
const cdnBase =
|
|
285
|
-
typeof source === "object" && source.kind === "npm"
|
|
286
|
-
? source.cdnBase
|
|
287
|
-
: undefined;
|
|
288
|
-
const fetchUrl = resolveModelSourceUrl(raw, {
|
|
289
|
-
npmCdnBase: cdnBase,
|
|
290
|
-
});
|
|
291
|
-
report({
|
|
292
|
-
stage: "settings",
|
|
293
|
-
progress: 0.05,
|
|
294
|
-
detail: fetchUrl,
|
|
295
|
-
});
|
|
296
|
-
json = await fetchModelJson(fetchUrl, (u) => {
|
|
297
|
-
const ratio =
|
|
298
|
-
u.bytesTotal && u.bytesTotal > 0
|
|
299
|
-
? u.bytesLoaded / u.bytesTotal
|
|
300
|
-
: 0;
|
|
301
|
-
report({
|
|
302
|
-
stage: "settings",
|
|
303
|
-
progress: lerp(0.05, 0.22, ratio),
|
|
304
|
-
detail: fetchUrl,
|
|
305
|
-
bytesLoaded: u.bytesLoaded,
|
|
306
|
-
bytesTotal: u.bytesTotal,
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
baseUrl = fetchUrl;
|
|
310
|
-
settingsUrl = fetchUrl;
|
|
311
|
-
}
|
|
312
|
-
if (gen !== loadGeneration) {
|
|
313
|
-
throw new Error("@doki-land/live2d: load cancelled");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const settings = normalizeModelSettings(json, settingsUrl);
|
|
317
|
-
report({
|
|
318
|
-
stage: "moc",
|
|
319
|
-
progress: 0.25,
|
|
320
|
-
detail: settings.moc,
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
const assetResolver =
|
|
324
|
-
resolver ??
|
|
325
|
-
createUrlAssetResolver(baseUrl, {
|
|
326
|
-
onBytesProgress: (key, u) => {
|
|
327
|
-
const isMoc = key === settings.moc;
|
|
328
|
-
const ratio =
|
|
329
|
-
u.bytesTotal && u.bytesTotal > 0
|
|
330
|
-
? u.bytesLoaded / u.bytesTotal
|
|
331
|
-
: 0;
|
|
332
|
-
if (isMoc) {
|
|
333
|
-
report({
|
|
334
|
-
stage: "moc",
|
|
335
|
-
progress: lerp(0.25, 0.8, ratio),
|
|
336
|
-
detail: key,
|
|
337
|
-
bytesLoaded: u.bytesLoaded,
|
|
338
|
-
bytesTotal: u.bytesTotal,
|
|
339
|
-
});
|
|
340
|
-
} else {
|
|
341
|
-
report({
|
|
342
|
-
stage: "textures",
|
|
343
|
-
progress: lerp(0.8, 0.9, ratio),
|
|
344
|
-
detail: key,
|
|
345
|
-
bytesLoaded: u.bytesLoaded,
|
|
346
|
-
bytesTotal: u.bytesTotal,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
activeResolver = assetResolver;
|
|
352
|
-
motionPlayer.clear();
|
|
353
|
-
motionCache.clear();
|
|
354
|
-
|
|
355
|
-
const backend = selectModelBackend(backends, json);
|
|
356
|
-
report({
|
|
357
|
-
stage: "decode",
|
|
358
|
-
progress: 0.85,
|
|
359
|
-
detail: `decode ${settings.format}`,
|
|
360
|
-
});
|
|
361
|
-
const next = await backend.createModel(settings, {
|
|
362
|
-
renderer,
|
|
363
|
-
resolver: assetResolver,
|
|
364
|
-
});
|
|
365
|
-
if (gen !== loadGeneration) {
|
|
366
|
-
backend.destroyModel(next);
|
|
367
|
-
throw new Error("@doki-land/live2d: load cancelled");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
clearTextures();
|
|
371
|
-
if (settings.textures.length > 0 && drawPass) {
|
|
372
|
-
report({
|
|
373
|
-
stage: "textures",
|
|
374
|
-
progress: 0.88,
|
|
375
|
-
detail: `${settings.textures.length} textures`,
|
|
376
|
-
});
|
|
377
|
-
const textures = await loadTextureData(
|
|
378
|
-
assetResolver,
|
|
379
|
-
settings.textures,
|
|
380
|
-
{
|
|
381
|
-
onProgress: (u) => {
|
|
382
|
-
const ratio =
|
|
383
|
-
u.total > 0 ? (u.index + 1) / u.total : 1;
|
|
384
|
-
report({
|
|
385
|
-
stage: "textures",
|
|
386
|
-
progress: lerp(0.88, 0.96, ratio),
|
|
387
|
-
detail: u.key,
|
|
388
|
-
bytesLoaded: u.bytesLoaded,
|
|
389
|
-
bytesTotal: u.bytesTotal,
|
|
390
|
-
});
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
);
|
|
394
|
-
if (gen !== loadGeneration) {
|
|
395
|
-
releaseTextureData(textures);
|
|
396
|
-
backend.destroyModel(next);
|
|
397
|
-
throw new Error("@doki-land/live2d: load cancelled");
|
|
398
|
-
}
|
|
399
|
-
loadedTextures = textures;
|
|
400
|
-
drawPass.setTextures(textures);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (model && activeBackend) {
|
|
404
|
-
activeBackend.destroyModel(model);
|
|
405
|
-
}
|
|
406
|
-
model = next;
|
|
407
|
-
activeBackend = backend;
|
|
408
|
-
lastError = null;
|
|
409
|
-
setPhase("live");
|
|
410
|
-
report({
|
|
411
|
-
stage: "ready",
|
|
412
|
-
progress: 1,
|
|
413
|
-
detail: model.id,
|
|
414
|
-
});
|
|
415
|
-
events.emit("ready", { modelId: model.id });
|
|
416
|
-
return model;
|
|
417
|
-
} catch (err) {
|
|
418
|
-
lastError = err;
|
|
419
|
-
setPhase("error");
|
|
420
|
-
events.emit("error", { error: err });
|
|
421
|
-
throw err;
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
captureFrame(): FrameSnapshot | null {
|
|
425
|
-
if (!model || !activeBackend?.captureFrame) return null;
|
|
426
|
-
return activeBackend.captureFrame(model);
|
|
427
|
-
},
|
|
428
|
-
setParameter(id, value) {
|
|
429
|
-
if (!model || !activeBackend?.setParameter) return;
|
|
430
|
-
activeBackend.setParameter(model, id, value);
|
|
431
|
-
},
|
|
432
|
-
hitTest(x, y) {
|
|
433
|
-
if (!model || !activeBackend) return null;
|
|
434
|
-
const drawables = activeBackend.getDrawables(model);
|
|
435
|
-
for (let n = drawables.length - 1; n >= 0; n -= 1) {
|
|
436
|
-
const d = drawables[n]!;
|
|
437
|
-
if (!d.visible || d.opacity <= 0) continue;
|
|
438
|
-
const p = d.vertexPositions;
|
|
439
|
-
const idx = d.indices;
|
|
440
|
-
for (let i = 0; i + 2 < idx.length; i += 3) {
|
|
441
|
-
const a = idx[i]! * 2,
|
|
442
|
-
b = idx[i + 1]! * 2,
|
|
443
|
-
c = idx[i + 2]! * 2;
|
|
444
|
-
const ax = p[a]!,
|
|
445
|
-
ay = p[a + 1]!;
|
|
446
|
-
const bx = p[b]!,
|
|
447
|
-
by = p[b + 1]!;
|
|
448
|
-
const cx = p[c]!,
|
|
449
|
-
cy = p[c + 1]!;
|
|
450
|
-
const s = (ax - cx) * (y - cy) - (ay - cy) * (x - cx);
|
|
451
|
-
const s1 = (bx - ax) * (y - ay) - (by - ay) * (x - ax);
|
|
452
|
-
const s2 = (cx - bx) * (y - by) - (cy - by) * (x - bx);
|
|
453
|
-
if (
|
|
454
|
-
(s >= 0 && s1 >= 0 && s2 >= 0) ||
|
|
455
|
-
(s <= 0 && s1 <= 0 && s2 <= 0)
|
|
456
|
-
) {
|
|
457
|
-
return `drawable:${d.index}`;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return null;
|
|
462
|
-
},
|
|
463
|
-
listParameters() {
|
|
464
|
-
if (!model || !activeBackend?.listParameters) return [];
|
|
465
|
-
return activeBackend.listParameters(model);
|
|
466
|
-
},
|
|
467
|
-
listMotionGroups() {
|
|
468
|
-
return model?.settings.motionGroups ?? {};
|
|
469
|
-
},
|
|
470
|
-
async playMotion(group, index = 0, options = {}) {
|
|
471
|
-
if (!model || !activeResolver) return false;
|
|
472
|
-
const list = model.settings.motionGroups[group];
|
|
473
|
-
const def = list?.[index];
|
|
474
|
-
if (!def) return false;
|
|
475
|
-
|
|
476
|
-
let clip = motionCache.get(def.file);
|
|
477
|
-
if (!clip) {
|
|
478
|
-
const json = await activeResolver.fetchJson(def.file);
|
|
479
|
-
clip = parseMotion3(json);
|
|
480
|
-
motionCache.set(def.file, clip);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const fadeInTime =
|
|
484
|
-
options.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
|
|
485
|
-
const fadeOutTime =
|
|
486
|
-
options.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
|
|
487
|
-
|
|
488
|
-
return motionPlayer.start(group, index, clip, {
|
|
489
|
-
priority: options.priority ?? MotionPriority.normal,
|
|
490
|
-
slot: options.slot,
|
|
491
|
-
queue: options.queue,
|
|
492
|
-
loop: options.loop,
|
|
493
|
-
fadeInTime,
|
|
494
|
-
fadeOutTime,
|
|
495
|
-
});
|
|
496
|
-
},
|
|
497
|
-
stopMotion(opts) {
|
|
498
|
-
motionPlayer.stop(opts?.fade !== false, opts?.slot);
|
|
499
|
-
},
|
|
500
|
-
listPlayingMotions() {
|
|
501
|
-
return motionPlayer.listPlaying();
|
|
502
|
-
},
|
|
503
|
-
async capturePng(opts = {}) {
|
|
504
|
-
if (!canvas) {
|
|
505
|
-
throw new Error(
|
|
506
|
-
"@doki-land/live2d: mount(canvas) before capturePng",
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
if (phase === "live" && model && activeBackend && drawPass) {
|
|
510
|
-
runtime.update(0);
|
|
511
|
-
}
|
|
512
|
-
const mime = opts.mimeType ?? "image/png";
|
|
513
|
-
return await new Promise<Blob>((resolve, reject) => {
|
|
514
|
-
canvas!.toBlob(
|
|
515
|
-
(blob) => {
|
|
516
|
-
if (blob) resolve(blob);
|
|
517
|
-
else
|
|
518
|
-
reject(
|
|
519
|
-
new Error(
|
|
520
|
-
"@doki-land/live2d: canvas.toBlob returned null",
|
|
521
|
-
),
|
|
522
|
-
);
|
|
523
|
-
},
|
|
524
|
-
mime,
|
|
525
|
-
opts.quality,
|
|
526
|
-
);
|
|
527
|
-
});
|
|
528
|
-
},
|
|
529
|
-
update(deltaTimeSeconds) {
|
|
530
|
-
if (!model || !activeBackend || !drawPass) return;
|
|
531
|
-
if (phase !== "live") return;
|
|
532
|
-
|
|
533
|
-
const t0 = nowMs();
|
|
534
|
-
applyMotionSamples(motionPlayer.update(deltaTimeSeconds));
|
|
535
|
-
activeBackend.updateModel(model, deltaTimeSeconds);
|
|
536
|
-
const drawables = activeBackend.getDrawables(model);
|
|
537
|
-
const t1 = nowMs();
|
|
538
|
-
|
|
539
|
-
renderer.beginFrame();
|
|
540
|
-
drawPass.draw(drawables, new Float32Array(16));
|
|
541
|
-
renderer.endFrame();
|
|
542
|
-
const t2 = nowMs();
|
|
543
|
-
|
|
544
|
-
let vertexCount = 0;
|
|
545
|
-
let indexCount = 0;
|
|
546
|
-
for (const d of drawables) {
|
|
547
|
-
vertexCount += d.vertexPositions.length / 2;
|
|
548
|
-
indexCount += d.indices.length;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const frameMs = t2 - t0;
|
|
552
|
-
const evaluateMs = t1 - t0;
|
|
553
|
-
const drawMs = t2 - t1;
|
|
554
|
-
const fps = deltaTimeSeconds > 0 ? 1 / deltaTimeSeconds : 0;
|
|
555
|
-
fpsSmooth = fpsSmooth <= 0 ? fps : fpsSmooth * 0.85 + fps * 0.15;
|
|
556
|
-
|
|
557
|
-
events.emit("profile", {
|
|
558
|
-
fps,
|
|
559
|
-
fpsSmooth,
|
|
560
|
-
frameMs,
|
|
561
|
-
evaluateMs,
|
|
562
|
-
drawMs,
|
|
563
|
-
drawableCount: drawables.length,
|
|
564
|
-
vertexCount,
|
|
565
|
-
indexCount,
|
|
566
|
-
});
|
|
567
|
-
},
|
|
568
|
-
destroy() {
|
|
569
|
-
loadGeneration += 1;
|
|
570
|
-
generation += 1;
|
|
571
|
-
motionPlayer.clear();
|
|
572
|
-
motionCache.clear();
|
|
573
|
-
activeResolver = null;
|
|
574
|
-
if (model && activeBackend) {
|
|
575
|
-
activeBackend.destroyModel(model);
|
|
576
|
-
}
|
|
577
|
-
model = null;
|
|
578
|
-
activeBackend = null;
|
|
579
|
-
clearTextures();
|
|
580
|
-
drawPass?.destroy();
|
|
581
|
-
drawPass = null;
|
|
582
|
-
initPromise = null;
|
|
583
|
-
renderer.destroy();
|
|
584
|
-
canvas = null;
|
|
585
|
-
setPhase("destroyed");
|
|
586
|
-
events.clear();
|
|
587
|
-
},
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
return runtime;
|
|
31
|
+
renderer:
|
|
32
|
+
options.renderer ?? createRenderer({ prefer: options.prefer }),
|
|
33
|
+
updateMode: options.updateMode ?? "manual",
|
|
34
|
+
}) as import("./stage/stage.js").Live2dStageImpl;
|
|
35
|
+
const actor = stage.createActor({
|
|
36
|
+
id: allocateActorId("default"),
|
|
37
|
+
}) as import("./stage/actor.js").Live2dActorImpl;
|
|
38
|
+
return createSingleActorFacade(stage, actor, backends);
|
|
591
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export type {
|
|
10
|
+
ActorHit,
|
|
11
|
+
ActorTransform,
|
|
10
12
|
AssetResolver,
|
|
13
|
+
CreateActorOptions,
|
|
14
|
+
CreateLive2dStageOptions,
|
|
11
15
|
FrameProfile,
|
|
12
16
|
FrameSnapshot,
|
|
13
17
|
InternalModel,
|
|
14
18
|
Live2DSession,
|
|
19
|
+
Live2dActor,
|
|
20
|
+
Live2dStage,
|
|
15
21
|
LoadProgress,
|
|
16
22
|
LoadProgressStage,
|
|
17
23
|
ModelFormat,
|
|
@@ -19,16 +25,19 @@ export type {
|
|
|
19
25
|
ModelProgram,
|
|
20
26
|
ModelSettings,
|
|
21
27
|
ModelSource,
|
|
28
|
+
PointerTrackingMode,
|
|
29
|
+
PointerTrackingPolicy,
|
|
22
30
|
SessionPhase,
|
|
23
31
|
SessionState,
|
|
32
|
+
StagePointerEvent,
|
|
33
|
+
StageUpdateMode,
|
|
24
34
|
} from "@doki-land/live2d-core";
|
|
25
|
-
export { EventEmitter } from "@doki-land/live2d-core";
|
|
35
|
+
export { DEFAULT_ACTOR_TRANSFORM, EventEmitter } from "@doki-land/live2d-core";
|
|
26
36
|
export {
|
|
27
37
|
DEFAULT_NPM_CDN,
|
|
28
38
|
resolveModelSourceUrl,
|
|
29
39
|
resolveNpmSpecifier,
|
|
30
40
|
} from "@doki-land/live2d-loader";
|
|
31
|
-
|
|
32
41
|
export {
|
|
33
42
|
createCanvas2DRenderer,
|
|
34
43
|
createMoc2Backend,
|
|
@@ -64,5 +73,9 @@ export {
|
|
|
64
73
|
MotionPlayer,
|
|
65
74
|
parseMotion3,
|
|
66
75
|
} from "./motion/index.js";
|
|
76
|
+
export {
|
|
77
|
+
type CreateLive2dStageFullOptions,
|
|
78
|
+
createLive2dStage,
|
|
79
|
+
} from "./stage/stage.js";
|
|
67
80
|
|
|
68
81
|
export const LIVE2D_VERSION = "0.0.0" as const;
|