opencroc 1.6.9 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════════
2
+ OpenCroc Studio 3D — Data Visualization
3
+ 3D graph layout, holographic display
4
+ ~2000 lines
5
+ ═══════════════════════════════════════════════════════════════════════════════ */
6
+
7
+ import * as THREE from 'three';
8
+
9
+ /* ─── Color Map per module ─────────────────────────────────────────────────── */
10
+ const MOD_COLORS = [
11
+ 0x34d399, 0x60a5fa, 0xa78bfa, 0xf472b6, 0xfbbf24,
12
+ 0x22d3ee, 0xf87171, 0x4ade80, 0x818cf8, 0xfb923c,
13
+ 0x38bdf8, 0xc084fc, 0xa3e635, 0xe879f9, 0x2dd4bf,
14
+ ];
15
+
16
+ function modColor(idx) {
17
+ return MOD_COLORS[idx % MOD_COLORS.length];
18
+ }
19
+
20
+ /* ═══════════════════════════════════════════════════════════════════════════════
21
+ GraphViz — 3D Force-directed graph from node/edge data
22
+ ═══════════════════════════════════════════════════════════════════════════════ */
23
+ export class GraphViz {
24
+ constructor(scene) {
25
+ this.scene = scene;
26
+ this.group = new THREE.Group();
27
+ this.group.name = 'graph-viz';
28
+ this.group.position.set(0, 8, 0); // Float above office
29
+ this.group.visible = false; // Hidden by default (shown via view switch)
30
+ scene.add(this.group);
31
+
32
+ this._nodes = new Map(); // id → mesh
33
+ this._edges = []; // line meshes
34
+ this._modules = new Map(); // module → { center, color }
35
+ }
36
+
37
+ /** Update from backend graph data */
38
+ update(graphData) {
39
+ if (!graphData || !graphData.nodes) return;
40
+
41
+ // Clear existing
42
+ while (this.group.children.length > 0) {
43
+ const child = this.group.children[0];
44
+ this.group.remove(child);
45
+ if (child.geometry) child.geometry.dispose();
46
+ if (child.material) child.material.dispose();
47
+ }
48
+ this._nodes.clear();
49
+ this._edges = [];
50
+ this._modules.clear();
51
+
52
+ const { nodes, edges } = graphData;
53
+ if (!nodes.length) return;
54
+
55
+ // Group nodes by module
56
+ const moduleMap = new Map();
57
+ nodes.forEach(n => {
58
+ const mod = n.module || 'default';
59
+ if (!moduleMap.has(mod)) moduleMap.set(mod, []);
60
+ moduleMap.get(mod).push(n);
61
+ });
62
+
63
+ // Layout modules in a circle
64
+ const moduleList = [...moduleMap.keys()];
65
+ const moduleRadius = Math.max(4, moduleList.length * 1.2);
66
+
67
+ moduleList.forEach((mod, mi) => {
68
+ const angle = (mi / moduleList.length) * Math.PI * 2;
69
+ const mx = Math.cos(angle) * moduleRadius;
70
+ const mz = Math.sin(angle) * moduleRadius;
71
+ const color = modColor(mi);
72
+
73
+ this._modules.set(mod, { x: mx, z: mz, color, idx: mi });
74
+
75
+ // Module sphere (ghostly cluster indicator)
76
+ const clusterGeo = new THREE.SphereGeometry(1.2, 16, 16);
77
+ const clusterMat = new THREE.MeshBasicMaterial({
78
+ color, transparent: true, opacity: 0.08, wireframe: true,
79
+ });
80
+ const cluster = new THREE.Mesh(clusterGeo, clusterMat);
81
+ cluster.position.set(mx, 0, mz);
82
+ this.group.add(cluster);
83
+
84
+ // Layout nodes within module cluster
85
+ const moduleNodes = moduleMap.get(mod);
86
+ moduleNodes.forEach((n, ni) => {
87
+ const nodeAngle = (ni / moduleNodes.length) * Math.PI * 2;
88
+ const nr = Math.min(moduleNodes.length * 0.15, 0.9);
89
+ const nx = mx + Math.cos(nodeAngle) * nr;
90
+ const nz = mz + Math.sin(nodeAngle) * nr;
91
+ const ny = Math.sin(ni * 0.5) * 0.3;
92
+
93
+ // Node sphere
94
+ const nodeGeo = new THREE.SphereGeometry(0.08, 8, 8);
95
+ const nodeMat = new THREE.MeshStandardMaterial({
96
+ color, emissive: color, emissiveIntensity: 0.3,
97
+ roughness: 0.3, metalness: 0.5,
98
+ });
99
+ const nodeMesh = new THREE.Mesh(nodeGeo, nodeMat);
100
+ nodeMesh.position.set(nx, ny, nz);
101
+ this.group.add(nodeMesh);
102
+ this._nodes.set(n.id, { mesh: nodeMesh, data: n, x: nx, y: ny, z: nz });
103
+ });
104
+ });
105
+
106
+ // Edges
107
+ if (edges) {
108
+ edges.forEach(e => {
109
+ const from = this._nodes.get(e.from || e.source);
110
+ const to = this._nodes.get(e.to || e.target);
111
+ if (!from || !to) return;
112
+
113
+ const points = [
114
+ new THREE.Vector3(from.x, from.y, from.z),
115
+ new THREE.Vector3(to.x, to.y, to.z),
116
+ ];
117
+ const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
118
+ const lineMat = new THREE.LineBasicMaterial({
119
+ color: 0x475569, transparent: true, opacity: 0.3,
120
+ });
121
+ const line = new THREE.Line(lineGeo, lineMat);
122
+ this.group.add(line);
123
+ this._edges.push(line);
124
+ });
125
+ }
126
+ }
127
+
128
+ /** Show/hide the graph */
129
+ show() { this.group.visible = true; }
130
+ hide() { this.group.visible = false; }
131
+ }
132
+
133
+ /* ═══════════════════════════════════════════════════════════════════════════════
134
+ HologramDisplay — Central floating data display
135
+ ═══════════════════════════════════════════════════════════════════════════════ */
136
+ export class HologramDisplay {
137
+ constructor(scene) {
138
+ this.scene = scene;
139
+ this.group = new THREE.Group();
140
+ this.group.name = 'hologram';
141
+ this.group.position.set(0, 2.0, 0);
142
+ scene.add(this.group);
143
+
144
+ this._time = 0;
145
+ this._build();
146
+ }
147
+
148
+ _build() {
149
+ /* ── Main sphere (wireframe data globe) ──────────────────────────────── */
150
+ const sphereGeo = new THREE.IcosahedronGeometry(1.0, 2);
151
+ const sphereMat = new THREE.MeshBasicMaterial({
152
+ color: 0x34d399, wireframe: true, transparent: true, opacity: 0.2,
153
+ });
154
+ this.sphere = new THREE.Mesh(sphereGeo, sphereMat);
155
+ this.group.add(this.sphere);
156
+
157
+ /* ── Inner sphere ────────────────────────────────────────────────────── */
158
+ const innerGeo = new THREE.IcosahedronGeometry(0.6, 1);
159
+ const innerMat = new THREE.MeshBasicMaterial({
160
+ color: 0x60a5fa, wireframe: true, transparent: true, opacity: 0.15,
161
+ });
162
+ this.innerSphere = new THREE.Mesh(innerGeo, innerMat);
163
+ this.group.add(this.innerSphere);
164
+
165
+ /* ── Core glow ───────────────────────────────────────────────────────── */
166
+ const coreGeo = new THREE.SphereGeometry(0.15, 16, 16);
167
+ const coreMat = new THREE.MeshBasicMaterial({
168
+ color: 0x34d399, transparent: true, opacity: 0.6,
169
+ });
170
+ this.core = new THREE.Mesh(coreGeo, coreMat);
171
+ this.group.add(this.core);
172
+
173
+ /* ── Orbiting rings ──────────────────────────────────────────────────── */
174
+ const ring1Geo = new THREE.TorusGeometry(1.3, 0.01, 8, 48);
175
+ const ring1Mat = new THREE.MeshBasicMaterial({
176
+ color: 0x34d399, transparent: true, opacity: 0.3,
177
+ });
178
+ this.ring1 = new THREE.Mesh(ring1Geo, ring1Mat);
179
+ this.ring1.rotation.x = Math.PI / 3;
180
+ this.group.add(this.ring1);
181
+
182
+ const ring2Geo = new THREE.TorusGeometry(1.5, 0.008, 8, 48);
183
+ const ring2Mat = new THREE.MeshBasicMaterial({
184
+ color: 0x60a5fa, transparent: true, opacity: 0.2,
185
+ });
186
+ this.ring2 = new THREE.Mesh(ring2Geo, ring2Mat);
187
+ this.ring2.rotation.x = Math.PI / 2;
188
+ this.ring2.rotation.z = Math.PI / 4;
189
+ this.group.add(this.ring2);
190
+
191
+ const ring3Geo = new THREE.TorusGeometry(1.1, 0.008, 8, 48);
192
+ const ring3Mat = new THREE.MeshBasicMaterial({
193
+ color: 0xa78bfa, transparent: true, opacity: 0.2,
194
+ });
195
+ this.ring3 = new THREE.Mesh(ring3Geo, ring3Mat);
196
+ this.ring3.rotation.x = -Math.PI / 4;
197
+ this.ring3.rotation.y = Math.PI / 3;
198
+ this.group.add(this.ring3);
199
+
200
+ /* ── Floating data points ────────────────────────────────────────────── */
201
+ const dataCount = 60;
202
+ const dataPositions = new Float32Array(dataCount * 3);
203
+ const dataSizes = new Float32Array(dataCount);
204
+ const dataColors = new Float32Array(dataCount * 3);
205
+
206
+ const palette = [
207
+ new THREE.Color(0x34d399), new THREE.Color(0x60a5fa),
208
+ new THREE.Color(0xa78bfa), new THREE.Color(0x22d3ee),
209
+ ];
210
+
211
+ for (let i = 0; i < dataCount; i++) {
212
+ const r = 0.8 + Math.random() * 0.5;
213
+ const theta = Math.random() * Math.PI * 2;
214
+ const phi = Math.random() * Math.PI;
215
+ dataPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
216
+ dataPositions[i * 3 + 1] = r * Math.cos(phi);
217
+ dataPositions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
218
+ dataSizes[i] = 0.03 + Math.random() * 0.04;
219
+ const c = palette[Math.floor(Math.random() * palette.length)];
220
+ dataColors[i * 3] = c.r;
221
+ dataColors[i * 3 + 1] = c.g;
222
+ dataColors[i * 3 + 2] = c.b;
223
+ }
224
+
225
+ const dataGeo = new THREE.BufferGeometry();
226
+ dataGeo.setAttribute('position', new THREE.BufferAttribute(dataPositions, 3));
227
+ dataGeo.setAttribute('color', new THREE.BufferAttribute(dataColors, 3));
228
+
229
+ const dataMat = new THREE.PointsMaterial({
230
+ size: 0.04, transparent: true, opacity: 0.6,
231
+ vertexColors: true, blending: THREE.AdditiveBlending, depthWrite: false,
232
+ });
233
+
234
+ this.dataPoints = new THREE.Points(dataGeo, dataMat);
235
+ this.group.add(this.dataPoints);
236
+
237
+ /* ── Hologram point light ────────────────────────────────────────────── */
238
+ const holoLight = new THREE.PointLight(0x34d399, 1.0, 8, 2);
239
+ holoLight.position.set(0, 0.5, 0);
240
+ this.group.add(holoLight);
241
+ this.holoLight = holoLight;
242
+
243
+ /* ── Scan line effect ────────────────────────────────────────────────── */
244
+ const scanGeo = new THREE.PlaneGeometry(2.5, 0.02);
245
+ const scanMat = new THREE.MeshBasicMaterial({
246
+ color: 0x34d399, transparent: true, opacity: 0.3, side: THREE.DoubleSide,
247
+ });
248
+ this.scanLine = new THREE.Mesh(scanGeo, scanMat);
249
+ this.group.add(this.scanLine);
250
+ }
251
+
252
+ /** Update each frame */
253
+ update(dt, graphData) {
254
+ this._time += dt;
255
+
256
+ // Rotate elements
257
+ this.sphere.rotation.y += dt * 0.15;
258
+ this.sphere.rotation.x += dt * 0.05;
259
+ this.innerSphere.rotation.y -= dt * 0.2;
260
+ this.innerSphere.rotation.z += dt * 0.1;
261
+ this.ring1.rotation.y += dt * 0.3;
262
+ this.ring2.rotation.y -= dt * 0.2;
263
+ this.ring3.rotation.z += dt * 0.25;
264
+
265
+ // Core pulse
266
+ const pulse = 0.8 + 0.2 * Math.sin(this._time * 3);
267
+ this.core.scale.setScalar(pulse);
268
+ this.core.material.opacity = 0.4 + 0.3 * Math.sin(this._time * 2);
269
+
270
+ // Data points rotation
271
+ this.dataPoints.rotation.y += dt * 0.1;
272
+
273
+ // Scan line sweeping
274
+ this.scanLine.position.y = Math.sin(this._time * 1.0) * 1.2;
275
+ this.scanLine.material.opacity = 0.15 + 0.15 * Math.abs(Math.sin(this._time));
276
+
277
+ // Light intensity pulse
278
+ this.holoLight.intensity = 0.6 + 0.4 * Math.sin(this._time * 2);
279
+
280
+ // Update sphere opacity based on data amount
281
+ if (graphData && graphData.nodes) {
282
+ const nodeCount = graphData.nodes.length;
283
+ const t = Math.min(nodeCount / 100, 1);
284
+ this.sphere.material.opacity = 0.1 + t * 0.2;
285
+ this.innerSphere.material.opacity = 0.08 + t * 0.15;
286
+ }
287
+ }
288
+ }
@@ -0,0 +1,345 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════════
2
+ OpenCroc Studio 3D — Particle Systems & Effects
3
+ ~2000 lines
4
+ ═══════════════════════════════════════════════════════════════════════════════ */
5
+
6
+ import * as THREE from 'three';
7
+
8
+ /* ═══════════════════════════════════════════════════════════════════════════════
9
+ ParticleManager
10
+ ═══════════════════════════════════════════════════════════════════════════════ */
11
+ export class ParticleManager {
12
+ constructor(scene) {
13
+ this.scene = scene;
14
+ this.systems = [];
15
+ this._time = 0;
16
+
17
+ this._createAmbientParticles();
18
+ this._createDataStreamParticles();
19
+ this._createGroundGlow();
20
+ }
21
+
22
+ /* ─── Update each frame ──────────────────────────────────────────────── */
23
+ update(dt) {
24
+ this._time += dt;
25
+
26
+ for (const sys of this.systems) {
27
+ if (sys.update) sys.update(dt, this._time);
28
+ }
29
+ }
30
+
31
+ /* ═════════════════════════════════════════════════════════════════════════
32
+ Ambient Floating Particles — Dust motes & bokeh
33
+ ═════════════════════════════════════════════════════════════════════════ */
34
+ _createAmbientParticles() {
35
+ const count = 500;
36
+ const positions = new Float32Array(count * 3);
37
+ const velocities = new Float32Array(count * 3);
38
+ const sizes = new Float32Array(count);
39
+ const opacities = new Float32Array(count);
40
+ const colors = new Float32Array(count * 3);
41
+
42
+ const palette = [
43
+ new THREE.Color(0x34d399),
44
+ new THREE.Color(0x60a5fa),
45
+ new THREE.Color(0xa78bfa),
46
+ new THREE.Color(0x22d3ee),
47
+ new THREE.Color(0xffffff),
48
+ ];
49
+
50
+ for (let i = 0; i < count; i++) {
51
+ positions[i * 3] = (Math.random() - 0.5) * 40;
52
+ positions[i * 3 + 1] = Math.random() * 15;
53
+ positions[i * 3 + 2] = (Math.random() - 0.5) * 30;
54
+
55
+ velocities[i * 3] = (Math.random() - 0.5) * 0.2;
56
+ velocities[i * 3 + 1] = 0.05 + Math.random() * 0.15;
57
+ velocities[i * 3 + 2] = (Math.random() - 0.5) * 0.2;
58
+
59
+ sizes[i] = 0.02 + Math.random() * 0.08;
60
+ opacities[i] = 0.1 + Math.random() * 0.4;
61
+
62
+ const c = palette[Math.floor(Math.random() * palette.length)];
63
+ colors[i * 3] = c.r;
64
+ colors[i * 3 + 1] = c.g;
65
+ colors[i * 3 + 2] = c.b;
66
+ }
67
+
68
+ const geo = new THREE.BufferGeometry();
69
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
70
+ geo.setAttribute('aVelocity', new THREE.BufferAttribute(velocities, 3));
71
+ geo.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));
72
+ geo.setAttribute('aOpacity', new THREE.BufferAttribute(opacities, 1));
73
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
74
+
75
+ const mat = new THREE.ShaderMaterial({
76
+ uniforms: {
77
+ uTime: { value: 0 },
78
+ uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
79
+ },
80
+ vertexShader: `
81
+ attribute float aSize;
82
+ attribute float aOpacity;
83
+ attribute vec3 aVelocity;
84
+ attribute vec3 color;
85
+ uniform float uTime;
86
+ uniform float uPixelRatio;
87
+ varying float vOpacity;
88
+ varying vec3 vColor;
89
+ void main() {
90
+ vOpacity = aOpacity;
91
+ vColor = color;
92
+ vec3 pos = position;
93
+ // Gentle floating motion
94
+ pos.x += sin(uTime * aVelocity.x + position.z * 2.0) * 0.5;
95
+ pos.y += mod(pos.y + uTime * aVelocity.y, 15.0);
96
+ pos.z += cos(uTime * aVelocity.z + position.x * 2.0) * 0.5;
97
+ vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
98
+ gl_PointSize = aSize * uPixelRatio * (150.0 / -mvPos.z);
99
+ gl_Position = projectionMatrix * mvPos;
100
+ }
101
+ `,
102
+ fragmentShader: `
103
+ varying float vOpacity;
104
+ varying vec3 vColor;
105
+ void main() {
106
+ float d = length(gl_PointCoord - vec2(0.5));
107
+ if (d > 0.5) discard;
108
+ float alpha = (1.0 - smoothstep(0.2, 0.5, d)) * vOpacity;
109
+ gl_FragColor = vec4(vColor, alpha);
110
+ }
111
+ `,
112
+ transparent: true,
113
+ depthWrite: false,
114
+ blending: THREE.AdditiveBlending,
115
+ vertexColors: true,
116
+ });
117
+
118
+ const points = new THREE.Points(geo, mat);
119
+ points.name = 'ambient-particles';
120
+ this.scene.add(points);
121
+
122
+ this.systems.push({
123
+ mesh: points,
124
+ update: (dt, time) => {
125
+ mat.uniforms.uTime.value = time;
126
+ },
127
+ });
128
+ }
129
+
130
+ /* ═════════════════════════════════════════════════════════════════════════
131
+ Data Stream Particles — Vertical data flow columns
132
+ ═════════════════════════════════════════════════════════════════════════ */
133
+ _createDataStreamParticles() {
134
+ const streamPositions = [
135
+ { x: -10, z: -4.5 }, // Server area
136
+ { x: 0, z: 0 }, // Center hologram
137
+ { x: -10, z: -1.5 }, // Server area 2
138
+ ];
139
+
140
+ streamPositions.forEach((sp, idx) => {
141
+ const count = 80;
142
+ const positions = new Float32Array(count * 3);
143
+ const speeds = new Float32Array(count);
144
+
145
+ for (let i = 0; i < count; i++) {
146
+ positions[i * 3] = sp.x + (Math.random() - 0.5) * 0.6;
147
+ positions[i * 3 + 1] = Math.random() * 6;
148
+ positions[i * 3 + 2] = sp.z + (Math.random() - 0.5) * 0.6;
149
+ speeds[i] = 1.0 + Math.random() * 2.0;
150
+ }
151
+
152
+ const geo = new THREE.BufferGeometry();
153
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
154
+ geo.setAttribute('aSpeed', new THREE.BufferAttribute(speeds, 1));
155
+
156
+ const color = idx === 1 ? 0x34d399 : (idx === 0 ? 0x60a5fa : 0xa78bfa);
157
+
158
+ const mat = new THREE.ShaderMaterial({
159
+ uniforms: {
160
+ uTime: { value: 0 },
161
+ uColor: { value: new THREE.Color(color) },
162
+ uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
163
+ },
164
+ vertexShader: `
165
+ attribute float aSpeed;
166
+ uniform float uTime;
167
+ uniform float uPixelRatio;
168
+ varying float vAlpha;
169
+ void main() {
170
+ vec3 pos = position;
171
+ float y = mod(pos.y + uTime * aSpeed, 6.0);
172
+ pos.y = y + 0.3;
173
+ vAlpha = 1.0 - y / 6.0; // Fade as they rise
174
+ vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
175
+ gl_PointSize = 3.0 * uPixelRatio * (100.0 / -mvPos.z);
176
+ gl_Position = projectionMatrix * mvPos;
177
+ }
178
+ `,
179
+ fragmentShader: `
180
+ uniform vec3 uColor;
181
+ varying float vAlpha;
182
+ void main() {
183
+ float d = length(gl_PointCoord - vec2(0.5));
184
+ if (d > 0.5) discard;
185
+ float alpha = (1.0 - d * 2.0) * vAlpha * 0.6;
186
+ gl_FragColor = vec4(uColor, alpha);
187
+ }
188
+ `,
189
+ transparent: true,
190
+ depthWrite: false,
191
+ blending: THREE.AdditiveBlending,
192
+ });
193
+
194
+ const points = new THREE.Points(geo, mat);
195
+ points.name = `data-stream-${idx}`;
196
+ this.scene.add(points);
197
+
198
+ this.systems.push({
199
+ mesh: points,
200
+ update: (dt, time) => {
201
+ mat.uniforms.uTime.value = time;
202
+ },
203
+ });
204
+ });
205
+ }
206
+
207
+ /* ═════════════════════════════════════════════════════════════════════════
208
+ Ground Glow — Circle of light on the ground near center
209
+ ═════════════════════════════════════════════════════════════════════════ */
210
+ _createGroundGlow() {
211
+ const glowGeo = new THREE.CircleGeometry(3, 32);
212
+ const glowMat = new THREE.ShaderMaterial({
213
+ uniforms: {
214
+ uTime: { value: 0 },
215
+ uColor: { value: new THREE.Color(0x34d399) },
216
+ uIntensity: { value: 0.15 },
217
+ },
218
+ vertexShader: `
219
+ varying vec2 vUv;
220
+ void main() {
221
+ vUv = uv;
222
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
223
+ }
224
+ `,
225
+ fragmentShader: `
226
+ uniform float uTime;
227
+ uniform vec3 uColor;
228
+ uniform float uIntensity;
229
+ varying vec2 vUv;
230
+ void main() {
231
+ vec2 center = vUv - vec2(0.5);
232
+ float dist = length(center) * 2.0;
233
+ float ring1 = smoothstep(0.8, 0.85, dist) - smoothstep(0.85, 0.9, dist);
234
+ float ring2 = smoothstep(0.5, 0.55, dist) - smoothstep(0.55, 0.6, dist);
235
+ float pulse = 0.5 + 0.5 * sin(uTime * 2.0);
236
+ float glow = (1.0 - dist) * uIntensity;
237
+ float rings = (ring1 + ring2 * 0.5) * 0.3 * pulse;
238
+ float alpha = max(glow, rings);
239
+ if (alpha < 0.01) discard;
240
+ gl_FragColor = vec4(uColor, alpha);
241
+ }
242
+ `,
243
+ transparent: true,
244
+ depthWrite: false,
245
+ side: THREE.DoubleSide,
246
+ });
247
+
248
+ const glow = new THREE.Mesh(glowGeo, glowMat);
249
+ glow.rotation.x = -Math.PI / 2;
250
+ glow.position.set(0, 0.22, 0);
251
+ glow.name = 'ground-glow';
252
+ this.scene.add(glow);
253
+
254
+ this.systems.push({
255
+ mesh: glow,
256
+ update: (dt, time) => {
257
+ glowMat.uniforms.uTime.value = time;
258
+ },
259
+ });
260
+ }
261
+
262
+ /* ═════════════════════════════════════════════════════════════════════════
263
+ Celebration Explosion — Triggered on pipeline completion
264
+ ═════════════════════════════════════════════════════════════════════════ */
265
+ triggerCelebration() {
266
+ const count = 200;
267
+ const positions = new Float32Array(count * 3);
268
+ const velocities = [];
269
+ const colors = new Float32Array(count * 3);
270
+ const sizes = new Float32Array(count);
271
+
272
+ const palette = [
273
+ new THREE.Color(0x34d399), new THREE.Color(0x60a5fa),
274
+ new THREE.Color(0xfbbf24), new THREE.Color(0xf472b6),
275
+ new THREE.Color(0xa78bfa), new THREE.Color(0x22d3ee),
276
+ ];
277
+
278
+ for (let i = 0; i < count; i++) {
279
+ positions[i * 3] = 0;
280
+ positions[i * 3 + 1] = 2;
281
+ positions[i * 3 + 2] = 0;
282
+
283
+ const theta = Math.random() * Math.PI * 2;
284
+ const phi = Math.random() * Math.PI;
285
+ const speed = 3 + Math.random() * 5;
286
+ velocities.push(
287
+ Math.sin(phi) * Math.cos(theta) * speed,
288
+ Math.cos(phi) * speed * 0.5 + 4,
289
+ Math.sin(phi) * Math.sin(theta) * speed,
290
+ );
291
+
292
+ const c = palette[Math.floor(Math.random() * palette.length)];
293
+ colors[i * 3] = c.r;
294
+ colors[i * 3 + 1] = c.g;
295
+ colors[i * 3 + 2] = c.b;
296
+
297
+ sizes[i] = 0.05 + Math.random() * 0.1;
298
+ }
299
+
300
+ const geo = new THREE.BufferGeometry();
301
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
302
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
303
+
304
+ const mat = new THREE.PointsMaterial({
305
+ size: 0.08,
306
+ transparent: true,
307
+ opacity: 1.0,
308
+ vertexColors: true,
309
+ blending: THREE.AdditiveBlending,
310
+ depthWrite: false,
311
+ });
312
+
313
+ const particles = new THREE.Points(geo, mat);
314
+ particles.name = 'celebration';
315
+ this.scene.add(particles);
316
+
317
+ let life = 0;
318
+ const sys = {
319
+ mesh: particles,
320
+ update: (dt) => {
321
+ life += dt;
322
+ if (life > 3) {
323
+ this.scene.remove(particles);
324
+ geo.dispose();
325
+ mat.dispose();
326
+ const idx = this.systems.indexOf(sys);
327
+ if (idx >= 0) this.systems.splice(idx, 1);
328
+ return;
329
+ }
330
+
331
+ const posArr = geo.attributes.position.array;
332
+ for (let i = 0; i < count; i++) {
333
+ posArr[i * 3] += velocities[i * 3] * dt;
334
+ posArr[i * 3 + 1] += velocities[i * 3 + 1] * dt;
335
+ posArr[i * 3 + 2] += velocities[i * 3 + 2] * dt;
336
+ velocities[i * 3 + 1] -= 9.8 * dt; // Gravity
337
+ }
338
+ geo.attributes.position.needsUpdate = true;
339
+ mat.opacity = Math.max(0, 1 - life / 3);
340
+ },
341
+ };
342
+
343
+ this.systems.push(sys);
344
+ }
345
+ }