synabun 1.3.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.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,884 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — 3D Background
3
+ // Floor effects, deep-grid theme, sky dome,
4
+ // star fields, pillars, horizon glow
5
+ // ═══════════════════════════════════════════
6
+
7
+ import { gfx } from './gfx.js';
8
+
9
+ // ── Module state ──
10
+ let bgGrid = null;
11
+ let bgGridPillars = null;
12
+ let bgWorldParticles = null;
13
+ let bgGridFloor2 = null;
14
+ let bgSkyDome = null;
15
+ let _cloudPlanes = [];
16
+ let bgSkyParticles = null;
17
+ let _origControlsUpdate = null;
18
+
19
+ let floorMesh = null;
20
+ let floorParticles = null;
21
+
22
+ export const FLOOR_Y = -500;
23
+
24
+ // ═══════════════════════════════════════════
25
+ // FLOOR EFFECTS — Selectable floor styles
26
+ // ═══════════════════════════════════════════
27
+
28
+ function removeFloor(scene) {
29
+ if (floorMesh) {
30
+ scene.remove(floorMesh);
31
+ floorMesh.geometry.dispose();
32
+ floorMesh.material.dispose();
33
+ floorMesh = null;
34
+ }
35
+ if (floorParticles) {
36
+ scene.remove(floorParticles);
37
+ floorParticles.geometry.dispose();
38
+ floorParticles.material.dispose();
39
+ floorParticles = null;
40
+ }
41
+ }
42
+
43
+ // ── Floor: Grid ──
44
+ function addFloorGrid(scene) {
45
+ const gridSize = 8000;
46
+ const gridGeo = new THREE.PlaneGeometry(gridSize, gridSize, 1, 1);
47
+ gridGeo.rotateX(-Math.PI / 2);
48
+
49
+ const gridMat = new THREE.ShaderMaterial({
50
+ transparent: true,
51
+ depthWrite: false,
52
+ side: THREE.DoubleSide,
53
+ uniforms: {
54
+ uTime: { value: 0 },
55
+ uCamY: { value: 300 },
56
+ },
57
+ vertexShader: `
58
+ varying vec3 vWorldPos;
59
+ void main() {
60
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
61
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
62
+ }
63
+ `,
64
+ fragmentShader: `
65
+ varying vec3 vWorldPos;
66
+ uniform float uTime;
67
+ uniform float uCamY;
68
+
69
+ float softLine(float coord, float spacing, float sharpness) {
70
+ float d = abs(mod(coord + spacing * 0.5, spacing) - spacing * 0.5);
71
+ // Combine gaussian glow with screen-space AA to kill edge aliasing
72
+ float fw = fwidth(coord / spacing) * spacing * 1.5;
73
+ float aa = 1.0 - smoothstep(0.0, max(fw, 0.5), d);
74
+ float glow = exp(-d * d * sharpness);
75
+ return max(aa * 0.6, glow);
76
+ }
77
+
78
+ void main() {
79
+ float distFromCenter = length(vWorldPos.xz);
80
+ float fade = 1.0 - smoothstep(1200.0, 3800.0, distFromCenter);
81
+
82
+ // Sharpness decreases with distance — lines get softer/wider further out
83
+ float sharpness = mix(1.2, 0.15, smoothstep(0.0, 2500.0, distFromCenter));
84
+
85
+ float lx = softLine(vWorldPos.x, 200.0, sharpness);
86
+ float lz = softLine(vWorldPos.z, 200.0, sharpness);
87
+ float grid = max(lx, lz);
88
+
89
+ float dot = lx * lz;
90
+ float pulse = sin(uTime * 2.0 + vWorldPos.x * 0.04 + vWorldPos.z * 0.03) * 0.5 + 0.5;
91
+ float dotGlow = dot * (0.08 + pulse * 0.08);
92
+
93
+ vec3 col = vec3(1.0);
94
+ float alpha = (grid * 0.045 + dotGlow) * fade;
95
+
96
+ float camFade = smoothstep(0.0, 10.0, abs(uCamY - ${FLOOR_Y}.0));
97
+ alpha *= 0.4 + camFade * 0.6;
98
+
99
+ if (alpha < 0.002) discard;
100
+ gl_FragColor = vec4(col, alpha);
101
+ }
102
+ `,
103
+ });
104
+
105
+ floorMesh = new THREE.Mesh(gridGeo, gridMat);
106
+ floorMesh.position.y = FLOOR_Y;
107
+ floorMesh.renderOrder = -10;
108
+ scene.add(floorMesh);
109
+ }
110
+
111
+ // ── Floor: Dot Field with proximity glow ──
112
+ function addFloorDots(scene) {
113
+ const spacing = 40;
114
+ const extent = 3200;
115
+ const count = Math.pow(Math.floor(extent * 2 / spacing) + 1, 2);
116
+ const positions = new Float32Array(count * 3);
117
+ const phases = new Float32Array(count);
118
+ let idx = 0;
119
+
120
+ for (let x = -extent; x <= extent; x += spacing) {
121
+ for (let z = -extent; z <= extent; z += spacing) {
122
+ positions[idx * 3] = x + (Math.random() - 0.5) * 6;
123
+ positions[idx * 3 + 1] = FLOOR_Y;
124
+ positions[idx * 3 + 2] = z + (Math.random() - 0.5) * 6;
125
+ phases[idx] = Math.random() * Math.PI * 2;
126
+ idx++;
127
+ }
128
+ }
129
+
130
+ const geo = new THREE.BufferGeometry();
131
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(positions.slice(0, idx * 3), 3));
132
+ geo.setAttribute('aPhase', new THREE.Float32BufferAttribute(phases.slice(0, idx), 1));
133
+
134
+ const mat = new THREE.ShaderMaterial({
135
+ transparent: true,
136
+ depthWrite: false,
137
+ blending: THREE.AdditiveBlending,
138
+ uniforms: {
139
+ uTime: { value: 0 },
140
+ uCamPos: { value: new THREE.Vector3(0, 300, 0) },
141
+ },
142
+ vertexShader: `
143
+ attribute float aPhase;
144
+ varying float vAlpha;
145
+ varying float vProximity;
146
+ varying float vWave;
147
+ uniform float uTime;
148
+ uniform vec3 uCamPos;
149
+ void main() {
150
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
151
+ float distFromCenter = length(worldPos.xz);
152
+ float fade = 1.0 - smoothstep(1500.0, 3200.0, distFromCenter);
153
+
154
+ // Proximity glow — wide, strong falloff around camera xz
155
+ float camDist = length(worldPos.xz - uCamPos.xz);
156
+ vProximity = exp(-camDist * camDist * 0.0000008);
157
+
158
+ // Ripple wave expanding outward from camera
159
+ float wave = sin(camDist * 0.015 - uTime * 2.5) * 0.5 + 0.5;
160
+ wave *= exp(-camDist * 0.0008);
161
+ vWave = wave;
162
+
163
+ float twinkle = 0.5 + 0.5 * sin(uTime * 2.0 + aPhase * 6.28);
164
+ vAlpha = fade * (0.15 + twinkle * 0.1 + vProximity * 0.55 + wave * 0.2);
165
+
166
+ vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
167
+ float size = (3.0 + vProximity * 8.0 + wave * 3.0) * (300.0 / max(-mvPos.z, 1.0));
168
+ gl_PointSize = size;
169
+ gl_Position = projectionMatrix * mvPos;
170
+ }
171
+ `,
172
+ fragmentShader: `
173
+ varying float vAlpha;
174
+ varying float vProximity;
175
+ varying float vWave;
176
+ void main() {
177
+ float dist = length(gl_PointCoord - vec2(0.5));
178
+ if (dist > 0.5) discard;
179
+ float core = exp(-dist * dist * 16.0);
180
+ float glow = exp(-dist * dist * 4.0) * 0.5;
181
+ float alpha = (core + glow) * vAlpha;
182
+ vec3 baseCol = vec3(0.55, 0.62, 0.95);
183
+ vec3 hotCol = vec3(0.85, 0.9, 1.0);
184
+ vec3 col = mix(baseCol, hotCol, vProximity * 0.8 + vWave * 0.3);
185
+ gl_FragColor = vec4(col, alpha);
186
+ }
187
+ `,
188
+ });
189
+
190
+ floorParticles = new THREE.Points(geo, mat);
191
+ floorParticles.renderOrder = -10;
192
+ scene.add(floorParticles);
193
+ }
194
+
195
+ // ── Floor: Hex Grid ──
196
+ function addFloorHex(scene) {
197
+ const gridSize = 8000;
198
+ const gridGeo = new THREE.PlaneGeometry(gridSize, gridSize, 1, 1);
199
+ gridGeo.rotateX(-Math.PI / 2);
200
+
201
+ const hexMat = new THREE.ShaderMaterial({
202
+ transparent: true,
203
+ depthWrite: false,
204
+ side: THREE.DoubleSide,
205
+ uniforms: {
206
+ uTime: { value: 0 },
207
+ uCamY: { value: 300 },
208
+ },
209
+ vertexShader: `
210
+ varying vec3 vWorldPos;
211
+ void main() {
212
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
213
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
214
+ }
215
+ `,
216
+ fragmentShader: `
217
+ varying vec3 vWorldPos;
218
+ uniform float uTime;
219
+ uniform float uCamY;
220
+
221
+ // Hex distance field — returns distance to nearest hex edge
222
+ float hexDist(vec2 p) {
223
+ p = abs(p);
224
+ return max(dot(p, normalize(vec2(1.0, 1.732))), p.x);
225
+ }
226
+
227
+ vec4 hexCoords(vec2 uv) {
228
+ vec2 r = vec2(1.0, 1.732);
229
+ vec2 h = r * 0.5;
230
+ vec2 a = mod(uv, r) - h;
231
+ vec2 b = mod(uv - h, r) - h;
232
+ vec2 gv = length(a) < length(b) ? a : b;
233
+ vec2 id = uv - gv;
234
+ return vec4(gv, id);
235
+ }
236
+
237
+ void main() {
238
+ float distFromCenter = length(vWorldPos.xz);
239
+ float fade = 1.0 - smoothstep(1500.0, 3800.0, distFromCenter);
240
+
241
+ float scale = 220.0;
242
+ vec2 uv = vWorldPos.xz / scale;
243
+ vec4 hc = hexCoords(uv);
244
+
245
+ // Thin crisp edge — slightly thicker than 1px for clean reads at distance
246
+ float d = 0.5 - hexDist(hc.xy);
247
+ float fw = fwidth(d);
248
+ float edge = 1.0 - smoothstep(fw * 0.3, fw * 1.8, d);
249
+
250
+ vec3 col = vec3(0.7, 0.75, 0.95);
251
+ float alpha = edge * 0.07 * fade;
252
+
253
+ float camFade = smoothstep(0.0, 10.0, abs(uCamY - ${FLOOR_Y}.0));
254
+ alpha *= 0.4 + camFade * 0.6;
255
+
256
+ if (alpha < 0.002) discard;
257
+ gl_FragColor = vec4(col, alpha);
258
+ }
259
+ `,
260
+ });
261
+
262
+ floorMesh = new THREE.Mesh(gridGeo, hexMat);
263
+ floorMesh.position.y = FLOOR_Y;
264
+ floorMesh.renderOrder = -10;
265
+ scene.add(floorMesh);
266
+ }
267
+
268
+ // ── Floor: Concentric Ripples ──
269
+ function addFloorRipples(scene) {
270
+ const gridSize = 8000;
271
+ const gridGeo = new THREE.PlaneGeometry(gridSize, gridSize, 1, 1);
272
+ gridGeo.rotateX(-Math.PI / 2);
273
+
274
+ const rippleMat = new THREE.ShaderMaterial({
275
+ transparent: true,
276
+ depthWrite: false,
277
+ side: THREE.DoubleSide,
278
+ uniforms: {
279
+ uTime: { value: 0 },
280
+ uCamY: { value: 300 },
281
+ },
282
+ vertexShader: `
283
+ varying vec3 vWorldPos;
284
+ void main() {
285
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
286
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
287
+ }
288
+ `,
289
+ fragmentShader: `
290
+ varying vec3 vWorldPos;
291
+ uniform float uTime;
292
+ uniform float uCamY;
293
+
294
+ void main() {
295
+ float distFromCenter = length(vWorldPos.xz);
296
+ float fade = 1.0 - smoothstep(1200.0, 3800.0, distFromCenter);
297
+
298
+ // Concentric rings expanding outward
299
+ float ringSpacing = 80.0;
300
+ float scrollDist = distFromCenter - uTime * 15.0;
301
+ float ring = abs(mod(scrollDist, ringSpacing) - ringSpacing * 0.5);
302
+ float ringLine = exp(-ring * ring * 0.08);
303
+
304
+ // Static rings for structure
305
+ float staticRing = abs(mod(distFromCenter, ringSpacing * 2.5) - ringSpacing * 1.25);
306
+ float staticLine = exp(-staticRing * staticRing * 0.04);
307
+
308
+ // Radial spokes — very subtle
309
+ float angle = atan(vWorldPos.z, vWorldPos.x);
310
+ float spoke = exp(-pow(mod(angle + 3.1416, 0.3927) - 0.19635, 2.0) * 600.0);
311
+ float spokeFade = smoothstep(0.0, 400.0, distFromCenter) * 0.3;
312
+
313
+ vec3 col = vec3(1.0);
314
+ float alpha = (ringLine * 0.06 + staticLine * 0.03 + spoke * spokeFade * 0.04) * fade;
315
+
316
+ float camFade = smoothstep(0.0, 10.0, abs(uCamY - ${FLOOR_Y}.0));
317
+ alpha *= 0.4 + camFade * 0.6;
318
+
319
+ if (alpha < 0.002) discard;
320
+ gl_FragColor = vec4(col, alpha);
321
+ }
322
+ `,
323
+ });
324
+
325
+ floorMesh = new THREE.Mesh(gridGeo, rippleMat);
326
+ floorMesh.position.y = FLOOR_Y;
327
+ floorMesh.renderOrder = -10;
328
+ scene.add(floorMesh);
329
+ }
330
+
331
+ // ── Floor: Ground Fog Particles ──
332
+ function addFloorFog(scene) {
333
+ const count = 3000;
334
+ const positions = new Float32Array(count * 3);
335
+ const sizes = new Float32Array(count);
336
+ const alphas = new Float32Array(count);
337
+ const phases = new Float32Array(count);
338
+
339
+ for (let i = 0; i < count; i++) {
340
+ const angle = Math.random() * Math.PI * 2;
341
+ const radius = Math.random() * 3000;
342
+ positions[i * 3] = Math.cos(angle) * radius;
343
+ positions[i * 3 + 1] = FLOOR_Y + Math.random() * 60;
344
+ positions[i * 3 + 2] = Math.sin(angle) * radius;
345
+ sizes[i] = 8 + Math.random() * 25;
346
+ alphas[i] = 0.01 + Math.random() * 0.04;
347
+ phases[i] = Math.random() * Math.PI * 2;
348
+ }
349
+
350
+ const geo = new THREE.BufferGeometry();
351
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
352
+ geo.setAttribute('aSize', new THREE.Float32BufferAttribute(sizes, 1));
353
+ geo.setAttribute('aAlpha', new THREE.Float32BufferAttribute(alphas, 1));
354
+ geo.setAttribute('aPhase', new THREE.Float32BufferAttribute(phases, 1));
355
+
356
+ const mat = new THREE.ShaderMaterial({
357
+ transparent: true,
358
+ depthWrite: false,
359
+ blending: THREE.AdditiveBlending,
360
+ uniforms: { uTime: { value: 0 } },
361
+ vertexShader: `
362
+ attribute float aSize;
363
+ attribute float aAlpha;
364
+ attribute float aPhase;
365
+ varying float vAlpha;
366
+ uniform float uTime;
367
+ void main() {
368
+ vec3 pos = position;
369
+ float t = uTime * 0.15 + aPhase;
370
+ pos.x += sin(t * 0.7 + aPhase * 3.0) * 8.0;
371
+ pos.z += cos(t * 0.5 + aPhase * 2.0) * 8.0;
372
+ pos.y += sin(t) * 3.0;
373
+
374
+ vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
375
+ float camDist = length(mvPos.xyz);
376
+ float distFade = 1.0 - smoothstep(500.0, 3000.0, camDist);
377
+ float breathe = 0.6 + 0.4 * sin(uTime * 0.4 + aPhase * 6.28);
378
+
379
+ vAlpha = aAlpha * distFade * breathe;
380
+ gl_PointSize = aSize * (400.0 / max(-mvPos.z, 1.0));
381
+ gl_Position = projectionMatrix * mvPos;
382
+ }
383
+ `,
384
+ fragmentShader: `
385
+ varying float vAlpha;
386
+ void main() {
387
+ float dist = length(gl_PointCoord - vec2(0.5));
388
+ if (dist > 0.5) discard;
389
+ float soft = exp(-dist * dist * 8.0);
390
+ gl_FragColor = vec4(0.75, 0.78, 0.88, soft * vAlpha);
391
+ }
392
+ `,
393
+ });
394
+
395
+ floorParticles = new THREE.Points(geo, mat);
396
+ floorParticles.renderOrder = -10;
397
+ scene.add(floorParticles);
398
+ }
399
+
400
+ // ── Floor dispatcher ──
401
+ export function applyFloorStyle(style, graph) {
402
+ if (!graph) return;
403
+ const scene = graph.scene();
404
+ removeFloor(scene);
405
+ switch (style) {
406
+ case 'grid': addFloorGrid(scene); break;
407
+ case 'dots': addFloorDots(scene); break;
408
+ case 'hex': addFloorHex(scene); break;
409
+ case 'ripples': addFloorRipples(scene); break;
410
+ case 'fog': addFloorFog(scene); break;
411
+ case 'none': break;
412
+ }
413
+ }
414
+
415
+ // ═══════════════════════════════════════════
416
+ // BACKGROUND — Deep Grid theme
417
+ // Perspective grid floor + world-space particles + vertical pillars
418
+ // Gives true spatial depth and grounding
419
+ // ═══════════════════════════════════════════
420
+ function addBackgroundDeepGrid(graph) {
421
+ const scene = graph.scene();
422
+
423
+ // Floor is now handled by applyFloorStyle() — separate system
424
+
425
+ // ── Layer B: Vertical Accent Pillars ──
426
+ // Muted grey-blue, very subtle
427
+ const pillarCount = 90;
428
+ const pillarPositions = new Float32Array(pillarCount * 6);
429
+ const pillarAlphas = new Float32Array(pillarCount * 2);
430
+ const pillarPhases = new Float32Array(pillarCount * 2);
431
+ const pillarSpacing = 70;
432
+ const pillarSpread = 2000;
433
+
434
+ for (let i = 0; i < pillarCount; i++) {
435
+ const gx = (Math.floor(Math.random() * (pillarSpread / pillarSpacing) * 2) - pillarSpread / pillarSpacing) * pillarSpacing;
436
+ const gz = (Math.floor(Math.random() * (pillarSpread / pillarSpacing) * 2) - pillarSpread / pillarSpacing) * pillarSpacing;
437
+ const height = 30 + Math.random() * 120;
438
+
439
+ pillarPositions[i * 6] = gx;
440
+ pillarPositions[i * 6 + 1] = FLOOR_Y;
441
+ pillarPositions[i * 6 + 2] = gz;
442
+ pillarPositions[i * 6 + 3] = gx;
443
+ pillarPositions[i * 6 + 4] = FLOOR_Y + height;
444
+ pillarPositions[i * 6 + 5] = gz;
445
+
446
+ const a = 0.04 + Math.random() * 0.08;
447
+ const ph = Math.random() * Math.PI * 2;
448
+ pillarAlphas[i * 2] = a * 0.2;
449
+ pillarAlphas[i * 2 + 1] = a;
450
+ pillarPhases[i * 2] = ph;
451
+ pillarPhases[i * 2 + 1] = ph;
452
+ }
453
+
454
+ const pillarGeo = new THREE.BufferGeometry();
455
+ pillarGeo.setAttribute('position', new THREE.Float32BufferAttribute(pillarPositions, 3));
456
+ pillarGeo.setAttribute('aAlpha', new THREE.Float32BufferAttribute(pillarAlphas, 1));
457
+ pillarGeo.setAttribute('aPhase', new THREE.Float32BufferAttribute(pillarPhases, 1));
458
+
459
+ const pillarMat = new THREE.ShaderMaterial({
460
+ transparent: true,
461
+ depthWrite: false,
462
+ blending: THREE.AdditiveBlending,
463
+ uniforms: { uTime: { value: 0 } },
464
+ vertexShader: `
465
+ attribute float aAlpha;
466
+ attribute float aPhase;
467
+ varying float vAlpha;
468
+ uniform float uTime;
469
+ void main() {
470
+ vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
471
+ float camDist = length(mvPos.xyz);
472
+ float distFade = 1.0 - smoothstep(300.0, 1500.0, camDist);
473
+ vAlpha = aAlpha * (0.6 + 0.4 * sin(uTime * 0.3 + aPhase * 6.28)) * distFade;
474
+ gl_Position = projectionMatrix * mvPos;
475
+ }
476
+ `,
477
+ fragmentShader: `
478
+ varying float vAlpha;
479
+ void main() {
480
+ if (vAlpha < 0.003) discard;
481
+ gl_FragColor = vec4(0.50, 0.56, 0.75, vAlpha);
482
+ }
483
+ `,
484
+ });
485
+
486
+ bgGridPillars = new THREE.LineSegments(pillarGeo, pillarMat);
487
+ bgGridPillars.renderOrder = -9;
488
+ scene.add(bgGridPillars);
489
+
490
+ // ── Layer C: World-Space Floating Particles ──
491
+ // Muted blue-grey dots — NOT camera-attached (real parallax)
492
+ const pCount = Math.max(200, gfx.bgParticleCount || 2000);
493
+ const pPositions = new Float32Array(pCount * 3);
494
+ const pSizes = new Float32Array(pCount);
495
+ const pAlphas = new Float32Array(pCount);
496
+ const pColors = new Float32Array(pCount * 3);
497
+ const pPhases = new Float32Array(pCount);
498
+
499
+ for (let i = 0; i < pCount; i++) {
500
+ const angle = Math.random() * Math.PI * 2;
501
+ const radius = 50 + Math.random() * 2800;
502
+ const height = FLOOR_Y + 10 + Math.random() * 600;
503
+
504
+ pPositions[i * 3] = Math.cos(angle) * radius;
505
+ pPositions[i * 3 + 1] = height;
506
+ pPositions[i * 3 + 2] = Math.sin(angle) * radius;
507
+
508
+ const distNorm = radius / 2800;
509
+ pSizes[i] = (1.0 - distNorm * 0.5) * (1.2 + Math.random() * 2.0);
510
+ pAlphas[i] = (1.0 - distNorm * 0.6) * (0.03 + Math.random() * 0.09);
511
+
512
+ // Muted blue-grey palette (matching website rgba(130,145,195))
513
+ const v = Math.random();
514
+ // Base: desaturated blue-grey with slight variation
515
+ pColors[i * 3] = 0.45 + (Math.random() - 0.5) * 0.12;
516
+ pColors[i * 3 + 1] = 0.50 + (Math.random() - 0.5) * 0.12;
517
+ pColors[i * 3 + 2] = 0.68 + (Math.random() - 0.5) * 0.12;
518
+ // 15% chance of slightly warmer accent
519
+ if (v > 0.85) {
520
+ pColors[i * 3] = 0.58 + Math.random() * 0.08;
521
+ pColors[i * 3 + 1] = 0.55 + Math.random() * 0.06;
522
+ pColors[i * 3 + 2] = 0.72 + Math.random() * 0.08;
523
+ }
524
+
525
+ pPhases[i] = Math.random() * Math.PI * 2;
526
+ }
527
+
528
+ const pGeo = new THREE.BufferGeometry();
529
+ pGeo.setAttribute('position', new THREE.Float32BufferAttribute(pPositions, 3));
530
+ pGeo.setAttribute('aSize', new THREE.Float32BufferAttribute(pSizes, 1));
531
+ pGeo.setAttribute('aAlpha', new THREE.Float32BufferAttribute(pAlphas, 1));
532
+ pGeo.setAttribute('aColor', new THREE.Float32BufferAttribute(pColors, 3));
533
+ pGeo.setAttribute('aPhase', new THREE.Float32BufferAttribute(pPhases, 1));
534
+
535
+ const pMat = new THREE.ShaderMaterial({
536
+ transparent: true,
537
+ depthWrite: false,
538
+ blending: THREE.AdditiveBlending,
539
+ uniforms: { uTime: { value: 0 }, uIntensity: { value: gfx.bgIntensity } },
540
+ vertexShader: `
541
+ attribute float aSize;
542
+ attribute float aAlpha;
543
+ attribute vec3 aColor;
544
+ attribute float aPhase;
545
+ varying float vAlpha;
546
+ varying vec3 vColor;
547
+ uniform float uTime;
548
+ uniform float uIntensity;
549
+ void main() {
550
+ vColor = aColor;
551
+ vec3 pos = position;
552
+ float t = uTime * 0.08 + aPhase;
553
+ pos.y += sin(t) * 6.0;
554
+ pos.x += cos(t * 0.7 + aPhase * 3.0) * 3.0;
555
+ pos.z += sin(t * 0.5 + aPhase * 2.0) * 3.0;
556
+
557
+ vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
558
+ float camDist = length(mvPos.xyz);
559
+ float distFade = 1.0 - smoothstep(200.0, 2800.0, camDist);
560
+
561
+ vAlpha = aAlpha * (0.5 + 0.5 * sin(uTime * 0.4 + aPhase * 6.28)) * distFade * uIntensity;
562
+ gl_PointSize = aSize * (400.0 / max(-mvPos.z, 1.0));
563
+ gl_Position = projectionMatrix * mvPos;
564
+ }
565
+ `,
566
+ fragmentShader: `
567
+ varying float vAlpha;
568
+ varying vec3 vColor;
569
+ void main() {
570
+ float dist = length(gl_PointCoord - vec2(0.5));
571
+ if (dist > 0.5) discard;
572
+ float core = smoothstep(0.5, 0.0, dist);
573
+ float glow = smoothstep(0.5, 0.15, dist);
574
+ float alpha = (core * 0.6 + glow * 0.4) * vAlpha;
575
+ gl_FragColor = vec4(vColor * (0.7 + core * 0.3), alpha);
576
+ }
577
+ `,
578
+ });
579
+
580
+ bgWorldParticles = new THREE.Points(pGeo, pMat);
581
+ bgWorldParticles.renderOrder = -8;
582
+ scene.add(bgWorldParticles);
583
+
584
+ // ── Layer D: Horizon Glow ──
585
+ // Cylindrical band at the grid edge creating a soft horizon line
586
+ const horizGeo = new THREE.CylinderGeometry(3800, 3800, 800, 64, 1, true);
587
+ const horizMat = new THREE.ShaderMaterial({
588
+ transparent: true,
589
+ depthWrite: false,
590
+ side: THREE.DoubleSide,
591
+ blending: THREE.AdditiveBlending,
592
+ uniforms: { uTime: { value: 0 } },
593
+ vertexShader: `
594
+ varying vec2 vUv;
595
+ varying vec3 vWorldPos;
596
+ void main() {
597
+ vUv = uv;
598
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
599
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
600
+ }
601
+ `,
602
+ fragmentShader: `
603
+ varying vec2 vUv;
604
+ varying vec3 vWorldPos;
605
+ uniform float uTime;
606
+ void main() {
607
+ // Wide, soft gaussian-like glow centered at lower-mid height
608
+ float center = 0.3;
609
+ float spread = 0.4; // very wide spread
610
+ float yFade = exp(-pow((vUv.y - center) / spread, 2.0));
611
+
612
+ // Muted blue-grey glow
613
+ vec3 col = vec3(0.22, 0.28, 0.42);
614
+
615
+ // Very subtle shimmer
616
+ float shimmer = 0.9 + 0.1 * sin(vUv.x * 20.0 + uTime * 0.15);
617
+
618
+ float alpha = yFade * 0.025 * shimmer;
619
+ gl_FragColor = vec4(col, alpha);
620
+ }
621
+ `,
622
+ });
623
+
624
+ bgGridFloor2 = new THREE.Mesh(horizGeo, horizMat);
625
+ bgGridFloor2.position.y = FLOOR_Y + 20;
626
+ bgGridFloor2.renderOrder = -7;
627
+ scene.add(bgGridFloor2);
628
+
629
+ // ── Layer E: Sky gradient sphere — atmospheric perspective ──
630
+ // Inverted sphere behind everything: zenith=black, horizon=faint warm-purple
631
+ const skyGradGeo = new THREE.SphereGeometry(30000, 24, 16);
632
+ const skyGradMat = new THREE.ShaderMaterial({
633
+ side: THREE.BackSide,
634
+ transparent: true,
635
+ depthWrite: false,
636
+ vertexShader: `
637
+ varying vec3 vWorldPos;
638
+ void main() {
639
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
640
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
641
+ }
642
+ `,
643
+ fragmentShader: `
644
+ varying vec3 vWorldPos;
645
+ void main() {
646
+ // Normalized height: 0 at bottom, 1 at top
647
+ float h = normalize(vWorldPos).y;
648
+ // Zenith (h=1): pure black
649
+ vec3 zenith = vec3(0.003, 0.003, 0.004);
650
+ // Mid-sky (h~0.4): barely-there dark grey
651
+ vec3 midSky = vec3(0.01, 0.01, 0.012);
652
+ // Horizon (h=0): subtle dark grey
653
+ vec3 horizon = vec3(0.022, 0.02, 0.022);
654
+ // Below horizon: black
655
+ vec3 below = vec3(0.002, 0.002, 0.002);
656
+
657
+ vec3 col;
658
+ if (h > 0.4) {
659
+ col = mix(midSky, zenith, smoothstep(0.4, 1.0, h));
660
+ } else if (h > 0.0) {
661
+ col = mix(horizon, midSky, smoothstep(0.0, 0.4, h));
662
+ } else {
663
+ col = mix(below, horizon, smoothstep(-0.3, 0.0, h));
664
+ }
665
+ gl_FragColor = vec4(col, 0.95);
666
+ }
667
+ `,
668
+ });
669
+ bgSkyDome = new THREE.Mesh(skyGradGeo, skyGradMat);
670
+ bgSkyDome.renderOrder = -10;
671
+ scene.add(bgSkyDome);
672
+ _cloudPlanes = [];
673
+
674
+ // ── Layer F: World-space parallax star field ──
675
+ // 3 shells at increasing radii with color temperature variation
676
+ // NOT camera-attached — real parallax as camera orbits
677
+ const shells = [
678
+ { start: 0, count: 600, rMin: 3500, rMax: 5000, sMin: 3.0, sMax: 6.0, aMin: 0.20, aMax: 0.50, col: [0.78, 0.74, 0.68] }, // near: warm white-gold
679
+ { start: 600, count: 1000, rMin: 6000, rMax: 10000, sMin: 4.0, sMax: 8.0, aMin: 0.15, aMax: 0.35, col: [0.62, 0.66, 0.80] }, // mid: neutral blue-white
680
+ { start: 1600, count: 1400, rMin: 14000, rMax: 25000, sMin: 6.0, sMax: 14.0, aMin: 0.10, aMax: 0.25, col: [0.48, 0.50, 0.75] }, // far: cool blue-violet
681
+ ];
682
+ const spCount = 3000;
683
+ const spPositions = new Float32Array(spCount * 3);
684
+ const spSizes = new Float32Array(spCount);
685
+ const spAlphas = new Float32Array(spCount);
686
+ const spColors = new Float32Array(spCount * 3);
687
+ const spPhases = new Float32Array(spCount);
688
+
689
+ for (const shell of shells) {
690
+ for (let i = 0; i < shell.count; i++) {
691
+ const idx = shell.start + i;
692
+ // Fibonacci sphere, upper hemisphere bias
693
+ const y = 1 - (i / shell.count) * 1.5; // range 1 to -0.5
694
+ const xyRadius = Math.sqrt(Math.max(0, 1 - y * y));
695
+ const theta = Math.PI * (1 + Math.sqrt(5)) * i;
696
+ const r = shell.rMin + Math.random() * (shell.rMax - shell.rMin);
697
+
698
+ spPositions[idx * 3] = Math.cos(theta) * xyRadius * r;
699
+ spPositions[idx * 3 + 1] = y * r;
700
+ spPositions[idx * 3 + 2] = Math.sin(theta) * xyRadius * r;
701
+
702
+ spSizes[idx] = shell.sMin + Math.random() * (shell.sMax - shell.sMin);
703
+ spAlphas[idx] = shell.aMin + Math.random() * (shell.aMax - shell.aMin);
704
+ // Color with slight per-star variation
705
+ spColors[idx * 3] = shell.col[0] + (Math.random() - 0.5) * 0.08;
706
+ spColors[idx * 3 + 1] = shell.col[1] + (Math.random() - 0.5) * 0.08;
707
+ spColors[idx * 3 + 2] = shell.col[2] + (Math.random() - 0.5) * 0.08;
708
+ spPhases[idx] = Math.random() * Math.PI * 2;
709
+ }
710
+ }
711
+
712
+ const spGeo = new THREE.BufferGeometry();
713
+ spGeo.setAttribute('position', new THREE.Float32BufferAttribute(spPositions, 3));
714
+ spGeo.setAttribute('aSize', new THREE.Float32BufferAttribute(spSizes, 1));
715
+ spGeo.setAttribute('aAlpha', new THREE.Float32BufferAttribute(spAlphas, 1));
716
+ spGeo.setAttribute('aColor', new THREE.Float32BufferAttribute(spColors, 3));
717
+ spGeo.setAttribute('aPhase', new THREE.Float32BufferAttribute(spPhases, 1));
718
+
719
+ const spMat = new THREE.ShaderMaterial({
720
+ transparent: true,
721
+ depthWrite: false,
722
+ blending: THREE.AdditiveBlending,
723
+ uniforms: { uTime: { value: 0 } },
724
+ vertexShader: `
725
+ attribute float aSize;
726
+ attribute float aAlpha;
727
+ attribute vec3 aColor;
728
+ attribute float aPhase;
729
+ varying float vAlpha;
730
+ varying vec3 vColor;
731
+ uniform float uTime;
732
+ void main() {
733
+ vColor = aColor;
734
+ // Differential Y-axis rotation: near stars drift faster than far stars
735
+ float r = length(position);
736
+ float rotSpeed = mix(0.003, 0.0008, smoothstep(3500.0, 25000.0, r));
737
+ float angle = uTime * rotSpeed;
738
+ float cs = cos(angle);
739
+ float sn = sin(angle);
740
+ vec3 rotPos = vec3(
741
+ position.x * cs - position.z * sn,
742
+ position.y,
743
+ position.x * sn + position.z * cs
744
+ );
745
+ vec4 mvPos = modelViewMatrix * vec4(rotPos, 1.0);
746
+ vAlpha = aAlpha * (0.3 + 0.7 * sin(uTime * 0.6 + aPhase * 6.28));
747
+ // Perspective size with floor so distant stars stay visible
748
+ gl_PointSize = max(1.0, aSize * (3000.0 / max(-mvPos.z, 1.0)));
749
+ gl_Position = projectionMatrix * mvPos;
750
+ }
751
+ `,
752
+ fragmentShader: `
753
+ varying float vAlpha;
754
+ varying vec3 vColor;
755
+ void main() {
756
+ float dist = length(gl_PointCoord - vec2(0.5));
757
+ if (dist > 0.5) discard;
758
+ float core = exp(-dist * dist * 80.0);
759
+ float glow = exp(-dist * dist * 12.0) * 0.2;
760
+ gl_FragColor = vec4(vColor, (core + glow) * vAlpha);
761
+ }
762
+ `,
763
+ });
764
+
765
+ bgSkyParticles = new THREE.Points(spGeo, spMat);
766
+ bgSkyParticles.renderOrder = -6;
767
+ scene.add(bgSkyParticles);
768
+
769
+ // ── Scene fog for atmospheric depth ──
770
+ scene.fog = new THREE.FogExp2(0x000000, 0.00012);
771
+
772
+ // ── Camera constraints & orbit polish ──
773
+ const controls = graph.controls();
774
+ if (controls) {
775
+ // Zoom limit: keep camera inside the sky dome
776
+ controls.maxDistance = 2800;
777
+ controls.minDistance = 30;
778
+
779
+ // Enable damping for smooth, polished orbit feel
780
+ controls.enableDamping = true;
781
+ controls.dampingFactor = 0.08;
782
+
783
+ // Smoother zoom
784
+ controls.zoomSpeed = 0.8;
785
+
786
+ // Smoother rotation
787
+ controls.rotateSpeed = 0.6;
788
+
789
+ // Smoother pan
790
+ controls.panSpeed = 0.6;
791
+
792
+ // Don't auto-rotate
793
+ controls.autoRotate = false;
794
+
795
+ // Floor + ceiling constraint via update override
796
+ _origControlsUpdate = controls.update.bind(controls);
797
+ controls.update = function() {
798
+ _origControlsUpdate();
799
+ const cam = graph.camera();
800
+ if (cam.position.y < FLOOR_Y + 15) cam.position.y = FLOOR_Y + 15;
801
+ if (cam.position.y > 2500) cam.position.y = 2500;
802
+ };
803
+ }
804
+ }
805
+
806
+ // ═══════════════════════════════════════════
807
+ // BACKGROUND — Cleanup
808
+ // ═══════════════════════════════════════════
809
+ function removeBackground(graph) {
810
+ const scene = graph.scene();
811
+ // Remove deep-grid objects (floor is managed separately by removeFloor)
812
+ if (bgGridPillars) { scene.remove(bgGridPillars); bgGridPillars.geometry.dispose(); bgGridPillars.material.dispose(); bgGridPillars = null; }
813
+ if (bgWorldParticles) { scene.remove(bgWorldParticles); bgWorldParticles.geometry.dispose(); bgWorldParticles.material.dispose(); bgWorldParticles = null; }
814
+ if (bgGridFloor2) { scene.remove(bgGridFloor2); bgGridFloor2.geometry.dispose(); bgGridFloor2.material.dispose(); bgGridFloor2 = null; }
815
+ if (bgSkyDome) {
816
+ scene.remove(bgSkyDome);
817
+ if (bgSkyDome.isGroup) {
818
+ bgSkyDome.children.forEach(c => { c.geometry.dispose(); c.material.dispose(); });
819
+ } else {
820
+ bgSkyDome.geometry.dispose(); bgSkyDome.material.dispose();
821
+ }
822
+ bgSkyDome = null; _cloudPlanes = [];
823
+ }
824
+ if (bgSkyParticles) { scene.remove(bgSkyParticles); bgSkyParticles.geometry.dispose(); bgSkyParticles.material.dispose(); bgSkyParticles = null; }
825
+ // Restore original controls if overridden
826
+ const controls = graph.controls();
827
+ if (controls && _origControlsUpdate) {
828
+ controls.update = _origControlsUpdate;
829
+ _origControlsUpdate = null;
830
+ controls.maxDistance = Infinity;
831
+ controls.minDistance = 0;
832
+ }
833
+ // Clear scene fog
834
+ scene.fog = null;
835
+ }
836
+
837
+ // ═══════════════════════════════════════════
838
+ // BACKGROUND — Dispatcher
839
+ // Calls the right background builder based on theme
840
+ // ═══════════════════════════════════════════
841
+ export function applyBgTheme(graph, bloomPass) {
842
+ if (!graph) return;
843
+ removeBackground(graph);
844
+ addBackgroundDeepGrid(graph);
845
+ graph.backgroundColor('#000000');
846
+ if (bloomPass) {
847
+ bloomPass.strength = 0.35;
848
+ bloomPass.threshold = 1.0;
849
+ bloomPass.radius = 0.3;
850
+ }
851
+ applyFloorStyle(gfx.floorStyle || 'grid', graph);
852
+ }
853
+
854
+ // ═══════════════════════════════════════════
855
+ // ANIMATION — Per-frame background updates
856
+ // Called from the main animate() loop
857
+ // ═══════════════════════════════════════════
858
+ export function animateBackground(time, graph) {
859
+ // ── Floor effect (independent of bg theme) ──
860
+ if (floorMesh && floorMesh.material.uniforms) {
861
+ if (floorMesh.material.uniforms.uTime) floorMesh.material.uniforms.uTime.value = time;
862
+ if (floorMesh.material.uniforms.uCamY && graph) floorMesh.material.uniforms.uCamY.value = graph.camera().position.y;
863
+ }
864
+ if (floorParticles && floorParticles.material.uniforms) {
865
+ if (floorParticles.material.uniforms.uTime) floorParticles.material.uniforms.uTime.value = time;
866
+ if (floorParticles.material.uniforms.uCamPos && graph) floorParticles.material.uniforms.uCamPos.value.copy(graph.camera().position);
867
+ }
868
+
869
+ // ── Background — Deep Grid theme (world-space, real depth) ──
870
+ if (bgGridPillars) {
871
+ bgGridPillars.material.uniforms.uTime.value = time;
872
+ }
873
+ if (bgWorldParticles) {
874
+ bgWorldParticles.material.uniforms.uTime.value = time;
875
+ bgWorldParticles.material.uniforms.uIntensity.value = gfx.bgIntensity;
876
+ }
877
+ if (bgGridFloor2) {
878
+ bgGridFloor2.material.uniforms.uTime.value = time;
879
+ }
880
+ // Layer F: world-space stars (no camera-attach — real parallax)
881
+ if (bgSkyParticles) {
882
+ bgSkyParticles.material.uniforms.uTime.value = time;
883
+ }
884
+ }