@doki-land/live2d 0.0.10 → 0.0.12
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 +3 -0
- package/dist/index.d.ts +326 -0
- package/dist/index.js +1667 -0
- package/dist/reexports/core.d.ts +3 -0
- package/dist/reexports/core.js +2 -0
- package/dist/reexports/loader.d.ts +1 -0
- package/dist/reexports/loader.js +2 -0
- package/dist/reexports/renderer.d.ts +1 -0
- package/dist/reexports/renderer.js +2 -0
- package/package.json +61 -43
- package/src/create-live2d.ts +39 -0
- package/src/focus.ts +53 -0
- package/src/index.ts +78 -187
- package/src/load-textures.ts +85 -0
- package/src/motion/evaluate-curve.ts +119 -0
- package/src/motion/index.ts +20 -0
- package/src/motion/motion-player.ts +389 -0
- package/src/motion/parse-motion3.ts +164 -0
- package/src/motion/types.ts +89 -0
- package/src/reexports/core.ts +1 -0
- package/src/reexports/loader.ts +1 -0
- package/src/reexports/renderer.ts +1 -0
- 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/dist/l2d.umd.js +0 -1208
- package/dist/l2d.umd.js.map +0 -1
- package/dist/live2d.css +0 -1
- package/lib/cubism2.d.ts +0 -179
- package/lib/cubism2.min.js +0 -2
- package/lib/cubism5.d.ts +0 -367
- package/lib/cubism5.min.js +0 -10
- package/lib/index.d.ts +0 -7
- package/readme.md +0 -60
- package/src/fs/index.ts +0 -93
- package/src/helper/index.ts +0 -25
- package/src/icons/icons.ts +0 -39
- package/src/icons/style.css +0 -44
- package/src/icons/switch-character.svg +0 -1
- package/src/icons/switch-costume.svg +0 -1
- package/src/types/Live2dOptions.ts +0 -56
- package/src/types/ModelOptions.ts +0 -40
- package/src/types/Resolve.ts +0 -16
- package/src/types/index.ts +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { DEFAULT_ACTOR_TRANSFORM as DEFAULT_ACTOR_TRANSFORM2, EventEmitter as EventEmitter2 } from "@doki-land/live2d-core";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_NPM_CDN,
|
|
5
|
+
resolveModelSourceUrl as resolveModelSourceUrl2,
|
|
6
|
+
resolveNpmSpecifier
|
|
7
|
+
} from "@doki-land/live2d-loader";
|
|
8
|
+
import {
|
|
9
|
+
createCanvas2DRenderer,
|
|
10
|
+
createMoc2Backend as createMoc2Backend3,
|
|
11
|
+
createMoc3Backend as createMoc3Backend3,
|
|
12
|
+
createQuadProgram,
|
|
13
|
+
createRenderer as createRenderer3,
|
|
14
|
+
createWebGl2Renderer,
|
|
15
|
+
createWebGpuRenderer,
|
|
16
|
+
decodeMoc3,
|
|
17
|
+
evaluateFrame,
|
|
18
|
+
fingerprintSnapshot,
|
|
19
|
+
parseCpuProgram,
|
|
20
|
+
serializeCpuProgram
|
|
21
|
+
} from "@doki-land/live2d-renderer";
|
|
22
|
+
|
|
23
|
+
// src/create-live2d.ts
|
|
24
|
+
import {
|
|
25
|
+
createMoc2Backend as createMoc2Backend2,
|
|
26
|
+
createMoc3Backend as createMoc3Backend2,
|
|
27
|
+
createRenderer as createRenderer2
|
|
28
|
+
} from "@doki-land/live2d-renderer";
|
|
29
|
+
|
|
30
|
+
// src/focus.ts
|
|
31
|
+
function focusParameterUpdates(parameters, dragX, dragY) {
|
|
32
|
+
const byId = new Map(parameters.map((p) => [p.id, p]));
|
|
33
|
+
const x = clampUnit(dragX);
|
|
34
|
+
const y = clampUnit(dragY);
|
|
35
|
+
const out = [];
|
|
36
|
+
const set = (id, normalized) => {
|
|
37
|
+
const binding = byId.get(id);
|
|
38
|
+
if (!binding) return;
|
|
39
|
+
out.push({ id, value: valueFromNormalized(binding, normalized) });
|
|
40
|
+
};
|
|
41
|
+
set("PARAM_ANGLE_X", x);
|
|
42
|
+
set("PARAM_ANGLE_Y", y);
|
|
43
|
+
set("PARAM_ANGLE_Z", clampUnit(x * y * -1));
|
|
44
|
+
set("PARAM_BODY_ANGLE_X", x);
|
|
45
|
+
set("PARAM_BODY_ANGLE_Y", y);
|
|
46
|
+
set("PARAM_EYE_BALL_X", x);
|
|
47
|
+
set("PARAM_EYE_BALL_Y", y);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function clampUnit(n) {
|
|
51
|
+
if (n > 1) return 1;
|
|
52
|
+
if (n < -1) return -1;
|
|
53
|
+
return n;
|
|
54
|
+
}
|
|
55
|
+
function valueFromNormalized(binding, normalized) {
|
|
56
|
+
const n = clampUnit(normalized);
|
|
57
|
+
return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/stage/actor-model-slot.ts
|
|
61
|
+
import { modelSourceUrl } from "@doki-land/live2d-core";
|
|
62
|
+
import {
|
|
63
|
+
createUrlAssetResolver,
|
|
64
|
+
fetchModelJson,
|
|
65
|
+
normalizeModelSettings,
|
|
66
|
+
resolveModelSourceUrl
|
|
67
|
+
} from "@doki-land/live2d-loader";
|
|
68
|
+
import { selectModelBackend } from "@doki-land/live2d-renderer";
|
|
69
|
+
|
|
70
|
+
// src/load-textures.ts
|
|
71
|
+
function guessMime(path) {
|
|
72
|
+
const lower = path.toLowerCase();
|
|
73
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
74
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
75
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
76
|
+
return "image/png";
|
|
77
|
+
}
|
|
78
|
+
async function bytesToImageBitmap(bytes, path) {
|
|
79
|
+
if (typeof createImageBitmap !== "function") {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"@doki-land/live2d: createImageBitmap is not available in this environment"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const blob = new Blob([new Uint8Array(bytes)], {
|
|
85
|
+
type: guessMime(path)
|
|
86
|
+
});
|
|
87
|
+
return createImageBitmap(blob);
|
|
88
|
+
}
|
|
89
|
+
async function loadTextureData(resolver, paths, options = {}) {
|
|
90
|
+
const out = [];
|
|
91
|
+
const total = paths.length;
|
|
92
|
+
for (let i = 0; i < paths.length; i++) {
|
|
93
|
+
const key = paths[i];
|
|
94
|
+
options.onProgress?.({
|
|
95
|
+
index: i,
|
|
96
|
+
total,
|
|
97
|
+
key,
|
|
98
|
+
bytesLoaded: 0,
|
|
99
|
+
bytesTotal: null
|
|
100
|
+
});
|
|
101
|
+
const bytes = await resolver.fetchBytes(key);
|
|
102
|
+
options.onProgress?.({
|
|
103
|
+
index: i,
|
|
104
|
+
total,
|
|
105
|
+
key,
|
|
106
|
+
bytesLoaded: bytes.byteLength,
|
|
107
|
+
bytesTotal: bytes.byteLength
|
|
108
|
+
});
|
|
109
|
+
const image = await bytesToImageBitmap(bytes, key);
|
|
110
|
+
out.push({
|
|
111
|
+
index: i,
|
|
112
|
+
image,
|
|
113
|
+
width: image.width,
|
|
114
|
+
height: image.height
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function releaseTextureData(textures) {
|
|
120
|
+
for (const t of textures) {
|
|
121
|
+
const img = t.image;
|
|
122
|
+
if (typeof ImageBitmap !== "undefined" && img instanceof ImageBitmap) {
|
|
123
|
+
img.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/motion/evaluate-curve.ts
|
|
129
|
+
function evaluateMotion3(clip, timeSeconds) {
|
|
130
|
+
const t = clamp(timeSeconds, 0, clip.duration);
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const curve of clip.curves) {
|
|
133
|
+
out.push({
|
|
134
|
+
target: curve.target,
|
|
135
|
+
id: curve.id,
|
|
136
|
+
value: evaluateCurve(curve, t, clip.areBeziersRestricted)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function evaluateCurve(curve, timeSeconds, areBeziersRestricted) {
|
|
142
|
+
const segs = curve.segments;
|
|
143
|
+
if (segs.length === 0) return 0;
|
|
144
|
+
if (timeSeconds <= segs[0].p0.time) return segs[0].p0.value;
|
|
145
|
+
const last = segs[segs.length - 1];
|
|
146
|
+
if (timeSeconds >= last.p3.time) return last.p3.value;
|
|
147
|
+
for (let i = 0; i < segs.length; i += 1) {
|
|
148
|
+
const seg = segs[i];
|
|
149
|
+
const isLast = i === segs.length - 1;
|
|
150
|
+
if (timeSeconds < seg.p3.time || isLast && timeSeconds <= seg.p3.time) {
|
|
151
|
+
return evaluateSegment(seg, timeSeconds, areBeziersRestricted);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return last.p3.value;
|
|
155
|
+
}
|
|
156
|
+
function evaluateSegment(seg, time, areBeziersRestricted) {
|
|
157
|
+
const { p0, p3 } = seg;
|
|
158
|
+
switch (seg.kind) {
|
|
159
|
+
case "linear": {
|
|
160
|
+
const span = p3.time - p0.time;
|
|
161
|
+
if (span <= 0) return p3.value;
|
|
162
|
+
const u = (time - p0.time) / span;
|
|
163
|
+
return p0.value + (p3.value - p0.value) * u;
|
|
164
|
+
}
|
|
165
|
+
case "stepped":
|
|
166
|
+
return p0.value;
|
|
167
|
+
case "inverseStepped":
|
|
168
|
+
return p3.value;
|
|
169
|
+
case "bezier": {
|
|
170
|
+
const p1 = seg.p1;
|
|
171
|
+
const p2 = seg.p2;
|
|
172
|
+
if (areBeziersRestricted) {
|
|
173
|
+
const span = p3.time - p0.time;
|
|
174
|
+
if (span <= 0) return p3.value;
|
|
175
|
+
const u2 = (time - p0.time) / span;
|
|
176
|
+
return cubic(p0.value, p1.value, p2.value, p3.value, u2);
|
|
177
|
+
}
|
|
178
|
+
const u = solveBezierTime(p0.time, p1.time, p2.time, p3.time, time);
|
|
179
|
+
return cubic(p0.value, p1.value, p2.value, p3.value, u);
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
return p3.value;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function cubic(a, b, c, d, t) {
|
|
186
|
+
const u = 1 - t;
|
|
187
|
+
return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d;
|
|
188
|
+
}
|
|
189
|
+
function solveBezierTime(t0, t1, t2, t3, target) {
|
|
190
|
+
let lo = 0;
|
|
191
|
+
let hi = 1;
|
|
192
|
+
for (let i = 0; i < 20; i += 1) {
|
|
193
|
+
const mid = (lo + hi) * 0.5;
|
|
194
|
+
const x = cubic(t0, t1, t2, t3, mid);
|
|
195
|
+
if (x < target) lo = mid;
|
|
196
|
+
else hi = mid;
|
|
197
|
+
}
|
|
198
|
+
return (lo + hi) * 0.5;
|
|
199
|
+
}
|
|
200
|
+
function clamp(n, min, max) {
|
|
201
|
+
if (n < min) return min;
|
|
202
|
+
if (n > max) return max;
|
|
203
|
+
return n;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/motion/types.ts
|
|
207
|
+
var MotionPriority = {
|
|
208
|
+
none: 0,
|
|
209
|
+
idle: 1,
|
|
210
|
+
normal: 2,
|
|
211
|
+
force: 3
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/motion/motion-player.ts
|
|
215
|
+
var MotionPlayer = class {
|
|
216
|
+
#slots = /* @__PURE__ */ new Map();
|
|
217
|
+
#queues = /* @__PURE__ */ new Map();
|
|
218
|
+
#handlers;
|
|
219
|
+
constructor(handlers = {}) {
|
|
220
|
+
this.#handlers = handlers;
|
|
221
|
+
}
|
|
222
|
+
get isPlaying() {
|
|
223
|
+
return this.#slots.size > 0;
|
|
224
|
+
}
|
|
225
|
+
listPlaying() {
|
|
226
|
+
return [...this.#slots.values()].map((a) => ({
|
|
227
|
+
slot: a.slot,
|
|
228
|
+
group: a.group,
|
|
229
|
+
index: a.index,
|
|
230
|
+
time: a.time,
|
|
231
|
+
priority: a.priority
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
/** @deprecated Prefer {@link listPlaying}; returns highest-priority slot. */
|
|
235
|
+
get current() {
|
|
236
|
+
const list = [...this.listPlaying()];
|
|
237
|
+
if (!list.length) return null;
|
|
238
|
+
list.sort((a, b) => b.priority - a.priority);
|
|
239
|
+
const top = list[0];
|
|
240
|
+
return {
|
|
241
|
+
group: top.group,
|
|
242
|
+
index: top.index,
|
|
243
|
+
time: top.time,
|
|
244
|
+
priority: top.priority
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Start a clip on a slot. Returns false if rejected by priority
|
|
249
|
+
* (and not queued).
|
|
250
|
+
*/
|
|
251
|
+
start(group, index, clip, options = {}) {
|
|
252
|
+
const priority = options.priority ?? MotionPriority.normal;
|
|
253
|
+
const slot = options.slot ?? `priority:${priority}`;
|
|
254
|
+
const existing = this.#slots.get(slot);
|
|
255
|
+
if (existing && priority < existing.priority) {
|
|
256
|
+
if (options.queue) {
|
|
257
|
+
this.#enqueue(slot, { group, index, clip, options });
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (existing && options.queue && !existing.fadingOut) {
|
|
263
|
+
this.#enqueue(slot, { group, index, clip, options });
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
if (existing) {
|
|
267
|
+
if (existing.fadeOutTime > 0 && !existing.fadingOut) {
|
|
268
|
+
existing.fadingOut = true;
|
|
269
|
+
existing.fadeOutElapsed = 0;
|
|
270
|
+
this.#enqueueFront(slot, { group, index, clip, options });
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
this.#finish(existing, false);
|
|
274
|
+
}
|
|
275
|
+
this.#slots.set(
|
|
276
|
+
slot,
|
|
277
|
+
this.#createActive(slot, group, index, clip, options)
|
|
278
|
+
);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
/** Fade out (default) or hard-stop; `slot` omits → all slots. */
|
|
282
|
+
stop(fade = true, slot) {
|
|
283
|
+
if (slot !== void 0) {
|
|
284
|
+
const a = this.#slots.get(slot);
|
|
285
|
+
if (!a) return;
|
|
286
|
+
this.#stopOne(a, fade);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (const a of [...this.#slots.values()]) {
|
|
290
|
+
this.#stopOne(a, fade);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
clear() {
|
|
294
|
+
this.#slots.clear();
|
|
295
|
+
this.#queues.clear();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Advance all slots and return blended samples (weight baked in; apply as absolute).
|
|
299
|
+
*/
|
|
300
|
+
update(deltaTimeSeconds) {
|
|
301
|
+
const dt = Math.max(0, deltaTimeSeconds);
|
|
302
|
+
const layerSamples = [];
|
|
303
|
+
for (const a of [...this.#slots.values()]) {
|
|
304
|
+
const samples = this.#tick(a, dt);
|
|
305
|
+
if (samples) {
|
|
306
|
+
layerSamples.push({ priority: a.priority, samples });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
layerSamples.sort((a, b) => a.priority - b.priority);
|
|
310
|
+
return blendMotionLayers(layerSamples);
|
|
311
|
+
}
|
|
312
|
+
#tick(a, dt) {
|
|
313
|
+
if (!a.started) {
|
|
314
|
+
a.started = true;
|
|
315
|
+
this.#handlers.onStart?.({
|
|
316
|
+
group: a.group,
|
|
317
|
+
index: a.index,
|
|
318
|
+
slot: a.slot
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
a.time += dt;
|
|
322
|
+
if (a.fadingOut) {
|
|
323
|
+
a.fadeOutElapsed += dt;
|
|
324
|
+
if (a.fadeOutElapsed >= a.fadeOutTime) {
|
|
325
|
+
this.#finish(a, true);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
} else if (!a.loop && a.time >= a.clip.duration) {
|
|
329
|
+
if (a.fadeOutTime > 0) {
|
|
330
|
+
a.fadingOut = true;
|
|
331
|
+
a.fadeOutElapsed = 0;
|
|
332
|
+
} else {
|
|
333
|
+
const samples = this.#sample(a, a.clip.duration, 1);
|
|
334
|
+
this.#finish(a, true);
|
|
335
|
+
return samples;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
let playTime = a.time;
|
|
339
|
+
if (a.loop && a.clip.duration > 0) {
|
|
340
|
+
playTime = a.time % a.clip.duration;
|
|
341
|
+
} else {
|
|
342
|
+
playTime = Math.min(playTime, a.clip.duration);
|
|
343
|
+
}
|
|
344
|
+
this.#emitEvents(a, playTime);
|
|
345
|
+
return this.#sample(a, playTime, this.#fadeWeight(a));
|
|
346
|
+
}
|
|
347
|
+
#createActive(slot, group, index, clip, options) {
|
|
348
|
+
const fadeIn = options.fadeInTime ?? (clip.fadeInTime > 0 ? clip.fadeInTime : 0);
|
|
349
|
+
const fadeOut = options.fadeOutTime ?? (clip.fadeOutTime > 0 ? clip.fadeOutTime : 0);
|
|
350
|
+
return {
|
|
351
|
+
slot,
|
|
352
|
+
group,
|
|
353
|
+
index,
|
|
354
|
+
clip,
|
|
355
|
+
priority: options.priority ?? MotionPriority.normal,
|
|
356
|
+
loop: options.loop ?? clip.loop,
|
|
357
|
+
fadeInTime: Math.max(0, fadeIn),
|
|
358
|
+
fadeOutTime: Math.max(0, fadeOut),
|
|
359
|
+
time: 0,
|
|
360
|
+
fadingOut: false,
|
|
361
|
+
fadeOutElapsed: 0,
|
|
362
|
+
lastEventIndex: -1,
|
|
363
|
+
started: false
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
#enqueue(slot, item) {
|
|
367
|
+
const q = this.#queues.get(slot) ?? [];
|
|
368
|
+
q.push(item);
|
|
369
|
+
this.#queues.set(slot, q);
|
|
370
|
+
}
|
|
371
|
+
#enqueueFront(slot, item) {
|
|
372
|
+
const q = this.#queues.get(slot) ?? [];
|
|
373
|
+
q.unshift(item);
|
|
374
|
+
this.#queues.set(slot, q);
|
|
375
|
+
}
|
|
376
|
+
#stopOne(a, fade) {
|
|
377
|
+
if (!fade || a.fadeOutTime <= 0) {
|
|
378
|
+
this.#finish(a, true);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
a.fadingOut = true;
|
|
382
|
+
a.fadeOutElapsed = 0;
|
|
383
|
+
}
|
|
384
|
+
#finish(a, promoteQueue) {
|
|
385
|
+
if (this.#slots.get(a.slot) !== a) return;
|
|
386
|
+
this.#slots.delete(a.slot);
|
|
387
|
+
this.#handlers.onFinish?.({
|
|
388
|
+
group: a.group,
|
|
389
|
+
index: a.index,
|
|
390
|
+
slot: a.slot
|
|
391
|
+
});
|
|
392
|
+
if (!promoteQueue) return;
|
|
393
|
+
const q = this.#queues.get(a.slot);
|
|
394
|
+
const next = q?.shift();
|
|
395
|
+
if (next) {
|
|
396
|
+
this.#slots.set(
|
|
397
|
+
a.slot,
|
|
398
|
+
this.#createActive(
|
|
399
|
+
a.slot,
|
|
400
|
+
next.group,
|
|
401
|
+
next.index,
|
|
402
|
+
next.clip,
|
|
403
|
+
next.options
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
#sample(a, playTime, weight) {
|
|
409
|
+
const values = evaluateMotion3(a.clip, playTime);
|
|
410
|
+
return values.map((v) => ({
|
|
411
|
+
target: v.target,
|
|
412
|
+
id: v.id,
|
|
413
|
+
value: v.value,
|
|
414
|
+
weight
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
#fadeWeight(a) {
|
|
418
|
+
let w = 1;
|
|
419
|
+
if (a.fadeInTime > 0 && a.time < a.fadeInTime) {
|
|
420
|
+
w = sineEase(a.time / a.fadeInTime);
|
|
421
|
+
}
|
|
422
|
+
if (a.fadingOut && a.fadeOutTime > 0) {
|
|
423
|
+
const u = 1 - a.fadeOutElapsed / a.fadeOutTime;
|
|
424
|
+
w *= sineEase(Math.max(0, u));
|
|
425
|
+
}
|
|
426
|
+
return w;
|
|
427
|
+
}
|
|
428
|
+
#emitEvents(a, playTime) {
|
|
429
|
+
const events = a.clip.userData;
|
|
430
|
+
for (let i = a.lastEventIndex + 1; i < events.length; i += 1) {
|
|
431
|
+
const e = events[i];
|
|
432
|
+
if (e.time > playTime) break;
|
|
433
|
+
a.lastEventIndex = i;
|
|
434
|
+
this.#handlers.onEvent?.({
|
|
435
|
+
group: a.group,
|
|
436
|
+
index: a.index,
|
|
437
|
+
slot: a.slot,
|
|
438
|
+
time: e.time,
|
|
439
|
+
value: e.value
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (a.loop && a.clip.duration > 0) {
|
|
443
|
+
const prevMod = (a.time - 1e-6) % a.clip.duration + (a.time - 1e-6 < 0 ? a.clip.duration : 0);
|
|
444
|
+
if (playTime < prevMod - 1e-4) {
|
|
445
|
+
a.lastEventIndex = -1;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
function blendMotionLayers(layers) {
|
|
451
|
+
const map = /* @__PURE__ */ new Map();
|
|
452
|
+
for (const layer of layers) {
|
|
453
|
+
for (const s of layer.samples) {
|
|
454
|
+
const key = `${s.target}\0${s.id}`;
|
|
455
|
+
const w = Math.min(1, Math.max(0, s.weight));
|
|
456
|
+
const prev = map.get(key);
|
|
457
|
+
if (!prev) {
|
|
458
|
+
map.set(key, {
|
|
459
|
+
target: s.target,
|
|
460
|
+
id: s.id,
|
|
461
|
+
value: s.value,
|
|
462
|
+
weight: w
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
prev.value = prev.value + (s.value - prev.value) * w;
|
|
466
|
+
prev.weight = Math.min(1, prev.weight + w * (1 - prev.weight));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return [...map.values()];
|
|
471
|
+
}
|
|
472
|
+
function sineEase(t) {
|
|
473
|
+
const x = Math.min(1, Math.max(0, t));
|
|
474
|
+
return 0.5 - 0.5 * Math.cos(x * Math.PI);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/motion/parse-motion3.ts
|
|
478
|
+
var SEGMENT_KIND = {
|
|
479
|
+
0: "linear",
|
|
480
|
+
1: "bezier",
|
|
481
|
+
2: "stepped",
|
|
482
|
+
3: "inverseStepped"
|
|
483
|
+
};
|
|
484
|
+
function parseMotion3(json) {
|
|
485
|
+
if (!json || typeof json !== "object") {
|
|
486
|
+
throw new Error(
|
|
487
|
+
"@doki-land/live2d: motion3.json root must be an object"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const root = json;
|
|
491
|
+
const version = Number(root.Version ?? 3);
|
|
492
|
+
const meta = root.Meta;
|
|
493
|
+
if (!meta || typeof meta !== "object") {
|
|
494
|
+
throw new Error("@doki-land/live2d: motion3.json missing Meta");
|
|
495
|
+
}
|
|
496
|
+
const m = meta;
|
|
497
|
+
const duration = num(m.Duration, "Meta.Duration");
|
|
498
|
+
const fps = num(m.Fps, "Meta.Fps");
|
|
499
|
+
const loop = m.Loop === true;
|
|
500
|
+
const areBeziersRestricted = m.AreBeziersRestricted !== false;
|
|
501
|
+
const fadeInTime = optionalNum(m.FadeInTime) ?? 0;
|
|
502
|
+
const fadeOutTime = optionalNum(m.FadeOutTime) ?? 0;
|
|
503
|
+
const curvesRaw = root.Curves;
|
|
504
|
+
if (!Array.isArray(curvesRaw)) {
|
|
505
|
+
throw new Error("@doki-land/live2d: motion3.json missing Curves");
|
|
506
|
+
}
|
|
507
|
+
const curves = curvesRaw.map((c, i) => parseCurve(c, i));
|
|
508
|
+
const userData = [];
|
|
509
|
+
if (Array.isArray(root.UserData)) {
|
|
510
|
+
for (const item of root.UserData) {
|
|
511
|
+
if (!item || typeof item !== "object") continue;
|
|
512
|
+
const u = item;
|
|
513
|
+
if (typeof u.Time === "number" && typeof u.Value === "string") {
|
|
514
|
+
userData.push({ time: u.Time, value: u.Value });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
userData.sort((a, b) => a.time - b.time);
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
version,
|
|
521
|
+
duration,
|
|
522
|
+
fps,
|
|
523
|
+
loop,
|
|
524
|
+
areBeziersRestricted,
|
|
525
|
+
fadeInTime,
|
|
526
|
+
fadeOutTime,
|
|
527
|
+
curves,
|
|
528
|
+
userData
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function parseCurve(raw, index) {
|
|
532
|
+
if (!raw || typeof raw !== "object") {
|
|
533
|
+
throw new Error(`@doki-land/live2d: Curves[${index}] invalid`);
|
|
534
|
+
}
|
|
535
|
+
const c = raw;
|
|
536
|
+
const target = c.Target;
|
|
537
|
+
const id = c.Id;
|
|
538
|
+
if (typeof target !== "string" || typeof id !== "string") {
|
|
539
|
+
throw new Error(`@doki-land/live2d: Curves[${index}] needs Target/Id`);
|
|
540
|
+
}
|
|
541
|
+
if (target !== "Parameter" && target !== "PartOpacity" && target !== "Model") {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`@doki-land/live2d: Curves[${index}] unknown Target ${target}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const segmentsFlat = c.Segments;
|
|
547
|
+
if (!Array.isArray(segmentsFlat) || segmentsFlat.length < 2) {
|
|
548
|
+
throw new Error(`@doki-land/live2d: Curves[${index}] empty Segments`);
|
|
549
|
+
}
|
|
550
|
+
const numbers = segmentsFlat.map((n, j) => {
|
|
551
|
+
if (typeof n !== "number" || !Number.isFinite(n)) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`@doki-land/live2d: Curves[${index}].Segments[${j}] not a number`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return n;
|
|
557
|
+
});
|
|
558
|
+
return {
|
|
559
|
+
target,
|
|
560
|
+
id,
|
|
561
|
+
fadeInTime: optionalNum(c.FadeInTime),
|
|
562
|
+
fadeOutTime: optionalNum(c.FadeOutTime),
|
|
563
|
+
segments: parseSegments(numbers, index)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function parseSegments(flat, curveIndex) {
|
|
567
|
+
let i = 0;
|
|
568
|
+
const p0 = { time: flat[i++], value: flat[i++] };
|
|
569
|
+
const out = [];
|
|
570
|
+
let prev = p0;
|
|
571
|
+
while (i < flat.length) {
|
|
572
|
+
const kindId = flat[i++];
|
|
573
|
+
const kind = SEGMENT_KIND[kindId];
|
|
574
|
+
if (!kind) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`@doki-land/live2d: Curves[${curveIndex}] unknown segment ${kindId}`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (kind === "bezier") {
|
|
580
|
+
if (i + 5 >= flat.length) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`@doki-land/live2d: Curves[${curveIndex}] truncated bezier`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const p1 = { time: flat[i++], value: flat[i++] };
|
|
586
|
+
const p2 = { time: flat[i++], value: flat[i++] };
|
|
587
|
+
const p3 = { time: flat[i++], value: flat[i++] };
|
|
588
|
+
out.push({ kind, p0: prev, p1, p2, p3 });
|
|
589
|
+
prev = p3;
|
|
590
|
+
} else {
|
|
591
|
+
if (i + 1 >= flat.length) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`@doki-land/live2d: Curves[${curveIndex}] truncated ${kind}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const p3 = { time: flat[i++], value: flat[i++] };
|
|
597
|
+
out.push({ kind, p0: prev, p3 });
|
|
598
|
+
prev = p3;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
function num(v, label) {
|
|
604
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
605
|
+
throw new Error(`@doki-land/live2d: motion3 ${label} must be a number`);
|
|
606
|
+
}
|
|
607
|
+
return v;
|
|
608
|
+
}
|
|
609
|
+
function optionalNum(v) {
|
|
610
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/stage/actor-model-slot.ts
|
|
614
|
+
function lerp(a, b, t) {
|
|
615
|
+
return a + (b - a) * Math.min(1, Math.max(0, t));
|
|
616
|
+
}
|
|
617
|
+
var ActorModelSlot = class {
|
|
618
|
+
#backends;
|
|
619
|
+
#renderer;
|
|
620
|
+
#onProgress;
|
|
621
|
+
#motionPlayer;
|
|
622
|
+
#motionCache = /* @__PURE__ */ new Map();
|
|
623
|
+
#drawPass = null;
|
|
624
|
+
#model = null;
|
|
625
|
+
#backend = null;
|
|
626
|
+
#textures = [];
|
|
627
|
+
#resolver = null;
|
|
628
|
+
#loadGeneration = 0;
|
|
629
|
+
constructor(options) {
|
|
630
|
+
this.#backends = options.backends;
|
|
631
|
+
this.#renderer = options.renderer;
|
|
632
|
+
this.#onProgress = options.onProgress;
|
|
633
|
+
this.#motionPlayer = new MotionPlayer({
|
|
634
|
+
onStart: options.onMotionStart,
|
|
635
|
+
onFinish: options.onMotionFinish
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
get model() {
|
|
639
|
+
return this.#model;
|
|
640
|
+
}
|
|
641
|
+
get drawPass() {
|
|
642
|
+
return this.#drawPass;
|
|
643
|
+
}
|
|
644
|
+
ensureDrawPass() {
|
|
645
|
+
if (!this.#drawPass) {
|
|
646
|
+
this.#drawPass = this.#renderer.createModelDrawPass();
|
|
647
|
+
}
|
|
648
|
+
return this.#drawPass;
|
|
649
|
+
}
|
|
650
|
+
#report(payload) {
|
|
651
|
+
this.#onProgress?.(payload);
|
|
652
|
+
}
|
|
653
|
+
#clearTextures() {
|
|
654
|
+
if (this.#textures.length > 0) {
|
|
655
|
+
releaseTextureData(this.#textures);
|
|
656
|
+
this.#textures = [];
|
|
657
|
+
}
|
|
658
|
+
this.#drawPass?.setTextures([]);
|
|
659
|
+
}
|
|
660
|
+
#applyMotionSamples(samples) {
|
|
661
|
+
if (!this.#model || !this.#backend) return;
|
|
662
|
+
for (const s of samples) {
|
|
663
|
+
if (s.weight <= 0) continue;
|
|
664
|
+
if (s.target === "PartOpacity") {
|
|
665
|
+
if (!this.#backend.setPartOpacity) continue;
|
|
666
|
+
if (s.weight >= 1) {
|
|
667
|
+
this.#backend.setPartOpacity(this.#model, s.id, s.value);
|
|
668
|
+
} else {
|
|
669
|
+
const cur2 = 1;
|
|
670
|
+
this.#backend.setPartOpacity(
|
|
671
|
+
this.#model,
|
|
672
|
+
s.id,
|
|
673
|
+
cur2 + (s.value - cur2) * s.weight
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (s.target !== "Parameter" || !this.#backend.setParameter)
|
|
679
|
+
continue;
|
|
680
|
+
if (s.weight >= 1) {
|
|
681
|
+
this.#backend.setParameter(this.#model, s.id, s.value);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const cur = this.#backend.listParameters?.(this.#model).find((p) => p.id === s.id)?.value ?? s.value;
|
|
685
|
+
this.#backend.setParameter(
|
|
686
|
+
this.#model,
|
|
687
|
+
s.id,
|
|
688
|
+
cur + (s.value - cur) * s.weight
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async load(source, resolver) {
|
|
693
|
+
const gen = ++this.#loadGeneration;
|
|
694
|
+
this.#report({
|
|
695
|
+
stage: "mounting",
|
|
696
|
+
progress: 0.01,
|
|
697
|
+
detail: "prepare draw pass"
|
|
698
|
+
});
|
|
699
|
+
const drawPass = this.ensureDrawPass();
|
|
700
|
+
this.#report({
|
|
701
|
+
stage: "resolve",
|
|
702
|
+
progress: 0.02,
|
|
703
|
+
detail: "resolve source"
|
|
704
|
+
});
|
|
705
|
+
let json;
|
|
706
|
+
let baseUrl;
|
|
707
|
+
let settingsUrl;
|
|
708
|
+
if (typeof source === "object" && source.kind === "json") {
|
|
709
|
+
json = source.json;
|
|
710
|
+
baseUrl = source.baseUrl;
|
|
711
|
+
settingsUrl = source.baseUrl;
|
|
712
|
+
this.#report({
|
|
713
|
+
stage: "settings",
|
|
714
|
+
progress: 0.2,
|
|
715
|
+
detail: "inline settings"
|
|
716
|
+
});
|
|
717
|
+
} else {
|
|
718
|
+
const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl(source) : source.url;
|
|
719
|
+
const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
|
|
720
|
+
const fetchUrl = resolveModelSourceUrl(raw, {
|
|
721
|
+
npmCdnBase: cdnBase
|
|
722
|
+
});
|
|
723
|
+
this.#report({
|
|
724
|
+
stage: "settings",
|
|
725
|
+
progress: 0.05,
|
|
726
|
+
detail: fetchUrl
|
|
727
|
+
});
|
|
728
|
+
json = await fetchModelJson(fetchUrl, (u) => {
|
|
729
|
+
const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
|
|
730
|
+
this.#report({
|
|
731
|
+
stage: "settings",
|
|
732
|
+
progress: lerp(0.05, 0.22, ratio),
|
|
733
|
+
detail: fetchUrl,
|
|
734
|
+
bytesLoaded: u.bytesLoaded,
|
|
735
|
+
bytesTotal: u.bytesTotal
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
baseUrl = fetchUrl;
|
|
739
|
+
settingsUrl = fetchUrl;
|
|
740
|
+
}
|
|
741
|
+
if (gen !== this.#loadGeneration) {
|
|
742
|
+
throw new Error("@doki-land/live2d: load cancelled");
|
|
743
|
+
}
|
|
744
|
+
const settings = normalizeModelSettings(json, settingsUrl);
|
|
745
|
+
this.#report({
|
|
746
|
+
stage: "moc",
|
|
747
|
+
progress: 0.25,
|
|
748
|
+
detail: settings.moc
|
|
749
|
+
});
|
|
750
|
+
const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
|
|
751
|
+
onBytesProgress: (key, u) => {
|
|
752
|
+
const isMoc = key === settings.moc;
|
|
753
|
+
const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
|
|
754
|
+
if (isMoc) {
|
|
755
|
+
this.#report({
|
|
756
|
+
stage: "moc",
|
|
757
|
+
progress: lerp(0.25, 0.8, ratio),
|
|
758
|
+
detail: key,
|
|
759
|
+
bytesLoaded: u.bytesLoaded,
|
|
760
|
+
bytesTotal: u.bytesTotal
|
|
761
|
+
});
|
|
762
|
+
} else {
|
|
763
|
+
this.#report({
|
|
764
|
+
stage: "textures",
|
|
765
|
+
progress: lerp(0.8, 0.9, ratio),
|
|
766
|
+
detail: key,
|
|
767
|
+
bytesLoaded: u.bytesLoaded,
|
|
768
|
+
bytesTotal: u.bytesTotal
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
this.#resolver = assetResolver;
|
|
774
|
+
this.#motionPlayer.clear();
|
|
775
|
+
this.#motionCache.clear();
|
|
776
|
+
const backend = selectModelBackend([...this.#backends], json);
|
|
777
|
+
this.#report({
|
|
778
|
+
stage: "decode",
|
|
779
|
+
progress: 0.85,
|
|
780
|
+
detail: `decode ${settings.format}`
|
|
781
|
+
});
|
|
782
|
+
const next = await backend.createModel(settings, {
|
|
783
|
+
renderer: this.#renderer,
|
|
784
|
+
resolver: assetResolver
|
|
785
|
+
});
|
|
786
|
+
if (gen !== this.#loadGeneration) {
|
|
787
|
+
backend.destroyModel(next);
|
|
788
|
+
throw new Error("@doki-land/live2d: load cancelled");
|
|
789
|
+
}
|
|
790
|
+
this.#clearTextures();
|
|
791
|
+
if (settings.textures.length > 0) {
|
|
792
|
+
this.#report({
|
|
793
|
+
stage: "textures",
|
|
794
|
+
progress: 0.88,
|
|
795
|
+
detail: `${settings.textures.length} textures`
|
|
796
|
+
});
|
|
797
|
+
const textures = await loadTextureData(
|
|
798
|
+
assetResolver,
|
|
799
|
+
settings.textures,
|
|
800
|
+
{
|
|
801
|
+
onProgress: (u) => {
|
|
802
|
+
const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
|
|
803
|
+
this.#report({
|
|
804
|
+
stage: "textures",
|
|
805
|
+
progress: lerp(0.88, 0.96, ratio),
|
|
806
|
+
detail: u.key,
|
|
807
|
+
bytesLoaded: u.bytesLoaded,
|
|
808
|
+
bytesTotal: u.bytesTotal
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
);
|
|
813
|
+
if (gen !== this.#loadGeneration) {
|
|
814
|
+
releaseTextureData(textures);
|
|
815
|
+
backend.destroyModel(next);
|
|
816
|
+
throw new Error("@doki-land/live2d: load cancelled");
|
|
817
|
+
}
|
|
818
|
+
this.#textures = textures;
|
|
819
|
+
drawPass.setTextures(textures);
|
|
820
|
+
}
|
|
821
|
+
if (this.#model && this.#backend) {
|
|
822
|
+
this.#backend.destroyModel(this.#model);
|
|
823
|
+
}
|
|
824
|
+
this.#model = next;
|
|
825
|
+
this.#backend = backend;
|
|
826
|
+
this.#report({
|
|
827
|
+
stage: "ready",
|
|
828
|
+
progress: 1,
|
|
829
|
+
detail: next.id
|
|
830
|
+
});
|
|
831
|
+
return next;
|
|
832
|
+
}
|
|
833
|
+
setParameter(id, value) {
|
|
834
|
+
if (!this.#model || !this.#backend?.setParameter) return;
|
|
835
|
+
this.#backend.setParameter(this.#model, id, value);
|
|
836
|
+
}
|
|
837
|
+
listParameters() {
|
|
838
|
+
if (!this.#model || !this.#backend?.listParameters) return [];
|
|
839
|
+
return this.#backend.listParameters(this.#model);
|
|
840
|
+
}
|
|
841
|
+
listMotionGroups() {
|
|
842
|
+
return this.#model?.settings.motionGroups ?? {};
|
|
843
|
+
}
|
|
844
|
+
async playMotion(group, index = 0, options = {}) {
|
|
845
|
+
if (!this.#model || !this.#resolver) return false;
|
|
846
|
+
const list = this.#model.settings.motionGroups[group];
|
|
847
|
+
const def = list?.[index];
|
|
848
|
+
if (!def) return false;
|
|
849
|
+
let clip = this.#motionCache.get(def.file);
|
|
850
|
+
if (!clip) {
|
|
851
|
+
const json = await this.#resolver.fetchJson(def.file);
|
|
852
|
+
clip = parseMotion3(json);
|
|
853
|
+
this.#motionCache.set(def.file, clip);
|
|
854
|
+
}
|
|
855
|
+
const fadeInTime = options.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
|
|
856
|
+
const fadeOutTime = options.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
|
|
857
|
+
return this.#motionPlayer.start(group, index, clip, {
|
|
858
|
+
priority: options.priority ?? MotionPriority.normal,
|
|
859
|
+
slot: options.slot,
|
|
860
|
+
queue: options.queue,
|
|
861
|
+
loop: options.loop,
|
|
862
|
+
fadeInTime,
|
|
863
|
+
fadeOutTime
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
stopMotion(opts) {
|
|
867
|
+
this.#motionPlayer.stop(opts?.fade !== false, opts?.slot);
|
|
868
|
+
}
|
|
869
|
+
listPlayingMotions() {
|
|
870
|
+
return this.#motionPlayer.listPlaying();
|
|
871
|
+
}
|
|
872
|
+
update(deltaTimeSeconds) {
|
|
873
|
+
if (!this.#model || !this.#backend || !this.#drawPass) return null;
|
|
874
|
+
this.#applyMotionSamples(this.#motionPlayer.update(deltaTimeSeconds));
|
|
875
|
+
this.#backend.updateModel(this.#model, deltaTimeSeconds);
|
|
876
|
+
return this.#backend.getDrawables(this.#model);
|
|
877
|
+
}
|
|
878
|
+
hitTestModelCoords(modelX, modelY) {
|
|
879
|
+
if (!this.#model || !this.#backend) return null;
|
|
880
|
+
const drawables = this.#backend.getDrawables(this.#model);
|
|
881
|
+
for (let n = drawables.length - 1; n >= 0; n -= 1) {
|
|
882
|
+
const d = drawables[n];
|
|
883
|
+
if (!d.visible || d.opacity <= 0) continue;
|
|
884
|
+
const p = d.vertexPositions;
|
|
885
|
+
const idx = d.indices;
|
|
886
|
+
for (let i = 0; i + 2 < idx.length; i += 3) {
|
|
887
|
+
const a = idx[i] * 2, b = idx[i + 1] * 2, c = idx[i + 2] * 2;
|
|
888
|
+
const ax = p[a], ay = p[a + 1];
|
|
889
|
+
const bx = p[b], by = p[b + 1];
|
|
890
|
+
const cx = p[c], cy = p[c + 1];
|
|
891
|
+
const s = (ax - cx) * (modelY - cy) - (ay - cy) * (modelX - cx);
|
|
892
|
+
const s1 = (bx - ax) * (modelY - ay) - (by - ay) * (modelX - ax);
|
|
893
|
+
const s2 = (cx - bx) * (modelY - by) - (cy - by) * (modelX - bx);
|
|
894
|
+
if (s >= 0 && s1 >= 0 && s2 >= 0 || s <= 0 && s1 <= 0 && s2 <= 0) {
|
|
895
|
+
const hitArea = this.#model.settings.hitAreas.find(
|
|
896
|
+
(h) => h.id === `D_${d.index}` || h.id === `${d.index}`
|
|
897
|
+
);
|
|
898
|
+
return hitArea?.name ?? `drawable:${d.index}`;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
destroy() {
|
|
905
|
+
this.#loadGeneration += 1;
|
|
906
|
+
this.#motionPlayer.clear();
|
|
907
|
+
this.#motionCache.clear();
|
|
908
|
+
this.#resolver = null;
|
|
909
|
+
if (this.#model && this.#backend) {
|
|
910
|
+
this.#backend.destroyModel(this.#model);
|
|
911
|
+
}
|
|
912
|
+
this.#model = null;
|
|
913
|
+
this.#backend = null;
|
|
914
|
+
this.#clearTextures();
|
|
915
|
+
this.#drawPass?.destroy();
|
|
916
|
+
this.#drawPass = null;
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// src/stage/transform.ts
|
|
921
|
+
import { DEFAULT_ACTOR_TRANSFORM } from "@doki-land/live2d-core";
|
|
922
|
+
function resolveActorTransform(patch) {
|
|
923
|
+
const base = DEFAULT_ACTOR_TRANSFORM;
|
|
924
|
+
const scale = patch?.scale ?? base.scale ?? 1;
|
|
925
|
+
return {
|
|
926
|
+
x: patch?.x ?? base.x,
|
|
927
|
+
y: patch?.y ?? base.y,
|
|
928
|
+
scale,
|
|
929
|
+
scaleX: patch?.scaleX ?? patch?.scale ?? scale,
|
|
930
|
+
scaleY: patch?.scaleY ?? patch?.scale ?? scale,
|
|
931
|
+
rotation: patch?.rotation ?? base.rotation ?? 0,
|
|
932
|
+
anchorX: patch?.anchorX ?? base.anchorX ?? 0.5,
|
|
933
|
+
anchorY: patch?.anchorY ?? base.anchorY ?? 1
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function modelNdcToStage(modelX, modelY, transform) {
|
|
937
|
+
const scaleX = transform.scaleX ?? transform.scale ?? 1;
|
|
938
|
+
const scaleY = transform.scaleY ?? transform.scale ?? 1;
|
|
939
|
+
const anchorX = transform.anchorX ?? 0.5;
|
|
940
|
+
const anchorY = transform.anchorY ?? 1;
|
|
941
|
+
const rotation = transform.rotation ?? 0;
|
|
942
|
+
const anchorModelX = -1 + anchorX * 2;
|
|
943
|
+
const anchorModelY = 1 - anchorY * 2;
|
|
944
|
+
let localX = (modelX - anchorModelX) * scaleX * 0.5;
|
|
945
|
+
let localY = (anchorModelY - modelY) * scaleY * 0.5;
|
|
946
|
+
if (rotation !== 0) {
|
|
947
|
+
const c = Math.cos(rotation);
|
|
948
|
+
const s = Math.sin(rotation);
|
|
949
|
+
const rx = localX * c - localY * s;
|
|
950
|
+
const ry = localX * s + localY * c;
|
|
951
|
+
localX = rx;
|
|
952
|
+
localY = ry;
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
stageX: transform.x + localX,
|
|
956
|
+
stageY: transform.y + localY
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function stageToModelNdc(stageX, stageY, transform) {
|
|
960
|
+
const scaleX = transform.scaleX ?? transform.scale ?? 1;
|
|
961
|
+
const scaleY = transform.scaleY ?? transform.scale ?? 1;
|
|
962
|
+
const anchorX = transform.anchorX ?? 0.5;
|
|
963
|
+
const anchorY = transform.anchorY ?? 1;
|
|
964
|
+
const anchorModelX = -1 + anchorX * 2;
|
|
965
|
+
const anchorModelY = 1 - anchorY * 2;
|
|
966
|
+
const localX = (stageX - transform.x) / (scaleX * 0.5);
|
|
967
|
+
const localY = (stageY - transform.y) / (scaleY * 0.5);
|
|
968
|
+
return {
|
|
969
|
+
modelX: localX + anchorModelX,
|
|
970
|
+
modelY: anchorModelY - localY
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function clientToStage(clientX, clientY, canvas) {
|
|
974
|
+
const rect = canvas.getBoundingClientRect();
|
|
975
|
+
const x = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;
|
|
976
|
+
const y = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
|
|
977
|
+
return {
|
|
978
|
+
stageX: Math.min(1, Math.max(0, x)),
|
|
979
|
+
stageY: Math.min(1, Math.max(0, y))
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function stageFocusDrag(stageX, stageY, transform) {
|
|
983
|
+
const dx = stageX - transform.x;
|
|
984
|
+
const dy = transform.y - stageY;
|
|
985
|
+
const dragX = Math.min(1, Math.max(-1, dx * 2));
|
|
986
|
+
const dragY = Math.min(1, Math.max(-1, dy * 2));
|
|
987
|
+
return { dragX, dragY };
|
|
988
|
+
}
|
|
989
|
+
function layerOrderIndex(layer, definedLayers) {
|
|
990
|
+
const idx = definedLayers.indexOf(layer);
|
|
991
|
+
return idx >= 0 ? idx : definedLayers.length;
|
|
992
|
+
}
|
|
993
|
+
function compareActorsForDraw(a, b, definedLayers) {
|
|
994
|
+
const la = layerOrderIndex(a.layer, definedLayers);
|
|
995
|
+
const lb = layerOrderIndex(b.layer, definedLayers);
|
|
996
|
+
return la - lb || a.order - b.order || a.creationIndex - b.creationIndex;
|
|
997
|
+
}
|
|
998
|
+
function compareActorsForHit(a, b, definedLayers) {
|
|
999
|
+
return compareActorsForDraw(b, a, definedLayers);
|
|
1000
|
+
}
|
|
1001
|
+
function transformDrawablesForStage(drawables, transform, opacity) {
|
|
1002
|
+
const alpha = Math.min(1, Math.max(0, opacity));
|
|
1003
|
+
return drawables.map((d) => {
|
|
1004
|
+
if (!d.visible || alpha <= 0) return { ...d, visible: false };
|
|
1005
|
+
const pos = new Float32Array(d.vertexPositions.length);
|
|
1006
|
+
for (let i = 0; i < pos.length; i += 2) {
|
|
1007
|
+
const { stageX, stageY } = modelNdcToStage(
|
|
1008
|
+
d.vertexPositions[i],
|
|
1009
|
+
d.vertexPositions[i + 1],
|
|
1010
|
+
transform
|
|
1011
|
+
);
|
|
1012
|
+
pos[i] = stageX * 2 - 1;
|
|
1013
|
+
pos[i + 1] = 1 - stageY * 2;
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
...d,
|
|
1017
|
+
vertexPositions: pos,
|
|
1018
|
+
opacity: d.opacity * alpha
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/stage/actor.ts
|
|
1024
|
+
var nextActorId = 0;
|
|
1025
|
+
var Live2dActorImpl = class {
|
|
1026
|
+
id;
|
|
1027
|
+
creationIndex;
|
|
1028
|
+
#transform;
|
|
1029
|
+
#visible = true;
|
|
1030
|
+
#opacity = 1;
|
|
1031
|
+
#layer = "characters";
|
|
1032
|
+
#order = 0;
|
|
1033
|
+
#destroyed = false;
|
|
1034
|
+
#lastDrawables = null;
|
|
1035
|
+
#slot;
|
|
1036
|
+
constructor(options, shared) {
|
|
1037
|
+
this.id = options?.id ?? shared.id;
|
|
1038
|
+
this.creationIndex = shared.creationIndex;
|
|
1039
|
+
this.#transform = resolveActorTransform(options?.transform);
|
|
1040
|
+
this.#visible = options?.visible ?? true;
|
|
1041
|
+
this.#opacity = options?.opacity ?? 1;
|
|
1042
|
+
this.#layer = options?.layer ?? "characters";
|
|
1043
|
+
this.#order = options?.order ?? 0;
|
|
1044
|
+
this.#slot = new ActorModelSlot({
|
|
1045
|
+
backends: shared.backends,
|
|
1046
|
+
renderer: shared.renderer
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
get model() {
|
|
1050
|
+
return this.#slot.model;
|
|
1051
|
+
}
|
|
1052
|
+
get visible() {
|
|
1053
|
+
return this.#visible;
|
|
1054
|
+
}
|
|
1055
|
+
set visible(value) {
|
|
1056
|
+
this.#visible = value;
|
|
1057
|
+
}
|
|
1058
|
+
get opacity() {
|
|
1059
|
+
return this.#opacity;
|
|
1060
|
+
}
|
|
1061
|
+
set opacity(value) {
|
|
1062
|
+
this.#opacity = Math.min(1, Math.max(0, value));
|
|
1063
|
+
}
|
|
1064
|
+
get layer() {
|
|
1065
|
+
return this.#layer;
|
|
1066
|
+
}
|
|
1067
|
+
set layer(value) {
|
|
1068
|
+
this.#layer = value;
|
|
1069
|
+
}
|
|
1070
|
+
get order() {
|
|
1071
|
+
return this.#order;
|
|
1072
|
+
}
|
|
1073
|
+
set order(value) {
|
|
1074
|
+
this.#order = value;
|
|
1075
|
+
}
|
|
1076
|
+
getTransform() {
|
|
1077
|
+
return { ...this.#transform };
|
|
1078
|
+
}
|
|
1079
|
+
setTransform(patch) {
|
|
1080
|
+
this.#transform = resolveActorTransform({
|
|
1081
|
+
...this.#transform,
|
|
1082
|
+
...patch
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async load(source, resolver) {
|
|
1086
|
+
if (this.#destroyed) {
|
|
1087
|
+
throw new Error("@doki-land/live2d: actor destroyed");
|
|
1088
|
+
}
|
|
1089
|
+
return await this.#slot.load(source, resolver);
|
|
1090
|
+
}
|
|
1091
|
+
setParameter(id, value) {
|
|
1092
|
+
this.#slot.setParameter(id, value);
|
|
1093
|
+
}
|
|
1094
|
+
lookAt(stageX, stageY) {
|
|
1095
|
+
const { dragX, dragY } = stageFocusDrag(
|
|
1096
|
+
stageX,
|
|
1097
|
+
stageY,
|
|
1098
|
+
this.#transform
|
|
1099
|
+
);
|
|
1100
|
+
for (const u of focusParameterUpdates(
|
|
1101
|
+
this.#slot.listParameters(),
|
|
1102
|
+
dragX,
|
|
1103
|
+
dragY
|
|
1104
|
+
)) {
|
|
1105
|
+
this.#slot.setParameter(u.id, u.value);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/** Internal: evaluate motion/physics and cache drawables for render. */
|
|
1109
|
+
update(deltaTimeSeconds) {
|
|
1110
|
+
if (!this.#visible || this.#opacity <= 0) {
|
|
1111
|
+
this.#lastDrawables = null;
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
this.#lastDrawables = this.#slot.update(deltaTimeSeconds);
|
|
1115
|
+
return this.#lastDrawables;
|
|
1116
|
+
}
|
|
1117
|
+
get lastDrawables() {
|
|
1118
|
+
return this.#lastDrawables;
|
|
1119
|
+
}
|
|
1120
|
+
get slot() {
|
|
1121
|
+
return this.#slot;
|
|
1122
|
+
}
|
|
1123
|
+
hitTestStage(stageX, stageY) {
|
|
1124
|
+
if (!this.#visible || this.#opacity <= 0 || !this.#slot.model)
|
|
1125
|
+
return null;
|
|
1126
|
+
const { modelX, modelY } = stageToModelNdc(
|
|
1127
|
+
stageX,
|
|
1128
|
+
stageY,
|
|
1129
|
+
this.#transform
|
|
1130
|
+
);
|
|
1131
|
+
const area = this.#slot.hitTestModelCoords(modelX, modelY);
|
|
1132
|
+
if (!area) return null;
|
|
1133
|
+
const drawableMatch = /^drawable:(\d+)$/.exec(area);
|
|
1134
|
+
return {
|
|
1135
|
+
actorId: this.id,
|
|
1136
|
+
area,
|
|
1137
|
+
drawableIndex: drawableMatch ? Number(drawableMatch[1]) : -1,
|
|
1138
|
+
stageX,
|
|
1139
|
+
stageY,
|
|
1140
|
+
localX: modelX,
|
|
1141
|
+
localY: modelY
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
destroy() {
|
|
1145
|
+
if (this.#destroyed) return;
|
|
1146
|
+
this.#destroyed = true;
|
|
1147
|
+
this.#slot.destroy();
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
function allocateActorId(prefix = "actor") {
|
|
1151
|
+
nextActorId += 1;
|
|
1152
|
+
return `${prefix}-${nextActorId}`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/stage/single-facade.ts
|
|
1156
|
+
import { EventEmitter } from "@doki-land/live2d-core";
|
|
1157
|
+
function nowMs() {
|
|
1158
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1159
|
+
}
|
|
1160
|
+
function createSingleActorFacade(stage, actor, backends) {
|
|
1161
|
+
const events = new EventEmitter();
|
|
1162
|
+
let canvas = null;
|
|
1163
|
+
let phase = "idle";
|
|
1164
|
+
let lastError = null;
|
|
1165
|
+
let generation = 0;
|
|
1166
|
+
let fpsSmooth = 0;
|
|
1167
|
+
const setPhase = (next) => {
|
|
1168
|
+
phase = next;
|
|
1169
|
+
events.emit("phase", { phase, generation });
|
|
1170
|
+
};
|
|
1171
|
+
const state = () => ({
|
|
1172
|
+
phase,
|
|
1173
|
+
lastError,
|
|
1174
|
+
generation
|
|
1175
|
+
});
|
|
1176
|
+
const runtime = {
|
|
1177
|
+
events,
|
|
1178
|
+
backends,
|
|
1179
|
+
renderer: stage.renderer,
|
|
1180
|
+
stage,
|
|
1181
|
+
actor,
|
|
1182
|
+
get model() {
|
|
1183
|
+
return actor.model;
|
|
1184
|
+
},
|
|
1185
|
+
get state() {
|
|
1186
|
+
return state();
|
|
1187
|
+
},
|
|
1188
|
+
mount(target) {
|
|
1189
|
+
generation += 1;
|
|
1190
|
+
canvas = target;
|
|
1191
|
+
setPhase("mounting");
|
|
1192
|
+
void stage.mount(target).then(
|
|
1193
|
+
() => setPhase(actor.model ? "live" : "ready"),
|
|
1194
|
+
(err) => {
|
|
1195
|
+
lastError = err;
|
|
1196
|
+
setPhase("error");
|
|
1197
|
+
events.emit("error", { error: err });
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
},
|
|
1201
|
+
async loadModel(source, resolver) {
|
|
1202
|
+
setPhase("loading");
|
|
1203
|
+
try {
|
|
1204
|
+
const model = await actor.load(source, resolver);
|
|
1205
|
+
lastError = null;
|
|
1206
|
+
setPhase("live");
|
|
1207
|
+
events.emit("ready", { modelId: model.id });
|
|
1208
|
+
return model;
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
lastError = err;
|
|
1211
|
+
setPhase("error");
|
|
1212
|
+
events.emit("error", { error: err });
|
|
1213
|
+
throw err;
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
captureFrame() {
|
|
1217
|
+
return null;
|
|
1218
|
+
},
|
|
1219
|
+
setParameter(id, value) {
|
|
1220
|
+
actor.setParameter(id, value);
|
|
1221
|
+
},
|
|
1222
|
+
hitTest(x, y) {
|
|
1223
|
+
const stageX = (x + 1) / 2;
|
|
1224
|
+
const stageY = (1 - y) / 2;
|
|
1225
|
+
const hit = stage.hitTest(stageX, stageY);
|
|
1226
|
+
if (!hit || hit.actorId !== actor.id) return null;
|
|
1227
|
+
return hit.area;
|
|
1228
|
+
},
|
|
1229
|
+
listParameters() {
|
|
1230
|
+
return actor.slot.listParameters();
|
|
1231
|
+
},
|
|
1232
|
+
listMotionGroups() {
|
|
1233
|
+
return actor.slot.listMotionGroups();
|
|
1234
|
+
},
|
|
1235
|
+
playMotion(group, index, options) {
|
|
1236
|
+
return actor.slot.playMotion(group, index, options);
|
|
1237
|
+
},
|
|
1238
|
+
stopMotion(opts) {
|
|
1239
|
+
actor.slot.stopMotion(opts);
|
|
1240
|
+
},
|
|
1241
|
+
listPlayingMotions() {
|
|
1242
|
+
return actor.slot.listPlayingMotions();
|
|
1243
|
+
},
|
|
1244
|
+
async capturePng(opts = {}) {
|
|
1245
|
+
if (!canvas) {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
"@doki-land/live2d: mount(canvas) before capturePng"
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (phase === "live") {
|
|
1251
|
+
runtime.update(0);
|
|
1252
|
+
}
|
|
1253
|
+
const mime = opts.mimeType ?? "image/png";
|
|
1254
|
+
return await new Promise((resolve, reject) => {
|
|
1255
|
+
canvas.toBlob(
|
|
1256
|
+
(blob) => {
|
|
1257
|
+
if (blob) resolve(blob);
|
|
1258
|
+
else
|
|
1259
|
+
reject(
|
|
1260
|
+
new Error(
|
|
1261
|
+
"@doki-land/live2d: canvas.toBlob returned null"
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
},
|
|
1265
|
+
mime,
|
|
1266
|
+
opts.quality
|
|
1267
|
+
);
|
|
1268
|
+
});
|
|
1269
|
+
},
|
|
1270
|
+
update(deltaTimeSeconds) {
|
|
1271
|
+
if (phase !== "live" && phase !== "ready") return;
|
|
1272
|
+
const t0 = nowMs();
|
|
1273
|
+
stage.update(deltaTimeSeconds);
|
|
1274
|
+
stage.render();
|
|
1275
|
+
const t1 = nowMs();
|
|
1276
|
+
const drawables = actor.lastDrawables ?? [];
|
|
1277
|
+
let vertexCount = 0;
|
|
1278
|
+
let indexCount = 0;
|
|
1279
|
+
for (const d of drawables) {
|
|
1280
|
+
vertexCount += d.vertexPositions.length / 2;
|
|
1281
|
+
indexCount += d.indices.length;
|
|
1282
|
+
}
|
|
1283
|
+
const frameMs = t1 - t0;
|
|
1284
|
+
const fps = deltaTimeSeconds > 0 ? 1 / deltaTimeSeconds : 0;
|
|
1285
|
+
fpsSmooth = fpsSmooth <= 0 ? fps : fpsSmooth * 0.85 + fps * 0.15;
|
|
1286
|
+
events.emit("profile", {
|
|
1287
|
+
fps,
|
|
1288
|
+
fpsSmooth,
|
|
1289
|
+
frameMs,
|
|
1290
|
+
evaluateMs: frameMs,
|
|
1291
|
+
drawMs: 0,
|
|
1292
|
+
drawableCount: drawables.length,
|
|
1293
|
+
vertexCount,
|
|
1294
|
+
indexCount
|
|
1295
|
+
});
|
|
1296
|
+
},
|
|
1297
|
+
destroy() {
|
|
1298
|
+
generation += 1;
|
|
1299
|
+
stage.destroy();
|
|
1300
|
+
canvas = null;
|
|
1301
|
+
setPhase("destroyed");
|
|
1302
|
+
events.clear();
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
return runtime;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/stage/stage.ts
|
|
1309
|
+
import {
|
|
1310
|
+
createMoc2Backend,
|
|
1311
|
+
createMoc3Backend,
|
|
1312
|
+
createRenderer
|
|
1313
|
+
} from "@doki-land/live2d-renderer";
|
|
1314
|
+
var Live2dStageImpl = class {
|
|
1315
|
+
#backends;
|
|
1316
|
+
#renderer;
|
|
1317
|
+
#updateMode;
|
|
1318
|
+
#actors = /* @__PURE__ */ new Map();
|
|
1319
|
+
#definedLayers = [
|
|
1320
|
+
"background",
|
|
1321
|
+
"characters-back",
|
|
1322
|
+
"characters",
|
|
1323
|
+
"characters-front",
|
|
1324
|
+
"effects"
|
|
1325
|
+
];
|
|
1326
|
+
#pointerListeners = /* @__PURE__ */ new Map([
|
|
1327
|
+
["pointerdown", /* @__PURE__ */ new Set()],
|
|
1328
|
+
["pointermove", /* @__PURE__ */ new Set()],
|
|
1329
|
+
["pointerup", /* @__PURE__ */ new Set()]
|
|
1330
|
+
]);
|
|
1331
|
+
#canvas = null;
|
|
1332
|
+
#initPromise = null;
|
|
1333
|
+
#rafId = null;
|
|
1334
|
+
#running = false;
|
|
1335
|
+
#paused = false;
|
|
1336
|
+
#lastFrameMs = 0;
|
|
1337
|
+
#creationCounter = 0;
|
|
1338
|
+
#focusedActorId = null;
|
|
1339
|
+
#lastPointer = null;
|
|
1340
|
+
#pointerTracking = { mode: "focused" };
|
|
1341
|
+
#boundPointerDown;
|
|
1342
|
+
#boundPointerMove;
|
|
1343
|
+
#boundPointerUp;
|
|
1344
|
+
#destroyed = false;
|
|
1345
|
+
constructor(options = {}) {
|
|
1346
|
+
this.#backends = options.backends ?? [
|
|
1347
|
+
createMoc2Backend(),
|
|
1348
|
+
createMoc3Backend()
|
|
1349
|
+
];
|
|
1350
|
+
this.#renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
|
|
1351
|
+
this.#updateMode = options.updateMode ?? "auto";
|
|
1352
|
+
}
|
|
1353
|
+
get actors() {
|
|
1354
|
+
return [...this.#actors.values()];
|
|
1355
|
+
}
|
|
1356
|
+
get pointerTracking() {
|
|
1357
|
+
return this.#pointerTracking;
|
|
1358
|
+
}
|
|
1359
|
+
set pointerTracking(policy) {
|
|
1360
|
+
this.#pointerTracking = policy;
|
|
1361
|
+
}
|
|
1362
|
+
get renderer() {
|
|
1363
|
+
return this.#renderer;
|
|
1364
|
+
}
|
|
1365
|
+
async mount(canvas) {
|
|
1366
|
+
if (this.#destroyed) {
|
|
1367
|
+
throw new Error("@doki-land/live2d: stage destroyed");
|
|
1368
|
+
}
|
|
1369
|
+
this.#canvas = canvas;
|
|
1370
|
+
this.#initPromise = this.#renderer.initialize(canvas);
|
|
1371
|
+
await this.#initPromise;
|
|
1372
|
+
this.#attachPointerListeners(canvas);
|
|
1373
|
+
}
|
|
1374
|
+
createActor(options) {
|
|
1375
|
+
if (this.#destroyed) {
|
|
1376
|
+
throw new Error("@doki-land/live2d: stage destroyed");
|
|
1377
|
+
}
|
|
1378
|
+
const id = options?.id ?? allocateActorId();
|
|
1379
|
+
if (this.#actors.has(id)) {
|
|
1380
|
+
throw new Error(`@doki-land/live2d: duplicate actor id "${id}"`);
|
|
1381
|
+
}
|
|
1382
|
+
const creationIndex = this.#creationCounter++;
|
|
1383
|
+
const actor = new Live2dActorImpl(options, {
|
|
1384
|
+
id,
|
|
1385
|
+
creationIndex,
|
|
1386
|
+
backends: this.#backends,
|
|
1387
|
+
renderer: this.#renderer
|
|
1388
|
+
});
|
|
1389
|
+
this.#actors.set(id, actor);
|
|
1390
|
+
if (!this.#focusedActorId) {
|
|
1391
|
+
this.#focusedActorId = id;
|
|
1392
|
+
}
|
|
1393
|
+
return actor;
|
|
1394
|
+
}
|
|
1395
|
+
removeActor(actorOrId) {
|
|
1396
|
+
const id = typeof actorOrId === "string" ? actorOrId : actorOrId.id;
|
|
1397
|
+
const actor = this.#actors.get(id);
|
|
1398
|
+
if (!actor) return;
|
|
1399
|
+
actor.destroy();
|
|
1400
|
+
this.#actors.delete(id);
|
|
1401
|
+
if (this.#focusedActorId === id) {
|
|
1402
|
+
this.#focusedActorId = this.#actors.keys().next().value ?? null;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
defineLayers(layers) {
|
|
1406
|
+
this.#definedLayers.length = 0;
|
|
1407
|
+
this.#definedLayers.push(...layers);
|
|
1408
|
+
}
|
|
1409
|
+
update(deltaTimeSeconds) {
|
|
1410
|
+
if (this.#destroyed) return;
|
|
1411
|
+
for (const actor of this.#actors.values()) {
|
|
1412
|
+
actor.update(deltaTimeSeconds);
|
|
1413
|
+
}
|
|
1414
|
+
this.#applyPointerTracking();
|
|
1415
|
+
}
|
|
1416
|
+
render() {
|
|
1417
|
+
if (this.#destroyed || !this.#canvas) return;
|
|
1418
|
+
const sorted = [...this.#actors.values()].sort(
|
|
1419
|
+
(a, b) => compareActorsForDraw(a, b, this.#definedLayers)
|
|
1420
|
+
);
|
|
1421
|
+
this.#renderer.beginFrame();
|
|
1422
|
+
for (const actor of sorted) {
|
|
1423
|
+
if (!actor.visible || actor.opacity <= 0) continue;
|
|
1424
|
+
const drawables = actor.lastDrawables;
|
|
1425
|
+
const pass = actor.slot.drawPass;
|
|
1426
|
+
if (!drawables || !pass) continue;
|
|
1427
|
+
const placed = transformDrawablesForStage(
|
|
1428
|
+
drawables,
|
|
1429
|
+
actor.getTransform(),
|
|
1430
|
+
actor.opacity
|
|
1431
|
+
);
|
|
1432
|
+
pass.draw(placed, new Float32Array(16));
|
|
1433
|
+
}
|
|
1434
|
+
this.#renderer.endFrame();
|
|
1435
|
+
}
|
|
1436
|
+
start() {
|
|
1437
|
+
if (this.#updateMode === "manual") {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
"@doki-land/live2d: start() is not available when updateMode is manual"
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (this.#running) return;
|
|
1443
|
+
this.#running = true;
|
|
1444
|
+
this.#paused = false;
|
|
1445
|
+
this.#lastFrameMs = nowMs2();
|
|
1446
|
+
const tick = () => {
|
|
1447
|
+
if (!this.#running) return;
|
|
1448
|
+
if (!this.#paused) {
|
|
1449
|
+
const t = nowMs2();
|
|
1450
|
+
const dt = Math.min(0.1, (t - this.#lastFrameMs) / 1e3);
|
|
1451
|
+
this.#lastFrameMs = t;
|
|
1452
|
+
this.update(dt);
|
|
1453
|
+
this.render();
|
|
1454
|
+
}
|
|
1455
|
+
this.#rafId = requestAnimationFrame(tick);
|
|
1456
|
+
};
|
|
1457
|
+
this.#rafId = requestAnimationFrame(tick);
|
|
1458
|
+
}
|
|
1459
|
+
pause() {
|
|
1460
|
+
this.#paused = true;
|
|
1461
|
+
}
|
|
1462
|
+
resume() {
|
|
1463
|
+
this.#paused = false;
|
|
1464
|
+
this.#lastFrameMs = nowMs2();
|
|
1465
|
+
}
|
|
1466
|
+
stop() {
|
|
1467
|
+
this.#running = false;
|
|
1468
|
+
this.#paused = false;
|
|
1469
|
+
if (this.#rafId !== null) {
|
|
1470
|
+
cancelAnimationFrame(this.#rafId);
|
|
1471
|
+
this.#rafId = null;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
hitTest(stageX, stageY) {
|
|
1475
|
+
const hits = this.hitTestAll(stageX, stageY);
|
|
1476
|
+
return hits[0] ?? null;
|
|
1477
|
+
}
|
|
1478
|
+
hitTestAll(stageX, stageY) {
|
|
1479
|
+
const sorted = [...this.#actors.values()].sort(
|
|
1480
|
+
(a, b) => compareActorsForHit(a, b, this.#definedLayers)
|
|
1481
|
+
);
|
|
1482
|
+
const hits = [];
|
|
1483
|
+
for (const actor of sorted) {
|
|
1484
|
+
const partial = actor.hitTestStage(stageX, stageY);
|
|
1485
|
+
if (!partial) continue;
|
|
1486
|
+
hits.push({ ...partial, actor });
|
|
1487
|
+
}
|
|
1488
|
+
return hits;
|
|
1489
|
+
}
|
|
1490
|
+
addEventListener(type, listener) {
|
|
1491
|
+
this.#pointerListeners.get(type)?.add(listener);
|
|
1492
|
+
}
|
|
1493
|
+
removeEventListener(type, listener) {
|
|
1494
|
+
this.#pointerListeners.get(type)?.delete(listener);
|
|
1495
|
+
}
|
|
1496
|
+
destroy() {
|
|
1497
|
+
if (this.#destroyed) return;
|
|
1498
|
+
this.#destroyed = true;
|
|
1499
|
+
this.stop();
|
|
1500
|
+
this.#detachPointerListeners();
|
|
1501
|
+
for (const actor of this.#actors.values()) {
|
|
1502
|
+
actor.destroy();
|
|
1503
|
+
}
|
|
1504
|
+
this.#actors.clear();
|
|
1505
|
+
this.#renderer.destroy();
|
|
1506
|
+
this.#canvas = null;
|
|
1507
|
+
this.#initPromise = null;
|
|
1508
|
+
for (const set of this.#pointerListeners.values()) {
|
|
1509
|
+
set.clear();
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
#attachPointerListeners(canvas) {
|
|
1513
|
+
this.#detachPointerListeners();
|
|
1514
|
+
this.#boundPointerDown = (e) => this.#onPointer("pointerdown", e);
|
|
1515
|
+
this.#boundPointerMove = (e) => this.#onPointer("pointermove", e);
|
|
1516
|
+
this.#boundPointerUp = (e) => this.#onPointer("pointerup", e);
|
|
1517
|
+
canvas.addEventListener("pointerdown", this.#boundPointerDown);
|
|
1518
|
+
canvas.addEventListener("pointermove", this.#boundPointerMove);
|
|
1519
|
+
canvas.addEventListener("pointerup", this.#boundPointerUp);
|
|
1520
|
+
}
|
|
1521
|
+
#detachPointerListeners() {
|
|
1522
|
+
if (!this.#canvas) return;
|
|
1523
|
+
if (this.#boundPointerDown) {
|
|
1524
|
+
this.#canvas.removeEventListener(
|
|
1525
|
+
"pointerdown",
|
|
1526
|
+
this.#boundPointerDown
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (this.#boundPointerMove) {
|
|
1530
|
+
this.#canvas.removeEventListener(
|
|
1531
|
+
"pointermove",
|
|
1532
|
+
this.#boundPointerMove
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (this.#boundPointerUp) {
|
|
1536
|
+
this.#canvas.removeEventListener("pointerup", this.#boundPointerUp);
|
|
1537
|
+
}
|
|
1538
|
+
this.#boundPointerDown = void 0;
|
|
1539
|
+
this.#boundPointerMove = void 0;
|
|
1540
|
+
this.#boundPointerUp = void 0;
|
|
1541
|
+
}
|
|
1542
|
+
#onPointer(type, event) {
|
|
1543
|
+
if (!this.#canvas) return;
|
|
1544
|
+
const { stageX, stageY } = clientToStage(
|
|
1545
|
+
event.clientX,
|
|
1546
|
+
event.clientY,
|
|
1547
|
+
this.#canvas
|
|
1548
|
+
);
|
|
1549
|
+
this.#lastPointer = { stageX, stageY };
|
|
1550
|
+
const hit = this.hitTest(stageX, stageY);
|
|
1551
|
+
if (type === "pointerdown" && hit) {
|
|
1552
|
+
this.#focusedActorId = hit.actorId;
|
|
1553
|
+
}
|
|
1554
|
+
const payload = {
|
|
1555
|
+
actor: hit?.actor ?? null,
|
|
1556
|
+
actorId: hit?.actorId ?? null,
|
|
1557
|
+
area: hit?.area ?? null,
|
|
1558
|
+
stageX,
|
|
1559
|
+
stageY,
|
|
1560
|
+
clientX: event.clientX,
|
|
1561
|
+
clientY: event.clientY,
|
|
1562
|
+
hit
|
|
1563
|
+
};
|
|
1564
|
+
for (const listener of this.#pointerListeners.get(type) ?? []) {
|
|
1565
|
+
listener(payload);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
#applyPointerTracking() {
|
|
1569
|
+
if (!this.#canvas || !this.#lastPointer) return;
|
|
1570
|
+
const { stageX, stageY } = this.#lastPointer;
|
|
1571
|
+
const mode = this.#pointerTracking.mode;
|
|
1572
|
+
if (mode === "none") return;
|
|
1573
|
+
if (mode === "all") {
|
|
1574
|
+
for (const actor of this.#actors.values()) {
|
|
1575
|
+
actor.lookAt(stageX, stageY);
|
|
1576
|
+
}
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (mode === "custom" && this.#pointerTracking.targetActorId) {
|
|
1580
|
+
const actor = this.#actors.get(this.#pointerTracking.targetActorId);
|
|
1581
|
+
actor?.lookAt(stageX, stageY);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (mode === "hovered") {
|
|
1585
|
+
for (const actor of this.#actors.values()) {
|
|
1586
|
+
if (actor.hitTestStage(stageX, stageY)) {
|
|
1587
|
+
actor.lookAt(stageX, stageY);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (mode === "nearest") {
|
|
1593
|
+
let best = null;
|
|
1594
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
1595
|
+
for (const actor of this.#actors.values()) {
|
|
1596
|
+
const t = actor.getTransform();
|
|
1597
|
+
const dx = t.x - stageX;
|
|
1598
|
+
const dy = t.y - stageY;
|
|
1599
|
+
const dist = dx * dx + dy * dy;
|
|
1600
|
+
if (dist < bestDist) {
|
|
1601
|
+
bestDist = dist;
|
|
1602
|
+
best = actor;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
best?.lookAt(stageX, stageY);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (mode === "focused" && this.#focusedActorId) {
|
|
1609
|
+
this.#actors.get(this.#focusedActorId)?.lookAt(stageX, stageY);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
function nowMs2() {
|
|
1614
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
1615
|
+
}
|
|
1616
|
+
function createLive2dStage(options) {
|
|
1617
|
+
return new Live2dStageImpl(options);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/create-live2d.ts
|
|
1621
|
+
function createLive2D(options = {}) {
|
|
1622
|
+
const backends = options.backends ?? [
|
|
1623
|
+
createMoc2Backend2(),
|
|
1624
|
+
createMoc3Backend2()
|
|
1625
|
+
];
|
|
1626
|
+
const stage = createLive2dStage({
|
|
1627
|
+
backends,
|
|
1628
|
+
renderer: options.renderer ?? createRenderer2({ prefer: options.prefer }),
|
|
1629
|
+
updateMode: options.updateMode ?? "manual"
|
|
1630
|
+
});
|
|
1631
|
+
const actor = stage.createActor({
|
|
1632
|
+
id: allocateActorId("default")
|
|
1633
|
+
});
|
|
1634
|
+
return createSingleActorFacade(stage, actor, backends);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// src/index.ts
|
|
1638
|
+
var LIVE2D_VERSION = "0.0.0";
|
|
1639
|
+
export {
|
|
1640
|
+
DEFAULT_ACTOR_TRANSFORM2 as DEFAULT_ACTOR_TRANSFORM,
|
|
1641
|
+
DEFAULT_NPM_CDN,
|
|
1642
|
+
EventEmitter2 as EventEmitter,
|
|
1643
|
+
LIVE2D_VERSION,
|
|
1644
|
+
MotionPlayer,
|
|
1645
|
+
MotionPriority,
|
|
1646
|
+
blendMotionLayers,
|
|
1647
|
+
createCanvas2DRenderer,
|
|
1648
|
+
createLive2D,
|
|
1649
|
+
createLive2dStage,
|
|
1650
|
+
createMoc2Backend3 as createMoc2Backend,
|
|
1651
|
+
createMoc3Backend3 as createMoc3Backend,
|
|
1652
|
+
createQuadProgram,
|
|
1653
|
+
createRenderer3 as createRenderer,
|
|
1654
|
+
createWebGl2Renderer,
|
|
1655
|
+
createWebGpuRenderer,
|
|
1656
|
+
decodeMoc3,
|
|
1657
|
+
evaluateCurve,
|
|
1658
|
+
evaluateFrame,
|
|
1659
|
+
evaluateMotion3,
|
|
1660
|
+
fingerprintSnapshot,
|
|
1661
|
+
focusParameterUpdates,
|
|
1662
|
+
parseCpuProgram,
|
|
1663
|
+
parseMotion3,
|
|
1664
|
+
resolveModelSourceUrl2 as resolveModelSourceUrl,
|
|
1665
|
+
resolveNpmSpecifier,
|
|
1666
|
+
serializeCpuProgram
|
|
1667
|
+
};
|