andorxotnot commited on
Commit
55136bb
·
verified ·
1 Parent(s): 8d7ad67

Deploy from anycoder

Browse files
Files changed (1) hide show
  1. index.html +651 -19
index.html CHANGED
@@ -1,19 +1,651 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>3D Continuous Particle Life</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --primary: #00f2ff;
11
+ --glass-bg: rgba(15, 23, 42, 0.6);
12
+ --glass-border: rgba(255, 255, 255, 0.1);
13
+ --text: #e2e8f0;
14
+ }
15
+
16
+ * {
17
+ margin: 0;
18
+ padding: 0;
19
+ box-sizing: border-box;
20
+ user-select: none;
21
+ }
22
+
23
+ body {
24
+ overflow: hidden;
25
+ background-color: #050505;
26
+ font-family: 'Inter', sans-serif;
27
+ color: var(--text);
28
+ }
29
+
30
+ /* Canvas */
31
+ #canvas-container {
32
+ position: fixed;
33
+ top: 0;
34
+ left: 0;
35
+ width: 100%;
36
+ height: 100%;
37
+ z-index: 1;
38
+ }
39
+
40
+ /* Header Link */
41
+ .brand-link {
42
+ position: fixed;
43
+ top: 20px;
44
+ left: 50%;
45
+ transform: translateX(-50%);
46
+ z-index: 100;
47
+ color: var(--primary);
48
+ text-decoration: none;
49
+ font-weight: 700;
50
+ font-size: 0.9rem;
51
+ background: var(--glass-bg);
52
+ padding: 8px 16px;
53
+ border-radius: 20px;
54
+ border: 1px solid var(--glass-border);
55
+ backdrop-filter: blur(10px);
56
+ transition: all 0.3s ease;
57
+ text-transform: uppercase;
58
+ letter-spacing: 1px;
59
+ box-shadow: 0 4px 15px rgba(0, 242, 255, 0.1);
60
+ }
61
+
62
+ .brand-link:hover {
63
+ background: rgba(0, 242, 255, 0.1);
64
+ box-shadow: 0 4px 25px rgba(0, 242, 255, 0.3);
65
+ transform: translateX(-50%) translateY(-2px);
66
+ }
67
+
68
+ /* UI Overlay */
69
+ .ui-panel {
70
+ position: fixed;
71
+ top: 20px;
72
+ right: 20px;
73
+ width: 300px;
74
+ background: var(--glass-bg);
75
+ backdrop-filter: blur(12px);
76
+ -webkit-backdrop-filter: blur(12px);
77
+ border: 1px solid var(--glass-border);
78
+ border-radius: 16px;
79
+ padding: 20px;
80
+ z-index: 10;
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 15px;
84
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
85
+ transition: transform 0.3s ease, opacity 0.3s ease;
86
+ max-height: 90vh;
87
+ overflow-y: auto;
88
+ }
89
+
90
+ .ui-panel.hidden {
91
+ transform: translateX(120%);
92
+ opacity: 0;
93
+ }
94
+
95
+ .panel-header {
96
+ display: flex;
97
+ justify-content: space-between;
98
+ align-items: center;
99
+ margin-bottom: 5px;
100
+ }
101
+
102
+ h1 {
103
+ font-size: 1.1rem;
104
+ font-weight: 700;
105
+ background: linear-gradient(90deg, #fff, #94a3b8);
106
+ -webkit-background-clip: text;
107
+ -webkit-text-fill-color: transparent;
108
+ }
109
+
110
+ .control-group {
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 8px;
114
+ }
115
+
116
+ label {
117
+ font-size: 0.8rem;
118
+ color: #94a3b8;
119
+ display: flex;
120
+ justify-content: space-between;
121
+ }
122
+
123
+ input[type="range"] {
124
+ -webkit-appearance: none;
125
+ width: 100%;
126
+ height: 4px;
127
+ background: rgba(255,255,255,0.1);
128
+ border-radius: 2px;
129
+ outline: none;
130
+ }
131
+
132
+ input[type="range"]::-webkit-slider-thumb {
133
+ -webkit-appearance: none;
134
+ width: 14px;
135
+ height: 14px;
136
+ border-radius: 50%;
137
+ background: var(--primary);
138
+ cursor: pointer;
139
+ transition: transform 0.1s;
140
+ }
141
+
142
+ input[type="range"]::-webkit-slider-thumb:hover {
143
+ transform: scale(1.2);
144
+ }
145
+
146
+ button {
147
+ background: rgba(255,255,255,0.05);
148
+ border: 1px solid var(--glass-border);
149
+ color: white;
150
+ padding: 10px;
151
+ border-radius: 8px;
152
+ cursor: pointer;
153
+ font-weight: 500;
154
+ transition: all 0.2s;
155
+ font-family: 'Inter', sans-serif;
156
+ }
157
+
158
+ button:hover {
159
+ background: var(--primary);
160
+ color: black;
161
+ border-color: var(--primary);
162
+ }
163
+
164
+ button.secondary {
165
+ background: transparent;
166
+ border: 1px solid rgba(255,255,255,0.2);
167
+ }
168
+ button.secondary:hover {
169
+ background: rgba(255,255,255,0.1);
170
+ color: white;
171
+ }
172
+
173
+ .toggle-btn {
174
+ position: fixed;
175
+ top: 20px;
176
+ right: 20px;
177
+ z-index: 11;
178
+ width: 40px;
179
+ height: 40px;
180
+ border-radius: 50%;
181
+ background: var(--glass-bg);
182
+ backdrop-filter: blur(10px);
183
+ border: 1px solid var(--glass-border);
184
+ color: white;
185
+ display: none; /* Shown via JS logic if needed, or just use CSS media queries */
186
+ align-items: center;
187
+ justify-content: center;
188
+ cursor: pointer;
189
+ }
190
+
191
+ .stats {
192
+ font-size: 0.7rem;
193
+ color: #64748b;
194
+ margin-top: 10px;
195
+ text-align: center;
196
+ }
197
+
198
+ /* Scrollbar */
199
+ .ui-panel::-webkit-scrollbar {
200
+ width: 4px;
201
+ }
202
+ .ui-panel::-webkit-scrollbar-thumb {
203
+ background: rgba(255,255,255,0.2);
204
+ border-radius: 4px;
205
+ }
206
+
207
+ @media (max-width: 600px) {
208
+ .ui-panel {
209
+ width: calc(100% - 40px);
210
+ bottom: 20px;
211
+ top: auto;
212
+ max-height: 50vh;
213
+ }
214
+ .brand-link {
215
+ top: 10px;
216
+ font-size: 0.75rem;
217
+ }
218
+ }
219
+ </style>
220
+
221
+ <!-- Three.js and dependencies -->
222
+ <script type="importmap">
223
+ {
224
+ "imports": {
225
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
226
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
227
+ }
228
+ }
229
+ </script>
230
+ </head>
231
+ <body>
232
+
233
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link">Built with anycoder</a>
234
+
235
+ <div id="canvas-container"></div>
236
+
237
+ <div class="ui-panel" id="uiPanel">
238
+ <div class="panel-header">
239
+ <h1>Particle Life 3D</h1>
240
+ </div>
241
+
242
+ <div class="control-group">
243
+ <label>Particles <span id="val-count">1000</span></label>
244
+ <input type="range" id="inp-count" min="200" max="2000" step="100" value="1000">
245
+ </div>
246
+
247
+ <div class="control-group">
248
+ <label>Interaction Radius <span id="val-radius">30</span></label>
249
+ <input type="range" id="inp-radius" min="10" max="100" value="30">
250
+ </div>
251
+
252
+ <div class="control-group">
253
+ <label>Force Strength <span id="val-force">1.0</span></label>
254
+ <input type="range" id="inp-force" min="0.1" max="5.0" step="0.1" value="1.0">
255
+ </div>
256
+
257
+ <div class="control-group">
258
+ <label>Friction <span id="val-friction">0.85</span></label>
259
+ <input type="range" id="inp-friction" min="0.50" max="0.99" step="0.01" value="0.85">
260
+ </div>
261
+
262
+ <button id="btn-randomize">🎲 Randomize Rules</button>
263
+ <button id="btn-reset" class="secondary">Example: Cells</button>
264
+ <button id="btn-reset-snake" class="secondary">Example: Serpents</button>
265
+
266
+ <div class="stats" id="stats">
267
+ FPS: 60 | Types: 4
268
+ </div>
269
+ </div>
270
+
271
+ <script type="module">
272
+ import * as THREE from 'three';
273
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
274
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
275
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
276
+ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
277
+
278
+ // --- Configuration ---
279
+ const CONFIG = {
280
+ particleCount: 1000,
281
+ types: 4,
282
+ radius: 30,
283
+ forceFactor: 1.0,
284
+ friction: 0.85,
285
+ worldSize: 200,
286
+ wrap: true
287
+ };
288
+
289
+ // --- State ---
290
+ let particles = [];
291
+ let rules = []; // Interaction matrix
292
+ let typeColors = [];
293
+
294
+ // --- Three.js Setup ---
295
+ const container = document.getElementById('canvas-container');
296
+ const scene = new THREE.Scene();
297
+ scene.fog = new THREE.FogExp2(0x050505, 0.002);
298
+
299
+ const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
300
+ camera.position.set(0, 100, 250);
301
+
302
+ const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
303
+ renderer.setSize(window.innerWidth, window.innerHeight);
304
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap pixel ratio for performance
305
+ container.appendChild(renderer.domElement);
306
+
307
+ // Post-processing (Bloom)
308
+ const renderScene = new RenderPass(scene, camera);
309
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
310
+ bloomPass.threshold = 0.1;
311
+ bloomPass.strength = 1.2; // Glowing effect
312
+ bloomPass.radius = 0.5;
313
+
314
+ const composer = new EffectComposer(renderer);
315
+ composer.addPass(renderScene);
316
+ composer.addPass(bloomPass);
317
+
318
+ // Controls
319
+ const controls = new OrbitControls(camera, renderer.domElement);
320
+ controls.enableDamping = true;
321
+ controls.dampingFactor = 0.05;
322
+ controls.autoRotate = true;
323
+ controls.autoRotateSpeed = 0.5;
324
+
325
+ // Lighting
326
+ const ambientLight = new THREE.AmbientLight(0x404040);
327
+ scene.add(ambientLight);
328
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1);
329
+ dirLight.position.set(10, 10, 10);
330
+ scene.add(dirLight);
331
+
332
+ // --- Particle System (InstancedMesh) ---
333
+ const geometry = new THREE.SphereGeometry(1, 16, 16);
334
+ const material = new THREE.MeshStandardMaterial({
335
+ color: 0xffffff,
336
+ roughness: 0.2,
337
+ metalness: 0.8,
338
+ emissive: 0xffffff,
339
+ emissiveIntensity: 0.2
340
+ });
341
+
342
+ let instancedMesh;
343
+ const dummy = new THREE.Object3D();
344
+ const _color = new THREE.Color();
345
+
346
+ // --- Logic ---
347
+
348
+ function initSystem() {
349
+ // Clear old mesh
350
+ if (instancedMesh) {
351
+ scene.remove(instancedMesh);
352
+ instancedMesh.dispose();
353
+ }
354
+
355
+ // Create Colors
356
+ typeColors = [
357
+ new THREE.Color(0x00f2ff), // Cyan
358
+ new THREE.Color(0xff0055), // Magenta
359
+ new THREE.Color(0x00ffaa), // Green
360
+ new THREE.Color(0xffaa00) // Orange
361
+ ];
362
+ CONFIG.types = typeColors.length;
363
+
364
+ // Create Particles
365
+ particles = new Float32Array(CONFIG.particleCount * 8); // x, y, z, vx, vy, vz, type, size
366
+
367
+ for (let i = 0; i < CONFIG.particleCount; i++) {
368
+ const i8 = i * 8;
369
+ // Position
370
+ particles[i8] = (Math.random() - 0.5) * CONFIG.worldSize;
371
+ particles[i8+1] = (Math.random() - 0.5) * CONFIG.worldSize;
372
+ particles[i8+2] = (Math.random() - 0.5) * CONFIG.worldSize;
373
+ // Velocity
374
+ particles[i8+3] = 0;
375
+ particles[i8+4] = 0;
376
+ particles[i8+5] = 0;
377
+ // Type
378
+ particles[i8+6] = Math.floor(Math.random() * CONFIG.types);
379
+ // Size (base)
380
+ particles[i8+7] = 1.0 + Math.random() * 1.5;
381
+ }
382
+
383
+ // Create Instanced Mesh
384
+ instancedMesh = new THREE.InstancedMesh(geometry, material, CONFIG.particleCount);
385
+ instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
386
+ scene.add(instancedMesh);
387
+
388
+ randomizeRules();
389
+ }
390
+
391
+ function randomizeRules() {
392
+ // Generate interaction matrix: force between type A and B
393
+ rules = [];
394
+ for (let i = 0; i < CONFIG.types; i++) {
395
+ rules[i] = [];
396
+ for (let j = 0; j < CONFIG.types; j++) {
397
+ // Value between -1 (repel) and 1 (attract)
398
+ rules[i][j] = (Math.random() * 2 - 1);
399
+ }
400
+ }
401
+ console.log("Rules Randomized", rules);
402
+ }
403
+
404
+ function setPreset(name) {
405
+ // Presets for interesting behaviors
406
+ if (name === 'cells') {
407
+ // Similar types attract, different repel slightly
408
+ for(let i=0; i<CONFIG.types; i++) {
409
+ for(let j=0; j<CONFIG.types; j++) {
410
+ if (i === j) rules[i][j] = 0.8;
411
+ else rules[i][j] = -0.4;
412
+ }
413
+ }
414
+ } else if (name === 'snakes') {
415
+ // Cyclic attraction: 0->1, 1->2, 2->3, 3->0
416
+ for(let i=0; i<CONFIG.types; i++) {
417
+ for(let j=0; j<CONFIG.types; j++) {
418
+ rules[i][j] = 0;
419
+ }
420
+ rules[i][(i+1)%CONFIG.types] = 0.6; // Chase next
421
+ rules[i][(i-1+CONFIG.types)%CONFIG.types] = -0.2; // Run from prev
422
+ }
423
+ }
424
+ }
425
+
426
+ // --- Physics Loop ---
427
+
428
+ function updatePhysics() {
429
+ const count = CONFIG.particleCount;
430
+ const rMax = CONFIG.radius;
431
+ const rMaxSq = rMax * rMax;
432
+ const forceFactor = CONFIG.forceFactor;
433
+ const friction = CONFIG.friction;
434
+ const worldSize = CONFIG.worldSize;
435
+ const halfWorld = worldSize / 2;
436
+
437
+ // Brute force O(N^2) - acceptable for N < 1500 in JS on modern devices
438
+ // Optimized slightly by pre-calculating constants
439
+
440
+ for (let i = 0; i < count; i++) {
441
+ const i8 = i * 8;
442
+ let fx = 0, fy = 0, fz = 0;
443
+ const typeI = particles[i8+6];
444
+
445
+ const px = particles[i8];
446
+ const py = particles[i8+1];
447
+ const pz = particles[i8+2];
448
+
449
+ for (let j = 0; j < count; j++) {
450
+ if (i === j) continue;
451
+
452
+ const j8 = j * 8;
453
+ let dx = particles[j8] - px;
454
+ let dy = particles[j8+1] - py;
455
+ let dz = particles[j8+2] - pz;
456
+
457
+ // Wrap around distance for continuous field illusion
458
+ if (dx > halfWorld) dx -= worldSize;
459
+ if (dx < -halfWorld) dx += worldSize;
460
+ if (dy > halfWorld) dy -= worldSize;
461
+ if (dy < -halfWorld) dy += worldSize;
462
+ if (dz > halfWorld) dz -= worldSize;
463
+ if (dz < -halfWorld) dz += worldSize;
464
+
465
+ const distSq = dx*dx + dy*dy + dz*dz;
466
+
467
+ if (distSq > 0 && distSq < rMaxSq) {
468
+ const dist = Math.sqrt(distSq);
469
+ const q = dist / rMax; // Normalized distance 0..1
470
+ const typeJ = particles[j8+6];
471
+
472
+ // Force calculation
473
+ // 1. Repulsion if very close (prevent overlap)
474
+ // 2. Interaction based on rule matrix
475
+
476
+ let f = 0;
477
+
478
+ if (q < 0.3) {
479
+ // Strong repulsion
480
+ f = q - 1;
481
+ } else {
482
+ // Rule based force.
483
+ // Smooth curve: rises then falls off
484
+ // Standard Particle Life formula variant
485
+ const g = rules[typeI][typeJ];
486
+ // Peak force at q=0.5 approx
487
+ f = g * (1 - Math.abs(2 * q - 1)) * 0.5;
488
+ }
489
+
490
+ // Normalize force vector
491
+ const fScaled = (f * forceFactor) / dist;
492
+ fx += dx * fScaled;
493
+ fy += dy * fScaled;
494
+ fz += dz * fScaled;
495
+ }
496
+ }
497
+
498
+ // Apply Force to Velocity
499
+ particles[i8+3] = (particles[i8+3] + fx) * friction;
500
+ particles[i8+4] = (particles[i8+4] + fy) * friction;
501
+ particles[i8+5] = (particles[i8+5] + fz) * friction;
502
+
503
+ // Update Position
504
+ particles[i8] += particles[i8+3];
505
+ particles[i8+1] += particles[i8+4];
506
+ particles[i8+2] += particles[i8+5];
507
+
508
+ // Boundary Wrap
509
+ if (particles[i8] <= -halfWorld) particles[i8] += worldSize;
510
+ if (particles[i8] >= halfWorld) particles[i8] -= worldSize;
511
+ if (particles[i8+1] <= -halfWorld) particles[i8+1] += worldSize;
512
+ if (particles[i8+1] >= halfWorld) particles[i8+1] -= worldSize;
513
+ if (particles[i8+2] <= -halfWorld) particles[i8+2] += worldSize;
514
+ if (particles[i8+2] >= halfWorld) particles[i8+2] -= worldSize;
515
+ }
516
+ }
517
+
518
+ function updateVisuals() {
519
+ const count = CONFIG.particleCount;
520
+
521
+ for (let i = 0; i < count; i++) {
522
+ const i8 = i * 8;
523
+
524
+ dummy.position.set(
525
+ particles[i8],
526
+ particles[i8+1],
527
+ particles[i8+2]
528
+ );
529
+
530
+ // Dynamic Scale based on velocity (stretch effect)
531
+ const speed = Math.sqrt(particles[i8+3]**2 + particles[i8+4]**2 + particles[i8+5]**2);
532
+ const baseSize = particles[i8+7];
533
+ const scale = baseSize + speed * 2;
534
+
535
+ // Orient towards velocity for "looking alive"
536
+ // Not strictly necessary for spheres but helps if we change geometry later
537
+ // dummy.lookAt(dummy.position.clone().add(new THREE.Vector3(particles[i8+3], particles[i8+4], particles[i8+5])));
538
+
539
+ dummy.scale.set(scale, scale, scale);
540
+ dummy.updateMatrix();
541
+
542
+ instancedMesh.setMatrixAt(i, dummy.matrix);
543
+
544
+ // Color
545
+ const type = particles[i8+6];
546
+ _color.copy(typeColors[type]);
547
+
548
+ // Brighten based on speed (activity)
549
+ const intensity = 1 + speed * 0.5;
550
+ _color.multiplyScalar(intensity);
551
+
552
+ instancedMesh.setColorAt(i, _color);
553
+ }
554
+ instancedMesh.instanceMatrix.needsUpdate = true;
555
+ if(instancedMesh.instanceColor) instancedMesh.instanceColor.needsUpdate = true;
556
+ }
557
+
558
+ // --- Main Loop ---
559
+ const statsEl = document.getElementById('stats');
560
+ let lastTime = 0;
561
+ let frames = 0;
562
+ let fpsTime = 0;
563
+
564
+ function animate(time) {
565
+ requestAnimationFrame(animate);
566
+
567
+ // FPS Calculation
568
+ frames++;
569
+ if (time - fpsTime > 1000) {
570
+ statsEl.innerHTML = `FPS: ${frames} | Particles: ${CONFIG.particleCount}`;
571
+ frames = 0;
572
+ fpsTime = time;
573
+ }
574
+
575
+ updatePhysics();
576
+ updateVisuals();
577
+ controls.update();
578
+
579
+ // Render with Bloom
580
+ composer.render();
581
+ }
582
+
583
+ // --- UI Event Listeners ---
584
+
585
+ document.getElementById('inp-count').addEventListener('input', (e) => {
586
+ CONFIG.particleCount = parseInt(e.target.value);
587
+ document.getElementById('val-count').innerText = CONFIG.particleCount;
588
+ initSystem();
589
+ });
590
+
591
+ document.getElementById('inp-radius').addEventListener('input', (e) => {
592
+ CONFIG.radius = parseInt(e.target.value);
593
+ document.getElementById('val-radius').innerText = CONFIG.radius;
594
+ });
595
+
596
+ document.getElementById('inp-force').addEventListener('input', (e) => {
597
+ CONFIG.forceFactor = parseFloat(e.target.value);
598
+ document.getElementById('val-force').innerText = CONFIG.forceFactor;
599
+ });
600
+
601
+ document.getElementById('inp-friction').addEventListener('input', (e) => {
602
+ CONFIG.friction = parseFloat(e.target.value);
603
+ document.getElementById('val-friction').innerText = CONFIG.friction;
604
+ });
605
+
606
+ document.getElementById('btn-randomize').addEventListener('click', () => {
607
+ randomizeRules();
608
+ // Add a flash effect to canvas maybe?
609
+ });
610
+
611
+ document.getElementById('btn-reset').addEventListener('click', () => {
612
+ setPreset('cells');
613
+ // Reset Friction/Force for this preset
614
+ CONFIG.friction = 0.80;
615
+ CONFIG.forceFactor = 1.2;
616
+ CONFIG.radius = 40;
617
+ updateUiValues();
618
+ });
619
+
620
+ document.getElementById('btn-reset-snake').addEventListener('click', () => {
621
+ setPreset('snakes');
622
+ CONFIG.friction = 0.90;
623
+ CONFIG.forceFactor = 2.0;
624
+ CONFIG.radius = 60;
625
+ updateUiValues();
626
+ });
627
+
628
+ function updateUiValues() {
629
+ document.getElementById('inp-friction').value = CONFIG.friction;
630
+ document.getElementById('val-friction').innerText = CONFIG.friction;
631
+ document.getElementById('inp-force').value = CONFIG.forceFactor;
632
+ document.getElementById('val-force').innerText = CONFIG.forceFactor;
633
+ document.getElementById('inp-radius').value = CONFIG.radius;
634
+ document.getElementById('val-radius').innerText = CONFIG.radius;
635
+ }
636
+
637
+ // Resize Handler
638
+ window.addEventListener('resize', () => {
639
+ camera.aspect = window.innerWidth / window.innerHeight;
640
+ camera.updateProjectionMatrix();
641
+ renderer.setSize(window.innerWidth, window.innerHeight);
642
+ composer.setSize(window.innerWidth, window.innerHeight);
643
+ });
644
+
645
+ // --- Boot ---
646
+ initSystem();
647
+ animate();
648
+
649
+ </script>
650
+ </body>
651
+ </html>