@lemondouble/featheranime2d 0.0.0-stage → 0.1.0-preview.2
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 +79 -2
- package/dist/index.d.ts +83 -0
- package/dist/index.js +92 -0
- package/dist/pixi.d.ts +23 -0
- package/dist/pixi.js +92 -0
- package/dist/react.d.ts +12 -0
- package/dist/react.js +57 -0
- package/package.json +52 -4
- package/wasm/featheranime2d_wasm.d.ts +84 -0
- package/wasm/featheranime2d_wasm.js +435 -0
- package/wasm/featheranime2d_wasm_bg.wasm +0 -0
- package/wasm/featheranime2d_wasm_bg.wasm.d.ts +29 -0
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lemondouble/featheranime2d
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
그림 한 장으로 만드는, 가벼운 움직이는 애니메이션 캐릭터 렌더러입니다. 캐릭터 패키지(`.fa2d`)를 불러와 표정, 모션, 숨쉬기, 깜빡임, 머리카락 흔들림을 계산하고 PixiJS로 그립니다. 계산은 Rust로 만든 Core를 WASM으로 돌립니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @lemondouble/featheranime2d pixi.js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `pixi.js`(8.x)는 `/pixi`, `/react`를 쓸 때 필요합니다. `react`(19.x)는 `/react`를 쓸 때 필요합니다.
|
|
12
|
+
- WASM 파일은 번들러(Vite, Next.js 등)가 `new URL(..., import.meta.url)`을 보고 빌드 결과에 함께 넣습니다. 따로 설정할 것은 없습니다.
|
|
13
|
+
|
|
14
|
+
## React
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
"use client";
|
|
18
|
+
|
|
19
|
+
import { useFeatherAnime2D } from "@lemondouble/featheranime2d/react";
|
|
20
|
+
import { useEffect } from "react";
|
|
21
|
+
|
|
22
|
+
export default function CharacterStage({ expression }: { expression?: string }) {
|
|
23
|
+
const { containerRef, view, error } = useFeatherAnime2D("/characters/witch.fa2d");
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
view?.character.setExpression(expression ?? null);
|
|
27
|
+
}, [view, expression]);
|
|
28
|
+
|
|
29
|
+
return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `containerRef`를 붙인 요소의 크기에 맞춰, 비율을 유지하며 가운데에 그립니다. 배경은 투명합니다.
|
|
34
|
+
- PixiJS와 WASM은 effect 안에서 불러오므로, Next.js에서 서버 렌더링을 빼는 처리(`dynamic(..., { ssr: false })`) 없이 서버 컴포넌트에서 바로 쓸 수 있습니다.
|
|
35
|
+
- 표정처럼 상태인 것은 위처럼 `useEffect`로 넣고, 모션처럼 한 번 실행하는 것은 `view.character.playMotion("wave", "normal")`을 직접 부릅니다.
|
|
36
|
+
|
|
37
|
+
## PixiJS
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { CharacterView } from "@lemondouble/featheranime2d/pixi";
|
|
41
|
+
|
|
42
|
+
const view = await CharacterView.load("/characters/witch.fa2d");
|
|
43
|
+
app.stage.addChild(view);
|
|
44
|
+
app.ticker.add((ticker) => view.update(ticker.deltaMS / 1000));
|
|
45
|
+
view.character.setExpression("happy");
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- 캔버스 픽셀 좌표로 그리므로 화면 크기에 맞추는 일(`scale`, `position`)은 앱이 합니다. 캔버스 크기는 `view.info.canvas`에 있습니다.
|
|
49
|
+
- `info.backdrop`이 색이면 앱이 화면을 그 색으로 칠합니다. 그림 배경은 `CharacterView`가 함께 그립니다.
|
|
50
|
+
- 다 쓰면 `view.destroy()`로 텍스처와 WASM 메모리를 해제합니다.
|
|
51
|
+
|
|
52
|
+
## Core만 쓰기
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { Character } from "@lemondouble/featheranime2d";
|
|
56
|
+
|
|
57
|
+
const character = await Character.load(bytes); // .fa2d 파일의 바이트
|
|
58
|
+
character.update(1 / 60);
|
|
59
|
+
character.vertices; // 모든 slot의 정점 (캔버스 픽셀)
|
|
60
|
+
character.slotVisible; // slot마다 보이는지
|
|
61
|
+
character.slotTextures; // slot마다 그릴 텍스처 번호 (info.textures 순서)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- 다른 그래픽 라이브러리로 그릴 때 씁니다. slot의 정점 수, uv, 삼각형은 `character.info.slots`에 있습니다.
|
|
65
|
+
- `vertices` 같은 버퍼는 WASM 메모리를 복사 없이 보는 뷰라서, 다음 `update` 전까지만 유효합니다.
|
|
66
|
+
- 시드(`{ seed }`)를 빼면 무작위 시드를 씁니다. 같은 시드의 캐릭터들은 같은 순간에 깜빡입니다.
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
| 이름 | 설명 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `setExpression(name \| null)` | 표정을 바꿉니다. 즉시 바뀝니다 |
|
|
73
|
+
| `playMotion(name, "idle" \| "normal" \| "force")` | 모션을 재생합니다. 재생 중인 모션보다 우선순위가 높지 않으면 버리고 `false`를 돌려줍니다 |
|
|
74
|
+
| `currentExpression`, `currentMotion` | 지금 표정과 재생 중인 모션입니다. 모션이 끝나면 `null`입니다 |
|
|
75
|
+
| `setInput(parameter, value)` | 얼굴 추적, 마우스 시선 같은 외부 입력입니다. 기본값에서 옮긴 만큼 더합니다 |
|
|
76
|
+
| `setLipSync(mouthOpen, speaking)` | 실제 음성으로 계산한 입 벌림 값입니다 |
|
|
77
|
+
| `setTalkSimulation(enabled, syllablesPerSecond, loudness)` | 음성 없이 말하는 흉내를 냅니다. 켜져 있으면 `setLipSync` 대신 씁니다 |
|
|
78
|
+
| `setDriverEnabled(driver, enabled)` | 모션, 깜빡임, 표정, 외부 입력, 숨쉬기, 물리, 립싱크를 하나씩 켜고 끕니다 |
|
|
79
|
+
|
|
80
|
+
캐릭터 패키지를 만드는 방법과 형식은 [FeatherAnime2D 레포](https://github.com/FeatherAnime2D/featheranime2d)에 있습니다.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type MotionPriority = "idle" | "normal" | "force";
|
|
2
|
+
export type DriverName = "motion" | "blink" | "expression" | "input" | "breath" | "physics" | "lipSync";
|
|
3
|
+
/** 처음에 한 번 읽는 정보입니다. Rust의 info.rs와 같은 모양입니다. */
|
|
4
|
+
export interface CharacterInfo {
|
|
5
|
+
canvas: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
backdrop: null | {
|
|
10
|
+
type: "color";
|
|
11
|
+
color: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "image";
|
|
14
|
+
texture: number;
|
|
15
|
+
offset: [number, number];
|
|
16
|
+
};
|
|
17
|
+
textures: {
|
|
18
|
+
path: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}[];
|
|
22
|
+
slots: {
|
|
23
|
+
id: string;
|
|
24
|
+
vertexCount: number;
|
|
25
|
+
firstVertex: number;
|
|
26
|
+
uvs: number[];
|
|
27
|
+
indices: number[];
|
|
28
|
+
}[];
|
|
29
|
+
parameters: {
|
|
30
|
+
name: string;
|
|
31
|
+
min: number;
|
|
32
|
+
max: number;
|
|
33
|
+
default: number;
|
|
34
|
+
}[];
|
|
35
|
+
inputs: string[];
|
|
36
|
+
expressions: string[];
|
|
37
|
+
motions: string[];
|
|
38
|
+
groups: {
|
|
39
|
+
id: string;
|
|
40
|
+
parts: string[];
|
|
41
|
+
}[];
|
|
42
|
+
drivers: {
|
|
43
|
+
name: DriverName;
|
|
44
|
+
parameters: string[];
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
/** 캐릭터 패키지(.fa2d) 하나를 계산합니다. 그리지는 않으며, 그리기는 `/pixi`의 CharacterView가 맡습니다. */
|
|
48
|
+
export declare class Character {
|
|
49
|
+
private readonly wasm;
|
|
50
|
+
readonly info: CharacterInfo;
|
|
51
|
+
private constructor();
|
|
52
|
+
/** seed를 빼면 무작위 시드를 씁니다. 같은 시드의 캐릭터들은 같은 순간에 깜빡이기 때문입니다. */
|
|
53
|
+
static load(bytes: Uint8Array, options?: {
|
|
54
|
+
seed?: bigint;
|
|
55
|
+
}): Promise<Character>;
|
|
56
|
+
/** `info.textures` 순서대로 패키지 안의 WebP 바이트를 돌려줍니다. */
|
|
57
|
+
textureBytes(index: number): Uint8Array;
|
|
58
|
+
update(deltaSeconds: number): void;
|
|
59
|
+
/** 모든 slot의 정점을 이어 붙인 (x, y) 목록입니다. 캔버스 픽셀이며 y는 아래쪽이 양수입니다. */
|
|
60
|
+
get vertices(): Float32Array<ArrayBuffer>;
|
|
61
|
+
/** slot마다 1이면 보이고 0이면 숨깁니다. */
|
|
62
|
+
get slotVisible(): Uint8Array<ArrayBuffer>;
|
|
63
|
+
/** slot마다 지금 그릴 텍스처 번호입니다. 보이지 않는 slot의 값은 쓰지 않습니다. */
|
|
64
|
+
get slotTextures(): Uint32Array<ArrayBuffer>;
|
|
65
|
+
/** 이번 프레임의 최종 파라미터 값입니다. 순서는 `info.parameters`와 같습니다. */
|
|
66
|
+
get parameterValues(): Float64Array<ArrayBuffer>;
|
|
67
|
+
setExpression(name: string | null): void;
|
|
68
|
+
get currentExpression(): string | null;
|
|
69
|
+
/** 시작했으면 true, 재생 중인 모션보다 우선순위가 높지 않아 버려졌으면 false입니다. */
|
|
70
|
+
playMotion(name: string, priority: MotionPriority): boolean;
|
|
71
|
+
/** 재생 중인 모션 이름입니다. 모션이 끝나면 null이 됩니다. */
|
|
72
|
+
get currentMotion(): string | null;
|
|
73
|
+
/** 얼굴 추적, 마우스 시선, 슬라이더 같은 외부 입력입니다. 기본값에서 옮긴 만큼 더합니다. */
|
|
74
|
+
setInput(parameter: string, value: number): void;
|
|
75
|
+
/** 실제 음성으로 계산한 입 벌림 값입니다. 말하기 시뮬레이션이 꺼져 있을 때 씁니다. */
|
|
76
|
+
setLipSync(mouthOpen: number, speaking: boolean): void;
|
|
77
|
+
/** 음성 없이 말하는 흉내를 냅니다. 켜져 있으면 setLipSync 값 대신 이것을 씁니다. */
|
|
78
|
+
setTalkSimulation(enabled: boolean, syllablesPerSecond: number, loudness: number): void;
|
|
79
|
+
setDriverEnabled(driver: DriverName, enabled: boolean): void;
|
|
80
|
+
/** WASM 메모리를 해제합니다. 이후에는 쓸 수 없습니다. */
|
|
81
|
+
free(): void;
|
|
82
|
+
private buffer;
|
|
83
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import initializeWasm, { Character as WasmCharacter } from "../wasm/featheranime2d_wasm.js";
|
|
2
|
+
let memory;
|
|
3
|
+
let initialization;
|
|
4
|
+
// WASM은 처음 불러올 때 한 번만 초기화합니다.
|
|
5
|
+
function initializeOnce() {
|
|
6
|
+
initialization ??= initializeWasm().then((exports) => {
|
|
7
|
+
memory = exports.memory;
|
|
8
|
+
});
|
|
9
|
+
return initialization;
|
|
10
|
+
}
|
|
11
|
+
/** 캐릭터 패키지(.fa2d) 하나를 계산합니다. 그리지는 않으며, 그리기는 `/pixi`의 CharacterView가 맡습니다. */
|
|
12
|
+
export class Character {
|
|
13
|
+
wasm;
|
|
14
|
+
info;
|
|
15
|
+
constructor(wasm) {
|
|
16
|
+
this.wasm = wasm;
|
|
17
|
+
this.info = JSON.parse(wasm.infoJson());
|
|
18
|
+
}
|
|
19
|
+
/** seed를 빼면 무작위 시드를 씁니다. 같은 시드의 캐릭터들은 같은 순간에 깜빡이기 때문입니다. */
|
|
20
|
+
static async load(bytes, options = {}) {
|
|
21
|
+
await initializeOnce();
|
|
22
|
+
return new Character(WasmCharacter.load(bytes, options.seed ?? randomSeed()));
|
|
23
|
+
}
|
|
24
|
+
/** `info.textures` 순서대로 패키지 안의 WebP 바이트를 돌려줍니다. */
|
|
25
|
+
textureBytes(index) {
|
|
26
|
+
const bytes = this.wasm.textureBytes(index);
|
|
27
|
+
if (!bytes)
|
|
28
|
+
throw new Error(`Unknown texture: ${index}`);
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
update(deltaSeconds) {
|
|
32
|
+
this.wasm.update(deltaSeconds);
|
|
33
|
+
}
|
|
34
|
+
// 아래 버퍼는 WASM 메모리를 복사 없이 봅니다. WASM 메모리가 커지면 이전 뷰가 끊어지므로 읽을 때마다 새로 만들고,
|
|
35
|
+
// 다음 update 전까지만 유효합니다.
|
|
36
|
+
/** 모든 slot의 정점을 이어 붙인 (x, y) 목록입니다. 캔버스 픽셀이며 y는 아래쪽이 양수입니다. */
|
|
37
|
+
get vertices() {
|
|
38
|
+
return new Float32Array(this.buffer(), this.wasm.verticesPointer(), this.wasm.verticesLength());
|
|
39
|
+
}
|
|
40
|
+
/** slot마다 1이면 보이고 0이면 숨깁니다. */
|
|
41
|
+
get slotVisible() {
|
|
42
|
+
return new Uint8Array(this.buffer(), this.wasm.slotVisiblePointer(), this.wasm.slotCount());
|
|
43
|
+
}
|
|
44
|
+
/** slot마다 지금 그릴 텍스처 번호입니다. 보이지 않는 slot의 값은 쓰지 않습니다. */
|
|
45
|
+
get slotTextures() {
|
|
46
|
+
return new Uint32Array(this.buffer(), this.wasm.slotTexturesPointer(), this.wasm.slotCount());
|
|
47
|
+
}
|
|
48
|
+
/** 이번 프레임의 최종 파라미터 값입니다. 순서는 `info.parameters`와 같습니다. */
|
|
49
|
+
get parameterValues() {
|
|
50
|
+
return new Float64Array(this.buffer(), this.wasm.parameterValuesPointer(), this.wasm.parameterCount());
|
|
51
|
+
}
|
|
52
|
+
setExpression(name) {
|
|
53
|
+
this.wasm.setExpression(name ?? undefined);
|
|
54
|
+
}
|
|
55
|
+
get currentExpression() {
|
|
56
|
+
return this.wasm.currentExpression() ?? null;
|
|
57
|
+
}
|
|
58
|
+
/** 시작했으면 true, 재생 중인 모션보다 우선순위가 높지 않아 버려졌으면 false입니다. */
|
|
59
|
+
playMotion(name, priority) {
|
|
60
|
+
return this.wasm.playMotion(name, priority);
|
|
61
|
+
}
|
|
62
|
+
/** 재생 중인 모션 이름입니다. 모션이 끝나면 null이 됩니다. */
|
|
63
|
+
get currentMotion() {
|
|
64
|
+
return this.wasm.currentMotion() ?? null;
|
|
65
|
+
}
|
|
66
|
+
/** 얼굴 추적, 마우스 시선, 슬라이더 같은 외부 입력입니다. 기본값에서 옮긴 만큼 더합니다. */
|
|
67
|
+
setInput(parameter, value) {
|
|
68
|
+
this.wasm.setInput(parameter, value);
|
|
69
|
+
}
|
|
70
|
+
/** 실제 음성으로 계산한 입 벌림 값입니다. 말하기 시뮬레이션이 꺼져 있을 때 씁니다. */
|
|
71
|
+
setLipSync(mouthOpen, speaking) {
|
|
72
|
+
this.wasm.setLipSync(mouthOpen, speaking);
|
|
73
|
+
}
|
|
74
|
+
/** 음성 없이 말하는 흉내를 냅니다. 켜져 있으면 setLipSync 값 대신 이것을 씁니다. */
|
|
75
|
+
setTalkSimulation(enabled, syllablesPerSecond, loudness) {
|
|
76
|
+
this.wasm.setTalkSimulation(enabled, syllablesPerSecond, loudness);
|
|
77
|
+
}
|
|
78
|
+
setDriverEnabled(driver, enabled) {
|
|
79
|
+
this.wasm.setDriverEnabled(driver, enabled);
|
|
80
|
+
}
|
|
81
|
+
/** WASM 메모리를 해제합니다. 이후에는 쓸 수 없습니다. */
|
|
82
|
+
free() {
|
|
83
|
+
this.wasm.free();
|
|
84
|
+
}
|
|
85
|
+
buffer() {
|
|
86
|
+
return memory.buffer;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function randomSeed() {
|
|
90
|
+
const [high, low] = crypto.getRandomValues(new Uint32Array(2));
|
|
91
|
+
return (BigInt(high) << 32n) | BigInt(low);
|
|
92
|
+
}
|
package/dist/pixi.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Container, type DestroyOptions } from "pixi.js";
|
|
2
|
+
import { Character, type CharacterInfo } from "./index.js";
|
|
3
|
+
/**
|
|
4
|
+
* 캐릭터를 PixiJS로 그리는 Container입니다. 캔버스 픽셀 좌표로 그리므로, 화면 크기에 맞추는 일은 앱이 합니다.
|
|
5
|
+
* 앱은 자기 루프에서 매 프레임 `update`를 부릅니다.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CharacterView extends Container {
|
|
8
|
+
readonly character: Character;
|
|
9
|
+
private readonly textures;
|
|
10
|
+
readonly info: CharacterInfo;
|
|
11
|
+
private readonly meshes;
|
|
12
|
+
private readonly hiddenSlots;
|
|
13
|
+
private constructor();
|
|
14
|
+
/** URL이나 패키지 바이트에서 캐릭터를 불러옵니다. */
|
|
15
|
+
static load(source: string | URL | Uint8Array, options?: {
|
|
16
|
+
seed?: bigint;
|
|
17
|
+
}): Promise<CharacterView>;
|
|
18
|
+
update(deltaSeconds: number): void;
|
|
19
|
+
/** 디버그용으로 slot 하나를 숨깁니다. Core의 계산과 상관없이 그리기만 건너뜁니다. */
|
|
20
|
+
setSlotHidden(id: string, hidden: boolean): void;
|
|
21
|
+
/** 텍스처와 WASM 메모리까지 해제합니다. */
|
|
22
|
+
destroy(options?: DestroyOptions): void;
|
|
23
|
+
}
|
package/dist/pixi.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Container, Graphics, ImageSource, Mesh, MeshGeometry, Sprite, Texture } from "pixi.js";
|
|
2
|
+
import { Character } from "./index.js";
|
|
3
|
+
/**
|
|
4
|
+
* 캐릭터를 PixiJS로 그리는 Container입니다. 캔버스 픽셀 좌표로 그리므로, 화면 크기에 맞추는 일은 앱이 합니다.
|
|
5
|
+
* 앱은 자기 루프에서 매 프레임 `update`를 부릅니다.
|
|
6
|
+
*/
|
|
7
|
+
export class CharacterView extends Container {
|
|
8
|
+
character;
|
|
9
|
+
textures;
|
|
10
|
+
info;
|
|
11
|
+
meshes;
|
|
12
|
+
hiddenSlots = new Set();
|
|
13
|
+
constructor(character, textures) {
|
|
14
|
+
super();
|
|
15
|
+
this.character = character;
|
|
16
|
+
this.textures = textures;
|
|
17
|
+
this.info = character.info;
|
|
18
|
+
const { canvas, backdrop, slots } = this.info;
|
|
19
|
+
// 배경 그림은 캔버스보다 넓게 그려 넓은 화면의 여백을 채우므로 잘라 내지 않습니다.
|
|
20
|
+
if (backdrop?.type === "image") {
|
|
21
|
+
const sprite = new Sprite(textures[backdrop.texture]);
|
|
22
|
+
sprite.position.set(...backdrop.offset);
|
|
23
|
+
this.addChild(sprite);
|
|
24
|
+
}
|
|
25
|
+
const content = new Container();
|
|
26
|
+
this.meshes = slots.map((slot) => {
|
|
27
|
+
const geometry = new MeshGeometry({
|
|
28
|
+
positions: new Float32Array(slot.vertexCount * 2),
|
|
29
|
+
uvs: new Float32Array(slot.uvs),
|
|
30
|
+
indices: new Uint32Array(slot.indices),
|
|
31
|
+
});
|
|
32
|
+
const mesh = new Mesh({ geometry, texture: Texture.EMPTY });
|
|
33
|
+
mesh.visible = false;
|
|
34
|
+
content.addChild(mesh);
|
|
35
|
+
return mesh;
|
|
36
|
+
});
|
|
37
|
+
// 캔버스 밖까지 그린 부위가 있어도 화면에는 캔버스 영역만 보여 줍니다.
|
|
38
|
+
const canvasArea = new Graphics().rect(0, 0, canvas.width, canvas.height).fill(0xffffff);
|
|
39
|
+
content.addChild(canvasArea);
|
|
40
|
+
content.mask = canvasArea;
|
|
41
|
+
this.addChild(content);
|
|
42
|
+
}
|
|
43
|
+
/** URL이나 패키지 바이트에서 캐릭터를 불러옵니다. */
|
|
44
|
+
static async load(source, options = {}) {
|
|
45
|
+
const bytes = source instanceof Uint8Array ? source : await fetchBytes(source);
|
|
46
|
+
const character = await Character.load(bytes, options);
|
|
47
|
+
const textures = await Promise.all(character.info.textures.map((_, index) => decode(character.textureBytes(index))));
|
|
48
|
+
return new CharacterView(character, textures);
|
|
49
|
+
}
|
|
50
|
+
update(deltaSeconds) {
|
|
51
|
+
this.character.update(deltaSeconds);
|
|
52
|
+
const { vertices, slotVisible, slotTextures } = this.character;
|
|
53
|
+
this.meshes.forEach((mesh, index) => {
|
|
54
|
+
mesh.visible = slotVisible[index] === 1 && !this.hiddenSlots.has(index);
|
|
55
|
+
if (!mesh.visible)
|
|
56
|
+
return;
|
|
57
|
+
mesh.texture = this.textures[slotTextures[index]];
|
|
58
|
+
const { firstVertex, vertexCount } = this.info.slots[index];
|
|
59
|
+
const positions = mesh.geometry.getBuffer("aPosition");
|
|
60
|
+
positions.data.set(vertices.subarray(firstVertex * 2, (firstVertex + vertexCount) * 2));
|
|
61
|
+
positions.update();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** 디버그용으로 slot 하나를 숨깁니다. Core의 계산과 상관없이 그리기만 건너뜁니다. */
|
|
65
|
+
setSlotHidden(id, hidden) {
|
|
66
|
+
const index = this.info.slots.findIndex((slot) => slot.id === id);
|
|
67
|
+
if (index === -1)
|
|
68
|
+
throw new Error(`Unknown slot: ${id}`);
|
|
69
|
+
if (hidden)
|
|
70
|
+
this.hiddenSlots.add(index);
|
|
71
|
+
else
|
|
72
|
+
this.hiddenSlots.delete(index);
|
|
73
|
+
}
|
|
74
|
+
/** 텍스처와 WASM 메모리까지 해제합니다. */
|
|
75
|
+
destroy(options) {
|
|
76
|
+
super.destroy(options);
|
|
77
|
+
for (const texture of this.textures)
|
|
78
|
+
texture.destroy(true);
|
|
79
|
+
this.character.free();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function fetchBytes(url) {
|
|
83
|
+
const response = await fetch(url);
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
throw new Error((await response.text()) || `Failed to load ${url} (${response.status})`);
|
|
86
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
87
|
+
}
|
|
88
|
+
// PixiJS의 Assets가 그림을 불러올 때와 같은 방식으로 텍스처를 만들어, 반투명한 가장자리도 같게 그립니다.
|
|
89
|
+
async function decode(webp) {
|
|
90
|
+
const bitmap = await createImageBitmap(new Blob([webp], { type: "image/webp" }));
|
|
91
|
+
return new Texture({ source: new ImageSource({ resource: bitmap, alphaMode: "premultiply-alpha-on-upload" }) });
|
|
92
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CharacterView } from "./pixi.js";
|
|
2
|
+
/**
|
|
3
|
+
* `containerRef`를 붙인 요소에 캐릭터를 띄웁니다. 요소 크기에 맞춰 비율을 유지하며 가운데에 놓고, 매 프레임 움직입니다.
|
|
4
|
+
* `src`가 바뀌거나 컴포넌트가 사라지면 화면, 텍스처, WASM 메모리를 모두 해제합니다.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useFeatherAnime2D(src: string, options?: {
|
|
7
|
+
seed?: bigint;
|
|
8
|
+
}): {
|
|
9
|
+
containerRef: import("react").RefObject<HTMLDivElement | null>;
|
|
10
|
+
view: CharacterView | undefined;
|
|
11
|
+
error: Error | undefined;
|
|
12
|
+
};
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* `containerRef`를 붙인 요소에 캐릭터를 띄웁니다. 요소 크기에 맞춰 비율을 유지하며 가운데에 놓고, 매 프레임 움직입니다.
|
|
4
|
+
* `src`가 바뀌거나 컴포넌트가 사라지면 화면, 텍스처, WASM 메모리를 모두 해제합니다.
|
|
5
|
+
*/
|
|
6
|
+
export function useFeatherAnime2D(src, options = {}) {
|
|
7
|
+
const containerRef = useRef(null);
|
|
8
|
+
const [view, setView] = useState();
|
|
9
|
+
const [error, setError] = useState();
|
|
10
|
+
const { seed } = options;
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const container = containerRef.current;
|
|
13
|
+
if (!container)
|
|
14
|
+
return;
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
let application;
|
|
17
|
+
let resizeObserver;
|
|
18
|
+
setView(undefined);
|
|
19
|
+
setError(undefined);
|
|
20
|
+
// PixiJS와 WASM은 브라우저에서만 동작하므로 effect 안에서 불러옵니다. 그래서 서버 렌더링에서 빼는 처리가 필요 없습니다.
|
|
21
|
+
const started = (async () => {
|
|
22
|
+
const [{ Application }, { CharacterView }] = await Promise.all([import("pixi.js"), import("./pixi.js")]);
|
|
23
|
+
application = new Application();
|
|
24
|
+
await application.init({ resizeTo: container, backgroundAlpha: 0, antialias: true, autoDensity: true });
|
|
25
|
+
const loaded = await CharacterView.load(src, { seed });
|
|
26
|
+
if (cancelled)
|
|
27
|
+
return loaded.destroy();
|
|
28
|
+
container.appendChild(application.canvas);
|
|
29
|
+
application.stage.addChild(loaded);
|
|
30
|
+
const app = application;
|
|
31
|
+
app.ticker.add((ticker) => {
|
|
32
|
+
loaded.update(ticker.deltaMS / 1000);
|
|
33
|
+
fitToScreen(loaded, app);
|
|
34
|
+
});
|
|
35
|
+
// resizeTo는 창 크기 변화만 감지하므로, 옆의 패널처럼 레이아웃 때문에 요소 크기가 바뀌는 경우도 반영합니다.
|
|
36
|
+
resizeObserver = new ResizeObserver(() => app.resize());
|
|
37
|
+
resizeObserver.observe(container);
|
|
38
|
+
setView(loaded);
|
|
39
|
+
})().catch((reason) => {
|
|
40
|
+
if (!cancelled)
|
|
41
|
+
setError(reason instanceof Error ? reason : new Error(String(reason)));
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
cancelled = true;
|
|
45
|
+
resizeObserver?.disconnect();
|
|
46
|
+
// init이 끝나기 전에 destroy하면 PixiJS가 예외를 던지므로 시작 작업을 기다립니다.
|
|
47
|
+
started.then(() => application?.destroy(true, { children: true }));
|
|
48
|
+
};
|
|
49
|
+
}, [src, seed]);
|
|
50
|
+
return { containerRef, view, error };
|
|
51
|
+
}
|
|
52
|
+
function fitToScreen(view, application) {
|
|
53
|
+
const { width, height } = view.info.canvas;
|
|
54
|
+
const scale = Math.min(application.screen.width / width, application.screen.height / height);
|
|
55
|
+
view.scale.set(scale);
|
|
56
|
+
view.position.set((application.screen.width - width * scale) / 2, (application.screen.height - height * scale) / 2);
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lemondouble/featheranime2d",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-preview.2",
|
|
4
|
+
"description": "그림 한 장으로 만드는, 가벼운 움직이는 애니메이션 캐릭터 렌더러",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"2d",
|
|
8
|
+
"animation",
|
|
9
|
+
"character",
|
|
10
|
+
"pixi",
|
|
11
|
+
"godot"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./pixi": {
|
|
20
|
+
"types": "./dist/pixi.d.ts",
|
|
21
|
+
"default": "./dist/pixi.js"
|
|
22
|
+
},
|
|
23
|
+
"./react": {
|
|
24
|
+
"types": "./dist/react.d.ts",
|
|
25
|
+
"default": "./dist/react.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"wasm"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build:wasm": "cd ../../rust && cargo build --release --target wasm32-unknown-unknown -p featheranime2d-wasm && wasm-bindgen --target web --out-dir ../typescript/featheranime2d/wasm target/wasm32-unknown-unknown/release/featheranime2d_wasm.wasm",
|
|
34
|
+
"build": "pnpm build:wasm && tsc -p ."
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"pixi.js": "^8.22.0",
|
|
38
|
+
"react": "^19.3.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"pixi.js": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/react": "^19.3.0",
|
|
50
|
+
"pixi.js": "^8.22.0",
|
|
51
|
+
"react": "^19.3.1",
|
|
52
|
+
"typescript": "^7.0.2"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
export class Character {
|
|
5
|
+
private constructor();
|
|
6
|
+
free(): void;
|
|
7
|
+
[Symbol.dispose](): void;
|
|
8
|
+
currentExpression(): string | undefined;
|
|
9
|
+
currentMotion(): string | undefined;
|
|
10
|
+
infoJson(): string;
|
|
11
|
+
static load(_package: Uint8Array, seed: bigint): Character;
|
|
12
|
+
parameterCount(): number;
|
|
13
|
+
parameterValuesPointer(): number;
|
|
14
|
+
playMotion(name: string, priority: string): boolean;
|
|
15
|
+
setDriverEnabled(driver: string, enabled: boolean): void;
|
|
16
|
+
setExpression(name?: string | null): void;
|
|
17
|
+
setInput(parameter: string, value: number): void;
|
|
18
|
+
setLipSync(mouth_open: number, speaking: boolean): void;
|
|
19
|
+
setTalkSimulation(enabled: boolean, syllables_per_second: number, loudness: number): void;
|
|
20
|
+
slotCount(): number;
|
|
21
|
+
slotTexturesPointer(): number;
|
|
22
|
+
/**
|
|
23
|
+
* bool은 1바이트(0 또는 1)라서 JS에서는 Uint8Array로 봅니다.
|
|
24
|
+
*/
|
|
25
|
+
slotVisiblePointer(): number;
|
|
26
|
+
textureBytes(index: number): Uint8Array | undefined;
|
|
27
|
+
update(delta_seconds: number): void;
|
|
28
|
+
verticesLength(): number;
|
|
29
|
+
verticesPointer(): number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
33
|
+
|
|
34
|
+
export interface InitOutput {
|
|
35
|
+
readonly memory: WebAssembly.Memory;
|
|
36
|
+
readonly __wbg_character_free: (a: number, b: number) => void;
|
|
37
|
+
readonly character_currentExpression: (a: number) => [number, number];
|
|
38
|
+
readonly character_currentMotion: (a: number) => [number, number];
|
|
39
|
+
readonly character_infoJson: (a: number) => [number, number];
|
|
40
|
+
readonly character_load: (a: number, b: number, c: bigint) => [number, number, number];
|
|
41
|
+
readonly character_parameterCount: (a: number) => number;
|
|
42
|
+
readonly character_parameterValuesPointer: (a: number) => number;
|
|
43
|
+
readonly character_playMotion: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
|
|
44
|
+
readonly character_setDriverEnabled: (a: number, b: number, c: number, d: number) => [number, number];
|
|
45
|
+
readonly character_setExpression: (a: number, b: number, c: number) => [number, number];
|
|
46
|
+
readonly character_setInput: (a: number, b: number, c: number, d: number) => [number, number];
|
|
47
|
+
readonly character_setLipSync: (a: number, b: number, c: number) => void;
|
|
48
|
+
readonly character_setTalkSimulation: (a: number, b: number, c: number, d: number) => void;
|
|
49
|
+
readonly character_slotCount: (a: number) => number;
|
|
50
|
+
readonly character_slotTexturesPointer: (a: number) => number;
|
|
51
|
+
readonly character_slotVisiblePointer: (a: number) => number;
|
|
52
|
+
readonly character_textureBytes: (a: number, b: number) => [number, number];
|
|
53
|
+
readonly character_update: (a: number, b: number) => void;
|
|
54
|
+
readonly character_verticesLength: (a: number) => number;
|
|
55
|
+
readonly character_verticesPointer: (a: number) => number;
|
|
56
|
+
readonly __wbindgen_externrefs: WebAssembly.Table;
|
|
57
|
+
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
58
|
+
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
59
|
+
readonly __externref_table_dealloc: (a: number) => void;
|
|
60
|
+
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
61
|
+
readonly __wbindgen_start: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
68
|
+
* a precompiled `WebAssembly.Module`.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
71
|
+
*
|
|
72
|
+
* @returns {InitOutput}
|
|
73
|
+
*/
|
|
74
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
78
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
79
|
+
*
|
|
80
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
81
|
+
*
|
|
82
|
+
* @returns {Promise<InitOutput>}
|
|
83
|
+
*/
|
|
84
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/* @ts-self-types="./featheranime2d_wasm.d.ts" */
|
|
2
|
+
|
|
3
|
+
export class Character {
|
|
4
|
+
static __wrap(ptr) {
|
|
5
|
+
const obj = Object.create(Character.prototype);
|
|
6
|
+
obj.__wbg_ptr = ptr;
|
|
7
|
+
CharacterFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
8
|
+
return obj;
|
|
9
|
+
}
|
|
10
|
+
__destroy_into_raw() {
|
|
11
|
+
const ptr = this.__wbg_ptr;
|
|
12
|
+
this.__wbg_ptr = 0;
|
|
13
|
+
CharacterFinalization.unregister(this);
|
|
14
|
+
return ptr;
|
|
15
|
+
}
|
|
16
|
+
free() {
|
|
17
|
+
const ptr = this.__destroy_into_raw();
|
|
18
|
+
wasm.__wbg_character_free(ptr, 0);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @returns {string | undefined}
|
|
22
|
+
*/
|
|
23
|
+
currentExpression() {
|
|
24
|
+
const ret = wasm.character_currentExpression(this.__wbg_ptr);
|
|
25
|
+
let v1;
|
|
26
|
+
if (ret[0] !== 0) {
|
|
27
|
+
v1 = getStringFromWasm0(ret[0], ret[1]);
|
|
28
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
29
|
+
}
|
|
30
|
+
return v1;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @returns {string | undefined}
|
|
34
|
+
*/
|
|
35
|
+
currentMotion() {
|
|
36
|
+
const ret = wasm.character_currentMotion(this.__wbg_ptr);
|
|
37
|
+
let v1;
|
|
38
|
+
if (ret[0] !== 0) {
|
|
39
|
+
v1 = getStringFromWasm0(ret[0], ret[1]);
|
|
40
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
41
|
+
}
|
|
42
|
+
return v1;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
infoJson() {
|
|
48
|
+
let deferred1_0;
|
|
49
|
+
let deferred1_1;
|
|
50
|
+
try {
|
|
51
|
+
const ret = wasm.character_infoJson(this.__wbg_ptr);
|
|
52
|
+
deferred1_0 = ret[0];
|
|
53
|
+
deferred1_1 = ret[1];
|
|
54
|
+
return getStringFromWasm0(ret[0], ret[1]);
|
|
55
|
+
} finally {
|
|
56
|
+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @param {Uint8Array} _package
|
|
61
|
+
* @param {bigint} seed
|
|
62
|
+
* @returns {Character}
|
|
63
|
+
*/
|
|
64
|
+
static load(_package, seed) {
|
|
65
|
+
const ptr0 = passArray8ToWasm0(_package, wasm.__wbindgen_malloc);
|
|
66
|
+
const len0 = WASM_VECTOR_LEN;
|
|
67
|
+
const ret = wasm.character_load(ptr0, len0, seed);
|
|
68
|
+
if (ret[2]) {
|
|
69
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
70
|
+
}
|
|
71
|
+
return Character.__wrap(ret[0]);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
parameterCount() {
|
|
77
|
+
const ret = wasm.character_parameterCount(this.__wbg_ptr);
|
|
78
|
+
return ret >>> 0;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
parameterValuesPointer() {
|
|
84
|
+
const ret = wasm.character_parameterValuesPointer(this.__wbg_ptr);
|
|
85
|
+
return ret >>> 0;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} name
|
|
89
|
+
* @param {string} priority
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
playMotion(name, priority) {
|
|
93
|
+
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
94
|
+
const len0 = WASM_VECTOR_LEN;
|
|
95
|
+
const ptr1 = passStringToWasm0(priority, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
96
|
+
const len1 = WASM_VECTOR_LEN;
|
|
97
|
+
const ret = wasm.character_playMotion(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
|
98
|
+
if (ret[2]) {
|
|
99
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
100
|
+
}
|
|
101
|
+
return ret[0] !== 0;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} driver
|
|
105
|
+
* @param {boolean} enabled
|
|
106
|
+
*/
|
|
107
|
+
setDriverEnabled(driver, enabled) {
|
|
108
|
+
const ptr0 = passStringToWasm0(driver, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
109
|
+
const len0 = WASM_VECTOR_LEN;
|
|
110
|
+
const ret = wasm.character_setDriverEnabled(this.__wbg_ptr, ptr0, len0, enabled);
|
|
111
|
+
if (ret[1]) {
|
|
112
|
+
throw takeFromExternrefTable0(ret[0]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @param {string | null} [name]
|
|
117
|
+
*/
|
|
118
|
+
setExpression(name) {
|
|
119
|
+
var ptr0 = isLikeNone(name) ? 0 : passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
120
|
+
var len0 = WASM_VECTOR_LEN;
|
|
121
|
+
const ret = wasm.character_setExpression(this.__wbg_ptr, ptr0, len0);
|
|
122
|
+
if (ret[1]) {
|
|
123
|
+
throw takeFromExternrefTable0(ret[0]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} parameter
|
|
128
|
+
* @param {number} value
|
|
129
|
+
*/
|
|
130
|
+
setInput(parameter, value) {
|
|
131
|
+
const ptr0 = passStringToWasm0(parameter, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
132
|
+
const len0 = WASM_VECTOR_LEN;
|
|
133
|
+
const ret = wasm.character_setInput(this.__wbg_ptr, ptr0, len0, value);
|
|
134
|
+
if (ret[1]) {
|
|
135
|
+
throw takeFromExternrefTable0(ret[0]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* @param {number} mouth_open
|
|
140
|
+
* @param {boolean} speaking
|
|
141
|
+
*/
|
|
142
|
+
setLipSync(mouth_open, speaking) {
|
|
143
|
+
wasm.character_setLipSync(this.__wbg_ptr, mouth_open, speaking);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* @param {boolean} enabled
|
|
147
|
+
* @param {number} syllables_per_second
|
|
148
|
+
* @param {number} loudness
|
|
149
|
+
*/
|
|
150
|
+
setTalkSimulation(enabled, syllables_per_second, loudness) {
|
|
151
|
+
wasm.character_setTalkSimulation(this.__wbg_ptr, enabled, syllables_per_second, loudness);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* @returns {number}
|
|
155
|
+
*/
|
|
156
|
+
slotCount() {
|
|
157
|
+
const ret = wasm.character_slotCount(this.__wbg_ptr);
|
|
158
|
+
return ret >>> 0;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* @returns {number}
|
|
162
|
+
*/
|
|
163
|
+
slotTexturesPointer() {
|
|
164
|
+
const ret = wasm.character_slotTexturesPointer(this.__wbg_ptr);
|
|
165
|
+
return ret >>> 0;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* bool은 1바이트(0 또는 1)라서 JS에서는 Uint8Array로 봅니다.
|
|
169
|
+
* @returns {number}
|
|
170
|
+
*/
|
|
171
|
+
slotVisiblePointer() {
|
|
172
|
+
const ret = wasm.character_slotVisiblePointer(this.__wbg_ptr);
|
|
173
|
+
return ret >>> 0;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* @param {number} index
|
|
177
|
+
* @returns {Uint8Array | undefined}
|
|
178
|
+
*/
|
|
179
|
+
textureBytes(index) {
|
|
180
|
+
const ret = wasm.character_textureBytes(this.__wbg_ptr, index);
|
|
181
|
+
let v1;
|
|
182
|
+
if (ret[0] !== 0) {
|
|
183
|
+
v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
184
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
185
|
+
}
|
|
186
|
+
return v1;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* @param {number} delta_seconds
|
|
190
|
+
*/
|
|
191
|
+
update(delta_seconds) {
|
|
192
|
+
wasm.character_update(this.__wbg_ptr, delta_seconds);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* @returns {number}
|
|
196
|
+
*/
|
|
197
|
+
verticesLength() {
|
|
198
|
+
const ret = wasm.character_verticesLength(this.__wbg_ptr);
|
|
199
|
+
return ret >>> 0;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* @returns {number}
|
|
203
|
+
*/
|
|
204
|
+
verticesPointer() {
|
|
205
|
+
const ret = wasm.character_verticesPointer(this.__wbg_ptr);
|
|
206
|
+
return ret >>> 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (Symbol.dispose) Character.prototype[Symbol.dispose] = Character.prototype.free;
|
|
210
|
+
function __wbg_get_imports() {
|
|
211
|
+
const import0 = {
|
|
212
|
+
__proto__: null,
|
|
213
|
+
__wbg_Error_30c8987f7c2ed4e2: function(arg0, arg1) {
|
|
214
|
+
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
215
|
+
return ret;
|
|
216
|
+
},
|
|
217
|
+
__wbg___wbindgen_throw_41e9ee4f547fc59a: function(arg0, arg1) {
|
|
218
|
+
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
219
|
+
},
|
|
220
|
+
__wbindgen_init_externref_table: function() {
|
|
221
|
+
const table = wasm.__wbindgen_externrefs;
|
|
222
|
+
const offset = table.grow(4);
|
|
223
|
+
table.set(0, undefined);
|
|
224
|
+
table.set(offset + 0, undefined);
|
|
225
|
+
table.set(offset + 1, null);
|
|
226
|
+
table.set(offset + 2, true);
|
|
227
|
+
table.set(offset + 3, false);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
__proto__: null,
|
|
232
|
+
"./featheranime2d_wasm_bg.js": import0,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const CharacterFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
237
|
+
? { register: () => {}, unregister: () => {} }
|
|
238
|
+
: new FinalizationRegistry(ptr => wasm.__wbg_character_free(ptr, 1));
|
|
239
|
+
|
|
240
|
+
function getArrayU8FromWasm0(ptr, len) {
|
|
241
|
+
ptr = ptr >>> 0;
|
|
242
|
+
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getStringFromWasm0(ptr, len) {
|
|
246
|
+
return decodeText(ptr >>> 0, len);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let cachedUint8ArrayMemory0 = null;
|
|
250
|
+
function getUint8ArrayMemory0() {
|
|
251
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
252
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
253
|
+
}
|
|
254
|
+
return cachedUint8ArrayMemory0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isLikeNone(x) {
|
|
258
|
+
return x === undefined || x === null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function passArray8ToWasm0(arg, malloc) {
|
|
262
|
+
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
263
|
+
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
264
|
+
WASM_VECTOR_LEN = arg.length;
|
|
265
|
+
return ptr;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
269
|
+
if (realloc === undefined) {
|
|
270
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
271
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
272
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
|
273
|
+
WASM_VECTOR_LEN = buf.length;
|
|
274
|
+
return ptr;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let len = arg.length;
|
|
278
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
279
|
+
|
|
280
|
+
const mem = getUint8ArrayMemory0();
|
|
281
|
+
|
|
282
|
+
let offset = 0;
|
|
283
|
+
|
|
284
|
+
for (; offset < len; offset++) {
|
|
285
|
+
const code = arg.charCodeAt(offset);
|
|
286
|
+
if (code > 0x7F) break;
|
|
287
|
+
mem[ptr + offset] = code;
|
|
288
|
+
}
|
|
289
|
+
if (offset !== len) {
|
|
290
|
+
if (offset !== 0) {
|
|
291
|
+
arg = arg.slice(offset);
|
|
292
|
+
}
|
|
293
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
294
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
295
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
296
|
+
|
|
297
|
+
offset += ret.written;
|
|
298
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
WASM_VECTOR_LEN = offset;
|
|
302
|
+
return ptr;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function takeFromExternrefTable0(idx) {
|
|
306
|
+
const value = wasm.__wbindgen_externrefs.get(idx);
|
|
307
|
+
wasm.__externref_table_dealloc(idx);
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
312
|
+
cachedTextDecoder.decode();
|
|
313
|
+
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
314
|
+
let numBytesDecoded = 0;
|
|
315
|
+
function decodeText(ptr, len) {
|
|
316
|
+
numBytesDecoded += len;
|
|
317
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
318
|
+
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
319
|
+
cachedTextDecoder.decode();
|
|
320
|
+
numBytesDecoded = len;
|
|
321
|
+
}
|
|
322
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const cachedTextEncoder = new TextEncoder();
|
|
326
|
+
|
|
327
|
+
if (!('encodeInto' in cachedTextEncoder)) {
|
|
328
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
329
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
330
|
+
view.set(buf);
|
|
331
|
+
return {
|
|
332
|
+
read: arg.length,
|
|
333
|
+
written: buf.length
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let WASM_VECTOR_LEN = 0;
|
|
339
|
+
|
|
340
|
+
let wasmModule, wasmInstance, wasm;
|
|
341
|
+
function __wbg_finalize_init(instance, module) {
|
|
342
|
+
wasmInstance = instance;
|
|
343
|
+
wasm = instance.exports;
|
|
344
|
+
wasmModule = module;
|
|
345
|
+
cachedUint8ArrayMemory0 = null;
|
|
346
|
+
wasm.__wbindgen_start();
|
|
347
|
+
return wasm;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function __wbg_load(module, imports) {
|
|
351
|
+
if (typeof Response === 'function' && module instanceof Response) {
|
|
352
|
+
if (!module.ok) {
|
|
353
|
+
throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
|
357
|
+
try {
|
|
358
|
+
return await WebAssembly.instantiateStreaming(module, imports);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
const validResponse = expectedResponseType(module.type);
|
|
361
|
+
|
|
362
|
+
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
|
363
|
+
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
|
364
|
+
|
|
365
|
+
} else { throw e; }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const bytes = await module.arrayBuffer();
|
|
370
|
+
return await WebAssembly.instantiate(bytes, imports);
|
|
371
|
+
} else {
|
|
372
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
373
|
+
|
|
374
|
+
if (instance instanceof WebAssembly.Instance) {
|
|
375
|
+
return { instance, module };
|
|
376
|
+
} else {
|
|
377
|
+
return instance;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function expectedResponseType(type) {
|
|
382
|
+
switch (type) {
|
|
383
|
+
case 'basic': case 'cors': case 'default': return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function initSync(module) {
|
|
390
|
+
if (wasm !== undefined) return wasm;
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if (module !== undefined) {
|
|
394
|
+
if (Object.getPrototypeOf(module) === Object.prototype) {
|
|
395
|
+
({module} = module)
|
|
396
|
+
} else {
|
|
397
|
+
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const imports = __wbg_get_imports();
|
|
402
|
+
if (!(module instanceof WebAssembly.Module)) {
|
|
403
|
+
module = new WebAssembly.Module(module);
|
|
404
|
+
}
|
|
405
|
+
const instance = new WebAssembly.Instance(module, imports);
|
|
406
|
+
return __wbg_finalize_init(instance, module);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function __wbg_init(module_or_path) {
|
|
410
|
+
if (wasm !== undefined) return wasm;
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if (module_or_path !== undefined) {
|
|
414
|
+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
|
415
|
+
({module_or_path} = module_or_path)
|
|
416
|
+
} else {
|
|
417
|
+
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (module_or_path === undefined) {
|
|
422
|
+
module_or_path = new URL('featheranime2d_wasm_bg.wasm', import.meta.url);
|
|
423
|
+
}
|
|
424
|
+
const imports = __wbg_get_imports();
|
|
425
|
+
|
|
426
|
+
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
|
427
|
+
module_or_path = fetch(module_or_path);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
|
431
|
+
|
|
432
|
+
return __wbg_finalize_init(instance, module);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export { initSync, __wbg_init as default };
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_character_free: (a: number, b: number) => void;
|
|
5
|
+
export const character_currentExpression: (a: number) => [number, number];
|
|
6
|
+
export const character_currentMotion: (a: number) => [number, number];
|
|
7
|
+
export const character_infoJson: (a: number) => [number, number];
|
|
8
|
+
export const character_load: (a: number, b: number, c: bigint) => [number, number, number];
|
|
9
|
+
export const character_parameterCount: (a: number) => number;
|
|
10
|
+
export const character_parameterValuesPointer: (a: number) => number;
|
|
11
|
+
export const character_playMotion: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
|
|
12
|
+
export const character_setDriverEnabled: (a: number, b: number, c: number, d: number) => [number, number];
|
|
13
|
+
export const character_setExpression: (a: number, b: number, c: number) => [number, number];
|
|
14
|
+
export const character_setInput: (a: number, b: number, c: number, d: number) => [number, number];
|
|
15
|
+
export const character_setLipSync: (a: number, b: number, c: number) => void;
|
|
16
|
+
export const character_setTalkSimulation: (a: number, b: number, c: number, d: number) => void;
|
|
17
|
+
export const character_slotCount: (a: number) => number;
|
|
18
|
+
export const character_slotTexturesPointer: (a: number) => number;
|
|
19
|
+
export const character_slotVisiblePointer: (a: number) => number;
|
|
20
|
+
export const character_textureBytes: (a: number, b: number) => [number, number];
|
|
21
|
+
export const character_update: (a: number, b: number) => void;
|
|
22
|
+
export const character_verticesLength: (a: number) => number;
|
|
23
|
+
export const character_verticesPointer: (a: number) => number;
|
|
24
|
+
export const __wbindgen_externrefs: WebAssembly.Table;
|
|
25
|
+
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
26
|
+
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
27
|
+
export const __externref_table_dealloc: (a: number) => void;
|
|
28
|
+
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
29
|
+
export const __wbindgen_start: () => void;
|