@doki-land/live2d 0.0.22 → 0.0.24
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/dist/index.d.ts +128 -11
- package/dist/index.js +406 -44
- package/package.json +4 -4
- package/src/expression/apply-expression.ts +41 -0
- package/src/expression/index.ts +7 -0
- package/src/expression/parse-expression3.ts +45 -0
- package/src/expression/types.ts +13 -0
- package/src/facade/create-live2d.ts +10 -3
- package/src/index.ts +32 -4
- package/src/physics/apply-physics3.ts +26 -0
- package/src/physics/index.ts +7 -0
- package/src/physics/parse-physics3.ts +52 -0
- package/src/physics/types.ts +20 -0
- package/src/pose/apply-pose3.ts +19 -0
- package/src/pose/index.ts +3 -0
- package/src/pose/parse-pose3.ts +33 -0
- package/src/pose/types.ts +4 -0
- package/src/stage/actor-model-slot.ts +172 -21
- package/src/stage/actor.ts +36 -1
- package/src/stage/hit-area.ts +24 -0
- package/src/stage/index.ts +5 -1
- package/src/stage/model-asset-registry.ts +49 -15
- package/src/stage/single-facade.ts +27 -7
- package/src/stage/stage.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doki-land/live2d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Live2D in the browser — load moc2/moc3 models, Stage + multi-actor, motion; WebGPU/WebGL2/Canvas2D. Main entry for live2d.ts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"test": "vitest run --passWithNoTests"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@doki-land/live2d-core": "0.0.
|
|
63
|
-
"@doki-land/live2d-loader": "0.0.
|
|
64
|
-
"@doki-land/live2d-renderer": "0.0.
|
|
62
|
+
"@doki-land/live2d-core": "0.0.24",
|
|
63
|
+
"@doki-land/live2d-loader": "0.0.24",
|
|
64
|
+
"@doki-land/live2d-renderer": "0.0.24"
|
|
65
65
|
},
|
|
66
66
|
"sideEffects": false
|
|
67
67
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Expression3Clip, ExpressionBlendMode } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface ExpressionApplyBinding {
|
|
4
|
+
readonly value: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function blendValue(
|
|
8
|
+
current: number,
|
|
9
|
+
target: number,
|
|
10
|
+
mode: ExpressionBlendMode,
|
|
11
|
+
weight: number,
|
|
12
|
+
): number {
|
|
13
|
+
if (weight <= 0) return current;
|
|
14
|
+
if (weight >= 1) {
|
|
15
|
+
if (mode === "Add") return current + target;
|
|
16
|
+
if (mode === "Multiply") return current * target;
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
const full =
|
|
20
|
+
mode === "Add"
|
|
21
|
+
? current + target
|
|
22
|
+
: mode === "Multiply"
|
|
23
|
+
? current * target
|
|
24
|
+
: target;
|
|
25
|
+
return current + (full - current) * weight;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Apply expression parameters on top of the current parameter state. */
|
|
29
|
+
export function applyExpression3Clip(
|
|
30
|
+
clip: Expression3Clip,
|
|
31
|
+
weight: number,
|
|
32
|
+
bindings: ReadonlyMap<string, ExpressionApplyBinding>,
|
|
33
|
+
setParameter: (id: string, value: number) => void,
|
|
34
|
+
): void {
|
|
35
|
+
if (weight <= 0) return;
|
|
36
|
+
for (const p of clip.parameters) {
|
|
37
|
+
const binding = bindings.get(p.id);
|
|
38
|
+
if (!binding) continue;
|
|
39
|
+
setParameter(p.id, blendValue(binding.value, p.value, p.blend, weight));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Expression3Clip, ExpressionBlendMode } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const BLENDS = new Set<ExpressionBlendMode>(["Add", "Multiply", "Override"]);
|
|
4
|
+
|
|
5
|
+
function parseBlend(raw: unknown): ExpressionBlendMode {
|
|
6
|
+
if (typeof raw !== "string") return "Add";
|
|
7
|
+
// Cubism official spelling is `Overwrite`; keep `Override` as an alias.
|
|
8
|
+
if (raw === "Overwrite" || raw === "Override") return "Override";
|
|
9
|
+
if (BLENDS.has(raw as ExpressionBlendMode)) {
|
|
10
|
+
return raw as ExpressionBlendMode;
|
|
11
|
+
}
|
|
12
|
+
return "Add";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse Cubism `exp3.json` (FileFormats/exp3.json.md subset). */
|
|
16
|
+
export function parseExpression3(json: unknown): Expression3Clip {
|
|
17
|
+
if (!json || typeof json !== "object") {
|
|
18
|
+
throw new Error("@doki-land/live2d: exp3.json root must be an object");
|
|
19
|
+
}
|
|
20
|
+
const root = json as Record<string, unknown>;
|
|
21
|
+
const version = Number(root.Version ?? 3);
|
|
22
|
+
const paramsRaw = root.Parameters;
|
|
23
|
+
if (!Array.isArray(paramsRaw)) {
|
|
24
|
+
throw new Error("@doki-land/live2d: exp3.json missing Parameters");
|
|
25
|
+
}
|
|
26
|
+
const parameters = paramsRaw.map((item, index) => {
|
|
27
|
+
if (!item || typeof item !== "object") {
|
|
28
|
+
throw new Error(`@doki-land/live2d: Parameters[${index}] invalid`);
|
|
29
|
+
}
|
|
30
|
+
const p = item as Record<string, unknown>;
|
|
31
|
+
const id = p.Id;
|
|
32
|
+
const value = p.Value;
|
|
33
|
+
if (typeof id !== "string" || typeof value !== "number") {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`@doki-land/live2d: Parameters[${index}] needs Id/Value`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
value,
|
|
41
|
+
blend: parseBlend(p.Blend),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
return { version, parameters };
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ExpressionBlendMode = "Add" | "Multiply" | "Override";
|
|
2
|
+
|
|
3
|
+
export interface Expression3Parameter {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly value: number;
|
|
6
|
+
readonly blend: ExpressionBlendMode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Parsed Cubism `exp3.json`. */
|
|
10
|
+
export interface Expression3Clip {
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly parameters: readonly Expression3Parameter[];
|
|
13
|
+
}
|
|
@@ -5,14 +5,18 @@ import {
|
|
|
5
5
|
} from "@doki-land/live2d-renderer";
|
|
6
6
|
import { allocateActorId } from "../stage/actor.js";
|
|
7
7
|
import {
|
|
8
|
-
type
|
|
8
|
+
type CreateLive2dOptions,
|
|
9
9
|
createSingleActorFacade,
|
|
10
|
-
type
|
|
10
|
+
type Live2dRuntime,
|
|
11
11
|
} from "../stage/single-facade.js";
|
|
12
12
|
import { createLive2dStage } from "../stage/stage.js";
|
|
13
13
|
|
|
14
14
|
export type {
|
|
15
|
+
CreateLive2dOptions,
|
|
16
|
+
Live2dRuntime,
|
|
17
|
+
/** @deprecated Use `CreateLive2dOptions`. */
|
|
15
18
|
CreateLive2DOptions,
|
|
19
|
+
/** @deprecated Use `Live2dRuntime`. */
|
|
16
20
|
Live2DRuntime,
|
|
17
21
|
} from "../stage/single-facade.js";
|
|
18
22
|
export {
|
|
@@ -21,7 +25,7 @@ export {
|
|
|
21
25
|
} from "../stage/single-facade.js";
|
|
22
26
|
|
|
23
27
|
/** Wire moc backends and a renderer into one single-actor session. */
|
|
24
|
-
export function
|
|
28
|
+
export function createLive2d(options: CreateLive2dOptions = {}): Live2dRuntime {
|
|
25
29
|
const backends = options.backends ?? [
|
|
26
30
|
createMoc2Backend(),
|
|
27
31
|
createMoc3Backend(),
|
|
@@ -37,3 +41,6 @@ export function createLive2D(options: CreateLive2DOptions = {}): Live2DRuntime {
|
|
|
37
41
|
}) as import("../stage/actor.js").Live2dActorImpl;
|
|
38
42
|
return createSingleActorFacade(stage, actor, backends);
|
|
39
43
|
}
|
|
44
|
+
|
|
45
|
+
/** @deprecated Use `createLive2d`. */
|
|
46
|
+
export const createLive2D = createLive2d;
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* `@doki-land/live2d` — public facade.
|
|
3
3
|
*
|
|
4
4
|
* Layout:
|
|
5
|
-
* - `facade/` — `
|
|
5
|
+
* - `facade/` — `createLive2d()` default stage + actor entry
|
|
6
6
|
* - `motion/` — motion3 parse + playback
|
|
7
7
|
* - `stage/` — multi-actor stage, assets, transforms
|
|
8
8
|
* - `reexports/` — optional subpath `@doki-land/live2d/{core,loader,renderer}`
|
|
9
9
|
*
|
|
10
10
|
* ```ts
|
|
11
|
-
* import {
|
|
11
|
+
* import { createLive2d } from "@doki-land/live2d";
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -22,8 +22,8 @@ export type {
|
|
|
22
22
|
FrameProfile,
|
|
23
23
|
FrameSnapshot,
|
|
24
24
|
InternalModel,
|
|
25
|
-
Live2DSession,
|
|
26
25
|
Live2dActor,
|
|
26
|
+
Live2dSession,
|
|
27
27
|
Live2dStage,
|
|
28
28
|
Live2dStageAssets,
|
|
29
29
|
LoadProgress,
|
|
@@ -67,8 +67,21 @@ export {
|
|
|
67
67
|
serializeCpuProgram,
|
|
68
68
|
} from "@doki-land/live2d-renderer";
|
|
69
69
|
export {
|
|
70
|
-
|
|
70
|
+
applyExpression3Clip,
|
|
71
|
+
type Expression3Clip,
|
|
72
|
+
type Expression3Parameter,
|
|
73
|
+
type ExpressionBlendMode,
|
|
74
|
+
parseExpression3,
|
|
75
|
+
} from "./expression/index.js";
|
|
76
|
+
export {
|
|
77
|
+
type CreateLive2dOptions,
|
|
78
|
+
createLive2d,
|
|
79
|
+
type Live2dRuntime,
|
|
80
|
+
/** @deprecated Use `createLive2d`. */
|
|
71
81
|
createLive2D,
|
|
82
|
+
/** @deprecated Use `CreateLive2dOptions`. */
|
|
83
|
+
type CreateLive2DOptions,
|
|
84
|
+
/** @deprecated Use `Live2dRuntime`. */
|
|
72
85
|
type Live2DRuntime,
|
|
73
86
|
MotionPriority,
|
|
74
87
|
type PlayMotionOptions,
|
|
@@ -82,7 +95,22 @@ export {
|
|
|
82
95
|
MotionPlayer,
|
|
83
96
|
parseMotion3,
|
|
84
97
|
} from "./motion/index.js";
|
|
98
|
+
export {
|
|
99
|
+
applyPhysics3,
|
|
100
|
+
type Physics3ApplyBinding,
|
|
101
|
+
type Physics3Clip,
|
|
102
|
+
type Physics3Output,
|
|
103
|
+
type Physics3Setting,
|
|
104
|
+
parsePhysics3,
|
|
105
|
+
} from "./physics/index.js";
|
|
106
|
+
export {
|
|
107
|
+
applyPose3Activation,
|
|
108
|
+
type Pose3Clip,
|
|
109
|
+
parsePose3,
|
|
110
|
+
} from "./pose/index.js";
|
|
111
|
+
export { allocateActorId } from "./stage/actor.js";
|
|
85
112
|
export { focusParameterUpdates } from "./stage/assets/focus.js";
|
|
113
|
+
export { resolveHitAreaName } from "./stage/hit-area.js";
|
|
86
114
|
export {
|
|
87
115
|
type CreateLive2dStageFullOptions,
|
|
88
116
|
createLive2dStage,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Physics3Clip } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface Physics3ApplyBinding {
|
|
4
|
+
readonly value: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal Physics3 apply gate — **not** a full Cubism spring / pendulum solver.
|
|
9
|
+
*
|
|
10
|
+
* For each output destination parameter id that exists in `bindings`, performs a
|
|
11
|
+
* trivial identity step (write current value back). Exists so load → update can
|
|
12
|
+
* exercise the physics clip API; real physics evaluation is a later milestone.
|
|
13
|
+
*/
|
|
14
|
+
export function applyPhysics3(
|
|
15
|
+
clip: Physics3Clip,
|
|
16
|
+
_deltaTimeSeconds: number,
|
|
17
|
+
bindings: ReadonlyMap<string, Physics3ApplyBinding>,
|
|
18
|
+
setParameter: (id: string, value: number) => void,
|
|
19
|
+
): void {
|
|
20
|
+
for (const id of clip.outputParameterIds) {
|
|
21
|
+
const binding = bindings.get(id);
|
|
22
|
+
if (!binding) continue;
|
|
23
|
+
// Identity / zero-step: prove the output walk without changing values.
|
|
24
|
+
setParameter(id, binding.value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Physics3Clip, Physics3Output, Physics3Setting } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse Cubism `physics3.json` enough to expose PhysicsSettings output parameter ids.
|
|
5
|
+
* Tolerates minimal fixtures (missing / empty PhysicsSettings → empty clip).
|
|
6
|
+
*/
|
|
7
|
+
export function parsePhysics3(json: unknown): Physics3Clip {
|
|
8
|
+
if (!json || typeof json !== "object") {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"@doki-land/live2d: physics3 json root must be an object",
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const root = json as Record<string, unknown>;
|
|
14
|
+
const settingsRaw = root.PhysicsSettings;
|
|
15
|
+
if (settingsRaw === undefined || settingsRaw === null) {
|
|
16
|
+
return { settings: [], outputParameterIds: [] };
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(settingsRaw)) {
|
|
19
|
+
throw new Error("@doki-land/live2d: PhysicsSettings must be an array");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const settings: Physics3Setting[] = [];
|
|
23
|
+
const outputParameterIds: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (let si = 0; si < settingsRaw.length; si++) {
|
|
26
|
+
const entry = settingsRaw[si];
|
|
27
|
+
if (!entry || typeof entry !== "object") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`@doki-land/live2d: PhysicsSettings[${si}] must be an object`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const rec = entry as Record<string, unknown>;
|
|
33
|
+
const id = typeof rec.Id === "string" ? rec.Id : `PhysicsSetting${si}`;
|
|
34
|
+
const outputsRaw = rec.Output;
|
|
35
|
+
const outputs: Physics3Output[] = [];
|
|
36
|
+
if (Array.isArray(outputsRaw)) {
|
|
37
|
+
for (let oi = 0; oi < outputsRaw.length; oi++) {
|
|
38
|
+
const out = outputsRaw[oi];
|
|
39
|
+
if (!out || typeof out !== "object") continue;
|
|
40
|
+
const dest = (out as Record<string, unknown>).Destination;
|
|
41
|
+
if (!dest || typeof dest !== "object") continue;
|
|
42
|
+
const destId = (dest as Record<string, unknown>).Id;
|
|
43
|
+
if (typeof destId !== "string" || !destId) continue;
|
|
44
|
+
outputs.push({ destinationId: destId });
|
|
45
|
+
outputParameterIds.push(destId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
settings.push({ id, outputs });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { settings, outputParameterIds };
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One physics output destination (parameter id only for the thin gate). */
|
|
2
|
+
export interface Physics3Output {
|
|
3
|
+
readonly destinationId: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** One PhysicsSettings entry with output destinations. */
|
|
7
|
+
export interface Physics3Setting {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly outputs: readonly Physics3Output[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parsed Cubism `physics3.json` (minimal).
|
|
14
|
+
* Full spring / pendulum simulation is intentionally out of scope for this gate.
|
|
15
|
+
*/
|
|
16
|
+
export interface Physics3Clip {
|
|
17
|
+
readonly settings: readonly Physics3Setting[];
|
|
18
|
+
/** Flattened destination parameter ids from all outputs. */
|
|
19
|
+
readonly outputParameterIds: readonly string[];
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Pose3Clip } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* When a part in a pose group becomes visible, hide sibling parts in that group.
|
|
5
|
+
* Mirrors Cubism Pose minimum semantics for part-opacity switching.
|
|
6
|
+
*/
|
|
7
|
+
export function applyPose3Activation(
|
|
8
|
+
clip: Pose3Clip,
|
|
9
|
+
activatedPartId: string,
|
|
10
|
+
setPartOpacity: (partId: string, opacity: number) => void,
|
|
11
|
+
): void {
|
|
12
|
+
for (const group of clip.groups) {
|
|
13
|
+
if (!group.includes(activatedPartId)) continue;
|
|
14
|
+
for (const partId of group) {
|
|
15
|
+
setPartOpacity(partId, partId === activatedPartId ? 1 : 0);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Pose3Clip } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Parse Cubism pose file (`pose3.json` or legacy `pose.json`). */
|
|
4
|
+
export function parsePose3(json: unknown): Pose3Clip {
|
|
5
|
+
if (!json || typeof json !== "object") {
|
|
6
|
+
throw new Error("@doki-land/live2d: pose json root must be an object");
|
|
7
|
+
}
|
|
8
|
+
const root = json as Record<string, unknown>;
|
|
9
|
+
const groupsRaw = root.Groups;
|
|
10
|
+
if (!Array.isArray(groupsRaw)) {
|
|
11
|
+
throw new Error("@doki-land/live2d: pose json missing Groups");
|
|
12
|
+
}
|
|
13
|
+
const groups = groupsRaw.map((group, gi) => {
|
|
14
|
+
if (!Array.isArray(group)) {
|
|
15
|
+
throw new Error(`@doki-land/live2d: Groups[${gi}] must be array`);
|
|
16
|
+
}
|
|
17
|
+
return group.map((entry, ei) => {
|
|
18
|
+
if (!entry || typeof entry !== "object") {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`@doki-land/live2d: Groups[${gi}][${ei}] invalid`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const id = (entry as Record<string, unknown>).Id;
|
|
24
|
+
if (typeof id !== "string" || !id) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`@doki-land/live2d: Groups[${gi}][${ei}] missing Id`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return id;
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return { groups };
|
|
33
|
+
}
|