loomlarge 0.1.0 → 0.1.4

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.
Files changed (2) hide show
  1. package/README.md +672 -539
  2. package/package.json +6 -1
package/README.md CHANGED
@@ -1,714 +1,847 @@
1
- ![Latticework by Lovelace LOL](LoomLarge.png)
1
+ # LoomLarge
2
2
 
3
- # LoomLarge (Latticework Animation Platform)
3
+ A FACS-based morph and bone mapping library for controlling high-definition 3D characters in Three.js.
4
4
 
5
- **LoomLarge** is a next-generation interactive 3D character animation platform built on **Latticework**, featuring reactive state management with XState, facial animation control via ARKit FACS (Facial Action Coding System), and modular agency-based architecture for lip-sync, eye/head tracking, prosodic expression, and conversational AI.
5
+ LoomLarge provides pre-built mappings that connect [Facial Action Coding System (FACS)](https://en.wikipedia.org/wiki/Facial_Action_Coding_System) Action Units to the morph targets and bone transforms found in Character Creator 4 (CC4) characters. Instead of manually figuring out which blend shapes correspond to which facial movements, you can simply say `setAU(12, 0.8)` and the library handles the rest.
6
6
 
7
7
  ---
8
8
 
9
9
  ## Table of Contents
10
- 1. [Quick Start](#quick-start)
11
- 2. [Core Concepts](#core-concepts)
12
- - [Latticework Architecture](#latticework-architecture)
13
- - [Composite Rotation System](#composite-rotation-system)
14
- - [XState & Reactive Services](#xstate--reactive-services)
15
- 3. [Installation](#installation)
16
- 4. [Project Structure](#project-structure)
17
- 5. [How It Works](#how-it-works)
18
- - [Animation Service](#animation-service)
19
- - [Eye & Head Tracking](#eye--head-tracking)
20
- - [Lip-Sync Agency](#lip-sync-agency)
21
- - [Prosodic Expression](#prosodic-expression)
22
- 6. [Modules](#modules)
23
- 7. [Development](#development)
24
- 8. [Deployment](#deployment)
25
- 9. [License & Acknowledgments](#license--acknowledgments)
10
+
11
+ 1. [Installation & Setup](#1-installation--setup)
12
+ 2. [Using Presets](#2-using-presets)
13
+ 3. [Extending & Custom Presets](#3-extending--custom-presets)
14
+ 4. [Action Unit Control](#4-action-unit-control)
15
+ 5. [Mix Weight System](#5-mix-weight-system)
16
+ 6. [Composite Rotation System](#6-composite-rotation-system)
17
+ 7. [Continuum Pairs](#7-continuum-pairs)
18
+ 8. [Direct Morph Control](#8-direct-morph-control)
19
+ 9. [Viseme System](#9-viseme-system)
20
+ 10. [Transition System](#10-transition-system)
21
+ 11. [Playback & State Control](#11-playback--state-control)
22
+ 12. [Hair Physics](#12-hair-physics)
26
23
 
27
24
  ---
28
25
 
29
- ## Quick Start
26
+ ## 1. Installation & Setup
27
+
28
+ ### Install the package
30
29
 
31
30
  ```bash
32
- # Clone the repository
33
- git clone https://github.com/meekmachine/LoomLarge.git
34
- cd LoomLarge
31
+ npm install loomlarge
32
+ ```
35
33
 
36
- # Install dependencies
37
- yarn install
34
+ ### Peer dependency
38
35
 
39
- # Start development server (Vite)
40
- yarn dev
36
+ LoomLarge requires Three.js as a peer dependency:
37
+
38
+ ```bash
39
+ npm install three
40
+ ```
41
41
 
42
- # Build for production
43
- yarn build
42
+ ### Basic setup
44
43
 
45
- # Deploy to GitHub Pages
46
- yarn deploy
44
+ ```typescript
45
+ import * as THREE from 'three';
46
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
47
+ import { LoomLargeThree, collectMorphMeshes, CC4_PRESET } from 'loomlarge';
48
+
49
+ // 1. Create the LoomLarge controller with a preset
50
+ const loom = new LoomLargeThree({ auMappings: CC4_PRESET });
51
+
52
+ // 2. Set up your Three.js scene
53
+ const scene = new THREE.Scene();
54
+ const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100);
55
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
56
+ renderer.setSize(window.innerWidth, window.innerHeight);
57
+ document.body.appendChild(renderer.domElement);
58
+
59
+ // 3. Load your character model
60
+ const loader = new GLTFLoader();
61
+ loader.load('/character.glb', (gltf) => {
62
+ scene.add(gltf.scene);
63
+
64
+ // 4. Collect all meshes that have morph targets
65
+ const meshes = collectMorphMeshes(gltf.scene);
66
+
67
+ // 5. Initialize LoomLarge with the meshes and model
68
+ loom.onReady({ meshes, model: gltf.scene });
69
+ });
70
+
71
+ // 6. In your animation loop, call loom.update(deltaSeconds)
72
+ // This drives all transitions and animations
47
73
  ```
48
74
 
49
- The development server will start at `http://localhost:5173` with hot module replacement enabled.
75
+ ### Quick start examples
50
76
 
51
- ---
77
+ Once your character is loaded, you can control facial expressions immediately:
52
78
 
53
- ## Core Concepts
79
+ ```typescript
80
+ // Make the character smile
81
+ loom.setAU(12, 0.8);
54
82
 
55
- ### Latticework Architecture
83
+ // Raise eyebrows
84
+ loom.setAU(1, 0.6);
85
+ loom.setAU(2, 0.6);
56
86
 
57
- - **Agency-Based Design**: Independent services (agencies) handle specialized tasks:
58
- - **Animation Agency**: Core snippet scheduling and playback
59
- - **Lip-Sync Agency**: Phoneme prediction and viseme animation
60
- - **Prosodic Expression Agency**: Emotional head gestures and speech timing
61
- - **Eye/Head Tracking Agency**: Gaze control with mouse, webcam, or manual modes
62
- - **Conversation Agency**: Multi-modal conversational AI orchestration
87
+ // Blink
88
+ loom.setAU(45, 1.0);
63
89
 
64
- - **Immutable State**: Reactive state management ensures predictable updates and time-travel debugging
65
- - **XState Machines**: Declarative state machines replace callback hell and ad-hoc timers
90
+ // Open jaw
91
+ loom.setAU(26, 0.5);
66
92
 
67
- ### Composite Rotation System
93
+ // Turn head left
94
+ loom.setAU(51, 0.4);
68
95
 
69
- The **Composite Rotation System** in EngineThree allows smooth blending of blendshapes (morphs) and bone rotations:
96
+ // Look up
97
+ loom.setAU(63, 0.6);
98
+ ```
70
99
 
71
- - **Continuum Values** (-1 to +1): Single value controls paired AUs (e.g., Head Left ↔ Right)
72
- - **Mix Weights** (0 to 1): Blend between 100% morph (0) and 100% bone (1)
73
- - **Unified Rotation State**: Prevents axis conflicts when multiple systems control the same bones
100
+ Animate smoothly with transitions:
74
101
 
75
- Example:
76
102
  ```typescript
77
- // Eyes: -1 (look left) to +1 (look right)
78
- engine.applyEyeComposite(yaw, pitch);
103
+ // Smile over 200ms
104
+ await loom.transitionAU(12, 0.8, 200).promise;
79
105
 
80
- // Head: yaw/pitch/roll with mix control
81
- engine.applyHeadComposite(yaw, pitch, roll);
106
+ // Then fade back to neutral
107
+ await loom.transitionAU(12, 0, 300).promise;
82
108
  ```
83
109
 
84
- ### XState & Reactive Services
110
+ ### The `collectMorphMeshes` helper
111
+
112
+ This utility function traverses a Three.js scene and returns all meshes that have `morphTargetInfluences` (i.e., blend shapes). It's the recommended way to gather meshes for LoomLarge:
85
113
 
86
- - **XState 5.x**: Modern state machines with TypeScript support
87
- - **Service Pattern**: Each agency exposes a service with start/stop/update lifecycle
88
- - **Global Context**: Services registered in `ModulesContext` for cross-component access
114
+ ```typescript
115
+ import { collectMorphMeshes } from 'loomlarge';
116
+
117
+ const meshes = collectMorphMeshes(gltf.scene);
118
+ // Returns: Array of THREE.Mesh objects with morph targets
119
+ ```
89
120
 
90
121
  ---
91
122
 
92
- ## Installation
123
+ ## 2. Using Presets
93
124
 
94
- ### Prerequisites
125
+ Presets define how FACS Action Units map to your character's morph targets and bones. LoomLarge ships with `CC4_PRESET` for Character Creator 4 characters.
95
126
 
96
- - **Node.js** 18+ (LTS recommended)
97
- - **Yarn** 1.22+ or npm 8+
98
- - Modern browser with WebGL 2.0 support
127
+ ### What's in a preset?
99
128
 
100
- ### Setup Steps
129
+ ```typescript
130
+ import { CC4_PRESET } from 'loomlarge';
131
+
132
+ // CC4_PRESET contains:
133
+ {
134
+ auToMorphs: {
135
+ // AU number → array of morph target names
136
+ 1: ['Brow_Raise_Inner_L', 'Brow_Raise_Inner_R'],
137
+ 12: ['Mouth_Smile_L', 'Mouth_Smile_R'],
138
+ 45: ['Eye_Blink_L', 'Eye_Blink_R'],
139
+ // ... 87 AUs total
140
+ },
141
+
142
+ auToBones: {
143
+ // AU number → array of bone bindings
144
+ 51: [{ node: 'HEAD', channel: 'ry', scale: -1, maxDegrees: 30 }],
145
+ 61: [{ node: 'EYE_L', channel: 'rz', scale: 1, maxDegrees: 25 }],
146
+ // ... 32 bone bindings
147
+ },
148
+
149
+ boneNodes: {
150
+ // Logical bone name → actual node name in skeleton
151
+ 'HEAD': 'CC_Base_Head',
152
+ 'JAW': 'CC_Base_JawRoot',
153
+ 'EYE_L': 'CC_Base_L_Eye',
154
+ 'EYE_R': 'CC_Base_R_Eye',
155
+ 'TONGUE': 'CC_Base_Tongue01',
156
+ },
157
+
158
+ visemeKeys: [
159
+ // 15 viseme morph names for lip-sync
160
+ 'V_EE', 'V_Er', 'V_IH', 'V_Ah', 'V_Oh',
161
+ 'V_W_OO', 'V_S_Z', 'V_Ch_J', 'V_F_V', 'V_TH',
162
+ 'V_T_L_D_N', 'V_B_M_P', 'V_K_G_H_NG', 'V_AE', 'V_R'
163
+ ],
101
164
 
102
- 1. **Clone and install**:
103
- ```bash
104
- git clone https://github.com/meekmachine/LoomLarge.git
105
- cd LoomLarge
106
- yarn install
107
- ```
165
+ morphToMesh: {
166
+ // Routes morph categories to specific meshes
167
+ 'face': ['CC_Base_Body'],
168
+ 'tongue': ['CC_Base_Tongue'],
169
+ 'eye': ['CC_Base_EyeOcclusion_L', 'CC_Base_EyeOcclusion_R'],
170
+ },
171
+
172
+ auMixDefaults: {
173
+ // Default morph/bone blend weights (0 = morph, 1 = bone)
174
+ 26: 0.5, // Jaw drop: 50% morph, 50% bone
175
+ 51: 0.7, // Head turn: 70% bone
176
+ },
177
+
178
+ auInfo: {
179
+ // Metadata about each AU
180
+ '12': {
181
+ name: 'Lip Corner Puller',
182
+ muscularBasis: 'zygomaticus major',
183
+ faceArea: 'Lower',
184
+ facePart: 'Mouth',
185
+ },
186
+ // ...
187
+ }
188
+ }
189
+ ```
108
190
 
109
- 2. **Add your 3D model**:
110
- - Place GLB file in `public/characters/`
111
- - Update model path in `src/App.tsx`:
112
- ```typescript
113
- const glbSrc = import.meta.env.BASE_URL + "characters/your-model.glb";
114
- ```
191
+ ### Passing a preset to LoomLarge
115
192
 
116
- 3. **Configure API keys** (optional, for AI modules):
117
- - Create `.env.local`:
118
- ```
119
- VITE_ANTHROPIC_API_KEY=sk-ant-...
120
- ```
193
+ ```typescript
194
+ import { LoomLargeThree, CC4_PRESET } from 'loomlarge';
121
195
 
122
- 4. **Start development**:
123
- ```bash
124
- yarn dev
125
- ```
196
+ const loom = new LoomLargeThree({ auMappings: CC4_PRESET });
197
+ ```
126
198
 
127
199
  ---
128
200
 
129
- ## Project Structure
130
-
131
- ```
132
- LoomLarge/
133
- ├── README.md # This file
134
- ├── package.json
135
- ├── vite.config.ts # Vite bundler config
136
- ├── public/
137
- │ ├── characters/ # GLB 3D models
138
- │ ├── animations/ # Pre-baked animation JSON
139
- │ ├── models/ # ML models (face-api.js)
140
- │ └── skyboxes/ # Environment maps
141
- ├── src/
142
- │ ├── App.tsx # Main React app entry
143
- │ ├── main.tsx # Vite entry point
144
- │ ├── engine/
145
- │ │ ├── EngineThree.ts # Three.js engine (AU/morph control)
146
- │ │ ├── EngineWind.ts # Wind physics for hair/cloth
147
- │ │ └── arkit/
148
- │ │ └── shapeDict.ts # ARKit FACS AU → morph mappings
149
- │ ├── latticework/ # Core agencies
150
- │ │ ├── animation/ # Animation scheduler (XState)
151
- │ │ ├── lipsync/ # Lip-sync phoneme predictor
152
- │ │ ├── prosodic/ # Prosodic expression (head gestures)
153
- │ │ ├── eyeHeadTracking/ # Eye/head tracking service
154
- │ │ ├── conversation/ # Conversational AI orchestration
155
- │ │ └── transcription/ # Speech-to-text services
156
- │ ├── components/
157
- │ │ ├── au/ # AU control UI components
158
- │ │ │ ├── AUSection.tsx # AU sliders
159
- │ │ │ ├── VisemeSection.tsx # Viseme controls
160
- │ │ │ ├── EyeHeadTrackingSection.tsx # Eye/head tracking UI
161
- │ │ │ └── ContinuumSlider.tsx # Bidirectional AU slider
162
- │ │ ├── SliderDrawer.tsx # Main dockable UI drawer
163
- │ │ ├── PlaybackControls.tsx # Animation playback controls
164
- │ │ ├── CurveEditor.tsx # Visual curve editor
165
- │ │ └── ModulesMenu.tsx # Module activation UI
166
- │ ├── modules/ # Pluggable modules
167
- │ │ ├── aiChat/ # AI chat module (Anthropic)
168
- │ │ ├── frenchQuiz/ # French quiz demo module
169
- │ │ └── config.ts # Module registry
170
- │ ├── context/
171
- │ │ ├── threeContext.tsx # Global engine/animation context
172
- │ │ └── ModulesContext.tsx # Module state management
173
- │ ├── hooks/
174
- │ │ └── useWebcamEyeTracking.ts # Webcam face tracking hook
175
- │ ├── scenes/
176
- │ │ └── CharacterGLBScene.tsx # Three.js React scene
177
- │ └── utils/
178
- │ └── animationLoader.ts # Load animation JSON files
179
- └── docs/ # Documentation
180
- ├── QUICK_START.md
181
- ├── LIPSYNC_COMPLETE_GUIDE.md
182
- ├── BACKEND_INTEGRATION.md
183
- └── DEPLOYMENT.md
201
+ ## 3. Extending & Custom Presets
202
+
203
+ ### Extending an existing preset
204
+
205
+ Use spread syntax to override specific mappings while keeping the rest:
206
+
207
+ ```typescript
208
+ import { CC4_PRESET } from 'loomlarge';
209
+
210
+ const MY_PRESET = {
211
+ ...CC4_PRESET,
212
+
213
+ // Override AU12 (smile) with custom morph names
214
+ auToMorphs: {
215
+ ...CC4_PRESET.auToMorphs,
216
+ 12: ['MySmile_Left', 'MySmile_Right'],
217
+ },
218
+
219
+ // Add a new bone binding
220
+ auToBones: {
221
+ ...CC4_PRESET.auToBones,
222
+ 99: [{ node: 'CUSTOM_BONE', channel: 'ry', scale: 1, maxDegrees: 45 }],
223
+ },
224
+
225
+ // Update bone node paths
226
+ boneNodes: {
227
+ ...CC4_PRESET.boneNodes,
228
+ 'CUSTOM_BONE': 'MyRig_CustomBone',
229
+ },
230
+ };
231
+
232
+ const loom = new LoomLargeThree({ auMappings: MY_PRESET });
233
+ ```
234
+
235
+ ### Creating a preset from scratch
236
+
237
+ ```typescript
238
+ import { AUMappingConfig } from 'loomlarge';
239
+
240
+ const CUSTOM_PRESET: AUMappingConfig = {
241
+ auToMorphs: {
242
+ 1: ['brow_inner_up_L', 'brow_inner_up_R'],
243
+ 2: ['brow_outer_up_L', 'brow_outer_up_R'],
244
+ 12: ['mouth_smile_L', 'mouth_smile_R'],
245
+ 45: ['eye_blink_L', 'eye_blink_R'],
246
+ },
247
+
248
+ auToBones: {
249
+ 51: [{ node: 'HEAD', channel: 'ry', scale: -1, maxDegrees: 30 }],
250
+ 52: [{ node: 'HEAD', channel: 'ry', scale: 1, maxDegrees: 30 }],
251
+ },
252
+
253
+ boneNodes: {
254
+ 'HEAD': 'head_bone',
255
+ 'JAW': 'jaw_bone',
256
+ },
257
+
258
+ visemeKeys: ['aa', 'ee', 'ih', 'oh', 'oo'],
259
+
260
+ morphToMesh: {
261
+ 'face': ['body_mesh'],
262
+ },
263
+ };
264
+ ```
265
+
266
+ ### Changing presets at runtime
267
+
268
+ ```typescript
269
+ // Switch to a different preset
270
+ loom.setAUMappings(ANOTHER_PRESET);
271
+
272
+ // Get current mappings
273
+ const current = loom.getAUMappings();
184
274
  ```
185
275
 
186
276
  ---
187
277
 
188
- ## How It Works
189
-
190
- ### Animation Service
191
-
192
- The **Animation Service** (`latticework/animation/animationService.ts`) is the core scheduler:
193
-
194
- 1. **Load Snippets**: JSON files defining AU/morph keyframe curves
195
- ```typescript
196
- const anim = createAnimationService(host);
197
- anim.loadSnippet('smile', smileSnippetJSON);
198
- ```
199
-
200
- 2. **Schedule Playback**: Queue snippets with priority and duration
201
- ```typescript
202
- anim.schedule('smile', {
203
- duration: 1000,
204
- priority: 10,
205
- loop: false
206
- });
207
- ```
208
-
209
- 3. **XState Machine**: Manages snippet lifecycle (`idle` `playing` → `paused`)
210
- - Driven by central frame loop (`threeContext.tsx`)
211
- - Handles overlapping snippets with priority-based blending
212
-
213
- 4. **Host Interface**: Abstraction layer for AU/morph application
214
- ```typescript
215
- const host = {
216
- applyAU: (id, value) => engine.setAU(id, value),
217
- setMorph: (key, value) => engine.setMorph(key, value),
218
- transitionAU: (id, value, duration) => engine.transitionAU(id, value, duration)
219
- };
220
- ```
221
-
222
- ### Eye & Head Tracking
223
-
224
- The **Eye/Head Tracking Service** (`latticework/eyeHeadTracking/eyeHeadTrackingService.ts`) provides three tracking modes:
225
-
226
- 1. **Manual Mode**: Direct slider control of gaze direction
227
- 2. **Mouse Mode**: Character follows cursor with mirror behavior
228
- - Mouse left → Character looks right (at user)
229
- - Negative x coordinate for natural gaze
230
- 3. **Webcam Mode**: Face tracking using BlazeFace model
231
- - Real-time eye position detection
232
- - Normalized coordinates (-1 to 1)
233
-
234
- **Key Features**:
235
- - **Composite Methods**: Uses `applyEyeComposite(yaw, pitch)` and `applyHeadComposite(yaw, pitch, roll)`
236
- - **Intensity Control**: Separate sliders for eye and head movement intensity
237
- - **Head Follow Eyes**: Optional delayed head movement matching eye gaze
238
- - **Global Service**: Created in App.tsx and shared via ModulesContext
239
-
240
- **Usage**:
241
- ```typescript
242
- // Initialize service with engine reference
243
- const service = createEyeHeadTrackingService({
244
- eyeTrackingEnabled: true,
245
- headTrackingEnabled: true,
246
- headFollowEyes: true,
247
- eyeIntensity: 1.0,
248
- headIntensity: 0.5,
249
- engine: engine
278
+ ## 4. Action Unit Control
279
+
280
+ Action Units are the core of FACS. Each AU represents a specific muscular movement of the face.
281
+
282
+ ### Setting an AU immediately
283
+
284
+ ```typescript
285
+ // Set AU12 (smile) to 80% intensity
286
+ loom.setAU(12, 0.8);
287
+
288
+ // Set AU45 (blink) to full intensity
289
+ loom.setAU(45, 1.0);
290
+
291
+ // Set to 0 to deactivate
292
+ loom.setAU(12, 0);
293
+ ```
294
+
295
+ ### Transitioning an AU over time
296
+
297
+ ```typescript
298
+ // Animate AU12 to 0.8 over 200ms
299
+ const handle = loom.transitionAU(12, 0.8, 200);
300
+
301
+ // Wait for completion
302
+ await handle.promise;
303
+
304
+ // Or chain transitions
305
+ loom.transitionAU(12, 1.0, 200).promise.then(() => {
306
+ loom.transitionAU(12, 0, 300); // Fade out
250
307
  });
308
+ ```
251
309
 
252
- service.start();
253
- service.setMode('mouse'); // or 'webcam' or 'manual'
310
+ ### Getting the current AU value
254
311
 
255
- // Set gaze target manually
256
- service.setGazeTarget({ x: 0.5, y: -0.2, z: 0 });
312
+ ```typescript
313
+ const smileAmount = loom.getAU(12);
314
+ console.log(`Current smile: ${smileAmount}`);
257
315
  ```
258
316
 
259
- ### Lip-Sync Agency
317
+ ### Asymmetric control with balance
260
318
 
261
- The **Lip-Sync Agency** (`latticework/lipsync/`) generates viseme animations from text:
319
+ Many AUs have left and right variants (e.g., `Mouth_Smile_L` and `Mouth_Smile_R`). The `balance` parameter lets you control them independently:
262
320
 
263
- 1. **Phoneme Prediction**: Enhanced predictor with coarticulation model
264
- ```typescript
265
- const predictor = new EnhancedPhonemePredictor();
266
- const phonemes = predictor.predict('Hello world');
267
- ```
321
+ ```typescript
322
+ // Balance range: -1 (left only) to +1 (right only), 0 = both equal
268
323
 
269
- 2. **Viseme Mapping**: Phonemes ARKit visemes (AA, CH_J, DD, etc.)
270
- - Timing based on phoneme duration and speech rate
271
- - Coarticulation smoothing between adjacent phonemes
324
+ // Smile on both sides equally
325
+ loom.setAU(12, 0.8, 0);
272
326
 
273
- 3. **Animation Snippet Generation**: Creates JSON snippets for animation service
274
- ```typescript
275
- const snippet = generateLipsyncSnippet(text, {
276
- speechRate: 1.0,
277
- intensity: 0.8,
278
- style: 'relaxed' // or 'precise', 'theatrical', etc.
279
- });
280
- ```
327
+ // Smile only on left side
328
+ loom.setAU(12, 0.8, -1);
281
329
 
282
- 4. **Integration**: Scheduled via animation service with high priority (30)
330
+ // Smile only on right side
331
+ loom.setAU(12, 0.8, 1);
283
332
 
284
- ### Prosodic Expression
333
+ // 70% left, 30% right
334
+ loom.setAU(12, 0.8, -0.4);
335
+ ```
285
336
 
286
- The **Prosodic Expression Agency** (`latticework/prosodic/prosodicService.ts`) adds emotional head movements:
337
+ ### String-based side selection
287
338
 
288
- 1. **XState Machine**: Models prosodic states (idle analyzing expressing)
289
- 2. **Gesture Library**: Pre-defined head nods, tilts, and shakes
290
- - Nod: Positive affirmation (head pitch down)
291
- - Shake: Negation (head yaw side-to-side)
292
- - Tilt: Curiosity/emphasis (head roll)
339
+ You can also specify the side directly in the AU ID:
293
340
 
294
- 3. **Emotion Mapping**: Text analysis triggers appropriate gestures
295
- ```typescript
296
- // Question slight head tilt
297
- // Exclamation head nod emphasis
298
- // Negation words → head shake
299
- ```
341
+ ```typescript
342
+ // These are equivalent:
343
+ loom.setAU('12L', 0.8); // Left side only
344
+ loom.setAU(12, 0.8, -1); // Left side only
300
345
 
301
- 4. **Scheduling**: Prosodic snippets scheduled with medium priority (20)
346
+ loom.setAU('12R', 0.8); // Right side only
347
+ loom.setAU(12, 0.8, 1); // Right side only
348
+ ```
302
349
 
303
350
  ---
304
351
 
305
- ## Modules
306
-
307
- LoomLarge supports pluggable modules for extended functionality:
308
-
309
- ### AI Chat Module
310
- - **Description**: Real-time conversational AI using Anthropic Claude
311
- - **Location**: `src/modules/aiChat/`
312
- - **Features**:
313
- - Streaming text-to-speech synthesis
314
- - Lip-sync integration with prosodic expression
315
- - Eye/head tracking during conversation
316
- - WebSocket or LiveKit audio streaming
317
-
318
- **Activation**:
319
- ```typescript
320
- // Via ModulesMenu UI or programmatically:
321
- import { AIChatApp } from './modules/aiChat';
322
- <AIChatApp animationManager={anim} />
323
- ```
324
-
325
- ### French Quiz Module
326
- - **Description**: Interactive language learning demo
327
- - **Location**: `src/modules/frenchQuiz/`
328
- - **Features**:
329
- - Survey-style question flow
330
- - Facial expressions tied to correct/incorrect answers
331
- - Modal-based UI with progress tracking
332
-
333
- ### Custom Modules
334
-
335
- Create your own modules by following this pattern:
336
-
337
- 1. **Define module config** (`src/modules/config.ts`):
338
- ```typescript
339
- export default {
340
- modules: [
341
- {
342
- name: 'My Module',
343
- description: 'Custom module description',
344
- component: './modules/myModule/index.tsx'
345
- }
346
- ]
347
- };
348
- ```
349
-
350
- 2. **Create module component**:
351
- ```typescript
352
- // src/modules/myModule/index.tsx
353
- import React from 'react';
354
- import { useModulesContext } from '../../context/ModulesContext';
355
-
356
- export default function MyModule({ animationManager }: any) {
357
- const { eyeHeadTrackingService } = useModulesContext();
358
-
359
- // Your module logic here
360
- return <div>My Module UI</div>;
361
- }
362
- ```
352
+ ## 5. Mix Weight System
363
353
 
364
- ---
354
+ Some AUs can be driven by both morph targets (blend shapes) AND bone rotations. The mix weight controls the blend between them.
365
355
 
366
- ## Development
356
+ ### Why mix weights?
367
357
 
368
- ### Running the Dev Server
358
+ Take jaw opening (AU26) as an example:
359
+ - **Morph-only (weight 0)**: Vertices deform to show open mouth, but jaw bone doesn't move
360
+ - **Bone-only (weight 1)**: Jaw bone rotates down, but no soft tissue deformation
361
+ - **Mixed (weight 0.5)**: Both contribute equally for realistic results
369
362
 
370
- ```bash
371
- yarn dev
363
+ ### Setting mix weights
364
+
365
+ ```typescript
366
+ // Get the default mix weight for AU26
367
+ const weight = loom.getAUMixWeight(26); // e.g., 0.5
368
+
369
+ // Set to pure morph
370
+ loom.setAUMixWeight(26, 0);
371
+
372
+ // Set to pure bone
373
+ loom.setAUMixWeight(26, 1);
374
+
375
+ // Set to 70% bone, 30% morph
376
+ loom.setAUMixWeight(26, 0.7);
377
+ ```
378
+
379
+ ### Which AUs support mixing?
380
+
381
+ Only AUs that have both `auToMorphs` AND `auToBones` entries support mixing. Common examples:
382
+ - AU26 (Jaw Drop)
383
+ - AU27 (Mouth Stretch)
384
+ - AU51-56 (Head movements)
385
+ - AU61-64 (Eye movements)
386
+
387
+ ```typescript
388
+ import { isMixedAU } from 'loomlarge';
389
+
390
+ if (isMixedAU(26)) {
391
+ console.log('AU26 supports morph/bone mixing');
392
+ }
372
393
  ```
373
394
 
374
- Access at `http://localhost:5173` with:
375
- - Hot module replacement (HMR)
376
- - Source maps for debugging
377
- - Console logging for all services
395
+ ---
378
396
 
379
- ### Testing Animation Snippets
397
+ ## 6. Composite Rotation System
380
398
 
381
- Load test animations in the browser console:
399
+ Bones like the head and eyes need multi-axis rotation (pitch, yaw, roll). The composite rotation system handles this automatically.
382
400
 
383
- ```javascript
384
- // Global handles (auto-exposed in dev mode)
385
- window.engine // EngineThree instance
386
- window.anim // Animation service
401
+ ### How it works
387
402
 
388
- // Load and play a snippet
389
- anim.loadSnippet('test', {
390
- duration: 2000,
391
- keyframes: {
392
- 'AU_12': [[0, 0], [1000, 1], [2000, 0]], // Smile curve
393
- 'AU_6': [[0, 0], [1000, 0.8], [2000, 0]] // Cheek raise
394
- }
395
- });
396
- anim.schedule('test', { priority: 10 });
397
- anim.play();
403
+ When you set an AU that affects a bone rotation, LoomLarge:
404
+ 1. Queues the rotation update in `pendingCompositeNodes`
405
+ 2. At the end of `update()`, calls `flushPendingComposites()`
406
+ 3. Applies all three axes (pitch, yaw, roll) together to prevent gimbal issues
407
+
408
+ ### Supported bones and their axes
409
+
410
+ | Bone | Pitch (X) | Yaw (Y) | Roll (Z) |
411
+ |------|-----------|---------|----------|
412
+ | HEAD | AU53 (up) / AU54 (down) | AU51 (left) / AU52 (right) | AU55 (tilt left) / AU56 (tilt right) |
413
+ | EYE_L | AU63 (up) / AU64 (down) | AU61 (left) / AU62 (right) | - |
414
+ | EYE_R | AU63 (up) / AU64 (down) | AU61 (left) / AU62 (right) | - |
415
+ | JAW | AU25-27 (open) | AU30 (left) / AU35 (right) | - |
416
+ | TONGUE | AU37 (up) / AU38 (down) | AU39 (left) / AU40 (right) | AU41 / AU42 (tilt) |
417
+
418
+ ### Example: Moving the head
419
+
420
+ ```typescript
421
+ // Turn head left 50%
422
+ loom.setAU(51, 0.5);
423
+
424
+ // Turn head right 50%
425
+ loom.setAU(52, 0.5);
426
+
427
+ // Tilt head up 30%
428
+ loom.setAU(53, 0.3);
429
+
430
+ // Combine: turn left AND tilt up
431
+ loom.setAU(51, 0.5);
432
+ loom.setAU(53, 0.3);
433
+ // Both are applied together in a single composite rotation
398
434
  ```
399
435
 
400
- ### Debugging Eye/Head Tracking
436
+ ### Example: Eye gaze
401
437
 
402
- The service includes comprehensive diagnostic logging:
438
+ ```typescript
439
+ // Look left
440
+ loom.setAU(61, 0.7);
403
441
 
404
- ```javascript
405
- // Check current mode
406
- window.eyeHeadTrackingService?.getMode(); // 'manual' | 'mouse' | 'webcam'
442
+ // Look right
443
+ loom.setAU(62, 0.7);
407
444
 
408
- // Set gaze manually
409
- window.eyeHeadTrackingService?.setGazeTarget({ x: 0.5, y: -0.3, z: 0 });
445
+ // Look up
446
+ loom.setAU(63, 0.5);
410
447
 
411
- // Update configuration
412
- window.eyeHeadTrackingService?.updateConfig({
413
- eyeIntensity: 1.0,
414
- headIntensity: 0.7,
415
- headFollowEyes: true
416
- });
448
+ // Look down-right (combined)
449
+ loom.setAU(62, 0.6);
450
+ loom.setAU(64, 0.4);
417
451
  ```
418
452
 
419
- ### TypeScript Type Checking
453
+ ---
420
454
 
421
- ```bash
422
- yarn typecheck
455
+ ## 7. Continuum Pairs
456
+
457
+ Continuum pairs are bidirectional AU pairs that represent opposite directions on the same axis. They're linked so that activating one should deactivate the other.
458
+
459
+ ### Pair mappings
460
+
461
+ | Pair | Description |
462
+ |------|-------------|
463
+ | AU51 ↔ AU52 | Head turn left / right |
464
+ | AU53 ↔ AU54 | Head up / down |
465
+ | AU55 ↔ AU56 | Head tilt left / right |
466
+ | AU61 ↔ AU62 | Eyes look left / right |
467
+ | AU63 ↔ AU64 | Eyes look up / down |
468
+ | AU30 ↔ AU35 | Jaw shift left / right |
469
+ | AU37 ↔ AU38 | Tongue up / down |
470
+ | AU39 ↔ AU40 | Tongue left / right |
471
+ | AU73 ↔ AU74 | Tongue narrow / wide |
472
+ | AU76 ↔ AU77 | Tongue tip up / down |
473
+
474
+ ### Working with pairs
475
+
476
+ When using continuum pairs, set one AU from the pair and leave the other at 0:
477
+
478
+ ```typescript
479
+ // Head looking left at 50%
480
+ loom.setAU(51, 0.5);
481
+ loom.setAU(52, 0); // Right should be 0
482
+
483
+ // Head looking right at 70%
484
+ loom.setAU(51, 0); // Left should be 0
485
+ loom.setAU(52, 0.7);
423
486
  ```
424
487
 
425
- Runs `tsc --noEmit` to validate types without building.
488
+ ### The CONTINUUM_PAIRS_MAP
489
+
490
+ You can access pair information programmatically:
491
+
492
+ ```typescript
493
+ import { CONTINUUM_PAIRS_MAP } from 'loomlarge';
494
+
495
+ const pair = CONTINUUM_PAIRS_MAP[51];
496
+ // { pairId: 52, isNegative: true, axis: 'yaw', node: 'HEAD' }
497
+ ```
426
498
 
427
499
  ---
428
500
 
429
- ## Deployment
501
+ ## 8. Direct Morph Control
430
502
 
431
- ### GitHub Pages Deployment
503
+ Sometimes you need to control morph targets directly by name, bypassing the AU system.
432
504
 
433
- The project is configured for automatic GitHub Pages deployment:
505
+ ### Setting a morph immediately
434
506
 
435
- ```bash
436
- yarn deploy
507
+ ```typescript
508
+ // Set a specific morph to 50%
509
+ loom.setMorph('Mouth_Smile_L', 0.5);
510
+
511
+ // Set on specific meshes only
512
+ loom.setMorph('Mouth_Smile_L', 0.5, ['CC_Base_Body']);
437
513
  ```
438
514
 
439
- This script:
440
- 1. Builds production bundle (`yarn build`)
441
- 2. Deploys to `gh-pages` branch
442
- 3. Publishes to `https://meekmachine.github.io/LoomLarge`
515
+ ### Transitioning a morph
443
516
 
444
- **Configuration** (`vite.config.ts`):
445
517
  ```typescript
446
- export default defineConfig({
447
- base: '/LoomLarge/', // GitHub repo name
448
- build: {
449
- outDir: 'dist',
450
- assetsDir: 'assets'
451
- }
452
- });
453
- ```
518
+ // Animate morph over 200ms
519
+ const handle = loom.transitionMorph('Mouth_Smile_L', 0.8, 200);
454
520
 
455
- ### Custom Domain
521
+ // With mesh targeting
522
+ loom.transitionMorph('Eye_Blink_L', 1.0, 100, ['CC_Base_Body']);
456
523
 
457
- To use a custom domain:
524
+ // Wait for completion
525
+ await handle.promise;
526
+ ```
458
527
 
459
- 1. Add `CNAME` file to `public/`:
460
- ```
461
- your-domain.com
462
- ```
528
+ ### Reading current morph value
529
+
530
+ ```typescript
531
+ const value = loom.getMorphValue('Mouth_Smile_L');
532
+ ```
463
533
 
464
- 2. Configure DNS:
465
- ```
466
- A 185.199.108.153
467
- A 185.199.109.153
468
- A 185.199.110.153
469
- A 185.199.111.153
470
- CNAME www your-username.github.io
471
- ```
534
+ ### Morph caching
472
535
 
473
- 3. Deploy:
474
- ```bash
475
- yarn deploy
476
- ```
536
+ LoomLarge caches morph target lookups for performance. The first time you access a morph, it searches all meshes and caches the index. Subsequent accesses are O(1).
477
537
 
478
538
  ---
479
539
 
480
- ## API Reference
540
+ ## 9. Viseme System
541
+
542
+ Visemes are mouth shapes used for lip-sync. LoomLarge includes 15 visemes with automatic jaw coupling.
543
+
544
+ ### The 15 visemes
481
545
 
482
- ### Animation Service API
546
+ | Index | Key | Phoneme Example |
547
+ |-------|-----|-----------------|
548
+ | 0 | EE | "b**ee**" |
549
+ | 1 | Er | "h**er**" |
550
+ | 2 | IH | "s**i**t" |
551
+ | 3 | Ah | "f**a**ther" |
552
+ | 4 | Oh | "g**o**" |
553
+ | 5 | W_OO | "t**oo**" |
554
+ | 6 | S_Z | "**s**un, **z**oo" |
555
+ | 7 | Ch_J | "**ch**ip, **j**ump" |
556
+ | 8 | F_V | "**f**un, **v**an" |
557
+ | 9 | TH | "**th**ink" |
558
+ | 10 | T_L_D_N | "**t**op, **l**ip, **d**og, **n**o" |
559
+ | 11 | B_M_P | "**b**at, **m**an, **p**op" |
560
+ | 12 | K_G_H_NG | "**k**ite, **g**o, **h**at, si**ng**" |
561
+ | 13 | AE | "c**a**t" |
562
+ | 14 | R | "**r**ed" |
563
+
564
+ ### Setting a viseme
483
565
 
484
566
  ```typescript
485
- interface AnimationService {
486
- loadSnippet(name: string, snippet: AnimationSnippet): void;
487
- schedule(name: string, options?: ScheduleOptions): void;
488
- play(): void;
489
- pause(): void;
490
- stop(): void;
491
- setLoop(loop: boolean): void;
492
- scrub(time: number): void;
493
- step(deltaSeconds: number): void;
494
- dispose(): void;
495
- }
567
+ // Set viseme 3 (Ah) to full intensity
568
+ loom.setViseme(3, 1.0);
569
+
570
+ // With jaw scale (0-1, default 1)
571
+ loom.setViseme(3, 1.0, 0.5); // Half jaw opening
496
572
  ```
497
573
 
498
- ### Eye/Head Tracking Service API
574
+ ### Transitioning visemes
499
575
 
500
576
  ```typescript
501
- interface EyeHeadTrackingService {
502
- start(): void;
503
- stop(): void;
504
- setGazeTarget(target: GazeTarget): void;
505
- setMode(mode: 'manual' | 'mouse' | 'webcam'): void;
506
- getMode(): 'manual' | 'mouse' | 'webcam';
507
- updateConfig(config: Partial<EyeHeadTrackingConfig>): void;
508
- setSpeaking(isSpeaking: boolean): void;
509
- setListening(isListening: boolean): void;
510
- blink(): void;
511
- dispose(): void;
512
- }
577
+ // Animate to viseme over 80ms (typical for speech)
578
+ const handle = loom.transitionViseme(3, 1.0, 80);
579
+
580
+ // Disable jaw coupling
581
+ loom.transitionViseme(3, 1.0, 80, 0);
513
582
  ```
514
583
 
515
- ### EngineThree Composite Methods
584
+ ### Automatic jaw coupling
516
585
 
517
- ```typescript
518
- class EngineThree {
519
- // Eye composite rotation (yaw/pitch)
520
- applyEyeComposite(yaw: number, pitch: number): void;
586
+ Each viseme has a predefined jaw opening amount. When you set a viseme, the jaw automatically opens proportionally:
521
587
 
522
- // Head composite rotation (yaw/pitch/roll)
523
- applyHeadComposite(yaw: number, pitch: number, roll?: number): void;
588
+ | Viseme | Jaw Amount |
589
+ |--------|------------|
590
+ | EE | 0.15 |
591
+ | Ah | 0.70 |
592
+ | Oh | 0.50 |
593
+ | B_M_P | 0.20 |
524
594
 
525
- // Get/Set AU mix weight (morph ↔ bone blend)
526
- getAUMixWeight(auId: number): number | undefined;
527
- setAUMixWeight(auId: number, mix: number): void;
595
+ The `jawScale` parameter multiplies this amount:
596
+ - `jawScale = 1.0`: Normal jaw opening
597
+ - `jawScale = 0.5`: Half jaw opening
598
+ - `jawScale = 0`: No jaw movement (viseme only)
528
599
 
529
- // Direct AU control
530
- setAU(id: number | string, value: number): void;
531
- setMorph(key: string, value: number): void;
600
+ ### Lip-sync example
601
+
602
+ ```typescript
603
+ async function speak(phonemes: number[]) {
604
+ for (const viseme of phonemes) {
605
+ // Clear previous viseme
606
+ for (let i = 0; i < 15; i++) loom.setViseme(i, 0);
532
607
 
533
- // Smooth transitions
534
- transitionAU(id: number | string, target: number, duration?: number): void;
535
- transitionMorph(key: string, target: number, duration?: number): void;
608
+ // Transition to new viseme
609
+ await loom.transitionViseme(viseme, 1.0, 80).promise;
610
+
611
+ // Hold briefly
612
+ await new Promise(r => setTimeout(r, 100));
613
+ }
614
+
615
+ // Return to neutral
616
+ for (let i = 0; i < 15; i++) loom.setViseme(i, 0);
536
617
  }
618
+
619
+ // "Hello" approximation
620
+ speak([5, 0, 10, 4]);
537
621
  ```
538
622
 
539
623
  ---
540
624
 
541
- ## Troubleshooting
625
+ ## 10. Transition System
542
626
 
543
- ### Character Not Moving
627
+ All animated changes in LoomLarge go through the transition system, which provides smooth interpolation with easing.
544
628
 
545
- 1. **Check engine initialization**:
546
- ```javascript
547
- console.log(window.engine); // Should show EngineThree instance
548
- ```
629
+ ### TransitionHandle
549
630
 
550
- 2. **Verify service startup**:
551
- ```javascript
552
- console.log(window.anim?.getSnapshot?.().value); // Should show 'playing' or 'idle'
553
- ```
631
+ Every transition method returns a `TransitionHandle`:
554
632
 
555
- 3. **Check eye/head tracking**:
556
- ```javascript
557
- window.eyeHeadTrackingService?.getState(); // Should show current gaze/status
558
- ```
633
+ ```typescript
634
+ interface TransitionHandle {
635
+ promise: Promise<void>; // Resolves when transition completes
636
+ pause(): void; // Pause this transition
637
+ resume(): void; // Resume this transition
638
+ cancel(): void; // Cancel immediately
639
+ }
640
+ ```
559
641
 
560
- ### Eye Tracking Direction Issues
642
+ ### Using handles
561
643
 
562
- - **Eyes looking wrong way**: Check coordinate negation in `eyeHeadTrackingService.ts:283`
563
- - **Head not following**: Verify `headFollowEyes` is enabled in config
564
- - **Intensity too low**: Increase `eyeIntensity` and `headIntensity` sliders
644
+ ```typescript
645
+ // Start a transition
646
+ const handle = loom.transitionAU(12, 1.0, 500);
565
647
 
566
- ### Animation Not Loading
648
+ // Pause it
649
+ handle.pause();
567
650
 
568
- 1. **Check JSON format**:
569
- - Must have `duration` and `keyframes` fields
570
- - AU keys must match ARKit spec (e.g., 'AU_12', not '12')
651
+ // Resume later
652
+ handle.resume();
571
653
 
572
- 2. **Verify snippet name**:
573
- ```javascript
574
- anim.listSnippets(); // Show all loaded snippets
575
- ```
654
+ // Or cancel entirely
655
+ handle.cancel();
576
656
 
577
- 3. **Check console for errors**:
578
- - Look for `[Animation]` prefixed logs
579
- - Validate keyframe curve format: `[[time, value], ...]`
657
+ // Wait for completion
658
+ await handle.promise;
659
+ ```
580
660
 
581
- ### TensorFlow.js Bundling Errors
661
+ ### Combining multiple transitions
582
662
 
583
- If you encounter errors related to TensorFlow.js modules (e.g., `@tensorflow/tfjs-core/dist/ops/ops_for_converter`), this is a **known issue** with TensorFlow.js 4.x and Vite.
663
+ When you call `transitionAU`, it may create multiple internal transitions (one per morph target). The returned handle controls all of them:
584
664
 
585
- **The Problem:**
586
- - TensorFlow.js references internal module paths that don't actually exist in the npm package
587
- - Vite's esbuild optimizer cannot resolve these phantom paths
588
- - Results in 100+ bundling errors about missing exports and modules
665
+ ```typescript
666
+ // AU12 might affect Mouth_Smile_L and Mouth_Smile_R
667
+ const handle = loom.transitionAU(12, 1.0, 200);
589
668
 
590
- **Solution (Already Implemented):**
591
- LoomLarge loads TensorFlow.js and BlazeFace from **CDN** instead of npm packages:
669
+ // Pausing the handle pauses both morph transitions
670
+ handle.pause();
671
+ ```
672
+
673
+ ### Easing
674
+
675
+ The default easing is `easeInOutQuad`. Custom easing can be provided when using the Animation system directly:
592
676
 
593
- ```html
594
- <!-- index.html -->
595
- <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js"></script>
596
- <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/blazeface@0.0.7/dist/blazeface.js"></script>
677
+ ```typescript
678
+ // The AnimationThree class supports custom easing
679
+ animation.addTransition(
680
+ 'custom',
681
+ 0,
682
+ 1,
683
+ 200,
684
+ (v) => console.log(v),
685
+ (t) => t * t // Custom ease-in quadratic
686
+ );
597
687
  ```
598
688
 
599
- The code uses global `blazeface` object with TypeScript declarations:
689
+ ### Active transition count
690
+
600
691
  ```typescript
601
- // useWebcamEyeTracking.ts
602
- declare const blazeface: any;
603
- const model = await blazeface.load();
692
+ const count = loom.getActiveTransitionCount();
693
+ console.log(`${count} transitions in progress`);
604
694
  ```
605
695
 
606
- Vite config excludes TensorFlow packages from optimization:
696
+ ### Clearing all transitions
697
+
607
698
  ```typescript
608
- // vite.config.ts
609
- optimizeDeps: {
610
- exclude: [
611
- '@tensorflow/tfjs',
612
- '@tensorflow/tfjs-core',
613
- '@tensorflow/tfjs-converter',
614
- '@tensorflow/tfjs-backend-cpu',
615
- '@tensorflow/tfjs-backend-webgl',
616
- '@tensorflow-models/blazeface',
617
- ],
618
- }
699
+ // Cancel everything immediately
700
+ loom.clearTransitions();
619
701
  ```
620
702
 
621
- **If you still see errors:**
622
- 1. Ensure TensorFlow packages are NOT in `package.json` dependencies
623
- 2. Clear Vite cache: `rm -rf node_modules/.vite`
624
- 3. Restart dev server: `yarn dev`
703
+ ---
625
704
 
626
- See [src/hooks/README_useWebcamEyeTracking.md](src/hooks/README_useWebcamEyeTracking.md) for full documentation.
705
+ ## 11. Playback & State Control
627
706
 
628
- ### Build Errors
707
+ ### Pausing and resuming
629
708
 
630
- ```bash
631
- # Clear cache and rebuild
632
- rm -rf node_modules dist .vite
633
- yarn install
634
- yarn build
709
+ ```typescript
710
+ // Pause all animation updates
711
+ loom.pause();
712
+
713
+ // Check pause state
714
+ if (loom.getPaused()) {
715
+ console.log('Animation is paused');
716
+ }
717
+
718
+ // Resume
719
+ loom.resume();
635
720
  ```
636
721
 
637
- ---
722
+ When paused, `loom.update()` stops processing transitions, but you can still call `setAU()` for immediate changes.
638
723
 
639
- ## Performance Optimization
724
+ ### Resetting to neutral
640
725
 
641
- ### Animation Loop
726
+ ```typescript
727
+ // Reset everything to rest state
728
+ loom.resetToNeutral();
729
+ ```
642
730
 
643
- - Central frame loop runs at 60 FPS (`threeContext.tsx`)
644
- - Animation scheduler ticks every frame via `step(deltaTime)`
645
- - Morph/bone updates batched per frame
731
+ This:
732
+ - Clears all AU values to 0
733
+ - Cancels all active transitions
734
+ - Resets all morph targets to 0
735
+ - Returns all bones to their original position/rotation
646
736
 
647
- ### Composite Rotation Caching
737
+ ### Mesh visibility
648
738
 
649
- - Rotation state cached in `compositeRotationState` map
650
- - Only recalculates when values change
651
- - Avoids redundant Three.js object updates
739
+ ```typescript
740
+ // Get list of all meshes
741
+ const meshes = loom.getMeshList();
742
+ // Returns: [{ name: 'CC_Base_Body', visible: true, morphCount: 80 }, ...]
652
743
 
653
- ### Snippet Scheduling
744
+ // Hide a mesh
745
+ loom.setMeshVisible('CC_Base_Hair', false);
654
746
 
655
- - Priority-based scheduler prevents conflicts
656
- - Lower priority snippets paused when higher priority plays
657
- - Automatic cleanup of completed snippets
747
+ // Show it again
748
+ loom.setMeshVisible('CC_Base_Hair', true);
749
+ ```
658
750
 
659
- ---
751
+ ### Cleanup
660
752
 
661
- ## Contributing
662
-
663
- We welcome contributions! Please follow these guidelines:
664
-
665
- 1. **Fork and clone** the repository
666
- 2. **Create a feature branch**: `git checkout -b feature/my-feature`
667
- 3. **Follow code style**:
668
- - TypeScript strict mode
669
- - ESLint/Prettier for formatting
670
- - Descriptive variable names
671
- 4. **Test thoroughly**:
672
- - Manual testing in dev mode
673
- - TypeScript type checking (`yarn typecheck`)
674
- 5. **Commit with descriptive messages**:
675
- ```
676
- feat: Add webcam eye tracking support
677
- fix: Correct head yaw direction in mouse mode
678
- docs: Update README with API reference
679
- ```
680
- 6. **Push and create PR** to `main` branch
753
+ ```typescript
754
+ // When done, dispose of resources
755
+ loom.dispose();
756
+ ```
681
757
 
682
758
  ---
683
759
 
684
- ## License & Acknowledgments
760
+ ## 12. Hair Physics
685
761
 
686
- **© 2025 Jonathan Sutton Fields, Lovelace LOL**
687
- Licensed under the **Loom Large, Latticework copyleft license**
762
+ LoomLarge includes an experimental hair physics system that simulates hair movement based on head motion.
688
763
 
689
- ### Acknowledgments
764
+ ### Basic setup
690
765
 
691
- - **Three.js** – 3D rendering engine
692
- - **XState** State machine library
693
- - **React** – UI framework
694
- - **Vite** – Lightning-fast bundler
695
- - **ARKit** – Facial Action Coding System specification
696
- - **BlazeFace** – Webcam face detection model
766
+ ```typescript
767
+ import { HairPhysics } from 'loomlarge';
697
768
 
698
- ### Related Projects
769
+ const hair = new HairPhysics();
770
+ ```
699
771
 
700
- - **VISOS** Predecessor architecture (object-oriented)
701
- - **eEVA Workbench** – Original survey/conversation platform
702
- - **Latticework** – Core agency framework
772
+ ### Updating in animation loop
703
773
 
704
- ---
774
+ ```typescript
775
+ function animate() {
776
+ // Get current head state (from your tracking system or AU values)
777
+ const headState = {
778
+ yaw: 0, // Head rotation in radians
779
+ pitch: 0,
780
+ roll: 0,
781
+ yawVelocity: 0.5, // Angular velocity
782
+ pitchVelocity: 0,
783
+ };
784
+
785
+ // Update hair physics
786
+ const hairMorphs = hair.update(deltaTime, headState);
787
+
788
+ // Apply hair morphs
789
+ for (const [morphName, value] of Object.entries(hairMorphs)) {
790
+ loom.setMorph(morphName, value);
791
+ }
792
+ }
793
+ ```
794
+
795
+ ### Output morphs
796
+
797
+ The physics system outputs 6 morph values:
705
798
 
706
- ## Support
799
+ | Morph | Description |
800
+ |-------|-------------|
801
+ | L_Hair_Left | Left side, swing left |
802
+ | L_Hair_Right | Left side, swing right |
803
+ | L_Hair_Front | Left side, swing forward |
804
+ | R_Hair_Left | Right side, swing left |
805
+ | R_Hair_Right | Right side, swing right |
806
+ | R_Hair_Front | Right side, swing forward |
707
807
 
708
- - **Issues**: [GitHub Issues](https://github.com/meekmachine/LoomLarge/issues)
709
- - **Discussions**: [GitHub Discussions](https://github.com/meekmachine/LoomLarge/discussions)
710
- - **Email**: jonathan@lovelacelol.com
808
+ ### Physics forces
809
+
810
+ The simulation models 5 forces:
811
+
812
+ 1. **Spring restoration** - Pulls hair back to rest position
813
+ 2. **Damping** - Air resistance prevents infinite oscillation
814
+ 3. **Gravity** - Hair swings based on head tilt
815
+ 4. **Inertia** - Hair lags behind head movement
816
+ 5. **Wind** - Optional oscillating wind force
817
+
818
+ ### Configuration
819
+
820
+ ```typescript
821
+ const hair = new HairPhysics({
822
+ mass: 1.0,
823
+ stiffness: 50,
824
+ damping: 5,
825
+ gravity: 9.8,
826
+ headInfluence: 0.8, // How much head movement affects hair
827
+ wind: {
828
+ strength: 0,
829
+ direction: { x: 1, y: 0, z: 0 },
830
+ turbulence: 0.2,
831
+ frequency: 1.0,
832
+ },
833
+ });
834
+ ```
711
835
 
712
836
  ---
713
837
 
714
- **Built with ❤️ by the Latticework team**
838
+ ## Resources
839
+
840
+ - [FACS on Wikipedia](https://en.wikipedia.org/wiki/Facial_Action_Coding_System)
841
+ - [Paul Ekman Group - FACS](https://www.paulekman.com/facial-action-coding-system/)
842
+ - [Character Creator 4](https://www.reallusion.com/character-creator/)
843
+ - [Three.js Documentation](https://threejs.org/docs/)
844
+
845
+ ## License
846
+
847
+ MIT License - see LICENSE file for details.