@mindexec/cli 0.2.476 → 0.2.478

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 (145) hide show
  1. package/README.md +323 -323
  2. package/codex-model-catalog.js +202 -202
  3. package/codex-runtime.js +1608 -1608
  4. package/desktop-workspace-state.cjs +120 -120
  5. package/electron/main.cjs +517 -517
  6. package/electron/preload.cjs +17 -17
  7. package/electron/recurring-update-owner-smoke.mjs +55 -55
  8. package/electron/recurring-update-owner.cjs +97 -97
  9. package/electron/source-smoke.mjs +108 -108
  10. package/electron/update-feed-resolver.cjs +79 -79
  11. package/electron/update-feed.json +10 -10
  12. package/electron/update-manager-smoke.mjs +146 -146
  13. package/electron/update-manager.cjs +297 -297
  14. package/electron/windows-package-smoke.mjs +156 -156
  15. package/launch-bridge.cjs +236 -236
  16. package/lzo1x.js +110 -110
  17. package/mode4-atlas-worker.js +440 -440
  18. package/mode4-atlas.js +130 -130
  19. package/package.json +1 -1
  20. package/port-guard.cjs +285 -285
  21. package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
  22. package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
  23. package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
  24. package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
  25. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  26. package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
  27. package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
  28. package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
  29. package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
  30. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  31. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  32. package/remote-hub.js +3698 -3698
  33. package/scripts/auth-session-smoke.mjs +262 -262
  34. package/scripts/board-render-ownership-smoke.mjs +85 -85
  35. package/scripts/codex-model-catalog-smoke.mjs +150 -150
  36. package/scripts/codex-runtime-home-smoke.mjs +58 -58
  37. package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
  38. package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
  39. package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
  40. package/scripts/desktop-update-publisher-smoke.mjs +89 -89
  41. package/scripts/desktop-update-worker-smoke.mjs +72 -72
  42. package/scripts/desktop-workspace-state-smoke.mjs +81 -81
  43. package/scripts/drive-installed-update-smoke.mjs +67 -67
  44. package/scripts/npm-fresh-install-smoke.mjs +216 -216
  45. package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
  46. package/scripts/remote-agent-managed-smoke.mjs +318 -318
  47. package/scripts/remote-agent-package-smoke.mjs +64 -64
  48. package/scripts/remote-agent-ws-smoke.mjs +269 -269
  49. package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
  50. package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
  51. package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
  52. package/scripts/remote-frame-ws-smoke.mjs +234 -234
  53. package/scripts/remote-http-smoke.mjs +592 -592
  54. package/scripts/remote-hub-identity-smoke.mjs +146 -146
  55. package/scripts/remote-hub-scale-smoke.mjs +124 -124
  56. package/scripts/remote-hub-smoke.mjs +699 -699
  57. package/scripts/remote-input-ws-smoke.mjs +263 -263
  58. package/scripts/remote-registry-follower-smoke.mjs +815 -815
  59. package/scripts/setup-tree-sitter-grammars.mjs +59 -59
  60. package/server.js +7690 -7690
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  64. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  65. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
  66. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  67. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  68. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  69. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  70. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  71. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  72. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  73. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  74. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  75. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10517
  76. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  77. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  78. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  79. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  80. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  81. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  82. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  100. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  101. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  102. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  103. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  104. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  105. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  106. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  107. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  108. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  109. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  110. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  111. package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
  112. package/wwwroot/_framework/{MindExecution.Core.cx2pre7qz2.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
  113. package/wwwroot/_framework/{MindExecution.Kernel.engfih12kr.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
  114. package/wwwroot/_framework/{MindExecution.Plugins.Admin.xfunemn9zi.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
  115. package/wwwroot/_framework/{MindExecution.Plugins.Business.ja94a5ze3i.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
  116. package/wwwroot/_framework/{MindExecution.Plugins.Concept.3q05vzviqb.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
  117. package/wwwroot/_framework/{MindExecution.Plugins.Directory.re4jjs91d1.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
  118. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.89etwfquz1.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
  119. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.aqf4vztjji.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
  120. package/wwwroot/_framework/{MindExecution.Shared.9b1rucdxvv.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
  121. package/wwwroot/_framework/{MindExecution.Web.ssp0n0xf1e.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
  122. package/wwwroot/_framework/blazor.boot.json +23 -23
  123. package/wwwroot/_headers +40 -40
  124. package/wwwroot/_redirects +1 -1
  125. package/wwwroot/index.html +772 -772
  126. package/wwwroot/js/marketing-tool.js +180 -180
  127. package/wwwroot/manifest.webmanifest +23 -23
  128. package/wwwroot/service-worker-assets.js +78 -106
  129. package/wwwroot/service-worker.js +33 -33
  130. package/tree-sitter-grammars/README.md +0 -18
  131. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  132. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  133. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  134. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  135. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  136. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  137. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  138. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  139. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  140. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  141. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  142. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
  143. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
  144. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
  145. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +0 -1258
@@ -6,29 +6,29 @@
6
6
  return;
7
7
  }
8
8
 
9
- const THREE = globalThis.THREE;
10
- let plusTexture = null; // Reusable '+' texture for SpatialGrid
11
- const THEME_ANIMATION_MODE = Object.freeze({
12
- None: 'none',
13
- Camera: 'camera',
14
- Continuous: 'continuous'
15
- });
16
- const THEME_ANIMATION_MODE_BY_NAME = Object.freeze({
17
- SpatialGrid2D: THEME_ANIMATION_MODE.None,
18
- GridPlane: THEME_ANIMATION_MODE.None,
19
- XYGrid: THEME_ANIMATION_MODE.None,
20
- SpinningBoxes: THEME_ANIMATION_MODE.Continuous,
21
- Space3D: THEME_ANIMATION_MODE.Continuous,
22
- SpiritFlow: THEME_ANIMATION_MODE.Continuous,
23
- None: THEME_ANIMATION_MODE.None
24
- });
25
- const THEME_CAMERA_EPSILON = 0.001;
26
- const THEME_SURFACE_BG = 0xe3e7ee;
27
- const THEME_PLAIN_BG = 0xe8edf3;
28
- const THEME_GRID_COLOR = 0xaab3bf;
29
- const THEME_GRID_COLOR_SOFT = 0xaeb6c0;
30
-
31
- // --- Helper 1: create '+' texture (for SpatialGrid) ---
9
+ const THREE = globalThis.THREE;
10
+ let plusTexture = null; // Reusable '+' texture for SpatialGrid
11
+ const THEME_ANIMATION_MODE = Object.freeze({
12
+ None: 'none',
13
+ Camera: 'camera',
14
+ Continuous: 'continuous'
15
+ });
16
+ const THEME_ANIMATION_MODE_BY_NAME = Object.freeze({
17
+ SpatialGrid2D: THEME_ANIMATION_MODE.None,
18
+ GridPlane: THEME_ANIMATION_MODE.None,
19
+ XYGrid: THEME_ANIMATION_MODE.None,
20
+ SpinningBoxes: THEME_ANIMATION_MODE.Continuous,
21
+ Space3D: THEME_ANIMATION_MODE.Continuous,
22
+ SpiritFlow: THEME_ANIMATION_MODE.Continuous,
23
+ None: THEME_ANIMATION_MODE.None
24
+ });
25
+ const THEME_CAMERA_EPSILON = 0.001;
26
+ const THEME_SURFACE_BG = 0xe3e7ee;
27
+ const THEME_PLAIN_BG = 0xe8edf3;
28
+ const THEME_GRID_COLOR = 0xaab3bf;
29
+ const THEME_GRID_COLOR_SOFT = 0xaeb6c0;
30
+
31
+ // --- Helper 1: create '+' texture (for SpatialGrid) ---
32
32
  function createPlusTexture() {
33
33
  if (plusTexture) return plusTexture; // Reuse if already created
34
34
 
@@ -36,8 +36,8 @@
36
36
  canvas.width = 64;
37
37
  canvas.height = 64;
38
38
  const ctx = canvas.getContext('2d');
39
- // Neutral gray so it remains visible on the darker default canvas.
40
- ctx.fillStyle = '#6f7782';
39
+ // Neutral gray so it remains visible on the darker default canvas.
40
+ ctx.fillStyle = '#6f7782';
41
41
  ctx.fillRect(28, 4, 8, 56);
42
42
  ctx.fillRect(4, 28, 56, 8);
43
43
 
@@ -48,11 +48,11 @@
48
48
  }
49
49
 
50
50
  // --- Helper 2: dispose theme resources ---
51
- function disposeThemeObject(themeObject) {
52
- if (!themeObject) return;
53
- if (typeof themeObject.customDispose === 'function') {
54
- themeObject.customDispose();
55
- return;
51
+ function disposeThemeObject(themeObject) {
52
+ if (!themeObject) return;
53
+ if (typeof themeObject.customDispose === 'function') {
54
+ themeObject.customDispose();
55
+ return;
56
56
  }
57
57
  themeObject.traverse(child => {
58
58
  if (child.geometry) child.geometry.dispose();
@@ -63,256 +63,256 @@
63
63
  if (child.material.map) child.material.map.dispose();
64
64
  child.material.dispose();
65
65
  }
66
- }
67
- });
68
- }
69
-
70
- function setThemeAnimationMode(themeObject, mode) {
71
- if (!themeObject) return themeObject;
72
- if (!themeObject.userData) {
73
- themeObject.userData = {};
74
- }
75
- themeObject.userData.backgroundThemeAnimationMode = mode;
76
- return themeObject;
77
- }
78
-
79
- function getThemeAnimationMode(themeName, themeObject) {
80
- const objectMode = themeObject?.userData?.backgroundThemeAnimationMode;
81
- if (objectMode) {
82
- return objectMode;
83
- }
84
- return THEME_ANIMATION_MODE_BY_NAME[themeName] || THEME_ANIMATION_MODE.None;
85
- }
86
-
87
- function readFiniteNumber(value, fallback = 0) {
88
- const number = Number(value);
89
- return Number.isFinite(number) ? number : fallback;
90
- }
91
-
92
- function captureThemeCameraSignature(camera) {
93
- if (!camera) return null;
94
- return {
95
- x: readFiniteNumber(camera.position?.x),
96
- y: readFiniteNumber(camera.position?.y),
97
- z: readFiniteNumber(camera.position?.z),
98
- rx: readFiniteNumber(camera.rotation?.x),
99
- ry: readFiniteNumber(camera.rotation?.y),
100
- rz: readFiniteNumber(camera.rotation?.z),
101
- zoom: readFiniteNumber(camera.zoom, 1),
102
- fov: readFiniteNumber(camera.fov),
103
- aspect: readFiniteNumber(camera.aspect)
104
- };
105
- }
106
-
107
- function hasThemeCameraChanged(previous, next) {
108
- if (!previous || !next) return true;
109
- return Object.keys(next).some(key => Math.abs(next[key] - previous[key]) > THEME_CAMERA_EPSILON);
110
- }
111
-
112
- function getThemeAnimationState(themeObject) {
113
- if (!themeObject) return null;
114
- if (!themeObject.userData) {
115
- themeObject.userData = {};
116
- }
117
- if (!themeObject.userData.backgroundThemeAnimationState) {
118
- themeObject.userData.backgroundThemeAnimationState = {
119
- lastCameraSignature: null
120
- };
121
- }
122
- return themeObject.userData.backgroundThemeAnimationState;
123
- }
124
-
125
- function shouldAnimateTheme(themeName, themeObject, camera, options = {}) {
126
- if (!themeObject || typeof themeObject.animate !== 'function') {
127
- return false;
128
- }
129
- if (themeObject.userData?.localSnapGrid === true) {
130
- return false;
131
- }
132
-
133
- const mode = getThemeAnimationMode(themeName, themeObject);
134
- if (mode === THEME_ANIMATION_MODE.Continuous) {
135
- return true;
136
- }
137
- if (mode === THEME_ANIMATION_MODE.None) {
138
- return options.force === true;
139
- }
140
-
141
- const state = getThemeAnimationState(themeObject);
142
- const nextSignature = captureThemeCameraSignature(camera);
143
- return options.force === true || hasThemeCameraChanged(state?.lastCameraSignature, nextSignature);
144
- }
145
-
146
- function runThemeAnimationIfNeeded(themeName, themeObject, camera, options = {}) {
147
- if (!shouldAnimateTheme(themeName, themeObject, camera, options)) {
148
- return false;
149
- }
150
-
151
- themeObject.animate(camera, options);
152
-
153
- const mode = getThemeAnimationMode(themeName, themeObject);
154
- if (mode === THEME_ANIMATION_MODE.Camera || mode === THEME_ANIMATION_MODE.None) {
155
- const state = getThemeAnimationState(themeObject);
156
- if (state) {
157
- state.lastCameraSignature = captureThemeCameraSignature(camera);
158
- }
159
- }
160
-
161
- return true;
162
- }
163
-
164
- function resolveThemeGridColor(colorValue, fallback = THEME_GRID_COLOR) {
165
- if (colorValue && colorValue.isColor) {
166
- return colorValue.clone();
167
- }
168
-
169
- try {
170
- return new THREE.Color(colorValue ?? fallback);
171
- } catch {
172
- return new THREE.Color(fallback);
173
- }
174
- }
175
-
176
- function setInfiniteGridHelperOpacity(gridHelper, opacityFactor, baseSmallOpacity, baseLargeOpacity) {
177
- if (!gridHelper || !gridHelper.children) return;
178
-
179
- const visible = opacityFactor > 0.01;
180
- gridHelper.visible = visible;
181
- if (!visible) {
182
- return;
183
- }
184
-
185
- if (gridHelper.children[0]?.material) {
186
- gridHelper.children[0].material.opacity = baseSmallOpacity * opacityFactor;
187
- }
188
- if (gridHelper.children[1]?.material) {
189
- gridHelper.children[1].material.opacity = baseLargeOpacity * opacityFactor;
190
- }
191
- }
192
-
193
- function getAdaptiveXYGridOpacityFactors(cameraZ) {
194
- if (cameraZ < 1800) {
195
- return { fine: 1.0, coarse: 0.0, far: 0.0 };
196
- }
197
- if (cameraZ < 3600) {
198
- const t = (cameraZ - 1800) / 1800;
199
- return { fine: 1.0 - t, coarse: t, far: 0.0 };
200
- }
201
- if (cameraZ < 12000) {
202
- return { fine: 0.0, coarse: 1.0, far: 0.0 };
203
- }
204
- if (cameraZ < 22000) {
205
- const t = (cameraZ - 12000) / 10000;
206
- return { fine: 0.0, coarse: 1.0 - t, far: t };
207
- }
208
- if (cameraZ < 50000) {
209
- return { fine: 0.0, coarse: 0.0, far: 0.9 };
210
- }
211
- if (cameraZ < 80000) {
212
- const t = (cameraZ - 50000) / 30000;
213
- return { fine: 0.0, coarse: 0.0, far: 0.9 - (t * 0.35) };
214
- }
215
-
216
- return { fine: 0.0, coarse: 0.0, far: 0.55 };
217
- }
218
-
219
- function createAdaptiveXYGridOverlay(colorValue, options = {}) {
220
- const settings = {
221
- fineDistance: 45000,
222
- coarseDistance: 120000,
223
- farDistance: 320000,
224
- fineSmallOpacity: 0.4,
225
- fineLargeOpacity: 0.25,
226
- coarseSmallOpacity: 0.34,
227
- coarseLargeOpacity: 0.22,
228
- farSmallOpacity: 0.26,
229
- farLargeOpacity: 0.16,
230
- ...options
231
- };
232
-
233
- if (settings.localCssOnly === true) {
234
- return createLocalSnapGridTheme(null, {
235
- themeAlias: settings.themeAlias || 'AdaptiveLocalSnapGrid',
236
- localSnapGridStyle: settings.localSnapGridStyle || 'line'
237
- });
238
- }
239
-
240
- const overlayGroup = new THREE.Group();
241
- const color = resolveThemeGridColor(colorValue, THEME_GRID_COLOR);
242
- const tmp = new THREE.Vector3();
243
- const grids = [
244
- {
245
- key: 'fine',
246
- helper: new window.InfiniteGridHelper(10, 100, color.clone(), settings.fineDistance, 'xyz'),
247
- snap: 100,
248
- baseSmallOpacity: settings.fineSmallOpacity,
249
- baseLargeOpacity: settings.fineLargeOpacity
250
- },
251
- {
252
- key: 'coarse',
253
- helper: new window.InfiniteGridHelper(100, 1000, color.clone(), settings.coarseDistance, 'xyz'),
254
- snap: 1000,
255
- baseSmallOpacity: settings.coarseSmallOpacity,
256
- baseLargeOpacity: settings.coarseLargeOpacity
257
- },
258
- {
259
- key: 'far',
260
- helper: new window.InfiniteGridHelper(1000, 10000, color.clone(), settings.farDistance, 'xyz'),
261
- snap: 10000,
262
- baseSmallOpacity: settings.farSmallOpacity,
263
- baseLargeOpacity: settings.farLargeOpacity
264
- }
265
- ];
266
-
267
- grids.forEach((grid, index) => {
268
- grid.helper.visible = index === 0;
269
- overlayGroup.add(grid.helper);
270
- });
271
-
272
- overlayGroup.animate = (camera) => {
273
- if (!camera) return;
274
-
275
- tmp.copy(camera.position);
276
- tmp.z = 0;
277
-
278
- grids.forEach(grid => {
279
- const snappedX = Math.round(tmp.x / grid.snap) * grid.snap;
280
- const snappedY = Math.round(tmp.y / grid.snap) * grid.snap;
281
- grid.helper.position.set(snappedX, snappedY, 0);
282
- });
283
-
284
- const factors = getAdaptiveXYGridOpacityFactors(camera.position.z);
285
- grids.forEach(grid => {
286
- setInfiniteGridHelperOpacity(
287
- grid.helper,
288
- factors[grid.key] || 0,
289
- grid.baseSmallOpacity,
290
- grid.baseLargeOpacity);
291
- });
292
- };
293
-
294
- return setThemeAnimationMode(overlayGroup, THEME_ANIMATION_MODE.Camera);
295
- }
296
-
297
- function createLocalSnapGridTheme(scene, options = {}) {
298
- const themeGroup = new THREE.Group();
299
- themeGroup.visible = false;
300
- themeGroup.userData.lightweightAdaptiveGrid = true;
301
- themeGroup.userData.localSnapGrid = true;
302
- themeGroup.userData.localSnapGridStyle = options.localSnapGridStyle || 'line';
303
- themeGroup.userData.localSnapGridRadiusPx = 250;
304
- themeGroup.userData.themeAlias = options.themeAlias || 'LocalSnapGrid';
305
- themeGroup.userData.localSnapGridBackgroundColor = options.backgroundColor;
306
- themeGroup.animate = () => { };
307
- return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.None);
308
- }
309
-
310
- // --- Theme definitions ---
66
+ }
67
+ });
68
+ }
69
+
70
+ function setThemeAnimationMode(themeObject, mode) {
71
+ if (!themeObject) return themeObject;
72
+ if (!themeObject.userData) {
73
+ themeObject.userData = {};
74
+ }
75
+ themeObject.userData.backgroundThemeAnimationMode = mode;
76
+ return themeObject;
77
+ }
78
+
79
+ function getThemeAnimationMode(themeName, themeObject) {
80
+ const objectMode = themeObject?.userData?.backgroundThemeAnimationMode;
81
+ if (objectMode) {
82
+ return objectMode;
83
+ }
84
+ return THEME_ANIMATION_MODE_BY_NAME[themeName] || THEME_ANIMATION_MODE.None;
85
+ }
86
+
87
+ function readFiniteNumber(value, fallback = 0) {
88
+ const number = Number(value);
89
+ return Number.isFinite(number) ? number : fallback;
90
+ }
91
+
92
+ function captureThemeCameraSignature(camera) {
93
+ if (!camera) return null;
94
+ return {
95
+ x: readFiniteNumber(camera.position?.x),
96
+ y: readFiniteNumber(camera.position?.y),
97
+ z: readFiniteNumber(camera.position?.z),
98
+ rx: readFiniteNumber(camera.rotation?.x),
99
+ ry: readFiniteNumber(camera.rotation?.y),
100
+ rz: readFiniteNumber(camera.rotation?.z),
101
+ zoom: readFiniteNumber(camera.zoom, 1),
102
+ fov: readFiniteNumber(camera.fov),
103
+ aspect: readFiniteNumber(camera.aspect)
104
+ };
105
+ }
106
+
107
+ function hasThemeCameraChanged(previous, next) {
108
+ if (!previous || !next) return true;
109
+ return Object.keys(next).some(key => Math.abs(next[key] - previous[key]) > THEME_CAMERA_EPSILON);
110
+ }
111
+
112
+ function getThemeAnimationState(themeObject) {
113
+ if (!themeObject) return null;
114
+ if (!themeObject.userData) {
115
+ themeObject.userData = {};
116
+ }
117
+ if (!themeObject.userData.backgroundThemeAnimationState) {
118
+ themeObject.userData.backgroundThemeAnimationState = {
119
+ lastCameraSignature: null
120
+ };
121
+ }
122
+ return themeObject.userData.backgroundThemeAnimationState;
123
+ }
124
+
125
+ function shouldAnimateTheme(themeName, themeObject, camera, options = {}) {
126
+ if (!themeObject || typeof themeObject.animate !== 'function') {
127
+ return false;
128
+ }
129
+ if (themeObject.userData?.localSnapGrid === true) {
130
+ return false;
131
+ }
132
+
133
+ const mode = getThemeAnimationMode(themeName, themeObject);
134
+ if (mode === THEME_ANIMATION_MODE.Continuous) {
135
+ return true;
136
+ }
137
+ if (mode === THEME_ANIMATION_MODE.None) {
138
+ return options.force === true;
139
+ }
140
+
141
+ const state = getThemeAnimationState(themeObject);
142
+ const nextSignature = captureThemeCameraSignature(camera);
143
+ return options.force === true || hasThemeCameraChanged(state?.lastCameraSignature, nextSignature);
144
+ }
145
+
146
+ function runThemeAnimationIfNeeded(themeName, themeObject, camera, options = {}) {
147
+ if (!shouldAnimateTheme(themeName, themeObject, camera, options)) {
148
+ return false;
149
+ }
150
+
151
+ themeObject.animate(camera, options);
152
+
153
+ const mode = getThemeAnimationMode(themeName, themeObject);
154
+ if (mode === THEME_ANIMATION_MODE.Camera || mode === THEME_ANIMATION_MODE.None) {
155
+ const state = getThemeAnimationState(themeObject);
156
+ if (state) {
157
+ state.lastCameraSignature = captureThemeCameraSignature(camera);
158
+ }
159
+ }
160
+
161
+ return true;
162
+ }
163
+
164
+ function resolveThemeGridColor(colorValue, fallback = THEME_GRID_COLOR) {
165
+ if (colorValue && colorValue.isColor) {
166
+ return colorValue.clone();
167
+ }
168
+
169
+ try {
170
+ return new THREE.Color(colorValue ?? fallback);
171
+ } catch {
172
+ return new THREE.Color(fallback);
173
+ }
174
+ }
175
+
176
+ function setInfiniteGridHelperOpacity(gridHelper, opacityFactor, baseSmallOpacity, baseLargeOpacity) {
177
+ if (!gridHelper || !gridHelper.children) return;
178
+
179
+ const visible = opacityFactor > 0.01;
180
+ gridHelper.visible = visible;
181
+ if (!visible) {
182
+ return;
183
+ }
184
+
185
+ if (gridHelper.children[0]?.material) {
186
+ gridHelper.children[0].material.opacity = baseSmallOpacity * opacityFactor;
187
+ }
188
+ if (gridHelper.children[1]?.material) {
189
+ gridHelper.children[1].material.opacity = baseLargeOpacity * opacityFactor;
190
+ }
191
+ }
192
+
193
+ function getAdaptiveXYGridOpacityFactors(cameraZ) {
194
+ if (cameraZ < 1800) {
195
+ return { fine: 1.0, coarse: 0.0, far: 0.0 };
196
+ }
197
+ if (cameraZ < 3600) {
198
+ const t = (cameraZ - 1800) / 1800;
199
+ return { fine: 1.0 - t, coarse: t, far: 0.0 };
200
+ }
201
+ if (cameraZ < 12000) {
202
+ return { fine: 0.0, coarse: 1.0, far: 0.0 };
203
+ }
204
+ if (cameraZ < 22000) {
205
+ const t = (cameraZ - 12000) / 10000;
206
+ return { fine: 0.0, coarse: 1.0 - t, far: t };
207
+ }
208
+ if (cameraZ < 50000) {
209
+ return { fine: 0.0, coarse: 0.0, far: 0.9 };
210
+ }
211
+ if (cameraZ < 80000) {
212
+ const t = (cameraZ - 50000) / 30000;
213
+ return { fine: 0.0, coarse: 0.0, far: 0.9 - (t * 0.35) };
214
+ }
215
+
216
+ return { fine: 0.0, coarse: 0.0, far: 0.55 };
217
+ }
218
+
219
+ function createAdaptiveXYGridOverlay(colorValue, options = {}) {
220
+ const settings = {
221
+ fineDistance: 45000,
222
+ coarseDistance: 120000,
223
+ farDistance: 320000,
224
+ fineSmallOpacity: 0.4,
225
+ fineLargeOpacity: 0.25,
226
+ coarseSmallOpacity: 0.34,
227
+ coarseLargeOpacity: 0.22,
228
+ farSmallOpacity: 0.26,
229
+ farLargeOpacity: 0.16,
230
+ ...options
231
+ };
232
+
233
+ if (settings.localCssOnly === true) {
234
+ return createLocalSnapGridTheme(null, {
235
+ themeAlias: settings.themeAlias || 'AdaptiveLocalSnapGrid',
236
+ localSnapGridStyle: settings.localSnapGridStyle || 'line'
237
+ });
238
+ }
239
+
240
+ const overlayGroup = new THREE.Group();
241
+ const color = resolveThemeGridColor(colorValue, THEME_GRID_COLOR);
242
+ const tmp = new THREE.Vector3();
243
+ const grids = [
244
+ {
245
+ key: 'fine',
246
+ helper: new window.InfiniteGridHelper(10, 100, color.clone(), settings.fineDistance, 'xyz'),
247
+ snap: 100,
248
+ baseSmallOpacity: settings.fineSmallOpacity,
249
+ baseLargeOpacity: settings.fineLargeOpacity
250
+ },
251
+ {
252
+ key: 'coarse',
253
+ helper: new window.InfiniteGridHelper(100, 1000, color.clone(), settings.coarseDistance, 'xyz'),
254
+ snap: 1000,
255
+ baseSmallOpacity: settings.coarseSmallOpacity,
256
+ baseLargeOpacity: settings.coarseLargeOpacity
257
+ },
258
+ {
259
+ key: 'far',
260
+ helper: new window.InfiniteGridHelper(1000, 10000, color.clone(), settings.farDistance, 'xyz'),
261
+ snap: 10000,
262
+ baseSmallOpacity: settings.farSmallOpacity,
263
+ baseLargeOpacity: settings.farLargeOpacity
264
+ }
265
+ ];
266
+
267
+ grids.forEach((grid, index) => {
268
+ grid.helper.visible = index === 0;
269
+ overlayGroup.add(grid.helper);
270
+ });
271
+
272
+ overlayGroup.animate = (camera) => {
273
+ if (!camera) return;
274
+
275
+ tmp.copy(camera.position);
276
+ tmp.z = 0;
277
+
278
+ grids.forEach(grid => {
279
+ const snappedX = Math.round(tmp.x / grid.snap) * grid.snap;
280
+ const snappedY = Math.round(tmp.y / grid.snap) * grid.snap;
281
+ grid.helper.position.set(snappedX, snappedY, 0);
282
+ });
283
+
284
+ const factors = getAdaptiveXYGridOpacityFactors(camera.position.z);
285
+ grids.forEach(grid => {
286
+ setInfiniteGridHelperOpacity(
287
+ grid.helper,
288
+ factors[grid.key] || 0,
289
+ grid.baseSmallOpacity,
290
+ grid.baseLargeOpacity);
291
+ });
292
+ };
293
+
294
+ return setThemeAnimationMode(overlayGroup, THEME_ANIMATION_MODE.Camera);
295
+ }
296
+
297
+ function createLocalSnapGridTheme(scene, options = {}) {
298
+ const themeGroup = new THREE.Group();
299
+ themeGroup.visible = false;
300
+ themeGroup.userData.lightweightAdaptiveGrid = true;
301
+ themeGroup.userData.localSnapGrid = true;
302
+ themeGroup.userData.localSnapGridStyle = options.localSnapGridStyle || 'line';
303
+ themeGroup.userData.localSnapGridRadiusPx = 250;
304
+ themeGroup.userData.themeAlias = options.themeAlias || 'LocalSnapGrid';
305
+ themeGroup.userData.localSnapGridBackgroundColor = options.backgroundColor;
306
+ themeGroup.animate = () => { };
307
+ return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.None);
308
+ }
309
+
310
+ // --- Theme definitions ---
311
311
  const themes = {
312
312
  // Theme 1: SpatialGrid (3D dots + '+' markers)
313
313
  'SpatialGrid3D': {
314
314
  create: (scene) => {
315
- scene.background = new THREE.Color(THEME_SURFACE_BG);
315
+ scene.background = new THREE.Color(THEME_SURFACE_BG);
316
316
  const gridRange = 50, gridStep = 1, markerStep = 10;
317
317
  const markerSize = 0.5, dotSize = 0.1;
318
318
  const dotVertices = [], markerVertices = [];
@@ -338,34 +338,34 @@
338
338
  dispose: (themeObject) => { disposeThemeObject(themeObject); }
339
339
  },
340
340
 
341
- // Theme 2: SpatialGrid2D (lightweight adaptive grid on the XY plane)
342
- 'SpatialGrid2D': {
343
- create: (scene) => {
344
- return createLocalSnapGridTheme(scene, {
345
- themeAlias: 'SpatialGrid2D',
346
- localSnapGridStyle: 'dot',
347
- backgroundColor: THEME_SURFACE_BG
348
- });
349
- },
350
- dispose: (themeObject) => { disposeThemeObject(themeObject); }
351
- },
352
-
353
- // Theme 3: GridPlane (basic 2D grid - rendered by the local CSS snap grid)
354
- 'GridPlane': {
355
- create: (scene) => {
356
- return createLocalSnapGridTheme(scene, {
357
- themeAlias: 'GridPlane',
358
- localSnapGridStyle: 'line',
359
- backgroundColor: THEME_PLAIN_BG
360
- });
361
- },
362
- dispose: (themeObject) => { disposeThemeObject(themeObject); }
363
- },
341
+ // Theme 2: SpatialGrid2D (lightweight adaptive grid on the XY plane)
342
+ 'SpatialGrid2D': {
343
+ create: (scene) => {
344
+ return createLocalSnapGridTheme(scene, {
345
+ themeAlias: 'SpatialGrid2D',
346
+ localSnapGridStyle: 'dot',
347
+ backgroundColor: THEME_SURFACE_BG
348
+ });
349
+ },
350
+ dispose: (themeObject) => { disposeThemeObject(themeObject); }
351
+ },
352
+
353
+ // Theme 3: GridPlane (basic 2D grid - rendered by the local CSS snap grid)
354
+ 'GridPlane': {
355
+ create: (scene) => {
356
+ return createLocalSnapGridTheme(scene, {
357
+ themeAlias: 'GridPlane',
358
+ localSnapGridStyle: 'line',
359
+ backgroundColor: THEME_PLAIN_BG
360
+ });
361
+ },
362
+ dispose: (themeObject) => { disposeThemeObject(themeObject); }
363
+ },
364
364
 
365
365
  // Theme 4: SpinningBoxes (reuses 3d-model-viewer logic)
366
366
  'SpinningBoxes': {
367
367
  create: (scene) => {
368
- scene.background = new THREE.Color(THEME_SURFACE_BG);
368
+ scene.background = new THREE.Color(THEME_SURFACE_BG);
369
369
 
370
370
  const themeGroup = new THREE.Group();
371
371
 
@@ -407,18 +407,18 @@
407
407
  light.position.y = Math.cos(time * 0.5) * 2000;
408
408
  light.position.z = Math.cos(time * 0.3) * 2000 - 1000;
409
409
  }
410
- };
411
- themeGroup.customDispose = () => { disposeThemeObject(themeGroup); themeGroup.userData = {}; themeGroup.animate = undefined; themeGroup.customDispose = undefined; };
412
-
413
- return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.Continuous);
414
- },
410
+ };
411
+ themeGroup.customDispose = () => { disposeThemeObject(themeGroup); themeGroup.userData = {}; themeGroup.animate = undefined; themeGroup.customDispose = undefined; };
412
+
413
+ return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.Continuous);
414
+ },
415
415
  dispose: (themeObject) => { if (typeof themeObject?.customDispose === 'function') { themeObject.customDispose(); } else { disposeThemeObject(themeObject); } }
416
416
  },
417
417
 
418
418
  // Theme 5: FloorBoxes (boxes placed on the XZ plane)
419
419
  'FloorBoxes': {
420
420
  create: (scene) => {
421
- scene.background = new THREE.Color(THEME_SURFACE_BG);
421
+ scene.background = new THREE.Color(THEME_SURFACE_BG);
422
422
 
423
423
  const themeGroup = new THREE.Group();
424
424
 
@@ -482,1151 +482,1151 @@
482
482
  }
483
483
  },
484
484
 
485
- // Theme 6: XYGrid (distance-based auto LOD grid)
486
- 'XYGrid': {
487
- create: (scene) => {
488
- return createLocalSnapGridTheme(scene, {
489
- themeAlias: 'XYGrid',
490
- localSnapGridStyle: 'line',
491
- backgroundColor: THEME_PLAIN_BG
492
- });
493
- },
494
- dispose: (themeObject) => { disposeThemeObject(themeObject); }
495
- },
496
-
497
- // Theme 7: Core Particles (inverted galaxy shader particles + faint XYGrid overlay)
498
- 'Space3D': {
499
- create: (scene, renderer) => {
500
- scene.background = new THREE.Color(THEME_PLAIN_BG);
501
-
502
- const themeGroup = new THREE.Group();
503
- const simSize = 64;
504
- const pointCount = simSize * simSize;
505
- const resolution = new THREE.Vector2(simSize, simSize);
506
- const galaxyCount = 2;
507
- const galaxyMass = 0.000075;
508
- const galaxyHaloSize = 6;
509
- const renderTargetOptions = {
510
- wrapS: THREE.ClampToEdgeWrapping,
511
- wrapT: THREE.ClampToEdgeWrapping,
512
- minFilter: THREE.NearestFilter,
513
- magFilter: THREE.NearestFilter,
514
- format: THREE.RGBAFormat,
515
- type: THREE.FloatType,
516
- depthBuffer: false,
517
- stencilBuffer: false
518
- };
519
-
520
- const createRenderTarget = () => {
521
- const target = new THREE.WebGLRenderTarget(simSize, simSize, renderTargetOptions);
522
- target.texture.generateMipmaps = false;
523
- return target;
524
- };
525
-
526
- const createInitialTextures = () => {
527
- const positionData = new Float32Array(pointCount * 4);
528
- const velocityData = new Float32Array(pointCount * 4);
529
- const randomSigned = () => Math.random() * 2 - 1;
530
- const particleCountWithoutCenters = pointCount - galaxyCount;
531
- const firstGalaxyParticleCount = Math.round(particleCountWithoutCenters / 2);
532
- const galaxyParticleCounts = [
533
- firstGalaxyParticleCount,
534
- particleCountWithoutCenters - firstGalaxyParticleCount
535
- ];
536
- const galaxySizes = [0.78 * galaxyHaloSize, 0.84 * galaxyHaloSize];
537
- const galaxyInclinations = [0.35 * Math.PI, -0.28 * Math.PI];
538
- const galaxyPositions = [
539
- new THREE.Vector3(-1.2 * galaxyHaloSize, -0.1 * galaxyHaloSize, 0),
540
- new THREE.Vector3(1.2 * galaxyHaloSize, 0.1 * galaxyHaloSize, 0)
541
- ];
542
- const galaxyVelocities = [
543
- new THREE.Vector3(0.00145, 0.00014, 0.0018),
544
- new THREE.Vector3(-0.00145, -0.00014, -0.0018)
545
- ];
546
-
547
- const writeParticle = (particleIndex, position, velocity) => {
548
- const offset = particleIndex * 4;
549
- positionData[offset] = position.x;
550
- positionData[offset + 1] = position.y;
551
- positionData[offset + 2] = position.z;
552
- positionData[offset + 3] = 1;
553
- velocityData[offset] = velocity.x;
554
- velocityData[offset + 1] = velocity.y;
555
- velocityData[offset + 2] = velocity.z;
556
- velocityData[offset + 3] = 1;
557
- };
558
-
559
- for (let i = 0; i < galaxyCount; i++) {
560
- writeParticle(i, galaxyPositions[i], galaxyVelocities[i]);
561
- }
562
-
563
- let startIndex = galaxyCount;
564
- for (let i = 0; i < galaxyCount; i++) {
565
- const center = galaxyPositions[i];
566
- const centerVelocity = galaxyVelocities[i];
567
- const sinInclination = Math.sin(galaxyInclinations[i]);
568
- const cosInclination = Math.cos(galaxyInclinations[i]);
569
-
570
- for (let j = startIndex; j < startIndex + galaxyParticleCounts[i]; j++) {
571
- const diffuseSeed = Math.random();
572
- const position = new THREE.Vector3();
573
- const velocity = new THREE.Vector3();
574
-
575
- if (diffuseSeed < 0.16) {
576
- const radius = galaxySizes[i] * (0.72 + Math.pow(Math.random(), 0.55) * 0.62);
577
- const angle = 2 * Math.PI * Math.random();
578
-
579
- position.set(
580
- center.x + Math.cos(angle) * radius + randomSigned() * 0.75,
581
- center.y + cosInclination * Math.sin(angle) * radius * 0.64 + randomSigned() * 0.72,
582
- center.z - sinInclination * Math.sin(angle) * radius * 0.64 + randomSigned() * 0.95
583
- );
584
- velocity.set(
585
- -Math.sin(angle) * 0.0034 + centerVelocity.x * 0.66,
586
- cosInclination * Math.cos(angle) * 0.0021 + centerVelocity.y * 0.66,
587
- -sinInclination * Math.cos(angle) * 0.0021 + randomSigned() * 0.0009 + centerVelocity.z * 0.66
588
- );
589
- } else {
590
- const distance = galaxySizes[i] * (0.18 + Math.pow(Math.random(), 0.7) * 0.82);
591
- const safeDistance = Math.max(distance, 0.08);
592
- const angle = 2 * Math.PI * Math.random();
593
- const massAtPosition = galaxyMass * Math.min(safeDistance, galaxyHaloSize) / galaxyHaloSize;
594
- const speed = Math.sqrt(massAtPosition / safeDistance);
595
- const x = distance * Math.cos(angle);
596
- const y = distance * Math.sin(angle);
597
- const vx = -speed * Math.sin(angle) * 1.75;
598
- const vy = speed * Math.cos(angle) * 1.75;
599
-
600
- position.set(
601
- center.x + x,
602
- center.y + cosInclination * y,
603
- center.z - sinInclination * y
604
- );
605
- velocity.set(
606
- centerVelocity.x + vx,
607
- centerVelocity.y + cosInclination * vy,
608
- centerVelocity.z - sinInclination * vy
609
- );
610
-
611
- const jitter = 0.14 + Math.pow(Math.random(), 2.0) * 0.62;
612
- position.x += randomSigned() * jitter;
613
- position.y += randomSigned() * jitter * 0.42;
614
- position.z += randomSigned() * jitter * 0.46;
615
-
616
- if (Math.random() < 0.16) {
617
- const direction = i === 0 ? -1 : 1;
618
- position.x += direction * (0.8 + Math.random() * 2.5);
619
- position.y += randomSigned() * 0.72;
620
- position.z += randomSigned() * 0.82;
621
- velocity.x += direction * (0.00022 + Math.random() * 0.00035);
622
- velocity.y += randomSigned() * 0.00018;
623
- velocity.z += randomSigned() * 0.00022;
624
- }
625
- }
626
-
627
- writeParticle(j, position, velocity);
628
- }
629
-
630
- startIndex += galaxyParticleCounts[i];
631
- }
632
-
633
- const positionTexture = new THREE.DataTexture(positionData, simSize, simSize, THREE.RGBAFormat, THREE.FloatType);
634
- const velocityTexture = new THREE.DataTexture(velocityData, simSize, simSize, THREE.RGBAFormat, THREE.FloatType);
635
- positionTexture.needsUpdate = true;
636
- velocityTexture.needsUpdate = true;
637
-
638
- return { positionTexture, velocityTexture };
639
- };
640
-
641
- const computeVertexShader = `
642
- void main() {
643
- gl_Position = vec4(position.xy, 0.0, 1.0);
644
- }
645
- `;
646
-
647
- const copyMaterial = new THREE.ShaderMaterial({
648
- uniforms: {
649
- u_texture: { value: null },
650
- u_resolution: { value: resolution }
651
- },
652
- vertexShader: computeVertexShader,
653
- fragmentShader: `
654
- precision highp float;
655
- uniform sampler2D u_texture;
656
- uniform vec2 u_resolution;
657
-
658
- void main() {
659
- vec2 uv = gl_FragCoord.xy / u_resolution;
660
- gl_FragColor = texture2D(u_texture, uv);
661
- }
662
- `,
663
- depthTest: false,
664
- depthWrite: false
665
- });
666
-
667
- const positionMaterial = new THREE.ShaderMaterial({
668
- uniforms: {
669
- u_positionTexture: { value: null },
670
- u_velocityTexture: { value: null },
671
- u_resolution: { value: resolution },
672
- u_dt: { value: 0.18 }
673
- },
674
- vertexShader: computeVertexShader,
675
- fragmentShader: `
676
- precision highp float;
677
- uniform sampler2D u_positionTexture;
678
- uniform sampler2D u_velocityTexture;
679
- uniform vec2 u_resolution;
680
- uniform float u_dt;
681
-
682
- void main() {
683
- vec2 uv = gl_FragCoord.xy / u_resolution;
684
- vec3 position = texture2D(u_positionTexture, uv).xyz;
685
- vec3 velocity = texture2D(u_velocityTexture, uv).xyz;
686
- float speedSeed = fract(sin(dot(uv, vec2(127.1, 311.7))) * 43758.5453);
687
- float particleDt = u_dt * mix(0.82, 1.22, speedSeed);
688
- gl_FragColor = vec4(position + particleDt * velocity, 1.0);
689
- }
690
- `,
691
- depthTest: false,
692
- depthWrite: false
693
- });
694
-
695
- const velocityMaterial = new THREE.ShaderMaterial({
696
- uniforms: {
697
- u_positionTexture: { value: null },
698
- u_velocityTexture: { value: null },
699
- u_resolution: { value: resolution },
700
- u_dt: { value: 0.18 },
701
- u_mass: { value: galaxyMass },
702
- u_haloSize: { value: galaxyHaloSize },
703
- u_softening: { value: 1.7 },
704
- u_damping: { value: 0.991 },
705
- u_maxSpeed: { value: 0.038 },
706
- u_velocityDeadzone: { value: 0.0026 },
707
- u_settleDamping: { value: 0.78 }
708
- },
709
- vertexShader: computeVertexShader,
710
- fragmentShader: `
711
- precision highp float;
712
- #define GALAXY_COUNT 2
713
-
714
- uniform sampler2D u_positionTexture;
715
- uniform sampler2D u_velocityTexture;
716
- uniform vec2 u_resolution;
717
- uniform float u_dt;
718
- uniform float u_mass;
719
- uniform float u_haloSize;
720
- uniform float u_softening;
721
- uniform float u_damping;
722
- uniform float u_maxSpeed;
723
- uniform float u_velocityDeadzone;
724
- uniform float u_settleDamping;
725
-
726
- void main() {
727
- vec2 uv = gl_FragCoord.xy / u_resolution;
728
- vec3 position = texture2D(u_positionTexture, uv).xyz;
729
- vec3 velocity = texture2D(u_velocityTexture, uv).xyz;
730
- vec3 totalForce = vec3(0.0);
731
- vec3 centerSum = vec3(0.0);
732
- vec3 nearestCenter = vec3(0.0);
733
- float nearestDistance = 99999.0;
734
-
735
- for (int i = 0; i < GALAXY_COUNT; i++) {
736
- float particleIndex = float(i);
737
- vec2 particleUv = vec2(
738
- mod(particleIndex, u_resolution.x) + 0.5,
739
- floor(particleIndex / u_resolution.x) + 0.5
740
- ) / u_resolution;
741
- vec3 particlePosition = texture2D(u_positionTexture, particleUv).xyz;
742
- vec3 forceDirection = particlePosition - position;
743
- float distance = length(forceDirection);
744
- centerSum += particlePosition;
745
-
746
- if (distance > 0.0001) {
747
- vec3 directionToCenter = forceDirection / distance;
748
- float nearFade = smoothstep(u_haloSize * 0.45, u_haloSize * 1.05, distance);
749
- float massAtPosition = u_mass * nearFade * min(distance, u_haloSize) / u_haloSize;
750
- float coreRepulsion = 1.0 - smoothstep(u_haloSize * 0.42, u_haloSize * 0.88, distance);
751
- totalForce += massAtPosition * directionToCenter / pow(distance + u_softening, 2.0);
752
- totalForce -= directionToCenter * coreRepulsion * u_mass * 0.36 / (distance + u_softening);
753
- }
754
-
755
- if (distance < nearestDistance) {
756
- nearestDistance = distance;
757
- nearestCenter = particlePosition;
758
- }
759
- }
760
-
761
- vec3 barycenter = centerSum / float(GALAXY_COUNT);
762
- vec3 fromBarycenter = position - barycenter;
763
- float baryDistance = length(fromBarycenter);
764
- if (baryDistance > 0.0001) {
765
- vec3 outwardDirection = fromBarycenter / baryDistance;
766
- float compression = 1.0 - smoothstep(u_haloSize * 0.65, u_haloSize * 1.75, baryDistance);
767
- vec3 orbitDirection = vec3(-outwardDirection.y, outwardDirection.x, 0.0);
768
- float orbitLength = length(orbitDirection);
769
- totalForce += outwardDirection * compression * u_mass * 0.42 / (baryDistance + 0.75);
770
- if (orbitLength > 0.0001) {
771
- totalForce += (orbitDirection / orbitLength) * compression * u_mass * 0.28;
772
- }
773
- }
774
-
775
- vec3 nextVelocity = (velocity + u_dt * totalForce) * u_damping;
776
- if (nearestDistance > 0.0001 && nearestDistance < u_haloSize * 0.72) {
777
- vec3 awayFromCore = (position - nearestCenter) / nearestDistance;
778
- float corePressure = (u_haloSize * 0.72 - nearestDistance) / (u_haloSize * 0.72);
779
- nextVelocity += awayFromCore * corePressure * 0.0018;
780
- }
781
- float settleFactor = 1.0 - smoothstep(u_velocityDeadzone, u_velocityDeadzone * 4.0, length(nextVelocity));
782
- nextVelocity *= mix(1.0, u_settleDamping, settleFactor);
783
- float speed = length(nextVelocity);
784
- if (speed > u_maxSpeed) {
785
- nextVelocity = normalize(nextVelocity) * u_maxSpeed;
786
- }
787
-
788
- gl_FragColor = vec4(nextVelocity, 1.0);
789
- }
790
- `,
791
- depthTest: false,
792
- depthWrite: false
793
- });
794
-
795
- const computeScene = new THREE.Scene();
796
- const computeCamera = new THREE.Camera();
797
- const computeQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMaterial);
798
- computeQuad.frustumCulled = false;
799
- computeScene.add(computeQuad);
800
-
801
- const initialTextures = createInitialTextures();
802
- let positionTargetA = createRenderTarget();
803
- let positionTargetB = createRenderTarget();
804
- let velocityTargetA = createRenderTarget();
805
- let velocityTargetB = createRenderTarget();
806
- let currentPositionTarget = positionTargetA;
807
- let nextPositionTarget = positionTargetB;
808
- let currentVelocityTarget = velocityTargetA;
809
- let nextVelocityTarget = velocityTargetB;
810
- let simulationReady = renderer && typeof renderer.setRenderTarget === 'function';
811
- let simulationWarningShown = false;
812
- const simulationStepsPerTick = 16;
813
-
814
- const renderComputePass = (material, target) => {
815
- computeQuad.material = material;
816
- renderer.setRenderTarget(target);
817
- renderer.render(computeScene, computeCamera);
818
- };
819
-
820
- const copyTextureToTarget = (texture, target) => {
821
- copyMaterial.uniforms.u_texture.value = texture;
822
- renderComputePass(copyMaterial, target);
823
- };
824
-
825
- if (simulationReady) {
826
- const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
827
- try {
828
- copyTextureToTarget(initialTextures.positionTexture, currentPositionTarget);
829
- copyTextureToTarget(initialTextures.velocityTexture, currentVelocityTarget);
830
- renderer.setRenderTarget(previousTarget);
831
- } catch (error) {
832
- simulationReady = false;
833
- console.warn('[BackgroundThemes] Galaxy particle simulation disabled.', error);
834
- }
835
- }
836
-
837
- const particleGeometry = new THREE.BufferGeometry();
838
- const indices = new Float32Array(pointCount);
839
- const dummyPositions = new Float32Array(pointCount * 3);
840
- for (let i = 0; i < pointCount; i++) {
841
- indices[i] = i;
842
- }
843
- particleGeometry.setAttribute('a_index', new THREE.BufferAttribute(indices, 1));
844
- particleGeometry.setAttribute('position', new THREE.BufferAttribute(dummyPositions, 3));
845
-
846
- const particleMaterial = new THREE.ShaderMaterial({
847
- uniforms: {
848
- u_width: { value: simSize },
849
- u_height: { value: simSize },
850
- u_particleSize: { value: 64800 },
851
- u_worldScale: { value: 720 },
852
- u_depthScale: { value: 620 },
853
- u_backgroundZ: { value: -14000 },
854
- u_time: { value: 0 },
855
- u_positionTexture: { value: currentPositionTarget.texture }
856
- },
857
- vertexShader: `
858
- attribute float a_index;
859
- uniform float u_width;
860
- uniform float u_height;
861
- uniform float u_particleSize;
862
- uniform float u_worldScale;
863
- uniform float u_depthScale;
864
- uniform float u_backgroundZ;
865
- uniform float u_time;
866
- uniform sampler2D u_positionTexture;
867
- varying float vDepth;
868
- varying float vNearness;
869
-
870
- void main() {
871
- const float TWO_PI = 6.28318530718;
872
- vec2 uv = vec2(
873
- (mod(a_index, u_width) + 0.5) / u_width,
874
- (floor(a_index / u_width) + 0.5) / u_height
875
- );
876
- vec3 gravityPosition = texture2D(u_positionTexture, uv).xyz;
877
- float seed = fract(sin(a_index * 12.9898) * 43758.5453);
878
- float sizeSeed = fract(sin((a_index + 17.31) * 78.233) * 43758.5453);
879
- float depthSeed = fract(sin((a_index + 91.73) * 39.425) * 24634.6345);
880
- float speedSeed = fract(sin((a_index + 211.13) * 17.317) * 19542.349);
881
- float phaseSeed = fract(sin((a_index + 47.77) * 63.721) * 31571.231);
882
- float localTime = u_time * mix(0.72, 1.38, speedSeed) + phaseSeed * TWO_PI;
883
- float radius = length(gravityPosition.xy);
884
- float fieldFlow = mod(u_time * mix(0.046, 0.062, speedSeed), TWO_PI);
885
- float fc = cos(fieldFlow);
886
- float fs = sin(fieldFlow);
887
- gravityPosition.xy = mat2(fc, -fs, fs, fc) * gravityPosition.xy;
888
-
889
- float flow = mod(localTime * mix(0.018, 0.054, seed) * (1.0 - smoothstep(9.0, 18.0, radius)), TWO_PI);
890
- float c = cos(flow);
891
- float s = sin(flow);
892
- gravityPosition.xy = mat2(c, -s, s, c) * gravityPosition.xy;
893
- gravityPosition.xy += vec2(
894
- sin(mod(localTime * mix(0.08, 0.22, seed) + seed * TWO_PI, TWO_PI)),
895
- cos(mod(localTime * mix(0.07, 0.2, seed) + seed * 8.713, TWO_PI))
896
- ) * mix(0.055, 0.18, seed);
897
- float depthLayer = (depthSeed - 0.5) * mix(4.0, 8.4, sizeSeed);
898
- float depthDrift = sin(mod(localTime * mix(0.035, 0.1, seed) + seed * 12.421, TWO_PI)) * mix(0.1, 0.34, depthSeed);
899
- float layeredZ = clamp(gravityPosition.z + depthLayer + depthDrift, -6.8, 4.2);
900
- vec3 simPosition = vec3(
901
- gravityPosition.xy * u_worldScale,
902
- u_backgroundZ + layeredZ * u_depthScale
903
- );
904
- vec4 mvPosition = modelViewMatrix * vec4(simPosition, 1.0);
905
- float viewDepth = max(-mvPosition.z, 1.0);
906
- float seededSize = mix(0.55, 2.05, pow(sizeSeed, 1.42));
907
- gl_PointSize = clamp((u_particleSize / viewDepth) * seededSize * 2.0, 1.0, 17.0);
908
- gl_Position = projectionMatrix * mvPosition;
909
- vDepth = clamp((viewDepth - 9000.0) / 52000.0, 0.0, 1.0);
910
- vNearness = 1.0 - clamp((viewDepth - 9000.0) / 28000.0, 0.0, 1.0);
911
- }
912
- `,
913
- fragmentShader: `
914
- precision mediump float;
915
- varying float vDepth;
916
- varying float vNearness;
917
-
918
- void main() {
919
- vec2 uv = gl_PointCoord - vec2(0.5);
920
- float dist = length(uv);
921
- float radius = mix(0.38, 0.49, vNearness);
922
- float core = 1.0 - smoothstep(0.02, radius, dist);
923
- float halo = (1.0 - smoothstep(0.28, 0.5, dist)) * vNearness * 0.24;
924
- float alpha = (pow(max(core, 0.0), mix(1.6, 1.05, vNearness)) + halo) * mix(0.92, 0.46, vDepth);
925
- if (alpha < 0.015) discard;
926
- gl_FragColor = vec4(vec3(0.0, 0.0, 0.0), min(alpha, 0.94));
927
- }
928
- `,
929
- transparent: true,
930
- depthTest: true,
931
- depthWrite: false,
932
- blending: THREE.NormalBlending
933
- });
934
-
935
- const gravityPoints = new THREE.Points(particleGeometry, particleMaterial);
936
- gravityPoints.frustumCulled = false;
937
- gravityPoints.renderOrder = -40;
938
- themeGroup.add(gravityPoints);
939
-
940
- const themeStartTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001;
941
-
942
- const syncParticleView = (camera) => {
943
- if (!camera) return;
944
-
945
- const cameraZ = Math.max(600, Math.abs(Number(camera.position?.z || 1200)));
946
- const cameraFar = Math.max(120000, Number(camera.far || 500000));
947
- const clipSafeDepth = Math.max(14000, cameraFar - cameraZ - 5000);
948
- const backgroundDepth = Math.min(Math.max(26000, cameraZ * 6.0), clipSafeDepth * 0.82);
949
- const viewDistance = cameraZ + backgroundDepth;
950
- const elapsedTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001 - themeStartTime;
951
- const animationTime = elapsedTime * 8.0;
952
- particleMaterial.uniforms.u_worldScale.value = Math.max(820, backgroundDepth * 0.052);
953
- particleMaterial.uniforms.u_depthScale.value = Math.max(1050, backgroundDepth * 0.075);
954
- particleMaterial.uniforms.u_backgroundZ.value = -backgroundDepth;
955
- particleMaterial.uniforms.u_particleSize.value = Math.max(68000, Math.min(cameraZ * 34, viewDistance * 2.75));
956
- particleMaterial.uniforms.u_time.value = animationTime;
957
- gravityPoints.rotation.z = (animationTime * 0.085) % (Math.PI * 2);
958
- gravityPoints.rotation.x = 0;
959
- gravityPoints.position.x = 0;
960
- gravityPoints.position.y = 0;
961
- gravityPoints.updateMatrixWorld(true);
962
- };
963
-
964
- gravityPoints.onBeforeRender = (_renderer, _scene, camera) => {
965
- syncParticleView(camera);
966
- };
967
-
968
- // --- 2) XYGrid overlay (same distance-based LOD as XYGrid theme) ---
969
- const adaptiveGridOverlay = createAdaptiveXYGridOverlay(new THREE.Color(THEME_GRID_COLOR_SOFT), {
970
- fineDistance: 45000,
971
- coarseDistance: 120000,
972
- farDistance: 320000,
973
- fineSmallOpacity: 0.17,
974
- fineLargeOpacity: 0.1,
975
- coarseSmallOpacity: 0.15,
976
- coarseLargeOpacity: 0.085,
977
- farSmallOpacity: 0.12,
978
- farLargeOpacity: 0.07
979
- });
980
- themeGroup.add(adaptiveGridOverlay);
981
-
982
- // Animation: galaxy texture simulation and camera tracking for grid
983
- themeGroup.animate = (camera, options = {}) => {
984
- if (!camera) return;
985
-
986
- const isInteractiveMotionFrame = options.isInteractiveFrame === true ||
987
- options.isCameraMoving === true ||
988
- options.isPanning === true ||
989
- options.isZooming === true;
990
- const targetFps = Math.max(0, Number(options.targetFps || 0) || 0);
991
- const steadySimulationStepCount = targetFps > 0
992
- ? Math.max(1, Math.round(32 / targetFps))
993
- : simulationStepsPerTick;
994
- const simulationStepCount = isInteractiveMotionFrame ? 1 : steadySimulationStepCount;
995
-
996
- if (simulationReady) {
997
- const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
998
- try {
999
- for (let i = 0; i < simulationStepCount; i++) {
1000
- velocityMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1001
- velocityMaterial.uniforms.u_velocityTexture.value = currentVelocityTarget.texture;
1002
- renderComputePass(velocityMaterial, nextVelocityTarget);
1003
-
1004
- positionMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1005
- positionMaterial.uniforms.u_velocityTexture.value = nextVelocityTarget.texture;
1006
- renderComputePass(positionMaterial, nextPositionTarget);
1007
-
1008
- const oldPositionTarget = currentPositionTarget;
1009
- currentPositionTarget = nextPositionTarget;
1010
- nextPositionTarget = oldPositionTarget;
1011
-
1012
- const oldVelocityTarget = currentVelocityTarget;
1013
- currentVelocityTarget = nextVelocityTarget;
1014
- nextVelocityTarget = oldVelocityTarget;
1015
- }
1016
-
1017
- renderer.setRenderTarget(previousTarget);
1018
-
1019
- particleMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1020
- } catch (error) {
1021
- simulationReady = false;
1022
- if (!simulationWarningShown) {
1023
- simulationWarningShown = true;
1024
- console.warn('[BackgroundThemes] Galaxy particle simulation stopped.', error);
1025
- }
1026
- if (renderer.setRenderTarget) {
1027
- renderer.setRenderTarget(previousTarget);
1028
- }
1029
- }
1030
- }
1031
-
1032
- syncParticleView(camera);
1033
-
1034
- adaptiveGridOverlay.animate(camera);
1035
- };
1036
-
1037
- themeGroup.customDispose = () => {
1038
- themeGroup.traverse(child => {
1039
- if (child.geometry) child.geometry.dispose();
1040
- if (child.material) {
1041
- if (Array.isArray(child.material)) {
1042
- child.material.forEach(material => material.dispose());
1043
- } else {
1044
- child.material.dispose();
1045
- }
1046
- }
1047
- });
1048
- initialTextures.positionTexture.dispose();
1049
- initialTextures.velocityTexture.dispose();
1050
- positionTargetA.dispose();
1051
- positionTargetB.dispose();
1052
- velocityTargetA.dispose();
1053
- velocityTargetB.dispose();
1054
- copyMaterial.dispose();
1055
- positionMaterial.dispose();
1056
- velocityMaterial.dispose();
1057
- particleMaterial.dispose();
1058
- };
1059
-
1060
- return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.Continuous);
1061
- },
1062
- dispose: (themeObject) => { disposeThemeObject(themeObject); }
1063
- },
1064
-
1065
- // Theme 8: SpiritFlow (curl-noise particle flow inspired by ref/~spirit)
1066
- 'SpiritFlow': {
1067
- create: (scene, renderer) => {
1068
- scene.background = new THREE.Color(THEME_SURFACE_BG);
1069
-
1070
- const themeGroup = new THREE.Group();
1071
- const textureWidth = 256;
1072
- const textureHeight = 256;
1073
- const pointCount = textureWidth * textureHeight;
1074
- const resolution = new THREE.Vector2(textureWidth, textureHeight);
1075
- const renderTargetOptions = {
1076
- wrapS: THREE.ClampToEdgeWrapping,
1077
- wrapT: THREE.ClampToEdgeWrapping,
1078
- minFilter: THREE.NearestFilter,
1079
- magFilter: THREE.NearestFilter,
1080
- format: THREE.RGBAFormat,
1081
- type: THREE.FloatType,
1082
- depthBuffer: false,
1083
- stencilBuffer: false
1084
- };
1085
-
1086
- const createRenderTarget = () => {
1087
- const target = new THREE.WebGLRenderTarget(textureWidth, textureHeight, renderTargetOptions);
1088
- target.texture.generateMipmaps = false;
1089
- return target;
1090
- };
1091
-
1092
- const createDefaultPositionTexture = () => {
1093
- const data = new Float32Array(pointCount * 4);
1094
- for (let i = 0; i < pointCount; i++) {
1095
- const offset = i * 4;
1096
- const radius = (0.5 + Math.random() * 0.5) * 50;
1097
- const phi = (Math.random() - 0.5) * Math.PI;
1098
- const theta = Math.random() * Math.PI * 2;
1099
- data[offset] = radius * Math.cos(theta) * Math.cos(phi);
1100
- data[offset + 1] = radius * Math.sin(phi);
1101
- data[offset + 2] = radius * Math.sin(theta) * Math.cos(phi);
1102
- data[offset + 3] = Math.random();
1103
- }
1104
-
1105
- const texture = new THREE.DataTexture(data, textureWidth, textureHeight, THREE.RGBAFormat, THREE.FloatType);
1106
- texture.minFilter = THREE.NearestFilter;
1107
- texture.magFilter = THREE.NearestFilter;
1108
- texture.generateMipmaps = false;
1109
- texture.flipY = false;
1110
- texture.needsUpdate = true;
1111
- return texture;
1112
- };
1113
-
1114
- const computeVertexShader = `
1115
- void main() {
1116
- gl_Position = vec4(position.xy, 0.0, 1.0);
1117
- }
1118
- `;
1119
-
1120
- const copyMaterial = new THREE.ShaderMaterial({
1121
- uniforms: {
1122
- u_texture: { value: null },
1123
- u_resolution: { value: resolution }
1124
- },
1125
- vertexShader: computeVertexShader,
1126
- fragmentShader: `
1127
- precision highp float;
1128
- uniform sampler2D u_texture;
1129
- uniform vec2 u_resolution;
1130
-
1131
- void main() {
1132
- vec2 uv = gl_FragCoord.xy / u_resolution;
1133
- gl_FragColor = texture2D(u_texture, uv);
1134
- }
1135
- `,
1136
- depthTest: false,
1137
- depthWrite: false
1138
- });
1139
-
1140
- const positionMaterial = new THREE.ShaderMaterial({
1141
- uniforms: {
1142
- u_positionTexture: { value: null },
1143
- u_defaultTexture: { value: null },
1144
- u_resolution: { value: resolution },
1145
- u_mouse3d: { value: new THREE.Vector3() },
1146
- u_speed: { value: 0.81 },
1147
- u_dieSpeed: { value: 0 },
1148
- u_radius: { value: 0.6 },
1149
- u_curlSize: { value: 0.018 },
1150
- u_attraction: { value: 0.87 },
1151
- u_time: { value: 0 },
1152
- u_initAnimation: { value: 0 }
1153
- },
1154
- vertexShader: computeVertexShader,
1155
- fragmentShader: `
1156
- precision highp float;
1157
-
1158
- uniform sampler2D u_positionTexture;
1159
- uniform sampler2D u_defaultTexture;
1160
- uniform vec2 u_resolution;
1161
- uniform vec3 u_mouse3d;
1162
- uniform float u_speed;
1163
- uniform float u_dieSpeed;
1164
- uniform float u_radius;
1165
- uniform float u_curlSize;
1166
- uniform float u_attraction;
1167
- uniform float u_time;
1168
- uniform float u_initAnimation;
1169
-
1170
- vec4 mod289(vec4 x) {
1171
- return x - floor(x * (1.0 / 289.0)) * 289.0;
1172
- }
1173
-
1174
- float mod289(float x) {
1175
- return x - floor(x * (1.0 / 289.0)) * 289.0;
1176
- }
1177
-
1178
- vec4 permute(vec4 x) {
1179
- return mod289(((x * 34.0) + 1.0) * x);
1180
- }
1181
-
1182
- float permute(float x) {
1183
- return mod289(((x * 34.0) + 1.0) * x);
1184
- }
1185
-
1186
- vec4 taylorInvSqrt(vec4 r) {
1187
- return 1.79284291400159 - 0.85373472095314 * r;
1188
- }
1189
-
1190
- float taylorInvSqrt(float r) {
1191
- return 1.79284291400159 - 0.85373472095314 * r;
1192
- }
1193
-
1194
- vec4 grad4(float j, vec4 ip) {
1195
- const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
1196
- vec4 p;
1197
- vec4 s;
1198
-
1199
- p.xyz = floor(fract(vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
1200
- p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
1201
- s = vec4(
1202
- p.x < 0.0 ? 1.0 : 0.0,
1203
- p.y < 0.0 ? 1.0 : 0.0,
1204
- p.z < 0.0 ? 1.0 : 0.0,
1205
- p.w < 0.0 ? 1.0 : 0.0
1206
- );
1207
- p.xyz = p.xyz + (s.xyz * 2.0 - 1.0) * s.www;
1208
-
1209
- return p;
1210
- }
1211
-
1212
- #define F4 0.309016994374947451
1213
-
1214
- vec4 simplexNoiseDerivatives(vec4 v) {
1215
- const vec4 C = vec4(0.138196601125011, 0.276393202250021, 0.414589803375032, -0.447213595499958);
1216
-
1217
- vec4 i = floor(v + dot(v, vec4(F4)));
1218
- vec4 x0 = v - i + dot(i, C.xxxx);
1219
-
1220
- vec4 i0;
1221
- vec3 isX = step(x0.yzw, x0.xxx);
1222
- vec3 isYZ = step(x0.zww, x0.yyz);
1223
- i0.x = isX.x + isX.y + isX.z;
1224
- i0.yzw = 1.0 - isX;
1225
- i0.y += isYZ.x + isYZ.y;
1226
- i0.zw += 1.0 - isYZ.xy;
1227
- i0.z += isYZ.z;
1228
- i0.w += 1.0 - isYZ.z;
1229
-
1230
- vec4 i3 = clamp(i0, 0.0, 1.0);
1231
- vec4 i2 = clamp(i0 - 1.0, 0.0, 1.0);
1232
- vec4 i1 = clamp(i0 - 2.0, 0.0, 1.0);
1233
-
1234
- vec4 x1 = x0 - i1 + C.xxxx;
1235
- vec4 x2 = x0 - i2 + C.yyyy;
1236
- vec4 x3 = x0 - i3 + C.zzzz;
1237
- vec4 x4 = x0 + C.wwww;
1238
-
1239
- i = mod289(i);
1240
- float j0 = permute(permute(permute(permute(i.w) + i.z) + i.y) + i.x);
1241
- vec4 j1 = permute(permute(permute(permute(
1242
- i.w + vec4(i1.w, i2.w, i3.w, 1.0))
1243
- + i.z + vec4(i1.z, i2.z, i3.z, 1.0))
1244
- + i.y + vec4(i1.y, i2.y, i3.y, 1.0))
1245
- + i.x + vec4(i1.x, i2.x, i3.x, 1.0));
1246
-
1247
- vec4 ip = vec4(1.0 / 294.0, 1.0 / 49.0, 1.0 / 7.0, 0.0);
1248
-
1249
- vec4 p0 = grad4(j0, ip);
1250
- vec4 p1 = grad4(j1.x, ip);
1251
- vec4 p2 = grad4(j1.y, ip);
1252
- vec4 p3 = grad4(j1.z, ip);
1253
- vec4 p4 = grad4(j1.w, ip);
1254
-
1255
- vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
1256
- p0 *= norm.x;
1257
- p1 *= norm.y;
1258
- p2 *= norm.z;
1259
- p3 *= norm.w;
1260
- p4 *= taylorInvSqrt(dot(p4, p4));
1261
-
1262
- vec3 values0 = vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2));
1263
- vec2 values1 = vec2(dot(p3, x3), dot(p4, x4));
1264
-
1265
- vec3 m0 = max(0.5 - vec3(dot(x0, x0), dot(x1, x1), dot(x2, x2)), 0.0);
1266
- vec2 m1 = max(0.5 - vec2(dot(x3, x3), dot(x4, x4)), 0.0);
1267
-
1268
- vec3 temp0 = -6.0 * m0 * m0 * values0;
1269
- vec2 temp1 = -6.0 * m1 * m1 * values1;
1270
-
1271
- vec3 mmm0 = m0 * m0 * m0;
1272
- vec2 mmm1 = m1 * m1 * m1;
1273
-
1274
- float dx = temp0[0] * x0.x + temp0[1] * x1.x + temp0[2] * x2.x + temp1[0] * x3.x + temp1[1] * x4.x + mmm0[0] * p0.x + mmm0[1] * p1.x + mmm0[2] * p2.x + mmm1[0] * p3.x + mmm1[1] * p4.x;
1275
- float dy = temp0[0] * x0.y + temp0[1] * x1.y + temp0[2] * x2.y + temp1[0] * x3.y + temp1[1] * x4.y + mmm0[0] * p0.y + mmm0[1] * p1.y + mmm0[2] * p2.y + mmm1[0] * p3.y + mmm1[1] * p4.y;
1276
- float dz = temp0[0] * x0.z + temp0[1] * x1.z + temp0[2] * x2.z + temp1[0] * x3.z + temp1[1] * x4.z + mmm0[0] * p0.z + mmm0[1] * p1.z + mmm0[2] * p2.z + mmm1[0] * p3.z + mmm1[1] * p4.z;
1277
- float dw = temp0[0] * x0.w + temp0[1] * x1.w + temp0[2] * x2.w + temp1[0] * x3.w + temp1[1] * x4.w + mmm0[0] * p0.w + mmm0[1] * p1.w + mmm0[2] * p2.w + mmm1[0] * p3.w + mmm1[1] * p4.w;
1278
-
1279
- return vec4(dx, dy, dz, dw) * 49.0;
1280
- }
1281
-
1282
- vec3 curl(vec3 p, float noiseTime, float persistence) {
1283
- vec4 xNoisePotentialDerivatives = vec4(0.0);
1284
- vec4 yNoisePotentialDerivatives = vec4(0.0);
1285
- vec4 zNoisePotentialDerivatives = vec4(0.0);
1286
-
1287
- for (int i = 0; i < 3; ++i) {
1288
- float twoPowI = pow(2.0, float(i));
1289
- float scale = 0.5 * twoPowI * pow(persistence, float(i));
1290
-
1291
- xNoisePotentialDerivatives += simplexNoiseDerivatives(vec4(p * twoPowI, noiseTime)) * scale;
1292
- yNoisePotentialDerivatives += simplexNoiseDerivatives(vec4((p + vec3(123.4, 129845.6, -1239.1)) * twoPowI, noiseTime)) * scale;
1293
- zNoisePotentialDerivatives += simplexNoiseDerivatives(vec4((p + vec3(-9519.0, 9051.0, -123.0)) * twoPowI, noiseTime)) * scale;
1294
- }
1295
-
1296
- return vec3(
1297
- zNoisePotentialDerivatives[1] - yNoisePotentialDerivatives[2],
1298
- xNoisePotentialDerivatives[2] - zNoisePotentialDerivatives[0],
1299
- yNoisePotentialDerivatives[0] - xNoisePotentialDerivatives[1]
1300
- );
1301
- }
1302
-
1303
- void main() {
1304
- vec2 uv = gl_FragCoord.xy / u_resolution;
1305
- vec4 positionInfo = texture2D(u_positionTexture, uv);
1306
- vec4 defaultInfo = texture2D(u_defaultTexture, uv);
1307
- vec3 position = mix(vec3(0.0, -200.0, 0.0), positionInfo.xyz, smoothstep(0.0, 0.3, u_initAnimation));
1308
- float life = positionInfo.a - u_dieSpeed;
1309
-
1310
- vec3 followPosition = mix(
1311
- vec3(0.0, -(1.0 - u_initAnimation) * 200.0, 0.0),
1312
- u_mouse3d,
1313
- smoothstep(0.2, 0.7, u_initAnimation)
1314
- );
1315
-
1316
- if (life < 0.0) {
1317
- position = defaultInfo.xyz * (1.0 + sin(u_time * 15.0) * 0.2 + (1.0 - u_initAnimation)) * 0.4 * u_radius;
1318
- position += followPosition;
1319
- life = 0.5 + fract(defaultInfo.w * 21.4131 + u_time);
1320
- } else {
1321
- vec3 delta = followPosition - position;
1322
- float distanceToFollow = length(delta);
1323
- position += delta * (0.005 + life * 0.01) * u_attraction * (1.0 - smoothstep(50.0, 350.0, distanceToFollow)) * u_speed;
1324
- position += curl(position * u_curlSize, u_time, 0.1 + (1.0 - life) * 0.1) * u_speed;
1325
- }
1326
-
1327
- gl_FragColor = vec4(position, life);
1328
- }
1329
- `,
1330
- depthTest: false,
1331
- depthWrite: false,
1332
- blending: THREE.NoBlending
1333
- });
1334
-
1335
- const computeScene = new THREE.Scene();
1336
- const computeCamera = new THREE.Camera();
1337
- const computeQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMaterial);
1338
- computeQuad.frustumCulled = false;
1339
- computeScene.add(computeQuad);
1340
-
1341
- const defaultTexture = createDefaultPositionTexture();
1342
- let positionTargetA = createRenderTarget();
1343
- let positionTargetB = createRenderTarget();
1344
- let currentPositionTarget = positionTargetA;
1345
- let nextPositionTarget = positionTargetB;
1346
- let simulationReady = renderer && typeof renderer.setRenderTarget === 'function';
1347
- let simulationWarningShown = false;
1348
-
1349
- const renderComputePass = (material, target) => {
1350
- computeQuad.material = material;
1351
- renderer.setRenderTarget(target);
1352
- renderer.render(computeScene, computeCamera);
1353
- };
1354
-
1355
- const copyTextureToTarget = (texture, target) => {
1356
- copyMaterial.uniforms.u_texture.value = texture;
1357
- renderComputePass(copyMaterial, target);
1358
- };
1359
-
1360
- if (simulationReady) {
1361
- const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
1362
- try {
1363
- copyTextureToTarget(defaultTexture, currentPositionTarget);
1364
- copyTextureToTarget(defaultTexture, nextPositionTarget);
1365
- renderer.setRenderTarget(previousTarget);
1366
- } catch (error) {
1367
- simulationReady = false;
1368
- console.warn('[BackgroundThemes] Spirit particle simulation disabled.', error);
1369
- if (renderer.setRenderTarget) {
1370
- renderer.setRenderTarget(previousTarget);
1371
- }
1372
- }
1373
- }
1374
-
1375
- const particleGeometry = new THREE.BufferGeometry();
1376
- const particleUvs = new Float32Array(pointCount * 3);
1377
- for (let i = 0; i < pointCount; i++) {
1378
- const offset = i * 3;
1379
- particleUvs[offset] = (i % textureWidth + 0.5) / textureWidth;
1380
- particleUvs[offset + 1] = (Math.floor(i / textureWidth) + 0.5) / textureHeight;
1381
- particleUvs[offset + 2] = 0;
1382
- }
1383
- particleGeometry.setAttribute('position', new THREE.BufferAttribute(particleUvs, 3));
1384
-
1385
- const backdropMaterial = new THREE.ShaderMaterial({
1386
- vertexShader: `
1387
- varying vec2 vUv;
1388
-
1389
- void main() {
1390
- vUv = uv;
1391
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1392
- }
1393
- `,
1394
- fragmentShader: `
1395
- precision mediump float;
1396
- varying vec2 vUv;
1397
-
1398
- void main() {
1399
- vec3 topColor = vec3(0.74, 0.76, 0.75);
1400
- vec3 horizonColor = vec3(0.88, 0.89, 0.86);
1401
- vec3 bottomColor = vec3(0.60, 0.62, 0.61);
1402
- float horizon = 1.0 - smoothstep(0.20, 0.64, abs(vUv.y - 0.53));
1403
- vec3 vertical = mix(bottomColor, topColor, smoothstep(0.0, 1.0, vUv.y));
1404
- vec3 color = mix(vertical, horizonColor, horizon * 0.78);
1405
- gl_FragColor = vec4(color, 1.0);
1406
- }
1407
- `,
1408
- depthTest: false,
1409
- depthWrite: false
1410
- });
1411
- const backdrop = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), backdropMaterial);
1412
- backdrop.frustumCulled = false;
1413
- backdrop.renderOrder = -90;
1414
- themeGroup.add(backdrop);
1415
-
1416
- const particleMaterial = new THREE.ShaderMaterial({
1417
- uniforms: {
1418
- u_positionTexture: { value: simulationReady ? currentPositionTarget.texture : defaultTexture },
1419
- u_particleSize: { value: 42000 },
1420
- u_worldScale: { value: 120 },
1421
- u_depthScale: { value: 85 },
1422
- u_backgroundZ: { value: -26000 },
1423
- u_color: { value: new THREE.Color(0xffffff) }
1424
- },
1425
- vertexShader: `
1426
- uniform sampler2D u_positionTexture;
1427
- uniform float u_particleSize;
1428
- uniform float u_worldScale;
1429
- uniform float u_depthScale;
1430
- uniform float u_backgroundZ;
1431
- varying float vLife;
1432
- varying float vAlpha;
1433
- varying float vNearness;
1434
- varying float vShade;
1435
-
1436
- void main() {
1437
- vec4 positionInfo = texture2D(u_positionTexture, position.xy);
1438
- float life = clamp(positionInfo.w, 0.0, 1.0);
1439
- vec3 flowPosition = positionInfo.xyz;
1440
- float sizeSeed = fract(sin(dot(position.xy, vec2(127.1, 311.7))) * 43758.5453);
1441
-
1442
- vec3 simPosition = vec3(
1443
- flowPosition.x * u_worldScale,
1444
- flowPosition.y * u_worldScale,
1445
- u_backgroundZ + flowPosition.z * u_depthScale
1446
- );
1447
-
1448
- vec4 mvPosition = modelViewMatrix * vec4(simPosition, 1.0);
1449
- float viewDepth = max(-mvPosition.z, 1.0);
1450
- float fadeOut = smoothstep(0.0, 0.28, life);
1451
- float seededSize = mix(0.62, 2.15, pow(sizeSeed, 1.35));
1452
- float nearness = 1.0 - clamp((viewDepth - 9000.0) / 28000.0, 0.0, 1.0);
1453
- float depthBoost = mix(0.78, 1.62, nearness);
1454
- gl_PointSize = clamp((u_particleSize / viewDepth) * seededSize * depthBoost * fadeOut, 0.12, 9.6);
1455
- gl_Position = projectionMatrix * mvPosition;
1456
- vLife = life;
1457
- vAlpha = fadeOut;
1458
- vNearness = nearness;
1459
- float upperLight = smoothstep(-20.0, 115.0, flowPosition.y);
1460
- float frontLight = smoothstep(-90.0, 85.0, flowPosition.z);
1461
- vShade = mix(0.46, 1.18, clamp(upperLight * 0.72 + frontLight * 0.35, 0.0, 1.0));
1462
- }
1463
- `,
1464
- fragmentShader: `
1465
- precision mediump float;
1466
- uniform vec3 u_color;
1467
- varying float vLife;
1468
- varying float vAlpha;
1469
- varying float vNearness;
1470
- varying float vShade;
1471
-
1472
- void main() {
1473
- vec2 uv = gl_PointCoord - vec2(0.5);
1474
- float dist = length(uv);
1475
- float core = 1.0 - smoothstep(0.16, mix(0.44, 0.5, vNearness), dist);
1476
- float halo = (1.0 - smoothstep(0.3, 0.5, dist)) * vNearness * 0.22;
1477
- float particleAlpha = (core + halo) * vAlpha;
1478
- if (particleAlpha < 0.01) discard;
1479
- vec3 outgoingLight = mix(u_color * 0.58, u_color, smoothstep(0.0, 0.7, vLife));
1480
- gl_FragColor = vec4(outgoingLight * vShade, min(particleAlpha, 0.96));
1481
- }
1482
- `,
1483
- transparent: true,
1484
- depthTest: true,
1485
- depthWrite: false,
1486
- blending: THREE.NormalBlending
1487
- });
1488
-
1489
- const particles = new THREE.Points(particleGeometry, particleMaterial);
1490
- particles.frustumCulled = false;
1491
- particles.renderOrder = -40;
1492
- themeGroup.add(particles);
1493
-
1494
- const themeStartTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001;
1495
- let lastElapsedTime = 0;
1496
- let followPointTime = 0;
1497
- const followPoint = new THREE.Vector3(0, 0, 0);
1498
- const autoFollowPoint = new THREE.Vector3(0, 0, 0);
1499
- const spiritMotion = Object.freeze({
1500
- followTimeScale: 0.79,
1501
- followBlend: 0.16,
1502
- followX: 182,
1503
- followY: 49,
1504
- followZ: 132,
1505
- depthTravelScale: 0.67,
1506
- particleSpeed: 0.81,
1507
- lifeDecay: 0.013
1508
- });
1509
-
1510
- const syncParticleView = (camera, elapsedTime) => {
1511
- if (!camera) return;
1512
-
1513
- const cameraZ = Math.max(600, Math.abs(Number(camera.position?.z || 1200)));
1514
- const cameraFar = Math.max(120000, Number(camera.far || 500000));
1515
- const clipSafeDepth = Math.max(14000, cameraFar - cameraZ - 5000);
1516
- const backgroundDepth = Math.min(Math.max(26000, cameraZ * 6.0), clipSafeDepth * 0.82);
1517
- const viewDistance = cameraZ + backgroundDepth;
1518
- const fovRadians = THREE.MathUtils?.degToRad
1519
- ? THREE.MathUtils.degToRad(Number(camera.fov || 45))
1520
- : Number(camera.fov || 45) * Math.PI / 180;
1521
- const viewportHeight = Math.max(1, Math.tan(fovRadians * 0.5) * viewDistance * 2);
1522
- const viewportWidth = viewportHeight * Math.max(0.1, Number(camera.aspect || 1));
1523
- particleMaterial.uniforms.u_worldScale.value = Math.max(105, backgroundDepth * 0.0043);
1524
- particleMaterial.uniforms.u_depthScale.value = Math.max(56, backgroundDepth * 0.0037 * spiritMotion.depthTravelScale);
1525
- particleMaterial.uniforms.u_backgroundZ.value = -backgroundDepth;
1526
- particleMaterial.uniforms.u_particleSize.value = Math.max(44000, viewDistance * 1.92);
1527
- particles.rotation.z = Math.sin(elapsedTime * 0.08) * 0.08;
1528
- particles.rotation.x = Math.sin(elapsedTime * 0.11) * 0.05;
1529
- particles.position.x = Number(camera.position?.x || 0);
1530
- particles.position.y = Number(camera.position?.y || 0);
1531
- particles.updateMatrixWorld(true);
1532
- backdrop.position.x = Number(camera.position?.x || 0);
1533
- backdrop.position.y = Number(camera.position?.y || 0);
1534
- backdrop.position.z = -backgroundDepth - 1200;
1535
- backdrop.scale.set(viewportWidth * 1.12, viewportHeight * 1.12, 1);
1536
- backdrop.updateMatrixWorld(true);
1537
- };
1538
-
1539
- particles.onBeforeRender = (_renderer, _scene, camera) => {
1540
- const elapsedTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001 - themeStartTime;
1541
- syncParticleView(camera, elapsedTime);
1542
- };
1543
-
1544
- themeGroup.animate = (camera) => {
1545
- if (!camera) return;
1546
-
1547
- const elapsedTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001 - themeStartTime;
1548
- const deltaMs = Math.min(48, Math.max(0, (elapsedTime - lastElapsedTime) * 1000 || 16.6667));
1549
- const deltaRatio = deltaMs / 16.6667;
1550
- lastElapsedTime = elapsedTime;
1551
-
1552
- followPointTime += deltaMs * 0.001 * spiritMotion.followTimeScale;
1553
- autoFollowPoint.set(
1554
- Math.cos(followPointTime * 0.9) * spiritMotion.followX,
1555
- Math.cos(followPointTime * 2.8) * spiritMotion.followY,
1556
- Math.sin(followPointTime * 1.55) * spiritMotion.followZ
1557
- );
1558
- followPoint.lerp(autoFollowPoint, Math.min(1.0, spiritMotion.followBlend * deltaRatio));
1559
-
1560
- if (simulationReady) {
1561
- const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
1562
- try {
1563
- positionMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1564
- positionMaterial.uniforms.u_defaultTexture.value = defaultTexture;
1565
- positionMaterial.uniforms.u_mouse3d.value.copy(followPoint);
1566
- positionMaterial.uniforms.u_speed.value = spiritMotion.particleSpeed * deltaRatio;
1567
- positionMaterial.uniforms.u_dieSpeed.value = spiritMotion.lifeDecay * deltaRatio;
1568
- positionMaterial.uniforms.u_time.value = elapsedTime;
1569
- positionMaterial.uniforms.u_initAnimation.value = Math.min(1, elapsedTime * 0.25);
1570
- renderComputePass(positionMaterial, nextPositionTarget);
1571
-
1572
- const oldPositionTarget = currentPositionTarget;
1573
- currentPositionTarget = nextPositionTarget;
1574
- nextPositionTarget = oldPositionTarget;
1575
- renderer.setRenderTarget(previousTarget);
1576
-
1577
- particleMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1578
- } catch (error) {
1579
- simulationReady = false;
1580
- if (!simulationWarningShown) {
1581
- simulationWarningShown = true;
1582
- console.warn('[BackgroundThemes] Spirit particle simulation stopped.', error);
1583
- }
1584
- if (renderer.setRenderTarget) {
1585
- renderer.setRenderTarget(previousTarget);
1586
- }
1587
- }
1588
- }
1589
-
1590
- syncParticleView(camera, elapsedTime);
1591
- };
1592
-
1593
- themeGroup.customDispose = () => {
1594
- themeGroup.traverse(child => {
1595
- if (child.geometry) child.geometry.dispose();
1596
- if (child.material) {
1597
- if (Array.isArray(child.material)) {
1598
- child.material.forEach(material => material.dispose());
1599
- } else {
1600
- child.material.dispose();
1601
- }
1602
- }
1603
- });
1604
- defaultTexture.dispose();
1605
- positionTargetA.dispose();
1606
- positionTargetB.dispose();
1607
- copyMaterial.dispose();
1608
- positionMaterial.dispose();
1609
- particleMaterial.dispose();
1610
- backdropMaterial.dispose();
1611
- };
1612
-
1613
- return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.Continuous);
1614
- },
1615
- dispose: (themeObject) => { disposeThemeObject(themeObject); }
1616
- },
1617
-
1618
- // Theme 9: None (no background)
1619
- 'None': { create: (scene) => { scene.background = new THREE.Color(THEME_SURFACE_BG); return null; }, dispose: (themeObject) => { } }
1620
- };
485
+ // Theme 6: XYGrid (distance-based auto LOD grid)
486
+ 'XYGrid': {
487
+ create: (scene) => {
488
+ return createLocalSnapGridTheme(scene, {
489
+ themeAlias: 'XYGrid',
490
+ localSnapGridStyle: 'line',
491
+ backgroundColor: THEME_PLAIN_BG
492
+ });
493
+ },
494
+ dispose: (themeObject) => { disposeThemeObject(themeObject); }
495
+ },
496
+
497
+ // Theme 7: Core Particles (inverted galaxy shader particles + faint XYGrid overlay)
498
+ 'Space3D': {
499
+ create: (scene, renderer) => {
500
+ scene.background = new THREE.Color(THEME_PLAIN_BG);
501
+
502
+ const themeGroup = new THREE.Group();
503
+ const simSize = 64;
504
+ const pointCount = simSize * simSize;
505
+ const resolution = new THREE.Vector2(simSize, simSize);
506
+ const galaxyCount = 2;
507
+ const galaxyMass = 0.000075;
508
+ const galaxyHaloSize = 6;
509
+ const renderTargetOptions = {
510
+ wrapS: THREE.ClampToEdgeWrapping,
511
+ wrapT: THREE.ClampToEdgeWrapping,
512
+ minFilter: THREE.NearestFilter,
513
+ magFilter: THREE.NearestFilter,
514
+ format: THREE.RGBAFormat,
515
+ type: THREE.FloatType,
516
+ depthBuffer: false,
517
+ stencilBuffer: false
518
+ };
519
+
520
+ const createRenderTarget = () => {
521
+ const target = new THREE.WebGLRenderTarget(simSize, simSize, renderTargetOptions);
522
+ target.texture.generateMipmaps = false;
523
+ return target;
524
+ };
525
+
526
+ const createInitialTextures = () => {
527
+ const positionData = new Float32Array(pointCount * 4);
528
+ const velocityData = new Float32Array(pointCount * 4);
529
+ const randomSigned = () => Math.random() * 2 - 1;
530
+ const particleCountWithoutCenters = pointCount - galaxyCount;
531
+ const firstGalaxyParticleCount = Math.round(particleCountWithoutCenters / 2);
532
+ const galaxyParticleCounts = [
533
+ firstGalaxyParticleCount,
534
+ particleCountWithoutCenters - firstGalaxyParticleCount
535
+ ];
536
+ const galaxySizes = [0.78 * galaxyHaloSize, 0.84 * galaxyHaloSize];
537
+ const galaxyInclinations = [0.35 * Math.PI, -0.28 * Math.PI];
538
+ const galaxyPositions = [
539
+ new THREE.Vector3(-1.2 * galaxyHaloSize, -0.1 * galaxyHaloSize, 0),
540
+ new THREE.Vector3(1.2 * galaxyHaloSize, 0.1 * galaxyHaloSize, 0)
541
+ ];
542
+ const galaxyVelocities = [
543
+ new THREE.Vector3(0.00145, 0.00014, 0.0018),
544
+ new THREE.Vector3(-0.00145, -0.00014, -0.0018)
545
+ ];
546
+
547
+ const writeParticle = (particleIndex, position, velocity) => {
548
+ const offset = particleIndex * 4;
549
+ positionData[offset] = position.x;
550
+ positionData[offset + 1] = position.y;
551
+ positionData[offset + 2] = position.z;
552
+ positionData[offset + 3] = 1;
553
+ velocityData[offset] = velocity.x;
554
+ velocityData[offset + 1] = velocity.y;
555
+ velocityData[offset + 2] = velocity.z;
556
+ velocityData[offset + 3] = 1;
557
+ };
558
+
559
+ for (let i = 0; i < galaxyCount; i++) {
560
+ writeParticle(i, galaxyPositions[i], galaxyVelocities[i]);
561
+ }
562
+
563
+ let startIndex = galaxyCount;
564
+ for (let i = 0; i < galaxyCount; i++) {
565
+ const center = galaxyPositions[i];
566
+ const centerVelocity = galaxyVelocities[i];
567
+ const sinInclination = Math.sin(galaxyInclinations[i]);
568
+ const cosInclination = Math.cos(galaxyInclinations[i]);
569
+
570
+ for (let j = startIndex; j < startIndex + galaxyParticleCounts[i]; j++) {
571
+ const diffuseSeed = Math.random();
572
+ const position = new THREE.Vector3();
573
+ const velocity = new THREE.Vector3();
574
+
575
+ if (diffuseSeed < 0.16) {
576
+ const radius = galaxySizes[i] * (0.72 + Math.pow(Math.random(), 0.55) * 0.62);
577
+ const angle = 2 * Math.PI * Math.random();
578
+
579
+ position.set(
580
+ center.x + Math.cos(angle) * radius + randomSigned() * 0.75,
581
+ center.y + cosInclination * Math.sin(angle) * radius * 0.64 + randomSigned() * 0.72,
582
+ center.z - sinInclination * Math.sin(angle) * radius * 0.64 + randomSigned() * 0.95
583
+ );
584
+ velocity.set(
585
+ -Math.sin(angle) * 0.0034 + centerVelocity.x * 0.66,
586
+ cosInclination * Math.cos(angle) * 0.0021 + centerVelocity.y * 0.66,
587
+ -sinInclination * Math.cos(angle) * 0.0021 + randomSigned() * 0.0009 + centerVelocity.z * 0.66
588
+ );
589
+ } else {
590
+ const distance = galaxySizes[i] * (0.18 + Math.pow(Math.random(), 0.7) * 0.82);
591
+ const safeDistance = Math.max(distance, 0.08);
592
+ const angle = 2 * Math.PI * Math.random();
593
+ const massAtPosition = galaxyMass * Math.min(safeDistance, galaxyHaloSize) / galaxyHaloSize;
594
+ const speed = Math.sqrt(massAtPosition / safeDistance);
595
+ const x = distance * Math.cos(angle);
596
+ const y = distance * Math.sin(angle);
597
+ const vx = -speed * Math.sin(angle) * 1.75;
598
+ const vy = speed * Math.cos(angle) * 1.75;
599
+
600
+ position.set(
601
+ center.x + x,
602
+ center.y + cosInclination * y,
603
+ center.z - sinInclination * y
604
+ );
605
+ velocity.set(
606
+ centerVelocity.x + vx,
607
+ centerVelocity.y + cosInclination * vy,
608
+ centerVelocity.z - sinInclination * vy
609
+ );
610
+
611
+ const jitter = 0.14 + Math.pow(Math.random(), 2.0) * 0.62;
612
+ position.x += randomSigned() * jitter;
613
+ position.y += randomSigned() * jitter * 0.42;
614
+ position.z += randomSigned() * jitter * 0.46;
615
+
616
+ if (Math.random() < 0.16) {
617
+ const direction = i === 0 ? -1 : 1;
618
+ position.x += direction * (0.8 + Math.random() * 2.5);
619
+ position.y += randomSigned() * 0.72;
620
+ position.z += randomSigned() * 0.82;
621
+ velocity.x += direction * (0.00022 + Math.random() * 0.00035);
622
+ velocity.y += randomSigned() * 0.00018;
623
+ velocity.z += randomSigned() * 0.00022;
624
+ }
625
+ }
626
+
627
+ writeParticle(j, position, velocity);
628
+ }
629
+
630
+ startIndex += galaxyParticleCounts[i];
631
+ }
632
+
633
+ const positionTexture = new THREE.DataTexture(positionData, simSize, simSize, THREE.RGBAFormat, THREE.FloatType);
634
+ const velocityTexture = new THREE.DataTexture(velocityData, simSize, simSize, THREE.RGBAFormat, THREE.FloatType);
635
+ positionTexture.needsUpdate = true;
636
+ velocityTexture.needsUpdate = true;
637
+
638
+ return { positionTexture, velocityTexture };
639
+ };
640
+
641
+ const computeVertexShader = `
642
+ void main() {
643
+ gl_Position = vec4(position.xy, 0.0, 1.0);
644
+ }
645
+ `;
646
+
647
+ const copyMaterial = new THREE.ShaderMaterial({
648
+ uniforms: {
649
+ u_texture: { value: null },
650
+ u_resolution: { value: resolution }
651
+ },
652
+ vertexShader: computeVertexShader,
653
+ fragmentShader: `
654
+ precision highp float;
655
+ uniform sampler2D u_texture;
656
+ uniform vec2 u_resolution;
657
+
658
+ void main() {
659
+ vec2 uv = gl_FragCoord.xy / u_resolution;
660
+ gl_FragColor = texture2D(u_texture, uv);
661
+ }
662
+ `,
663
+ depthTest: false,
664
+ depthWrite: false
665
+ });
666
+
667
+ const positionMaterial = new THREE.ShaderMaterial({
668
+ uniforms: {
669
+ u_positionTexture: { value: null },
670
+ u_velocityTexture: { value: null },
671
+ u_resolution: { value: resolution },
672
+ u_dt: { value: 0.18 }
673
+ },
674
+ vertexShader: computeVertexShader,
675
+ fragmentShader: `
676
+ precision highp float;
677
+ uniform sampler2D u_positionTexture;
678
+ uniform sampler2D u_velocityTexture;
679
+ uniform vec2 u_resolution;
680
+ uniform float u_dt;
681
+
682
+ void main() {
683
+ vec2 uv = gl_FragCoord.xy / u_resolution;
684
+ vec3 position = texture2D(u_positionTexture, uv).xyz;
685
+ vec3 velocity = texture2D(u_velocityTexture, uv).xyz;
686
+ float speedSeed = fract(sin(dot(uv, vec2(127.1, 311.7))) * 43758.5453);
687
+ float particleDt = u_dt * mix(0.82, 1.22, speedSeed);
688
+ gl_FragColor = vec4(position + particleDt * velocity, 1.0);
689
+ }
690
+ `,
691
+ depthTest: false,
692
+ depthWrite: false
693
+ });
694
+
695
+ const velocityMaterial = new THREE.ShaderMaterial({
696
+ uniforms: {
697
+ u_positionTexture: { value: null },
698
+ u_velocityTexture: { value: null },
699
+ u_resolution: { value: resolution },
700
+ u_dt: { value: 0.18 },
701
+ u_mass: { value: galaxyMass },
702
+ u_haloSize: { value: galaxyHaloSize },
703
+ u_softening: { value: 1.7 },
704
+ u_damping: { value: 0.991 },
705
+ u_maxSpeed: { value: 0.038 },
706
+ u_velocityDeadzone: { value: 0.0026 },
707
+ u_settleDamping: { value: 0.78 }
708
+ },
709
+ vertexShader: computeVertexShader,
710
+ fragmentShader: `
711
+ precision highp float;
712
+ #define GALAXY_COUNT 2
713
+
714
+ uniform sampler2D u_positionTexture;
715
+ uniform sampler2D u_velocityTexture;
716
+ uniform vec2 u_resolution;
717
+ uniform float u_dt;
718
+ uniform float u_mass;
719
+ uniform float u_haloSize;
720
+ uniform float u_softening;
721
+ uniform float u_damping;
722
+ uniform float u_maxSpeed;
723
+ uniform float u_velocityDeadzone;
724
+ uniform float u_settleDamping;
725
+
726
+ void main() {
727
+ vec2 uv = gl_FragCoord.xy / u_resolution;
728
+ vec3 position = texture2D(u_positionTexture, uv).xyz;
729
+ vec3 velocity = texture2D(u_velocityTexture, uv).xyz;
730
+ vec3 totalForce = vec3(0.0);
731
+ vec3 centerSum = vec3(0.0);
732
+ vec3 nearestCenter = vec3(0.0);
733
+ float nearestDistance = 99999.0;
734
+
735
+ for (int i = 0; i < GALAXY_COUNT; i++) {
736
+ float particleIndex = float(i);
737
+ vec2 particleUv = vec2(
738
+ mod(particleIndex, u_resolution.x) + 0.5,
739
+ floor(particleIndex / u_resolution.x) + 0.5
740
+ ) / u_resolution;
741
+ vec3 particlePosition = texture2D(u_positionTexture, particleUv).xyz;
742
+ vec3 forceDirection = particlePosition - position;
743
+ float distance = length(forceDirection);
744
+ centerSum += particlePosition;
745
+
746
+ if (distance > 0.0001) {
747
+ vec3 directionToCenter = forceDirection / distance;
748
+ float nearFade = smoothstep(u_haloSize * 0.45, u_haloSize * 1.05, distance);
749
+ float massAtPosition = u_mass * nearFade * min(distance, u_haloSize) / u_haloSize;
750
+ float coreRepulsion = 1.0 - smoothstep(u_haloSize * 0.42, u_haloSize * 0.88, distance);
751
+ totalForce += massAtPosition * directionToCenter / pow(distance + u_softening, 2.0);
752
+ totalForce -= directionToCenter * coreRepulsion * u_mass * 0.36 / (distance + u_softening);
753
+ }
754
+
755
+ if (distance < nearestDistance) {
756
+ nearestDistance = distance;
757
+ nearestCenter = particlePosition;
758
+ }
759
+ }
760
+
761
+ vec3 barycenter = centerSum / float(GALAXY_COUNT);
762
+ vec3 fromBarycenter = position - barycenter;
763
+ float baryDistance = length(fromBarycenter);
764
+ if (baryDistance > 0.0001) {
765
+ vec3 outwardDirection = fromBarycenter / baryDistance;
766
+ float compression = 1.0 - smoothstep(u_haloSize * 0.65, u_haloSize * 1.75, baryDistance);
767
+ vec3 orbitDirection = vec3(-outwardDirection.y, outwardDirection.x, 0.0);
768
+ float orbitLength = length(orbitDirection);
769
+ totalForce += outwardDirection * compression * u_mass * 0.42 / (baryDistance + 0.75);
770
+ if (orbitLength > 0.0001) {
771
+ totalForce += (orbitDirection / orbitLength) * compression * u_mass * 0.28;
772
+ }
773
+ }
774
+
775
+ vec3 nextVelocity = (velocity + u_dt * totalForce) * u_damping;
776
+ if (nearestDistance > 0.0001 && nearestDistance < u_haloSize * 0.72) {
777
+ vec3 awayFromCore = (position - nearestCenter) / nearestDistance;
778
+ float corePressure = (u_haloSize * 0.72 - nearestDistance) / (u_haloSize * 0.72);
779
+ nextVelocity += awayFromCore * corePressure * 0.0018;
780
+ }
781
+ float settleFactor = 1.0 - smoothstep(u_velocityDeadzone, u_velocityDeadzone * 4.0, length(nextVelocity));
782
+ nextVelocity *= mix(1.0, u_settleDamping, settleFactor);
783
+ float speed = length(nextVelocity);
784
+ if (speed > u_maxSpeed) {
785
+ nextVelocity = normalize(nextVelocity) * u_maxSpeed;
786
+ }
787
+
788
+ gl_FragColor = vec4(nextVelocity, 1.0);
789
+ }
790
+ `,
791
+ depthTest: false,
792
+ depthWrite: false
793
+ });
794
+
795
+ const computeScene = new THREE.Scene();
796
+ const computeCamera = new THREE.Camera();
797
+ const computeQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMaterial);
798
+ computeQuad.frustumCulled = false;
799
+ computeScene.add(computeQuad);
800
+
801
+ const initialTextures = createInitialTextures();
802
+ let positionTargetA = createRenderTarget();
803
+ let positionTargetB = createRenderTarget();
804
+ let velocityTargetA = createRenderTarget();
805
+ let velocityTargetB = createRenderTarget();
806
+ let currentPositionTarget = positionTargetA;
807
+ let nextPositionTarget = positionTargetB;
808
+ let currentVelocityTarget = velocityTargetA;
809
+ let nextVelocityTarget = velocityTargetB;
810
+ let simulationReady = renderer && typeof renderer.setRenderTarget === 'function';
811
+ let simulationWarningShown = false;
812
+ const simulationStepsPerTick = 16;
813
+
814
+ const renderComputePass = (material, target) => {
815
+ computeQuad.material = material;
816
+ renderer.setRenderTarget(target);
817
+ renderer.render(computeScene, computeCamera);
818
+ };
819
+
820
+ const copyTextureToTarget = (texture, target) => {
821
+ copyMaterial.uniforms.u_texture.value = texture;
822
+ renderComputePass(copyMaterial, target);
823
+ };
824
+
825
+ if (simulationReady) {
826
+ const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
827
+ try {
828
+ copyTextureToTarget(initialTextures.positionTexture, currentPositionTarget);
829
+ copyTextureToTarget(initialTextures.velocityTexture, currentVelocityTarget);
830
+ renderer.setRenderTarget(previousTarget);
831
+ } catch (error) {
832
+ simulationReady = false;
833
+ console.warn('[BackgroundThemes] Galaxy particle simulation disabled.', error);
834
+ }
835
+ }
836
+
837
+ const particleGeometry = new THREE.BufferGeometry();
838
+ const indices = new Float32Array(pointCount);
839
+ const dummyPositions = new Float32Array(pointCount * 3);
840
+ for (let i = 0; i < pointCount; i++) {
841
+ indices[i] = i;
842
+ }
843
+ particleGeometry.setAttribute('a_index', new THREE.BufferAttribute(indices, 1));
844
+ particleGeometry.setAttribute('position', new THREE.BufferAttribute(dummyPositions, 3));
845
+
846
+ const particleMaterial = new THREE.ShaderMaterial({
847
+ uniforms: {
848
+ u_width: { value: simSize },
849
+ u_height: { value: simSize },
850
+ u_particleSize: { value: 64800 },
851
+ u_worldScale: { value: 720 },
852
+ u_depthScale: { value: 620 },
853
+ u_backgroundZ: { value: -14000 },
854
+ u_time: { value: 0 },
855
+ u_positionTexture: { value: currentPositionTarget.texture }
856
+ },
857
+ vertexShader: `
858
+ attribute float a_index;
859
+ uniform float u_width;
860
+ uniform float u_height;
861
+ uniform float u_particleSize;
862
+ uniform float u_worldScale;
863
+ uniform float u_depthScale;
864
+ uniform float u_backgroundZ;
865
+ uniform float u_time;
866
+ uniform sampler2D u_positionTexture;
867
+ varying float vDepth;
868
+ varying float vNearness;
869
+
870
+ void main() {
871
+ const float TWO_PI = 6.28318530718;
872
+ vec2 uv = vec2(
873
+ (mod(a_index, u_width) + 0.5) / u_width,
874
+ (floor(a_index / u_width) + 0.5) / u_height
875
+ );
876
+ vec3 gravityPosition = texture2D(u_positionTexture, uv).xyz;
877
+ float seed = fract(sin(a_index * 12.9898) * 43758.5453);
878
+ float sizeSeed = fract(sin((a_index + 17.31) * 78.233) * 43758.5453);
879
+ float depthSeed = fract(sin((a_index + 91.73) * 39.425) * 24634.6345);
880
+ float speedSeed = fract(sin((a_index + 211.13) * 17.317) * 19542.349);
881
+ float phaseSeed = fract(sin((a_index + 47.77) * 63.721) * 31571.231);
882
+ float localTime = u_time * mix(0.72, 1.38, speedSeed) + phaseSeed * TWO_PI;
883
+ float radius = length(gravityPosition.xy);
884
+ float fieldFlow = mod(u_time * mix(0.046, 0.062, speedSeed), TWO_PI);
885
+ float fc = cos(fieldFlow);
886
+ float fs = sin(fieldFlow);
887
+ gravityPosition.xy = mat2(fc, -fs, fs, fc) * gravityPosition.xy;
888
+
889
+ float flow = mod(localTime * mix(0.018, 0.054, seed) * (1.0 - smoothstep(9.0, 18.0, radius)), TWO_PI);
890
+ float c = cos(flow);
891
+ float s = sin(flow);
892
+ gravityPosition.xy = mat2(c, -s, s, c) * gravityPosition.xy;
893
+ gravityPosition.xy += vec2(
894
+ sin(mod(localTime * mix(0.08, 0.22, seed) + seed * TWO_PI, TWO_PI)),
895
+ cos(mod(localTime * mix(0.07, 0.2, seed) + seed * 8.713, TWO_PI))
896
+ ) * mix(0.055, 0.18, seed);
897
+ float depthLayer = (depthSeed - 0.5) * mix(4.0, 8.4, sizeSeed);
898
+ float depthDrift = sin(mod(localTime * mix(0.035, 0.1, seed) + seed * 12.421, TWO_PI)) * mix(0.1, 0.34, depthSeed);
899
+ float layeredZ = clamp(gravityPosition.z + depthLayer + depthDrift, -6.8, 4.2);
900
+ vec3 simPosition = vec3(
901
+ gravityPosition.xy * u_worldScale,
902
+ u_backgroundZ + layeredZ * u_depthScale
903
+ );
904
+ vec4 mvPosition = modelViewMatrix * vec4(simPosition, 1.0);
905
+ float viewDepth = max(-mvPosition.z, 1.0);
906
+ float seededSize = mix(0.55, 2.05, pow(sizeSeed, 1.42));
907
+ gl_PointSize = clamp((u_particleSize / viewDepth) * seededSize * 2.0, 1.0, 17.0);
908
+ gl_Position = projectionMatrix * mvPosition;
909
+ vDepth = clamp((viewDepth - 9000.0) / 52000.0, 0.0, 1.0);
910
+ vNearness = 1.0 - clamp((viewDepth - 9000.0) / 28000.0, 0.0, 1.0);
911
+ }
912
+ `,
913
+ fragmentShader: `
914
+ precision mediump float;
915
+ varying float vDepth;
916
+ varying float vNearness;
917
+
918
+ void main() {
919
+ vec2 uv = gl_PointCoord - vec2(0.5);
920
+ float dist = length(uv);
921
+ float radius = mix(0.38, 0.49, vNearness);
922
+ float core = 1.0 - smoothstep(0.02, radius, dist);
923
+ float halo = (1.0 - smoothstep(0.28, 0.5, dist)) * vNearness * 0.24;
924
+ float alpha = (pow(max(core, 0.0), mix(1.6, 1.05, vNearness)) + halo) * mix(0.92, 0.46, vDepth);
925
+ if (alpha < 0.015) discard;
926
+ gl_FragColor = vec4(vec3(0.0, 0.0, 0.0), min(alpha, 0.94));
927
+ }
928
+ `,
929
+ transparent: true,
930
+ depthTest: true,
931
+ depthWrite: false,
932
+ blending: THREE.NormalBlending
933
+ });
934
+
935
+ const gravityPoints = new THREE.Points(particleGeometry, particleMaterial);
936
+ gravityPoints.frustumCulled = false;
937
+ gravityPoints.renderOrder = -40;
938
+ themeGroup.add(gravityPoints);
939
+
940
+ const themeStartTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001;
941
+
942
+ const syncParticleView = (camera) => {
943
+ if (!camera) return;
944
+
945
+ const cameraZ = Math.max(600, Math.abs(Number(camera.position?.z || 1200)));
946
+ const cameraFar = Math.max(120000, Number(camera.far || 500000));
947
+ const clipSafeDepth = Math.max(14000, cameraFar - cameraZ - 5000);
948
+ const backgroundDepth = Math.min(Math.max(26000, cameraZ * 6.0), clipSafeDepth * 0.82);
949
+ const viewDistance = cameraZ + backgroundDepth;
950
+ const elapsedTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001 - themeStartTime;
951
+ const animationTime = elapsedTime * 8.0;
952
+ particleMaterial.uniforms.u_worldScale.value = Math.max(820, backgroundDepth * 0.052);
953
+ particleMaterial.uniforms.u_depthScale.value = Math.max(1050, backgroundDepth * 0.075);
954
+ particleMaterial.uniforms.u_backgroundZ.value = -backgroundDepth;
955
+ particleMaterial.uniforms.u_particleSize.value = Math.max(68000, Math.min(cameraZ * 34, viewDistance * 2.75));
956
+ particleMaterial.uniforms.u_time.value = animationTime;
957
+ gravityPoints.rotation.z = (animationTime * 0.085) % (Math.PI * 2);
958
+ gravityPoints.rotation.x = 0;
959
+ gravityPoints.position.x = 0;
960
+ gravityPoints.position.y = 0;
961
+ gravityPoints.updateMatrixWorld(true);
962
+ };
963
+
964
+ gravityPoints.onBeforeRender = (_renderer, _scene, camera) => {
965
+ syncParticleView(camera);
966
+ };
967
+
968
+ // --- 2) XYGrid overlay (same distance-based LOD as XYGrid theme) ---
969
+ const adaptiveGridOverlay = createAdaptiveXYGridOverlay(new THREE.Color(THEME_GRID_COLOR_SOFT), {
970
+ fineDistance: 45000,
971
+ coarseDistance: 120000,
972
+ farDistance: 320000,
973
+ fineSmallOpacity: 0.17,
974
+ fineLargeOpacity: 0.1,
975
+ coarseSmallOpacity: 0.15,
976
+ coarseLargeOpacity: 0.085,
977
+ farSmallOpacity: 0.12,
978
+ farLargeOpacity: 0.07
979
+ });
980
+ themeGroup.add(adaptiveGridOverlay);
981
+
982
+ // Animation: galaxy texture simulation and camera tracking for grid
983
+ themeGroup.animate = (camera, options = {}) => {
984
+ if (!camera) return;
985
+
986
+ const isInteractiveMotionFrame = options.isInteractiveFrame === true ||
987
+ options.isCameraMoving === true ||
988
+ options.isPanning === true ||
989
+ options.isZooming === true;
990
+ const targetFps = Math.max(0, Number(options.targetFps || 0) || 0);
991
+ const steadySimulationStepCount = targetFps > 0
992
+ ? Math.max(1, Math.round(32 / targetFps))
993
+ : simulationStepsPerTick;
994
+ const simulationStepCount = isInteractiveMotionFrame ? 1 : steadySimulationStepCount;
995
+
996
+ if (simulationReady) {
997
+ const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
998
+ try {
999
+ for (let i = 0; i < simulationStepCount; i++) {
1000
+ velocityMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1001
+ velocityMaterial.uniforms.u_velocityTexture.value = currentVelocityTarget.texture;
1002
+ renderComputePass(velocityMaterial, nextVelocityTarget);
1003
+
1004
+ positionMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1005
+ positionMaterial.uniforms.u_velocityTexture.value = nextVelocityTarget.texture;
1006
+ renderComputePass(positionMaterial, nextPositionTarget);
1007
+
1008
+ const oldPositionTarget = currentPositionTarget;
1009
+ currentPositionTarget = nextPositionTarget;
1010
+ nextPositionTarget = oldPositionTarget;
1011
+
1012
+ const oldVelocityTarget = currentVelocityTarget;
1013
+ currentVelocityTarget = nextVelocityTarget;
1014
+ nextVelocityTarget = oldVelocityTarget;
1015
+ }
1016
+
1017
+ renderer.setRenderTarget(previousTarget);
1018
+
1019
+ particleMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1020
+ } catch (error) {
1021
+ simulationReady = false;
1022
+ if (!simulationWarningShown) {
1023
+ simulationWarningShown = true;
1024
+ console.warn('[BackgroundThemes] Galaxy particle simulation stopped.', error);
1025
+ }
1026
+ if (renderer.setRenderTarget) {
1027
+ renderer.setRenderTarget(previousTarget);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ syncParticleView(camera);
1033
+
1034
+ adaptiveGridOverlay.animate(camera);
1035
+ };
1036
+
1037
+ themeGroup.customDispose = () => {
1038
+ themeGroup.traverse(child => {
1039
+ if (child.geometry) child.geometry.dispose();
1040
+ if (child.material) {
1041
+ if (Array.isArray(child.material)) {
1042
+ child.material.forEach(material => material.dispose());
1043
+ } else {
1044
+ child.material.dispose();
1045
+ }
1046
+ }
1047
+ });
1048
+ initialTextures.positionTexture.dispose();
1049
+ initialTextures.velocityTexture.dispose();
1050
+ positionTargetA.dispose();
1051
+ positionTargetB.dispose();
1052
+ velocityTargetA.dispose();
1053
+ velocityTargetB.dispose();
1054
+ copyMaterial.dispose();
1055
+ positionMaterial.dispose();
1056
+ velocityMaterial.dispose();
1057
+ particleMaterial.dispose();
1058
+ };
1059
+
1060
+ return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.Continuous);
1061
+ },
1062
+ dispose: (themeObject) => { disposeThemeObject(themeObject); }
1063
+ },
1064
+
1065
+ // Theme 8: SpiritFlow (curl-noise particle flow inspired by ref/~spirit)
1066
+ 'SpiritFlow': {
1067
+ create: (scene, renderer) => {
1068
+ scene.background = new THREE.Color(THEME_SURFACE_BG);
1069
+
1070
+ const themeGroup = new THREE.Group();
1071
+ const textureWidth = 256;
1072
+ const textureHeight = 256;
1073
+ const pointCount = textureWidth * textureHeight;
1074
+ const resolution = new THREE.Vector2(textureWidth, textureHeight);
1075
+ const renderTargetOptions = {
1076
+ wrapS: THREE.ClampToEdgeWrapping,
1077
+ wrapT: THREE.ClampToEdgeWrapping,
1078
+ minFilter: THREE.NearestFilter,
1079
+ magFilter: THREE.NearestFilter,
1080
+ format: THREE.RGBAFormat,
1081
+ type: THREE.FloatType,
1082
+ depthBuffer: false,
1083
+ stencilBuffer: false
1084
+ };
1085
+
1086
+ const createRenderTarget = () => {
1087
+ const target = new THREE.WebGLRenderTarget(textureWidth, textureHeight, renderTargetOptions);
1088
+ target.texture.generateMipmaps = false;
1089
+ return target;
1090
+ };
1091
+
1092
+ const createDefaultPositionTexture = () => {
1093
+ const data = new Float32Array(pointCount * 4);
1094
+ for (let i = 0; i < pointCount; i++) {
1095
+ const offset = i * 4;
1096
+ const radius = (0.5 + Math.random() * 0.5) * 50;
1097
+ const phi = (Math.random() - 0.5) * Math.PI;
1098
+ const theta = Math.random() * Math.PI * 2;
1099
+ data[offset] = radius * Math.cos(theta) * Math.cos(phi);
1100
+ data[offset + 1] = radius * Math.sin(phi);
1101
+ data[offset + 2] = radius * Math.sin(theta) * Math.cos(phi);
1102
+ data[offset + 3] = Math.random();
1103
+ }
1104
+
1105
+ const texture = new THREE.DataTexture(data, textureWidth, textureHeight, THREE.RGBAFormat, THREE.FloatType);
1106
+ texture.minFilter = THREE.NearestFilter;
1107
+ texture.magFilter = THREE.NearestFilter;
1108
+ texture.generateMipmaps = false;
1109
+ texture.flipY = false;
1110
+ texture.needsUpdate = true;
1111
+ return texture;
1112
+ };
1113
+
1114
+ const computeVertexShader = `
1115
+ void main() {
1116
+ gl_Position = vec4(position.xy, 0.0, 1.0);
1117
+ }
1118
+ `;
1119
+
1120
+ const copyMaterial = new THREE.ShaderMaterial({
1121
+ uniforms: {
1122
+ u_texture: { value: null },
1123
+ u_resolution: { value: resolution }
1124
+ },
1125
+ vertexShader: computeVertexShader,
1126
+ fragmentShader: `
1127
+ precision highp float;
1128
+ uniform sampler2D u_texture;
1129
+ uniform vec2 u_resolution;
1130
+
1131
+ void main() {
1132
+ vec2 uv = gl_FragCoord.xy / u_resolution;
1133
+ gl_FragColor = texture2D(u_texture, uv);
1134
+ }
1135
+ `,
1136
+ depthTest: false,
1137
+ depthWrite: false
1138
+ });
1139
+
1140
+ const positionMaterial = new THREE.ShaderMaterial({
1141
+ uniforms: {
1142
+ u_positionTexture: { value: null },
1143
+ u_defaultTexture: { value: null },
1144
+ u_resolution: { value: resolution },
1145
+ u_mouse3d: { value: new THREE.Vector3() },
1146
+ u_speed: { value: 0.81 },
1147
+ u_dieSpeed: { value: 0 },
1148
+ u_radius: { value: 0.6 },
1149
+ u_curlSize: { value: 0.018 },
1150
+ u_attraction: { value: 0.87 },
1151
+ u_time: { value: 0 },
1152
+ u_initAnimation: { value: 0 }
1153
+ },
1154
+ vertexShader: computeVertexShader,
1155
+ fragmentShader: `
1156
+ precision highp float;
1157
+
1158
+ uniform sampler2D u_positionTexture;
1159
+ uniform sampler2D u_defaultTexture;
1160
+ uniform vec2 u_resolution;
1161
+ uniform vec3 u_mouse3d;
1162
+ uniform float u_speed;
1163
+ uniform float u_dieSpeed;
1164
+ uniform float u_radius;
1165
+ uniform float u_curlSize;
1166
+ uniform float u_attraction;
1167
+ uniform float u_time;
1168
+ uniform float u_initAnimation;
1169
+
1170
+ vec4 mod289(vec4 x) {
1171
+ return x - floor(x * (1.0 / 289.0)) * 289.0;
1172
+ }
1173
+
1174
+ float mod289(float x) {
1175
+ return x - floor(x * (1.0 / 289.0)) * 289.0;
1176
+ }
1177
+
1178
+ vec4 permute(vec4 x) {
1179
+ return mod289(((x * 34.0) + 1.0) * x);
1180
+ }
1181
+
1182
+ float permute(float x) {
1183
+ return mod289(((x * 34.0) + 1.0) * x);
1184
+ }
1185
+
1186
+ vec4 taylorInvSqrt(vec4 r) {
1187
+ return 1.79284291400159 - 0.85373472095314 * r;
1188
+ }
1189
+
1190
+ float taylorInvSqrt(float r) {
1191
+ return 1.79284291400159 - 0.85373472095314 * r;
1192
+ }
1193
+
1194
+ vec4 grad4(float j, vec4 ip) {
1195
+ const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
1196
+ vec4 p;
1197
+ vec4 s;
1198
+
1199
+ p.xyz = floor(fract(vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
1200
+ p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
1201
+ s = vec4(
1202
+ p.x < 0.0 ? 1.0 : 0.0,
1203
+ p.y < 0.0 ? 1.0 : 0.0,
1204
+ p.z < 0.0 ? 1.0 : 0.0,
1205
+ p.w < 0.0 ? 1.0 : 0.0
1206
+ );
1207
+ p.xyz = p.xyz + (s.xyz * 2.0 - 1.0) * s.www;
1208
+
1209
+ return p;
1210
+ }
1211
+
1212
+ #define F4 0.309016994374947451
1213
+
1214
+ vec4 simplexNoiseDerivatives(vec4 v) {
1215
+ const vec4 C = vec4(0.138196601125011, 0.276393202250021, 0.414589803375032, -0.447213595499958);
1216
+
1217
+ vec4 i = floor(v + dot(v, vec4(F4)));
1218
+ vec4 x0 = v - i + dot(i, C.xxxx);
1219
+
1220
+ vec4 i0;
1221
+ vec3 isX = step(x0.yzw, x0.xxx);
1222
+ vec3 isYZ = step(x0.zww, x0.yyz);
1223
+ i0.x = isX.x + isX.y + isX.z;
1224
+ i0.yzw = 1.0 - isX;
1225
+ i0.y += isYZ.x + isYZ.y;
1226
+ i0.zw += 1.0 - isYZ.xy;
1227
+ i0.z += isYZ.z;
1228
+ i0.w += 1.0 - isYZ.z;
1229
+
1230
+ vec4 i3 = clamp(i0, 0.0, 1.0);
1231
+ vec4 i2 = clamp(i0 - 1.0, 0.0, 1.0);
1232
+ vec4 i1 = clamp(i0 - 2.0, 0.0, 1.0);
1233
+
1234
+ vec4 x1 = x0 - i1 + C.xxxx;
1235
+ vec4 x2 = x0 - i2 + C.yyyy;
1236
+ vec4 x3 = x0 - i3 + C.zzzz;
1237
+ vec4 x4 = x0 + C.wwww;
1238
+
1239
+ i = mod289(i);
1240
+ float j0 = permute(permute(permute(permute(i.w) + i.z) + i.y) + i.x);
1241
+ vec4 j1 = permute(permute(permute(permute(
1242
+ i.w + vec4(i1.w, i2.w, i3.w, 1.0))
1243
+ + i.z + vec4(i1.z, i2.z, i3.z, 1.0))
1244
+ + i.y + vec4(i1.y, i2.y, i3.y, 1.0))
1245
+ + i.x + vec4(i1.x, i2.x, i3.x, 1.0));
1246
+
1247
+ vec4 ip = vec4(1.0 / 294.0, 1.0 / 49.0, 1.0 / 7.0, 0.0);
1248
+
1249
+ vec4 p0 = grad4(j0, ip);
1250
+ vec4 p1 = grad4(j1.x, ip);
1251
+ vec4 p2 = grad4(j1.y, ip);
1252
+ vec4 p3 = grad4(j1.z, ip);
1253
+ vec4 p4 = grad4(j1.w, ip);
1254
+
1255
+ vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
1256
+ p0 *= norm.x;
1257
+ p1 *= norm.y;
1258
+ p2 *= norm.z;
1259
+ p3 *= norm.w;
1260
+ p4 *= taylorInvSqrt(dot(p4, p4));
1261
+
1262
+ vec3 values0 = vec3(dot(p0, x0), dot(p1, x1), dot(p2, x2));
1263
+ vec2 values1 = vec2(dot(p3, x3), dot(p4, x4));
1264
+
1265
+ vec3 m0 = max(0.5 - vec3(dot(x0, x0), dot(x1, x1), dot(x2, x2)), 0.0);
1266
+ vec2 m1 = max(0.5 - vec2(dot(x3, x3), dot(x4, x4)), 0.0);
1267
+
1268
+ vec3 temp0 = -6.0 * m0 * m0 * values0;
1269
+ vec2 temp1 = -6.0 * m1 * m1 * values1;
1270
+
1271
+ vec3 mmm0 = m0 * m0 * m0;
1272
+ vec2 mmm1 = m1 * m1 * m1;
1273
+
1274
+ float dx = temp0[0] * x0.x + temp0[1] * x1.x + temp0[2] * x2.x + temp1[0] * x3.x + temp1[1] * x4.x + mmm0[0] * p0.x + mmm0[1] * p1.x + mmm0[2] * p2.x + mmm1[0] * p3.x + mmm1[1] * p4.x;
1275
+ float dy = temp0[0] * x0.y + temp0[1] * x1.y + temp0[2] * x2.y + temp1[0] * x3.y + temp1[1] * x4.y + mmm0[0] * p0.y + mmm0[1] * p1.y + mmm0[2] * p2.y + mmm1[0] * p3.y + mmm1[1] * p4.y;
1276
+ float dz = temp0[0] * x0.z + temp0[1] * x1.z + temp0[2] * x2.z + temp1[0] * x3.z + temp1[1] * x4.z + mmm0[0] * p0.z + mmm0[1] * p1.z + mmm0[2] * p2.z + mmm1[0] * p3.z + mmm1[1] * p4.z;
1277
+ float dw = temp0[0] * x0.w + temp0[1] * x1.w + temp0[2] * x2.w + temp1[0] * x3.w + temp1[1] * x4.w + mmm0[0] * p0.w + mmm0[1] * p1.w + mmm0[2] * p2.w + mmm1[0] * p3.w + mmm1[1] * p4.w;
1278
+
1279
+ return vec4(dx, dy, dz, dw) * 49.0;
1280
+ }
1281
+
1282
+ vec3 curl(vec3 p, float noiseTime, float persistence) {
1283
+ vec4 xNoisePotentialDerivatives = vec4(0.0);
1284
+ vec4 yNoisePotentialDerivatives = vec4(0.0);
1285
+ vec4 zNoisePotentialDerivatives = vec4(0.0);
1286
+
1287
+ for (int i = 0; i < 3; ++i) {
1288
+ float twoPowI = pow(2.0, float(i));
1289
+ float scale = 0.5 * twoPowI * pow(persistence, float(i));
1290
+
1291
+ xNoisePotentialDerivatives += simplexNoiseDerivatives(vec4(p * twoPowI, noiseTime)) * scale;
1292
+ yNoisePotentialDerivatives += simplexNoiseDerivatives(vec4((p + vec3(123.4, 129845.6, -1239.1)) * twoPowI, noiseTime)) * scale;
1293
+ zNoisePotentialDerivatives += simplexNoiseDerivatives(vec4((p + vec3(-9519.0, 9051.0, -123.0)) * twoPowI, noiseTime)) * scale;
1294
+ }
1295
+
1296
+ return vec3(
1297
+ zNoisePotentialDerivatives[1] - yNoisePotentialDerivatives[2],
1298
+ xNoisePotentialDerivatives[2] - zNoisePotentialDerivatives[0],
1299
+ yNoisePotentialDerivatives[0] - xNoisePotentialDerivatives[1]
1300
+ );
1301
+ }
1302
+
1303
+ void main() {
1304
+ vec2 uv = gl_FragCoord.xy / u_resolution;
1305
+ vec4 positionInfo = texture2D(u_positionTexture, uv);
1306
+ vec4 defaultInfo = texture2D(u_defaultTexture, uv);
1307
+ vec3 position = mix(vec3(0.0, -200.0, 0.0), positionInfo.xyz, smoothstep(0.0, 0.3, u_initAnimation));
1308
+ float life = positionInfo.a - u_dieSpeed;
1309
+
1310
+ vec3 followPosition = mix(
1311
+ vec3(0.0, -(1.0 - u_initAnimation) * 200.0, 0.0),
1312
+ u_mouse3d,
1313
+ smoothstep(0.2, 0.7, u_initAnimation)
1314
+ );
1315
+
1316
+ if (life < 0.0) {
1317
+ position = defaultInfo.xyz * (1.0 + sin(u_time * 15.0) * 0.2 + (1.0 - u_initAnimation)) * 0.4 * u_radius;
1318
+ position += followPosition;
1319
+ life = 0.5 + fract(defaultInfo.w * 21.4131 + u_time);
1320
+ } else {
1321
+ vec3 delta = followPosition - position;
1322
+ float distanceToFollow = length(delta);
1323
+ position += delta * (0.005 + life * 0.01) * u_attraction * (1.0 - smoothstep(50.0, 350.0, distanceToFollow)) * u_speed;
1324
+ position += curl(position * u_curlSize, u_time, 0.1 + (1.0 - life) * 0.1) * u_speed;
1325
+ }
1326
+
1327
+ gl_FragColor = vec4(position, life);
1328
+ }
1329
+ `,
1330
+ depthTest: false,
1331
+ depthWrite: false,
1332
+ blending: THREE.NoBlending
1333
+ });
1334
+
1335
+ const computeScene = new THREE.Scene();
1336
+ const computeCamera = new THREE.Camera();
1337
+ const computeQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), copyMaterial);
1338
+ computeQuad.frustumCulled = false;
1339
+ computeScene.add(computeQuad);
1340
+
1341
+ const defaultTexture = createDefaultPositionTexture();
1342
+ let positionTargetA = createRenderTarget();
1343
+ let positionTargetB = createRenderTarget();
1344
+ let currentPositionTarget = positionTargetA;
1345
+ let nextPositionTarget = positionTargetB;
1346
+ let simulationReady = renderer && typeof renderer.setRenderTarget === 'function';
1347
+ let simulationWarningShown = false;
1348
+
1349
+ const renderComputePass = (material, target) => {
1350
+ computeQuad.material = material;
1351
+ renderer.setRenderTarget(target);
1352
+ renderer.render(computeScene, computeCamera);
1353
+ };
1354
+
1355
+ const copyTextureToTarget = (texture, target) => {
1356
+ copyMaterial.uniforms.u_texture.value = texture;
1357
+ renderComputePass(copyMaterial, target);
1358
+ };
1359
+
1360
+ if (simulationReady) {
1361
+ const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
1362
+ try {
1363
+ copyTextureToTarget(defaultTexture, currentPositionTarget);
1364
+ copyTextureToTarget(defaultTexture, nextPositionTarget);
1365
+ renderer.setRenderTarget(previousTarget);
1366
+ } catch (error) {
1367
+ simulationReady = false;
1368
+ console.warn('[BackgroundThemes] Spirit particle simulation disabled.', error);
1369
+ if (renderer.setRenderTarget) {
1370
+ renderer.setRenderTarget(previousTarget);
1371
+ }
1372
+ }
1373
+ }
1374
+
1375
+ const particleGeometry = new THREE.BufferGeometry();
1376
+ const particleUvs = new Float32Array(pointCount * 3);
1377
+ for (let i = 0; i < pointCount; i++) {
1378
+ const offset = i * 3;
1379
+ particleUvs[offset] = (i % textureWidth + 0.5) / textureWidth;
1380
+ particleUvs[offset + 1] = (Math.floor(i / textureWidth) + 0.5) / textureHeight;
1381
+ particleUvs[offset + 2] = 0;
1382
+ }
1383
+ particleGeometry.setAttribute('position', new THREE.BufferAttribute(particleUvs, 3));
1384
+
1385
+ const backdropMaterial = new THREE.ShaderMaterial({
1386
+ vertexShader: `
1387
+ varying vec2 vUv;
1388
+
1389
+ void main() {
1390
+ vUv = uv;
1391
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
1392
+ }
1393
+ `,
1394
+ fragmentShader: `
1395
+ precision mediump float;
1396
+ varying vec2 vUv;
1397
+
1398
+ void main() {
1399
+ vec3 topColor = vec3(0.74, 0.76, 0.75);
1400
+ vec3 horizonColor = vec3(0.88, 0.89, 0.86);
1401
+ vec3 bottomColor = vec3(0.60, 0.62, 0.61);
1402
+ float horizon = 1.0 - smoothstep(0.20, 0.64, abs(vUv.y - 0.53));
1403
+ vec3 vertical = mix(bottomColor, topColor, smoothstep(0.0, 1.0, vUv.y));
1404
+ vec3 color = mix(vertical, horizonColor, horizon * 0.78);
1405
+ gl_FragColor = vec4(color, 1.0);
1406
+ }
1407
+ `,
1408
+ depthTest: false,
1409
+ depthWrite: false
1410
+ });
1411
+ const backdrop = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), backdropMaterial);
1412
+ backdrop.frustumCulled = false;
1413
+ backdrop.renderOrder = -90;
1414
+ themeGroup.add(backdrop);
1415
+
1416
+ const particleMaterial = new THREE.ShaderMaterial({
1417
+ uniforms: {
1418
+ u_positionTexture: { value: simulationReady ? currentPositionTarget.texture : defaultTexture },
1419
+ u_particleSize: { value: 42000 },
1420
+ u_worldScale: { value: 120 },
1421
+ u_depthScale: { value: 85 },
1422
+ u_backgroundZ: { value: -26000 },
1423
+ u_color: { value: new THREE.Color(0xffffff) }
1424
+ },
1425
+ vertexShader: `
1426
+ uniform sampler2D u_positionTexture;
1427
+ uniform float u_particleSize;
1428
+ uniform float u_worldScale;
1429
+ uniform float u_depthScale;
1430
+ uniform float u_backgroundZ;
1431
+ varying float vLife;
1432
+ varying float vAlpha;
1433
+ varying float vNearness;
1434
+ varying float vShade;
1435
+
1436
+ void main() {
1437
+ vec4 positionInfo = texture2D(u_positionTexture, position.xy);
1438
+ float life = clamp(positionInfo.w, 0.0, 1.0);
1439
+ vec3 flowPosition = positionInfo.xyz;
1440
+ float sizeSeed = fract(sin(dot(position.xy, vec2(127.1, 311.7))) * 43758.5453);
1441
+
1442
+ vec3 simPosition = vec3(
1443
+ flowPosition.x * u_worldScale,
1444
+ flowPosition.y * u_worldScale,
1445
+ u_backgroundZ + flowPosition.z * u_depthScale
1446
+ );
1447
+
1448
+ vec4 mvPosition = modelViewMatrix * vec4(simPosition, 1.0);
1449
+ float viewDepth = max(-mvPosition.z, 1.0);
1450
+ float fadeOut = smoothstep(0.0, 0.28, life);
1451
+ float seededSize = mix(0.62, 2.15, pow(sizeSeed, 1.35));
1452
+ float nearness = 1.0 - clamp((viewDepth - 9000.0) / 28000.0, 0.0, 1.0);
1453
+ float depthBoost = mix(0.78, 1.62, nearness);
1454
+ gl_PointSize = clamp((u_particleSize / viewDepth) * seededSize * depthBoost * fadeOut, 0.12, 9.6);
1455
+ gl_Position = projectionMatrix * mvPosition;
1456
+ vLife = life;
1457
+ vAlpha = fadeOut;
1458
+ vNearness = nearness;
1459
+ float upperLight = smoothstep(-20.0, 115.0, flowPosition.y);
1460
+ float frontLight = smoothstep(-90.0, 85.0, flowPosition.z);
1461
+ vShade = mix(0.46, 1.18, clamp(upperLight * 0.72 + frontLight * 0.35, 0.0, 1.0));
1462
+ }
1463
+ `,
1464
+ fragmentShader: `
1465
+ precision mediump float;
1466
+ uniform vec3 u_color;
1467
+ varying float vLife;
1468
+ varying float vAlpha;
1469
+ varying float vNearness;
1470
+ varying float vShade;
1471
+
1472
+ void main() {
1473
+ vec2 uv = gl_PointCoord - vec2(0.5);
1474
+ float dist = length(uv);
1475
+ float core = 1.0 - smoothstep(0.16, mix(0.44, 0.5, vNearness), dist);
1476
+ float halo = (1.0 - smoothstep(0.3, 0.5, dist)) * vNearness * 0.22;
1477
+ float particleAlpha = (core + halo) * vAlpha;
1478
+ if (particleAlpha < 0.01) discard;
1479
+ vec3 outgoingLight = mix(u_color * 0.58, u_color, smoothstep(0.0, 0.7, vLife));
1480
+ gl_FragColor = vec4(outgoingLight * vShade, min(particleAlpha, 0.96));
1481
+ }
1482
+ `,
1483
+ transparent: true,
1484
+ depthTest: true,
1485
+ depthWrite: false,
1486
+ blending: THREE.NormalBlending
1487
+ });
1488
+
1489
+ const particles = new THREE.Points(particleGeometry, particleMaterial);
1490
+ particles.frustumCulled = false;
1491
+ particles.renderOrder = -40;
1492
+ themeGroup.add(particles);
1493
+
1494
+ const themeStartTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001;
1495
+ let lastElapsedTime = 0;
1496
+ let followPointTime = 0;
1497
+ const followPoint = new THREE.Vector3(0, 0, 0);
1498
+ const autoFollowPoint = new THREE.Vector3(0, 0, 0);
1499
+ const spiritMotion = Object.freeze({
1500
+ followTimeScale: 0.79,
1501
+ followBlend: 0.16,
1502
+ followX: 182,
1503
+ followY: 49,
1504
+ followZ: 132,
1505
+ depthTravelScale: 0.67,
1506
+ particleSpeed: 0.81,
1507
+ lifeDecay: 0.013
1508
+ });
1509
+
1510
+ const syncParticleView = (camera, elapsedTime) => {
1511
+ if (!camera) return;
1512
+
1513
+ const cameraZ = Math.max(600, Math.abs(Number(camera.position?.z || 1200)));
1514
+ const cameraFar = Math.max(120000, Number(camera.far || 500000));
1515
+ const clipSafeDepth = Math.max(14000, cameraFar - cameraZ - 5000);
1516
+ const backgroundDepth = Math.min(Math.max(26000, cameraZ * 6.0), clipSafeDepth * 0.82);
1517
+ const viewDistance = cameraZ + backgroundDepth;
1518
+ const fovRadians = THREE.MathUtils?.degToRad
1519
+ ? THREE.MathUtils.degToRad(Number(camera.fov || 45))
1520
+ : Number(camera.fov || 45) * Math.PI / 180;
1521
+ const viewportHeight = Math.max(1, Math.tan(fovRadians * 0.5) * viewDistance * 2);
1522
+ const viewportWidth = viewportHeight * Math.max(0.1, Number(camera.aspect || 1));
1523
+ particleMaterial.uniforms.u_worldScale.value = Math.max(105, backgroundDepth * 0.0043);
1524
+ particleMaterial.uniforms.u_depthScale.value = Math.max(56, backgroundDepth * 0.0037 * spiritMotion.depthTravelScale);
1525
+ particleMaterial.uniforms.u_backgroundZ.value = -backgroundDepth;
1526
+ particleMaterial.uniforms.u_particleSize.value = Math.max(44000, viewDistance * 1.92);
1527
+ particles.rotation.z = Math.sin(elapsedTime * 0.08) * 0.08;
1528
+ particles.rotation.x = Math.sin(elapsedTime * 0.11) * 0.05;
1529
+ particles.position.x = Number(camera.position?.x || 0);
1530
+ particles.position.y = Number(camera.position?.y || 0);
1531
+ particles.updateMatrixWorld(true);
1532
+ backdrop.position.x = Number(camera.position?.x || 0);
1533
+ backdrop.position.y = Number(camera.position?.y || 0);
1534
+ backdrop.position.z = -backgroundDepth - 1200;
1535
+ backdrop.scale.set(viewportWidth * 1.12, viewportHeight * 1.12, 1);
1536
+ backdrop.updateMatrixWorld(true);
1537
+ };
1538
+
1539
+ particles.onBeforeRender = (_renderer, _scene, camera) => {
1540
+ const elapsedTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001 - themeStartTime;
1541
+ syncParticleView(camera, elapsedTime);
1542
+ };
1543
+
1544
+ themeGroup.animate = (camera) => {
1545
+ if (!camera) return;
1546
+
1547
+ const elapsedTime = (typeof performance !== 'undefined' ? performance.now() : Date.now()) * 0.001 - themeStartTime;
1548
+ const deltaMs = Math.min(48, Math.max(0, (elapsedTime - lastElapsedTime) * 1000 || 16.6667));
1549
+ const deltaRatio = deltaMs / 16.6667;
1550
+ lastElapsedTime = elapsedTime;
1551
+
1552
+ followPointTime += deltaMs * 0.001 * spiritMotion.followTimeScale;
1553
+ autoFollowPoint.set(
1554
+ Math.cos(followPointTime * 0.9) * spiritMotion.followX,
1555
+ Math.cos(followPointTime * 2.8) * spiritMotion.followY,
1556
+ Math.sin(followPointTime * 1.55) * spiritMotion.followZ
1557
+ );
1558
+ followPoint.lerp(autoFollowPoint, Math.min(1.0, spiritMotion.followBlend * deltaRatio));
1559
+
1560
+ if (simulationReady) {
1561
+ const previousTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
1562
+ try {
1563
+ positionMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1564
+ positionMaterial.uniforms.u_defaultTexture.value = defaultTexture;
1565
+ positionMaterial.uniforms.u_mouse3d.value.copy(followPoint);
1566
+ positionMaterial.uniforms.u_speed.value = spiritMotion.particleSpeed * deltaRatio;
1567
+ positionMaterial.uniforms.u_dieSpeed.value = spiritMotion.lifeDecay * deltaRatio;
1568
+ positionMaterial.uniforms.u_time.value = elapsedTime;
1569
+ positionMaterial.uniforms.u_initAnimation.value = Math.min(1, elapsedTime * 0.25);
1570
+ renderComputePass(positionMaterial, nextPositionTarget);
1571
+
1572
+ const oldPositionTarget = currentPositionTarget;
1573
+ currentPositionTarget = nextPositionTarget;
1574
+ nextPositionTarget = oldPositionTarget;
1575
+ renderer.setRenderTarget(previousTarget);
1576
+
1577
+ particleMaterial.uniforms.u_positionTexture.value = currentPositionTarget.texture;
1578
+ } catch (error) {
1579
+ simulationReady = false;
1580
+ if (!simulationWarningShown) {
1581
+ simulationWarningShown = true;
1582
+ console.warn('[BackgroundThemes] Spirit particle simulation stopped.', error);
1583
+ }
1584
+ if (renderer.setRenderTarget) {
1585
+ renderer.setRenderTarget(previousTarget);
1586
+ }
1587
+ }
1588
+ }
1589
+
1590
+ syncParticleView(camera, elapsedTime);
1591
+ };
1592
+
1593
+ themeGroup.customDispose = () => {
1594
+ themeGroup.traverse(child => {
1595
+ if (child.geometry) child.geometry.dispose();
1596
+ if (child.material) {
1597
+ if (Array.isArray(child.material)) {
1598
+ child.material.forEach(material => material.dispose());
1599
+ } else {
1600
+ child.material.dispose();
1601
+ }
1602
+ }
1603
+ });
1604
+ defaultTexture.dispose();
1605
+ positionTargetA.dispose();
1606
+ positionTargetB.dispose();
1607
+ copyMaterial.dispose();
1608
+ positionMaterial.dispose();
1609
+ particleMaterial.dispose();
1610
+ backdropMaterial.dispose();
1611
+ };
1612
+
1613
+ return setThemeAnimationMode(themeGroup, THEME_ANIMATION_MODE.Continuous);
1614
+ },
1615
+ dispose: (themeObject) => { disposeThemeObject(themeObject); }
1616
+ },
1617
+
1618
+ // Theme 9: None (no background)
1619
+ 'None': { create: (scene) => { scene.background = new THREE.Color(THEME_SURFACE_BG); return null; }, dispose: (themeObject) => { } }
1620
+ };
1621
1621
 
1622
1622
  // --- Expose theme manager on window ---
1623
- window.MindMapThemes = {
1624
- getTheme: (themeName) => themes[themeName],
1625
- getAnimationMode: getThemeAnimationMode,
1626
- shouldAnimate: shouldAnimateTheme,
1627
- runAnimationIfNeeded: runThemeAnimationIfNeeded,
1628
- disposeShared: () => { if (plusTexture) { plusTexture.dispose(); plusTexture = null; } console.log('[BackgroundThemes] Shared resources disposed.'); }
1629
- };
1623
+ window.MindMapThemes = {
1624
+ getTheme: (themeName) => themes[themeName],
1625
+ getAnimationMode: getThemeAnimationMode,
1626
+ shouldAnimate: shouldAnimateTheme,
1627
+ runAnimationIfNeeded: runThemeAnimationIfNeeded,
1628
+ disposeShared: () => { if (plusTexture) { plusTexture.dispose(); plusTexture = null; } console.log('[BackgroundThemes] Shared resources disposed.'); }
1629
+ };
1630
1630
 
1631
1631
  console.log('✅ background-themes.js loaded (InfiniteGridHelper integrated)');
1632
1632
  })();