@doki-land/live2d-core 0.0.0 → 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 -1
- package/dist/index.d.ts +346 -0
- package/dist/index.js +131 -0
- package/package.json +42 -1
- package/src/contracts.ts +63 -0
- package/src/detect-format.ts +31 -0
- package/src/events.ts +92 -0
- package/src/frame.ts +35 -0
- package/src/index.ts +60 -0
- package/src/model.ts +41 -0
- package/src/program.ts +50 -0
- package/src/session.ts +78 -0
- package/src/stage.ts +147 -0
package/README.md
CHANGED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal loading / session contracts shared across packages.
|
|
3
|
+
* Format decode and GPU details stay in live2d-renderer.
|
|
4
|
+
*/
|
|
5
|
+
/** How the host names a model settings document. */
|
|
6
|
+
type ModelSource = string | {
|
|
7
|
+
readonly kind: "url";
|
|
8
|
+
readonly url: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "json";
|
|
11
|
+
readonly json: unknown;
|
|
12
|
+
readonly baseUrl: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "npm";
|
|
15
|
+
/** Package name, optionally with `@version` (e.g. `live2d-widget-model-hijiki@1.0.5`). */
|
|
16
|
+
readonly package: string;
|
|
17
|
+
/** Path inside the package to model JSON. */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
/** Optional CDN base; default is applied by the loader. */
|
|
20
|
+
readonly cdnBase?: string;
|
|
21
|
+
};
|
|
22
|
+
declare function modelSourceUrl(source: ModelSource): string;
|
|
23
|
+
/** Relative asset key as declared in settings (moc, texture, motion, …). */
|
|
24
|
+
type AssetKey = string;
|
|
25
|
+
/** Resolve and fetch assets relative to a model settings URL. */
|
|
26
|
+
interface AssetResolver {
|
|
27
|
+
readonly baseUrl: string;
|
|
28
|
+
resolve(key: AssetKey): string;
|
|
29
|
+
fetchJson(key: AssetKey): Promise<unknown>;
|
|
30
|
+
fetchBytes(key: AssetKey): Promise<ArrayBuffer>;
|
|
31
|
+
}
|
|
32
|
+
/** Session lifecycle (facade state machine). */
|
|
33
|
+
type SessionPhase = "idle" | "mounting" | "ready" | "loading" | "live" | "error" | "destroyed";
|
|
34
|
+
interface SessionState {
|
|
35
|
+
readonly phase: SessionPhase;
|
|
36
|
+
readonly lastError: unknown | null;
|
|
37
|
+
readonly generation: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Model binary family (file extension / settings shape). */
|
|
41
|
+
type ModelFormat = "moc2" | "moc3";
|
|
42
|
+
/** Normalized, format-agnostic model settings. */
|
|
43
|
+
interface ModelSettings {
|
|
44
|
+
format: ModelFormat;
|
|
45
|
+
url: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
moc: string;
|
|
48
|
+
textures: string[];
|
|
49
|
+
motionGroups: Record<string, MotionDefinition[]>;
|
|
50
|
+
expressions: ExpressionDefinition[];
|
|
51
|
+
physics?: string;
|
|
52
|
+
pose?: string;
|
|
53
|
+
hitAreas: HitAreaDefinition[];
|
|
54
|
+
layout?: Record<string, number>;
|
|
55
|
+
}
|
|
56
|
+
interface MotionDefinition {
|
|
57
|
+
file: string;
|
|
58
|
+
sound?: string;
|
|
59
|
+
fadeInTime?: number;
|
|
60
|
+
fadeOutTime?: number;
|
|
61
|
+
}
|
|
62
|
+
interface ExpressionDefinition {
|
|
63
|
+
name: string;
|
|
64
|
+
file: string;
|
|
65
|
+
}
|
|
66
|
+
interface HitAreaDefinition {
|
|
67
|
+
name: string;
|
|
68
|
+
id: string;
|
|
69
|
+
}
|
|
70
|
+
/** Opaque in-memory model handle. */
|
|
71
|
+
interface InternalModel {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly settings: ModelSettings;
|
|
74
|
+
readonly format: ModelFormat;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Settings JSON format detection (moc2 vs moc3).
|
|
79
|
+
* Shared by loader normalize and renderer backends — keep binary peek in renderer.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** Detect moc2 vs moc3 from model settings JSON shape. Returns null if unknown. */
|
|
83
|
+
declare function detectModelSettingsFormat(json: unknown): ModelFormat | null;
|
|
84
|
+
|
|
85
|
+
/** Load pipeline stage for progress UI. */
|
|
86
|
+
type LoadProgressStage = "mounting" | "resolve" | "settings" | "moc" | "textures" | "decode" | "ready";
|
|
87
|
+
interface LoadProgress {
|
|
88
|
+
readonly stage: LoadProgressStage;
|
|
89
|
+
/** Overall 0..1 */
|
|
90
|
+
readonly progress: number;
|
|
91
|
+
readonly detail?: string;
|
|
92
|
+
readonly bytesLoaded?: number;
|
|
93
|
+
readonly bytesTotal?: number | null;
|
|
94
|
+
}
|
|
95
|
+
/** Per-frame timing / mesh cost for Status / profiler UIs. */
|
|
96
|
+
interface FrameProfile {
|
|
97
|
+
/** Instantaneous FPS from wall dt (0 if first frame). */
|
|
98
|
+
readonly fps: number;
|
|
99
|
+
/** EMA-smoothed FPS for readable UI. */
|
|
100
|
+
readonly fpsSmooth: number;
|
|
101
|
+
/** Whole `update()` wall time in ms. */
|
|
102
|
+
readonly frameMs: number;
|
|
103
|
+
/** `updateModel` + `getDrawables` in ms. */
|
|
104
|
+
readonly evaluateMs: number;
|
|
105
|
+
/** `beginFrame` + `draw` + `endFrame` in ms. */
|
|
106
|
+
readonly drawMs: number;
|
|
107
|
+
readonly drawableCount: number;
|
|
108
|
+
/** Sum of drawable vertex counts (xy pairs). */
|
|
109
|
+
readonly vertexCount: number;
|
|
110
|
+
/** Sum of drawable index counts. */
|
|
111
|
+
readonly indexCount: number;
|
|
112
|
+
}
|
|
113
|
+
/** Minimal event bus used by sessions and loaders. */
|
|
114
|
+
type Live2DEventMap = {
|
|
115
|
+
ready: {
|
|
116
|
+
modelId: string;
|
|
117
|
+
};
|
|
118
|
+
error: {
|
|
119
|
+
error: unknown;
|
|
120
|
+
};
|
|
121
|
+
progress: LoadProgress;
|
|
122
|
+
/** Fired after each successful `update()` while live. */
|
|
123
|
+
profile: FrameProfile;
|
|
124
|
+
hit: {
|
|
125
|
+
area: string;
|
|
126
|
+
x: number;
|
|
127
|
+
y: number;
|
|
128
|
+
};
|
|
129
|
+
"motion:start": {
|
|
130
|
+
group: string;
|
|
131
|
+
index: number;
|
|
132
|
+
slot: string;
|
|
133
|
+
};
|
|
134
|
+
"motion:finish": {
|
|
135
|
+
group: string;
|
|
136
|
+
index: number;
|
|
137
|
+
slot: string;
|
|
138
|
+
};
|
|
139
|
+
phase: {
|
|
140
|
+
phase: string;
|
|
141
|
+
generation: number;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
type Live2DEventName = keyof Live2DEventMap;
|
|
145
|
+
type Live2DListener<K extends Live2DEventName> = (payload: Live2DEventMap[K]) => void;
|
|
146
|
+
declare class EventEmitter {
|
|
147
|
+
#private;
|
|
148
|
+
on<K extends Live2DEventName>(event: K, listener: Live2DListener<K>): () => void;
|
|
149
|
+
off<K extends Live2DEventName>(event: K, listener: Live2DListener<K>): void;
|
|
150
|
+
emit<K extends Live2DEventName>(event: K, payload: Live2DEventMap[K]): void;
|
|
151
|
+
clear(): void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* CPU frame output — format-agnostic mesh list ready for any RenderDevice.
|
|
156
|
+
*/
|
|
157
|
+
/** Blend modes aligned with renderer DrawableMesh. */
|
|
158
|
+
declare const FrameBlendMode: {
|
|
159
|
+
readonly Normal: 0;
|
|
160
|
+
readonly Additive: 1;
|
|
161
|
+
readonly Multiplicative: 2;
|
|
162
|
+
};
|
|
163
|
+
type FrameBlendMode = (typeof FrameBlendMode)[keyof typeof FrameBlendMode];
|
|
164
|
+
/** One drawable after CPU deform for a single frame. */
|
|
165
|
+
interface FrameDrawable {
|
|
166
|
+
readonly index: number;
|
|
167
|
+
readonly textureIndex: number;
|
|
168
|
+
/** Interleaved x,y in model space. */
|
|
169
|
+
readonly positions: Float32Array;
|
|
170
|
+
readonly uvs: Float32Array;
|
|
171
|
+
readonly indices: Uint16Array;
|
|
172
|
+
readonly opacity: number;
|
|
173
|
+
readonly blendMode: FrameBlendMode;
|
|
174
|
+
readonly renderOrder: number;
|
|
175
|
+
readonly visible: boolean;
|
|
176
|
+
readonly invertedMask: boolean;
|
|
177
|
+
readonly maskIndices: readonly number[];
|
|
178
|
+
}
|
|
179
|
+
/** Immutable CPU snapshot consumed by GPU backends or golden tests. */
|
|
180
|
+
interface FrameSnapshot {
|
|
181
|
+
readonly timeSeconds: number;
|
|
182
|
+
readonly drawables: readonly FrameDrawable[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* CPU model program / instance shapes (format adapters fill these).
|
|
187
|
+
*/
|
|
188
|
+
interface ParameterProgram {
|
|
189
|
+
readonly id: string;
|
|
190
|
+
readonly min: number;
|
|
191
|
+
readonly max: number;
|
|
192
|
+
readonly defaultValue: number;
|
|
193
|
+
}
|
|
194
|
+
interface DrawableProgram {
|
|
195
|
+
readonly index: number;
|
|
196
|
+
readonly textureIndex: number;
|
|
197
|
+
/** Rest-pose x,y pairs. */
|
|
198
|
+
readonly positions: Float32Array;
|
|
199
|
+
readonly uvs: Float32Array;
|
|
200
|
+
readonly indices: Uint16Array;
|
|
201
|
+
readonly opacity: number;
|
|
202
|
+
readonly renderOrder: number;
|
|
203
|
+
/** {@link FrameBlendMode} value. */
|
|
204
|
+
readonly blendMode: number;
|
|
205
|
+
readonly invertedMask: boolean;
|
|
206
|
+
/** Indices into `ModelProgram.drawables` used as clipping masks. */
|
|
207
|
+
readonly maskIndices: readonly number[];
|
|
208
|
+
readonly visible: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Optional linear deform driven by one parameter.
|
|
211
|
+
* `deltas[i] = positions_at_max[i] - rest[i]`.
|
|
212
|
+
*/
|
|
213
|
+
readonly deformParamIndex: number;
|
|
214
|
+
readonly deformDeltas: Float32Array | null;
|
|
215
|
+
}
|
|
216
|
+
/** Static topology + deform tables after decode. */
|
|
217
|
+
interface ModelProgram {
|
|
218
|
+
readonly format: "moc2" | "moc3";
|
|
219
|
+
/** Codec tag, e.g. `cpu-program` or `moc3`. */
|
|
220
|
+
readonly codec: string;
|
|
221
|
+
readonly parameters: readonly ParameterProgram[];
|
|
222
|
+
readonly drawables: readonly DrawableProgram[];
|
|
223
|
+
}
|
|
224
|
+
/** Mutable runtime values over a ModelProgram. */
|
|
225
|
+
interface ModelInstance {
|
|
226
|
+
readonly program: ModelProgram;
|
|
227
|
+
/** Parallel to program.parameters. */
|
|
228
|
+
readonly parameterValues: Float32Array;
|
|
229
|
+
timeSeconds: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** High-level runtime session for one mounted surface. */
|
|
233
|
+
interface Live2DSession {
|
|
234
|
+
readonly events: EventEmitter;
|
|
235
|
+
readonly model: InternalModel | null;
|
|
236
|
+
readonly state: SessionState;
|
|
237
|
+
mount(canvas: HTMLCanvasElement): void;
|
|
238
|
+
loadModel(source: ModelSource, resolver?: AssetResolver): Promise<InternalModel>;
|
|
239
|
+
/** Latest CPU frame after update, or null if no model. */
|
|
240
|
+
captureFrame(): FrameSnapshot | null;
|
|
241
|
+
update(deltaTimeSeconds: number): void;
|
|
242
|
+
hitTest(x: number, y: number): string | null;
|
|
243
|
+
destroy(): void;
|
|
244
|
+
}
|
|
245
|
+
declare function createSessionStub(): Live2DSession;
|
|
246
|
+
|
|
247
|
+
/** Normalized stage placement for one actor (0,0) top-left → (1,1) bottom-right. */
|
|
248
|
+
interface ActorTransform {
|
|
249
|
+
/** Anchor point X in stage space. */
|
|
250
|
+
x: number;
|
|
251
|
+
/** Anchor point Y in stage space. */
|
|
252
|
+
y: number;
|
|
253
|
+
/** Uniform scale; ignored when `scaleX` / `scaleY` are set. */
|
|
254
|
+
scale?: number;
|
|
255
|
+
scaleX?: number;
|
|
256
|
+
scaleY?: number;
|
|
257
|
+
/** Radians, counter-clockwise. */
|
|
258
|
+
rotation?: number;
|
|
259
|
+
/** Horizontal anchor within the model bounds (0 = left, 1 = right). */
|
|
260
|
+
anchorX?: number;
|
|
261
|
+
/** Vertical anchor within the model bounds (0 = top, 1 = bottom in stage space). */
|
|
262
|
+
anchorY?: number;
|
|
263
|
+
}
|
|
264
|
+
declare const DEFAULT_ACTOR_TRANSFORM: Readonly<ActorTransform>;
|
|
265
|
+
type PointerTrackingMode = "all" | "focused" | "hovered" | "nearest" | "none" | "custom";
|
|
266
|
+
interface PointerTrackingPolicy {
|
|
267
|
+
mode: PointerTrackingMode;
|
|
268
|
+
/** Used when `mode` is `custom`. */
|
|
269
|
+
targetActorId?: string;
|
|
270
|
+
}
|
|
271
|
+
interface CreateActorOptions {
|
|
272
|
+
id?: string;
|
|
273
|
+
transform?: Partial<ActorTransform>;
|
|
274
|
+
layer?: string;
|
|
275
|
+
order?: number;
|
|
276
|
+
visible?: boolean;
|
|
277
|
+
opacity?: number;
|
|
278
|
+
}
|
|
279
|
+
interface ActorHit {
|
|
280
|
+
readonly actor: Live2dActor;
|
|
281
|
+
readonly actorId: string;
|
|
282
|
+
readonly area: string;
|
|
283
|
+
readonly drawableIndex: number;
|
|
284
|
+
readonly stageX: number;
|
|
285
|
+
readonly stageY: number;
|
|
286
|
+
readonly localX: number;
|
|
287
|
+
readonly localY: number;
|
|
288
|
+
}
|
|
289
|
+
interface StagePointerEvent {
|
|
290
|
+
readonly actor: Live2dActor | null;
|
|
291
|
+
readonly actorId: string | null;
|
|
292
|
+
readonly area: string | null;
|
|
293
|
+
readonly stageX: number;
|
|
294
|
+
readonly stageY: number;
|
|
295
|
+
readonly clientX: number;
|
|
296
|
+
readonly clientY: number;
|
|
297
|
+
readonly hit: ActorHit | null;
|
|
298
|
+
}
|
|
299
|
+
type StageUpdateMode = "auto" | "manual";
|
|
300
|
+
interface CreateLive2dStageOptions {
|
|
301
|
+
updateMode?: StageUpdateMode;
|
|
302
|
+
}
|
|
303
|
+
/** Read-only actor surface exposed by the stage. */
|
|
304
|
+
interface Live2dActor {
|
|
305
|
+
readonly id: string;
|
|
306
|
+
readonly model: InternalModel | null;
|
|
307
|
+
readonly visible: boolean;
|
|
308
|
+
readonly opacity: number;
|
|
309
|
+
readonly layer: string;
|
|
310
|
+
readonly order: number;
|
|
311
|
+
readonly creationIndex: number;
|
|
312
|
+
getTransform(): ActorTransform;
|
|
313
|
+
setTransform(patch: Partial<ActorTransform>): void;
|
|
314
|
+
load(source: ModelSource, resolver?: AssetResolver): Promise<InternalModel>;
|
|
315
|
+
setParameter(id: string, value: number): void;
|
|
316
|
+
lookAt(stageX: number, stageY: number): void;
|
|
317
|
+
destroy(): void;
|
|
318
|
+
}
|
|
319
|
+
/** Multi-character stage owning one canvas surface and shared renderer. */
|
|
320
|
+
interface Live2dStage {
|
|
321
|
+
readonly actors: readonly Live2dActor[];
|
|
322
|
+
mount(canvas: HTMLCanvasElement): Promise<void>;
|
|
323
|
+
createActor(options?: CreateActorOptions): Live2dActor;
|
|
324
|
+
removeActor(actor: Live2dActor | string): void;
|
|
325
|
+
defineLayers(layers: readonly string[]): void;
|
|
326
|
+
update(deltaTimeSeconds: number): void;
|
|
327
|
+
render(): void;
|
|
328
|
+
start(): void;
|
|
329
|
+
pause(): void;
|
|
330
|
+
resume(): void;
|
|
331
|
+
stop(): void;
|
|
332
|
+
hitTest(stageX: number, stageY: number): ActorHit | null;
|
|
333
|
+
hitTestAll(stageX: number, stageY: number): readonly ActorHit[];
|
|
334
|
+
addEventListener(type: "pointerdown" | "pointermove" | "pointerup", listener: (event: StagePointerEvent) => void): void;
|
|
335
|
+
removeEventListener(type: "pointerdown" | "pointermove" | "pointerup", listener: (event: StagePointerEvent) => void): void;
|
|
336
|
+
destroy(): void;
|
|
337
|
+
pointerTracking: PointerTrackingPolicy;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* `@doki-land/live2d-core` — types, events, session contracts.
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
declare const LIVE2D_CORE_VERSION: "0.0.0";
|
|
345
|
+
|
|
346
|
+
export { type ActorHit, type ActorTransform, type AssetKey, type AssetResolver, type CreateActorOptions, type CreateLive2dStageOptions, DEFAULT_ACTOR_TRANSFORM, type DrawableProgram, EventEmitter, type ExpressionDefinition, FrameBlendMode, type FrameDrawable, type FrameProfile, type FrameSnapshot, type HitAreaDefinition, type InternalModel, LIVE2D_CORE_VERSION, type Live2DEventMap, type Live2DEventName, type Live2DListener, type Live2DSession, type Live2dActor, type Live2dStage, type LoadProgress, type LoadProgressStage, type ModelFormat, type ModelInstance, type ModelProgram, type ModelSettings, type ModelSource, type MotionDefinition, type ParameterProgram, type PointerTrackingMode, type PointerTrackingPolicy, type SessionPhase, type SessionState, type StagePointerEvent, type StageUpdateMode, createSessionStub, detectModelSettingsFormat, modelSourceUrl };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// src/contracts.ts
|
|
2
|
+
function modelSourceUrl(source) {
|
|
3
|
+
if (typeof source === "string") return source;
|
|
4
|
+
if (source.kind === "url") return source.url;
|
|
5
|
+
if (source.kind === "npm") {
|
|
6
|
+
const path = source.path.replace(/^\/+/, "");
|
|
7
|
+
return `npm:${source.package}/${path}`;
|
|
8
|
+
}
|
|
9
|
+
return source.baseUrl;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/detect-format.ts
|
|
13
|
+
function detectModelSettingsFormat(json) {
|
|
14
|
+
if (!json || typeof json !== "object") return null;
|
|
15
|
+
const o = json;
|
|
16
|
+
const fileRefs = o.FileReferences;
|
|
17
|
+
if (fileRefs && typeof fileRefs === "object") {
|
|
18
|
+
const moc = fileRefs.Moc;
|
|
19
|
+
if (typeof moc === "string") {
|
|
20
|
+
const lower = moc.toLowerCase();
|
|
21
|
+
if (lower.endsWith(".moc3") || lower.endsWith(".program.json")) {
|
|
22
|
+
return "moc3";
|
|
23
|
+
}
|
|
24
|
+
if (lower.endsWith(".moc")) {
|
|
25
|
+
return "moc2";
|
|
26
|
+
}
|
|
27
|
+
return "moc3";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (typeof o.model === "string" && Array.isArray(o.textures)) {
|
|
31
|
+
return "moc2";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/events.ts
|
|
37
|
+
var EventEmitter = class {
|
|
38
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
39
|
+
on(event, listener) {
|
|
40
|
+
const set = this.#listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
41
|
+
set.add(listener);
|
|
42
|
+
this.#listeners.set(event, set);
|
|
43
|
+
return () => this.off(event, listener);
|
|
44
|
+
}
|
|
45
|
+
off(event, listener) {
|
|
46
|
+
this.#listeners.get(event)?.delete(listener);
|
|
47
|
+
}
|
|
48
|
+
emit(event, payload) {
|
|
49
|
+
for (const listener of this.#listeners.get(event) ?? []) {
|
|
50
|
+
listener(payload);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
clear() {
|
|
54
|
+
this.#listeners.clear();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/frame.ts
|
|
59
|
+
var FrameBlendMode = {
|
|
60
|
+
Normal: 0,
|
|
61
|
+
Additive: 1,
|
|
62
|
+
Multiplicative: 2
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/session.ts
|
|
66
|
+
function createSessionStub() {
|
|
67
|
+
let canvas = null;
|
|
68
|
+
let model = null;
|
|
69
|
+
const events = new EventEmitter();
|
|
70
|
+
let generation = 0;
|
|
71
|
+
return {
|
|
72
|
+
events,
|
|
73
|
+
get model() {
|
|
74
|
+
return model;
|
|
75
|
+
},
|
|
76
|
+
get state() {
|
|
77
|
+
return {
|
|
78
|
+
phase: model ? "live" : canvas ? "ready" : "idle",
|
|
79
|
+
lastError: null,
|
|
80
|
+
generation
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
mount(target) {
|
|
84
|
+
canvas = target;
|
|
85
|
+
generation += 1;
|
|
86
|
+
},
|
|
87
|
+
async loadModel(_source, _resolver) {
|
|
88
|
+
void canvas;
|
|
89
|
+
void _resolver;
|
|
90
|
+
throw new Error(
|
|
91
|
+
"@doki-land/live2d-core: loadModel is provided by @doki-land/live2d facade"
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
captureFrame() {
|
|
95
|
+
return null;
|
|
96
|
+
},
|
|
97
|
+
update(_deltaTimeSeconds) {
|
|
98
|
+
if (!model) return;
|
|
99
|
+
},
|
|
100
|
+
hitTest(_x, _y) {
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
destroy() {
|
|
104
|
+
model = null;
|
|
105
|
+
canvas = null;
|
|
106
|
+
generation += 1;
|
|
107
|
+
events.clear();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/stage.ts
|
|
113
|
+
var DEFAULT_ACTOR_TRANSFORM = {
|
|
114
|
+
x: 0.5,
|
|
115
|
+
y: 1,
|
|
116
|
+
scale: 1,
|
|
117
|
+
anchorX: 0.5,
|
|
118
|
+
anchorY: 1
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/index.ts
|
|
122
|
+
var LIVE2D_CORE_VERSION = "0.0.0";
|
|
123
|
+
export {
|
|
124
|
+
DEFAULT_ACTOR_TRANSFORM,
|
|
125
|
+
EventEmitter,
|
|
126
|
+
FrameBlendMode,
|
|
127
|
+
LIVE2D_CORE_VERSION,
|
|
128
|
+
createSessionStub,
|
|
129
|
+
detectModelSettingsFormat,
|
|
130
|
+
modelSourceUrl
|
|
131
|
+
};
|
package/package.json
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@doki-land/live2d-core",
|
|
3
|
+
"version": "0.0.12",
|
|
4
|
+
"description": "Live2D types, events, and session contracts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Doki Land",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"live2d",
|
|
10
|
+
"doki-land"
|
|
11
|
+
],
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run --passWithNoTests"
|
|
36
|
+
},
|
|
37
|
+
"sideEffects": false,
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/doki-land/live2d.ts.git"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal loading / session contracts shared across packages.
|
|
3
|
+
* Format decode and GPU details stay in live2d-renderer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** How the host names a model settings document. */
|
|
7
|
+
export type ModelSource =
|
|
8
|
+
| string
|
|
9
|
+
| { readonly kind: "url"; readonly url: string }
|
|
10
|
+
| {
|
|
11
|
+
readonly kind: "json";
|
|
12
|
+
readonly json: unknown;
|
|
13
|
+
readonly baseUrl: string;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
readonly kind: "npm";
|
|
17
|
+
/** Package name, optionally with `@version` (e.g. `live2d-widget-model-hijiki@1.0.5`). */
|
|
18
|
+
readonly package: string;
|
|
19
|
+
/** Path inside the package to model JSON. */
|
|
20
|
+
readonly path: string;
|
|
21
|
+
/** Optional CDN base; default is applied by the loader. */
|
|
22
|
+
readonly cdnBase?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function modelSourceUrl(source: ModelSource): string {
|
|
26
|
+
if (typeof source === "string") return source;
|
|
27
|
+
if (source.kind === "url") return source.url;
|
|
28
|
+
if (source.kind === "npm") {
|
|
29
|
+
const path = source.path.replace(/^\/+/, "");
|
|
30
|
+
return `npm:${source.package}/${path}`;
|
|
31
|
+
}
|
|
32
|
+
return source.baseUrl;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Relative asset key as declared in settings (moc, texture, motion, …). */
|
|
36
|
+
export type AssetKey = string;
|
|
37
|
+
|
|
38
|
+
/** Resolve and fetch assets relative to a model settings URL. */
|
|
39
|
+
export interface AssetResolver {
|
|
40
|
+
readonly baseUrl: string;
|
|
41
|
+
|
|
42
|
+
resolve(key: AssetKey): string;
|
|
43
|
+
|
|
44
|
+
fetchJson(key: AssetKey): Promise<unknown>;
|
|
45
|
+
|
|
46
|
+
fetchBytes(key: AssetKey): Promise<ArrayBuffer>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Session lifecycle (facade state machine). */
|
|
50
|
+
export type SessionPhase =
|
|
51
|
+
| "idle"
|
|
52
|
+
| "mounting"
|
|
53
|
+
| "ready"
|
|
54
|
+
| "loading"
|
|
55
|
+
| "live"
|
|
56
|
+
| "error"
|
|
57
|
+
| "destroyed";
|
|
58
|
+
|
|
59
|
+
export interface SessionState {
|
|
60
|
+
readonly phase: SessionPhase;
|
|
61
|
+
readonly lastError: unknown | null;
|
|
62
|
+
readonly generation: number;
|
|
63
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings JSON format detection (moc2 vs moc3).
|
|
3
|
+
* Shared by loader normalize and renderer backends — keep binary peek in renderer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ModelFormat } from "./model.js";
|
|
7
|
+
|
|
8
|
+
/** Detect moc2 vs moc3 from model settings JSON shape. Returns null if unknown. */
|
|
9
|
+
export function detectModelSettingsFormat(json: unknown): ModelFormat | null {
|
|
10
|
+
if (!json || typeof json !== "object") return null;
|
|
11
|
+
const o = json as Record<string, unknown>;
|
|
12
|
+
const fileRefs = o.FileReferences;
|
|
13
|
+
if (fileRefs && typeof fileRefs === "object") {
|
|
14
|
+
const moc = (fileRefs as Record<string, unknown>).Moc;
|
|
15
|
+
if (typeof moc === "string") {
|
|
16
|
+
const lower = moc.toLowerCase();
|
|
17
|
+
if (lower.endsWith(".moc3") || lower.endsWith(".program.json")) {
|
|
18
|
+
return "moc3";
|
|
19
|
+
}
|
|
20
|
+
if (lower.endsWith(".moc")) {
|
|
21
|
+
return "moc2";
|
|
22
|
+
}
|
|
23
|
+
// Cubism 3 settings without a recognized extension still count as moc3.
|
|
24
|
+
return "moc3";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (typeof o.model === "string" && Array.isArray(o.textures)) {
|
|
28
|
+
return "moc2";
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Load pipeline stage for progress UI. */
|
|
2
|
+
export type LoadProgressStage =
|
|
3
|
+
| "mounting"
|
|
4
|
+
| "resolve"
|
|
5
|
+
| "settings"
|
|
6
|
+
| "moc"
|
|
7
|
+
| "textures"
|
|
8
|
+
| "decode"
|
|
9
|
+
| "ready";
|
|
10
|
+
|
|
11
|
+
export interface LoadProgress {
|
|
12
|
+
readonly stage: LoadProgressStage;
|
|
13
|
+
/** Overall 0..1 */
|
|
14
|
+
readonly progress: number;
|
|
15
|
+
readonly detail?: string;
|
|
16
|
+
readonly bytesLoaded?: number;
|
|
17
|
+
readonly bytesTotal?: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Per-frame timing / mesh cost for Status / profiler UIs. */
|
|
21
|
+
export interface FrameProfile {
|
|
22
|
+
/** Instantaneous FPS from wall dt (0 if first frame). */
|
|
23
|
+
readonly fps: number;
|
|
24
|
+
/** EMA-smoothed FPS for readable UI. */
|
|
25
|
+
readonly fpsSmooth: number;
|
|
26
|
+
/** Whole `update()` wall time in ms. */
|
|
27
|
+
readonly frameMs: number;
|
|
28
|
+
/** `updateModel` + `getDrawables` in ms. */
|
|
29
|
+
readonly evaluateMs: number;
|
|
30
|
+
/** `beginFrame` + `draw` + `endFrame` in ms. */
|
|
31
|
+
readonly drawMs: number;
|
|
32
|
+
readonly drawableCount: number;
|
|
33
|
+
/** Sum of drawable vertex counts (xy pairs). */
|
|
34
|
+
readonly vertexCount: number;
|
|
35
|
+
/** Sum of drawable index counts. */
|
|
36
|
+
readonly indexCount: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Minimal event bus used by sessions and loaders. */
|
|
40
|
+
export type Live2DEventMap = {
|
|
41
|
+
ready: { modelId: string };
|
|
42
|
+
error: { error: unknown };
|
|
43
|
+
progress: LoadProgress;
|
|
44
|
+
/** Fired after each successful `update()` while live. */
|
|
45
|
+
profile: FrameProfile;
|
|
46
|
+
hit: { area: string; x: number; y: number };
|
|
47
|
+
"motion:start": { group: string; index: number; slot: string };
|
|
48
|
+
"motion:finish": { group: string; index: number; slot: string };
|
|
49
|
+
phase: { phase: string; generation: number };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type Live2DEventName = keyof Live2DEventMap;
|
|
53
|
+
|
|
54
|
+
export type Live2DListener<K extends Live2DEventName> = (
|
|
55
|
+
payload: Live2DEventMap[K],
|
|
56
|
+
) => void;
|
|
57
|
+
|
|
58
|
+
export class EventEmitter {
|
|
59
|
+
#listeners = new Map<string, Set<(payload: unknown) => void>>();
|
|
60
|
+
|
|
61
|
+
on<K extends Live2DEventName>(
|
|
62
|
+
event: K,
|
|
63
|
+
listener: Live2DListener<K>,
|
|
64
|
+
): () => void {
|
|
65
|
+
const set = this.#listeners.get(event) ?? new Set();
|
|
66
|
+
set.add(listener as (payload: unknown) => void);
|
|
67
|
+
this.#listeners.set(event, set);
|
|
68
|
+
return () => this.off(event, listener);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
off<K extends Live2DEventName>(
|
|
72
|
+
event: K,
|
|
73
|
+
listener: Live2DListener<K>,
|
|
74
|
+
): void {
|
|
75
|
+
this.#listeners
|
|
76
|
+
.get(event)
|
|
77
|
+
?.delete(listener as (payload: unknown) => void);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
emit<K extends Live2DEventName>(
|
|
81
|
+
event: K,
|
|
82
|
+
payload: Live2DEventMap[K],
|
|
83
|
+
): void {
|
|
84
|
+
for (const listener of this.#listeners.get(event) ?? []) {
|
|
85
|
+
listener(payload);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
clear(): void {
|
|
90
|
+
this.#listeners.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/frame.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU frame output — format-agnostic mesh list ready for any RenderDevice.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Blend modes aligned with renderer DrawableMesh. */
|
|
6
|
+
export const FrameBlendMode = {
|
|
7
|
+
Normal: 0,
|
|
8
|
+
Additive: 1,
|
|
9
|
+
Multiplicative: 2,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type FrameBlendMode =
|
|
13
|
+
(typeof FrameBlendMode)[keyof typeof FrameBlendMode];
|
|
14
|
+
|
|
15
|
+
/** One drawable after CPU deform for a single frame. */
|
|
16
|
+
export interface FrameDrawable {
|
|
17
|
+
readonly index: number;
|
|
18
|
+
readonly textureIndex: number;
|
|
19
|
+
/** Interleaved x,y in model space. */
|
|
20
|
+
readonly positions: Float32Array;
|
|
21
|
+
readonly uvs: Float32Array;
|
|
22
|
+
readonly indices: Uint16Array;
|
|
23
|
+
readonly opacity: number;
|
|
24
|
+
readonly blendMode: FrameBlendMode;
|
|
25
|
+
readonly renderOrder: number;
|
|
26
|
+
readonly visible: boolean;
|
|
27
|
+
readonly invertedMask: boolean;
|
|
28
|
+
readonly maskIndices: readonly number[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Immutable CPU snapshot consumed by GPU backends or golden tests. */
|
|
32
|
+
export interface FrameSnapshot {
|
|
33
|
+
readonly timeSeconds: number;
|
|
34
|
+
readonly drawables: readonly FrameDrawable[];
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@doki-land/live2d-core` — types, events, session contracts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
AssetKey,
|
|
7
|
+
AssetResolver,
|
|
8
|
+
ModelSource,
|
|
9
|
+
SessionPhase,
|
|
10
|
+
SessionState,
|
|
11
|
+
} from "./contracts.js";
|
|
12
|
+
export { modelSourceUrl } from "./contracts.js";
|
|
13
|
+
export { detectModelSettingsFormat } from "./detect-format.js";
|
|
14
|
+
export {
|
|
15
|
+
EventEmitter,
|
|
16
|
+
type FrameProfile,
|
|
17
|
+
type Live2DEventMap,
|
|
18
|
+
type Live2DEventName,
|
|
19
|
+
type Live2DListener,
|
|
20
|
+
type LoadProgress,
|
|
21
|
+
type LoadProgressStage,
|
|
22
|
+
} from "./events.js";
|
|
23
|
+
export {
|
|
24
|
+
FrameBlendMode,
|
|
25
|
+
type FrameDrawable,
|
|
26
|
+
type FrameSnapshot,
|
|
27
|
+
} from "./frame.js";
|
|
28
|
+
export type {
|
|
29
|
+
ExpressionDefinition,
|
|
30
|
+
HitAreaDefinition,
|
|
31
|
+
InternalModel,
|
|
32
|
+
ModelFormat,
|
|
33
|
+
ModelSettings,
|
|
34
|
+
MotionDefinition,
|
|
35
|
+
} from "./model.js";
|
|
36
|
+
export type {
|
|
37
|
+
DrawableProgram,
|
|
38
|
+
ModelInstance,
|
|
39
|
+
ModelProgram,
|
|
40
|
+
ParameterProgram,
|
|
41
|
+
} from "./program.js";
|
|
42
|
+
export {
|
|
43
|
+
createSessionStub,
|
|
44
|
+
type Live2DSession,
|
|
45
|
+
} from "./session.js";
|
|
46
|
+
export type {
|
|
47
|
+
ActorHit,
|
|
48
|
+
ActorTransform,
|
|
49
|
+
CreateActorOptions,
|
|
50
|
+
CreateLive2dStageOptions,
|
|
51
|
+
Live2dActor,
|
|
52
|
+
Live2dStage,
|
|
53
|
+
PointerTrackingMode,
|
|
54
|
+
PointerTrackingPolicy,
|
|
55
|
+
StagePointerEvent,
|
|
56
|
+
StageUpdateMode,
|
|
57
|
+
} from "./stage.js";
|
|
58
|
+
export { DEFAULT_ACTOR_TRANSFORM } from "./stage.js";
|
|
59
|
+
|
|
60
|
+
export const LIVE2D_CORE_VERSION = "0.0.0" as const;
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Model binary family (file extension / settings shape). */
|
|
2
|
+
export type ModelFormat = "moc2" | "moc3";
|
|
3
|
+
|
|
4
|
+
/** Normalized, format-agnostic model settings. */
|
|
5
|
+
export interface ModelSettings {
|
|
6
|
+
format: ModelFormat;
|
|
7
|
+
url: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
moc: string;
|
|
10
|
+
textures: string[];
|
|
11
|
+
motionGroups: Record<string, MotionDefinition[]>;
|
|
12
|
+
expressions: ExpressionDefinition[];
|
|
13
|
+
physics?: string;
|
|
14
|
+
pose?: string;
|
|
15
|
+
hitAreas: HitAreaDefinition[];
|
|
16
|
+
layout?: Record<string, number>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MotionDefinition {
|
|
20
|
+
file: string;
|
|
21
|
+
sound?: string;
|
|
22
|
+
fadeInTime?: number;
|
|
23
|
+
fadeOutTime?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExpressionDefinition {
|
|
27
|
+
name: string;
|
|
28
|
+
file: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HitAreaDefinition {
|
|
32
|
+
name: string;
|
|
33
|
+
id: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Opaque in-memory model handle. */
|
|
37
|
+
export interface InternalModel {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly settings: ModelSettings;
|
|
40
|
+
readonly format: ModelFormat;
|
|
41
|
+
}
|
package/src/program.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU model program / instance shapes (format adapters fill these).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ParameterProgram {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly min: number;
|
|
8
|
+
readonly max: number;
|
|
9
|
+
readonly defaultValue: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DrawableProgram {
|
|
13
|
+
readonly index: number;
|
|
14
|
+
readonly textureIndex: number;
|
|
15
|
+
/** Rest-pose x,y pairs. */
|
|
16
|
+
readonly positions: Float32Array;
|
|
17
|
+
readonly uvs: Float32Array;
|
|
18
|
+
readonly indices: Uint16Array;
|
|
19
|
+
readonly opacity: number;
|
|
20
|
+
readonly renderOrder: number;
|
|
21
|
+
/** {@link FrameBlendMode} value. */
|
|
22
|
+
readonly blendMode: number;
|
|
23
|
+
readonly invertedMask: boolean;
|
|
24
|
+
/** Indices into `ModelProgram.drawables` used as clipping masks. */
|
|
25
|
+
readonly maskIndices: readonly number[];
|
|
26
|
+
readonly visible: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Optional linear deform driven by one parameter.
|
|
29
|
+
* `deltas[i] = positions_at_max[i] - rest[i]`.
|
|
30
|
+
*/
|
|
31
|
+
readonly deformParamIndex: number;
|
|
32
|
+
readonly deformDeltas: Float32Array | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Static topology + deform tables after decode. */
|
|
36
|
+
export interface ModelProgram {
|
|
37
|
+
readonly format: "moc2" | "moc3";
|
|
38
|
+
/** Codec tag, e.g. `cpu-program` or `moc3`. */
|
|
39
|
+
readonly codec: string;
|
|
40
|
+
readonly parameters: readonly ParameterProgram[];
|
|
41
|
+
readonly drawables: readonly DrawableProgram[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Mutable runtime values over a ModelProgram. */
|
|
45
|
+
export interface ModelInstance {
|
|
46
|
+
readonly program: ModelProgram;
|
|
47
|
+
/** Parallel to program.parameters. */
|
|
48
|
+
readonly parameterValues: Float32Array;
|
|
49
|
+
timeSeconds: number;
|
|
50
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AssetResolver, ModelSource, SessionState } from "./contracts.js";
|
|
2
|
+
import { EventEmitter } from "./events.js";
|
|
3
|
+
import type { FrameSnapshot } from "./frame.js";
|
|
4
|
+
import type { InternalModel } from "./model.js";
|
|
5
|
+
|
|
6
|
+
/** High-level runtime session for one mounted surface. */
|
|
7
|
+
export interface Live2DSession {
|
|
8
|
+
readonly events: EventEmitter;
|
|
9
|
+
readonly model: InternalModel | null;
|
|
10
|
+
readonly state: SessionState;
|
|
11
|
+
|
|
12
|
+
mount(canvas: HTMLCanvasElement): void;
|
|
13
|
+
|
|
14
|
+
loadModel(
|
|
15
|
+
source: ModelSource,
|
|
16
|
+
resolver?: AssetResolver,
|
|
17
|
+
): Promise<InternalModel>;
|
|
18
|
+
|
|
19
|
+
/** Latest CPU frame after update, or null if no model. */
|
|
20
|
+
captureFrame(): FrameSnapshot | null;
|
|
21
|
+
|
|
22
|
+
update(deltaTimeSeconds: number): void;
|
|
23
|
+
|
|
24
|
+
hitTest(x: number, y: number): string | null;
|
|
25
|
+
|
|
26
|
+
destroy(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createSessionStub(): Live2DSession {
|
|
30
|
+
let canvas: HTMLCanvasElement | null = null;
|
|
31
|
+
let model: InternalModel | null = null;
|
|
32
|
+
const events = new EventEmitter();
|
|
33
|
+
let generation = 0;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
events,
|
|
37
|
+
get model() {
|
|
38
|
+
return model;
|
|
39
|
+
},
|
|
40
|
+
get state() {
|
|
41
|
+
return {
|
|
42
|
+
phase: model
|
|
43
|
+
? ("live" as const)
|
|
44
|
+
: canvas
|
|
45
|
+
? ("ready" as const)
|
|
46
|
+
: ("idle" as const),
|
|
47
|
+
lastError: null,
|
|
48
|
+
generation,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
mount(target) {
|
|
52
|
+
canvas = target;
|
|
53
|
+
generation += 1;
|
|
54
|
+
},
|
|
55
|
+
async loadModel(_source, _resolver) {
|
|
56
|
+
void canvas;
|
|
57
|
+
void _resolver;
|
|
58
|
+
throw new Error(
|
|
59
|
+
"@doki-land/live2d-core: loadModel is provided by @doki-land/live2d facade",
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
captureFrame() {
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
update(_deltaTimeSeconds) {
|
|
66
|
+
if (!model) return;
|
|
67
|
+
},
|
|
68
|
+
hitTest(_x, _y) {
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
destroy() {
|
|
72
|
+
model = null;
|
|
73
|
+
canvas = null;
|
|
74
|
+
generation += 1;
|
|
75
|
+
events.clear();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/stage.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { InternalModel } from "./model.js";
|
|
2
|
+
|
|
3
|
+
/** Normalized stage placement for one actor (0,0) top-left → (1,1) bottom-right. */
|
|
4
|
+
export interface ActorTransform {
|
|
5
|
+
/** Anchor point X in stage space. */
|
|
6
|
+
x: number;
|
|
7
|
+
/** Anchor point Y in stage space. */
|
|
8
|
+
y: number;
|
|
9
|
+
/** Uniform scale; ignored when `scaleX` / `scaleY` are set. */
|
|
10
|
+
scale?: number;
|
|
11
|
+
scaleX?: number;
|
|
12
|
+
scaleY?: number;
|
|
13
|
+
/** Radians, counter-clockwise. */
|
|
14
|
+
rotation?: number;
|
|
15
|
+
/** Horizontal anchor within the model bounds (0 = left, 1 = right). */
|
|
16
|
+
anchorX?: number;
|
|
17
|
+
/** Vertical anchor within the model bounds (0 = top, 1 = bottom in stage space). */
|
|
18
|
+
anchorY?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_ACTOR_TRANSFORM: Readonly<ActorTransform> = {
|
|
22
|
+
x: 0.5,
|
|
23
|
+
y: 1,
|
|
24
|
+
scale: 1,
|
|
25
|
+
anchorX: 0.5,
|
|
26
|
+
anchorY: 1,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PointerTrackingMode =
|
|
30
|
+
| "all"
|
|
31
|
+
| "focused"
|
|
32
|
+
| "hovered"
|
|
33
|
+
| "nearest"
|
|
34
|
+
| "none"
|
|
35
|
+
| "custom";
|
|
36
|
+
|
|
37
|
+
export interface PointerTrackingPolicy {
|
|
38
|
+
mode: PointerTrackingMode;
|
|
39
|
+
/** Used when `mode` is `custom`. */
|
|
40
|
+
targetActorId?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreateActorOptions {
|
|
44
|
+
id?: string;
|
|
45
|
+
transform?: Partial<ActorTransform>;
|
|
46
|
+
layer?: string;
|
|
47
|
+
order?: number;
|
|
48
|
+
visible?: boolean;
|
|
49
|
+
opacity?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ActorHit {
|
|
53
|
+
readonly actor: Live2dActor;
|
|
54
|
+
readonly actorId: string;
|
|
55
|
+
readonly area: string;
|
|
56
|
+
readonly drawableIndex: number;
|
|
57
|
+
readonly stageX: number;
|
|
58
|
+
readonly stageY: number;
|
|
59
|
+
readonly localX: number;
|
|
60
|
+
readonly localY: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StagePointerEvent {
|
|
64
|
+
readonly actor: Live2dActor | null;
|
|
65
|
+
readonly actorId: string | null;
|
|
66
|
+
readonly area: string | null;
|
|
67
|
+
readonly stageX: number;
|
|
68
|
+
readonly stageY: number;
|
|
69
|
+
readonly clientX: number;
|
|
70
|
+
readonly clientY: number;
|
|
71
|
+
readonly hit: ActorHit | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type StageUpdateMode = "auto" | "manual";
|
|
75
|
+
|
|
76
|
+
export interface CreateLive2dStageOptions {
|
|
77
|
+
updateMode?: StageUpdateMode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read-only actor surface exposed by the stage. */
|
|
81
|
+
export interface Live2dActor {
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly model: InternalModel | null;
|
|
84
|
+
readonly visible: boolean;
|
|
85
|
+
readonly opacity: number;
|
|
86
|
+
readonly layer: string;
|
|
87
|
+
readonly order: number;
|
|
88
|
+
readonly creationIndex: number;
|
|
89
|
+
|
|
90
|
+
getTransform(): ActorTransform;
|
|
91
|
+
|
|
92
|
+
setTransform(patch: Partial<ActorTransform>): void;
|
|
93
|
+
|
|
94
|
+
load(
|
|
95
|
+
source: import("./contracts.js").ModelSource,
|
|
96
|
+
resolver?: import("./contracts.js").AssetResolver,
|
|
97
|
+
): Promise<InternalModel>;
|
|
98
|
+
|
|
99
|
+
setParameter(id: string, value: number): void;
|
|
100
|
+
|
|
101
|
+
lookAt(stageX: number, stageY: number): void;
|
|
102
|
+
|
|
103
|
+
destroy(): void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Multi-character stage owning one canvas surface and shared renderer. */
|
|
107
|
+
export interface Live2dStage {
|
|
108
|
+
readonly actors: readonly Live2dActor[];
|
|
109
|
+
|
|
110
|
+
mount(canvas: HTMLCanvasElement): Promise<void>;
|
|
111
|
+
|
|
112
|
+
createActor(options?: CreateActorOptions): Live2dActor;
|
|
113
|
+
|
|
114
|
+
removeActor(actor: Live2dActor | string): void;
|
|
115
|
+
|
|
116
|
+
defineLayers(layers: readonly string[]): void;
|
|
117
|
+
|
|
118
|
+
update(deltaTimeSeconds: number): void;
|
|
119
|
+
|
|
120
|
+
render(): void;
|
|
121
|
+
|
|
122
|
+
start(): void;
|
|
123
|
+
|
|
124
|
+
pause(): void;
|
|
125
|
+
|
|
126
|
+
resume(): void;
|
|
127
|
+
|
|
128
|
+
stop(): void;
|
|
129
|
+
|
|
130
|
+
hitTest(stageX: number, stageY: number): ActorHit | null;
|
|
131
|
+
|
|
132
|
+
hitTestAll(stageX: number, stageY: number): readonly ActorHit[];
|
|
133
|
+
|
|
134
|
+
addEventListener(
|
|
135
|
+
type: "pointerdown" | "pointermove" | "pointerup",
|
|
136
|
+
listener: (event: StagePointerEvent) => void,
|
|
137
|
+
): void;
|
|
138
|
+
|
|
139
|
+
removeEventListener(
|
|
140
|
+
type: "pointerdown" | "pointermove" | "pointerup",
|
|
141
|
+
listener: (event: StagePointerEvent) => void,
|
|
142
|
+
): void;
|
|
143
|
+
|
|
144
|
+
destroy(): void;
|
|
145
|
+
|
|
146
|
+
pointerTracking: PointerTrackingPolicy;
|
|
147
|
+
}
|