@makefinks/daemon 0.1.0
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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.js +22 -0
- package/package.json +79 -0
- package/src/ai/agent-turn-runner.ts +130 -0
- package/src/ai/daemon-ai.ts +403 -0
- package/src/ai/exa-client.ts +21 -0
- package/src/ai/exa-fetch-cache.ts +104 -0
- package/src/ai/model-config.ts +99 -0
- package/src/ai/sanitize-messages.ts +83 -0
- package/src/ai/system-prompt.ts +363 -0
- package/src/ai/tools/fetch-urls.ts +187 -0
- package/src/ai/tools/grounding-manager.ts +94 -0
- package/src/ai/tools/index.ts +52 -0
- package/src/ai/tools/read-file.ts +100 -0
- package/src/ai/tools/render-url.ts +275 -0
- package/src/ai/tools/run-bash.ts +224 -0
- package/src/ai/tools/subagents.ts +195 -0
- package/src/ai/tools/todo-manager.ts +150 -0
- package/src/ai/tools/web-search.ts +91 -0
- package/src/app/App.tsx +711 -0
- package/src/app/components/AppOverlays.tsx +131 -0
- package/src/app/components/AvatarLayer.tsx +51 -0
- package/src/app/components/ConversationPane.tsx +476 -0
- package/src/avatar/DaemonAvatarRenderable.ts +343 -0
- package/src/avatar/daemon-avatar-rig.ts +1165 -0
- package/src/avatar-preview.ts +186 -0
- package/src/cli.ts +26 -0
- package/src/components/ApiKeyInput.tsx +99 -0
- package/src/components/ApiKeyStep.tsx +95 -0
- package/src/components/ApprovalPicker.tsx +109 -0
- package/src/components/ContentBlockView.tsx +141 -0
- package/src/components/DaemonText.tsx +34 -0
- package/src/components/DeviceMenu.tsx +166 -0
- package/src/components/GroundingBadge.tsx +21 -0
- package/src/components/GroundingMenu.tsx +310 -0
- package/src/components/HotkeysPane.tsx +115 -0
- package/src/components/InlineStatusIndicator.tsx +106 -0
- package/src/components/ModelMenu.tsx +411 -0
- package/src/components/OnboardingOverlay.tsx +446 -0
- package/src/components/ProviderMenu.tsx +177 -0
- package/src/components/SessionMenu.tsx +297 -0
- package/src/components/SettingsMenu.tsx +291 -0
- package/src/components/StatusBar.tsx +126 -0
- package/src/components/TokenUsageDisplay.tsx +92 -0
- package/src/components/ToolCallView.tsx +113 -0
- package/src/components/TypingInputBar.tsx +131 -0
- package/src/components/tool-layouts/components.tsx +120 -0
- package/src/components/tool-layouts/defaults.ts +9 -0
- package/src/components/tool-layouts/index.ts +22 -0
- package/src/components/tool-layouts/layouts/bash.ts +110 -0
- package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
- package/src/components/tool-layouts/layouts/index.ts +8 -0
- package/src/components/tool-layouts/layouts/read-file.ts +59 -0
- package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
- package/src/components/tool-layouts/layouts/system-info.ts +8 -0
- package/src/components/tool-layouts/layouts/todo.tsx +139 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
- package/src/components/tool-layouts/layouts/web-search.ts +110 -0
- package/src/components/tool-layouts/registry.ts +17 -0
- package/src/components/tool-layouts/types.ts +94 -0
- package/src/hooks/daemon-event-handlers.ts +944 -0
- package/src/hooks/keyboard-handlers.ts +399 -0
- package/src/hooks/menu-navigation.ts +147 -0
- package/src/hooks/use-app-audio-devices-loader.ts +71 -0
- package/src/hooks/use-app-callbacks.ts +202 -0
- package/src/hooks/use-app-context-builder.ts +159 -0
- package/src/hooks/use-app-display-state.ts +162 -0
- package/src/hooks/use-app-menus.ts +51 -0
- package/src/hooks/use-app-model-pricing-loader.ts +45 -0
- package/src/hooks/use-app-model.ts +123 -0
- package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
- package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
- package/src/hooks/use-app-sessions.ts +105 -0
- package/src/hooks/use-app-settings.ts +62 -0
- package/src/hooks/use-conversation-manager.ts +163 -0
- package/src/hooks/use-copy-on-select.ts +50 -0
- package/src/hooks/use-daemon-events.ts +396 -0
- package/src/hooks/use-daemon-keyboard.ts +397 -0
- package/src/hooks/use-grounding.ts +46 -0
- package/src/hooks/use-input-history.ts +92 -0
- package/src/hooks/use-menu-keyboard.ts +93 -0
- package/src/hooks/use-playwright-notification.ts +23 -0
- package/src/hooks/use-reasoning-animation.ts +97 -0
- package/src/hooks/use-response-timer.ts +55 -0
- package/src/hooks/use-tool-approval.tsx +202 -0
- package/src/hooks/use-typing-mode.ts +137 -0
- package/src/hooks/use-voice-dependencies-notification.ts +37 -0
- package/src/index.tsx +48 -0
- package/src/scripts/setup-browsers.ts +42 -0
- package/src/state/app-context.tsx +160 -0
- package/src/state/daemon-events.ts +67 -0
- package/src/state/daemon-state.ts +493 -0
- package/src/state/migrations/001-init.ts +33 -0
- package/src/state/migrations/index.ts +8 -0
- package/src/state/model-history-store.ts +45 -0
- package/src/state/runtime-context.ts +21 -0
- package/src/state/session-store.ts +359 -0
- package/src/types/index.ts +405 -0
- package/src/types/theme.ts +52 -0
- package/src/ui/constants.ts +157 -0
- package/src/utils/clipboard.ts +89 -0
- package/src/utils/debug-logger.ts +69 -0
- package/src/utils/formatters.ts +242 -0
- package/src/utils/js-rendering.ts +77 -0
- package/src/utils/markdown-tables.ts +234 -0
- package/src/utils/model-metadata.ts +191 -0
- package/src/utils/openrouter-endpoints.ts +212 -0
- package/src/utils/openrouter-models.ts +205 -0
- package/src/utils/openrouter-pricing.ts +59 -0
- package/src/utils/openrouter-reported-cost.ts +16 -0
- package/src/utils/paste.ts +33 -0
- package/src/utils/preferences.ts +289 -0
- package/src/utils/text-fragment.ts +39 -0
- package/src/utils/tool-output-preview.ts +250 -0
- package/src/utils/voice-dependencies.ts +107 -0
- package/src/utils/workspace-manager.ts +85 -0
- package/src/voice/audio-recorder.ts +579 -0
- package/src/voice/mic-level.ts +35 -0
- package/src/voice/tts/openai-tts-stream.ts +222 -0
- package/src/voice/tts/speech-controller.ts +64 -0
- package/src/voice/tts/tts-player.ts +257 -0
- package/src/voice/voice-input-controller.ts +96 -0
|
@@ -0,0 +1,1165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If you have opened this file to understand it, this is your friendly suggestion to leave and forget this file existed.
|
|
3
|
+
* This file is a messy god component that requires refractoring and is a result of multiple LLM atrocities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { THREE } from "@opentui/core/3d";
|
|
7
|
+
import type { AvatarColorTheme } from "../types";
|
|
8
|
+
|
|
9
|
+
// Re-export for consumers that expect DaemonColorTheme
|
|
10
|
+
export type { AvatarColorTheme as DaemonColorTheme } from "../types";
|
|
11
|
+
|
|
12
|
+
export type ToolCategory = "web" | "file" | "bash" | "subagent";
|
|
13
|
+
|
|
14
|
+
export const TOOL_CATEGORY_COLORS: Record<ToolCategory, number> = {
|
|
15
|
+
web: 0x22d3ee,
|
|
16
|
+
file: 0x4ade80,
|
|
17
|
+
bash: 0xfbbf24,
|
|
18
|
+
subagent: 0xa78bfa,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface DaemonRig {
|
|
22
|
+
scene: THREE.Scene;
|
|
23
|
+
camera: THREE.PerspectiveCamera;
|
|
24
|
+
update(deltaS: number): void;
|
|
25
|
+
setColors(theme: AvatarColorTheme): void;
|
|
26
|
+
setIntensity(intensity: number, options?: { immediate?: boolean }): void;
|
|
27
|
+
setAudioLevel(level: number, options?: { immediate?: boolean }): void;
|
|
28
|
+
/** Set whether a tool is currently active (affects sigil lines and ambient state) */
|
|
29
|
+
setToolActive(active: boolean, category?: ToolCategory): void;
|
|
30
|
+
/** Trigger eye flash and fragment scatter burst when tool is invoked */
|
|
31
|
+
triggerToolFlash(category?: ToolCategory): void;
|
|
32
|
+
/** Trigger settle animation when tool completes */
|
|
33
|
+
triggerToolComplete(): void;
|
|
34
|
+
/** Set reasoning mode - activates contemplative animations (slow pulse, dilated pupil, inward drift) */
|
|
35
|
+
setReasoningMode(active: boolean): void;
|
|
36
|
+
/** Set typing mode - activates eye micro-tracking */
|
|
37
|
+
setTypingMode(active: boolean): void;
|
|
38
|
+
/** Trigger a subtle pulse/reaction for a typing keystroke */
|
|
39
|
+
triggerTypingPulse(): void;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
|
+
// UTILITY FUNCTIONS
|
|
45
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
|
|
48
|
+
|
|
49
|
+
const clamp01 = (value: number): number => clamp(value, 0, 1);
|
|
50
|
+
|
|
51
|
+
function lerpColor(current: number, target: number, t: number): number {
|
|
52
|
+
const cr = (current >> 16) & 0xff;
|
|
53
|
+
const cg = (current >> 8) & 0xff;
|
|
54
|
+
const cb = current & 0xff;
|
|
55
|
+
const tr = (target >> 16) & 0xff;
|
|
56
|
+
const tg = (target >> 8) & 0xff;
|
|
57
|
+
const tb = target & 0xff;
|
|
58
|
+
return (
|
|
59
|
+
(Math.round(cr + (tr - cr) * t) << 16) |
|
|
60
|
+
(Math.round(cg + (tg - cg) * t) << 8) |
|
|
61
|
+
Math.round(cb + (tb - cb) * t)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
66
|
+
// STATE INTERFACES - Grouped for maintainability
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
|
|
69
|
+
interface PhaseState {
|
|
70
|
+
drift: number;
|
|
71
|
+
corePulse: number;
|
|
72
|
+
eyePulse: number;
|
|
73
|
+
pupilPulse: number;
|
|
74
|
+
sigilPulse: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface IntensityState {
|
|
78
|
+
current: number;
|
|
79
|
+
target: number;
|
|
80
|
+
spinBoost: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface AudioState {
|
|
84
|
+
current: number;
|
|
85
|
+
target: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface GlitchState {
|
|
89
|
+
timer: number;
|
|
90
|
+
isActive: boolean;
|
|
91
|
+
duration: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ToolState {
|
|
95
|
+
active: boolean;
|
|
96
|
+
flashTimer: number;
|
|
97
|
+
flashColor: number;
|
|
98
|
+
fragmentScatterBoost: number;
|
|
99
|
+
sigilBrightnessBoost: number;
|
|
100
|
+
settleTimer: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ReasoningState {
|
|
104
|
+
active: boolean;
|
|
105
|
+
blend: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface TypingState {
|
|
109
|
+
active: boolean;
|
|
110
|
+
pulse: number;
|
|
111
|
+
eyeScanTimer: number;
|
|
112
|
+
eyeScanInterval: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface IdleMicroGlitchState {
|
|
116
|
+
timer: number;
|
|
117
|
+
cooldown: number;
|
|
118
|
+
active: boolean;
|
|
119
|
+
duration: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface EyeDriftState {
|
|
123
|
+
x: number;
|
|
124
|
+
y: number;
|
|
125
|
+
targetX: number;
|
|
126
|
+
targetY: number;
|
|
127
|
+
timer: number;
|
|
128
|
+
interval: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ParticlePulseState {
|
|
132
|
+
timer: number;
|
|
133
|
+
interval: number;
|
|
134
|
+
brightness: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface CoreDriftState {
|
|
138
|
+
x: number;
|
|
139
|
+
y: number;
|
|
140
|
+
z: number;
|
|
141
|
+
phaseX: number;
|
|
142
|
+
phaseY: number;
|
|
143
|
+
phaseZ: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface ThemeState {
|
|
147
|
+
current: AvatarColorTheme;
|
|
148
|
+
target: AvatarColorTheme;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
152
|
+
// RING & FRAGMENT DATA STRUCTURES
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
|
|
155
|
+
interface RingData {
|
|
156
|
+
mesh: THREE.Line;
|
|
157
|
+
material: THREE.LineBasicMaterial;
|
|
158
|
+
speed: number;
|
|
159
|
+
axis: THREE.Vector3;
|
|
160
|
+
phase: number;
|
|
161
|
+
wobblePhase: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface FragmentData {
|
|
165
|
+
mesh: THREE.Mesh;
|
|
166
|
+
material: THREE.MeshBasicMaterial;
|
|
167
|
+
orbitRadius: number;
|
|
168
|
+
orbitSpeed: number;
|
|
169
|
+
orbitAngle: number;
|
|
170
|
+
bobSpeed: number;
|
|
171
|
+
bobPhase: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface ParticleVelocity {
|
|
175
|
+
x: number;
|
|
176
|
+
y: number;
|
|
177
|
+
z: number;
|
|
178
|
+
phase: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
182
|
+
// SCENE ELEMENTS INTERFACE
|
|
183
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
184
|
+
|
|
185
|
+
interface SceneElements {
|
|
186
|
+
mainAnchor: THREE.Group;
|
|
187
|
+
coreGroup: THREE.Group;
|
|
188
|
+
orbitGroup: THREE.Group;
|
|
189
|
+
fragmentGroup: THREE.Group;
|
|
190
|
+
coreMesh: THREE.Mesh;
|
|
191
|
+
glowMesh: THREE.Mesh;
|
|
192
|
+
glowMat: THREE.MeshBasicMaterial;
|
|
193
|
+
eye: THREE.Mesh;
|
|
194
|
+
eyeMat: THREE.MeshBasicMaterial;
|
|
195
|
+
pupil: THREE.Mesh;
|
|
196
|
+
pupilMat: THREE.MeshBasicMaterial;
|
|
197
|
+
rings: RingData[];
|
|
198
|
+
fragments: FragmentData[];
|
|
199
|
+
particleSystem: THREE.Points;
|
|
200
|
+
particleMat: THREE.PointsMaterial;
|
|
201
|
+
particlePos: THREE.BufferAttribute;
|
|
202
|
+
particleVelocities: ParticleVelocity[];
|
|
203
|
+
sigilLines: THREE.LineSegments;
|
|
204
|
+
sigilMat: THREE.LineBasicMaterial;
|
|
205
|
+
sigilPos: THREE.BufferAttribute;
|
|
206
|
+
pointLight: THREE.PointLight;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
// SCENE CONSTRUCTION
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
+
|
|
213
|
+
function createSceneElements(
|
|
214
|
+
scene: THREE.Scene,
|
|
215
|
+
trackGeo: <T extends THREE.BufferGeometry>(g: T) => T,
|
|
216
|
+
trackMat: <T extends THREE.Material>(m: T) => T
|
|
217
|
+
): SceneElements {
|
|
218
|
+
const mainAnchor = new THREE.Group();
|
|
219
|
+
scene.add(mainAnchor);
|
|
220
|
+
|
|
221
|
+
const coreGroup = new THREE.Group();
|
|
222
|
+
mainAnchor.add(coreGroup);
|
|
223
|
+
|
|
224
|
+
const orbitGroup = new THREE.Group();
|
|
225
|
+
mainAnchor.add(orbitGroup);
|
|
226
|
+
|
|
227
|
+
const fragmentGroup = new THREE.Group();
|
|
228
|
+
mainAnchor.add(fragmentGroup);
|
|
229
|
+
|
|
230
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
231
|
+
// THE CORE - A pulsing void at the center
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
233
|
+
const coreGeo = trackGeo(new THREE.IcosahedronGeometry(0.35, 0));
|
|
234
|
+
const coreMat = trackMat(
|
|
235
|
+
new THREE.MeshBasicMaterial({
|
|
236
|
+
color: 0x000000,
|
|
237
|
+
transparent: true,
|
|
238
|
+
opacity: 0.95,
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
const coreMesh = new THREE.Mesh(coreGeo, coreMat);
|
|
242
|
+
coreGroup.add(coreMesh);
|
|
243
|
+
|
|
244
|
+
// Inner glow sphere
|
|
245
|
+
const glowGeo = trackGeo(new THREE.IcosahedronGeometry(0.38, 1));
|
|
246
|
+
const glowMat = trackMat<THREE.MeshBasicMaterial>(
|
|
247
|
+
new THREE.MeshBasicMaterial({
|
|
248
|
+
color: 0x666666,
|
|
249
|
+
wireframe: true,
|
|
250
|
+
transparent: true,
|
|
251
|
+
opacity: 0.6,
|
|
252
|
+
blending: THREE.AdditiveBlending,
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
const glowMesh = new THREE.Mesh(glowGeo, glowMat);
|
|
256
|
+
coreGroup.add(glowMesh);
|
|
257
|
+
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
259
|
+
// THE EYE - Single cyclopean sensor
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
261
|
+
const eyeGeo = trackGeo(new THREE.RingGeometry(0.08, 0.16, 6));
|
|
262
|
+
const eyeMat = trackMat<THREE.MeshBasicMaterial>(
|
|
263
|
+
new THREE.MeshBasicMaterial({
|
|
264
|
+
color: 0xffffff,
|
|
265
|
+
side: THREE.DoubleSide,
|
|
266
|
+
transparent: true,
|
|
267
|
+
opacity: 1.0,
|
|
268
|
+
blending: THREE.AdditiveBlending,
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
const eye = new THREE.Mesh(eyeGeo, eyeMat);
|
|
272
|
+
eye.position.set(0, 0, 0.36);
|
|
273
|
+
coreGroup.add(eye);
|
|
274
|
+
|
|
275
|
+
// Inner pupil
|
|
276
|
+
const pupilGeo = trackGeo(new THREE.CircleGeometry(0.06, 6));
|
|
277
|
+
const pupilMat = trackMat<THREE.MeshBasicMaterial>(
|
|
278
|
+
new THREE.MeshBasicMaterial({
|
|
279
|
+
color: 0xffffff,
|
|
280
|
+
transparent: true,
|
|
281
|
+
opacity: 1.0,
|
|
282
|
+
blending: THREE.AdditiveBlending,
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
const pupil = new THREE.Mesh(pupilGeo, pupilMat);
|
|
286
|
+
pupil.position.set(0, 0, 0.37);
|
|
287
|
+
coreGroup.add(pupil);
|
|
288
|
+
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
290
|
+
// ORBITING GEOMETRY - Arcane rings and fragments
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
292
|
+
const rings: RingData[] = [];
|
|
293
|
+
|
|
294
|
+
for (let i = 0; i < 3; i++) {
|
|
295
|
+
const radius = 0.7 + i * 0.25;
|
|
296
|
+
const segments = 32;
|
|
297
|
+
const points: THREE.Vector3[] = [];
|
|
298
|
+
|
|
299
|
+
for (let j = 0; j <= segments; j++) {
|
|
300
|
+
const theta = (j / segments) * Math.PI * 2;
|
|
301
|
+
// Add gaps in the ring for that broken/glitchy feel
|
|
302
|
+
if (j % 8 < 6 || i === 1) {
|
|
303
|
+
points.push(new THREE.Vector3(Math.cos(theta) * radius, Math.sin(theta) * radius, 0));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const geo = trackGeo(new THREE.BufferGeometry().setFromPoints(points));
|
|
308
|
+
const mat = trackMat<THREE.LineBasicMaterial>(
|
|
309
|
+
new THREE.LineBasicMaterial({
|
|
310
|
+
color: 0x888888,
|
|
311
|
+
transparent: true,
|
|
312
|
+
opacity: 0.5 + i * 0.15,
|
|
313
|
+
blending: THREE.AdditiveBlending,
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const ring = new THREE.Line(geo, mat);
|
|
318
|
+
|
|
319
|
+
// Tilt each ring differently
|
|
320
|
+
ring.rotation.x = (i * Math.PI) / 3 + Math.random() * 0.3;
|
|
321
|
+
ring.rotation.y = (i * Math.PI) / 4;
|
|
322
|
+
|
|
323
|
+
rings.push({
|
|
324
|
+
mesh: ring,
|
|
325
|
+
material: mat,
|
|
326
|
+
speed: 0.3 + i * 0.15,
|
|
327
|
+
axis: new THREE.Vector3(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(),
|
|
328
|
+
phase: Math.random() * Math.PI * 2,
|
|
329
|
+
wobblePhase: Math.random() * Math.PI * 2,
|
|
330
|
+
});
|
|
331
|
+
orbitGroup.add(ring);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
335
|
+
// FLOATING FRAGMENTS - Shattered pieces of alien geometry
|
|
336
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
337
|
+
const fragments: FragmentData[] = [];
|
|
338
|
+
const fragmentCount = 12;
|
|
339
|
+
|
|
340
|
+
for (let i = 0; i < fragmentCount; i++) {
|
|
341
|
+
let geo: THREE.BufferGeometry;
|
|
342
|
+
const shapeType = i % 4;
|
|
343
|
+
const size = 0.08 + Math.random() * 0.06;
|
|
344
|
+
|
|
345
|
+
switch (shapeType) {
|
|
346
|
+
case 0:
|
|
347
|
+
geo = trackGeo(new THREE.TetrahedronGeometry(size));
|
|
348
|
+
break;
|
|
349
|
+
case 1:
|
|
350
|
+
geo = trackGeo(new THREE.OctahedronGeometry(size));
|
|
351
|
+
break;
|
|
352
|
+
case 2:
|
|
353
|
+
geo = trackGeo(new THREE.BoxGeometry(size, size * 0.3, size * 0.3));
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
geo = trackGeo(new THREE.IcosahedronGeometry(size * 0.7, 0));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const mat = trackMat<THREE.MeshBasicMaterial>(
|
|
360
|
+
new THREE.MeshBasicMaterial({
|
|
361
|
+
color: 0x666666,
|
|
362
|
+
wireframe: Math.random() > 0.5,
|
|
363
|
+
transparent: true,
|
|
364
|
+
opacity: 0.6 + Math.random() * 0.3,
|
|
365
|
+
blending: THREE.AdditiveBlending,
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const mesh = new THREE.Mesh(geo, mat);
|
|
370
|
+
|
|
371
|
+
const orbitRadius = 1.6;
|
|
372
|
+
const orbitOffset = (i / fragmentCount) * Math.PI * 2;
|
|
373
|
+
|
|
374
|
+
mesh.position.set(
|
|
375
|
+
Math.cos(orbitOffset) * orbitRadius,
|
|
376
|
+
(Math.random() - 0.5) * 0.4,
|
|
377
|
+
Math.sin(orbitOffset) * orbitRadius
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
|
|
381
|
+
|
|
382
|
+
fragments.push({
|
|
383
|
+
mesh,
|
|
384
|
+
material: mat,
|
|
385
|
+
orbitRadius,
|
|
386
|
+
orbitSpeed: 0.4,
|
|
387
|
+
orbitAngle: orbitOffset,
|
|
388
|
+
bobSpeed: 1 + Math.random() * 1.5,
|
|
389
|
+
bobPhase: Math.random() * Math.PI * 2,
|
|
390
|
+
});
|
|
391
|
+
fragmentGroup.add(mesh);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
395
|
+
// GLITCH PARTICLES - Digital noise in the void
|
|
396
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
397
|
+
const particleCount = 60;
|
|
398
|
+
const pGeo = trackGeo(new THREE.BufferGeometry());
|
|
399
|
+
const pPos = new Float32Array(particleCount * 3);
|
|
400
|
+
const particleVelocities: ParticleVelocity[] = [];
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < particleCount; i++) {
|
|
403
|
+
const r = 1.5 + Math.random() * 2.5;
|
|
404
|
+
const theta = Math.random() * Math.PI * 2;
|
|
405
|
+
const phi = Math.acos(Math.random() * 2 - 1);
|
|
406
|
+
|
|
407
|
+
pPos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
|
408
|
+
pPos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
|
409
|
+
pPos[i * 3 + 2] = r * Math.cos(phi);
|
|
410
|
+
|
|
411
|
+
particleVelocities.push({
|
|
412
|
+
x: (Math.random() - 0.5) * 0.15,
|
|
413
|
+
y: (Math.random() - 0.5) * 0.15,
|
|
414
|
+
z: (Math.random() - 0.5) * 0.15,
|
|
415
|
+
phase: Math.random() * Math.PI * 2,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
pGeo.setAttribute("position", new THREE.BufferAttribute(pPos, 3));
|
|
420
|
+
const particleMat = trackMat<THREE.PointsMaterial>(
|
|
421
|
+
new THREE.PointsMaterial({
|
|
422
|
+
color: 0x888888,
|
|
423
|
+
size: 0.025,
|
|
424
|
+
transparent: true,
|
|
425
|
+
opacity: 0.5,
|
|
426
|
+
blending: THREE.AdditiveBlending,
|
|
427
|
+
})
|
|
428
|
+
);
|
|
429
|
+
const particleSystem = new THREE.Points(pGeo, particleMat);
|
|
430
|
+
scene.add(particleSystem);
|
|
431
|
+
const particlePos = particleSystem.geometry.attributes.position as THREE.BufferAttribute;
|
|
432
|
+
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
434
|
+
// SIGIL LINES - Connecting fragments like a constellation
|
|
435
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
436
|
+
const sigilGeo = trackGeo(new THREE.BufferGeometry());
|
|
437
|
+
const sigilPositions = new Float32Array(fragmentCount * 2 * 3);
|
|
438
|
+
sigilGeo.setAttribute("position", new THREE.BufferAttribute(sigilPositions, 3));
|
|
439
|
+
|
|
440
|
+
const sigilMat = trackMat<THREE.LineBasicMaterial>(
|
|
441
|
+
new THREE.LineBasicMaterial({
|
|
442
|
+
color: 0x444444,
|
|
443
|
+
transparent: true,
|
|
444
|
+
opacity: 0.3,
|
|
445
|
+
blending: THREE.AdditiveBlending,
|
|
446
|
+
})
|
|
447
|
+
);
|
|
448
|
+
const sigilLines = new THREE.LineSegments(sigilGeo, sigilMat);
|
|
449
|
+
scene.add(sigilLines);
|
|
450
|
+
const sigilPos = sigilLines.geometry.attributes.position as THREE.BufferAttribute;
|
|
451
|
+
|
|
452
|
+
// Central light
|
|
453
|
+
const pointLight = new THREE.PointLight(0xffffff, 0.8, 6);
|
|
454
|
+
pointLight.position.set(0, 0, 0.5);
|
|
455
|
+
coreGroup.add(pointLight);
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
mainAnchor,
|
|
459
|
+
coreGroup,
|
|
460
|
+
orbitGroup,
|
|
461
|
+
fragmentGroup,
|
|
462
|
+
coreMesh,
|
|
463
|
+
glowMesh,
|
|
464
|
+
glowMat,
|
|
465
|
+
eye,
|
|
466
|
+
eyeMat,
|
|
467
|
+
pupil,
|
|
468
|
+
pupilMat,
|
|
469
|
+
rings,
|
|
470
|
+
fragments,
|
|
471
|
+
particleSystem,
|
|
472
|
+
particleMat,
|
|
473
|
+
particlePos,
|
|
474
|
+
particleVelocities,
|
|
475
|
+
sigilLines,
|
|
476
|
+
sigilMat,
|
|
477
|
+
sigilPos,
|
|
478
|
+
pointLight,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
483
|
+
// STATE FACTORY
|
|
484
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
485
|
+
|
|
486
|
+
const DEFAULT_THEME: AvatarColorTheme = {
|
|
487
|
+
primary: 0x9ca3af,
|
|
488
|
+
glow: 0x67e8f9,
|
|
489
|
+
eye: 0xff0000,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
function createInitialState() {
|
|
493
|
+
return {
|
|
494
|
+
phase: {
|
|
495
|
+
drift: 0,
|
|
496
|
+
corePulse: 0,
|
|
497
|
+
eyePulse: 0,
|
|
498
|
+
pupilPulse: 0,
|
|
499
|
+
sigilPulse: 0,
|
|
500
|
+
} as PhaseState,
|
|
501
|
+
|
|
502
|
+
intensity: {
|
|
503
|
+
current: 0,
|
|
504
|
+
target: 0,
|
|
505
|
+
spinBoost: 0,
|
|
506
|
+
} as IntensityState,
|
|
507
|
+
|
|
508
|
+
audio: {
|
|
509
|
+
current: 0,
|
|
510
|
+
target: 0,
|
|
511
|
+
} as AudioState,
|
|
512
|
+
|
|
513
|
+
glitch: {
|
|
514
|
+
timer: 0,
|
|
515
|
+
isActive: false,
|
|
516
|
+
duration: 0,
|
|
517
|
+
} as GlitchState,
|
|
518
|
+
|
|
519
|
+
tool: {
|
|
520
|
+
active: false,
|
|
521
|
+
flashTimer: 0,
|
|
522
|
+
flashColor: 0xffffff,
|
|
523
|
+
fragmentScatterBoost: 0,
|
|
524
|
+
sigilBrightnessBoost: 0,
|
|
525
|
+
settleTimer: 0,
|
|
526
|
+
} as ToolState,
|
|
527
|
+
|
|
528
|
+
reasoning: {
|
|
529
|
+
active: false,
|
|
530
|
+
blend: 0,
|
|
531
|
+
} as ReasoningState,
|
|
532
|
+
|
|
533
|
+
typing: {
|
|
534
|
+
active: false,
|
|
535
|
+
pulse: 0,
|
|
536
|
+
eyeScanTimer: 0,
|
|
537
|
+
eyeScanInterval: 0.5,
|
|
538
|
+
} as TypingState,
|
|
539
|
+
|
|
540
|
+
idleMicroGlitch: {
|
|
541
|
+
timer: 0,
|
|
542
|
+
cooldown: 3 + Math.random() * 5,
|
|
543
|
+
active: false,
|
|
544
|
+
duration: 0,
|
|
545
|
+
} as IdleMicroGlitchState,
|
|
546
|
+
|
|
547
|
+
eyeDrift: {
|
|
548
|
+
x: 0,
|
|
549
|
+
y: 0,
|
|
550
|
+
targetX: 0,
|
|
551
|
+
targetY: 0,
|
|
552
|
+
timer: 0,
|
|
553
|
+
interval: 1.5 + Math.random() * 2,
|
|
554
|
+
} as EyeDriftState,
|
|
555
|
+
|
|
556
|
+
particlePulse: {
|
|
557
|
+
timer: 0,
|
|
558
|
+
interval: 1 + Math.random() * 2,
|
|
559
|
+
brightness: 0,
|
|
560
|
+
} as ParticlePulseState,
|
|
561
|
+
|
|
562
|
+
coreDrift: {
|
|
563
|
+
x: 0,
|
|
564
|
+
y: 0,
|
|
565
|
+
z: 0,
|
|
566
|
+
phaseX: Math.random() * Math.PI * 2,
|
|
567
|
+
phaseY: Math.random() * Math.PI * 2,
|
|
568
|
+
phaseZ: Math.random() * Math.PI * 2,
|
|
569
|
+
} as CoreDriftState,
|
|
570
|
+
|
|
571
|
+
theme: {
|
|
572
|
+
current: { ...DEFAULT_THEME },
|
|
573
|
+
target: { ...DEFAULT_THEME },
|
|
574
|
+
} as ThemeState,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
type RigState = ReturnType<typeof createInitialState>;
|
|
579
|
+
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
581
|
+
// UPDATE SUBSYSTEMS - Each handles a specific aspect of the animation
|
|
582
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
583
|
+
|
|
584
|
+
function updateIntensityAndAudio(state: RigState, dt: number): number {
|
|
585
|
+
const { intensity, audio, typing, reasoning } = state;
|
|
586
|
+
|
|
587
|
+
typing.pulse = Math.max(0, typing.pulse - dt * 5);
|
|
588
|
+
|
|
589
|
+
// Faster decay than rise so avatar "calms down" quickly when returning to IDLE
|
|
590
|
+
const intensityRate = intensity.target > intensity.current ? 10 : 8;
|
|
591
|
+
intensity.current += (intensity.target - intensity.current) * dt * intensityRate;
|
|
592
|
+
|
|
593
|
+
intensity.spinBoost += (0 - intensity.spinBoost) * dt * 2.5;
|
|
594
|
+
audio.current += (audio.target - audio.current) * dt * 25;
|
|
595
|
+
reasoning.blend += ((reasoning.active ? 1 : 0) - reasoning.blend) * dt * 6;
|
|
596
|
+
|
|
597
|
+
return intensity.current;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function updateMainAnchor(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
601
|
+
const { phase, audio } = state;
|
|
602
|
+
|
|
603
|
+
const driftSpeed = 0.08 + intensity * 0.3;
|
|
604
|
+
phase.drift += dt * driftSpeed;
|
|
605
|
+
const driftAmount = 0.03 + intensity * 0.12;
|
|
606
|
+
|
|
607
|
+
elements.mainAnchor.rotation.y = Math.sin(phase.drift) * driftAmount;
|
|
608
|
+
elements.mainAnchor.rotation.x = Math.sin(phase.drift * 0.7) * driftAmount * 0.5;
|
|
609
|
+
elements.mainAnchor.rotation.z = Math.sin(phase.drift * 0.5) * intensity * 0.08;
|
|
610
|
+
elements.mainAnchor.scale.setScalar(1 + audio.current * 0.12);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function updateCore(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
614
|
+
const { phase, audio, reasoning } = state;
|
|
615
|
+
|
|
616
|
+
const coreSpeed = 0.1 + intensity * 0.9;
|
|
617
|
+
elements.glowMesh.rotation.y += dt * coreSpeed;
|
|
618
|
+
elements.glowMesh.rotation.x += dt * coreSpeed * 0.6;
|
|
619
|
+
|
|
620
|
+
const glowScale = 1 + intensity * 0.25;
|
|
621
|
+
elements.glowMesh.scale.setScalar(glowScale);
|
|
622
|
+
elements.glowMat.opacity = clamp01(0.4 + intensity * 0.4 + audio.current * 0.25);
|
|
623
|
+
const normalCorePulseSpeed = 1 + intensity * 4;
|
|
624
|
+
const reasoningCorePulseSpeed = 0.4;
|
|
625
|
+
const corePulseSpeed =
|
|
626
|
+
normalCorePulseSpeed * (1 - reasoning.blend) + reasoningCorePulseSpeed * reasoning.blend;
|
|
627
|
+
phase.corePulse += dt * corePulseSpeed;
|
|
628
|
+
|
|
629
|
+
const normalCorePulseAmount = 0.01 + intensity * 0.15;
|
|
630
|
+
const reasoningCorePulseAmount = 0.08;
|
|
631
|
+
const corePulseAmount =
|
|
632
|
+
normalCorePulseAmount * (1 - reasoning.blend) + reasoningCorePulseAmount * reasoning.blend;
|
|
633
|
+
const corePulse = 1 + Math.sin(phase.corePulse) * corePulseAmount;
|
|
634
|
+
|
|
635
|
+
elements.coreGroup.scale.setScalar(corePulse * (1 + audio.current * 0.18));
|
|
636
|
+
|
|
637
|
+
// Core squash-stretch - vertical stretch based on audio amplitude
|
|
638
|
+
const squashStretch = 1 + audio.current * 0.15;
|
|
639
|
+
elements.coreMesh.scale.set(1, squashStretch, 1);
|
|
640
|
+
elements.glowMesh.scale.y *= squashStretch;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function updateEye(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
644
|
+
const { phase, audio, typing, reasoning, tool, theme } = state;
|
|
645
|
+
|
|
646
|
+
const eyeSpeed = 1.5 + intensity * 4;
|
|
647
|
+
phase.eyePulse += dt * eyeSpeed;
|
|
648
|
+
const eyePulseAmount = 0.1 + intensity * 0.3 + typing.pulse * 0.1;
|
|
649
|
+
const eyePulse = 0.9 + Math.sin(phase.eyePulse) * eyePulseAmount;
|
|
650
|
+
elements.eye.scale.setScalar(eyePulse * (1 + audio.current * 0.1));
|
|
651
|
+
|
|
652
|
+
const pupilSpeed = 2 + intensity * 5;
|
|
653
|
+
phase.pupilPulse += dt * pupilSpeed;
|
|
654
|
+
const pupilPulseAmount = 0.15 + intensity * 0.35 + typing.pulse * 0.15;
|
|
655
|
+
const normalPupilBase = 0.8 + Math.sin(phase.pupilPulse) * pupilPulseAmount;
|
|
656
|
+
const reasoningPupilDilation = 1.4;
|
|
657
|
+
const pupilBase = normalPupilBase * (1 - reasoning.blend) + reasoningPupilDilation * reasoning.blend;
|
|
658
|
+
elements.pupil.scale.setScalar(pupilBase * (1 + audio.current * 0.08));
|
|
659
|
+
|
|
660
|
+
if (tool.flashTimer > 0) {
|
|
661
|
+
tool.flashTimer -= dt;
|
|
662
|
+
const flashIntensity = clamp01(tool.flashTimer / 0.15);
|
|
663
|
+
elements.eyeMat.color.setHex(lerpColor(theme.current.eye, tool.flashColor, flashIntensity));
|
|
664
|
+
elements.pupilMat.color.setHex(lerpColor(theme.current.eye, tool.flashColor, flashIntensity));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
elements.eyeMat.opacity = clamp01(0.85 + intensity * 0.15 + audio.current * 0.15);
|
|
668
|
+
elements.pupilMat.opacity = clamp01(0.9 + intensity * 0.1 + audio.current * 0.1);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function updateRings(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
672
|
+
const { audio, intensity: intensityState } = state;
|
|
673
|
+
|
|
674
|
+
// Scale the entire orbit group based on intensity
|
|
675
|
+
const orbitScale = 0.75 + intensity * 0.4;
|
|
676
|
+
elements.orbitGroup.scale.setScalar(orbitScale);
|
|
677
|
+
|
|
678
|
+
elements.rings.forEach((ring, i) => {
|
|
679
|
+
// Use a gentle curve so small intensity changes don't disproportionately increase angular velocity
|
|
680
|
+
const ringIntensity = Math.pow(intensity, 1.35);
|
|
681
|
+
const ringSpeed = ring.speed * (0.4 + ringIntensity * 1.5) + intensityState.spinBoost * (1 + i * 0.2);
|
|
682
|
+
ring.mesh.rotateOnAxis(ring.axis, dt * ringSpeed);
|
|
683
|
+
|
|
684
|
+
// Ring wobble with audio - oscillatory wobble around perpendicular axes
|
|
685
|
+
const wobbleSpeed = 3 + i * 0.5;
|
|
686
|
+
ring.wobblePhase += dt * wobbleSpeed;
|
|
687
|
+
const wobbleAmount = audio.current * 0.06;
|
|
688
|
+
const wobbleX = Math.sin(ring.wobblePhase) * wobbleAmount;
|
|
689
|
+
const wobbleZ = Math.cos(ring.wobblePhase * 1.3) * wobbleAmount;
|
|
690
|
+
ring.mesh.rotation.x += wobbleX;
|
|
691
|
+
ring.mesh.rotation.z += wobbleZ;
|
|
692
|
+
|
|
693
|
+
// Update opacity phase
|
|
694
|
+
const phaseSpeed = 1 + intensity * 3;
|
|
695
|
+
ring.phase += dt * phaseSpeed;
|
|
696
|
+
|
|
697
|
+
// Ring opacity: visible in idle, bright when active
|
|
698
|
+
const baseOpacity = 0.4 + intensity * 0.4 + i * 0.1;
|
|
699
|
+
if (intensity > 0.1) {
|
|
700
|
+
const wave = Math.sin(ring.phase + i * 1.5) * 0.2 * intensity;
|
|
701
|
+
ring.material.opacity = clamp01(baseOpacity + wave + audio.current * 0.25);
|
|
702
|
+
} else {
|
|
703
|
+
ring.material.opacity = clamp01(baseOpacity + audio.current * 0.25);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function updateFragments(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
709
|
+
const { audio, tool, reasoning, intensity: intensityState } = state;
|
|
710
|
+
|
|
711
|
+
// Decay tool effects
|
|
712
|
+
tool.fragmentScatterBoost += (0 - tool.fragmentScatterBoost) * dt * 6;
|
|
713
|
+
tool.settleTimer = Math.max(0, tool.settleTimer - dt);
|
|
714
|
+
|
|
715
|
+
const settleContraction = tool.settleTimer > 0 ? Math.sin(tool.settleTimer * 20) * 0.08 : 0;
|
|
716
|
+
const reasoningContraction = reasoning.blend * 0.25;
|
|
717
|
+
const fragmentScale =
|
|
718
|
+
0.5 + intensity * 0.6 + tool.fragmentScatterBoost - settleContraction - reasoningContraction;
|
|
719
|
+
elements.fragmentGroup.scale.setScalar(fragmentScale);
|
|
720
|
+
|
|
721
|
+
elements.fragments.forEach((frag) => {
|
|
722
|
+
const reasoningOrbitSlowdown = 1 - reasoning.blend * 0.6;
|
|
723
|
+
// Audio-modulated orbit speed - fragments orbit faster with audio
|
|
724
|
+
const audioOrbitBoost = audio.current * 0.4;
|
|
725
|
+
const orbitSpeed =
|
|
726
|
+
frag.orbitSpeed * (0.4 + intensity * 1.2 + audioOrbitBoost) * reasoningOrbitSlowdown +
|
|
727
|
+
intensityState.spinBoost * 0.5;
|
|
728
|
+
frag.orbitAngle += dt * orbitSpeed;
|
|
729
|
+
|
|
730
|
+
const dynamicRadius = frag.orbitRadius;
|
|
731
|
+
const bobAmount = 0.08 + intensity * 0.2;
|
|
732
|
+
const bobSpeed = frag.bobSpeed * (0.5 + intensity * 1.0);
|
|
733
|
+
frag.bobPhase += dt * bobSpeed;
|
|
734
|
+
|
|
735
|
+
const bob = Math.sin(frag.bobPhase) * bobAmount;
|
|
736
|
+
|
|
737
|
+
frag.mesh.position.x = Math.cos(frag.orbitAngle) * dynamicRadius;
|
|
738
|
+
frag.mesh.position.z = Math.sin(frag.orbitAngle) * dynamicRadius;
|
|
739
|
+
frag.mesh.position.y += (bob - frag.mesh.position.y) * dt * 3;
|
|
740
|
+
|
|
741
|
+
// Tumble speed
|
|
742
|
+
const tumbleSpeed = 0.1 + intensity * 0.5;
|
|
743
|
+
frag.mesh.rotation.x += dt * tumbleSpeed;
|
|
744
|
+
frag.mesh.rotation.y += dt * tumbleSpeed * 1.5;
|
|
745
|
+
|
|
746
|
+
// Fragment opacity
|
|
747
|
+
frag.material.opacity = clamp01(0.45 + intensity * 0.4 + audio.current * 0.22);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function updateSigils(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
752
|
+
const { phase, audio, tool } = state;
|
|
753
|
+
|
|
754
|
+
// Update sigil line positions (connect fragments)
|
|
755
|
+
for (let i = 0; i < elements.fragments.length; i++) {
|
|
756
|
+
const curr = elements.fragments[i]!;
|
|
757
|
+
const next = elements.fragments[(i + 1) % elements.fragments.length]!;
|
|
758
|
+
|
|
759
|
+
elements.sigilPos.setXYZ(i * 2, curr.mesh.position.x, curr.mesh.position.y, curr.mesh.position.z);
|
|
760
|
+
elements.sigilPos.setXYZ(i * 2 + 1, next.mesh.position.x, next.mesh.position.y, next.mesh.position.z);
|
|
761
|
+
}
|
|
762
|
+
elements.sigilPos.needsUpdate = true;
|
|
763
|
+
|
|
764
|
+
// Sigil opacity with tool brightness boost
|
|
765
|
+
const sigilPulseSpeed = 1 + intensity * 2;
|
|
766
|
+
phase.sigilPulse += dt * sigilPulseSpeed;
|
|
767
|
+
|
|
768
|
+
tool.sigilBrightnessBoost += ((tool.active ? 1 : 0) - tool.sigilBrightnessBoost) * dt * 8;
|
|
769
|
+
|
|
770
|
+
const sigilBaseOpacity = 0.2 + intensity * 0.2 + tool.sigilBrightnessBoost * 0.5;
|
|
771
|
+
const sigilPulseAmount = 0.05 + intensity * 0.12;
|
|
772
|
+
elements.sigilMat.opacity = clamp01(
|
|
773
|
+
sigilBaseOpacity + Math.sin(phase.sigilPulse) * sigilPulseAmount + audio.current * 0.2
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function updateParticles(
|
|
778
|
+
elements: SceneElements,
|
|
779
|
+
state: RigState,
|
|
780
|
+
dt: number,
|
|
781
|
+
intensity: number,
|
|
782
|
+
allowGlitch: boolean
|
|
783
|
+
): void {
|
|
784
|
+
const { audio, particlePulse } = state;
|
|
785
|
+
const particleCount = elements.particleVelocities.length;
|
|
786
|
+
|
|
787
|
+
const glitchChance = allowGlitch ? 0.0002 + intensity * 0.006 : 0;
|
|
788
|
+
// Particle dance intensity - velocity multiplier increases with audio
|
|
789
|
+
const audioJitterBoost = 1 + audio.current * 0.8;
|
|
790
|
+
const particleSpeedMult = (0.3 + intensity * 1.2) * audioJitterBoost;
|
|
791
|
+
|
|
792
|
+
for (let i = 0; i < particleCount; i++) {
|
|
793
|
+
const vel = elements.particleVelocities[i]!;
|
|
794
|
+
let x = elements.particlePos.getX(i) + vel.x * dt * particleSpeedMult;
|
|
795
|
+
let y = elements.particlePos.getY(i) + vel.y * dt * particleSpeedMult;
|
|
796
|
+
let z = elements.particlePos.getZ(i) + vel.z * dt * particleSpeedMult;
|
|
797
|
+
|
|
798
|
+
// Occasional teleport glitch
|
|
799
|
+
if (Math.random() < glitchChance) {
|
|
800
|
+
const r = 1.5 + Math.random() * 2;
|
|
801
|
+
const theta = Math.random() * Math.PI * 2;
|
|
802
|
+
const phi = Math.acos(Math.random() * 2 - 1);
|
|
803
|
+
x = r * Math.sin(phi) * Math.cos(theta);
|
|
804
|
+
y = r * Math.sin(phi) * Math.sin(theta);
|
|
805
|
+
z = r * Math.cos(phi);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Boundary wrap
|
|
809
|
+
const distSq = x * x + y * y + z * z;
|
|
810
|
+
if (distSq > 20) {
|
|
811
|
+
x *= -0.8;
|
|
812
|
+
y *= -0.8;
|
|
813
|
+
z *= -0.8;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
elements.particlePos.setXYZ(i, x, y, z);
|
|
817
|
+
}
|
|
818
|
+
elements.particlePos.needsUpdate = true;
|
|
819
|
+
|
|
820
|
+
// Particle appearance
|
|
821
|
+
const idleParticleBoost = particlePulse.brightness * 0.4;
|
|
822
|
+
elements.particleMat.opacity = clamp01(0.3 + intensity * 0.4 + audio.current * 0.25 + idleParticleBoost);
|
|
823
|
+
elements.particleMat.size =
|
|
824
|
+
0.02 + intensity * 0.015 + audio.current * 0.01 + particlePulse.brightness * 0.02;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function updateGlitchBehavior(
|
|
828
|
+
elements: SceneElements,
|
|
829
|
+
state: RigState,
|
|
830
|
+
dt: number,
|
|
831
|
+
intensity: number,
|
|
832
|
+
allowGlitch: boolean
|
|
833
|
+
): void {
|
|
834
|
+
const { glitch } = state;
|
|
835
|
+
|
|
836
|
+
if (!allowGlitch) {
|
|
837
|
+
glitch.timer = 0;
|
|
838
|
+
if (glitch.isActive) {
|
|
839
|
+
glitch.isActive = false;
|
|
840
|
+
elements.coreGroup.position.set(0, 0, 0);
|
|
841
|
+
elements.fragmentGroup.scale.set(1, 1, 1);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
glitch.timer += dt;
|
|
847
|
+
|
|
848
|
+
const intensityFactor = Math.max(0.1, intensity);
|
|
849
|
+
const baseInterval = 3.0 / intensityFactor;
|
|
850
|
+
const randomFactor = 0.5 + Math.random();
|
|
851
|
+
const glitchInterval = baseInterval * randomFactor;
|
|
852
|
+
|
|
853
|
+
if (!glitch.isActive && glitch.timer > glitchInterval) {
|
|
854
|
+
glitch.isActive = true;
|
|
855
|
+
glitch.duration = 0.05 + Math.random() * 0.1 + intensity * 0.05;
|
|
856
|
+
glitch.timer = 0;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (glitch.isActive) {
|
|
860
|
+
glitch.duration -= dt;
|
|
861
|
+
|
|
862
|
+
const displaceMult = intensity * 0.5;
|
|
863
|
+
|
|
864
|
+
elements.coreGroup.position.set(
|
|
865
|
+
(Math.random() - 0.5) * 0.05 * displaceMult,
|
|
866
|
+
(Math.random() - 0.5) * 0.05 * displaceMult,
|
|
867
|
+
(Math.random() - 0.5) * 0.03 * displaceMult
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
const scatterAmount = 1.0 + (Math.random() - 0.5) * intensity * 0.15;
|
|
871
|
+
elements.fragmentGroup.scale.setScalar(scatterAmount);
|
|
872
|
+
|
|
873
|
+
elements.rings.forEach((ring, i) => {
|
|
874
|
+
const baseOpacity = 0.5 + i * 0.15;
|
|
875
|
+
const flickerRange = intensity * 0.2;
|
|
876
|
+
ring.material.opacity = baseOpacity * (1 - flickerRange + Math.random() * flickerRange * 2);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
if (glitch.duration <= 0) {
|
|
880
|
+
glitch.isActive = false;
|
|
881
|
+
elements.coreGroup.position.set(0, 0, 0);
|
|
882
|
+
elements.fragmentGroup.scale.set(1, 1, 1);
|
|
883
|
+
elements.rings.forEach((ring, i) => {
|
|
884
|
+
ring.material.opacity = 0.5 + i * 0.15;
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function updateIdleAmbience(elements: SceneElements, state: RigState, dt: number, isIdle: boolean): void {
|
|
891
|
+
const { idleMicroGlitch, eyeDrift, particlePulse, coreDrift, typing } = state;
|
|
892
|
+
|
|
893
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
894
|
+
// Idle micro-glitches
|
|
895
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
896
|
+
if (isIdle) {
|
|
897
|
+
idleMicroGlitch.timer += dt;
|
|
898
|
+
if (!idleMicroGlitch.active && idleMicroGlitch.timer > idleMicroGlitch.cooldown) {
|
|
899
|
+
idleMicroGlitch.active = true;
|
|
900
|
+
idleMicroGlitch.duration = 0.08 + Math.random() * 0.12;
|
|
901
|
+
idleMicroGlitch.timer = 0;
|
|
902
|
+
idleMicroGlitch.cooldown = 3 + Math.random() * 5;
|
|
903
|
+
}
|
|
904
|
+
if (idleMicroGlitch.active) {
|
|
905
|
+
idleMicroGlitch.duration -= dt;
|
|
906
|
+
const jitterAmount = 0.06;
|
|
907
|
+
elements.coreGroup.position.x += (Math.random() - 0.5) * jitterAmount;
|
|
908
|
+
elements.coreGroup.position.y += (Math.random() - 0.5) * jitterAmount;
|
|
909
|
+
if (idleMicroGlitch.duration <= 0) {
|
|
910
|
+
idleMicroGlitch.active = false;
|
|
911
|
+
elements.coreGroup.position.x = 0;
|
|
912
|
+
elements.coreGroup.position.y = 0;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
idleMicroGlitch.timer = 0;
|
|
917
|
+
idleMicroGlitch.active = false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
921
|
+
// Eye drift behavior (typing mode, idle, or centering)
|
|
922
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
923
|
+
if (typing.active) {
|
|
924
|
+
typing.eyeScanTimer += dt;
|
|
925
|
+
if (typing.eyeScanTimer > typing.eyeScanInterval) {
|
|
926
|
+
typing.eyeScanTimer = 0;
|
|
927
|
+
const scanWidth = 0.2;
|
|
928
|
+
eyeDrift.targetX = (Math.random() - 0.5) * scanWidth;
|
|
929
|
+
eyeDrift.targetY = (Math.random() - 0.5) * 0.05;
|
|
930
|
+
typing.eyeScanInterval = 0.3 + Math.random() * 0.8;
|
|
931
|
+
}
|
|
932
|
+
const trackSpeed = 5;
|
|
933
|
+
eyeDrift.x += (eyeDrift.targetX - eyeDrift.x) * dt * trackSpeed;
|
|
934
|
+
eyeDrift.y += (eyeDrift.targetY - eyeDrift.y) * dt * trackSpeed;
|
|
935
|
+
} else if (isIdle) {
|
|
936
|
+
eyeDrift.timer += dt;
|
|
937
|
+
if (eyeDrift.timer > eyeDrift.interval) {
|
|
938
|
+
eyeDrift.timer = 0;
|
|
939
|
+
// Frequent scary fast snaps (60%) mixed with slow drifting
|
|
940
|
+
const isFast = Math.random() > 0.4;
|
|
941
|
+
eyeDrift.interval = isFast ? 0.15 + Math.random() * 0.25 : 2.0 + Math.random() * 3.0;
|
|
942
|
+
eyeDrift.targetX = (Math.random() - 0.5) * 0.25;
|
|
943
|
+
eyeDrift.targetY = (Math.random() - 0.5) * 0.15;
|
|
944
|
+
}
|
|
945
|
+
// Scary fast snap (30x) vs very slow drift (1.5x)
|
|
946
|
+
const interpSpeed = eyeDrift.interval < 0.5 ? 30 : 1.5;
|
|
947
|
+
eyeDrift.x += (eyeDrift.targetX - eyeDrift.x) * dt * interpSpeed;
|
|
948
|
+
eyeDrift.y += (eyeDrift.targetY - eyeDrift.y) * dt * interpSpeed;
|
|
949
|
+
} else {
|
|
950
|
+
// Return to center when active
|
|
951
|
+
eyeDrift.x += (0 - eyeDrift.x) * dt * 4;
|
|
952
|
+
eyeDrift.y += (0 - eyeDrift.y) * dt * 4;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Apply eye drift position
|
|
956
|
+
elements.eye.position.x = eyeDrift.x;
|
|
957
|
+
elements.eye.position.y = eyeDrift.y;
|
|
958
|
+
elements.pupil.position.x = eyeDrift.x;
|
|
959
|
+
elements.pupil.position.y = eyeDrift.y;
|
|
960
|
+
|
|
961
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
962
|
+
// Particle brightness pulses in idle
|
|
963
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
964
|
+
if (isIdle) {
|
|
965
|
+
particlePulse.timer += dt;
|
|
966
|
+
if (particlePulse.timer > particlePulse.interval) {
|
|
967
|
+
particlePulse.timer = 0;
|
|
968
|
+
particlePulse.interval = 1 + Math.random() * 2;
|
|
969
|
+
particlePulse.brightness = 1.0;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
particlePulse.brightness = Math.max(0, particlePulse.brightness - dt * 1.5);
|
|
973
|
+
|
|
974
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
975
|
+
// Core micro-drift in idle
|
|
976
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
977
|
+
const coreDriftSpeed = 0.4;
|
|
978
|
+
const coreDriftAmount = 0.08;
|
|
979
|
+
coreDrift.phaseX += dt * coreDriftSpeed;
|
|
980
|
+
coreDrift.phaseY += dt * coreDriftSpeed * 0.7;
|
|
981
|
+
coreDrift.phaseZ += dt * coreDriftSpeed * 0.5;
|
|
982
|
+
|
|
983
|
+
const targetDriftX = Math.sin(coreDrift.phaseX) * coreDriftAmount;
|
|
984
|
+
const targetDriftY = Math.sin(coreDrift.phaseY) * coreDriftAmount;
|
|
985
|
+
const targetDriftZ = Math.sin(coreDrift.phaseZ) * coreDriftAmount * 0.5;
|
|
986
|
+
|
|
987
|
+
coreDrift.x += (targetDriftX - coreDrift.x) * dt * 2;
|
|
988
|
+
coreDrift.y += (targetDriftY - coreDrift.y) * dt * 2;
|
|
989
|
+
coreDrift.z += (targetDriftZ - coreDrift.z) * dt * 2;
|
|
990
|
+
|
|
991
|
+
elements.mainAnchor.position.set(coreDrift.x, coreDrift.y, coreDrift.z);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function updateColors(elements: SceneElements, state: RigState, dt: number): void {
|
|
995
|
+
const { theme, typing, tool } = state;
|
|
996
|
+
|
|
997
|
+
const t = dt * 4;
|
|
998
|
+
theme.current.primary = lerpColor(theme.current.primary, theme.target.primary, t);
|
|
999
|
+
theme.current.glow = lerpColor(theme.current.glow, theme.target.glow, t);
|
|
1000
|
+
theme.current.eye = lerpColor(theme.current.eye, theme.target.eye, t);
|
|
1001
|
+
|
|
1002
|
+
let displayPrimary = theme.current.primary;
|
|
1003
|
+
let displayEye = theme.current.eye;
|
|
1004
|
+
|
|
1005
|
+
// Typing flash effect
|
|
1006
|
+
if (typing.pulse > 0.01) {
|
|
1007
|
+
const flashStrength = Math.pow(typing.pulse, 1.5) * 0.5;
|
|
1008
|
+
displayPrimary = lerpColor(displayPrimary, 0xffffff, flashStrength);
|
|
1009
|
+
displayEye = lerpColor(displayEye, 0xff8888, flashStrength * 0.3);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Apply colors to materials
|
|
1013
|
+
elements.glowMat.color.setHex(displayPrimary);
|
|
1014
|
+
if (tool.flashTimer <= 0) {
|
|
1015
|
+
elements.eyeMat.color.setHex(displayEye);
|
|
1016
|
+
elements.pupilMat.color.setHex(displayEye);
|
|
1017
|
+
}
|
|
1018
|
+
elements.pointLight.color.setHex(theme.current.glow);
|
|
1019
|
+
|
|
1020
|
+
// Update rings and fragments
|
|
1021
|
+
elements.rings.forEach((r) => (r.mesh.material as THREE.LineBasicMaterial).color.setHex(displayPrimary));
|
|
1022
|
+
elements.fragments.forEach((f) => f.material.color.setHex(displayPrimary));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1026
|
+
// MAIN RIG FACTORY
|
|
1027
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Creates the DAEMON avatar - An eldritch geometric entity
|
|
1031
|
+
* Alien technology glitching through dimensions
|
|
1032
|
+
*/
|
|
1033
|
+
export function createDaemonRig(options: { aspectRatio: number }): DaemonRig {
|
|
1034
|
+
const scene = new THREE.Scene();
|
|
1035
|
+
|
|
1036
|
+
const camera = new THREE.PerspectiveCamera(28, options.aspectRatio, 0.1, 100);
|
|
1037
|
+
camera.position.set(0, 0, 8);
|
|
1038
|
+
camera.lookAt(0, 0, 0);
|
|
1039
|
+
|
|
1040
|
+
// Resource tracking for disposal
|
|
1041
|
+
const disposables: { dispose(): void }[] = [];
|
|
1042
|
+
const trackGeo = <T extends THREE.BufferGeometry>(g: T): T => {
|
|
1043
|
+
disposables.push(g);
|
|
1044
|
+
return g;
|
|
1045
|
+
};
|
|
1046
|
+
const trackMat = <T extends THREE.Material>(m: T): T => {
|
|
1047
|
+
disposables.push(m);
|
|
1048
|
+
return m;
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// Create all scene elements
|
|
1052
|
+
const elements = createSceneElements(scene, trackGeo, trackMat);
|
|
1053
|
+
|
|
1054
|
+
// Initialize state
|
|
1055
|
+
const state = createInitialState();
|
|
1056
|
+
|
|
1057
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
1058
|
+
// MAIN UPDATE LOOP
|
|
1059
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
1060
|
+
function update(deltaS: number): void {
|
|
1061
|
+
const dt = Math.min(0.1, deltaS);
|
|
1062
|
+
state.glitch.timer += dt;
|
|
1063
|
+
|
|
1064
|
+
// Update core animation state
|
|
1065
|
+
const intensity = updateIntensityAndAudio(state, dt);
|
|
1066
|
+
const isIdle = intensity < 0.1;
|
|
1067
|
+
const allowGlitch = intensity > 0.4 && !state.reasoning.active;
|
|
1068
|
+
|
|
1069
|
+
// Update all subsystems
|
|
1070
|
+
updateMainAnchor(elements, state, dt, intensity);
|
|
1071
|
+
updateCore(elements, state, dt, intensity);
|
|
1072
|
+
updateEye(elements, state, dt, intensity);
|
|
1073
|
+
updateRings(elements, state, dt, intensity);
|
|
1074
|
+
updateFragments(elements, state, dt, intensity);
|
|
1075
|
+
updateSigils(elements, state, dt, intensity);
|
|
1076
|
+
updateParticles(elements, state, dt, intensity, allowGlitch);
|
|
1077
|
+
updateGlitchBehavior(elements, state, dt, intensity, allowGlitch);
|
|
1078
|
+
updateIdleAmbience(elements, state, dt, isIdle);
|
|
1079
|
+
updateColors(elements, state, dt);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
1083
|
+
// PUBLIC API
|
|
1084
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
1085
|
+
function setColors(theme: AvatarColorTheme): void {
|
|
1086
|
+
state.theme.target = { ...theme };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function setIntensity(intensity: number, options?: { immediate?: boolean }): void {
|
|
1090
|
+
const next = clamp01(intensity);
|
|
1091
|
+
if (options?.immediate) {
|
|
1092
|
+
state.intensity.target = next;
|
|
1093
|
+
state.intensity.current = next;
|
|
1094
|
+
} else {
|
|
1095
|
+
// Trigger burst if intensity is rising significantly
|
|
1096
|
+
if (next > state.intensity.target + 0.1) {
|
|
1097
|
+
state.intensity.spinBoost = 12.0;
|
|
1098
|
+
}
|
|
1099
|
+
state.intensity.target = next;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function setAudioLevel(level: number, options?: { immediate?: boolean }): void {
|
|
1104
|
+
const next = clamp01(level);
|
|
1105
|
+
if (options?.immediate) {
|
|
1106
|
+
state.audio.target = next;
|
|
1107
|
+
state.audio.current = next;
|
|
1108
|
+
} else {
|
|
1109
|
+
state.audio.target = next;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function setToolActive(active: boolean, category?: ToolCategory): void {
|
|
1114
|
+
state.tool.active = active;
|
|
1115
|
+
if (active && category) {
|
|
1116
|
+
elements.sigilMat.color.setHex(TOOL_CATEGORY_COLORS[category]);
|
|
1117
|
+
} else {
|
|
1118
|
+
elements.sigilMat.color.setHex(state.theme.current.primary);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function triggerToolFlash(category?: ToolCategory): void {
|
|
1123
|
+
state.tool.flashColor = category ? TOOL_CATEGORY_COLORS[category] : 0xffffff;
|
|
1124
|
+
state.tool.flashTimer = 0.15;
|
|
1125
|
+
state.tool.fragmentScatterBoost = 0.3;
|
|
1126
|
+
state.intensity.spinBoost = Math.max(state.intensity.spinBoost, 8);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function triggerToolComplete(): void {
|
|
1130
|
+
state.tool.settleTimer = 0.2;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function setReasoningMode(active: boolean): void {
|
|
1134
|
+
state.reasoning.active = active;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function setTypingMode(active: boolean): void {
|
|
1138
|
+
state.typing.active = active;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function triggerTypingPulse(): void {
|
|
1142
|
+
state.typing.pulse = Math.min(1.0, state.typing.pulse + 0.3);
|
|
1143
|
+
state.intensity.spinBoost = Math.max(state.intensity.spinBoost, 1.5);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function dispose(): void {
|
|
1147
|
+
disposables.forEach((d) => d.dispose());
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return {
|
|
1151
|
+
scene,
|
|
1152
|
+
camera,
|
|
1153
|
+
update,
|
|
1154
|
+
setColors,
|
|
1155
|
+
setIntensity,
|
|
1156
|
+
setAudioLevel,
|
|
1157
|
+
setToolActive,
|
|
1158
|
+
triggerToolFlash,
|
|
1159
|
+
triggerToolComplete,
|
|
1160
|
+
setReasoningMode,
|
|
1161
|
+
setTypingMode,
|
|
1162
|
+
triggerTypingPulse,
|
|
1163
|
+
dispose,
|
|
1164
|
+
};
|
|
1165
|
+
}
|