agent-office 0.5.0 → 0.6.1
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 +1 -1
- package/README.md +259 -228
- package/dist/commands/cron-requests.d.ts +7 -0
- package/dist/commands/cron-requests.d.ts.map +1 -0
- package/dist/commands/cron-requests.js +31 -0
- package/dist/commands/cron-requests.js.map +1 -0
- package/dist/commands/crons.d.ts +10 -0
- package/dist/commands/crons.d.ts.map +1 -0
- package/dist/commands/crons.js +45 -0
- package/dist/commands/crons.js.map +1 -0
- package/dist/commands/hello.d.ts +5 -0
- package/dist/commands/hello.d.ts.map +1 -0
- package/dist/commands/hello.js +4 -0
- package/dist/commands/hello.js.map +1 -0
- package/dist/commands/messages.d.ts +5 -0
- package/dist/commands/messages.d.ts.map +1 -0
- package/dist/commands/messages.js +18 -0
- package/dist/commands/messages.js.map +1 -0
- package/dist/commands/sessions.d.ts +13 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +58 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/task-columns.d.ts +2 -0
- package/dist/commands/task-columns.d.ts.map +1 -0
- package/dist/commands/task-columns.js +13 -0
- package/dist/commands/task-columns.js.map +1 -0
- package/dist/commands/tasks.d.ts +11 -0
- package/dist/commands/tasks.d.ts.map +1 -0
- package/dist/commands/tasks.js +75 -0
- package/dist/commands/tasks.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +50 -0
- package/dist/config.test.js.map +1 -0
- package/dist/db/index.d.ts +6 -70
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +4 -11
- package/dist/db/index.js.map +1 -0
- package/dist/db/mock-storage.d.ts +79 -0
- package/dist/db/mock-storage.d.ts.map +1 -0
- package/dist/db/mock-storage.js +381 -0
- package/dist/db/mock-storage.js.map +1 -0
- package/dist/db/mock-storage.test.d.ts +2 -0
- package/dist/db/mock-storage.test.d.ts.map +1 -0
- package/dist/db/mock-storage.test.js +234 -0
- package/dist/db/mock-storage.test.js.map +1 -0
- package/dist/db/postgresql-storage.d.ts +10 -8
- package/dist/db/postgresql-storage.d.ts.map +1 -0
- package/dist/db/postgresql-storage.js +76 -42
- package/dist/db/postgresql-storage.js.map +1 -0
- package/dist/db/sqlite-storage.d.ts +9 -8
- package/dist/db/sqlite-storage.d.ts.map +1 -0
- package/dist/db/sqlite-storage.js +75 -41
- package/dist/db/sqlite-storage.js.map +1 -0
- package/dist/db/storage-base.d.ts +7 -8
- package/dist/db/storage-base.d.ts.map +1 -0
- package/dist/db/storage-base.js +3 -2
- package/dist/db/storage-base.js.map +1 -0
- package/dist/db/storage.d.ts +12 -12
- package/dist/db/storage.d.ts.map +1 -0
- package/dist/db/storage.js +1 -0
- package/dist/db/storage.js.map +1 -0
- package/dist/db/types.d.ts +67 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +397 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +49 -0
- package/dist/index.test.js.map +1 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +8 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/services/cron-service.constraints.test.d.ts +2 -0
- package/dist/services/cron-service.constraints.test.d.ts.map +1 -0
- package/dist/services/cron-service.constraints.test.js +90 -0
- package/dist/services/cron-service.constraints.test.js.map +1 -0
- package/dist/services/cron-service.d.ts +45 -0
- package/dist/services/cron-service.d.ts.map +1 -0
- package/dist/services/cron-service.js +157 -0
- package/dist/services/cron-service.js.map +1 -0
- package/dist/services/cron-service.test.d.ts +2 -0
- package/dist/services/cron-service.test.d.ts.map +1 -0
- package/dist/services/cron-service.test.js +280 -0
- package/dist/services/cron-service.test.js.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +5 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/message-service.d.ts +16 -0
- package/dist/services/message-service.d.ts.map +1 -0
- package/dist/services/message-service.js +39 -0
- package/dist/services/message-service.js.map +1 -0
- package/dist/services/message-service.test.d.ts +2 -0
- package/dist/services/message-service.test.d.ts.map +1 -0
- package/dist/services/message-service.test.js +145 -0
- package/dist/services/message-service.test.js.map +1 -0
- package/dist/services/session-service.constraints.test.d.ts +2 -0
- package/dist/services/session-service.constraints.test.d.ts.map +1 -0
- package/dist/services/session-service.constraints.test.js +34 -0
- package/dist/services/session-service.constraints.test.js.map +1 -0
- package/dist/services/session-service.d.ts +27 -0
- package/dist/services/session-service.d.ts.map +1 -0
- package/dist/services/session-service.js +55 -0
- package/dist/services/session-service.js.map +1 -0
- package/dist/services/session-service.test.d.ts +2 -0
- package/dist/services/session-service.test.d.ts.map +1 -0
- package/dist/services/session-service.test.js +87 -0
- package/dist/services/session-service.test.js.map +1 -0
- package/dist/services/task-service.d.ts +25 -0
- package/dist/services/task-service.d.ts.map +1 -0
- package/dist/services/task-service.js +87 -0
- package/dist/services/task-service.js.map +1 -0
- package/dist/services/task-service.test.d.ts +2 -0
- package/dist/services/task-service.test.d.ts.map +1 -0
- package/dist/services/task-service.test.js +180 -0
- package/dist/services/task-service.test.js.map +1 -0
- package/package.json +41 -42
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -317
- package/dist/commands/communicator.d.ts +0 -9
- package/dist/commands/communicator.js +0 -2232
- package/dist/commands/manage.d.ts +0 -5
- package/dist/commands/manage.js +0 -20
- package/dist/commands/notifier.d.ts +0 -11
- package/dist/commands/notifier.js +0 -100
- package/dist/commands/screensaver.d.ts +0 -8
- package/dist/commands/screensaver.js +0 -1280
- package/dist/commands/serve.d.ts +0 -13
- package/dist/commands/serve.js +0 -95
- package/dist/commands/task-board.d.ts +0 -29
- package/dist/commands/task-board.js +0 -251
- package/dist/commands/worker.d.ts +0 -16
- package/dist/commands/worker.js +0 -145
- package/dist/db/migrate.d.ts +0 -2
- package/dist/db/migrate.js +0 -3
- package/dist/lib/agentic-coding-server.d.ts +0 -66
- package/dist/lib/agentic-coding-server.js +0 -7
- package/dist/lib/notifier.d.ts +0 -18
- package/dist/lib/notifier.js +0 -15
- package/dist/lib/opencode-coding-server.d.ts +0 -11
- package/dist/lib/opencode-coding-server.js +0 -66
- package/dist/lib/pi-coding-server.d.ts +0 -20
- package/dist/lib/pi-coding-server.js +0 -162
- package/dist/manage/app.d.ts +0 -6
- package/dist/manage/app.js +0 -128
- package/dist/manage/components/AgentCode.d.ts +0 -8
- package/dist/manage/components/AgentCode.js +0 -73
- package/dist/manage/components/CreateSession.d.ts +0 -8
- package/dist/manage/components/CreateSession.js +0 -37
- package/dist/manage/components/CronList.d.ts +0 -9
- package/dist/manage/components/CronList.js +0 -321
- package/dist/manage/components/CronRequests.d.ts +0 -8
- package/dist/manage/components/CronRequests.js +0 -181
- package/dist/manage/components/DeleteSession.d.ts +0 -7
- package/dist/manage/components/DeleteSession.js +0 -55
- package/dist/manage/components/InjectText.d.ts +0 -8
- package/dist/manage/components/InjectText.js +0 -51
- package/dist/manage/components/ItemSelector.d.ts +0 -7
- package/dist/manage/components/ItemSelector.js +0 -20
- package/dist/manage/components/MenuSelect.d.ts +0 -13
- package/dist/manage/components/MenuSelect.js +0 -22
- package/dist/manage/components/MyMail.d.ts +0 -9
- package/dist/manage/components/MyMail.js +0 -143
- package/dist/manage/components/Profile.d.ts +0 -8
- package/dist/manage/components/Profile.js +0 -60
- package/dist/manage/components/ReadMail.d.ts +0 -8
- package/dist/manage/components/ReadMail.js +0 -110
- package/dist/manage/components/SendMessage.d.ts +0 -9
- package/dist/manage/components/SendMessage.js +0 -79
- package/dist/manage/components/SessionList.d.ts +0 -9
- package/dist/manage/components/SessionList.js +0 -608
- package/dist/manage/components/SessionSidebar.d.ts +0 -6
- package/dist/manage/components/SessionSidebar.js +0 -24
- package/dist/manage/components/TailMessages.d.ts +0 -8
- package/dist/manage/components/TailMessages.js +0 -126
- package/dist/manage/hooks/useApi.d.ts +0 -147
- package/dist/manage/hooks/useApi.js +0 -181
- package/dist/server/cron.d.ts +0 -25
- package/dist/server/cron.js +0 -107
- package/dist/server/index.d.ts +0 -4
- package/dist/server/index.js +0 -22
- package/dist/server/routes.d.ts +0 -13
- package/dist/server/routes.js +0 -1396
|
@@ -1,1280 +0,0 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
export async function appScreensaver(options) {
|
|
3
|
-
const { url: agentUrl, password, host, port: portStr } = options;
|
|
4
|
-
const port = parseInt(portStr, 10);
|
|
5
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
6
|
-
console.error(`Error: invalid port "${portStr}"`);
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
try {
|
|
10
|
-
new URL(agentUrl);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
console.error(`Error: invalid --url "${agentUrl}"`);
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
console.log(`Screensaver: visualizing mail activity from ${agentUrl}/watch`);
|
|
17
|
-
const app = express();
|
|
18
|
-
app.use(express.json());
|
|
19
|
-
// ── GET /coworkers — proxies coworker list from agent-office ───────────────
|
|
20
|
-
app.get("/coworkers", async (_req, res) => {
|
|
21
|
-
res.setHeader("Content-Type", "application/json");
|
|
22
|
-
try {
|
|
23
|
-
const response = await fetch(`${agentUrl}/coworkers`, {
|
|
24
|
-
headers: {
|
|
25
|
-
"Authorization": `Bearer ${password}`,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
if (!response.ok) {
|
|
29
|
-
res.json([]);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const data = await response.json();
|
|
33
|
-
res.json(data);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
res.json([]);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
// ── GET / — Three.js ring visualization ─────────────────────────────────────
|
|
40
|
-
app.get("/", (_req, res) => {
|
|
41
|
-
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
42
|
-
res.send(renderScreensaverPage());
|
|
43
|
-
});
|
|
44
|
-
// ── GET /watch-stream — proxies SSE from agent-office /watch ───────────────
|
|
45
|
-
app.get("/watch-stream", async (req, res) => {
|
|
46
|
-
// Set up SSE headers
|
|
47
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
48
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
49
|
-
res.setHeader("Connection", "keep-alive");
|
|
50
|
-
const abortController = new AbortController();
|
|
51
|
-
// Handle client disconnect
|
|
52
|
-
req.on("close", () => {
|
|
53
|
-
abortController.abort();
|
|
54
|
-
res.end();
|
|
55
|
-
});
|
|
56
|
-
req.on("error", () => {
|
|
57
|
-
abortController.abort();
|
|
58
|
-
});
|
|
59
|
-
try {
|
|
60
|
-
// Connect to the agent-office /watch endpoint
|
|
61
|
-
const response = await fetch(`${agentUrl}/watch`, {
|
|
62
|
-
headers: {
|
|
63
|
-
"Authorization": `Bearer ${password}`,
|
|
64
|
-
},
|
|
65
|
-
signal: abortController.signal,
|
|
66
|
-
});
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
res.write(`event: error\n`);
|
|
69
|
-
res.write(`data: ${JSON.stringify({ error: `HTTP ${response.status}` })}\n\n`);
|
|
70
|
-
res.end();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
// Stream data from agent-office to client
|
|
74
|
-
const reader = response.body?.getReader();
|
|
75
|
-
if (!reader) {
|
|
76
|
-
res.write(`event: error\n`);
|
|
77
|
-
res.write(`data: ${JSON.stringify({ error: "No response body" })}\n\n`);
|
|
78
|
-
res.end();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// Read and forward chunks
|
|
82
|
-
while (!abortController.signal.aborted) {
|
|
83
|
-
const { done, value } = await reader.read();
|
|
84
|
-
if (done)
|
|
85
|
-
break;
|
|
86
|
-
// Forward the raw SSE data
|
|
87
|
-
const chunk = new TextDecoder().decode(value);
|
|
88
|
-
res.write(chunk);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
if (!abortController.signal.aborted) {
|
|
93
|
-
res.write(`event: error\n`);
|
|
94
|
-
res.write(`data: ${JSON.stringify({ error: String(err) })}\n\n`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
res.end();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
const server = app.listen(port, host, () => {
|
|
102
|
-
console.log(`Screensaver running at http://${host}:${port}`);
|
|
103
|
-
console.log(`Press Ctrl+C to stop.`);
|
|
104
|
-
});
|
|
105
|
-
server.on('error', (err) => {
|
|
106
|
-
if (err.code === 'EADDRINUSE') {
|
|
107
|
-
console.error(`Error: Port ${port} is already in use. Is another instance running?`);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
console.error(`Error: ${err.message}`);
|
|
111
|
-
}
|
|
112
|
-
process.exit(1);
|
|
113
|
-
});
|
|
114
|
-
// Keep process alive
|
|
115
|
-
await new Promise(() => { });
|
|
116
|
-
}
|
|
117
|
-
// ── HTML page with Three.js ring visualization ───────────────────────────────
|
|
118
|
-
function renderScreensaverPage() {
|
|
119
|
-
return `<!DOCTYPE html>
|
|
120
|
-
<html lang="en">
|
|
121
|
-
<head>
|
|
122
|
-
<meta charset="UTF-8">
|
|
123
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
124
|
-
<title>Agent Office Screensaver</title>
|
|
125
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
126
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
127
|
-
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@200;300&display=swap" rel="stylesheet">
|
|
128
|
-
<style>
|
|
129
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
130
|
-
body {
|
|
131
|
-
background: #000;
|
|
132
|
-
height: 100vh;
|
|
133
|
-
overflow: hidden;
|
|
134
|
-
}
|
|
135
|
-
canvas { display: block; }
|
|
136
|
-
</style>
|
|
137
|
-
</head>
|
|
138
|
-
<body>
|
|
139
|
-
|
|
140
|
-
<script type="importmap">
|
|
141
|
-
{
|
|
142
|
-
"imports": {
|
|
143
|
-
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
144
|
-
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
</script>
|
|
148
|
-
|
|
149
|
-
<script type="module">
|
|
150
|
-
import * as THREE from 'three'
|
|
151
|
-
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
|
152
|
-
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
|
|
153
|
-
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
|
|
154
|
-
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
|
|
155
|
-
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'
|
|
156
|
-
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
|
|
157
|
-
import { Reflector } from 'three/addons/objects/Reflector.js'
|
|
158
|
-
|
|
159
|
-
// ── Constants ──────────────────────────────────────────────────────────
|
|
160
|
-
const RING_RADIUS = 8
|
|
161
|
-
const NODE_SIZE = 0.35
|
|
162
|
-
const ARROW_DECAY_SECONDS = 5 // arrows fully fade after this many seconds
|
|
163
|
-
const ARROW_COLOR = 0xff3333
|
|
164
|
-
const ARROW_CURVE_HEIGHT = 2.5
|
|
165
|
-
const ARROW_HEAD_SIZE = 0.25
|
|
166
|
-
const LABEL_SCALE = 0.012
|
|
167
|
-
|
|
168
|
-
// ── State ──────────────────────────────────────────────────────────────
|
|
169
|
-
let agentNames = [] // sorted list of agent names
|
|
170
|
-
let agentNodes = new Map() // name -> { mesh, label, angle }
|
|
171
|
-
let arrowObjects = [] // { from, to, mesh, arrowHead, createdAt, opacity }
|
|
172
|
-
let watchState = {} // latest state from SSE
|
|
173
|
-
let coworkersSet = new Set() // valid coworker names from /coworkers endpoint
|
|
174
|
-
|
|
175
|
-
// Track all messages we've seen: key = "from->to", value = lastSent ISO string
|
|
176
|
-
let messageEdges = new Map() // "from->to" -> { lastSent: Date }
|
|
177
|
-
|
|
178
|
-
// Wait for IBM Plex Sans to load before rendering text sprites
|
|
179
|
-
let fontReady = false
|
|
180
|
-
document.fonts.load('200 48px "IBM Plex Sans"').then(() => {
|
|
181
|
-
fontReady = true
|
|
182
|
-
// Re-layout agents so labels render with the loaded font
|
|
183
|
-
if (agentNames.length > 0) layoutAgents(agentNames)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
// ── Three.js setup ─────────────────────────────────────────────────────
|
|
187
|
-
const scene = new THREE.Scene()
|
|
188
|
-
|
|
189
|
-
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000)
|
|
190
|
-
camera.position.set(0, 14, 14)
|
|
191
|
-
camera.lookAt(0, 0, 0)
|
|
192
|
-
|
|
193
|
-
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
|
194
|
-
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
195
|
-
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
196
|
-
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
197
|
-
renderer.toneMappingExposure = 1.0
|
|
198
|
-
document.body.appendChild(renderer.domElement)
|
|
199
|
-
|
|
200
|
-
const controls = new OrbitControls(camera, renderer.domElement)
|
|
201
|
-
controls.enableDamping = true
|
|
202
|
-
controls.dampingFactor = 0.05
|
|
203
|
-
controls.minDistance = 5
|
|
204
|
-
controls.maxDistance = 40
|
|
205
|
-
controls.maxPolarAngle = Math.PI / 2.1
|
|
206
|
-
|
|
207
|
-
// ── Bloom post-processing ──────────────────────────────────────────────
|
|
208
|
-
const composer = new EffectComposer(renderer)
|
|
209
|
-
|
|
210
|
-
// First pass: render the starfield background into the composer buffer
|
|
211
|
-
// (we create bgScene/bgCamera above the composer, referenced here)
|
|
212
|
-
let bgRenderPass // assigned after bgScene is created below
|
|
213
|
-
|
|
214
|
-
const renderPass = new RenderPass(scene, camera)
|
|
215
|
-
renderPass.clear = false // don't clear - stars are already in the buffer
|
|
216
|
-
renderPass.clearDepth = true // but do clear depth so scene renders on top
|
|
217
|
-
composer.addPass(renderPass)
|
|
218
|
-
|
|
219
|
-
const bloomPass = new UnrealBloomPass(
|
|
220
|
-
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
|
221
|
-
1.0, // strength
|
|
222
|
-
0.5, // radius
|
|
223
|
-
0.2 // threshold - low so the red arrows and emissive objects bloom
|
|
224
|
-
)
|
|
225
|
-
composer.addPass(bloomPass)
|
|
226
|
-
composer.addPass(new OutputPass())
|
|
227
|
-
|
|
228
|
-
// ── Starfield background (fullscreen shader) ───────────────────────────
|
|
229
|
-
// Procedural stars with varying brightness, color temperature, and twinkling
|
|
230
|
-
const starVertexShader = \`
|
|
231
|
-
varying vec2 vUv;
|
|
232
|
-
void main() {
|
|
233
|
-
vUv = uv;
|
|
234
|
-
gl_Position = vec4(position, 1.0);
|
|
235
|
-
}
|
|
236
|
-
\`
|
|
237
|
-
const starFragmentShader = \`
|
|
238
|
-
precision highp float;
|
|
239
|
-
varying vec2 vUv;
|
|
240
|
-
uniform float uTime;
|
|
241
|
-
uniform vec2 uResolution;
|
|
242
|
-
|
|
243
|
-
// Hash functions for pseudo-random values
|
|
244
|
-
float hash(vec2 p) {
|
|
245
|
-
p = fract(p * vec2(443.897, 441.423));
|
|
246
|
-
p += dot(p, p.yx + 19.19);
|
|
247
|
-
return fract((p.x + p.y) * p.x);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
float hash3(vec3 p) {
|
|
251
|
-
p = fract(p * vec3(443.897, 441.423, 437.195));
|
|
252
|
-
p += dot(p, p.yzx + 19.19);
|
|
253
|
-
return fract((p.x + p.y + p.z) * p.x);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Smooth noise
|
|
257
|
-
float noise(vec2 p) {
|
|
258
|
-
vec2 i = floor(p);
|
|
259
|
-
vec2 f = fract(p);
|
|
260
|
-
f = f * f * (3.0 - 2.0 * f);
|
|
261
|
-
float a = hash(i);
|
|
262
|
-
float b = hash(i + vec2(1.0, 0.0));
|
|
263
|
-
float c = hash(i + vec2(0.0, 1.0));
|
|
264
|
-
float d = hash(i + vec2(1.0, 1.0));
|
|
265
|
-
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Star layer - each cell may contain a star
|
|
269
|
-
float starLayer(vec2 uv, float scale, float seed) {
|
|
270
|
-
vec2 gv = fract(uv * scale) - 0.5;
|
|
271
|
-
vec2 id = floor(uv * scale);
|
|
272
|
-
|
|
273
|
-
float d = length(gv);
|
|
274
|
-
float r = hash(id + seed);
|
|
275
|
-
|
|
276
|
-
// Only rare cells have stars (realistic sparse density)
|
|
277
|
-
if (r > 0.08) return 0.0;
|
|
278
|
-
|
|
279
|
-
// Star brightness varies
|
|
280
|
-
float brightness = pow(hash(id + seed + 100.0), 3.0) * 2.0;
|
|
281
|
-
|
|
282
|
-
// Twinkling: each star twinkles at its own frequency and phase
|
|
283
|
-
float twinkleFreq = 0.5 + hash(id + seed + 200.0) * 3.0;
|
|
284
|
-
float twinklePhase = hash(id + seed + 300.0) * 6.28;
|
|
285
|
-
float twinkle = 0.7 + 0.3 * sin(uTime * twinkleFreq + twinklePhase);
|
|
286
|
-
brightness *= twinkle;
|
|
287
|
-
|
|
288
|
-
// Star size varies
|
|
289
|
-
float starSize = 0.005 + hash(id + seed + 400.0) * 0.015;
|
|
290
|
-
|
|
291
|
-
// Soft circular falloff
|
|
292
|
-
float star = smoothstep(starSize, starSize * 0.1, d) * brightness;
|
|
293
|
-
|
|
294
|
-
// Add subtle diffraction spikes on brighter stars
|
|
295
|
-
if (brightness > 0.8) {
|
|
296
|
-
float spike = max(
|
|
297
|
-
exp(-abs(gv.x) * 80.0) * exp(-abs(gv.y) * 8.0),
|
|
298
|
-
exp(-abs(gv.y) * 80.0) * exp(-abs(gv.x) * 8.0)
|
|
299
|
-
) * brightness * 0.15;
|
|
300
|
-
star += spike;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return star;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
void main() {
|
|
307
|
-
vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution) / min(uResolution.x, uResolution.y);
|
|
308
|
-
|
|
309
|
-
// Very dark blue-black base
|
|
310
|
-
vec3 bg = vec3(0.005, 0.007, 0.015);
|
|
311
|
-
|
|
312
|
-
// Extremely subtle nebula-like color variation
|
|
313
|
-
float n1 = noise(uv * 2.0 + uTime * 0.01);
|
|
314
|
-
float n2 = noise(uv * 3.0 - uTime * 0.015 + 50.0);
|
|
315
|
-
bg += vec3(0.008, 0.004, 0.015) * n1;
|
|
316
|
-
bg += vec3(0.003, 0.008, 0.012) * n2;
|
|
317
|
-
|
|
318
|
-
// Accumulate multiple star layers at different scales (depth)
|
|
319
|
-
float stars = 0.0;
|
|
320
|
-
vec3 starColor = vec3(0.0);
|
|
321
|
-
|
|
322
|
-
for (float i = 0.0; i < 2.0; i++) {
|
|
323
|
-
float scale = 40.0 + i * 50.0;
|
|
324
|
-
float layerStars = starLayer(uv, scale, i * 73.156);
|
|
325
|
-
|
|
326
|
-
// Color temperature varies per star (white, blue-white, warm yellow)
|
|
327
|
-
vec2 id = floor(uv * scale);
|
|
328
|
-
float temp = hash(id + i * 73.156 + 500.0);
|
|
329
|
-
vec3 col;
|
|
330
|
-
if (temp < 0.25) {
|
|
331
|
-
col = vec3(0.8, 0.85, 1.0); // blue-white (hot)
|
|
332
|
-
} else if (temp < 0.45) {
|
|
333
|
-
col = vec3(0.7, 0.8, 1.0); // blue
|
|
334
|
-
} else if (temp < 0.75) {
|
|
335
|
-
col = vec3(1.0, 0.97, 0.92); // white-warm
|
|
336
|
-
} else if (temp < 0.9) {
|
|
337
|
-
col = vec3(1.0, 0.9, 0.7); // warm yellow
|
|
338
|
-
} else {
|
|
339
|
-
col = vec3(1.0, 0.75, 0.6); // orange-ish (cool star)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
starColor += layerStars * col;
|
|
343
|
-
stars += layerStars;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
vec3 finalColor = bg + starColor;
|
|
347
|
-
|
|
348
|
-
// Very faint milky way band across the diagonal
|
|
349
|
-
float band = exp(-pow((uv.x + uv.y) * 1.5, 2.0) * 3.0);
|
|
350
|
-
float bandNoise = noise(uv * 8.0 + 100.0) * noise(uv * 16.0 + 200.0);
|
|
351
|
-
finalColor += vec3(0.012, 0.015, 0.025) * band * bandNoise * 3.0;
|
|
352
|
-
|
|
353
|
-
gl_FragColor = vec4(finalColor, 1.0);
|
|
354
|
-
}
|
|
355
|
-
\`
|
|
356
|
-
|
|
357
|
-
const starUniforms = {
|
|
358
|
-
uTime: { value: 0.0 },
|
|
359
|
-
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Fullscreen quad rendered behind everything
|
|
363
|
-
const starMaterial = new THREE.ShaderMaterial({
|
|
364
|
-
vertexShader: starVertexShader,
|
|
365
|
-
fragmentShader: starFragmentShader,
|
|
366
|
-
uniforms: starUniforms,
|
|
367
|
-
depthWrite: false,
|
|
368
|
-
depthTest: false,
|
|
369
|
-
})
|
|
370
|
-
const starQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), starMaterial)
|
|
371
|
-
starQuad.frustumCulled = false
|
|
372
|
-
|
|
373
|
-
// Render stars as a separate background scene so they're always behind
|
|
374
|
-
const bgScene = new THREE.Scene()
|
|
375
|
-
const bgCamera = new THREE.Camera()
|
|
376
|
-
bgScene.add(starQuad)
|
|
377
|
-
|
|
378
|
-
// Insert the bg render pass at the start of the composer chain
|
|
379
|
-
bgRenderPass = new RenderPass(bgScene, bgCamera)
|
|
380
|
-
bgRenderPass.clear = true
|
|
381
|
-
composer.insertPass(bgRenderPass, 0)
|
|
382
|
-
|
|
383
|
-
// ── Lighting ───────────────────────────────────────────────────────────
|
|
384
|
-
const ambientLight = new THREE.AmbientLight(0x334466, 0.6)
|
|
385
|
-
scene.add(ambientLight)
|
|
386
|
-
|
|
387
|
-
const pointLight = new THREE.PointLight(0x6c8eff, 1.5, 50)
|
|
388
|
-
pointLight.position.set(0, 8, 0)
|
|
389
|
-
scene.add(pointLight)
|
|
390
|
-
|
|
391
|
-
// Subtle ground glow
|
|
392
|
-
const groundGlow = new THREE.PointLight(0x1a3a5a, 0.5, 30)
|
|
393
|
-
groundGlow.position.set(0, -1, 0)
|
|
394
|
-
scene.add(groundGlow)
|
|
395
|
-
|
|
396
|
-
// ── Procedural environment cubemap for metal reflections ─────────────
|
|
397
|
-
// Without an envMap, MeshStandardMaterial with metalness looks flat black.
|
|
398
|
-
// Generate a simple dark gradient cubemap so the metal has something to reflect.
|
|
399
|
-
function generateEnvMap() {
|
|
400
|
-
const size = 128
|
|
401
|
-
const faces = []
|
|
402
|
-
for (let f = 0; f < 6; f++) {
|
|
403
|
-
const canvas = document.createElement('canvas')
|
|
404
|
-
canvas.width = size
|
|
405
|
-
canvas.height = size
|
|
406
|
-
const ctx = canvas.getContext('2d')
|
|
407
|
-
// Dark gradient: bluish at top, near-black at bottom
|
|
408
|
-
const grad = ctx.createLinearGradient(0, 0, 0, size)
|
|
409
|
-
if (f === 2) { // +Y (top)
|
|
410
|
-
grad.addColorStop(0, '#0a1828')
|
|
411
|
-
grad.addColorStop(1, '#162540')
|
|
412
|
-
} else if (f === 3) { // -Y (bottom)
|
|
413
|
-
grad.addColorStop(0, '#020408')
|
|
414
|
-
grad.addColorStop(1, '#050a10')
|
|
415
|
-
} else {
|
|
416
|
-
grad.addColorStop(0, '#0c1a2a')
|
|
417
|
-
grad.addColorStop(0.5, '#081018')
|
|
418
|
-
grad.addColorStop(1, '#040810')
|
|
419
|
-
}
|
|
420
|
-
ctx.fillStyle = grad
|
|
421
|
-
ctx.fillRect(0, 0, size, size)
|
|
422
|
-
|
|
423
|
-
// Scatter a few dim specks to give the reflections some texture
|
|
424
|
-
for (let i = 0; i < 40; i++) {
|
|
425
|
-
const x = Math.random() * size
|
|
426
|
-
const y = Math.random() * size
|
|
427
|
-
const brightness = Math.floor(20 + Math.random() * 30)
|
|
428
|
-
ctx.fillStyle = \`rgb(\${brightness}, \${brightness + 5}, \${brightness + 15})\`
|
|
429
|
-
ctx.beginPath()
|
|
430
|
-
ctx.arc(x, y, 0.5 + Math.random() * 1.0, 0, Math.PI * 2)
|
|
431
|
-
ctx.fill()
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
faces.push(canvas)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const cubeTexture = new THREE.CubeTexture(faces)
|
|
438
|
-
cubeTexture.needsUpdate = true
|
|
439
|
-
return cubeTexture
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const envMap = generateEnvMap()
|
|
443
|
-
scene.environment = envMap
|
|
444
|
-
|
|
445
|
-
// ── Ground ring (the platform) — dark brushed metal ────────────────────
|
|
446
|
-
const ringGeo = new THREE.RingGeometry(RING_RADIUS - 1.8, RING_RADIUS + 1.8, 128, 4)
|
|
447
|
-
const ringMat = new THREE.MeshStandardMaterial({
|
|
448
|
-
color: 0x1a1e28,
|
|
449
|
-
roughness: 0.55,
|
|
450
|
-
metalness: 0.92,
|
|
451
|
-
envMap: envMap,
|
|
452
|
-
envMapIntensity: 1.5,
|
|
453
|
-
side: THREE.DoubleSide,
|
|
454
|
-
})
|
|
455
|
-
const ringMesh = new THREE.Mesh(ringGeo, ringMat)
|
|
456
|
-
ringMesh.rotation.x = -Math.PI / 2
|
|
457
|
-
ringMesh.position.y = -0.05
|
|
458
|
-
scene.add(ringMesh)
|
|
459
|
-
|
|
460
|
-
// ── Glass fence on outer edge ─────────────────────────────────────────
|
|
461
|
-
const fenceRadius = RING_RADIUS + 1.75
|
|
462
|
-
const fenceHeight = 1.8
|
|
463
|
-
|
|
464
|
-
// Single thin cylinder for the glass wall
|
|
465
|
-
const glassGeo = new THREE.CylinderGeometry(fenceRadius, fenceRadius, fenceHeight, 128, 1, true)
|
|
466
|
-
const glassMat = new THREE.MeshPhysicalMaterial({
|
|
467
|
-
color: 0x88aabb,
|
|
468
|
-
transparent: true,
|
|
469
|
-
opacity: 0.1,
|
|
470
|
-
roughness: 0.05,
|
|
471
|
-
metalness: 0.2,
|
|
472
|
-
transmission: 0.9,
|
|
473
|
-
thickness: 0.03,
|
|
474
|
-
ior: 1.5,
|
|
475
|
-
envMap: envMap,
|
|
476
|
-
envMapIntensity: 3.0,
|
|
477
|
-
side: THREE.DoubleSide,
|
|
478
|
-
depthWrite: false,
|
|
479
|
-
})
|
|
480
|
-
const glassWall = new THREE.Mesh(glassGeo, glassMat)
|
|
481
|
-
glassWall.position.y = fenceHeight / 2
|
|
482
|
-
scene.add(glassWall)
|
|
483
|
-
|
|
484
|
-
// Metal rail around the top
|
|
485
|
-
const railMat = new THREE.MeshStandardMaterial({
|
|
486
|
-
color: 0x3a3e48,
|
|
487
|
-
roughness: 0.3,
|
|
488
|
-
metalness: 0.95,
|
|
489
|
-
envMap: envMap,
|
|
490
|
-
envMapIntensity: 2.0,
|
|
491
|
-
})
|
|
492
|
-
const rail = new THREE.Mesh(
|
|
493
|
-
new THREE.TorusGeometry(fenceRadius, 0.02, 6, 128),
|
|
494
|
-
railMat
|
|
495
|
-
)
|
|
496
|
-
rail.rotation.x = -Math.PI / 2
|
|
497
|
-
rail.position.y = fenceHeight
|
|
498
|
-
scene.add(rail)
|
|
499
|
-
|
|
500
|
-
// Thin metal poles evenly spaced
|
|
501
|
-
const poleMat = new THREE.MeshStandardMaterial({
|
|
502
|
-
color: 0x3a3e48,
|
|
503
|
-
roughness: 0.35,
|
|
504
|
-
metalness: 0.95,
|
|
505
|
-
envMap: envMap,
|
|
506
|
-
envMapIntensity: 2.0,
|
|
507
|
-
})
|
|
508
|
-
const poleCount = 12
|
|
509
|
-
for (let i = 0; i < poleCount; i++) {
|
|
510
|
-
const angle = (i / poleCount) * Math.PI * 2
|
|
511
|
-
const pole = new THREE.Mesh(
|
|
512
|
-
new THREE.CylinderGeometry(0.02, 0.02, fenceHeight, 6),
|
|
513
|
-
poleMat
|
|
514
|
-
)
|
|
515
|
-
pole.position.set(
|
|
516
|
-
Math.cos(angle) * fenceRadius,
|
|
517
|
-
fenceHeight / 2,
|
|
518
|
-
Math.sin(angle) * fenceRadius
|
|
519
|
-
)
|
|
520
|
-
scene.add(pole)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ── Reflective water pool in the center ───────────────────────────────
|
|
524
|
-
const poolGeo = new THREE.CircleGeometry(RING_RADIUS - 1.85, 64)
|
|
525
|
-
const pool = new Reflector(poolGeo, {
|
|
526
|
-
color: 0x445566,
|
|
527
|
-
textureWidth: 1024,
|
|
528
|
-
textureHeight: 1024,
|
|
529
|
-
clipBias: 0.003,
|
|
530
|
-
})
|
|
531
|
-
pool.rotation.x = -Math.PI / 2
|
|
532
|
-
pool.position.y = -0.04
|
|
533
|
-
scene.add(pool)
|
|
534
|
-
|
|
535
|
-
// Dark water surface shader overlay
|
|
536
|
-
const waterUniforms = {
|
|
537
|
-
uTime: { value: 0.0 },
|
|
538
|
-
uReflectTex: { value: pool.getRenderTarget().texture },
|
|
539
|
-
}
|
|
540
|
-
const waterMat = new THREE.ShaderMaterial({
|
|
541
|
-
uniforms: waterUniforms,
|
|
542
|
-
transparent: true,
|
|
543
|
-
depthWrite: false,
|
|
544
|
-
vertexShader: \`
|
|
545
|
-
varying vec2 vUv;
|
|
546
|
-
varying vec3 vWorldPos;
|
|
547
|
-
varying vec3 vNormal;
|
|
548
|
-
void main() {
|
|
549
|
-
vUv = uv;
|
|
550
|
-
vNormal = normalize(normalMatrix * normal);
|
|
551
|
-
vec4 wp = modelMatrix * vec4(position, 1.0);
|
|
552
|
-
vWorldPos = wp.xyz;
|
|
553
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
554
|
-
}
|
|
555
|
-
\`,
|
|
556
|
-
fragmentShader: \`
|
|
557
|
-
precision highp float;
|
|
558
|
-
varying vec2 vUv;
|
|
559
|
-
varying vec3 vWorldPos;
|
|
560
|
-
varying vec3 vNormal;
|
|
561
|
-
uniform float uTime;
|
|
562
|
-
uniform sampler2D uReflectTex;
|
|
563
|
-
|
|
564
|
-
// --- Noise functions ---
|
|
565
|
-
vec2 hash2(vec2 p) {
|
|
566
|
-
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
|
|
567
|
-
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
float noise(vec2 p) {
|
|
571
|
-
vec2 i = floor(p);
|
|
572
|
-
vec2 f = fract(p);
|
|
573
|
-
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
574
|
-
return mix(mix(dot(hash2(i + vec2(0,0)), f - vec2(0,0)),
|
|
575
|
-
dot(hash2(i + vec2(1,0)), f - vec2(1,0)), u.x),
|
|
576
|
-
mix(dot(hash2(i + vec2(0,1)), f - vec2(0,1)),
|
|
577
|
-
dot(hash2(i + vec2(1,1)), f - vec2(1,1)), u.x), u.y);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
float fbm(vec2 p) {
|
|
581
|
-
float v = 0.0;
|
|
582
|
-
float a = 0.5;
|
|
583
|
-
vec2 shift = vec2(100.0);
|
|
584
|
-
mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
|
|
585
|
-
for (int i = 0; i < 5; i++) {
|
|
586
|
-
v += a * noise(p);
|
|
587
|
-
p = rot * p * 2.0 + shift;
|
|
588
|
-
a *= 0.5;
|
|
589
|
-
}
|
|
590
|
-
return v;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
void main() {
|
|
594
|
-
vec2 p = (vUv - 0.5) * 2.0;
|
|
595
|
-
float r = length(p);
|
|
596
|
-
|
|
597
|
-
// Edge fade so water doesn't touch the ring hard
|
|
598
|
-
float edgeFade = smoothstep(1.0, 0.85, r);
|
|
599
|
-
if (edgeFade < 0.001) discard;
|
|
600
|
-
|
|
601
|
-
// Animated FBM water surface
|
|
602
|
-
float t = uTime * 0.15;
|
|
603
|
-
vec2 q = vec2(fbm(p * 3.0 + vec2(t, 0.0)),
|
|
604
|
-
fbm(p * 3.0 + vec2(0.0, t)));
|
|
605
|
-
vec2 rr = vec2(fbm(p * 3.0 + 4.0 * q + vec2(1.7, 9.2) + vec2(t * 0.3)),
|
|
606
|
-
fbm(p * 3.0 + 4.0 * q + vec2(8.3, 2.8) + vec2(0.0, t * 0.2)));
|
|
607
|
-
float f = fbm(p * 3.0 + 4.0 * rr);
|
|
608
|
-
|
|
609
|
-
// Perturbed normal for subtle light interaction
|
|
610
|
-
float dx = fbm((p + vec2(0.01, 0.0)) * 3.0 + 4.0 * rr) - f;
|
|
611
|
-
float dy = fbm((p + vec2(0.0, 0.01)) * 3.0 + 4.0 * rr) - f;
|
|
612
|
-
vec3 perturbedNormal = normalize(vec3(-dx * 8.0, 1.0, -dy * 8.0));
|
|
613
|
-
|
|
614
|
-
// Fresnel - more reflective at glancing angles
|
|
615
|
-
vec3 viewDir = normalize(cameraPosition - vWorldPos);
|
|
616
|
-
float fresnel = pow(1.0 - max(dot(viewDir, perturbedNormal), 0.0), 3.0);
|
|
617
|
-
fresnel = 0.15 + 0.85 * fresnel;
|
|
618
|
-
|
|
619
|
-
// Deep water color with subtle variation
|
|
620
|
-
vec3 deepColor = vec3(0.01, 0.025, 0.04);
|
|
621
|
-
vec3 midColor = vec3(0.02, 0.05, 0.08);
|
|
622
|
-
vec3 waterCol = mix(deepColor, midColor, f * 0.5 + 0.5);
|
|
623
|
-
|
|
624
|
-
// Subtle caustic-like bright spots from the FBM pattern
|
|
625
|
-
float caustic = pow(max(f * 0.5 + 0.5, 0.0), 6.0) * 0.3;
|
|
626
|
-
waterCol += vec3(0.05, 0.12, 0.15) * caustic;
|
|
627
|
-
|
|
628
|
-
// Reflection lookup (screen-space UV from the reflector)
|
|
629
|
-
vec2 reflUv = gl_FragCoord.xy / vec2(\${Math.round(1024)}.0);
|
|
630
|
-
reflUv += perturbedNormal.xz * 0.02; // distort reflection
|
|
631
|
-
vec3 reflColor = texture2D(uReflectTex, clamp(reflUv, 0.0, 1.0)).rgb;
|
|
632
|
-
|
|
633
|
-
// Blend water color with reflection via fresnel
|
|
634
|
-
vec3 color = mix(waterCol, waterCol + reflColor * 0.3, fresnel);
|
|
635
|
-
|
|
636
|
-
// Very subtle surface highlights (specular)
|
|
637
|
-
vec3 lightDir = normalize(vec3(0.0, 1.0, 0.0));
|
|
638
|
-
vec3 halfDir = normalize(lightDir + viewDir);
|
|
639
|
-
float spec = pow(max(dot(perturbedNormal, halfDir), 0.0), 120.0);
|
|
640
|
-
color += vec3(0.15, 0.25, 0.3) * spec * 0.5;
|
|
641
|
-
|
|
642
|
-
gl_FragColor = vec4(color, edgeFade * 0.85);
|
|
643
|
-
}
|
|
644
|
-
\`,
|
|
645
|
-
})
|
|
646
|
-
const waterOverlay = new THREE.Mesh(
|
|
647
|
-
new THREE.CircleGeometry(RING_RADIUS - 1.84, 64),
|
|
648
|
-
waterMat
|
|
649
|
-
)
|
|
650
|
-
waterOverlay.rotation.x = -Math.PI / 2
|
|
651
|
-
waterOverlay.position.y = -0.025
|
|
652
|
-
scene.add(waterOverlay)
|
|
653
|
-
|
|
654
|
-
// ── Center wireframe icosahedron ───────────────────────────────────────
|
|
655
|
-
const icoGeo = new THREE.IcosahedronGeometry(1.8, 1)
|
|
656
|
-
const icoWire = new THREE.WireframeGeometry(icoGeo)
|
|
657
|
-
const icoMat = new THREE.LineBasicMaterial({ color: 0x5ae0c0, transparent: true, opacity: 0.7 })
|
|
658
|
-
const icoLines = new THREE.LineSegments(icoWire, icoMat)
|
|
659
|
-
icoLines.position.y = 2.5
|
|
660
|
-
scene.add(icoLines)
|
|
661
|
-
|
|
662
|
-
// Inner glow sphere
|
|
663
|
-
const glowSphereGeo = new THREE.SphereGeometry(1.5, 32, 32)
|
|
664
|
-
const glowSphereMat = new THREE.MeshBasicMaterial({
|
|
665
|
-
color: 0x2a8a7a,
|
|
666
|
-
transparent: true,
|
|
667
|
-
opacity: 0.12,
|
|
668
|
-
})
|
|
669
|
-
const glowSphere = new THREE.Mesh(glowSphereGeo, glowSphereMat)
|
|
670
|
-
glowSphere.position.y = 2.5
|
|
671
|
-
scene.add(glowSphere)
|
|
672
|
-
|
|
673
|
-
// Core point light inside the icosahedron to cast glow on nearby objects
|
|
674
|
-
const icoLight = new THREE.PointLight(0x4ae0b0, 1.2, 12)
|
|
675
|
-
icoLight.position.y = 2.5
|
|
676
|
-
scene.add(icoLight)
|
|
677
|
-
|
|
678
|
-
// ── Text label helper (Canvas-based sprites) ───────────────────────────
|
|
679
|
-
const FONT_FAMILY = '"IBM Plex Sans", sans-serif'
|
|
680
|
-
|
|
681
|
-
function createTextSprite(text, fontSize = 48, color = '#b0c4de') {
|
|
682
|
-
const canvas = document.createElement('canvas')
|
|
683
|
-
const ctx = canvas.getContext('2d')
|
|
684
|
-
ctx.font = \`200 \${fontSize}px \${FONT_FAMILY}\`
|
|
685
|
-
const metrics = ctx.measureText(text)
|
|
686
|
-
const width = metrics.width + 20
|
|
687
|
-
const height = fontSize * 1.4
|
|
688
|
-
|
|
689
|
-
canvas.width = width
|
|
690
|
-
canvas.height = height
|
|
691
|
-
|
|
692
|
-
ctx.font = \`200 \${fontSize}px \${FONT_FAMILY}\`
|
|
693
|
-
ctx.textAlign = 'center'
|
|
694
|
-
ctx.textBaseline = 'middle'
|
|
695
|
-
ctx.fillStyle = color
|
|
696
|
-
ctx.fillText(text, width / 2, height / 2)
|
|
697
|
-
|
|
698
|
-
const texture = new THREE.CanvasTexture(canvas)
|
|
699
|
-
texture.minFilter = THREE.LinearFilter
|
|
700
|
-
const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false })
|
|
701
|
-
const sprite = new THREE.Sprite(spriteMat)
|
|
702
|
-
sprite.scale.set(width * LABEL_SCALE, height * LABEL_SCALE, 1)
|
|
703
|
-
return sprite
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// ── Agent node helper ──────────────────────────────────────────────────
|
|
707
|
-
// Shared materials for all agent figures
|
|
708
|
-
const figureMat = new THREE.MeshStandardMaterial({
|
|
709
|
-
color: 0x8090a0,
|
|
710
|
-
roughness: 0.6,
|
|
711
|
-
metalness: 0.3,
|
|
712
|
-
envMap: envMap,
|
|
713
|
-
})
|
|
714
|
-
const deskMat = new THREE.MeshStandardMaterial({
|
|
715
|
-
color: 0x3a2a1a,
|
|
716
|
-
roughness: 0.7,
|
|
717
|
-
metalness: 0.1,
|
|
718
|
-
envMap: envMap,
|
|
719
|
-
})
|
|
720
|
-
const chairMat = new THREE.MeshStandardMaterial({
|
|
721
|
-
color: 0x1a1a22,
|
|
722
|
-
roughness: 0.8,
|
|
723
|
-
metalness: 0.2,
|
|
724
|
-
envMap: envMap,
|
|
725
|
-
})
|
|
726
|
-
// Monitor screen material -- emissive so it glows and responds to pulse
|
|
727
|
-
const screenMat = new THREE.MeshStandardMaterial({
|
|
728
|
-
color: 0x4488aa,
|
|
729
|
-
emissive: 0x2a4a7a,
|
|
730
|
-
emissiveIntensity: 0.5,
|
|
731
|
-
roughness: 0.1,
|
|
732
|
-
metalness: 0.5,
|
|
733
|
-
})
|
|
734
|
-
const monitorFrameMat = new THREE.MeshStandardMaterial({
|
|
735
|
-
color: 0x151518,
|
|
736
|
-
roughness: 0.4,
|
|
737
|
-
metalness: 0.8,
|
|
738
|
-
envMap: envMap,
|
|
739
|
-
})
|
|
740
|
-
|
|
741
|
-
function createAgentNode(name, angle) {
|
|
742
|
-
const group = new THREE.Group()
|
|
743
|
-
|
|
744
|
-
// Everything is built in local space facing +Z (inward),
|
|
745
|
-
// then the group is rotated to face the center.
|
|
746
|
-
const figureGroup = new THREE.Group()
|
|
747
|
-
|
|
748
|
-
// ── Desk ──
|
|
749
|
-
// Tabletop
|
|
750
|
-
const tabletop = new THREE.Mesh(
|
|
751
|
-
new THREE.BoxGeometry(0.9, 0.04, 0.45),
|
|
752
|
-
deskMat
|
|
753
|
-
)
|
|
754
|
-
tabletop.position.set(0, 0.38, 0.25)
|
|
755
|
-
figureGroup.add(tabletop)
|
|
756
|
-
|
|
757
|
-
// Desk legs (4)
|
|
758
|
-
const legGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.38, 6)
|
|
759
|
-
const legPositions = [[-0.4, 0.19, 0.05], [0.4, 0.19, 0.05], [-0.4, 0.19, 0.45], [0.4, 0.19, 0.45]]
|
|
760
|
-
for (const [lx, ly, lz] of legPositions) {
|
|
761
|
-
const leg = new THREE.Mesh(legGeo, monitorFrameMat)
|
|
762
|
-
leg.position.set(lx, ly, lz)
|
|
763
|
-
figureGroup.add(leg)
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// ── Monitor ──
|
|
767
|
-
// Monitor frame (thin box)
|
|
768
|
-
const monitorFrame = new THREE.Mesh(
|
|
769
|
-
new THREE.BoxGeometry(0.5, 0.35, 0.025),
|
|
770
|
-
monitorFrameMat
|
|
771
|
-
)
|
|
772
|
-
monitorFrame.position.set(0, 0.60, 0.35)
|
|
773
|
-
figureGroup.add(monitorFrame)
|
|
774
|
-
|
|
775
|
-
// Monitor screen (slightly inset, emissive) — unique per agent for pulse
|
|
776
|
-
const agentScreenMat = screenMat.clone()
|
|
777
|
-
const screen = new THREE.Mesh(
|
|
778
|
-
new THREE.BoxGeometry(0.44, 0.29, 0.005),
|
|
779
|
-
agentScreenMat
|
|
780
|
-
)
|
|
781
|
-
screen.position.set(0, 0.60, 0.336)
|
|
782
|
-
figureGroup.add(screen)
|
|
783
|
-
|
|
784
|
-
// Monitor stand (thin cylinder)
|
|
785
|
-
const stand = new THREE.Mesh(
|
|
786
|
-
new THREE.CylinderGeometry(0.02, 0.03, 0.18, 6),
|
|
787
|
-
monitorFrameMat
|
|
788
|
-
)
|
|
789
|
-
stand.position.set(0, 0.42, 0.35)
|
|
790
|
-
figureGroup.add(stand)
|
|
791
|
-
|
|
792
|
-
// ── Chair ──
|
|
793
|
-
// Seat
|
|
794
|
-
const seat = new THREE.Mesh(
|
|
795
|
-
new THREE.BoxGeometry(0.4, 0.04, 0.38),
|
|
796
|
-
chairMat
|
|
797
|
-
)
|
|
798
|
-
seat.position.set(0, 0.30, -0.2)
|
|
799
|
-
figureGroup.add(seat)
|
|
800
|
-
|
|
801
|
-
// Chair back
|
|
802
|
-
const chairBack = new THREE.Mesh(
|
|
803
|
-
new THREE.BoxGeometry(0.4, 0.4, 0.04),
|
|
804
|
-
chairMat
|
|
805
|
-
)
|
|
806
|
-
chairBack.position.set(0, 0.50, -0.37)
|
|
807
|
-
figureGroup.add(chairBack)
|
|
808
|
-
|
|
809
|
-
// Chair pedestal
|
|
810
|
-
const pedestal = new THREE.Mesh(
|
|
811
|
-
new THREE.CylinderGeometry(0.03, 0.03, 0.28, 6),
|
|
812
|
-
monitorFrameMat
|
|
813
|
-
)
|
|
814
|
-
pedestal.position.set(0, 0.16, -0.2)
|
|
815
|
-
figureGroup.add(pedestal)
|
|
816
|
-
|
|
817
|
-
// Chair base (5 legs radiating out)
|
|
818
|
-
for (let i = 0; i < 5; i++) {
|
|
819
|
-
const a = (i / 5) * Math.PI * 2
|
|
820
|
-
const cLeg = new THREE.Mesh(
|
|
821
|
-
new THREE.CylinderGeometry(0.012, 0.012, 0.18, 4),
|
|
822
|
-
monitorFrameMat
|
|
823
|
-
)
|
|
824
|
-
cLeg.position.set(
|
|
825
|
-
Math.sin(a) * 0.09,
|
|
826
|
-
0.02,
|
|
827
|
-
-0.2 + Math.cos(a) * 0.09
|
|
828
|
-
)
|
|
829
|
-
cLeg.rotation.z = Math.sin(a) * 0.8
|
|
830
|
-
cLeg.rotation.x = -Math.cos(a) * 0.8
|
|
831
|
-
figureGroup.add(cLeg)
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// ── Person (seated figure) ──
|
|
835
|
-
// Torso
|
|
836
|
-
const torso = new THREE.Mesh(
|
|
837
|
-
new THREE.CylinderGeometry(0.12, 0.10, 0.28, 8),
|
|
838
|
-
figureMat
|
|
839
|
-
)
|
|
840
|
-
torso.position.set(0, 0.50, -0.2)
|
|
841
|
-
figureGroup.add(torso)
|
|
842
|
-
|
|
843
|
-
// Head
|
|
844
|
-
const head = new THREE.Mesh(
|
|
845
|
-
new THREE.SphereGeometry(0.09, 12, 10),
|
|
846
|
-
figureMat
|
|
847
|
-
)
|
|
848
|
-
head.position.set(0, 0.73, -0.2)
|
|
849
|
-
figureGroup.add(head)
|
|
850
|
-
|
|
851
|
-
// Upper arms (angled forward toward desk)
|
|
852
|
-
const armGeo = new THREE.CylinderGeometry(0.035, 0.03, 0.22, 6)
|
|
853
|
-
for (const side of [-1, 1]) {
|
|
854
|
-
const upperArm = new THREE.Mesh(armGeo, figureMat)
|
|
855
|
-
upperArm.position.set(side * 0.16, 0.48, -0.05)
|
|
856
|
-
upperArm.rotation.x = -0.9
|
|
857
|
-
upperArm.rotation.z = side * 0.15
|
|
858
|
-
figureGroup.add(upperArm)
|
|
859
|
-
|
|
860
|
-
// Forearm resting on desk
|
|
861
|
-
const forearm = new THREE.Mesh(
|
|
862
|
-
new THREE.CylinderGeometry(0.03, 0.025, 0.2, 6),
|
|
863
|
-
figureMat
|
|
864
|
-
)
|
|
865
|
-
forearm.position.set(side * 0.2, 0.40, 0.15)
|
|
866
|
-
forearm.rotation.x = -1.5
|
|
867
|
-
figureGroup.add(forearm)
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Upper legs (seated, going forward)
|
|
871
|
-
const legGeoFig = new THREE.CylinderGeometry(0.05, 0.04, 0.3, 6)
|
|
872
|
-
for (const side of [-1, 1]) {
|
|
873
|
-
const thigh = new THREE.Mesh(legGeoFig, figureMat)
|
|
874
|
-
thigh.position.set(side * 0.08, 0.30, -0.05)
|
|
875
|
-
thigh.rotation.x = -1.45
|
|
876
|
-
figureGroup.add(thigh)
|
|
877
|
-
|
|
878
|
-
// Lower legs (hanging down)
|
|
879
|
-
const shin = new THREE.Mesh(
|
|
880
|
-
new THREE.CylinderGeometry(0.04, 0.035, 0.28, 6),
|
|
881
|
-
figureMat
|
|
882
|
-
)
|
|
883
|
-
shin.position.set(side * 0.08, 0.14, 0.08)
|
|
884
|
-
figureGroup.add(shin)
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// ── Subtle screen glow light ──
|
|
888
|
-
const nodeLight = new THREE.PointLight(0x6c8eff, 0.4, 3)
|
|
889
|
-
nodeLight.position.set(0, 0.6, 0.3)
|
|
890
|
-
figureGroup.add(nodeLight)
|
|
891
|
-
|
|
892
|
-
// Rotate the whole figure to face inward (toward center)
|
|
893
|
-
figureGroup.rotation.y = angle + Math.PI
|
|
894
|
-
group.add(figureGroup)
|
|
895
|
-
|
|
896
|
-
// Name label above
|
|
897
|
-
const label = createTextSprite(name, 36, '#8ab4f8')
|
|
898
|
-
label.position.y = 1.05
|
|
899
|
-
group.add(label)
|
|
900
|
-
|
|
901
|
-
// Position on ring
|
|
902
|
-
const x = Math.cos(angle) * RING_RADIUS
|
|
903
|
-
const z = Math.sin(angle) * RING_RADIUS
|
|
904
|
-
group.position.set(x, 0, z)
|
|
905
|
-
|
|
906
|
-
return { group, sphereMat: agentScreenMat, label, angle }
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// ── Standing scientists along the inner ring edge ─────────────────────
|
|
910
|
-
{
|
|
911
|
-
const innerEdgeRadius = RING_RADIUS - 1.4
|
|
912
|
-
const scientistCount = 2
|
|
913
|
-
// Spread them at irregular angles so they look casually placed
|
|
914
|
-
const baseAngles = [0.8, 3.6]
|
|
915
|
-
|
|
916
|
-
for (let i = 0; i < scientistCount; i++) {
|
|
917
|
-
const angle = baseAngles[i]
|
|
918
|
-
const group = new THREE.Group()
|
|
919
|
-
|
|
920
|
-
// Slight random rotation so they're not all identical
|
|
921
|
-
const facingJitter = (Math.random() - 0.5) * 0.3
|
|
922
|
-
|
|
923
|
-
// ── Standing body ──
|
|
924
|
-
// Legs
|
|
925
|
-
const legGeo = new THREE.CylinderGeometry(0.035, 0.03, 0.45, 6)
|
|
926
|
-
for (const side of [-1, 1]) {
|
|
927
|
-
const leg = new THREE.Mesh(legGeo, figureMat)
|
|
928
|
-
leg.position.set(side * 0.06, 0.225, 0)
|
|
929
|
-
group.add(leg)
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Torso
|
|
933
|
-
const torso = new THREE.Mesh(
|
|
934
|
-
new THREE.CylinderGeometry(0.11, 0.09, 0.35, 8),
|
|
935
|
-
figureMat
|
|
936
|
-
)
|
|
937
|
-
torso.position.y = 0.625
|
|
938
|
-
group.add(torso)
|
|
939
|
-
|
|
940
|
-
// Head - tilted slightly downward (looking at water)
|
|
941
|
-
const headGroup = new THREE.Group()
|
|
942
|
-
headGroup.position.y = 0.88
|
|
943
|
-
const head = new THREE.Mesh(
|
|
944
|
-
new THREE.SphereGeometry(0.08, 10, 8),
|
|
945
|
-
figureMat
|
|
946
|
-
)
|
|
947
|
-
headGroup.add(head)
|
|
948
|
-
headGroup.rotation.x = 0.25 // tilted down, looking at water
|
|
949
|
-
group.add(headGroup)
|
|
950
|
-
|
|
951
|
-
// Arms - one arm down, one arm up near chin (thoughtful pose)
|
|
952
|
-
// Left arm: hanging at side
|
|
953
|
-
const armDown = new THREE.Mesh(
|
|
954
|
-
new THREE.CylinderGeometry(0.03, 0.025, 0.32, 6),
|
|
955
|
-
figureMat
|
|
956
|
-
)
|
|
957
|
-
armDown.position.set(-0.14, 0.50, 0)
|
|
958
|
-
armDown.rotation.z = 0.08
|
|
959
|
-
group.add(armDown)
|
|
960
|
-
|
|
961
|
-
// Right arm: bent up toward chin
|
|
962
|
-
const upperArmR = new THREE.Mesh(
|
|
963
|
-
new THREE.CylinderGeometry(0.03, 0.025, 0.2, 6),
|
|
964
|
-
figureMat
|
|
965
|
-
)
|
|
966
|
-
upperArmR.position.set(0.14, 0.58, 0.02)
|
|
967
|
-
upperArmR.rotation.z = -0.3
|
|
968
|
-
upperArmR.rotation.x = -0.5
|
|
969
|
-
group.add(upperArmR)
|
|
970
|
-
|
|
971
|
-
const forearmR = new THREE.Mesh(
|
|
972
|
-
new THREE.CylinderGeometry(0.025, 0.022, 0.18, 6),
|
|
973
|
-
figureMat
|
|
974
|
-
)
|
|
975
|
-
forearmR.position.set(0.10, 0.74, -0.06)
|
|
976
|
-
forearmR.rotation.x = -1.2
|
|
977
|
-
group.add(forearmR)
|
|
978
|
-
|
|
979
|
-
// Position on the inner edge, facing inward (toward center/water)
|
|
980
|
-
const x = Math.cos(angle) * innerEdgeRadius
|
|
981
|
-
const z = Math.sin(angle) * innerEdgeRadius
|
|
982
|
-
group.position.set(x, 0, z)
|
|
983
|
-
group.rotation.y = angle + Math.PI + facingJitter // face center
|
|
984
|
-
|
|
985
|
-
scene.add(group)
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// ── Curved arrow helper ────────────────────────────────────────────────
|
|
990
|
-
function createCurvedArrow(fromPos, toPos, opacity) {
|
|
991
|
-
const mid = new THREE.Vector3().addVectors(fromPos, toPos).multiplyScalar(0.5)
|
|
992
|
-
mid.y += ARROW_CURVE_HEIGHT
|
|
993
|
-
|
|
994
|
-
// Push the midpoint outward from center slightly for visual separation
|
|
995
|
-
const centerToMid = new THREE.Vector3(mid.x, 0, mid.z)
|
|
996
|
-
const dist = centerToMid.length()
|
|
997
|
-
if (dist > 0.01) {
|
|
998
|
-
centerToMid.normalize().multiplyScalar(dist * 0.3)
|
|
999
|
-
mid.x += centerToMid.x
|
|
1000
|
-
mid.z += centerToMid.z
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const curve = new THREE.QuadraticBezierCurve3(fromPos.clone(), mid, toPos.clone())
|
|
1004
|
-
const points = curve.getPoints(40)
|
|
1005
|
-
const geometry = new THREE.BufferGeometry().setFromPoints(points)
|
|
1006
|
-
|
|
1007
|
-
const group = new THREE.Group()
|
|
1008
|
-
|
|
1009
|
-
// Core line
|
|
1010
|
-
const material = new THREE.LineBasicMaterial({
|
|
1011
|
-
color: ARROW_COLOR,
|
|
1012
|
-
transparent: true,
|
|
1013
|
-
opacity: opacity,
|
|
1014
|
-
})
|
|
1015
|
-
const line = new THREE.Line(geometry, material)
|
|
1016
|
-
group.add(line)
|
|
1017
|
-
|
|
1018
|
-
// Directional cones along the curve (at 25%, 50%, 75%)
|
|
1019
|
-
const coneMaterials = []
|
|
1020
|
-
const conePositions = [0.25, 0.5, 0.75]
|
|
1021
|
-
const up = new THREE.Vector3(0, 1, 0)
|
|
1022
|
-
|
|
1023
|
-
for (const t of conePositions) {
|
|
1024
|
-
const pos = curve.getPointAt(t)
|
|
1025
|
-
const tangent = curve.getTangentAt(t).normalize()
|
|
1026
|
-
|
|
1027
|
-
const coneGeo = new THREE.ConeGeometry(ARROW_HEAD_SIZE * 0.35, ARROW_HEAD_SIZE * 2.5, 8)
|
|
1028
|
-
const coneMat = new THREE.MeshBasicMaterial({
|
|
1029
|
-
color: ARROW_COLOR,
|
|
1030
|
-
transparent: true,
|
|
1031
|
-
opacity: opacity,
|
|
1032
|
-
})
|
|
1033
|
-
const cone = new THREE.Mesh(coneGeo, coneMat)
|
|
1034
|
-
cone.position.copy(pos)
|
|
1035
|
-
|
|
1036
|
-
const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent)
|
|
1037
|
-
cone.setRotationFromQuaternion(quat)
|
|
1038
|
-
|
|
1039
|
-
group.add(cone)
|
|
1040
|
-
coneMaterials.push(coneMat)
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return { group, lineMaterial: material, coneMaterials }
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// ── Layout agents in a ring ────────────────────────────────────────────
|
|
1047
|
-
function layoutAgents(names) {
|
|
1048
|
-
// Remove old nodes
|
|
1049
|
-
for (const [, node] of agentNodes) {
|
|
1050
|
-
scene.remove(node.group)
|
|
1051
|
-
}
|
|
1052
|
-
agentNodes.clear()
|
|
1053
|
-
|
|
1054
|
-
const count = names.length
|
|
1055
|
-
if (count === 0) return
|
|
1056
|
-
|
|
1057
|
-
for (let i = 0; i < count; i++) {
|
|
1058
|
-
const angle = (i / count) * Math.PI * 2 - Math.PI / 2
|
|
1059
|
-
const name = names[i]
|
|
1060
|
-
const node = createAgentNode(name, angle)
|
|
1061
|
-
scene.add(node.group)
|
|
1062
|
-
agentNodes.set(name, node)
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// ── Update arrows based on message state ───────────────────────────────
|
|
1067
|
-
function updateArrows() {
|
|
1068
|
-
// Remove old arrows
|
|
1069
|
-
for (const arrow of arrowObjects) {
|
|
1070
|
-
scene.remove(arrow.group)
|
|
1071
|
-
arrow.group.traverse(child => {
|
|
1072
|
-
if (child.geometry) child.geometry.dispose()
|
|
1073
|
-
if (child.material) child.material.dispose()
|
|
1074
|
-
})
|
|
1075
|
-
}
|
|
1076
|
-
arrowObjects = []
|
|
1077
|
-
|
|
1078
|
-
const now = Date.now()
|
|
1079
|
-
|
|
1080
|
-
for (const [key, edge] of messageEdges) {
|
|
1081
|
-
const [fromName, toName] = key.split('->')
|
|
1082
|
-
const fromNode = agentNodes.get(fromName)
|
|
1083
|
-
const toNode = agentNodes.get(toName)
|
|
1084
|
-
if (!fromNode || !toNode) continue
|
|
1085
|
-
|
|
1086
|
-
const ageSec = (now - edge.lastSent.getTime()) / 1000
|
|
1087
|
-
if (ageSec > ARROW_DECAY_SECONDS) continue
|
|
1088
|
-
|
|
1089
|
-
// Opacity: 1.0 at time 0, fading to 0.0 at ARROW_DECAY_SECONDS
|
|
1090
|
-
const opacity = Math.max(0, 1.0 - (ageSec / ARROW_DECAY_SECONDS))
|
|
1091
|
-
if (opacity <= 0.01) continue
|
|
1092
|
-
|
|
1093
|
-
const fromPos = fromNode.group.position.clone()
|
|
1094
|
-
fromPos.y += 0.3
|
|
1095
|
-
const toPos = toNode.group.position.clone()
|
|
1096
|
-
toPos.y += 0.3
|
|
1097
|
-
|
|
1098
|
-
const arrow = createCurvedArrow(fromPos, toPos, opacity)
|
|
1099
|
-
scene.add(arrow.group)
|
|
1100
|
-
arrowObjects.push({
|
|
1101
|
-
group: arrow.group,
|
|
1102
|
-
lineMaterial: arrow.lineMaterial,
|
|
1103
|
-
coneMaterials: arrow.coneMaterials,
|
|
1104
|
-
fromName,
|
|
1105
|
-
toName,
|
|
1106
|
-
lastSent: edge.lastSent,
|
|
1107
|
-
})
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// ── Process incoming watch state ───────────────────────────────────────
|
|
1112
|
-
function processWatchState(state) {
|
|
1113
|
-
watchState = state
|
|
1114
|
-
const nameSet = new Set()
|
|
1115
|
-
|
|
1116
|
-
// Collect all agent names and message edges (only for valid coworkers)
|
|
1117
|
-
for (const agentName in state) {
|
|
1118
|
-
// Only include if this is a known coworker
|
|
1119
|
-
if (!coworkersSet.has(agentName)) continue
|
|
1120
|
-
nameSet.add(agentName)
|
|
1121
|
-
const senders = state[agentName] || {}
|
|
1122
|
-
for (const senderName in senders) {
|
|
1123
|
-
// Only include sender if they're also a known coworker
|
|
1124
|
-
if (!coworkersSet.has(senderName)) continue
|
|
1125
|
-
nameSet.add(senderName)
|
|
1126
|
-
const edgeKey = senderName + '->' + agentName
|
|
1127
|
-
const lastSent = new Date(senders[senderName].lastSent)
|
|
1128
|
-
messageEdges.set(edgeKey, { lastSent })
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
const newNames = Array.from(nameSet).sort()
|
|
1133
|
-
|
|
1134
|
-
// Re-layout only if agents changed
|
|
1135
|
-
if (JSON.stringify(newNames) !== JSON.stringify(agentNames)) {
|
|
1136
|
-
agentNames = newNames
|
|
1137
|
-
layoutAgents(agentNames)
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Pulse nodes that just received messages
|
|
1141
|
-
const now = Date.now()
|
|
1142
|
-
for (const [, node] of agentNodes) {
|
|
1143
|
-
// Reset emissive
|
|
1144
|
-
node.sphereMat.emissive.setHex(0x2a4a7a)
|
|
1145
|
-
node.sphereMat.emissiveIntensity = 0.5
|
|
1146
|
-
}
|
|
1147
|
-
for (const [key, edge] of messageEdges) {
|
|
1148
|
-
const ageSec = (now - edge.lastSent.getTime()) / 1000
|
|
1149
|
-
if (ageSec < 5) {
|
|
1150
|
-
// Recently active - pulse the receiver
|
|
1151
|
-
const toName = key.split('->')[1]
|
|
1152
|
-
const node = agentNodes.get(toName)
|
|
1153
|
-
if (node) {
|
|
1154
|
-
node.sphereMat.emissive.setHex(0x6c8eff)
|
|
1155
|
-
node.sphereMat.emissiveIntensity = 1.5
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
updateArrows()
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// ── Fetch coworkers list on startup ──────────────────────────────────────
|
|
1164
|
-
async function fetchCoworkers() {
|
|
1165
|
-
try {
|
|
1166
|
-
const response = await fetch('/coworkers')
|
|
1167
|
-
const coworkers = await response.json()
|
|
1168
|
-
coworkersSet = new Set(coworkers.map(c => c.name))
|
|
1169
|
-
console.log('Loaded coworkers:', Array.from(coworkersSet))
|
|
1170
|
-
// Re-process watch state if we already have data
|
|
1171
|
-
if (Object.keys(watchState).length > 0) {
|
|
1172
|
-
processWatchState(watchState)
|
|
1173
|
-
}
|
|
1174
|
-
} catch (err) {
|
|
1175
|
-
console.error('Failed to fetch coworkers:', err)
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Fetch coworkers immediately
|
|
1180
|
-
fetchCoworkers()
|
|
1181
|
-
|
|
1182
|
-
// ── SSE connection ─────────────────────────────────────────────────────
|
|
1183
|
-
const es = new EventSource('/watch-stream')
|
|
1184
|
-
|
|
1185
|
-
es.addEventListener('open', () => {
|
|
1186
|
-
console.log('SSE connected')
|
|
1187
|
-
})
|
|
1188
|
-
|
|
1189
|
-
es.addEventListener('state', (event) => {
|
|
1190
|
-
try {
|
|
1191
|
-
const state = JSON.parse(event.data)
|
|
1192
|
-
processWatchState(state)
|
|
1193
|
-
} catch (err) {
|
|
1194
|
-
console.error('Failed to parse state:', err)
|
|
1195
|
-
}
|
|
1196
|
-
})
|
|
1197
|
-
|
|
1198
|
-
es.addEventListener('error', () => {
|
|
1199
|
-
console.warn('SSE disconnected, retrying...')
|
|
1200
|
-
})
|
|
1201
|
-
|
|
1202
|
-
// ── Animation loop ─────────────────────────────────────────────────────
|
|
1203
|
-
const clock = new THREE.Clock()
|
|
1204
|
-
let arrowUpdateTimer = 0
|
|
1205
|
-
|
|
1206
|
-
function animate() {
|
|
1207
|
-
requestAnimationFrame(animate)
|
|
1208
|
-
const delta = clock.getDelta()
|
|
1209
|
-
const elapsed = clock.getElapsedTime()
|
|
1210
|
-
|
|
1211
|
-
// Update shader uniforms
|
|
1212
|
-
starUniforms.uTime.value = elapsed
|
|
1213
|
-
waterUniforms.uTime.value = elapsed
|
|
1214
|
-
|
|
1215
|
-
// Slowly rotate icosahedron
|
|
1216
|
-
icoLines.rotation.y = elapsed * 0.15
|
|
1217
|
-
icoLines.rotation.x = Math.sin(elapsed * 0.1) * 0.2
|
|
1218
|
-
glowSphere.rotation.y = elapsed * 0.15
|
|
1219
|
-
|
|
1220
|
-
// Pulse the center glow subtly
|
|
1221
|
-
const icoPulse = 0.10 + Math.sin(elapsed * 0.8) * 0.04
|
|
1222
|
-
glowSphereMat.opacity = icoPulse
|
|
1223
|
-
icoLight.intensity = 1.0 + Math.sin(elapsed * 0.8) * 0.4
|
|
1224
|
-
|
|
1225
|
-
// Update arrow opacities periodically (every 0.5s to avoid thrash)
|
|
1226
|
-
arrowUpdateTimer += delta
|
|
1227
|
-
if (arrowUpdateTimer > 0.5) {
|
|
1228
|
-
arrowUpdateTimer = 0
|
|
1229
|
-
updateArrowOpacities()
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Pulse recently-active nodes
|
|
1233
|
-
const now = Date.now()
|
|
1234
|
-
for (const [key, edge] of messageEdges) {
|
|
1235
|
-
const ageSec = (now - edge.lastSent.getTime()) / 1000
|
|
1236
|
-
if (ageSec < 5) {
|
|
1237
|
-
const toName = key.split('->')[1]
|
|
1238
|
-
const node = agentNodes.get(toName)
|
|
1239
|
-
if (node) {
|
|
1240
|
-
const pulse = 0.8 + Math.sin(elapsed * 6) * 0.7
|
|
1241
|
-
node.sphereMat.emissiveIntensity = pulse
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
controls.update()
|
|
1247
|
-
|
|
1248
|
-
// Render: bg pass -> scene pass -> bloom -> output (all via composer)
|
|
1249
|
-
composer.render()
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function updateArrowOpacities() {
|
|
1253
|
-
const now = Date.now()
|
|
1254
|
-
for (const arrow of arrowObjects) {
|
|
1255
|
-
const edge = messageEdges.get(arrow.fromName + '->' + arrow.toName)
|
|
1256
|
-
if (!edge) continue
|
|
1257
|
-
|
|
1258
|
-
const ageSec = (now - edge.lastSent.getTime()) / 1000
|
|
1259
|
-
const opacity = Math.max(0, 1.0 - (ageSec / ARROW_DECAY_SECONDS))
|
|
1260
|
-
arrow.lineMaterial.opacity = opacity
|
|
1261
|
-
for (const mat of arrow.coneMaterials) mat.opacity = opacity
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// ── Window resize ──────────────────────────────────────────────────────
|
|
1266
|
-
window.addEventListener('resize', () => {
|
|
1267
|
-
camera.aspect = window.innerWidth / window.innerHeight
|
|
1268
|
-
camera.updateProjectionMatrix()
|
|
1269
|
-
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
1270
|
-
composer.setSize(window.innerWidth, window.innerHeight)
|
|
1271
|
-
bloomPass.resolution.set(window.innerWidth, window.innerHeight)
|
|
1272
|
-
starUniforms.uResolution.value.set(window.innerWidth, window.innerHeight)
|
|
1273
|
-
})
|
|
1274
|
-
|
|
1275
|
-
// ── Start ──────────────────────────────────────────────────────────────
|
|
1276
|
-
animate()
|
|
1277
|
-
</script>
|
|
1278
|
-
</body>
|
|
1279
|
-
</html>`;
|
|
1280
|
-
}
|