wop commited on
Commit
b79f369
·
verified ·
1 Parent(s): 860e698

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +800 -18
index.html CHANGED
@@ -1,19 +1,801 @@
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
+
2
+ <!DOCTYPE html>
3
+ <html lang="en" class="h-full">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Coulomb's Law & Electric Field Visualizer</title>
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <style>
11
+ /* Custom styled sliders for consistent UI */
12
+ input[type="range"] {
13
+ -webkit-appearance: none;
14
+ appearance: none;
15
+ background: #2d3748;
16
+ height: 6px;
17
+ border-radius: 9999px;
18
+ outline: none;
19
+ }
20
+ input[type="range"]::-webkit-slider-thumb {
21
+ -webkit-appearance: none;
22
+ appearance: none;
23
+ width: 16px;
24
+ height: 16px;
25
+ border-radius: 50%;
26
+ background: #3b82f6;
27
+ cursor: pointer;
28
+ transition: transform 0.1s ease;
29
+ }
30
+ input[type="range"]::-webkit-slider-thumb:hover {
31
+ transform: scale(1.2);
32
+ }
33
+ /* Glowing neon utilities */
34
+ .neon-shadow-red {
35
+ box-shadow: 0 0 15px rgba(239, 68, 68, 0.6);
36
+ }
37
+ .neon-shadow-blue {
38
+ box-shadow: 0 0 15px rgba(59, 130, 246, 0.6);
39
+ }
40
+ </style>
41
+ </head>
42
+ <body class="bg-[#0b0f19] text-gray-200 h-full overflow-hidden flex flex-col font-sans select-none">
43
+
44
+ <!-- Header / Navbar -->
45
+ <header class="bg-[#111827]/80 backdrop-blur border-b border-gray-800 px-6 py-4 flex items-center justify-between z-10 shrink-0">
46
+ <div class="flex items-center space-x-3">
47
+ <div class="bg-gradient-to-tr from-blue-500 to-red-500 p-2 rounded-lg">
48
+ <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
50
+ </svg>
51
+ </div>
52
+ <div>
53
+ <h1 class="text-lg font-bold tracking-tight text-white flex items-center gap-2">
54
+ Coulomb Field Sandbox <span class="text-xs bg-gray-800 text-blue-400 border border-blue-500/30 px-2 py-0.5 rounded-full font-mono">v2.5</span>
55
+ </h1>
56
+ <p class="text-xs text-gray-400 hidden sm:block">Dynamic electric field lines & many-body Coulomb interactions</p>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Desktop Quick Stats -->
61
+ <div class="flex items-center space-x-6 text-xs text-gray-400 font-mono">
62
+ <div>Charges: <span id="stat-charges" class="text-white font-bold">0</span></div>
63
+ <div>FPS: <span id="stat-fps" class="text-emerald-400 font-bold">60</span></div>
64
+ </div>
65
+ </header>
66
+
67
+ <!-- Main Container Layout -->
68
+ <main class="flex-1 flex flex-col md:flex-row relative overflow-hidden">
69
+
70
+ <!-- Canvas Viewport -->
71
+ <div class="flex-1 relative bg-black overflow-hidden h-full">
72
+ <canvas id="physics-canvas" class="block w-full h-full cursor-grab active:cursor-grabbing"></canvas>
73
+
74
+ <!-- On-Screen Controls Overlay (Floating) -->
75
+ <div class="absolute bottom-6 left-6 right-6 md:right-auto flex flex-wrap gap-3 z-10">
76
+ <button id="btn-add-pos" class="flex items-center gap-2 px-4 py-2.5 bg-red-600/90 hover:bg-red-500 text-white font-semibold rounded-lg shadow-lg hover:shadow-red-500/20 active:scale-95 transition duration-150 text-sm">
77
+ <span class="text-lg font-black">+</span> Add Positive
78
+ </button>
79
+ <button id="btn-add-neg" class="flex items-center gap-2 px-4 py-2.5 bg-blue-600/90 hover:bg-blue-500 text-white font-semibold rounded-lg shadow-lg hover:shadow-blue-500/20 active:scale-95 transition duration-150 text-sm">
80
+ <span class="text-lg font-black">&minus;</span> Add Negative
81
+ </button>
82
+ <button id="btn-clear" class="flex items-center gap-2 px-4 py-2.5 bg-gray-800/90 hover:bg-gray-700 text-gray-300 rounded-lg border border-gray-700 hover:border-gray-600 active:scale-95 transition duration-150 text-sm">
83
+ Clear Sandbox
84
+ </button>
85
+ </div>
86
+
87
+ <!-- Welcome/Instructive overlay banner -->
88
+ <div id="instruction-overlay" class="absolute top-4 left-4 right-4 bg-gray-950/85 border border-gray-800 p-4 rounded-xl max-w-md pointer-events-none transition-opacity duration-500 z-10">
89
+ <h3 class="text-sm font-semibold text-white flex items-center gap-2">
90
+ 💡 Sandbox Instructions
91
+ </h3>
92
+ <ul class="text-xs text-gray-400 mt-1.5 space-y-1 list-disc list-inside">
93
+ <li>Drag existing charges to reposition them.</li>
94
+ <li>Toggle physics to let them orbit, attract, or repel!</li>
95
+ <li>Add multiple charges to see intricate field patterns.</li>
96
+ </ul>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Sidebar Config & Tuning Panel -->
101
+ <aside class="w-full md:w-80 bg-[#111827] border-t md:border-t-0 md:border-l border-gray-800 p-6 flex flex-col overflow-y-auto shrink-0 z-10 max-h-[40vh] md:max-h-none">
102
+
103
+ <!-- Subsection: Preset Configurations -->
104
+ <div class="mb-6">
105
+ <h2 class="text-xs font-semibold text-gray-400 tracking-wider uppercase mb-3">Simulation Presets</h2>
106
+ <div class="grid grid-cols-2 gap-2">
107
+ <button class="preset-btn px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-xs font-medium text-white transition text-center" data-preset="dipole">
108
+ 🧲 Dipole
109
+ </button>
110
+ <button class="preset-btn px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-xs font-medium text-white transition text-center" data-preset="quadrupole">
111
+ 🌀 Quadrupole
112
+ </button>
113
+ <button class="preset-btn px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-xs font-medium text-white transition text-center" data-preset="chaos">
114
+ 💫 Chaotic Orbit
115
+ </button>
116
+ <button class="preset-btn px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-xs font-medium text-white transition text-center" data-preset="grid">
117
+ ⏹️ Particle Grid
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Subsection: Physics Play/Pause -->
123
+ <div class="mb-6 flex gap-2">
124
+ <button id="btn-toggle-physics" class="flex-1 py-2.5 rounded-lg font-bold text-sm flex items-center justify-center gap-2 shadow transition duration-200 bg-emerald-600 hover:bg-emerald-500 text-white">
125
+ <span id="play-pause-icon">⏸</span> <span id="play-pause-text">Pause Physics</span>
126
+ </button>
127
+ </div>
128
+
129
+ <!-- Divider -->
130
+ <hr class="border-gray-800 mb-6" />
131
+
132
+ <!-- Subsection: Toggles -->
133
+ <div class="space-y-3 mb-6">
134
+ <h2 class="text-xs font-semibold text-gray-400 tracking-wider uppercase mb-1">Display Toggles</h2>
135
+
136
+ <label class="flex items-center justify-between cursor-pointer py-1">
137
+ <span class="text-xs text-gray-300">Show Field Lines</span>
138
+ <input type="checkbox" id="toggle-field-lines" class="rounded bg-gray-800 border-gray-700 text-blue-600 focus:ring-blue-500 h-4 w-4" checked>
139
+ </label>
140
+
141
+ <label class="flex items-center justify-between cursor-pointer py-1">
142
+ <span class="text-xs text-gray-300">Animate Vector Flow</span>
143
+ <input type="checkbox" id="toggle-vector-flow" class="rounded bg-gray-800 border-gray-700 text-blue-600 focus:ring-blue-500 h-4 w-4" checked>
144
+ </label>
145
+
146
+ <label class="flex items-center justify-between cursor-pointer py-1">
147
+ <span class="text-xs text-gray-300">Bounce on Canvas Edge</span>
148
+ <input type="checkbox" id="toggle-bounds" class="rounded bg-gray-800 border-gray-700 text-blue-600 focus:ring-blue-500 h-4 w-4" checked>
149
+ </label>
150
+ </div>
151
+
152
+ <!-- Divider -->
153
+ <hr class="border-gray-800 mb-6" />
154
+
155
+ <!-- Subsection: Param Sliders -->
156
+ <div class="space-y-5 flex-1">
157
+ <h2 class="text-xs font-semibold text-gray-400 tracking-wider uppercase mb-1">Physical Constants</h2>
158
+
159
+ <!-- Coulomb Strength -->
160
+ <div>
161
+ <div class="flex justify-between items-center mb-1.5">
162
+ <span class="text-xs text-gray-300">Coulomb Constant ($k_e$)</span>
163
+ <span id="val-k" class="text-xs font-mono text-blue-400">1000</span>
164
+ </div>
165
+ <input type="range" id="slide-k" min="100" max="5000" step="50" value="1200" class="w-full">
166
+ </div>
167
+
168
+ <!-- Simulation Friction (Damping) -->
169
+ <div>
170
+ <div class="flex justify-between items-center mb-1.5">
171
+ <span class="text-xs text-gray-300">Medium Viscosity (Damping)</span>
172
+ <span id="val-damping" class="text-xs font-mono text-blue-400">1%</span>
173
+ </div>
174
+ <input type="range" id="slide-damping" min="0" max="100" step="1" value="5" class="w-full">
175
+ </div>
176
+
177
+ <!-- Softening Factor (to avoid divide by zero singularities) -->
178
+ <div>
179
+ <div class="flex justify-between items-center mb-1.5">
180
+ <span class="text-xs text-gray-300">Singularity Softening ($\epsilon^2$)</span>
181
+ <span id="val-softening" class="text-xs font-mono text-blue-400">400</span>
182
+ </div>
183
+ <input type="range" id="slide-softening" min="100" max="2500" step="50" value="400" class="w-full">
184
+ </div>
185
+
186
+ <!-- Line Density -->
187
+ <div>
188
+ <div class="flex justify-between items-center mb-1.5">
189
+ <span class="text-xs text-gray-300">Lines per Charge</span>
190
+ <span id="val-density" class="text-xs font-mono text-blue-400">12</span>
191
+ </div>
192
+ <input type="range" id="slide-density" min="4" max="24" step="2" value="12" class="w-full">
193
+ </div>
194
+
195
+ <!-- Vector Flow speed -->
196
+ <div>
197
+ <div class="flex justify-between items-center mb-1.5">
198
+ <span class="text-xs text-gray-300">Flow Indicator Velocity</span>
199
+ <span id="val-flow-speed" class="text-xs font-mono text-blue-400">Medium</span>
200
+ </div>
201
+ <input type="range" id="slide-flow-speed" min="1" max="10" step="0.5" value="4" class="w-full">
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Bottom Disclaimer/Footer info -->
206
+ <div class="mt-6 pt-6 border-t border-gray-800 text-[10px] text-gray-500 leading-relaxed font-mono">
207
+ Formula used: $F = k_e \frac{q_1 q_2}{r^2 + \epsilon^2}$. Lines integrated via Euler-Heun path-tracer.
208
+ </div>
209
+ </aside>
210
+ </main>
211
+
212
+ <!-- App Logic Script -->
213
+ <script>
214
+ // Setup state variables
215
+ const canvas = document.getElementById('physics-canvas');
216
+ const ctx = canvas.getContext('2d');
217
+
218
+ // High DPI Support
219
+ function resizeCanvas() {
220
+ const rect = canvas.getBoundingClientRect();
221
+ canvas.width = rect.width * window.devicePixelRatio;
222
+ canvas.height = rect.height * window.devicePixelRatio;
223
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
224
+ }
225
+ window.addEventListener('resize', resizeCanvas);
226
+
227
+ // Physics variables & configs
228
+ let particles = [];
229
+ let physicsEnabled = true;
230
+ let showFieldLines = true;
231
+ let showVectorFlow = true;
232
+ let bounceBounds = true;
233
+
234
+ // Constants from slider inputs
235
+ let k_e = 1200;
236
+ let dampingCoeff = 0.005; // Friction % (damping = 1 - dampingCoeff)
237
+ let softening = 400; // soft factor to prevent infinite forces at proximity
238
+ let linesPerCharge = 12;
239
+ let flowVelocityMultiplier = 4;
240
+
241
+ // Interaction state
242
+ let selectedParticle = null;
243
+ let isDragging = false;
244
+ let lastMousePos = { x: 0, y: 0 };
245
+
246
+ // FPS tracking
247
+ let lastTime = performance.now();
248
+ let fps = 60;
249
+ let flowAnimOffset = 0; // global offset parameter for flowing arrows
250
+
251
+ // Model of particle charge
252
+ class ChargeParticle {
253
+ constructor(x, y, charge) {
254
+ this.x = x;
255
+ this.y = y;
256
+ this.charge = charge; // Positive (+1) or Negative (-1)
257
+ this.vx = 0;
258
+ this.vy = 0;
259
+ this.radius = 18;
260
+ this.mass = Math.abs(charge) * 1.5; // proportional to charge value
261
+ }
262
+
263
+ draw() {
264
+ ctx.save();
265
+ ctx.beginPath();
266
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
267
+
268
+ // Glow style based on charge sign
269
+ if (this.charge > 0) {
270
+ ctx.fillStyle = '#ef4444'; // Red
271
+ ctx.shadowColor = 'rgba(239, 68, 68, 0.7)';
272
+ } else {
273
+ ctx.fillStyle = '#3b82f6'; // Blue
274
+ ctx.shadowColor = 'rgba(59, 130, 246, 0.7)';
275
+ }
276
+ ctx.shadowBlur = 15;
277
+ ctx.fill();
278
+
279
+ // Draw outline border
280
+ ctx.lineWidth = 2;
281
+ ctx.strokeStyle = '#ffffff';
282
+ ctx.shadowBlur = 0; // turn off shadow for lines
283
+ ctx.stroke();
284
+
285
+ // Draw central sign indicator (+ or -)
286
+ ctx.font = 'bold 20px monospace';
287
+ ctx.fillStyle = '#ffffff';
288
+ ctx.textAlign = 'center';
289
+ ctx.textBaseline = 'middle';
290
+ ctx.fillText(this.charge > 0 ? '+' : '−', this.x, this.y - 1);
291
+ ctx.restore();
292
+ }
293
+
294
+ containsPoint(px, py) {
295
+ const dist = Math.hypot(this.x - px, this.y - py);
296
+ return dist < this.radius + 8;
297
+ }
298
+ }
299
+
300
+ // Initialize Simulation Canvas
301
+ function init() {
302
+ resizeCanvas();
303
+ loadPreset('dipole');
304
+ setupControls();
305
+
306
+ // Fade out instructions after 5s
307
+ setTimeout(() => {
308
+ document.getElementById('instruction-overlay').style.opacity = '0';
309
+ }, 6000);
310
+
311
+ // Start animation loop
312
+ requestAnimationFrame(updateLoop);
313
+ }
314
+
315
+ // Setup Controls & UI Observers
316
+ function setupControls() {
317
+ // Slider linkages
318
+ const bindSlider = (id, valId, callback, suffix = '') => {
319
+ const slider = document.getElementById(id);
320
+ const display = document.getElementById(valId);
321
+ slider.addEventListener('input', (e) => {
322
+ const val = parseFloat(e.target.value);
323
+ display.innerText = val + suffix;
324
+ callback(val);
325
+ });
326
+ };
327
+
328
+ bindSlider('slide-k', 'val-k', (v) => k_e = v);
329
+ bindSlider('slide-damping', 'val-damping', (v) => dampingCoeff = v / 100, '%');
330
+ bindSlider('slide-softening', 'val-softening', (v) => softening = v);
331
+ bindSlider('slide-density', 'val-density', (v) => linesPerCharge = v);
332
+ bindSlider('slide-flow-speed', 'val-flow-speed', (v) => {
333
+ flowVelocityMultiplier = v;
334
+ const txt = v < 3 ? 'Slow' : (v < 7 ? 'Medium' : 'Fast');
335
+ document.getElementById('val-flow-speed').innerText = txt;
336
+ });
337
+
338
+ // Toggles
339
+ document.getElementById('toggle-field-lines').addEventListener('change', (e) => {
340
+ showFieldLines = e.target.checked;
341
+ });
342
+ document.getElementById('toggle-vector-flow').addEventListener('change', (e) => {
343
+ showVectorFlow = e.target.checked;
344
+ });
345
+ document.getElementById('toggle-bounds').addEventListener('change', (e) => {
346
+ bounceBounds = e.target.checked;
347
+ });
348
+
349
+ // Play/Pause Action
350
+ const toggleBtn = document.getElementById('btn-toggle-physics');
351
+ const playPauseIcon = document.getElementById('play-pause-icon');
352
+ const playPauseText = document.getElementById('play-pause-text');
353
+
354
+ toggleBtn.addEventListener('click', () => {
355
+ physicsEnabled = !physicsEnabled;
356
+ if (physicsEnabled) {
357
+ toggleBtn.className = "flex-1 py-2.5 rounded-lg font-bold text-sm flex items-center justify-center gap-2 shadow transition duration-200 bg-emerald-600 hover:bg-emerald-500 text-white";
358
+ playPauseIcon.innerText = "⏸";
359
+ playPauseText.innerText = "Pause Physics";
360
+ } else {
361
+ toggleBtn.className = "flex-1 py-2.5 rounded-lg font-bold text-sm flex items-center justify-center gap-2 shadow transition duration-200 bg-amber-600 hover:bg-amber-500 text-white";
362
+ playPauseIcon.innerText = "▶";
363
+ playPauseText.innerText = "Resume Physics";
364
+ }
365
+ });
366
+
367
+ // Sandbox clear/add actions
368
+ document.getElementById('btn-clear').addEventListener('click', () => {
369
+ particles = [];
370
+ updateStats();
371
+ });
372
+
373
+ document.getElementById('btn-add-pos').addEventListener('click', () => {
374
+ const { w, h } = getCanvasLogicalSize();
375
+ const x = w / 2 + (Math.random() - 0.5) * 150;
376
+ const y = h / 2 + (Math.random() - 0.5) * 150;
377
+ particles.push(new ChargeParticle(x, y, 1.0));
378
+ updateStats();
379
+ });
380
+
381
+ document.getElementById('btn-add-neg').addEventListener('click', () => {
382
+ const { w, h } = getCanvasLogicalSize();
383
+ const x = w / 2 + (Math.random() - 0.5) * 150;
384
+ const y = h / 2 + (Math.random() - 0.5) * 150;
385
+ particles.push(new ChargeParticle(x, y, -1.0));
386
+ updateStats();
387
+ });
388
+
389
+ // Presets
390
+ document.querySelectorAll('.preset-btn').forEach(btn => {
391
+ btn.addEventListener('click', (e) => {
392
+ const preset = e.target.getAttribute('data-preset');
393
+ loadPreset(preset);
394
+ });
395
+ });
396
+
397
+ // Interaction Drag Logic (Mouse & Touch)
398
+ const getCoords = (e) => {
399
+ const rect = canvas.getBoundingClientRect();
400
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX;
401
+ const clientY = e.touches ? e.touches[0].clientY : e.clientY;
402
+ // Scale back based on element bounding box vs coordinate spacing
403
+ return {
404
+ x: (clientX - rect.left),
405
+ y: (clientY - rect.top)
406
+ };
407
+ };
408
+
409
+ const dragStart = (coords) => {
410
+ for (let p of particles) {
411
+ if (p.containsPoint(coords.x, coords.y)) {
412
+ selectedParticle = p;
413
+ isDragging = true;
414
+ lastMousePos = coords;
415
+ break;
416
+ }
417
+ }
418
+ };
419
+
420
+ const dragMove = (coords) => {
421
+ if (isDragging && selectedParticle) {
422
+ selectedParticle.x = coords.x;
423
+ selectedParticle.y = coords.y;
424
+ // Reset velocities during drag so it doesn't build crazy speed
425
+ selectedParticle.vx = 0;
426
+ selectedParticle.vy = 0;
427
+ }
428
+ };
429
+
430
+ const dragEnd = () => {
431
+ isDragging = false;
432
+ selectedParticle = null;
433
+ };
434
+
435
+ canvas.addEventListener('mousedown', (e) => dragStart(getCoords(e)));
436
+ canvas.addEventListener('mousemove', (e) => dragMove(getCoords(e)));
437
+ window.addEventListener('mouseup', dragEnd);
438
+
439
+ canvas.addEventListener('touchstart', (e) => {
440
+ e.preventDefault();
441
+ dragStart(getCoords(e));
442
+ }, { passive: false });
443
+
444
+ canvas.addEventListener('touchmove', (e) => {
445
+ e.preventDefault();
446
+ dragMove(getCoords(e));
447
+ }, { passive: false });
448
+
449
+ window.addEventListener('touchend', dragEnd);
450
+ }
451
+
452
+ // Get Logical size for styling boundaries
453
+ function getCanvasLogicalSize() {
454
+ const rect = canvas.getBoundingClientRect();
455
+ return { w: rect.width, h: rect.height };
456
+ }
457
+
458
+ // Reusable configuration setup
459
+ function loadPreset(name) {
460
+ const { w, h } = getCanvasLogicalSize();
461
+ particles = [];
462
+
463
+ // Re-center setup dynamically based on size
464
+ const cx = w > 0 ? w / 2 : 400;
465
+ const cy = h > 0 ? h / 2 : 300;
466
+
467
+ if (name === 'dipole') {
468
+ particles.push(new ChargeParticle(cx - 100, cy, 1.0));
469
+ particles.push(new ChargeParticle(cx + 100, cy, -1.0));
470
+ } else if (name === 'quadrupole') {
471
+ particles.push(new ChargeParticle(cx - 100, cy - 80, 1.0));
472
+ particles.push(new ChargeParticle(cx + 100, cy - 80, -1.0));
473
+ particles.push(new ChargeParticle(cx - 100, cy + 80, -1.0));
474
+ particles.push(new ChargeParticle(cx + 100, cy + 80, 1.0));
475
+ } else if (name === 'chaos') {
476
+ // Fixed massive heavy negative charge inside, lightweight positive orbits
477
+ const heavyCenter = new ChargeParticle(cx, cy, -3.0);
478
+ heavyCenter.radius = 26;
479
+ particles.push(heavyCenter);
480
+
481
+ const orbiter1 = new ChargeParticle(cx - 160, cy, 0.8);
482
+ orbiter1.vy = 4.5; // horizontal spin-velocity
483
+ orbiter1.vx = -1.2;
484
+ particles.push(orbiter1);
485
+
486
+ const orbiter2 = new ChargeParticle(cx + 180, cy + 10, 0.8);
487
+ orbiter2.vy = -3.8;
488
+ orbiter2.vx = 0.5;
489
+ particles.push(orbiter2);
490
+ } else if (name === 'grid') {
491
+ // Alternating matrix of positive and negatives
492
+ const sizeX = 3;
493
+ const sizeY = 3;
494
+ const spacingX = Math.min(150, w / 4);
495
+ const spacingY = Math.min(130, h / 4);
496
+ const startX = cx - ((sizeX - 1) * spacingX) / 2;
497
+ const startY = cy - ((sizeY - 1) * spacingY) / 2;
498
+
499
+ for (let i = 0; i < sizeX; i++) {
500
+ for (let j = 0; j < sizeY; j++) {
501
+ const type = (i + j) % 2 === 0 ? 1.0 : -1.0;
502
+ particles.push(new ChargeParticle(startX + i * spacingX, startY + j * spacingY, type));
503
+ }
504
+ }
505
+ }
506
+ updateStats();
507
+ }
508
+
509
+ // Live stats rendering
510
+ function updateStats() {
511
+ document.getElementById('stat-charges').innerText = particles.length;
512
+ }
513
+
514
+ // Calculate Electric Field Vector E(x, y) at any coordinate
515
+ function calculateEField(x, y) {
516
+ let Ex = 0;
517
+ let Ey = 0;
518
+
519
+ for (let p of particles) {
520
+ const dx = x - p.x;
521
+ const dy = y - p.y;
522
+ const dSq = dx * dx + dy * dy;
523
+
524
+ // Introduce softening factor to avoid numerical infinity (singularity) near the point charge
525
+ const denominator = Math.pow(dSq + softening, 1.5);
526
+
527
+ // E = k_e * q / r^2
528
+ const magnitude = (k_e * p.charge) / denominator;
529
+
530
+ Ex += magnitude * dx;
531
+ Ey += magnitude * dy;
532
+ }
533
+
534
+ return { Ex, Ey };
535
+ }
536
+
537
+ // Vector tracing algorithm for field lines
538
+ function traceFieldLine(startX, startY, direction) {
539
+ const { w, h } = getCanvasLogicalSize();
540
+ const points = [{ x: startX, y: startY }];
541
+ let x = startX;
542
+ let y = startY;
543
+ const maxSteps = 160;
544
+ const stepSize = 8; // Step size of the Euler integration path tracer
545
+
546
+ for (let step = 0; step < maxSteps; step++) {
547
+ const { Ex, Ey } = calculateEField(x, y);
548
+ const E_mag = Math.hypot(Ex, Ey);
549
+
550
+ if (E_mag < 0.0001) break; // field is zero
551
+
552
+ // Direction of integration step
553
+ const dx = (Ex / E_mag) * stepSize * direction;
554
+ const dy = (Ey / E_mag) * stepSize * direction;
555
+
556
+ x += dx;
557
+ y += dy;
558
+
559
+ points.push({ x, y });
560
+
561
+ // Bounds termination check
562
+ if (x < -100 || x > w + 100 || y < -100 || y > h + 100) {
563
+ break;
564
+ }
565
+
566
+ // Hit detection check with nearby charge
567
+ let hitCharge = false;
568
+ for (let p of particles) {
569
+ const distToP = Math.hypot(x - p.x, y - p.y);
570
+ if (distToP < p.radius - 2) {
571
+ // Landed on or inside the charge
572
+ hitCharge = true;
573
+ break;
574
+ }
575
+ }
576
+ if (hitCharge) {
577
+ break;
578
+ }
579
+ }
580
+
581
+ return points;
582
+ }
583
+
584
+ // Render calculated lines and fluid movement vectors
585
+ function drawElectricField() {
586
+ if (particles.length === 0) return;
587
+
588
+ const linesToTrace = [];
589
+
590
+ // Spawn paths radiating outward from positive charges, and inward to negative ones
591
+ particles.forEach(p => {
592
+ const N = Math.floor(linesPerCharge * Math.abs(p.charge));
593
+ for (let i = 0; i < N; i++) {
594
+ const angle = (Math.PI * 2 * i) / N;
595
+ // Slightly offset outward from the border of the charge
596
+ const startX = p.x + Math.cos(angle) * (p.radius + 1);
597
+ const startY = p.y + Math.sin(angle) * (p.radius + 1);
598
+
599
+ // Integrate forward (+) if charge is positive, backward (-) if negative
600
+ const dir = p.charge > 0 ? 1 : -1;
601
+ linesToTrace.push(traceFieldLine(startX, startY, dir));
602
+ }
603
+ });
604
+
605
+ // Draw Paths on Canvas
606
+ linesToTrace.forEach(line => {
607
+ if (line.length < 2) return;
608
+
609
+ if (showFieldLines) {
610
+ ctx.beginPath();
611
+ ctx.moveTo(line[0].x, line[0].y);
612
+ for (let i = 1; i < line.length; i++) {
613
+ ctx.lineTo(line[i].x, line[i].y);
614
+ }
615
+ // Field line styling: Neon semi-transparent teal-white line style
616
+ ctx.strokeStyle = 'rgba(243, 244, 246, 0.16)';
617
+ ctx.lineWidth = 1.5;
618
+ ctx.stroke();
619
+ }
620
+
621
+ // Show Animated Energy Flow
622
+ if (showVectorFlow) {
623
+ // We can sample points along the calculated line to draw glowing field markers
624
+ // The markers move down the path continuously based on global offset
625
+ const speed = flowVelocityMultiplier * 0.5;
626
+ const dashSpacing = 70; // gap distance between cascading glow points
627
+
628
+ // Track path-length spacing to place dots perfectly
629
+ let lengthAccumulator = 0;
630
+ const segments = [];
631
+ for (let i = 0; i < line.length - 1; i++) {
632
+ const segmentLen = Math.hypot(line[i+1].x - line[i].x, line[i+1].y - line[i].y);
633
+ segments.push({
634
+ start: line[i],
635
+ end: line[i+1],
636
+ len: segmentLen,
637
+ accum: lengthAccumulator
638
+ });
639
+ lengthAccumulator += segmentLen;
640
+ }
641
+
642
+ // Draw moving flow indicators
643
+ let flowDist = flowAnimOffset % dashSpacing;
644
+ while (flowDist < lengthAccumulator) {
645
+ // Find the segment representing this distance
646
+ const seg = segments.find(s => flowDist >= s.accum && flowDist <= s.accum + s.len);
647
+ if (seg) {
648
+ const ratio = (flowDist - seg.accum) / seg.len;
649
+ const dotX = seg.start.x + (seg.end.x - seg.start.x) * ratio;
650
+ const dotY = seg.start.y + (seg.end.y - seg.start.y) * ratio;
651
+
652
+ // Draw a small bright field vector dot
653
+ ctx.beginPath();
654
+ ctx.arc(dotX, dotY, 2, 0, Math.PI * 2);
655
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
656
+ ctx.shadowColor = '#00f2ff';
657
+ ctx.shadowBlur = 4;
658
+ ctx.fill();
659
+ ctx.shadowBlur = 0; // reset
660
+ }
661
+ flowDist += dashSpacing;
662
+ }
663
+ }
664
+ });
665
+ }
666
+
667
+ // Apply Coulomb many-body forces
668
+ function updatePhysics() {
669
+ if (!physicsEnabled) return;
670
+
671
+ // 1. Calculate and accumulate net force vector
672
+ const forces = particles.map(() => ({ fx: 0, fy: 0 }));
673
+
674
+ for (let i = 0; i < particles.length; i++) {
675
+ // Skip calculations for the charge currently held by user
676
+ if (particles[i] === selectedParticle) continue;
677
+
678
+ for (let j = 0; j < particles.length; j++) {
679
+ if (i === j) continue;
680
+
681
+ const p1 = particles[i];
682
+ const p2 = particles[j];
683
+
684
+ const dx = p1.x - p2.x;
685
+ const dy = p1.y - p2.y;
686
+ const dSq = dx * dx + dy * dy;
687
+
688
+ // Compute vector distance
689
+ const distance = Math.hypot(dx, dy);
690
+ if (distance < 0.1) continue;
691
+
692
+ // Electrostatic Force (Coulomb's Law): F = k_e * q1 * q2 / r^2
693
+ // Softening factor avoids divide-by-zero singularity when particles overlaps
694
+ const forceMag = (k_e * p1.charge * p2.charge) / (dSq + softening);
695
+
696
+ // Direction of forces (like charges repel, opposite charges attract)
697
+ forces[i].fx += (dx / distance) * forceMag;
698
+ forces[i].fy += (dy / distance) * forceMag;
699
+ }
700
+ }
701
+
702
+ // 2. Integration and Update of Position and Velocity Vectors
703
+ const { w, h } = getCanvasLogicalSize();
704
+ const damping = 1 - dampingCoeff;
705
+
706
+ for (let i = 0; i < particles.length; i++) {
707
+ const p = particles[i];
708
+ if (p === selectedParticle) continue; // locked under mouse drag
709
+
710
+ const f = forces[i];
711
+
712
+ // F = m * a -> a = F / m
713
+ const ax = f.fx / p.mass;
714
+ const ay = f.fy / p.mass;
715
+
716
+ p.vx += ax;
717
+ p.vy += ay;
718
+
719
+ // Apply friction / viscous damping
720
+ p.vx *= damping;
721
+ p.vy *= damping;
722
+
723
+ p.x += p.vx;
724
+ p.y += p.vy;
725
+
726
+ // 3. Keep within canvas boundaries (Elastic or non-destructive bounces)
727
+ if (bounceBounds) {
728
+ const padding = p.radius;
729
+ if (p.x < padding) {
730
+ p.x = padding;
731
+ p.vx *= -0.6; // absorb partial kinetic energy on hit
732
+ } else if (p.x > w - padding) {
733
+ p.x = w - padding;
734
+ p.vx *= -0.6;
735
+ }
736
+
737
+ if (p.y < padding) {
738
+ p.y = padding;
739
+ p.vy *= -0.6;
740
+ } else if (p.y > h - padding) {
741
+ p.y = h - padding;
742
+ p.vy *= -0.6;
743
+ }
744
+ }
745
+ }
746
+ }
747
+
748
+ // Animation Frame loop handler
749
+ function updateLoop() {
750
+ // Calculate real-time FPS
751
+ const now = performance.now();
752
+ const delta = now - lastTime;
753
+ lastTime = now;
754
+ fps = Math.round(1000 / delta);
755
+ document.getElementById('stat-fps').innerText = fps;
756
+
757
+ // Clear Canvas Frame
758
+ const { w, h } = getCanvasLogicalSize();
759
+ ctx.clearRect(0, 0, w, h);
760
+
761
+ // Redraw background space grid
762
+ drawBackgroundSpaceGrid(w, h);
763
+
764
+ // Update physical positions & velocities
765
+ updatePhysics();
766
+
767
+ // Render field lines and flow tracers
768
+ flowAnimOffset += flowVelocityMultiplier * 0.4;
769
+ drawElectricField();
770
+
771
+ // Render actual charged particles
772
+ particles.forEach(p => p.draw());
773
+
774
+ requestAnimationFrame(updateLoop);
775
+ }
776
+
777
+ // Premium ambient digital grid background
778
+ function drawBackgroundSpaceGrid(w, h) {
779
+ ctx.strokeStyle = 'rgba(31, 41, 55, 0.4)';
780
+ ctx.lineWidth = 1;
781
+ const step = 40;
782
+
783
+ for (let x = 0; x < w; x += step) {
784
+ ctx.beginPath();
785
+ ctx.moveTo(x, 0);
786
+ ctx.lineTo(x, h);
787
+ ctx.stroke();
788
+ }
789
+ for (let y = 0; y < h; y += step) {
790
+ ctx.beginPath();
791
+ ctx.moveTo(0, y);
792
+ ctx.lineTo(w, y);
793
+ ctx.stroke();
794
+ }
795
+ }
796
+
797
+ // Launch Application sandbox
798
+ window.onload = init;
799
+ </script>
800
+ </body>
801
  </html>