@guoquan.net/flow-engine 0.1.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 ADDED
@@ -0,0 +1,86 @@
1
+ # Flow (服喽) 🌊
2
+ `@guoquan.net/flow-engine`
3
+
4
+ [![CI](https://github.com/guoquan/flow-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/guoquan/flow-engine/actions/workflows/ci.yml)
5
+ [![codecov](https://codecov.io/gh/guoquan/flow-engine/graph/badge.svg?token=2T5SGUBMK4)](https://codecov.io/gh/guoquan/flow-engine)
6
+ [![Deploy to GitHub Pages](https://github.com/guoquan/flow-engine/actions/workflows/deploy.yml/badge.svg)](https://github.com/guoquan/flow-engine/actions/workflows/deploy.yml)
7
+
8
+ [English](#english) | [中文](#中文)
9
+
10
+ ---
11
+
12
+ ## English
13
+
14
+ > **"Flow: Convincing at first breath."**
15
+
16
+ **Flow** (distributed as `@guoquan.net/flow-engine`) is a high-performance, lightweight web-based digital human engine. It empowers web applications with lifelike AI avatars through simple API-driven interactions.
17
+
18
+ ### 🌟 Highlights
19
+ - **Modern Rendering**: Based on WebGPU for next-gen performance and visual quality.
20
+ - **Data-Driven**: Animation and behavior fully controlled via JSON configuration.
21
+ - **Zero-Dependency Core**: Pure frontend architecture, easy to integrate into any project.
22
+
23
+ ### 🛠 Tech Stack
24
+ - **Core**: TypeScript
25
+ - **Rendering**: Three.js (WebGPU Renderer)
26
+ - **Build**: Vite
27
+
28
+ ### 📦 Installation
29
+
30
+ You can install the SDK directly from GitHub:
31
+
32
+ ```bash
33
+ # Using npm
34
+ npm install github:guoquan/flow-engine
35
+
36
+ # Using pnpm
37
+ pnpm add github:guoquan/flow-engine
38
+ ```
39
+
40
+ To use a specific version (recommended):
41
+
42
+ ```bash
43
+ npm install github:guoquan/flow-engine#v0.1.0
44
+ ```
45
+
46
+ ### 📚 Documentation
47
+ Please visit **[docs/](./docs/)** for the full documentation and API references.
48
+
49
+ ---
50
+
51
+ ## 中文
52
+
53
+ > **"一开口,就服喽。"**
54
+
55
+ **Flow** (包名称:`@guoquan.net/flow-engine`) 是一个高性能、轻量级的 Web 端数字人引擎。它旨在通过简单的 API 驱动,为 Web 应用赋予栩栩如生的 AI 化身交互能力。
56
+
57
+ ### 🌟 项目亮点
58
+ - **现代化渲染**:基于 WebGPU,提供下一代渲染性能与视觉效果。
59
+ - **数据驱动**:动画与行为完全通过 JSON 配置文件控制。
60
+ - **零依赖核心**:纯前端架构,无需后端即可运行,易于集成。
61
+
62
+ ### 🛠 技术栈
63
+ - **核心**: TypeScript
64
+ - **渲染**: Three.js (WebGPU Renderer)
65
+ - **构建**: Vite
66
+
67
+ ### 📦 安装
68
+
69
+ 你可以直接从 GitHub 安装该 SDK:
70
+
71
+ ```bash
72
+ # 使用 npm
73
+ npm install github:guoquan/flow-engine
74
+
75
+ # 使用 pnpm
76
+ pnpm add github:guoquan/flow-engine
77
+ ```
78
+
79
+ 建议安装特定版本以保证稳定性:
80
+
81
+ ```bash
82
+ npm install github:guoquan/flow-engine#v0.1.0
83
+ ```
84
+
85
+ ### 📚 文档索引
86
+ 请访问 **[docs/](./docs/)** 查看完整文档与 API 说明。
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "flow-bot-01",
3
+ "name": "Flow Bot",
4
+ "modelSrc": "model.glb",
5
+ "scale": 1.5,
6
+ "initialPosition": [0, -1, 0],
7
+ "version": "1.0.0"
8
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "Robot Expressive",
3
+ "modelSrc": "model.glb",
4
+ "scale": 0.3,
5
+ "initialPosition": [0, 0, 0],
6
+ "animations": {
7
+ "defaultState": "idle",
8
+ "states": {
9
+ "idle": {
10
+ "clipName": "Idle",
11
+ "loop": true,
12
+ "timeScale": 0.4,
13
+ "fadeDuration": 1.0
14
+ },
15
+ "wave": {
16
+ "clipName": "Wave",
17
+ "loop": false,
18
+ "next": "idle",
19
+ "fadeDuration": 0.5
20
+ },
21
+ "dance": {
22
+ "clipName": "Dance",
23
+ "loop": false,
24
+ "next": "idle",
25
+ "timeScale": 1.2,
26
+ "fadeDuration": 0.5
27
+ },
28
+ "death": {
29
+ "clipName": "Death",
30
+ "loop": false,
31
+ "next": "idle",
32
+ "fadeDuration": 0.8,
33
+ "holdDuration": 3.0
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "Sci-Fi Podium",
3
+ "position": [0, 0, 0],
4
+ "scale": 1.0,
5
+ "animations": {
6
+ "defaultState": "idle",
7
+ "states": {
8
+ "idle": { "clipName": "Idle", "loop": true }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,31 @@
1
+ import { AnimationStateConfig } from '../types';
2
+ import * as THREE from 'three';
3
+ export declare class AnimationController {
4
+ private mixer;
5
+ private clips;
6
+ private states;
7
+ private activeActions;
8
+ private currentState;
9
+ private defaultState;
10
+ constructor(model: THREE.Object3D, animations: THREE.AnimationClip[]);
11
+ /**
12
+ * Initialize with configuration
13
+ */
14
+ init(config: {
15
+ defaultState: string;
16
+ states: Record<string, AnimationStateConfig>;
17
+ }): void;
18
+ /**
19
+ * Update mixer (call this every frame)
20
+ */
21
+ update(delta: number): void;
22
+ /**
23
+ * Play a state
24
+ */
25
+ play(stateName: string, forceReset?: boolean): void;
26
+ private onFinished;
27
+ /**
28
+ * Finds an animation clip by name using exact, case-insensitive, or fuzzy matching.
29
+ */
30
+ private findClip;
31
+ }
@@ -0,0 +1,29 @@
1
+ import { AvatarConfig } from '../types';
2
+ import * as THREE from 'three';
3
+ /**
4
+ * AvatarLoader
5
+ * 负责加载数字人的配置和模型资源
6
+ */
7
+ export declare class AvatarLoader {
8
+ private loader;
9
+ constructor();
10
+ /**
11
+ * 加载数字人
12
+ * @param configUrl 指向 config.json 的 URL
13
+ * @returns Promise<{ model: THREE.Object3D, config: AvatarConfig, animations: THREE.AnimationClip[] }>
14
+ */
15
+ load(configUrl: string): Promise<{
16
+ model: THREE.Object3D;
17
+ config: AvatarConfig;
18
+ animations: THREE.AnimationClip[];
19
+ }>;
20
+ /**
21
+ * 应用配置到模型
22
+ */
23
+ private applyConfig;
24
+ /**
25
+ * 创建一个占位符 Avatar (当模型文件缺失时)
26
+ * 生成一个简单的机器人形状
27
+ */
28
+ private createFallbackAvatar;
29
+ }
@@ -0,0 +1,31 @@
1
+ export declare class FlowEngine {
2
+ private container;
3
+ private scene;
4
+ private camera;
5
+ private renderer;
6
+ private controls;
7
+ private clock;
8
+ private loader;
9
+ private stageLoader;
10
+ private avatarModel;
11
+ private stageModel;
12
+ private animController;
13
+ private stageAnimController;
14
+ constructor(containerId: string);
15
+ private setupLights;
16
+ /**
17
+ * Load an avatar by config URL
18
+ */
19
+ loadAvatar(configUrl: string): Promise<void>;
20
+ /**
21
+ * Load a stage (podium/scene) by config URL
22
+ */
23
+ loadStage(configUrl: string): Promise<void>;
24
+ private onWindowResize;
25
+ isAutoRotate: boolean;
26
+ /**
27
+ * Play a specific action
28
+ */
29
+ playAction(action: string): void;
30
+ private animate;
31
+ }
@@ -0,0 +1,16 @@
1
+ import { StageConfig } from '../types';
2
+ import * as THREE from 'three';
3
+ export declare class StageLoader {
4
+ private loader;
5
+ constructor();
6
+ load(configUrl: string): Promise<{
7
+ model: THREE.Object3D;
8
+ config: StageConfig;
9
+ animations: THREE.AnimationClip[];
10
+ }>;
11
+ private applyConfig;
12
+ /**
13
+ * Generates a cool Sci-Fi Podium if no model is provided
14
+ */
15
+ private createProceduralStage;
16
+ }
@@ -0,0 +1,253 @@
1
+ import * as i from "three";
2
+ import { WebGPURenderer as g } from "three/webgpu";
3
+ import { OrbitControls as y } from "three/examples/jsm/controls/OrbitControls.js";
4
+ import { GLTFLoader as f } from "three/examples/jsm/loaders/GLTFLoader.js";
5
+ class M {
6
+ loader;
7
+ constructor() {
8
+ this.loader = new f();
9
+ }
10
+ /**
11
+ * 加载数字人
12
+ * @param configUrl 指向 config.json 的 URL
13
+ * @returns Promise<{ model: THREE.Object3D, config: AvatarConfig, animations: THREE.AnimationClip[] }>
14
+ */
15
+ async load(t) {
16
+ console.log(`[Flow] Loading avatar config from: ${t}`);
17
+ try {
18
+ const e = await fetch(t);
19
+ if (!e.ok)
20
+ throw new Error(`Failed to load config: ${e.statusText}`);
21
+ const o = await e.json(), n = t.substring(0, t.lastIndexOf("/") + 1) + o.modelSrc;
22
+ console.log(`[Flow] Config loaded. Loading model from: ${n}`);
23
+ let r, s = [];
24
+ try {
25
+ const l = await this.loader.loadAsync(n);
26
+ r = l.scene, s = l.animations || [];
27
+ } catch (l) {
28
+ console.warn(`[Flow] Failed to load 3D model at ${n}. Using fallback placeholder.`, l), r = this.createFallbackAvatar();
29
+ }
30
+ return this.applyConfig(r, o), { model: r, config: o, animations: s };
31
+ } catch (e) {
32
+ throw console.error("[Flow] Error loading avatar:", e), e;
33
+ }
34
+ }
35
+ /**
36
+ * 应用配置到模型
37
+ */
38
+ applyConfig(t, e) {
39
+ e.scale && t.scale.setScalar(e.scale), e.initialPosition && t.position.set(...e.initialPosition), t.traverse((o) => {
40
+ o.isMesh && (o.castShadow = !0, o.receiveShadow = !0);
41
+ });
42
+ }
43
+ /**
44
+ * 创建一个占位符 Avatar (当模型文件缺失时)
45
+ * 生成一个简单的机器人形状
46
+ */
47
+ createFallbackAvatar() {
48
+ const t = new i.Group(), e = new i.MeshStandardMaterial({
49
+ color: 54015,
50
+ roughness: 0.3,
51
+ metalness: 0.8
52
+ }), o = new i.BoxGeometry(0.8, 0.9, 0.8), a = new i.Mesh(o, e);
53
+ a.position.y = 1.5, a.name = "Head", t.add(a);
54
+ const n = new i.SphereGeometry(0.1), r = new i.MeshBasicMaterial({ color: 16777215 }), s = new i.Mesh(n, r);
55
+ s.position.set(-0.2, 1.5, 0.4), t.add(s);
56
+ const l = new i.Mesh(n, r);
57
+ l.position.set(0.2, 1.5, 0.4), t.add(l);
58
+ const d = new i.CylinderGeometry(0.6, 0.4, 1.5, 8), c = new i.Mesh(d, e);
59
+ c.position.y = 0.5, c.name = "Body", t.add(c);
60
+ const h = new i.CapsuleGeometry(0.15, 1), p = new i.Mesh(h, e);
61
+ p.position.set(-0.9, 0.8, 0), p.rotation.z = Math.PI / 4, p.name = "LeftArm", t.add(p);
62
+ const w = new i.Mesh(h, e);
63
+ return w.position.set(0.9, 0.8, 0), w.rotation.z = -Math.PI / 4, w.name = "RightArm", t.add(w), t;
64
+ }
65
+ }
66
+ class S {
67
+ loader;
68
+ constructor() {
69
+ this.loader = new f();
70
+ }
71
+ async load(t) {
72
+ try {
73
+ const e = await fetch(t);
74
+ if (!e.ok) throw new Error("Failed to load stage config");
75
+ const o = await e.json();
76
+ let a, n = [];
77
+ if (o.modelSrc) {
78
+ const s = t.substring(0, t.lastIndexOf("/") + 1) + o.modelSrc, l = await this.loader.loadAsync(s);
79
+ a = l.scene, n = l.animations || [];
80
+ } else
81
+ a = this.createProceduralStage();
82
+ return this.applyConfig(a, o), { model: a, config: o, animations: n };
83
+ } catch (e) {
84
+ throw console.error("[Flow] Stage load error:", e), e;
85
+ }
86
+ }
87
+ applyConfig(t, e) {
88
+ e.scale && t.scale.setScalar(e.scale), e.position && t.position.set(...e.position), e.rotation && t.rotation.set(...e.rotation), t.traverse((o) => {
89
+ o.isMesh && (o.receiveShadow = !0, o.castShadow = !0);
90
+ });
91
+ }
92
+ /**
93
+ * Generates a cool Sci-Fi Podium if no model is provided
94
+ */
95
+ createProceduralStage() {
96
+ const t = new i.Group(), e = new i.CylinderGeometry(2, 2.2, 0.2, 32), o = new i.MeshStandardMaterial({ color: 2236962, roughness: 0.2, metalness: 0.8 }), a = new i.Mesh(e, o);
97
+ a.position.y = -0.1, t.add(a);
98
+ const n = new i.TorusGeometry(1.8, 0.05, 16, 100), r = new i.MeshBasicMaterial({ color: 54015 }), s = new i.Mesh(n, r);
99
+ s.rotation.x = -Math.PI / 2, s.position.y = 0.01, t.add(s);
100
+ const l = new i.BoxGeometry(0.5, 0.05, 0.5);
101
+ for (let d = 0; d < 4; d++) {
102
+ const c = new i.Mesh(l, o), h = d / 4 * Math.PI * 2;
103
+ c.position.set(Math.cos(h) * 2.5, 0, Math.sin(h) * 2.5), c.lookAt(0, 0, 0), t.add(c);
104
+ }
105
+ return t;
106
+ }
107
+ }
108
+ class u {
109
+ mixer;
110
+ clips;
111
+ states = {};
112
+ activeActions = /* @__PURE__ */ new Map();
113
+ currentState = null;
114
+ defaultState = "idle";
115
+ constructor(t, e) {
116
+ this.mixer = new i.AnimationMixer(t), this.clips = e, this.mixer.addEventListener("finished", this.onFinished.bind(this));
117
+ }
118
+ /**
119
+ * Initialize with configuration
120
+ */
121
+ init(t) {
122
+ this.defaultState = t.defaultState, this.states = t.states, Object.entries(this.states).forEach(([e, o]) => {
123
+ this.findClip(o.clipName) || console.warn(`[AnimationController] Clip "${o.clipName}" for state "${e}" not found.`);
124
+ }), this.play(this.defaultState);
125
+ }
126
+ /**
127
+ * Update mixer (call this every frame)
128
+ */
129
+ update(t) {
130
+ this.mixer.update(t);
131
+ }
132
+ /**
133
+ * Play a state
134
+ */
135
+ play(t, e = !1) {
136
+ const o = this.states[t];
137
+ if (!o) {
138
+ console.warn(`[AnimationController] State "${t}" not defined.`);
139
+ return;
140
+ }
141
+ const a = this.findClip(o.clipName);
142
+ if (!a || this.currentState === t && !e) return;
143
+ const n = this.mixer.clipAction(a);
144
+ n.setLoop(o.loop ? i.LoopRepeat : i.LoopOnce, o.loop ? 1 / 0 : 1), n.clampWhenFinished = !o.loop, n.timeScale = o.timeScale ?? 1;
145
+ const r = o.fadeDuration ?? 0.3;
146
+ if (this.currentState) {
147
+ const s = this.activeActions.get(this.currentState);
148
+ s && s !== n ? (s.fadeOut(r), n.reset(), n.fadeIn(r), n.play()) : n.reset().play();
149
+ } else
150
+ n.reset().play();
151
+ this.activeActions.set(t, n), this.currentState = t, console.log(`[Anim] Transition to: ${t} (Loop: ${o.loop})`);
152
+ }
153
+ onFinished(t) {
154
+ if (this.currentState && this.activeActions.get(this.currentState) === t.action) {
155
+ const e = this.states[this.currentState], o = () => {
156
+ e && e.next ? this.play(e.next) : !e.loop && this.currentState !== this.defaultState && this.play(this.defaultState);
157
+ };
158
+ e.holdDuration && e.holdDuration > 0 ? setTimeout(o, e.holdDuration * 1e3) : o();
159
+ }
160
+ }
161
+ /**
162
+ * Finds an animation clip by name using exact, case-insensitive, or fuzzy matching.
163
+ */
164
+ findClip(t) {
165
+ let e = this.clips.find((o) => o.name === t);
166
+ return e || (e = this.clips.find((o) => o.name.toLowerCase() === t.toLowerCase())), e || (e = this.clips.find((o) => o.name.toLowerCase().includes(t.toLowerCase()))), e;
167
+ }
168
+ }
169
+ class L {
170
+ container;
171
+ scene;
172
+ camera;
173
+ renderer;
174
+ controls;
175
+ clock;
176
+ loader;
177
+ stageLoader;
178
+ avatarModel = null;
179
+ stageModel = null;
180
+ animController = null;
181
+ stageAnimController = null;
182
+ constructor(t) {
183
+ const e = document.getElementById(t);
184
+ if (!e) throw new Error(`Container #${t} not found`);
185
+ this.container = e, this.clock = new i.Clock(), this.loader = new M(), this.stageLoader = new S(), this.scene = new i.Scene(), this.scene.background = new i.Color(1710618), this.scene.fog = new i.Fog(1710618, 10, 50), this.camera = new i.PerspectiveCamera(
186
+ 45,
187
+ window.innerWidth / window.innerHeight,
188
+ 0.1,
189
+ 100
190
+ ), this.camera.position.set(0, 1.5, 5), this.renderer = new g({ antialias: !0, alpha: !0 }), this.renderer.setSize(window.innerWidth, window.innerHeight), this.renderer.setPixelRatio(window.devicePixelRatio), this.container.appendChild(this.renderer.domElement), this.setupLights(), this.controls = new y(this.camera, this.renderer.domElement), this.controls.enableDamping = !0, this.controls.target.set(0, 1, 0), window.addEventListener("resize", this.onWindowResize.bind(this)), this.renderer.setAnimationLoop(this.animate.bind(this));
191
+ }
192
+ setupLights() {
193
+ const t = new i.AmbientLight(16777215, 0.6);
194
+ this.scene.add(t);
195
+ const e = new i.DirectionalLight(16777215, 1);
196
+ e.position.set(5, 10, 7), e.castShadow = !0, this.scene.add(e);
197
+ const o = new i.SpotLight(54015, 5);
198
+ o.position.set(-5, 5, -5), o.lookAt(0, 1, 0), this.scene.add(o);
199
+ }
200
+ /**
201
+ * Load an avatar by config URL
202
+ */
203
+ async loadAvatar(t) {
204
+ this.avatarModel && this.scene.remove(this.avatarModel);
205
+ const { model: e, config: o, animations: a } = await this.loader.load(t);
206
+ if (this.avatarModel = e, this.scene.add(this.avatarModel), a.length > 0) {
207
+ this.animController = new u(this.avatarModel, a);
208
+ const n = o.animations || {
209
+ defaultState: "idle",
210
+ states: {
211
+ idle: { clipName: "Idle", loop: !0 },
212
+ wave: { clipName: "Wave", loop: !1, next: "idle" },
213
+ dance: { clipName: "Dance", loop: !1, next: "idle" },
214
+ bow: { clipName: "Bow", loop: !1, next: "idle" },
215
+ walk: { clipName: "Walking", loop: !0 }
216
+ }
217
+ };
218
+ this.animController.init(n);
219
+ }
220
+ console.log(`[Flow] Avatar "${o.name}" loaded successfully.`);
221
+ }
222
+ /**
223
+ * Load a stage (podium/scene) by config URL
224
+ */
225
+ async loadStage(t) {
226
+ this.stageModel && this.scene.remove(this.stageModel);
227
+ const { model: e, config: o, animations: a } = await this.stageLoader.load(t);
228
+ this.stageModel = e, this.scene.add(this.stageModel), a.length > 0 && o.animations && (this.stageAnimController = new u(this.stageModel, a), this.stageAnimController.init(o.animations)), console.log(`[Flow] Stage "${o.name}" loaded successfully.`);
229
+ }
230
+ onWindowResize() {
231
+ this.camera.aspect = window.innerWidth / window.innerHeight, this.camera.updateProjectionMatrix(), this.renderer.setSize(window.innerWidth, window.innerHeight);
232
+ }
233
+ // State
234
+ isAutoRotate = !1;
235
+ /**
236
+ * Play a specific action
237
+ */
238
+ playAction(t) {
239
+ if (console.log(`[FlowEngine] Playing action: ${t}`), this.animController) {
240
+ this.animController.play(t.toLowerCase());
241
+ return;
242
+ }
243
+ }
244
+ animate(t) {
245
+ const e = this.clock.getDelta();
246
+ this.avatarModel && this.animController && this.animController.update(e), this.stageModel && this.stageAnimController && this.stageAnimController.update(e), this.controls.autoRotate = this.isAutoRotate, this.controls.update(), this.renderer.render(this.scene, this.camera);
247
+ }
248
+ }
249
+ export {
250
+ u as AnimationController,
251
+ M as AvatarLoader,
252
+ L as FlowEngine
253
+ };
@@ -0,0 +1 @@
1
+ (function(l,p){typeof exports=="object"&&typeof module<"u"?p(exports,require("three"),require("three/webgpu"),require("three/examples/jsm/controls/OrbitControls.js"),require("three/examples/jsm/loaders/GLTFLoader.js")):typeof define=="function"&&define.amd?define(["exports","three","three/webgpu","three/examples/jsm/controls/OrbitControls.js","three/examples/jsm/loaders/GLTFLoader.js"],p):(l=typeof globalThis<"u"?globalThis:l||self,p(l.Flow={},l.THREE,l.THREE,l.OrbitControls_js,l.GLTFLoader_js))})(this,(function(l,p,S,C,y){"use strict";function A(d){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(d){for(const e in d)if(e!=="default"){const o=Object.getOwnPropertyDescriptor(d,e);Object.defineProperty(t,e,o.get?o:{enumerable:!0,get:()=>d[e]})}}return t.default=d,Object.freeze(t)}const i=A(p);class M{loader;constructor(){this.loader=new y.GLTFLoader}async load(t){console.log(`[Flow] Loading avatar config from: ${t}`);try{const e=await fetch(t);if(!e.ok)throw new Error(`Failed to load config: ${e.statusText}`);const o=await e.json(),n=t.substring(0,t.lastIndexOf("/")+1)+o.modelSrc;console.log(`[Flow] Config loaded. Loading model from: ${n}`);let r,a=[];try{const c=await this.loader.loadAsync(n);r=c.scene,a=c.animations||[]}catch(c){console.warn(`[Flow] Failed to load 3D model at ${n}. Using fallback placeholder.`,c),r=this.createFallbackAvatar()}return this.applyConfig(r,o),{model:r,config:o,animations:a}}catch(e){throw console.error("[Flow] Error loading avatar:",e),e}}applyConfig(t,e){e.scale&&t.scale.setScalar(e.scale),e.initialPosition&&t.position.set(...e.initialPosition),t.traverse(o=>{o.isMesh&&(o.castShadow=!0,o.receiveShadow=!0)})}createFallbackAvatar(){const t=new i.Group,e=new i.MeshStandardMaterial({color:54015,roughness:.3,metalness:.8}),o=new i.BoxGeometry(.8,.9,.8),s=new i.Mesh(o,e);s.position.y=1.5,s.name="Head",t.add(s);const n=new i.SphereGeometry(.1),r=new i.MeshBasicMaterial({color:16777215}),a=new i.Mesh(n,r);a.position.set(-.2,1.5,.4),t.add(a);const c=new i.Mesh(n,r);c.position.set(.2,1.5,.4),t.add(c);const w=new i.CylinderGeometry(.6,.4,1.5,8),h=new i.Mesh(w,e);h.position.y=.5,h.name="Body",t.add(h);const m=new i.CapsuleGeometry(.15,1),u=new i.Mesh(m,e);u.position.set(-.9,.8,0),u.rotation.z=Math.PI/4,u.name="LeftArm",t.add(u);const f=new i.Mesh(m,e);return f.position.set(.9,.8,0),f.rotation.z=-Math.PI/4,f.name="RightArm",t.add(f),t}}class L{loader;constructor(){this.loader=new y.GLTFLoader}async load(t){try{const e=await fetch(t);if(!e.ok)throw new Error("Failed to load stage config");const o=await e.json();let s,n=[];if(o.modelSrc){const a=t.substring(0,t.lastIndexOf("/")+1)+o.modelSrc,c=await this.loader.loadAsync(a);s=c.scene,n=c.animations||[]}else s=this.createProceduralStage();return this.applyConfig(s,o),{model:s,config:o,animations:n}}catch(e){throw console.error("[Flow] Stage load error:",e),e}}applyConfig(t,e){e.scale&&t.scale.setScalar(e.scale),e.position&&t.position.set(...e.position),e.rotation&&t.rotation.set(...e.rotation),t.traverse(o=>{o.isMesh&&(o.receiveShadow=!0,o.castShadow=!0)})}createProceduralStage(){const t=new i.Group,e=new i.CylinderGeometry(2,2.2,.2,32),o=new i.MeshStandardMaterial({color:2236962,roughness:.2,metalness:.8}),s=new i.Mesh(e,o);s.position.y=-.1,t.add(s);const n=new i.TorusGeometry(1.8,.05,16,100),r=new i.MeshBasicMaterial({color:54015}),a=new i.Mesh(n,r);a.rotation.x=-Math.PI/2,a.position.y=.01,t.add(a);const c=new i.BoxGeometry(.5,.05,.5);for(let w=0;w<4;w++){const h=new i.Mesh(c,o),m=w/4*Math.PI*2;h.position.set(Math.cos(m)*2.5,0,Math.sin(m)*2.5),h.lookAt(0,0,0),t.add(h)}return t}}class g{mixer;clips;states={};activeActions=new Map;currentState=null;defaultState="idle";constructor(t,e){this.mixer=new i.AnimationMixer(t),this.clips=e,this.mixer.addEventListener("finished",this.onFinished.bind(this))}init(t){this.defaultState=t.defaultState,this.states=t.states,Object.entries(this.states).forEach(([e,o])=>{this.findClip(o.clipName)||console.warn(`[AnimationController] Clip "${o.clipName}" for state "${e}" not found.`)}),this.play(this.defaultState)}update(t){this.mixer.update(t)}play(t,e=!1){const o=this.states[t];if(!o){console.warn(`[AnimationController] State "${t}" not defined.`);return}const s=this.findClip(o.clipName);if(!s||this.currentState===t&&!e)return;const n=this.mixer.clipAction(s);n.setLoop(o.loop?i.LoopRepeat:i.LoopOnce,o.loop?1/0:1),n.clampWhenFinished=!o.loop,n.timeScale=o.timeScale??1;const r=o.fadeDuration??.3;if(this.currentState){const a=this.activeActions.get(this.currentState);a&&a!==n?(a.fadeOut(r),n.reset(),n.fadeIn(r),n.play()):n.reset().play()}else n.reset().play();this.activeActions.set(t,n),this.currentState=t,console.log(`[Anim] Transition to: ${t} (Loop: ${o.loop})`)}onFinished(t){if(this.currentState&&this.activeActions.get(this.currentState)===t.action){const e=this.states[this.currentState],o=()=>{e&&e.next?this.play(e.next):!e.loop&&this.currentState!==this.defaultState&&this.play(this.defaultState)};e.holdDuration&&e.holdDuration>0?setTimeout(o,e.holdDuration*1e3):o()}}findClip(t){let e=this.clips.find(o=>o.name===t);return e||(e=this.clips.find(o=>o.name.toLowerCase()===t.toLowerCase())),e||(e=this.clips.find(o=>o.name.toLowerCase().includes(t.toLowerCase()))),e}}class b{container;scene;camera;renderer;controls;clock;loader;stageLoader;avatarModel=null;stageModel=null;animController=null;stageAnimController=null;constructor(t){const e=document.getElementById(t);if(!e)throw new Error(`Container #${t} not found`);this.container=e,this.clock=new i.Clock,this.loader=new M,this.stageLoader=new L,this.scene=new i.Scene,this.scene.background=new i.Color(1710618),this.scene.fog=new i.Fog(1710618,10,50),this.camera=new i.PerspectiveCamera(45,window.innerWidth/window.innerHeight,.1,100),this.camera.position.set(0,1.5,5),this.renderer=new S.WebGPURenderer({antialias:!0,alpha:!0}),this.renderer.setSize(window.innerWidth,window.innerHeight),this.renderer.setPixelRatio(window.devicePixelRatio),this.container.appendChild(this.renderer.domElement),this.setupLights(),this.controls=new C.OrbitControls(this.camera,this.renderer.domElement),this.controls.enableDamping=!0,this.controls.target.set(0,1,0),window.addEventListener("resize",this.onWindowResize.bind(this)),this.renderer.setAnimationLoop(this.animate.bind(this))}setupLights(){const t=new i.AmbientLight(16777215,.6);this.scene.add(t);const e=new i.DirectionalLight(16777215,1);e.position.set(5,10,7),e.castShadow=!0,this.scene.add(e);const o=new i.SpotLight(54015,5);o.position.set(-5,5,-5),o.lookAt(0,1,0),this.scene.add(o)}async loadAvatar(t){this.avatarModel&&this.scene.remove(this.avatarModel);const{model:e,config:o,animations:s}=await this.loader.load(t);if(this.avatarModel=e,this.scene.add(this.avatarModel),s.length>0){this.animController=new g(this.avatarModel,s);const n=o.animations||{defaultState:"idle",states:{idle:{clipName:"Idle",loop:!0},wave:{clipName:"Wave",loop:!1,next:"idle"},dance:{clipName:"Dance",loop:!1,next:"idle"},bow:{clipName:"Bow",loop:!1,next:"idle"},walk:{clipName:"Walking",loop:!0}}};this.animController.init(n)}console.log(`[Flow] Avatar "${o.name}" loaded successfully.`)}async loadStage(t){this.stageModel&&this.scene.remove(this.stageModel);const{model:e,config:o,animations:s}=await this.stageLoader.load(t);this.stageModel=e,this.scene.add(this.stageModel),s.length>0&&o.animations&&(this.stageAnimController=new g(this.stageModel,s),this.stageAnimController.init(o.animations)),console.log(`[Flow] Stage "${o.name}" loaded successfully.`)}onWindowResize(){this.camera.aspect=window.innerWidth/window.innerHeight,this.camera.updateProjectionMatrix(),this.renderer.setSize(window.innerWidth,window.innerHeight)}isAutoRotate=!1;playAction(t){if(console.log(`[FlowEngine] Playing action: ${t}`),this.animController){this.animController.play(t.toLowerCase());return}}animate(t){const e=this.clock.getDelta();this.avatarModel&&this.animController&&this.animController.update(e),this.stageModel&&this.stageAnimController&&this.stageAnimController.update(e),this.controls.autoRotate=this.isAutoRotate,this.controls.update(),this.renderer.render(this.scene,this.camera)}}l.AnimationController=g,l.AvatarLoader=M,l.FlowEngine=b,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,4 @@
1
+ export { FlowEngine } from './core/FlowEngine';
2
+ export { AvatarLoader } from './core/AvatarLoader';
3
+ export { AnimationController } from './core/AnimationController';
4
+ export type { AvatarConfig, AnimationStateConfig } from './types';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Avatar Configuration Interface
3
+ * 定义数字人的元数据结构,对应资源包中的 config.json
4
+ */
5
+ export interface AvatarConfig {
6
+ name: string;
7
+ modelSrc: string;
8
+ scale?: number;
9
+ initialPosition?: [number, number, number];
10
+ /** Animation State Machine Configuration */
11
+ animations?: {
12
+ defaultState: string;
13
+ states: Record<string, AnimationStateConfig>;
14
+ };
15
+ }
16
+ export interface AnimationStateConfig {
17
+ /** The exact name of the clip in the GLB (fuzzy matching supported if not found) */
18
+ clipName: string;
19
+ /** Whether the animation should loop */
20
+ loop?: boolean;
21
+ /** The state to transition to after this one finishes (for non-looping) */
22
+ next?: string;
23
+ /** Cross-fade duration in seconds (default: 0.3) */
24
+ fadeDuration?: number;
25
+ /** Time scale (speed), default 1.0 */
26
+ timeScale?: number;
27
+ /** Duration to hold the last frame before transitioning (seconds) */
28
+ holdDuration?: number;
29
+ }
30
+ export interface StageConfig {
31
+ name: string;
32
+ modelSrc?: string;
33
+ scale?: number;
34
+ position?: [number, number, number];
35
+ rotation?: [number, number, number];
36
+ /** Animation logic for the stage (e.g. rotating, rising) */
37
+ animations?: {
38
+ defaultState: string;
39
+ states: Record<string, AnimationStateConfig>;
40
+ };
41
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@guoquan.net/flow-engine",
3
+ "version": "0.1.2",
4
+ "description": "A high-performance, WebGPU-based digital human engine.",
5
+ "type": "module",
6
+ "main": "./dist/flow.umd.js",
7
+ "module": "./dist/flow.es.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "scripts": {
16
+ "dev": "vite",
17
+ "build": "tsc && vite build",
18
+ "build:demo": "tsc && cross-env BUILD_DEMO=true vite build",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:coverage": "vitest run --coverage",
22
+ "preview": "vite preview",
23
+ "prepare": "npm run build"
24
+ },
25
+ "peerDependencies": {
26
+ "three": ">=0.170.0"
27
+ },
28
+ "devDependencies": {
29
+ "@codecov/vite-plugin": "^1.9.1",
30
+ "@types/node": "^25.0.3",
31
+ "@types/three": "^0.182.0",
32
+ "@vitest/coverage-v8": "^4.0.16",
33
+ "cross-env": "^10.1.0",
34
+ "jsdom": "^27.4.0",
35
+ "three": "^0.182.0",
36
+ "typescript": "~5.9.3",
37
+ "vite": "^7.2.4",
38
+ "vite-plugin-dts": "^4.5.4",
39
+ "vitest": "^4.0.16"
40
+ }
41
+ }