@makefinks/daemon 0.5.0 → 0.6.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/package.json +1 -1
- package/src/app/App.tsx +3 -0
- package/src/app/components/AvatarLayer.tsx +96 -24
- package/src/app/components/ConversationPane.tsx +3 -2
- package/src/avatar/DaemonAvatarRenderable.ts +26 -3
- package/src/avatar/daemon-avatar-rig.ts +10 -1159
- package/src/avatar/rig/core/rig-engine.ts +202 -0
- package/src/avatar/rig/core/rig-types.ts +17 -0
- package/src/avatar/rig/scene/create-scene-elements.ts +298 -0
- package/src/avatar/rig/state/rig-state.ts +193 -0
- package/src/avatar/rig/theme/rig-theme.ts +31 -0
- package/src/avatar/rig/tools/rig-tools.ts +8 -0
- package/src/avatar/rig/update/update-core.ts +32 -0
- package/src/avatar/rig/update/update-eye.ts +31 -0
- package/src/avatar/rig/update/update-fragments.ts +46 -0
- package/src/avatar/rig/update/update-glitch.ts +64 -0
- package/src/avatar/rig/update/update-idle.ts +95 -0
- package/src/avatar/rig/update/update-intensity.ts +16 -0
- package/src/avatar/rig/update/update-main-anchor.ts +20 -0
- package/src/avatar/rig/update/update-particles.ts +49 -0
- package/src/avatar/rig/update/update-rings.ts +35 -0
- package/src/avatar/rig/update/update-sigils.ts +26 -0
- package/src/avatar/rig/update/update-spawn.ts +83 -0
- package/src/avatar/rig/utils/math.ts +17 -0
- package/src/hooks/use-app-controller.ts +51 -1
- package/src/hooks/use-app-display-state.ts +1 -1
- package/src/hooks/use-glitchy-banner.ts +175 -0
- package/src/ui/startup.ts +5 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { lerpColor } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateThemeColors(elements: SceneElements, state: RigState, dt: number): void {
|
|
6
|
+
const { theme, typing, tool } = state;
|
|
7
|
+
|
|
8
|
+
const t = dt * 4;
|
|
9
|
+
theme.current.primary = lerpColor(theme.current.primary, theme.target.primary, t);
|
|
10
|
+
theme.current.glow = lerpColor(theme.current.glow, theme.target.glow, t);
|
|
11
|
+
theme.current.eye = lerpColor(theme.current.eye, theme.target.eye, t);
|
|
12
|
+
|
|
13
|
+
let displayPrimary = theme.current.primary;
|
|
14
|
+
let displayEye = theme.current.eye;
|
|
15
|
+
|
|
16
|
+
if (typing.pulse > 0.01) {
|
|
17
|
+
const flashStrength = Math.pow(typing.pulse, 1.5) * 0.5;
|
|
18
|
+
displayPrimary = lerpColor(displayPrimary, 0xffffff, flashStrength);
|
|
19
|
+
displayEye = lerpColor(displayEye, 0xff8888, flashStrength * 0.3);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
elements.glowMat.color.setHex(displayPrimary);
|
|
23
|
+
if (tool.flashTimer <= 0) {
|
|
24
|
+
elements.eyeMat.color.setHex(displayEye);
|
|
25
|
+
elements.pupilMat.color.setHex(displayEye);
|
|
26
|
+
}
|
|
27
|
+
elements.pointLight.color.setHex(theme.current.glow);
|
|
28
|
+
|
|
29
|
+
elements.rings.forEach((ring) => ring.material.color.setHex(displayPrimary));
|
|
30
|
+
elements.fragments.forEach((frag) => frag.material.color.setHex(displayPrimary));
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { clamp01 } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateCore(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
6
|
+
const { phase, audio, reasoning } = state;
|
|
7
|
+
|
|
8
|
+
const coreSpeed = 0.1 + intensity * 0.9;
|
|
9
|
+
elements.glowMesh.rotation.y += dt * coreSpeed;
|
|
10
|
+
elements.glowMesh.rotation.x += dt * coreSpeed * 0.6;
|
|
11
|
+
|
|
12
|
+
const glowScale = 1 + intensity * 0.25;
|
|
13
|
+
elements.glowMesh.scale.setScalar(glowScale);
|
|
14
|
+
elements.glowMat.opacity = clamp01(0.4 + intensity * 0.4 + audio.current * 0.25);
|
|
15
|
+
const normalCorePulseSpeed = 1 + intensity * 4;
|
|
16
|
+
const reasoningCorePulseSpeed = 0.4;
|
|
17
|
+
const corePulseSpeed =
|
|
18
|
+
normalCorePulseSpeed * (1 - reasoning.blend) + reasoningCorePulseSpeed * reasoning.blend;
|
|
19
|
+
phase.corePulse += dt * corePulseSpeed;
|
|
20
|
+
|
|
21
|
+
const normalCorePulseAmount = 0.01 + intensity * 0.15;
|
|
22
|
+
const reasoningCorePulseAmount = 0.08;
|
|
23
|
+
const corePulseAmount =
|
|
24
|
+
normalCorePulseAmount * (1 - reasoning.blend) + reasoningCorePulseAmount * reasoning.blend;
|
|
25
|
+
const corePulse = 1 + Math.sin(phase.corePulse) * corePulseAmount;
|
|
26
|
+
|
|
27
|
+
elements.coreGroup.scale.setScalar(corePulse * (1 + audio.current * 0.18));
|
|
28
|
+
|
|
29
|
+
const squashStretch = 1 + audio.current * 0.15;
|
|
30
|
+
elements.coreMesh.scale.set(1, squashStretch, 1);
|
|
31
|
+
elements.glowMesh.scale.y *= squashStretch;
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { clamp01, lerpColor } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateEye(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
6
|
+
const { phase, audio, typing, reasoning, tool, theme } = state;
|
|
7
|
+
|
|
8
|
+
const eyeSpeed = 1.5 + intensity * 4;
|
|
9
|
+
phase.eyePulse += dt * eyeSpeed;
|
|
10
|
+
const eyePulseAmount = 0.1 + intensity * 0.3 + typing.pulse * 0.1;
|
|
11
|
+
const eyePulse = 0.9 + Math.sin(phase.eyePulse) * eyePulseAmount;
|
|
12
|
+
elements.eye.scale.setScalar(eyePulse * (1 + audio.current * 0.1));
|
|
13
|
+
|
|
14
|
+
const pupilSpeed = 2 + intensity * 5;
|
|
15
|
+
phase.pupilPulse += dt * pupilSpeed;
|
|
16
|
+
const pupilPulseAmount = 0.15 + intensity * 0.35 + typing.pulse * 0.15;
|
|
17
|
+
const normalPupilBase = 0.8 + Math.sin(phase.pupilPulse) * pupilPulseAmount;
|
|
18
|
+
const reasoningPupilDilation = 1.4;
|
|
19
|
+
const pupilBase = normalPupilBase * (1 - reasoning.blend) + reasoningPupilDilation * reasoning.blend;
|
|
20
|
+
elements.pupil.scale.setScalar(pupilBase * (1 + audio.current * 0.08));
|
|
21
|
+
|
|
22
|
+
if (tool.flashTimer > 0) {
|
|
23
|
+
tool.flashTimer -= dt;
|
|
24
|
+
const flashIntensity = clamp01(tool.flashTimer / 0.15);
|
|
25
|
+
elements.eyeMat.color.setHex(lerpColor(theme.current.eye, tool.flashColor, flashIntensity));
|
|
26
|
+
elements.pupilMat.color.setHex(lerpColor(theme.current.eye, tool.flashColor, flashIntensity));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
elements.eyeMat.opacity = clamp01(0.85 + intensity * 0.15 + audio.current * 0.15);
|
|
30
|
+
elements.pupilMat.opacity = clamp01(0.9 + intensity * 0.1 + audio.current * 0.1);
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { clamp01 } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateFragments(
|
|
6
|
+
elements: SceneElements,
|
|
7
|
+
state: RigState,
|
|
8
|
+
dt: number,
|
|
9
|
+
intensity: number
|
|
10
|
+
): void {
|
|
11
|
+
const { audio, tool, reasoning, intensity: intensityState } = state;
|
|
12
|
+
|
|
13
|
+
tool.fragmentScatterBoost += (0 - tool.fragmentScatterBoost) * dt * 6;
|
|
14
|
+
tool.settleTimer = Math.max(0, tool.settleTimer - dt);
|
|
15
|
+
|
|
16
|
+
const settleContraction = tool.settleTimer > 0 ? Math.sin(tool.settleTimer * 20) * 0.08 : 0;
|
|
17
|
+
const reasoningContraction = reasoning.blend * 0.25;
|
|
18
|
+
const fragmentScale =
|
|
19
|
+
0.5 + intensity * 0.6 + tool.fragmentScatterBoost - settleContraction - reasoningContraction;
|
|
20
|
+
elements.fragmentGroup.scale.setScalar(fragmentScale);
|
|
21
|
+
|
|
22
|
+
elements.fragments.forEach((frag) => {
|
|
23
|
+
const reasoningOrbitSlowdown = 1 - reasoning.blend * 0.6;
|
|
24
|
+
const audioOrbitBoost = audio.current * 0.4;
|
|
25
|
+
const orbitSpeed =
|
|
26
|
+
frag.orbitSpeed * (0.4 + intensity * 1.2 + audioOrbitBoost) * reasoningOrbitSlowdown +
|
|
27
|
+
intensityState.spinBoost * 0.5;
|
|
28
|
+
frag.orbitAngle += dt * orbitSpeed;
|
|
29
|
+
|
|
30
|
+
const dynamicRadius = frag.orbitRadius;
|
|
31
|
+
const bobAmount = 0.08 + intensity * 0.2;
|
|
32
|
+
const bobSpeed = frag.bobSpeed * (0.5 + intensity * 1.0);
|
|
33
|
+
frag.bobPhase += dt * bobSpeed;
|
|
34
|
+
|
|
35
|
+
const bob = Math.sin(frag.bobPhase) * bobAmount;
|
|
36
|
+
frag.mesh.position.x = Math.cos(frag.orbitAngle) * dynamicRadius;
|
|
37
|
+
frag.mesh.position.z = Math.sin(frag.orbitAngle) * dynamicRadius;
|
|
38
|
+
frag.mesh.position.y += (bob - frag.mesh.position.y) * dt * 3;
|
|
39
|
+
|
|
40
|
+
const tumbleSpeed = 0.1 + intensity * 0.5;
|
|
41
|
+
frag.mesh.rotation.x += dt * tumbleSpeed;
|
|
42
|
+
frag.mesh.rotation.y += dt * tumbleSpeed * 1.5;
|
|
43
|
+
|
|
44
|
+
frag.material.opacity = clamp01(0.45 + intensity * 0.4 + audio.current * 0.22);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
|
|
4
|
+
export function updateGlitchBehavior(
|
|
5
|
+
elements: SceneElements,
|
|
6
|
+
state: RigState,
|
|
7
|
+
dt: number,
|
|
8
|
+
intensity: number,
|
|
9
|
+
allowGlitch: boolean
|
|
10
|
+
): void {
|
|
11
|
+
const { glitch } = state;
|
|
12
|
+
|
|
13
|
+
if (!allowGlitch) {
|
|
14
|
+
glitch.timer = 0;
|
|
15
|
+
if (glitch.isActive) {
|
|
16
|
+
glitch.isActive = false;
|
|
17
|
+
elements.coreGroup.position.set(0, 0, 0);
|
|
18
|
+
elements.fragmentGroup.scale.set(1, 1, 1);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
glitch.timer += dt;
|
|
24
|
+
|
|
25
|
+
const intensityFactor = Math.max(0.1, intensity);
|
|
26
|
+
const baseInterval = 3.0 / intensityFactor;
|
|
27
|
+
const randomFactor = 0.5 + Math.random();
|
|
28
|
+
const glitchInterval = baseInterval * randomFactor;
|
|
29
|
+
|
|
30
|
+
if (!glitch.isActive && glitch.timer > glitchInterval) {
|
|
31
|
+
glitch.isActive = true;
|
|
32
|
+
glitch.duration = 0.05 + Math.random() * 0.1 + intensity * 0.05;
|
|
33
|
+
glitch.timer = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (glitch.isActive) {
|
|
37
|
+
glitch.duration -= dt;
|
|
38
|
+
|
|
39
|
+
const displaceMult = intensity * 0.5;
|
|
40
|
+
elements.coreGroup.position.set(
|
|
41
|
+
(Math.random() - 0.5) * 0.05 * displaceMult,
|
|
42
|
+
(Math.random() - 0.5) * 0.05 * displaceMult,
|
|
43
|
+
(Math.random() - 0.5) * 0.03 * displaceMult
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const scatterAmount = 1.0 + (Math.random() - 0.5) * intensity * 0.15;
|
|
47
|
+
elements.fragmentGroup.scale.setScalar(scatterAmount);
|
|
48
|
+
|
|
49
|
+
elements.rings.forEach((ring, i) => {
|
|
50
|
+
const baseOpacity = 0.5 + i * 0.15;
|
|
51
|
+
const flickerRange = intensity * 0.2;
|
|
52
|
+
ring.material.opacity = baseOpacity * (1 - flickerRange + Math.random() * flickerRange * 2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (glitch.duration <= 0) {
|
|
56
|
+
glitch.isActive = false;
|
|
57
|
+
elements.coreGroup.position.set(0, 0, 0);
|
|
58
|
+
elements.fragmentGroup.scale.set(1, 1, 1);
|
|
59
|
+
elements.rings.forEach((ring, i) => {
|
|
60
|
+
ring.material.opacity = 0.5 + i * 0.15;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
|
|
4
|
+
export function updateIdleAmbience(
|
|
5
|
+
elements: SceneElements,
|
|
6
|
+
state: RigState,
|
|
7
|
+
dt: number,
|
|
8
|
+
isIdle: boolean
|
|
9
|
+
): void {
|
|
10
|
+
const { idleMicroGlitch, eyeDrift, particlePulse, coreDrift, typing } = state;
|
|
11
|
+
|
|
12
|
+
if (isIdle) {
|
|
13
|
+
idleMicroGlitch.timer += dt;
|
|
14
|
+
if (!idleMicroGlitch.active && idleMicroGlitch.timer > idleMicroGlitch.cooldown) {
|
|
15
|
+
idleMicroGlitch.active = true;
|
|
16
|
+
idleMicroGlitch.duration = 0.08 + Math.random() * 0.12;
|
|
17
|
+
idleMicroGlitch.timer = 0;
|
|
18
|
+
idleMicroGlitch.cooldown = 3 + Math.random() * 5;
|
|
19
|
+
}
|
|
20
|
+
if (idleMicroGlitch.active) {
|
|
21
|
+
idleMicroGlitch.duration -= dt;
|
|
22
|
+
const jitterAmount = 0.06;
|
|
23
|
+
elements.coreGroup.position.x += (Math.random() - 0.5) * jitterAmount;
|
|
24
|
+
elements.coreGroup.position.y += (Math.random() - 0.5) * jitterAmount;
|
|
25
|
+
if (idleMicroGlitch.duration <= 0) {
|
|
26
|
+
idleMicroGlitch.active = false;
|
|
27
|
+
elements.coreGroup.position.x = 0;
|
|
28
|
+
elements.coreGroup.position.y = 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
idleMicroGlitch.timer = 0;
|
|
33
|
+
idleMicroGlitch.active = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typing.active) {
|
|
37
|
+
typing.eyeScanTimer += dt;
|
|
38
|
+
if (typing.eyeScanTimer > typing.eyeScanInterval) {
|
|
39
|
+
typing.eyeScanTimer = 0;
|
|
40
|
+
const scanWidth = 0.2;
|
|
41
|
+
eyeDrift.targetX = (Math.random() - 0.5) * scanWidth;
|
|
42
|
+
eyeDrift.targetY = (Math.random() - 0.5) * 0.05;
|
|
43
|
+
typing.eyeScanInterval = 0.3 + Math.random() * 0.8;
|
|
44
|
+
}
|
|
45
|
+
const trackSpeed = 5;
|
|
46
|
+
eyeDrift.x += (eyeDrift.targetX - eyeDrift.x) * dt * trackSpeed;
|
|
47
|
+
eyeDrift.y += (eyeDrift.targetY - eyeDrift.y) * dt * trackSpeed;
|
|
48
|
+
} else if (isIdle) {
|
|
49
|
+
eyeDrift.timer += dt;
|
|
50
|
+
if (eyeDrift.timer > eyeDrift.interval) {
|
|
51
|
+
eyeDrift.timer = 0;
|
|
52
|
+
const isFast = Math.random() > 0.4;
|
|
53
|
+
eyeDrift.interval = isFast ? 0.15 + Math.random() * 0.25 : 2.0 + Math.random() * 3.0;
|
|
54
|
+
eyeDrift.targetX = (Math.random() - 0.5) * 0.25;
|
|
55
|
+
eyeDrift.targetY = (Math.random() - 0.5) * 0.15;
|
|
56
|
+
}
|
|
57
|
+
const interpSpeed = eyeDrift.interval < 0.5 ? 30 : 1.5;
|
|
58
|
+
eyeDrift.x += (eyeDrift.targetX - eyeDrift.x) * dt * interpSpeed;
|
|
59
|
+
eyeDrift.y += (eyeDrift.targetY - eyeDrift.y) * dt * interpSpeed;
|
|
60
|
+
} else {
|
|
61
|
+
eyeDrift.x += (0 - eyeDrift.x) * dt * 4;
|
|
62
|
+
eyeDrift.y += (0 - eyeDrift.y) * dt * 4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
elements.eye.position.x = eyeDrift.x;
|
|
66
|
+
elements.eye.position.y = eyeDrift.y;
|
|
67
|
+
elements.pupil.position.x = eyeDrift.x;
|
|
68
|
+
elements.pupil.position.y = eyeDrift.y;
|
|
69
|
+
|
|
70
|
+
if (isIdle) {
|
|
71
|
+
particlePulse.timer += dt;
|
|
72
|
+
if (particlePulse.timer > particlePulse.interval) {
|
|
73
|
+
particlePulse.timer = 0;
|
|
74
|
+
particlePulse.interval = 1 + Math.random() * 2;
|
|
75
|
+
particlePulse.brightness = 1.0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
particlePulse.brightness = Math.max(0, particlePulse.brightness - dt * 1.5);
|
|
79
|
+
|
|
80
|
+
const coreDriftSpeed = 0.4;
|
|
81
|
+
const coreDriftAmount = 0.08;
|
|
82
|
+
coreDrift.phaseX += dt * coreDriftSpeed;
|
|
83
|
+
coreDrift.phaseY += dt * coreDriftSpeed * 0.7;
|
|
84
|
+
coreDrift.phaseZ += dt * coreDriftSpeed * 0.5;
|
|
85
|
+
|
|
86
|
+
const targetDriftX = Math.sin(coreDrift.phaseX) * coreDriftAmount;
|
|
87
|
+
const targetDriftY = Math.sin(coreDrift.phaseY) * coreDriftAmount;
|
|
88
|
+
const targetDriftZ = Math.sin(coreDrift.phaseZ) * coreDriftAmount * 0.5;
|
|
89
|
+
|
|
90
|
+
coreDrift.x += (targetDriftX - coreDrift.x) * dt * 2;
|
|
91
|
+
coreDrift.y += (targetDriftY - coreDrift.y) * dt * 2;
|
|
92
|
+
coreDrift.z += (targetDriftZ - coreDrift.z) * dt * 2;
|
|
93
|
+
|
|
94
|
+
elements.mainAnchor.position.set(coreDrift.x, coreDrift.y, coreDrift.z);
|
|
95
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RigState } from "../state/rig-state";
|
|
2
|
+
|
|
3
|
+
export function updateIntensityAndAudio(state: RigState, dt: number): number {
|
|
4
|
+
const { intensity, audio, typing, reasoning } = state;
|
|
5
|
+
|
|
6
|
+
typing.pulse = Math.max(0, typing.pulse - dt * 5);
|
|
7
|
+
|
|
8
|
+
const intensityRate = intensity.target > intensity.current ? 10 : 8;
|
|
9
|
+
intensity.current += (intensity.target - intensity.current) * dt * intensityRate;
|
|
10
|
+
|
|
11
|
+
intensity.spinBoost += (0 - intensity.spinBoost) * dt * 2.5;
|
|
12
|
+
audio.current += (audio.target - audio.current) * dt * 25;
|
|
13
|
+
reasoning.blend += ((reasoning.active ? 1 : 0) - reasoning.blend) * dt * 6;
|
|
14
|
+
|
|
15
|
+
return intensity.current;
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
|
|
4
|
+
export function updateMainAnchor(
|
|
5
|
+
elements: SceneElements,
|
|
6
|
+
state: RigState,
|
|
7
|
+
dt: number,
|
|
8
|
+
intensity: number
|
|
9
|
+
): void {
|
|
10
|
+
const { phase, audio } = state;
|
|
11
|
+
|
|
12
|
+
const driftSpeed = 0.08 + intensity * 0.3;
|
|
13
|
+
phase.drift += dt * driftSpeed;
|
|
14
|
+
const driftAmount = 0.03 + intensity * 0.12;
|
|
15
|
+
|
|
16
|
+
elements.mainAnchor.rotation.y = Math.sin(phase.drift) * driftAmount;
|
|
17
|
+
elements.mainAnchor.rotation.x = Math.sin(phase.drift * 0.7) * driftAmount * 0.5;
|
|
18
|
+
elements.mainAnchor.rotation.z = Math.sin(phase.drift * 0.5) * intensity * 0.08;
|
|
19
|
+
elements.mainAnchor.scale.setScalar(1 + audio.current * 0.12);
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { clamp01 } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateParticles(
|
|
6
|
+
elements: SceneElements,
|
|
7
|
+
state: RigState,
|
|
8
|
+
dt: number,
|
|
9
|
+
intensity: number,
|
|
10
|
+
allowGlitch: boolean
|
|
11
|
+
): void {
|
|
12
|
+
const { audio, particlePulse } = state;
|
|
13
|
+
const particleCount = elements.particleVelocities.length;
|
|
14
|
+
|
|
15
|
+
const glitchChance = allowGlitch ? 0.0002 + intensity * 0.006 : 0;
|
|
16
|
+
const audioJitterBoost = 1 + audio.current * 0.8;
|
|
17
|
+
const particleSpeedMult = (0.3 + intensity * 1.2) * audioJitterBoost;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < particleCount; i++) {
|
|
20
|
+
const vel = elements.particleVelocities[i]!;
|
|
21
|
+
let x = elements.particlePos.getX(i) + vel.x * dt * particleSpeedMult;
|
|
22
|
+
let y = elements.particlePos.getY(i) + vel.y * dt * particleSpeedMult;
|
|
23
|
+
let z = elements.particlePos.getZ(i) + vel.z * dt * particleSpeedMult;
|
|
24
|
+
|
|
25
|
+
if (Math.random() < glitchChance) {
|
|
26
|
+
const r = 1.5 + Math.random() * 2;
|
|
27
|
+
const theta = Math.random() * Math.PI * 2;
|
|
28
|
+
const phi = Math.acos(Math.random() * 2 - 1);
|
|
29
|
+
x = r * Math.sin(phi) * Math.cos(theta);
|
|
30
|
+
y = r * Math.sin(phi) * Math.sin(theta);
|
|
31
|
+
z = r * Math.cos(phi);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const distSq = x * x + y * y + z * z;
|
|
35
|
+
if (distSq > 20) {
|
|
36
|
+
x *= -0.8;
|
|
37
|
+
y *= -0.8;
|
|
38
|
+
z *= -0.8;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
elements.particlePos.setXYZ(i, x, y, z);
|
|
42
|
+
}
|
|
43
|
+
elements.particlePos.needsUpdate = true;
|
|
44
|
+
|
|
45
|
+
const idleParticleBoost = particlePulse.brightness * 0.4;
|
|
46
|
+
elements.particleMat.opacity = clamp01(0.3 + intensity * 0.4 + audio.current * 0.25 + idleParticleBoost);
|
|
47
|
+
elements.particleMat.size =
|
|
48
|
+
0.02 + intensity * 0.015 + audio.current * 0.01 + particlePulse.brightness * 0.02;
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { clamp01 } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateRings(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
6
|
+
const { audio, intensity: intensityState } = state;
|
|
7
|
+
|
|
8
|
+
const orbitScale = 0.75 + intensity * 0.4;
|
|
9
|
+
elements.orbitGroup.scale.setScalar(orbitScale);
|
|
10
|
+
|
|
11
|
+
elements.rings.forEach((ring, i) => {
|
|
12
|
+
const ringIntensity = Math.pow(intensity, 1.35);
|
|
13
|
+
const ringSpeed = ring.speed * (0.4 + ringIntensity * 1.5) + intensityState.spinBoost * (1 + i * 0.2);
|
|
14
|
+
ring.mesh.rotateOnAxis(ring.axis, dt * ringSpeed);
|
|
15
|
+
|
|
16
|
+
const wobbleSpeed = 3 + i * 0.5;
|
|
17
|
+
ring.wobblePhase += dt * wobbleSpeed;
|
|
18
|
+
const wobbleAmount = audio.current * 0.06;
|
|
19
|
+
const wobbleX = Math.sin(ring.wobblePhase) * wobbleAmount;
|
|
20
|
+
const wobbleZ = Math.cos(ring.wobblePhase * 1.3) * wobbleAmount;
|
|
21
|
+
ring.mesh.rotation.x += wobbleX;
|
|
22
|
+
ring.mesh.rotation.z += wobbleZ;
|
|
23
|
+
|
|
24
|
+
const phaseSpeed = 1 + intensity * 3;
|
|
25
|
+
ring.phase += dt * phaseSpeed;
|
|
26
|
+
|
|
27
|
+
const baseOpacity = 0.4 + intensity * 0.4 + i * 0.1;
|
|
28
|
+
if (intensity > 0.1) {
|
|
29
|
+
const wave = Math.sin(ring.phase + i * 1.5) * 0.2 * intensity;
|
|
30
|
+
ring.material.opacity = clamp01(baseOpacity + wave + audio.current * 0.25);
|
|
31
|
+
} else {
|
|
32
|
+
ring.material.opacity = clamp01(baseOpacity + audio.current * 0.25);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
import { clamp01 } from "../utils/math";
|
|
4
|
+
|
|
5
|
+
export function updateSigils(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
|
|
6
|
+
const { phase, audio, tool } = state;
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < elements.fragments.length; i++) {
|
|
9
|
+
const curr = elements.fragments[i]!;
|
|
10
|
+
const next = elements.fragments[(i + 1) % elements.fragments.length]!;
|
|
11
|
+
elements.sigilPos.setXYZ(i * 2, curr.mesh.position.x, curr.mesh.position.y, curr.mesh.position.z);
|
|
12
|
+
elements.sigilPos.setXYZ(i * 2 + 1, next.mesh.position.x, next.mesh.position.y, next.mesh.position.z);
|
|
13
|
+
}
|
|
14
|
+
elements.sigilPos.needsUpdate = true;
|
|
15
|
+
|
|
16
|
+
const sigilPulseSpeed = 1 + intensity * 2;
|
|
17
|
+
phase.sigilPulse += dt * sigilPulseSpeed;
|
|
18
|
+
|
|
19
|
+
tool.sigilBrightnessBoost += ((tool.active ? 1 : 0) - tool.sigilBrightnessBoost) * dt * 8;
|
|
20
|
+
|
|
21
|
+
const sigilBaseOpacity = 0.2 + intensity * 0.2 + tool.sigilBrightnessBoost * 0.5;
|
|
22
|
+
const sigilPulseAmount = 0.05 + intensity * 0.12;
|
|
23
|
+
elements.sigilMat.opacity = clamp01(
|
|
24
|
+
sigilBaseOpacity + Math.sin(phase.sigilPulse) * sigilPulseAmount + audio.current * 0.2
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SceneElements } from "../scene/create-scene-elements";
|
|
2
|
+
import type { RigState } from "../state/rig-state";
|
|
3
|
+
|
|
4
|
+
import { STARTUP_BANNER_DURATION_S } from "../../../ui/startup";
|
|
5
|
+
|
|
6
|
+
const SPAWN_DURATION = STARTUP_BANNER_DURATION_S;
|
|
7
|
+
|
|
8
|
+
/** Easing for jitter intensity - high at start, fades quickly */
|
|
9
|
+
function jitterEasing(t: number): number {
|
|
10
|
+
return Math.pow(1 - t, 2);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Updates the spawn animation state and applies visual effects.
|
|
15
|
+
* Returns the current spawn progress (0-1) for other systems to use.
|
|
16
|
+
*/
|
|
17
|
+
export function updateSpawn(elements: SceneElements, state: RigState, dt: number): number {
|
|
18
|
+
const easedProgress = advanceSpawn(state, dt);
|
|
19
|
+
applySpawn(elements, state);
|
|
20
|
+
return easedProgress;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Advances spawn timers/state (no rendering side effects).
|
|
25
|
+
* Returns the eased progress (0-1).
|
|
26
|
+
*/
|
|
27
|
+
export function advanceSpawn(state: RigState, dt: number): number {
|
|
28
|
+
if (state.spawn.complete) return 1;
|
|
29
|
+
|
|
30
|
+
state.spawn.elapsed += dt;
|
|
31
|
+
const rawProgress = Math.min(1, state.spawn.elapsed / SPAWN_DURATION);
|
|
32
|
+
state.spawn.progress = rawProgress;
|
|
33
|
+
state.spawn.glitchIntensity = jitterEasing(rawProgress);
|
|
34
|
+
|
|
35
|
+
// Mark complete when done
|
|
36
|
+
if (rawProgress >= 1) {
|
|
37
|
+
state.spawn.complete = true;
|
|
38
|
+
state.spawn.progress = 1;
|
|
39
|
+
state.spawn.glitchIntensity = 0;
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return rawProgress;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Applies spawn animation effects to scene elements using current state.
|
|
48
|
+
* Safe to call multiple times per frame.
|
|
49
|
+
*/
|
|
50
|
+
export function applySpawn(elements: SceneElements, state: RigState): void {
|
|
51
|
+
// Minimal startup effect: jitter only (synced to banner reveal duration).
|
|
52
|
+
if (state.spawn.complete) {
|
|
53
|
+
elements.coreGroup.position.x = 0;
|
|
54
|
+
elements.coreGroup.position.y = 0;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const jitterAmount = state.spawn.glitchIntensity * 0.15;
|
|
59
|
+
const jitterX = (Math.random() - 0.5) * jitterAmount;
|
|
60
|
+
const jitterY = (Math.random() - 0.5) * jitterAmount;
|
|
61
|
+
elements.coreGroup.position.x = jitterX;
|
|
62
|
+
elements.coreGroup.position.y = jitterY;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resets spawn state for a fresh spawn animation.
|
|
67
|
+
*/
|
|
68
|
+
export function resetSpawnState(state: RigState): void {
|
|
69
|
+
state.spawn.progress = 0;
|
|
70
|
+
state.spawn.elapsed = 0;
|
|
71
|
+
state.spawn.complete = false;
|
|
72
|
+
state.spawn.glitchIntensity = 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Skips spawn animation and sets everything to fully spawned.
|
|
77
|
+
*/
|
|
78
|
+
export function skipSpawnAnimation(state: RigState): void {
|
|
79
|
+
state.spawn.progress = 1;
|
|
80
|
+
state.spawn.elapsed = SPAWN_DURATION;
|
|
81
|
+
state.spawn.complete = true;
|
|
82
|
+
state.spawn.glitchIntensity = 0;
|
|
83
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
|
|
2
|
+
|
|
3
|
+
export const clamp01 = (value: number): number => clamp(value, 0, 1);
|
|
4
|
+
|
|
5
|
+
export function lerpColor(current: number, target: number, t: number): number {
|
|
6
|
+
const cr = (current >> 16) & 0xff;
|
|
7
|
+
const cg = (current >> 8) & 0xff;
|
|
8
|
+
const cb = current & 0xff;
|
|
9
|
+
const tr = (target >> 16) & 0xff;
|
|
10
|
+
const tg = (target >> 8) & 0xff;
|
|
11
|
+
const tb = target & 0xff;
|
|
12
|
+
return (
|
|
13
|
+
(Math.round(cr + (tr - cr) * t) << 16) |
|
|
14
|
+
(Math.round(cg + (tg - cg) * t) << 8) |
|
|
15
|
+
Math.round(cb + (tb - cb) * t)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRenderer } from "@opentui/react";
|
|
1
|
+
import { useOnResize, useRenderer } from "@opentui/react";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
|
|
4
4
|
import type { ConversationPaneProps } from "../app/components/ConversationPane";
|
|
@@ -21,6 +21,7 @@ import { useSessionController } from "./use-session-controller";
|
|
|
21
21
|
import { getDaemonManager } from "../state/daemon-state";
|
|
22
22
|
import { deleteSession } from "../state/session-store";
|
|
23
23
|
import { DaemonState } from "../types";
|
|
24
|
+
import { STARTUP_BANNER_DURATION_MS, STARTUP_IDLE_CHROME_LEAD_MS } from "../ui/startup";
|
|
24
25
|
|
|
25
26
|
export interface AppControllerResult {
|
|
26
27
|
handleCopyOnSelectMouseUp: () => void;
|
|
@@ -31,6 +32,9 @@ export interface AppControllerResult {
|
|
|
31
32
|
width: number;
|
|
32
33
|
height: number;
|
|
33
34
|
zIndex: number;
|
|
35
|
+
showBanner: boolean;
|
|
36
|
+
animateBanner: boolean;
|
|
37
|
+
startupAnimationActive: boolean;
|
|
34
38
|
};
|
|
35
39
|
isListeningDim: boolean;
|
|
36
40
|
listeningDimTop: number;
|
|
@@ -54,6 +58,18 @@ export function useAppController({
|
|
|
54
58
|
const { handleCopyOnSelectMouseUp } = useCopyOnSelect();
|
|
55
59
|
|
|
56
60
|
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
|
61
|
+
const [terminalSize, setTerminalSize] = useState({
|
|
62
|
+
width: renderer.terminalWidth,
|
|
63
|
+
height: renderer.terminalHeight,
|
|
64
|
+
});
|
|
65
|
+
// Track if this is initial app load for startup animation
|
|
66
|
+
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
|
67
|
+
const [startupIntroDone, setStartupIntroDone] = useState(false);
|
|
68
|
+
|
|
69
|
+
// Update terminal size state on resize to trigger re-render
|
|
70
|
+
useOnResize((width, height) => {
|
|
71
|
+
setTerminalSize({ width, height });
|
|
72
|
+
});
|
|
57
73
|
|
|
58
74
|
const menus = useAppMenus();
|
|
59
75
|
const {
|
|
@@ -119,6 +135,19 @@ export function useAppController({
|
|
|
119
135
|
preferencesLoaded,
|
|
120
136
|
showDeviceMenu,
|
|
121
137
|
});
|
|
138
|
+
const onboardingComplete = preferencesLoaded && !bootstrap.onboardingActive;
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!onboardingComplete) {
|
|
142
|
+
setStartupIntroDone(false);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Delay idle UI chrome (status/hotkeys) so the banner can resolve first.
|
|
146
|
+
const delayMs = Math.max(0, STARTUP_BANNER_DURATION_MS - STARTUP_IDLE_CHROME_LEAD_MS);
|
|
147
|
+
setStartupIntroDone(false);
|
|
148
|
+
const t = setTimeout(() => setStartupIntroDone(true), delayMs);
|
|
149
|
+
return () => clearTimeout(t);
|
|
150
|
+
}, [onboardingComplete]);
|
|
122
151
|
|
|
123
152
|
const daemon = useDaemonRuntimeController({
|
|
124
153
|
currentModelId,
|
|
@@ -373,6 +402,21 @@ export function useAppController({
|
|
|
373
402
|
}
|
|
374
403
|
}, [daemon.daemonState]);
|
|
375
404
|
|
|
405
|
+
// Turn off initial load state once user interacts (banner animation is one-time only)
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (daemon.hasInteracted && isInitialLoad) {
|
|
408
|
+
setIsInitialLoad(false);
|
|
409
|
+
}
|
|
410
|
+
}, [daemon.hasInteracted, isInitialLoad]);
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (daemon.hasInteracted && !startupIntroDone) {
|
|
414
|
+
setStartupIntroDone(true);
|
|
415
|
+
}
|
|
416
|
+
}, [daemon.hasInteracted, startupIntroDone]);
|
|
417
|
+
|
|
418
|
+
const startupAnimationActive = onboardingComplete && isInitialLoad;
|
|
419
|
+
|
|
376
420
|
const appContextValue = useAppContextBuilder({
|
|
377
421
|
menus: {
|
|
378
422
|
showDeviceMenu,
|
|
@@ -485,6 +529,11 @@ export function useAppController({
|
|
|
485
529
|
width: avatarWidth,
|
|
486
530
|
height: avatarHeight,
|
|
487
531
|
zIndex: isListening && daemon.hasInteracted ? 2 : 0,
|
|
532
|
+
// Show banner only when idle, not interacted, and terminal is large enough
|
|
533
|
+
showBanner:
|
|
534
|
+
onboardingComplete && !daemon.hasInteracted && terminalSize.height >= 30 && terminalSize.width >= 100,
|
|
535
|
+
animateBanner: startupAnimationActive,
|
|
536
|
+
startupAnimationActive,
|
|
488
537
|
},
|
|
489
538
|
isListeningDim,
|
|
490
539
|
listeningDimTop: statusBarHeight,
|
|
@@ -536,6 +585,7 @@ export function useAppController({
|
|
|
536
585
|
modelName: modelName ?? "",
|
|
537
586
|
sessionTitle: sessionTitle ?? "",
|
|
538
587
|
isVoiceOutputEnabled: interactionMode === "voice",
|
|
588
|
+
startupIntroDone,
|
|
539
589
|
},
|
|
540
590
|
appContextValue,
|
|
541
591
|
overlaysProps: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { DaemonState } from "../types";
|
|
3
3
|
import type { ContentBlock, SessionInfo } from "../types";
|
|
4
|
-
import type { ModelMetadata } from "../utils/model-metadata";
|
|
5
4
|
import { COLORS, STATE_COLOR_HEX, STATUS_TEXT } from "../ui/constants";
|
|
6
5
|
import { formatElapsedTime } from "../utils/formatters";
|
|
6
|
+
import type { ModelMetadata } from "../utils/model-metadata";
|
|
7
7
|
|
|
8
8
|
export interface UseAppDisplayStateParams {
|
|
9
9
|
daemonState: DaemonState;
|