engerl commited on
Commit
112228c
·
verified ·
1 Parent(s): 0268943

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +1343 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Physics Collision Lab
3
- emoji: 👁
4
- colorFrom: green
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: physics-collision-lab
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: gray
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1343 @@
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>Physics Collision Lab</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ canvas {
11
+ background-color: #f0f4f8;
12
+ border-radius: 0.5rem;
13
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
14
+ }
15
+
16
+ .slider-container {
17
+ position: relative;
18
+ height: 24px;
19
+ }
20
+
21
+ .slider-track {
22
+ position: absolute;
23
+ top: 50%;
24
+ transform: translateY(-50%);
25
+ width: 100%;
26
+ height: 4px;
27
+ background-color: #e2e8f0;
28
+ border-radius: 2px;
29
+ }
30
+
31
+ .slider-thumb {
32
+ position: absolute;
33
+ top: 50%;
34
+ transform: translate(-50%, -50%);
35
+ width: 16px;
36
+ height: 16px;
37
+ background-color: #4f46e5;
38
+ border-radius: 50%;
39
+ cursor: pointer;
40
+ z-index: 2;
41
+ }
42
+
43
+ .slider-fill {
44
+ position: absolute;
45
+ top: 50%;
46
+ transform: translateY(-50%);
47
+ height: 4px;
48
+ background-color: #4f46e5;
49
+ border-radius: 2px;
50
+ z-index: 1;
51
+ }
52
+
53
+ .particle {
54
+ position: absolute;
55
+ border-radius: 50%;
56
+ cursor: move;
57
+ user-select: none;
58
+ }
59
+
60
+ .energy-meter {
61
+ height: 8px;
62
+ background: linear-gradient(to right, #ef4444, #f59e0b, #10b981);
63
+ border-radius: 4px;
64
+ margin-top: 4px;
65
+ }
66
+
67
+ .property-card {
68
+ transition: all 0.2s ease;
69
+ }
70
+
71
+ .property-card:hover {
72
+ transform: translateY(-2px);
73
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
74
+ }
75
+
76
+ .particle-counter {
77
+ position: absolute;
78
+ top: 10px;
79
+ left: 10px;
80
+ background-color: rgba(255, 255, 255, 0.8);
81
+ padding: 5px 10px;
82
+ border-radius: 20px;
83
+ font-size: 14px;
84
+ font-weight: bold;
85
+ color: #4f46e5;
86
+ }
87
+ </style>
88
+ </head>
89
+ <body class="bg-gray-50 min-h-screen">
90
+ <div class="container mx-auto px-4 py-8">
91
+ <header class="mb-8 text-center">
92
+ <h1 class="text-4xl font-bold text-indigo-700 mb-2">Physics Collision Lab</h1>
93
+ <p class="text-gray-600 max-w-2xl mx-auto">Interactive simulation of elastic and inelastic collisions with customizable parameters</p>
94
+ </header>
95
+
96
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
97
+ <!-- Simulation Canvas -->
98
+ <div class="lg:col-span-2 bg-white p-4 rounded-xl shadow-md">
99
+ <div class="flex justify-between items-center mb-4">
100
+ <h2 class="text-xl font-semibold text-gray-800">Simulation Area</h2>
101
+ <div class="flex space-x-2">
102
+ <button id="playPauseBtn" class="px-3 py-1 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition">
103
+ <i class="fas fa-play"></i> Play
104
+ </button>
105
+ <button id="resetBtn" class="px-3 py-1 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">
106
+ <i class="fas fa-redo"></i> Reset
107
+ </button>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="relative">
112
+ <canvas id="simulationCanvas" width="800" height="500" class="w-full"></canvas>
113
+ <div class="particle-counter" id="particleCounter">0 particles</div>
114
+ <div id="particleCreator" class="absolute top-4 right-4 bg-white p-3 rounded-lg shadow-lg hidden">
115
+ <div class="flex items-center mb-2">
116
+ <span class="text-sm font-medium text-gray-700 mr-2">Quantity:</span>
117
+ <input type="number" min="1" max="20" value="1" class="w-20 px-2 py-1 border rounded" id="creatorQuantity">
118
+ </div>
119
+ <div class="flex items-center mb-2">
120
+ <span class="text-sm font-medium text-gray-700 mr-2">Radius:</span>
121
+ <input type="range" min="10" max="50" value="20" class="w-24" id="creatorRadius">
122
+ <span id="creatorRadiusValue" class="ml-2 text-sm w-8 text-center">20</span>
123
+ </div>
124
+ <div class="flex items-center mb-2">
125
+ <span class="text-sm font-medium text-gray-700 mr-2">Mass:</span>
126
+ <input type="range" min="1" max="20" value="5" class="w-24" id="creatorMass">
127
+ <span id="creatorMassValue" class="ml-2 text-sm w-8 text-center">5</span>
128
+ </div>
129
+ <div class="flex items-center mb-3">
130
+ <span class="text-sm font-medium text-gray-700 mr-2">Color:</span>
131
+ <input type="color" value="#4f46e5" id="creatorColor" class="h-6 w-6 cursor-pointer">
132
+ </div>
133
+ <div class="flex items-center mb-3">
134
+ <span class="text-sm font-medium text-gray-700 mr-2">Randomize:</span>
135
+ <input type="checkbox" id="randomizeProps" class="h-4 w-4" checked>
136
+ </div>
137
+ <div class="flex justify-between">
138
+ <button id="cancelCreate" class="px-2 py-1 bg-gray-200 text-gray-700 rounded text-sm hover:bg-gray-300 mr-2">
139
+ Cancel
140
+ </button>
141
+ <button id="confirmCreate" class="px-2 py-1 bg-indigo-600 text-white rounded text-sm hover:bg-indigo-700">
142
+ Create
143
+ </button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <div class="mt-4 grid grid-cols-2 gap-4">
149
+ <div class="bg-indigo-50 p-3 rounded-lg">
150
+ <div class="flex justify-between items-center mb-1">
151
+ <span class="text-sm font-medium text-indigo-700">System Momentum</span>
152
+ <span id="momentumValue" class="text-sm font-mono">0.00 kg·m/s</span>
153
+ </div>
154
+ <div class="energy-meter" id="momentumMeter"></div>
155
+ </div>
156
+ <div class="bg-green-50 p-3 rounded-lg">
157
+ <div class="flex justify-between items-center mb-1">
158
+ <span class="text-sm font-medium text-green-700">System Kinetic Energy</span>
159
+ <span id="energyValue" class="text-sm font-mono">0.00 J</span>
160
+ </div>
161
+ <div class="energy-meter" id="energyMeter"></div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Control Panel -->
167
+ <div class="bg-white p-6 rounded-xl shadow-md">
168
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Control Panel</h2>
169
+
170
+ <div class="mb-6">
171
+ <h3 class="text-lg font-medium text-gray-700 mb-3">Environment Settings</h3>
172
+ <div class="space-y-4">
173
+ <div>
174
+ <label class="block text-sm font-medium text-gray-700 mb-1">Gravity</label>
175
+ <div class="slider-container">
176
+ <div class="slider-track"></div>
177
+ <div class="slider-fill" id="gravityFill"></div>
178
+ <div class="slider-thumb" id="gravityThumb"></div>
179
+ </div>
180
+ <div class="flex justify-between mt-1">
181
+ <span class="text-xs text-gray-500">0</span>
182
+ <span id="gravityValue" class="text-xs font-mono">5.0 m/s²</span>
183
+ <span class="text-xs text-gray-500">20</span>
184
+ </div>
185
+ </div>
186
+
187
+ <div>
188
+ <label class="block text-sm font-medium text-gray-700 mb-1">Restitution (Bounciness)</label>
189
+ <div class="slider-container">
190
+ <div class="slider-track"></div>
191
+ <div class="slider-fill" id="restitutionFill"></div>
192
+ <div class="slider-thumb" id="restitutionThumb"></div>
193
+ </div>
194
+ <div class="flex justify-between mt-1">
195
+ <span class="text-xs text-gray-500">0%</span>
196
+ <span id="restitutionValue" class="text-xs font-mono">80%</span>
197
+ <span class="text-xs text-gray-500">100%</span>
198
+ </div>
199
+ </div>
200
+
201
+ <div>
202
+ <label class="block text-sm font-medium text-gray-700 mb-1">Friction</label>
203
+ <div class="slider-container">
204
+ <div class="slider-track"></div>
205
+ <div class="slider-fill" id="frictionFill"></div>
206
+ <div class="slider-thumb" id="frictionThumb"></div>
207
+ </div>
208
+ <div class="flex justify-between mt-1">
209
+ <span class="text-xs text-gray-500">0%</span>
210
+ <span id="frictionValue" class="text-xs font-mono">10%</span>
211
+ <span class="text-xs text-gray-500">100%</span>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <div class="mb-6">
218
+ <h3 class="text-lg font-medium text-gray-700 mb-3">Particle Tools</h3>
219
+ <div class="grid grid-cols-2 gap-3">
220
+ <button id="addParticleBtn" class="px-3 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition flex items-center justify-center">
221
+ <i class="fas fa-plus mr-2"></i> Add Particles
222
+ </button>
223
+ <button id="removeParticleBtn" class="px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition flex items-center justify-center">
224
+ <i class="fas fa-trash mr-2"></i> Remove All
225
+ </button>
226
+ </div>
227
+ <div class="mt-3 grid grid-cols-2 gap-3">
228
+ <button id="randomizeBtn" class="px-3 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition">
229
+ <i class="fas fa-random mr-1"></i> Randomize
230
+ </button>
231
+ <button id="freezeAllBtn" class="px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition">
232
+ <i class="fas fa-snowflake mr-1"></i> Freeze All
233
+ </button>
234
+ </div>
235
+ </div>
236
+
237
+ <div>
238
+ <h3 class="text-lg font-medium text-gray-700 mb-3">Preset Scenarios</h3>
239
+ <div class="grid grid-cols-2 gap-3 mb-3">
240
+ <button id="elasticCollisionBtn" class="px-3 py-2 bg-blue-100 text-blue-700 rounded-md hover:bg-blue-200 transition">
241
+ Elastic (2)
242
+ </button>
243
+ <button id="inelasticCollisionBtn" class="px-3 py-2 bg-purple-100 text-purple-700 rounded-md hover:bg-purple-200 transition">
244
+ Inelastic (2)
245
+ </button>
246
+ </div>
247
+ <div class="grid grid-cols-2 gap-3">
248
+ <button id="newtonsCradleBtn" class="px-3 py-2 bg-green-100 text-green-700 rounded-md hover:bg-green-200 transition">
249
+ Newton's Cradle (5)
250
+ </button>
251
+ <button id="particleExplosionBtn" class="px-3 py-2 bg-yellow-100 text-yellow-700 rounded-md hover:bg-yellow-200 transition">
252
+ Explosion (10)
253
+ </button>
254
+ </div>
255
+ </div>
256
+
257
+ <div class="mt-6">
258
+ <h3 class="text-lg font-medium text-gray-700 mb-3">Particles Manager</h3>
259
+ <div id="particleProperties" class="space-y-3 max-h-64 overflow-y-auto pr-2">
260
+ <div class="text-center text-gray-500 py-4">
261
+ Select particles to manage them
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <div class="mt-8 bg-white p-6 rounded-xl shadow-md">
269
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Collision Data</h2>
270
+ <div class="overflow-x-auto">
271
+ <table class="min-w-full divide-y divide-gray-200">
272
+ <thead class="bg-gray-50">
273
+ <tr>
274
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
275
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Particles</th>
276
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Velocity Before</th>
277
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Velocity After</th>
278
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Momentum Change</th>
279
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Energy Change</th>
280
+ </tr>
281
+ </thead>
282
+ <tbody id="collisionData" class="bg-white divide-y divide-gray-200">
283
+ <tr>
284
+ <td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">No collision data yet</td>
285
+ </tr>
286
+ </tbody>
287
+ </table>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ <script>
293
+ // Physics Constants
294
+ const PHYSICS = {
295
+ GRAVITY: 9.8,
296
+ PIXELS_PER_METER: 50,
297
+ MAX_VELOCITY: 20
298
+ };
299
+
300
+ // Simulation State
301
+ let state = {
302
+ running: false,
303
+ particles: [],
304
+ selectedParticles: [],
305
+ creatingParticles: false,
306
+ gravity: 5,
307
+ restitution: 0.8,
308
+ friction: 0.1,
309
+ collisionHistory: [],
310
+ lastTime: 0,
311
+ systemMomentum: 0,
312
+ systemEnergy: 0,
313
+ maxMomentum: 1,
314
+ maxEnergy: 1
315
+ };
316
+
317
+ // DOM Elements
318
+ const canvas = document.getElementById('simulationCanvas');
319
+ const ctx = canvas.getContext('2d');
320
+ const playPauseBtn = document.getElementById('playPauseBtn');
321
+ const resetBtn = document.getElementById('resetBtn');
322
+ const addParticleBtn = document.getElementById('addParticleBtn');
323
+ const removeParticleBtn = document.getElementById('removeParticleBtn');
324
+ const randomizeBtn = document.getElementById('randomizeBtn');
325
+ const freezeAllBtn = document.getElementById('freezeAllBtn');
326
+ const particleCreator = document.getElementById('particleCreator');
327
+ const cancelCreate = document.getElementById('cancelCreate');
328
+ const confirmCreate = document.getElementById('confirmCreate');
329
+ const creatorQuantity = document.getElementById('creatorQuantity');
330
+ const creatorRadius = document.getElementById('creatorRadius');
331
+ const creatorRadiusValue = document.getElementById('creatorRadiusValue');
332
+ const creatorMass = document.getElementById('creatorMass');
333
+ const creatorMassValue = document.getElementById('creatorMassValue');
334
+ const creatorColor = document.getElementById('creatorColor');
335
+ const randomizeProps = document.getElementById('randomizeProps');
336
+ const particleProperties = document.getElementById('particleProperties');
337
+ const collisionData = document.getElementById('collisionData');
338
+ const momentumValue = document.getElementById('momentumValue');
339
+ const energyValue = document.getElementById('energyValue');
340
+ const momentumMeter = document.getElementById('momentumMeter');
341
+ const energyMeter = document.getElementById('energyMeter');
342
+ const particleCounter = document.getElementById('particleCounter');
343
+
344
+ // Preset buttons
345
+ const elasticCollisionBtn = document.getElementById('elasticCollisionBtn');
346
+ const inelasticCollisionBtn = document.getElementById('inelasticCollisionBtn');
347
+ const newtonsCradleBtn = document.getElementById('newtonsCradleBtn');
348
+ const particleExplosionBtn = document.getElementById('particleExplosionBtn');
349
+
350
+ // Slider elements
351
+ const gravityThumb = document.getElementById('gravityThumb');
352
+ const gravityFill = document.getElementById('gravityFill');
353
+ const gravityValue = document.getElementById('gravityValue');
354
+
355
+ const restitutionThumb = document.getElementById('restitutionThumb');
356
+ const restitutionFill = document.getElementById('restitutionFill');
357
+ const restitutionValue = document.getElementById('restitutionValue');
358
+
359
+ const frictionThumb = document.getElementById('frictionThumb');
360
+ const frictionFill = document.getElementById('frictionFill');
361
+ const frictionValue = document.getElementById('frictionValue');
362
+
363
+ // Initialize sliders
364
+ initSlider(gravityThumb, gravityFill, 0, 20, state.gravity, value => {
365
+ state.gravity = value;
366
+ gravityValue.textContent = `${value.toFixed(1)} m/s²`;
367
+ // Update gravity for all particles
368
+ state.particles.forEach(p => p.ay = state.gravity);
369
+ });
370
+
371
+ initSlider(restitutionThumb, restitutionFill, 0, 1, state.restitution, value => {
372
+ state.restitution = value;
373
+ restitutionValue.textContent = `${Math.round(value * 100)}%`;
374
+ });
375
+
376
+ initSlider(frictionThumb, frictionFill, 0, 1, state.friction, value => {
377
+ state.friction = value;
378
+ frictionValue.textContent = `${Math.round(value * 100)}%`;
379
+ });
380
+
381
+ // Particle class
382
+ class Particle {
383
+ constructor(x, y, radius, mass, color, vx = 0, vy = 0) {
384
+ this.x = x;
385
+ this.y = y;
386
+ this.radius = radius;
387
+ this.mass = mass;
388
+ this.color = color;
389
+ this.vx = vx;
390
+ this.vy = vy;
391
+ this.ax = 0;
392
+ this.ay = state.gravity;
393
+ this.selected = false;
394
+ this.dragging = false;
395
+ this.dragOffsetX = 0;
396
+ this.dragOffsetY = 0;
397
+ this.id = Math.random().toString(36).substr(2, 9);
398
+ this.collisions = 0;
399
+ this.frozen = false;
400
+ }
401
+
402
+ update(dt) {
403
+ if (this.frozen) return;
404
+
405
+ // Apply friction
406
+ this.vx *= (1 - state.friction * 0.1);
407
+ this.vy *= (1 - state.friction * 0.1);
408
+
409
+ // Update velocity
410
+ this.vx += this.ax * dt;
411
+ this.vy += this.ay * dt;
412
+
413
+ // Limit velocity
414
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
415
+ if (speed > PHYSICS.MAX_VELOCITY) {
416
+ const ratio = PHYSICS.MAX_VELOCITY / speed;
417
+ this.vx *= ratio;
418
+ this.vy *= ratio;
419
+ }
420
+
421
+ // Update position
422
+ this.x += this.vx * dt * PHYSICS.PIXELS_PER_METER;
423
+ this.y += this.vy * dt * PHYSICS.PIXELS_PER_METER;
424
+
425
+ // Boundary collision
426
+ this.handleBoundaryCollision();
427
+ }
428
+
429
+ handleBoundaryCollision() {
430
+ // Left wall
431
+ if (this.x - this.radius < 0) {
432
+ this.x = this.radius;
433
+ this.vx = -this.vx * state.restitution;
434
+ }
435
+ // Right wall
436
+ if (this.x + this.radius > canvas.width) {
437
+ this.x = canvas.width - this.radius;
438
+ this.vx = -this.vx * state.restitution;
439
+ }
440
+ // Top wall
441
+ if (this.y - this.radius < 0) {
442
+ this.y = this.radius;
443
+ this.vy = -this.vy * state.restitution;
444
+ }
445
+ // Bottom wall
446
+ if (this.y + this.radius > canvas.height) {
447
+ this.y = canvas.height - this.radius;
448
+ this.vy = -this.vy * state.restitution;
449
+ }
450
+ }
451
+
452
+ draw() {
453
+ ctx.beginPath();
454
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
455
+ ctx.fillStyle = this.selected ? this.lightenColor(this.color, 20) : this.color;
456
+ ctx.fill();
457
+
458
+ if (this.selected) {
459
+ ctx.strokeStyle = '#000';
460
+ ctx.lineWidth = 2;
461
+ ctx.stroke();
462
+
463
+ // Draw velocity vector
464
+ if (this.vx !== 0 || this.vy !== 0) {
465
+ ctx.beginPath();
466
+ ctx.moveTo(this.x, this.y);
467
+ ctx.lineTo(
468
+ this.x + this.vx * 10,
469
+ this.y + this.vy * 10
470
+ );
471
+ ctx.strokeStyle = '#f00';
472
+ ctx.lineWidth = 2;
473
+ ctx.stroke();
474
+
475
+ // Arrow head
476
+ const angle = Math.atan2(this.vy, this.vx);
477
+ ctx.beginPath();
478
+ ctx.moveTo(
479
+ this.x + this.vx * 10,
480
+ this.y + this.vy * 10
481
+ );
482
+ ctx.lineTo(
483
+ this.x + this.vx * 10 - 8 * Math.cos(angle - Math.PI/6),
484
+ this.y + this.vy * 10 - 8 * Math.sin(angle - Math.PI/6)
485
+ );
486
+ ctx.lineTo(
487
+ this.x + this.vx * 10 - 8 * Math.cos(angle + Math.PI/6),
488
+ this.y + this.vy * 10 - 8 * Math.sin(angle + Math.PI/6)
489
+ );
490
+ ctx.closePath();
491
+ ctx.fillStyle = '#f00';
492
+ ctx.fill();
493
+ }
494
+ }
495
+
496
+ // Draw frozen indicator
497
+ if (this.frozen) {
498
+ ctx.beginPath();
499
+ ctx.arc(this.x, this.y, this.radius * 0.7, 0, Math.PI * 2);
500
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
501
+ ctx.lineWidth = 2;
502
+ ctx.stroke();
503
+ }
504
+ }
505
+
506
+ lightenColor(color, percent) {
507
+ const num = parseInt(color.replace("#", ""), 16);
508
+ const amt = Math.round(2.55 * percent);
509
+ const R = (num >> 16) + amt;
510
+ const B = (num >> 8 & 0x00FF) + amt;
511
+ const G = (num & 0x0000FF) + amt;
512
+
513
+ return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
514
+ (B < 255 ? B < 1 ? 0 : B : 255) * 0x100 +
515
+ (G < 255 ? G < 1 ? 0 : G : 255)).toString(16).slice(1)}`;
516
+ }
517
+
518
+ isPointInside(px, py) {
519
+ const dx = px - this.x;
520
+ const dy = py - this.y;
521
+ return dx * dx + dy * dy <= this.radius * this.radius;
522
+ }
523
+
524
+ get momentum() {
525
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
526
+ return this.mass * speed;
527
+ }
528
+
529
+ get kineticEnergy() {
530
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
531
+ return 0.5 * this.mass * speed * speed;
532
+ }
533
+ }
534
+
535
+ // Initialize slider
536
+ function initSlider(thumb, fill, min, max, initialValue, callback) {
537
+ const container = thumb.parentElement;
538
+ const track = container.querySelector('.slider-track');
539
+ const range = max - min;
540
+ let isDragging = false;
541
+
542
+ // Set initial position
543
+ const initialPos = ((initialValue - min) / range) * track.offsetWidth;
544
+ thumb.style.left = `${initialPos}px`;
545
+ fill.style.width = `${initialPos}px`;
546
+
547
+ // Mouse down event
548
+ thumb.addEventListener('mousedown', (e) => {
549
+ isDragging = true;
550
+ document.addEventListener('mousemove', handleMove);
551
+ document.addEventListener('mouseup', () => {
552
+ isDragging = false;
553
+ document.removeEventListener('mousemove', handleMove);
554
+ });
555
+ e.preventDefault();
556
+ });
557
+
558
+ // Track click event
559
+ track.addEventListener('click', (e) => {
560
+ const rect = track.getBoundingClientRect();
561
+ const pos = e.clientX - rect.left;
562
+ updateSlider(pos);
563
+ });
564
+
565
+ function handleMove(e) {
566
+ const rect = track.getBoundingClientRect();
567
+ let pos = e.clientX - rect.left;
568
+ pos = Math.max(0, Math.min(pos, track.offsetWidth));
569
+ updateSlider(pos);
570
+ }
571
+
572
+ function updateSlider(pos) {
573
+ const value = min + (pos / track.offsetWidth) * range;
574
+ thumb.style.left = `${pos}px`;
575
+ fill.style.width = `${pos}px`;
576
+ callback(value);
577
+ }
578
+ }
579
+
580
+ // Check collision between two particles
581
+ function checkCollision(p1, p2) {
582
+ const dx = p2.x - p1.x;
583
+ const dy = p2.y - p1.y;
584
+ const distance = Math.sqrt(dx * dx + dy * dy);
585
+ return distance < p1.radius + p2.radius;
586
+ }
587
+
588
+ // Resolve collision between two particles
589
+ function resolveCollision(p1, p2) {
590
+ // Store pre-collision velocities for history
591
+ const p1vBefore = { x: p1.vx, y: p1.vy };
592
+ const p2vBefore = { x: p2.vx, y: p2.vy };
593
+
594
+ // Calculate collision normal
595
+ const dx = p2.x - p1.x;
596
+ const dy = p2.y - p1.y;
597
+ const distance = Math.sqrt(dx * dx + dy * dy);
598
+ const nx = dx / distance;
599
+ const ny = dy / distance;
600
+
601
+ // Calculate relative velocity
602
+ const vx = p2.vx - p1.vx;
603
+ const vy = p2.vy - p1.vy;
604
+ const relativeVelocity = vx * nx + vy * ny;
605
+
606
+ // Do not resolve if particles are moving away from each other
607
+ if (relativeVelocity > 0) return;
608
+
609
+ // Calculate impulse scalar
610
+ const restitution = state.restitution;
611
+ const impulse = -(1 + restitution) * relativeVelocity /
612
+ (1/p1.mass + 1/p2.mass);
613
+
614
+ // Apply impulse
615
+ p1.vx -= (impulse * nx) / p1.mass;
616
+ p1.vy -= (impulse * ny) / p1.mass;
617
+ p2.vx += (impulse * nx) / p2.mass;
618
+ p2.vy += (impulse * ny) / p2.mass;
619
+
620
+ // Separate particles to prevent sticking
621
+ const overlap = (p1.radius + p2.radius - distance) / 2;
622
+ p1.x -= overlap * nx;
623
+ p1.y -= overlap * ny;
624
+ p2.x += overlap * nx;
625
+ p2.y += overlap * ny;
626
+
627
+ // Increment collision counters
628
+ p1.collisions++;
629
+ p2.collisions++;
630
+
631
+ // Record collision
632
+ recordCollision(p1, p2, p1vBefore, p2vBefore);
633
+ }
634
+
635
+ // Record collision data
636
+ function recordCollision(p1, p2, p1vBefore, p2vBefore) {
637
+ const now = new Date();
638
+ const timeStr = now.toLocaleTimeString();
639
+
640
+ // Calculate momentum and energy changes
641
+ const p1MomentumBefore = Math.sqrt(p1vBefore.x*p1vBefore.x + p1vBefore.y*p1vBefore.y) * p1.mass;
642
+ const p2MomentumBefore = Math.sqrt(p2vBefore.x*p2vBefore.x + p2vBefore.y*p2vBefore.y) * p2.mass;
643
+ const p1MomentumAfter = Math.sqrt(p1.vx*p1.vx + p1.vy*p1.vy) * p1.mass;
644
+ const p2MomentumAfter = Math.sqrt(p2.vx*p2.vx + p2.vy*p2.vy) * p2.mass;
645
+
646
+ const p1EnergyBefore = 0.5 * p1.mass * (p1vBefore.x*p1vBefore.x + p1vBefore.y*p1vBefore.y);
647
+ const p2EnergyBefore = 0.5 * p2.mass * (p2vBefore.x*p2vBefore.x + p2vBefore.y*p2vBefore.y);
648
+ const p1EnergyAfter = 0.5 * p1.mass * (p1.vx*p1.vx + p1.vy*p1.vy);
649
+ const p2EnergyAfter = 0.5 * p2.mass * (p2.vx*p2.vx + p2.vy*p2.vy);
650
+
651
+ const totalMomentumBefore = p1MomentumBefore + p2MomentumBefore;
652
+ const totalMomentumAfter = p1MomentumAfter + p2MomentumAfter;
653
+ const momentumChange = totalMomentumAfter - totalMomentumBefore;
654
+
655
+ const totalEnergyBefore = p1EnergyBefore + p2EnergyBefore;
656
+ const totalEnergyAfter = p1EnergyAfter + p2EnergyAfter;
657
+ const energyChange = totalEnergyAfter - totalEnergyBefore;
658
+
659
+ // Add to history
660
+ state.collisionHistory.unshift({
661
+ time: timeStr,
662
+ particles: [p1.id, p2.id],
663
+ velocityBefore: [
664
+ { x: p1vBefore.x.toFixed(2), y: p1vBefore.y.toFixed(2) },
665
+ { x: p2vBefore.x.toFixed(2), y: p2vBefore.y.toFixed(2) }
666
+ ],
667
+ velocityAfter: [
668
+ { x: p1.vx.toFixed(2), y: p1.vy.toFixed(2) },
669
+ { x: p2.vx.toFixed(2), y: p2.vy.toFixed(2) }
670
+ ],
671
+ momentumChange: momentumChange.toFixed(2),
672
+ energyChange: energyChange.toFixed(2)
673
+ });
674
+
675
+ // Keep only last 10 collisions
676
+ if (state.collisionHistory.length > 10) {
677
+ state.collisionHistory.pop();
678
+ }
679
+
680
+ // Update collision table
681
+ updateCollisionTable();
682
+ }
683
+
684
+ // Update collision table
685
+ function updateCollisionTable() {
686
+ collisionData.innerHTML = '';
687
+
688
+ if (state.collisionHistory.length === 0) {
689
+ collisionData.innerHTML = `
690
+ <tr>
691
+ <td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">No collision data yet</td>
692
+ </tr>
693
+ `;
694
+ return;
695
+ }
696
+
697
+ state.collisionHistory.forEach(collision => {
698
+ const row = document.createElement('tr');
699
+ row.className = 'hover:bg-gray-50';
700
+
701
+ row.innerHTML = `
702
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${collision.time}</td>
703
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${collision.particles[0]} & ${collision.particles[1]}</td>
704
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
705
+ (${collision.velocityBefore[0].x}, ${collision.velocityBefore[0].y})<br>
706
+ (${collision.velocityBefore[1].x}, ${collision.velocityBefore[1].y})
707
+ </td>
708
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
709
+ (${collision.velocityAfter[0].x}, ${collision.velocityAfter[0].y})<br>
710
+ (${collision.velocityAfter[1].x}, ${collision.velocityAfter[1].y})
711
+ </td>
712
+ <td class="px-6 py-4 whitespace-nowrap text-sm ${collision.momentumChange == 0 ? 'text-green-600' : 'text-red-600'}">
713
+ ${collision.momentumChange}
714
+ </td>
715
+ <td class="px-6 py-4 whitespace-nowrap text-sm ${collision.energyChange <= 0 ? 'text-green-600' : 'text-red-600'}">
716
+ ${collision.energyChange}
717
+ </td>
718
+ `;
719
+
720
+ collisionData.appendChild(row);
721
+ });
722
+ }
723
+
724
+ // Update particle counter
725
+ function updateParticleCounter() {
726
+ particleCounter.textContent = `${state.particles.length} particle${state.particles.length !== 1 ? 's' : ''}`;
727
+ }
728
+
729
+ // Update particle properties panel
730
+ function updateParticleProperties() {
731
+ particleProperties.innerHTML = '';
732
+
733
+ if (state.selectedParticles.length === 0) {
734
+ particleProperties.innerHTML = `
735
+ <div class="text-center text-gray-500 py-4">
736
+ Select particles to manage them
737
+ </div>
738
+ `;
739
+ return;
740
+ }
741
+
742
+ if (state.selectedParticles.length > 1) {
743
+ const card = document.createElement('div');
744
+ card.className = 'property-card bg-gray-50 p-4 rounded-lg border border-gray-200';
745
+
746
+ card.innerHTML = `
747
+ <div class="flex justify-between items-center mb-3">
748
+ <h4 class="font-medium text-gray-700">${state.selectedParticles.length} Particles Selected</h4>
749
+ <div class="flex space-x-1">
750
+ <button class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs" id="freezeSelectedBtn">
751
+ <i class="fas fa-snowflake mr-1"></i> Freeze
752
+ </button>
753
+ <button class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs" id="removeSelectedBtn">
754
+ <i class="fas fa-trash mr-1"></i> Remove
755
+ </button>
756
+ </div>
757
+ </div>
758
+
759
+ <button class="w-full px-2 py-1 bg-indigo-100 text-indigo-700 rounded text-sm mt-2" id="randomizeVelocitiesBtn">
760
+ <i class="fas fa-random mr-1"></i> Randomize Velocities
761
+ </button>
762
+ <button class="w-full px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm mt-1" id="zeroVelocitiesBtn">
763
+ <i class="fas fa-ban mr-1"></i> Zero Velocities
764
+ </button>
765
+ `;
766
+
767
+ // Add event listeners
768
+ card.querySelector('#freezeSelectedBtn').addEventListener('click', () => {
769
+ state.selectedParticles.forEach(p => p.frozen = !p.frozen);
770
+ updateParticleProperties();
771
+ });
772
+
773
+ card.querySelector('#removeSelectedBtn').addEventListener('click', () => {
774
+ state.particles = state.particles.filter(p => !state.selectedParticles.includes(p));
775
+ state.selectedParticles = [];
776
+ updateParticleProperties();
777
+ updateParticleCounter();
778
+ });
779
+
780
+ card.querySelector('#randomizeVelocitiesBtn').addEventListener('click', () => {
781
+ state.selectedParticles.forEach(p => {
782
+ p.vx = (Math.random() - 0.5) * 10;
783
+ p.vy = (Math.random() - 0.5) * 10;
784
+ });
785
+ });
786
+
787
+ card.querySelector('#zeroVelocitiesBtn').addEventListener('click', () => {
788
+ state.selectedParticles.forEach(p => {
789
+ p.vx = 0;
790
+ p.vy = 0;
791
+ });
792
+ });
793
+
794
+ particleProperties.appendChild(card);
795
+ return;
796
+ }
797
+
798
+ const p = state.selectedParticles[0];
799
+
800
+ const card = document.createElement('div');
801
+ card.className = 'property-card bg-gray-50 p-4 rounded-lg border border-gray-200';
802
+
803
+ card.innerHTML = `
804
+ <div class="flex justify-between items-center mb-3">
805
+ <h4 class="font-medium text-gray-700">Particle ${p.id.substr(0, 6)}</h4>
806
+ <div class="flex items-center">
807
+ <span class="inline-block w-4 h-4 rounded-full mr-2" style="background-color: ${p.color};"></span>
808
+ <button class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs" id="freezeBtn">
809
+ <i class="fas ${p.frozen ? 'fa-fire' : 'fa-snowflake'} mr-1"></i> ${p.frozen ? 'Unfreeze' : 'Freeze'}
810
+ </button>
811
+ </div>
812
+ </div>
813
+
814
+ <div class="grid grid-cols-2 gap-3 mb-3">
815
+ <div>
816
+ <label class="block text-xs text-gray-500 mb-1">Position X</label>
817
+ <input type="number" value="${p.x.toFixed(1)}" class="w-full px-2 py-1 text-sm border rounded" data-property="x">
818
+ </div>
819
+ <div>
820
+ <label class="block text-xs text-gray-500 mb-1">Position Y</label>
821
+ <input type="number" value="${p.y.toFixed(1)}" class="w-full px-2 py-1 text-sm border rounded" data-property="y">
822
+ </div>
823
+ </div>
824
+
825
+ <div class="grid grid-cols-2 gap-3 mb-3">
826
+ <div>
827
+ <label class="block text-xs text-gray-500 mb-1">Velocity X</label>
828
+ <input type="number" value="${p.vx.toFixed(1)}" class="w-full px-2 py-1 text-sm border rounded" data-property="vx">
829
+ </div>
830
+ <div>
831
+ <label class="block text-xs text-gray-500 mb-1">Velocity Y</label>
832
+ <input type="number" value="${p.vy.toFixed(1)}" class="w-full px-2 py-1 text-sm border rounded" data-property="vy">
833
+ </div>
834
+ </div>
835
+
836
+ <div class="grid grid-cols-2 gap-3 mb-3">
837
+ <div>
838
+ <label class="block text-xs text-gray-500 mb-1">Radius</label>
839
+ <input type="number" value="${p.radius}" class="w-full px-2 py-1 text-sm border rounded" data-property="radius">
840
+ </div>
841
+ <div>
842
+ <label class="block text-xs text-gray-500 mb-1">Mass</label>
843
+ <input type="number" value="${p.mass}" class="w-full px-2 py-1 text-sm border rounded" data-property="mass">
844
+ </div>
845
+ </div>
846
+
847
+ <div class="mb-3">
848
+ <label class="block text-xs text-gray-500 mb-1">Color</label>
849
+ <input type="color" value="${p.color}" class="w-full h-8" data-property="color">
850
+ </div>
851
+
852
+ <div class="text-xs text-gray-500">
853
+ <div class="flex justify-between mb-1">
854
+ <span>Momentum:</span>
855
+ <span>${p.momentum.toFixed(2)} kg·m/s</span>
856
+ </div>
857
+ <div class="flex justify-between">
858
+ <span>Kinetic Energy:</span>
859
+ <span>${p.kineticEnergy.toFixed(2)} J</span>
860
+ </div>
861
+ <div class="flex justify-between mt-2">
862
+ <span>Collisions:</span>
863
+ <span>${p.collisions}</span>
864
+ </div>
865
+ </div>
866
+ `;
867
+
868
+ // Add freeze button event
869
+ card.querySelector('#freezeBtn').addEventListener('click', () => {
870
+ p.frozen = !p.frozen;
871
+ updateParticleProperties();
872
+ });
873
+
874
+ // Add event listeners to inputs
875
+ const inputs = card.querySelectorAll('input');
876
+ inputs.forEach(input => {
877
+ input.addEventListener('change', (e) => {
878
+ const property = e.target.dataset.property;
879
+ let value = e.target.value;
880
+
881
+ if (property === 'color') {
882
+ p[property] = value;
883
+ } else {
884
+ value = parseFloat(value);
885
+ if (!isNaN(value)) {
886
+ // Special handling for radius to prevent negative values
887
+ if (property === 'radius') {
888
+ p[property] = Math.max(5, value);
889
+ } else {
890
+ p[property] = value;
891
+ }
892
+ }
893
+ }
894
+
895
+ // If position changed, make sure particle stays within bounds
896
+ if (property === 'x' || property === 'y' || property === 'radius') {
897
+ p.x = Math.max(p.radius, Math.min(p.x, canvas.width - p.radius));
898
+ p.y = Math.max(p.radius, Math.min(p.y, canvas.height - p.radius));
899
+ }
900
+ });
901
+ });
902
+
903
+ particleProperties.appendChild(card);
904
+ }
905
+
906
+ // Calculate system momentum and energy
907
+ function calculateSystemMetrics() {
908
+ let totalMomentum = 0;
909
+ let totalEnergy = 0;
910
+
911
+ state.particles.forEach(p => {
912
+ totalMomentum += p.momentum;
913
+ totalEnergy += p.kineticEnergy;
914
+ });
915
+
916
+ state.systemMomentum = totalMomentum;
917
+ state.systemEnergy = totalEnergy;
918
+
919
+ // Update max values for meter scaling
920
+ state.maxMomentum = Math.max(state.maxMomentum, totalMomentum);
921
+ state.maxEnergy = Math.max(state.maxEnergy, totalEnergy);
922
+
923
+ // Update UI
924
+ momentumValue.textContent = `${totalMomentum.toFixed(2)} kg·m/s`;
925
+ energyValue.textContent = `${totalEnergy.toFixed(2)} J`;
926
+
927
+ // Update meters
928
+ const momentumPercent = Math.min(100, (totalMomentum / (state.maxMomentum || 1)) * 100);
929
+ const energyPercent = Math.min(100, (totalEnergy / (state.maxEnergy || 1)) * 100);
930
+
931
+ momentumMeter.style.width = `${momentumPercent}%`;
932
+ energyMeter.style.width = `${energyPercent}%`;
933
+ }
934
+
935
+ // Main animation loop
936
+ function animate(timestamp) {
937
+ if (!state.lastTime) state.lastTime = timestamp;
938
+ const dt = (timestamp - state.lastTime) / 1000; // delta time in seconds
939
+ state.lastTime = timestamp;
940
+
941
+ // Clear canvas
942
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
943
+
944
+ // Update and draw particles
945
+ if (state.running) {
946
+ // Update particles
947
+ state.particles.forEach(p => p.update(dt));
948
+
949
+ // Check collisions
950
+ for (let i = 0; i < state.particles.length; i++) {
951
+ for (let j = i + 1; j < state.particles.length; j++) {
952
+ if (checkCollision(state.particles[i], state.particles[j])) {
953
+ resolveCollision(state.particles[i], state.particles[j]);
954
+ }
955
+ }
956
+ }
957
+
958
+ // Calculate system metrics
959
+ calculateSystemMetrics();
960
+ }
961
+
962
+ // Draw particles
963
+ state.particles.forEach(p => p.draw());
964
+
965
+ // Continue animation loop
966
+ if (state.running || state.selectedParticles.length > 0) {
967
+ requestAnimationFrame(animate);
968
+ }
969
+ }
970
+
971
+ // Generate random color
972
+ function getRandomColor() {
973
+ const letters = '0123456789ABCDEF';
974
+ let color = '#';
975
+ for (let i = 0; i < 6; i++) {
976
+ color += letters[Math.floor(Math.random() * 16)];
977
+ }
978
+ return color;
979
+ }
980
+
981
+ // Create multiple particles
982
+ function createParticles(count, radius, mass, color, randomize = true) {
983
+ const newParticles = [];
984
+
985
+ for (let i = 0; i < count; i++) {
986
+ // Randomize properties if enabled
987
+ const partRadius = randomize ? radius * (0.8 + Math.random() * 0.4) : radius;
988
+ const partMass = randomize ? mass * (0.8 + Math.random() * 0.4) : mass;
989
+ const partColor = randomize ? getRandomColor() : color;
990
+
991
+ // Random position that doesn't collide with existing particles
992
+ let x, y, attempts = 0;
993
+ do {
994
+ x = partRadius + Math.random() * (canvas.width - 2 * partRadius);
995
+ y = partRadius + Math.random() * (canvas.height - 2 * partRadius);
996
+ attempts++;
997
+
998
+ // Give up after 100 attempts (probably not enough space)
999
+ if (attempts > 100) break;
1000
+ } while (state.particles.some(p => {
1001
+ const dx = p.x - x;
1002
+ const dy = p.y - y;
1003
+ const distance = Math.sqrt(dx * dx + dy * dy);
1004
+ return distance < p.radius + partRadius;
1005
+ }));
1006
+
1007
+ // Random velocity if randomize is enabled
1008
+ const vx = randomize ? (Math.random() - 0.5) * 5 : 0;
1009
+ const vy = randomize ? (Math.random() - 0.5) * 5 : 0;
1010
+
1011
+ const particle = new Particle(
1012
+ x, y,
1013
+ partRadius,
1014
+ partMass,
1015
+ partColor,
1016
+ vx, vy
1017
+ );
1018
+
1019
+ newParticles.push(particle);
1020
+ }
1021
+
1022
+ return newParticles;
1023
+ }
1024
+
1025
+ // Event listeners
1026
+ playPauseBtn.addEventListener('click', () => {
1027
+ state.running = !state.running;
1028
+ playPauseBtn.innerHTML = state.running ?
1029
+ '<i class="fas fa-pause"></i> Pause' :
1030
+ '<i class="fas fa-play"></i> Play';
1031
+
1032
+ if (state.running) {
1033
+ state.lastTime = 0;
1034
+ requestAnimationFrame(animate);
1035
+ }
1036
+ });
1037
+
1038
+ resetBtn.addEventListener('click', () => {
1039
+ state.particles = [];
1040
+ state.selectedParticles = [];
1041
+ state.running = false;
1042
+ state.collisionHistory = [];
1043
+ state.systemMomentum = 0;
1044
+ state.systemEnergy = 0;
1045
+ state.maxMomentum = 1;
1046
+ state.maxEnergy = 1;
1047
+
1048
+ playPauseBtn.innerHTML = '<i class="fas fa-play"></i> Play';
1049
+ momentumValue.textContent = '0.00 kg·m/s';
1050
+ energyValue.textContent = '0.00 J';
1051
+ momentumMeter.style.width = '0%';
1052
+ energyMeter.style.width = '0%';
1053
+ updateParticleProperties();
1054
+ updateParticleCounter();
1055
+ updateCollisionTable();
1056
+
1057
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1058
+ });
1059
+
1060
+ addParticleBtn.addEventListener('click', () => {
1061
+ state.creatingParticles = true;
1062
+ particleCreator.classList.remove('hidden');
1063
+ });
1064
+
1065
+ cancelCreate.addEventListener('click', () => {
1066
+ state.creatingParticles = false;
1067
+ particleCreator.classList.add('hidden');
1068
+ });
1069
+
1070
+ confirmCreate.addEventListener('click', () => {
1071
+ const count = parseInt(creatorQuantity.value) || 1;
1072
+ const radius = parseInt(creatorRadius.value) || 20;
1073
+ const mass = parseInt(creatorMass.value) || 5;
1074
+ const color = creatorColor.value;
1075
+ const randomize = randomizeProps.checked;
1076
+
1077
+ const newParticles = createParticles(
1078
+ count, radius, mass, color, randomize
1079
+ );
1080
+
1081
+ state.particles.push(...newParticles);
1082
+ state.creatingParticles = false;
1083
+ particleCreator.classList.add('hidden');
1084
+ updateParticleCounter();
1085
+
1086
+ if (!state.running) {
1087
+ animate(performance.now());
1088
+ }
1089
+ });
1090
+
1091
+ removeParticleBtn.addEventListener('click', () => {
1092
+ state.particles = [];
1093
+ state.selectedParticles = [];
1094
+ updateParticleProperties();
1095
+ updateParticleCounter();
1096
+
1097
+ if (!state.running) {
1098
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1099
+ }
1100
+ });
1101
+
1102
+ randomizeBtn.addEventListener('click', () => {
1103
+ state.particles.forEach(p => {
1104
+ p.vx = (Math.random() - 0.5) * 10;
1105
+ p.vy = (Math.random() - 0.5) * 10;
1106
+ p.frozen = false;
1107
+ });
1108
+ updateParticleProperties();
1109
+ });
1110
+
1111
+ freezeAllBtn.addEventListener('click', () => {
1112
+ const allFrozen = state.particles.every(p => p.frozen);
1113
+ state.particles.forEach(p => p.frozen = !allFrozen);
1114
+ updateParticleProperties();
1115
+ });
1116
+
1117
+ // Preset scenarios
1118
+ elasticCollisionBtn.addEventListener('click', () => {
1119
+ state.particles = [];
1120
+ state.restitution = 1.0;
1121
+ restitutionThumb.style.left = '100%';
1122
+ restitutionFill.style.width = '100%';
1123
+ restitutionValue.textContent = '100%';
1124
+
1125
+ // Create two particles moving toward each other
1126
+ const p1 = new Particle(
1127
+ canvas.width * 0.3, canvas.height / 2,
1128
+ 30, 5, '#4f46e5',
1129
+ 200, 0
1130
+ );
1131
+
1132
+ const p2 = new Particle(
1133
+ canvas.width * 0.7, canvas.height / 2,
1134
+ 30, 5, '#10b981',
1135
+ -200, 0
1136
+ );
1137
+
1138
+ state.particles.push(p1, p2);
1139
+ updateParticleCounter();
1140
+
1141
+ if (!state.running) {
1142
+ animate(performance.now());
1143
+ }
1144
+ });
1145
+
1146
+ inelasticCollisionBtn.addEventListener('click', () => {
1147
+ state.particles = [];
1148
+ state.restitution = 0.2;
1149
+ restitutionThumb.style.left = '20%';
1150
+ restitutionFill.style.width = '20%';
1151
+ restitutionValue.textContent = '20%';
1152
+
1153
+ // Create two particles moving toward each other
1154
+ const p1 = new Particle(
1155
+ canvas.width * 0.3, canvas.height / 2,
1156
+ 30, 5, '#4f46e5',
1157
+ 200, 0
1158
+ );
1159
+
1160
+ const p2 = new Particle(
1161
+ canvas.width * 0.7, canvas.height / 2,
1162
+ 30, 5, '#10b981',
1163
+ -200, 0
1164
+ );
1165
+
1166
+ state.particles.push(p1, p2);
1167
+ updateParticleCounter();
1168
+
1169
+ if (!state.running) {
1170
+ animate(performance.now());
1171
+ }
1172
+ });
1173
+
1174
+ newtonsCradleBtn.addEventListener('click', () => {
1175
+ state.particles = [];
1176
+ state.restitution = 0.95;
1177
+ restitutionThumb.style.left = '95%';
1178
+ restitutionFill.style.width = '95%';
1179
+ restitutionValue.textContent = '95%';
1180
+
1181
+ // Create Newton's Cradle with 5 balls
1182
+ const radius = 25;
1183
+ const startX = canvas.width / 2 - radius * 5;
1184
+ const startY = canvas.height / 2;
1185
+
1186
+ for (let i = 0; i < 5; i++) {
1187
+ const p = new Particle(
1188
+ startX + i * radius * 2, startY,
1189
+ radius, 5, '#3b82f6',
1190
+ 0, 0
1191
+ );
1192
+ state.particles.push(p);
1193
+ }
1194
+
1195
+ // Pull first ball and release
1196
+ state.particles[0].vx = 300;
1197
+
1198
+ updateParticleCounter();
1199
+
1200
+ if (!state.running) {
1201
+ animate(performance.now());
1202
+ }
1203
+ });
1204
+
1205
+ particleExplosionBtn.addEventListener('click', () => {
1206
+ state.particles = [];
1207
+ state.restitution = 0.8;
1208
+ restitutionThumb.style.left = '80%';
1209
+ restitutionFill.style.width = '80%';
1210
+ restitutionValue.textContent = '80%';
1211
+
1212
+ // Create explosion at center
1213
+ const centerX = canvas.width / 2;
1214
+ const centerY = canvas.height / 2;
1215
+ const count = 10;
1216
+
1217
+ for (let i = 0; i < count; i++) {
1218
+ const angle = (i / count) * Math.PI * 2;
1219
+ const speed = 100 + Math.random() * 100;
1220
+
1221
+ const p = new Particle(
1222
+ centerX, centerY,
1223
+ 15 + Math.random() * 10,
1224
+ 2 + Math.random() * 5,
1225
+ getRandomColor(),
1226
+ Math.cos(angle) * speed,
1227
+ Math.sin(angle) * speed
1228
+ );
1229
+
1230
+ state.particles.push(p);
1231
+ }
1232
+
1233
+ updateParticleCounter();
1234
+
1235
+ if (!state.running) {
1236
+ animate(performance.now());
1237
+ }
1238
+ });
1239
+
1240
+ // Slider value displays
1241
+ creatorRadius.addEventListener('input', () => {
1242
+ creatorRadiusValue.textContent = creatorRadius.value;
1243
+ });
1244
+
1245
+ creatorMass.addEventListener('input', () => {
1246
+ creatorMassValue.textContent = creatorMass.value;
1247
+ });
1248
+
1249
+ // Canvas interaction
1250
+ canvas.addEventListener('mousedown', (e) => {
1251
+ if (state.creatingParticles) return;
1252
+
1253
+ const rect = canvas.getBoundingClientRect();
1254
+ const x = e.clientX - rect.left;
1255
+ const y = e.clientY - rect.top;
1256
+
1257
+ // Check if clicking on a particle
1258
+ let clickedParticle = null;
1259
+ for (let i = state.particles.length - 1; i >= 0; i--) {
1260
+ if (state.particles[i].isPointInside(x, y)) {
1261
+ clickedParticle = state.particles[i];
1262
+ break;
1263
+ }
1264
+ }
1265
+
1266
+ // Handle shift key for multi-selection
1267
+ if (e.shiftKey && clickedParticle) {
1268
+ clickedParticle.selected = !clickedParticle.selected;
1269
+
1270
+ if (clickedParticle.selected) {
1271
+ state.selectedParticles.push(clickedParticle);
1272
+ } else {
1273
+ state.selectedParticles = state.selectedParticles.filter(p => p !== clickedParticle);
1274
+ }
1275
+ } else {
1276
+ // Single selection or drag start
1277
+ if (clickedParticle) {
1278
+ // Toggle selection if clicking the same particle
1279
+ if (state.selectedParticles.length === 1 && state.selectedParticles[0] === clickedParticle) {
1280
+ clickedParticle.selected = false;
1281
+ state.selectedParticles = [];
1282
+ } else {
1283
+ // Deselect all and select clicked particle
1284
+ state.particles.forEach(p => p.selected = false);
1285
+ state.selectedParticles = [clickedParticle];
1286
+ clickedParticle.selected = true;
1287
+
1288
+ // Prepare for dragging
1289
+ clickedParticle.dragging = true;
1290
+ clickedParticle.dragOffsetX = x - clickedParticle.x;
1291
+ clickedParticle.dragOffsetY = y - clickedParticle.y;
1292
+ }
1293
+ } else {
1294
+ // Clicked empty space - deselect all
1295
+ state.particles.forEach(p => p.selected = false);
1296
+ state.selectedParticles = [];
1297
+ }
1298
+ }
1299
+
1300
+ updateParticleProperties();
1301
+
1302
+ if (!state.running && (state.particles.length > 0 || state.selectedParticles.length > 0)) {
1303
+ animate(performance.now());
1304
+ }
1305
+ });
1306
+
1307
+ canvas.addEventListener('mousemove', (e) => {
1308
+ if (state.selectedParticles.length === 1 && state.selectedParticles[0].dragging) {
1309
+ const rect = canvas.getBoundingClientRect();
1310
+ const x = e.clientX - rect.left;
1311
+ const y = e.clientY - rect.top;
1312
+
1313
+ const p = state.selectedParticles[0];
1314
+ p.x = x - p.dragOffsetX;
1315
+ p.y = y - p.dragOffsetY;
1316
+
1317
+ // Keep particle within bounds
1318
+ p.x = Math.max(p.radius, Math.min(p.x, canvas.width - p.radius));
1319
+ p.y = Math.max(p.radius, Math.min(p.y, canvas.height - p.radius));
1320
+
1321
+ if (!state.running) {
1322
+ animate(performance.now());
1323
+ }
1324
+ }
1325
+ });
1326
+
1327
+ canvas.addEventListener('mouseup', () => {
1328
+ if (state.selectedParticles.length === 1) {
1329
+ state.selectedParticles[0].dragging = false;
1330
+ }
1331
+ });
1332
+
1333
+ canvas.addEventListener('mouseleave', () => {
1334
+ if (state.selectedParticles.length === 1) {
1335
+ state.selectedParticles[0].dragging = false;
1336
+ }
1337
+ });
1338
+
1339
+ // Initial setup
1340
+ updateParticleCounter();
1341
+ </script>
1342
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=engerl/physics-collision-lab" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
1343
+ </html>