cocos-plugin-live2d 0.0.0 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -1
- package/package.json +40 -3
- package/src/creator2/Live2D.ts +56 -0
- package/src/creator2/api.ts +2 -0
- package/src/creator2/index.ts +2 -0
- package/src/creator2/shims-cc.d.ts +65 -0
- package/src/creator3/Live2D.ts +313 -0
- package/src/creator3/api.ts +2 -0
- package/src/creator3/canvas-bridge.ts +49 -0
- package/src/creator3/index.ts +7 -0
- package/src/creator3/prefer.ts +23 -0
- package/src/creator3/shims-cc.d.ts +112 -0
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
1
|
# cocos-plugin-live2d
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One package, two host folders — session / evaluate / render stay in `@doki-land/live2d`:
|
|
4
|
+
|
|
5
|
+
| Import | Folder | Status |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `cocos-plugin-live2d` or `…/creator3` | `src/creator3` | Creator **3.x** Web runtime |
|
|
8
|
+
| `cocos-plugin-live2d/creator2` | `src/creator2` | Creator **2.4** scaffold (component shell only) |
|
|
9
|
+
|
|
10
|
+
Check which entry resolved:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { COCOS_CREATOR_API, Live2D } from "cocos-plugin-live2d/creator3";
|
|
14
|
+
// → COCOS_CREATOR_API === 3
|
|
15
|
+
|
|
16
|
+
import { COCOS_CREATOR_API, Live2D } from "cocos-plugin-live2d/creator2";
|
|
17
|
+
// → COCOS_CREATOR_API === 2
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Creator 3
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Live2D } from "cocos-plugin-live2d/creator3";
|
|
24
|
+
|
|
25
|
+
const live2d = node.addComponent(Live2D);
|
|
26
|
+
live2d.model = "https://example.com/model/model3.json";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Creator 2 (scaffold)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Live2D } from "cocos-plugin-live2d/creator2";
|
|
33
|
+
|
|
34
|
+
const live2d = this.addComponent(Live2D); // or node.addComponent(Live2D)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`creator2` currently mounts a `cc.Component` shell and logs a warning in `start()` —
|
|
38
|
+
no offscreen canvas / `@doki-land/live2d` wiring yet. Use it to verify the project
|
|
39
|
+
resolves this package and the `/creator2` export.
|
|
40
|
+
|
|
41
|
+
Native Android / iOS are out of this slice. Tips / chrome stay in `@doki-land/live2d-widget`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cocos-plugin-live2d",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.13",
|
|
4
|
+
"description": "Cocos Creator Live2D adaptor for @doki-land/live2d — one package, creator3 / creator2 entries",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
+
"author": "Doki Land",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"live2d",
|
|
10
|
+
"cocos",
|
|
11
|
+
"cocos-creator",
|
|
12
|
+
"cocos-creator-2",
|
|
13
|
+
"cocos-creator-3",
|
|
14
|
+
"plugin",
|
|
15
|
+
"doki-land"
|
|
16
|
+
],
|
|
17
|
+
"main": "./src/index.ts",
|
|
18
|
+
"types": "./src/index.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.ts",
|
|
21
|
+
"./creator3": "./src/creator3/index.ts",
|
|
22
|
+
"./creator2": "./src/creator2/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"test": "vitest run"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@doki-land/live2d": "0.0.13"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/doki-land/live2d.ts.git"
|
|
43
|
+
}
|
|
7
44
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { COCOS_CREATOR_API } from "./api.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creator 2.4 entry — scaffold only.
|
|
5
|
+
*
|
|
6
|
+
* Same package / property surface as creator3 so you can verify both hosts
|
|
7
|
+
* resolve `cocos-plugin-live2d/creator2`. Runtime bridge lands later.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { COCOS_CREATOR_API };
|
|
11
|
+
|
|
12
|
+
const { ccclass, property } = cc._decorator;
|
|
13
|
+
|
|
14
|
+
@ccclass("Live2D")
|
|
15
|
+
export class Live2D extends cc.Component {
|
|
16
|
+
@property
|
|
17
|
+
model = "";
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
width = 320;
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
height = 320;
|
|
24
|
+
|
|
25
|
+
/** Comma-separated prefer list in the editor; parsed at runtime later. */
|
|
26
|
+
@property
|
|
27
|
+
prefer = "canvas2d,webgl2,webgpu";
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
autoplay = true;
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
autoSway = true;
|
|
34
|
+
|
|
35
|
+
onLoad(): void {
|
|
36
|
+
if (!this.getComponent(cc.Sprite)) {
|
|
37
|
+
this.addComponent(cc.Sprite);
|
|
38
|
+
}
|
|
39
|
+
this.node.setContentSize(this.width, this.height);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
start(): void {
|
|
43
|
+
console.warn(
|
|
44
|
+
"[cocos-plugin-live2d/creator2] scaffold only — Live2D runtime not wired yet. API=",
|
|
45
|
+
COCOS_CREATOR_API,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
update(_dt: number): void {
|
|
50
|
+
// runtime later
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onDestroy(): void {
|
|
54
|
+
// runtime later
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Creator 2.4 ambient `cc` for typechecking this entry.
|
|
3
|
+
* Real Creator 2 projects use the engine's creator.d.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare namespace cc {
|
|
7
|
+
type Constructor<T = unknown> = new (...args: never[]) => T;
|
|
8
|
+
|
|
9
|
+
class Vec2 {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
constructor(x?: number, y?: number);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class Node {
|
|
16
|
+
static EventType: {
|
|
17
|
+
TOUCH_START: string;
|
|
18
|
+
TOUCH_MOVE: string;
|
|
19
|
+
TOUCH_END: string;
|
|
20
|
+
TOUCH_CANCEL: string;
|
|
21
|
+
};
|
|
22
|
+
name: string;
|
|
23
|
+
on(
|
|
24
|
+
type: string,
|
|
25
|
+
callback: (...args: never[]) => void,
|
|
26
|
+
target?: unknown,
|
|
27
|
+
): void;
|
|
28
|
+
off(
|
|
29
|
+
type: string,
|
|
30
|
+
callback: (...args: never[]) => void,
|
|
31
|
+
target?: unknown,
|
|
32
|
+
): void;
|
|
33
|
+
getComponent<T>(type: Constructor<T> | string): T | null;
|
|
34
|
+
addComponent<T>(type: Constructor<T> | string): T;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
setContentSize(width: number, height: number): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class Component {
|
|
41
|
+
node: Node;
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
getComponent<T>(type: Constructor<T> | string): T | null;
|
|
44
|
+
addComponent<T>(type: Constructor<T> | string): T;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class Sprite extends Component {
|
|
48
|
+
spriteFrame: SpriteFrame | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class SpriteFrame {
|
|
52
|
+
constructor();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class Texture2D {
|
|
56
|
+
initWithElement(element: HTMLCanvasElement | HTMLImageElement): void;
|
|
57
|
+
handleLoadedTexture(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const _decorator: {
|
|
61
|
+
ccclass: (name?: string) => ClassDecorator;
|
|
62
|
+
property: ((options?: Record<string, unknown>) => PropertyDecorator) &
|
|
63
|
+
PropertyDecorator;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLive2D as createLive2DRuntime,
|
|
3
|
+
createRenderer,
|
|
4
|
+
focusParameterUpdates,
|
|
5
|
+
type Live2DRuntime,
|
|
6
|
+
type RendererKind,
|
|
7
|
+
} from "@doki-land/live2d";
|
|
8
|
+
import {
|
|
9
|
+
_decorator,
|
|
10
|
+
Component,
|
|
11
|
+
type EventTouch,
|
|
12
|
+
Node,
|
|
13
|
+
PixelFormat,
|
|
14
|
+
type Size,
|
|
15
|
+
Sprite,
|
|
16
|
+
SpriteFrame,
|
|
17
|
+
Texture2D,
|
|
18
|
+
UITransform,
|
|
19
|
+
Vec2,
|
|
20
|
+
Vec3,
|
|
21
|
+
} from "cc";
|
|
22
|
+
import { readCanvasRgba } from "./canvas-bridge.js";
|
|
23
|
+
import { DEFAULT_COCOS_PREFER, normalizePrefer } from "./prefer.js";
|
|
24
|
+
|
|
25
|
+
const { ccclass, property } = _decorator;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cocos Creator 3.x (Web) Live2D component.
|
|
29
|
+
*
|
|
30
|
+
* Draws via `@doki-land/live2d` into an offscreen canvas, then uploads pixels
|
|
31
|
+
* to a Sprite on the same node each frame. Native Android/iOS is out of scope.
|
|
32
|
+
*/
|
|
33
|
+
@ccclass("Live2D")
|
|
34
|
+
export class Live2D extends Component {
|
|
35
|
+
/** Model settings URL, site path, or `npm:pkg[@ver]/path`. */
|
|
36
|
+
@property
|
|
37
|
+
model = "";
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
width = 320;
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
height = 320;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Renderer try order. Default prefers Canvas2D for Texture2D readback.
|
|
47
|
+
* Editor may leave this empty → {@link DEFAULT_COCOS_PREFER}.
|
|
48
|
+
*/
|
|
49
|
+
@property({ type: [String] })
|
|
50
|
+
prefer: string[] = [...DEFAULT_COCOS_PREFER];
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
autoplay = true;
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
autoSway = true;
|
|
57
|
+
|
|
58
|
+
/** Optional touch hit callback (drawable id / area). */
|
|
59
|
+
onHit: ((payload: { area: string; x: number; y: number }) => void) | null =
|
|
60
|
+
null;
|
|
61
|
+
|
|
62
|
+
#canvas: HTMLCanvasElement | null = null;
|
|
63
|
+
#scratch: HTMLCanvasElement | null = null;
|
|
64
|
+
#runtime: Live2DRuntime | null = null;
|
|
65
|
+
#texture: Texture2D | null = null;
|
|
66
|
+
#spriteFrame: SpriteFrame | null = null;
|
|
67
|
+
#sprite: Sprite | null = null;
|
|
68
|
+
#mountGeneration = 0;
|
|
69
|
+
#elapsed = 0;
|
|
70
|
+
#autoSwayPausedByPointer = false;
|
|
71
|
+
#uiLoc = new Vec2();
|
|
72
|
+
#world = new Vec3();
|
|
73
|
+
#local = new Vec3();
|
|
74
|
+
|
|
75
|
+
onLoad(): void {
|
|
76
|
+
this.#sprite = this.getComponent(Sprite) ?? this.addComponent(Sprite);
|
|
77
|
+
const ui =
|
|
78
|
+
this.getComponent(UITransform) ?? this.addComponent(UITransform);
|
|
79
|
+
ui.setContentSize(this.width, this.height);
|
|
80
|
+
|
|
81
|
+
this.node.on(Node.EventType.TOUCH_MOVE, this.#onTouchMove, this);
|
|
82
|
+
this.node.on(Node.EventType.TOUCH_END, this.#onTouchEnd, this);
|
|
83
|
+
this.node.on(Node.EventType.TOUCH_CANCEL, this.#onTouchEnd, this);
|
|
84
|
+
this.node.on(Node.EventType.TOUCH_START, this.#onTouchStart, this);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
start(): void {
|
|
88
|
+
void this.#remount();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
update(dt: number): void {
|
|
92
|
+
const runtime = this.#runtime;
|
|
93
|
+
if (!runtime || !this.autoplay) return;
|
|
94
|
+
|
|
95
|
+
this.#elapsed += dt;
|
|
96
|
+
if (this.autoSway && !this.#autoSwayPausedByPointer) {
|
|
97
|
+
runtime.setParameter(
|
|
98
|
+
"PARAM_ANGLE_X",
|
|
99
|
+
this.#parameterFromNormalized(
|
|
100
|
+
"PARAM_ANGLE_X",
|
|
101
|
+
Math.sin(this.#elapsed) * 0.25,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (this.#autoSwayPausedByPointer) {
|
|
106
|
+
this.#autoSwayPausedByPointer = false;
|
|
107
|
+
}
|
|
108
|
+
runtime.update(dt);
|
|
109
|
+
this.#syncTexture();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onDestroy(): void {
|
|
113
|
+
this.#mountGeneration += 1;
|
|
114
|
+
this.node.off(Node.EventType.TOUCH_MOVE, this.#onTouchMove, this);
|
|
115
|
+
this.node.off(Node.EventType.TOUCH_END, this.#onTouchEnd, this);
|
|
116
|
+
this.node.off(Node.EventType.TOUCH_CANCEL, this.#onTouchEnd, this);
|
|
117
|
+
this.node.off(Node.EventType.TOUCH_START, this.#onTouchStart, this);
|
|
118
|
+
this.#teardownRuntime();
|
|
119
|
+
this.#texture?.destroy();
|
|
120
|
+
this.#texture = null;
|
|
121
|
+
this.#spriteFrame = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getRuntime(): Live2DRuntime | null {
|
|
125
|
+
return this.#runtime;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setParameter(id: string, value: number): void {
|
|
129
|
+
this.#runtime?.setParameter(id, value);
|
|
130
|
+
if (!this.autoplay) {
|
|
131
|
+
this.#runtime?.update(0);
|
|
132
|
+
this.#syncTexture();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
listParameters() {
|
|
137
|
+
return this.#runtime?.listParameters() ?? [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async reload(): Promise<void> {
|
|
141
|
+
if (!this.#runtime || !this.model) {
|
|
142
|
+
await this.#remount();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const gen = this.#mountGeneration;
|
|
146
|
+
try {
|
|
147
|
+
await this.#runtime.loadModel(this.model);
|
|
148
|
+
if (gen !== this.#mountGeneration) return;
|
|
149
|
+
if (!this.autoplay) {
|
|
150
|
+
this.#runtime.update(0);
|
|
151
|
+
this.#syncTexture();
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// error emitted via runtime events
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async #remount(): Promise<void> {
|
|
159
|
+
const gen = ++this.#mountGeneration;
|
|
160
|
+
this.#teardownRuntime();
|
|
161
|
+
|
|
162
|
+
if (typeof document === "undefined") {
|
|
163
|
+
console.warn(
|
|
164
|
+
"cocos-plugin-live2d: Live2D requires Creator Web (DOM canvas).",
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const canvas = document.createElement("canvas");
|
|
170
|
+
canvas.width = Math.max(1, Math.round(this.width));
|
|
171
|
+
canvas.height = Math.max(1, Math.round(this.height));
|
|
172
|
+
this.#canvas = canvas;
|
|
173
|
+
this.#scratch = document.createElement("canvas");
|
|
174
|
+
|
|
175
|
+
const prefer = normalizePrefer(this.prefer);
|
|
176
|
+
const runtime = createLive2DRuntime({
|
|
177
|
+
renderer: createRenderer({ prefer }),
|
|
178
|
+
});
|
|
179
|
+
this.#runtime = runtime;
|
|
180
|
+
runtime.mount(canvas);
|
|
181
|
+
|
|
182
|
+
const ui = this.getComponent(UITransform);
|
|
183
|
+
ui?.setContentSize(this.width, this.height);
|
|
184
|
+
|
|
185
|
+
if (this.model) {
|
|
186
|
+
try {
|
|
187
|
+
await runtime.loadModel(this.model);
|
|
188
|
+
if (gen !== this.#mountGeneration) return;
|
|
189
|
+
if (!this.autoplay) {
|
|
190
|
+
runtime.update(0);
|
|
191
|
+
}
|
|
192
|
+
this.#syncTexture();
|
|
193
|
+
} catch {
|
|
194
|
+
// error emitted via runtime events
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#teardownRuntime(): void {
|
|
200
|
+
this.#runtime?.destroy();
|
|
201
|
+
this.#runtime = null;
|
|
202
|
+
this.#canvas = null;
|
|
203
|
+
this.#scratch = null;
|
|
204
|
+
this.#elapsed = 0;
|
|
205
|
+
this.#autoSwayPausedByPointer = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#syncTexture(): void {
|
|
209
|
+
const canvas = this.#canvas;
|
|
210
|
+
const sprite = this.#sprite;
|
|
211
|
+
if (!canvas || !sprite) return;
|
|
212
|
+
|
|
213
|
+
const pixels = readCanvasRgba(canvas, this.#scratch);
|
|
214
|
+
if (!pixels) {
|
|
215
|
+
// Last resort: let Cocos ingest the canvas directly when possible.
|
|
216
|
+
this.#ensureSpriteFrame(canvas.width, canvas.height);
|
|
217
|
+
try {
|
|
218
|
+
this.#texture?.uploadData(canvas);
|
|
219
|
+
} catch {
|
|
220
|
+
// ignore upload failure
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.#ensureSpriteFrame(pixels.width, pixels.height);
|
|
226
|
+
this.#texture?.uploadData(pixels.data);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#ensureSpriteFrame(width: number, height: number): void {
|
|
230
|
+
if (!this.#texture) {
|
|
231
|
+
this.#texture = new Texture2D();
|
|
232
|
+
}
|
|
233
|
+
this.#texture.reset({
|
|
234
|
+
width,
|
|
235
|
+
height,
|
|
236
|
+
format: PixelFormat.RGBA8888,
|
|
237
|
+
});
|
|
238
|
+
if (!this.#spriteFrame) {
|
|
239
|
+
this.#spriteFrame = new SpriteFrame();
|
|
240
|
+
}
|
|
241
|
+
this.#spriteFrame.texture = this.#texture;
|
|
242
|
+
if (this.#sprite && this.#sprite.spriteFrame !== this.#spriteFrame) {
|
|
243
|
+
this.#sprite.spriteFrame = this.#spriteFrame;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#parameterFromNormalized(id: string, normalized: number): number {
|
|
248
|
+
const binding = this.#runtime
|
|
249
|
+
?.listParameters()
|
|
250
|
+
.find((p) => p.id === id);
|
|
251
|
+
if (!binding) return normalized;
|
|
252
|
+
return normalized >= 0
|
|
253
|
+
? binding.defaultValue +
|
|
254
|
+
(binding.max - binding.defaultValue) * normalized
|
|
255
|
+
: binding.defaultValue +
|
|
256
|
+
(binding.defaultValue - binding.min) * normalized;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#touchToModel(event: EventTouch): { x: number; y: number } | null {
|
|
260
|
+
const ui = this.getComponent(UITransform);
|
|
261
|
+
if (!ui) return null;
|
|
262
|
+
const loc = event.getUILocation(this.#uiLoc);
|
|
263
|
+
this.#world.set(loc.x, loc.y, 0);
|
|
264
|
+
ui.convertToNodeSpaceAR(this.#world, this.#local);
|
|
265
|
+
const size: Size = ui.contentSize;
|
|
266
|
+
const hw = size.width * 0.5;
|
|
267
|
+
const hh = size.height * 0.5;
|
|
268
|
+
if (hw <= 0 || hh <= 0) return null;
|
|
269
|
+
return {
|
|
270
|
+
x: this.#local.x / hw,
|
|
271
|
+
y: this.#local.y / hh,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#applyFocus(x: number, y: number): void {
|
|
276
|
+
const runtime = this.#runtime;
|
|
277
|
+
if (!runtime) return;
|
|
278
|
+
this.#autoSwayPausedByPointer = true;
|
|
279
|
+
for (const { id, value } of focusParameterUpdates(
|
|
280
|
+
runtime.listParameters(),
|
|
281
|
+
x,
|
|
282
|
+
y,
|
|
283
|
+
)) {
|
|
284
|
+
runtime.setParameter(id, value);
|
|
285
|
+
}
|
|
286
|
+
if (!this.autoplay) {
|
|
287
|
+
runtime.update(0);
|
|
288
|
+
this.#syncTexture();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#onTouchMove = (event: EventTouch): void => {
|
|
293
|
+
const p = this.#touchToModel(event);
|
|
294
|
+
if (!p) return;
|
|
295
|
+
this.#applyFocus(p.x, p.y);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
#onTouchStart = (event: EventTouch): void => {
|
|
299
|
+
const p = this.#touchToModel(event);
|
|
300
|
+
if (!p) return;
|
|
301
|
+
this.#applyFocus(p.x, p.y);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
#onTouchEnd = (event: EventTouch): void => {
|
|
305
|
+
const p = this.#touchToModel(event);
|
|
306
|
+
const runtime = this.#runtime;
|
|
307
|
+
if (!p || !runtime) return;
|
|
308
|
+
const area = runtime.hitTest(p.x, p.y);
|
|
309
|
+
if (area) this.onHit?.({ area, x: p.x, y: p.y });
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export type { RendererKind };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface CanvasRgbaPixels {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
readonly data: Uint8ClampedArray;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read RGBA pixels from a canvas for Cocos Texture2D.uploadData.
|
|
9
|
+
* Prefers a 2D context on the source; otherwise blits through a scratch 2D canvas
|
|
10
|
+
* (needed when the Live2D backend used WebGL2 / WebGPU).
|
|
11
|
+
*/
|
|
12
|
+
export function readCanvasRgba(
|
|
13
|
+
source: HTMLCanvasElement,
|
|
14
|
+
scratch?: HTMLCanvasElement | null,
|
|
15
|
+
): CanvasRgbaPixels | null {
|
|
16
|
+
const width = source.width | 0;
|
|
17
|
+
const height = source.height | 0;
|
|
18
|
+
if (width <= 0 || height <= 0) return null;
|
|
19
|
+
|
|
20
|
+
const direct = source.getContext("2d");
|
|
21
|
+
if (direct) {
|
|
22
|
+
try {
|
|
23
|
+
const img = direct.getImageData(0, 0, width, height);
|
|
24
|
+
return { width, height, data: img.data };
|
|
25
|
+
} catch {
|
|
26
|
+
// tainted or lost context — fall through
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const blit =
|
|
31
|
+
scratch && scratch.width >= 0
|
|
32
|
+
? scratch
|
|
33
|
+
: typeof document !== "undefined"
|
|
34
|
+
? document.createElement("canvas")
|
|
35
|
+
: null;
|
|
36
|
+
if (!blit) return null;
|
|
37
|
+
if (blit.width !== width) blit.width = width;
|
|
38
|
+
if (blit.height !== height) blit.height = height;
|
|
39
|
+
const ctx = blit.getContext("2d");
|
|
40
|
+
if (!ctx) return null;
|
|
41
|
+
try {
|
|
42
|
+
ctx.clearRect(0, 0, width, height);
|
|
43
|
+
ctx.drawImage(source, 0, 0);
|
|
44
|
+
const img = ctx.getImageData(0, 0, width, height);
|
|
45
|
+
return { width, height, data: img.data };
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RendererKind } from "@doki-land/live2d";
|
|
2
|
+
|
|
3
|
+
const ALLOWED = new Set<RendererKind>(["webgpu", "webgl2", "canvas2d"]);
|
|
4
|
+
|
|
5
|
+
/** Default prefer order for Cocos Web: Canvas2D first so pixels can be read back. */
|
|
6
|
+
export const DEFAULT_COCOS_PREFER: readonly RendererKind[] = [
|
|
7
|
+
"canvas2d",
|
|
8
|
+
"webgl2",
|
|
9
|
+
"webgpu",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keep only known renderer kinds. Empty / invalid input → default Cocos order.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizePrefer(
|
|
16
|
+
prefer: readonly string[] | undefined | null,
|
|
17
|
+
): RendererKind[] {
|
|
18
|
+
if (!prefer?.length) return [...DEFAULT_COCOS_PREFER];
|
|
19
|
+
const out = prefer.filter((k): k is RendererKind =>
|
|
20
|
+
ALLOWED.has(k as RendererKind),
|
|
21
|
+
);
|
|
22
|
+
return out.length ? out : [...DEFAULT_COCOS_PREFER];
|
|
23
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Cocos Creator 3.x ambient types for typechecking this adaptor
|
|
3
|
+
* without vendoring the full engine. Creator projects use the real `cc`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module "cc" {
|
|
7
|
+
export type Constructor<T = unknown> = new (...args: never[]) => T;
|
|
8
|
+
|
|
9
|
+
export class Vec2 {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
constructor(x?: number, y?: number);
|
|
13
|
+
set(x: number, y: number): this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Vec3 {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
z: number;
|
|
20
|
+
constructor(x?: number, y?: number, z?: number);
|
|
21
|
+
set(x: number, y: number, z?: number): this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Size {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
constructor(width?: number, height?: number);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Node {
|
|
31
|
+
static EventType: {
|
|
32
|
+
TOUCH_START: string;
|
|
33
|
+
TOUCH_MOVE: string;
|
|
34
|
+
TOUCH_END: string;
|
|
35
|
+
TOUCH_CANCEL: string;
|
|
36
|
+
};
|
|
37
|
+
name: string;
|
|
38
|
+
on(
|
|
39
|
+
type: string,
|
|
40
|
+
callback: (...args: never[]) => void,
|
|
41
|
+
target?: unknown,
|
|
42
|
+
): void;
|
|
43
|
+
off(
|
|
44
|
+
type: string,
|
|
45
|
+
callback: (...args: never[]) => void,
|
|
46
|
+
target?: unknown,
|
|
47
|
+
): void;
|
|
48
|
+
getComponent<T>(type: Constructor<T>): T | null;
|
|
49
|
+
addComponent<T>(type: Constructor<T>): T;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class Component {
|
|
53
|
+
node: Node;
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
getComponent<T>(type: Constructor<T>): T | null;
|
|
56
|
+
addComponent<T>(type: Constructor<T>): T;
|
|
57
|
+
scheduleOnce(callback: () => void, delay?: number): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class UITransform extends Component {
|
|
61
|
+
contentSize: Size;
|
|
62
|
+
setContentSize(width: number, height: number): void;
|
|
63
|
+
convertToNodeSpaceAR(worldPoint: Vec3, out?: Vec3): Vec3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class ImageAsset {
|
|
67
|
+
constructor(data?: HTMLCanvasElement | HTMLImageElement | ImageBitmap);
|
|
68
|
+
reset(data: HTMLCanvasElement | HTMLImageElement | ImageBitmap): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export enum PixelFormat {
|
|
72
|
+
RGBA8888 = 35,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class Texture2D {
|
|
76
|
+
reset(info: {
|
|
77
|
+
width: number;
|
|
78
|
+
height: number;
|
|
79
|
+
format?: PixelFormat;
|
|
80
|
+
}): void;
|
|
81
|
+
uploadData(
|
|
82
|
+
source:
|
|
83
|
+
| HTMLCanvasElement
|
|
84
|
+
| HTMLImageElement
|
|
85
|
+
| ArrayBufferView
|
|
86
|
+
| ImageBitmap,
|
|
87
|
+
level?: number,
|
|
88
|
+
arrayIndex?: number,
|
|
89
|
+
): void;
|
|
90
|
+
destroy(): void;
|
|
91
|
+
image: ImageAsset | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class SpriteFrame {
|
|
95
|
+
texture: Texture2D | null;
|
|
96
|
+
constructor();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class Sprite extends Component {
|
|
100
|
+
spriteFrame: SpriteFrame | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class EventTouch {
|
|
104
|
+
getUILocation(out?: Vec2): Vec2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const _decorator: {
|
|
108
|
+
ccclass: (name?: string) => ClassDecorator;
|
|
109
|
+
property: ((options?: Record<string, unknown>) => PropertyDecorator) &
|
|
110
|
+
PropertyDecorator;
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/index.ts
ADDED