Spaces:
Running
Running
Update index.html
Browse files- index.html +411 -531
index.html
CHANGED
@@ -2,161 +2,188 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
-
<title>
|
6 |
<style>
|
7 |
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
|
8 |
-
canvas { display: block; }
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
15 |
z-index: 1000;
|
16 |
-
}
|
17 |
-
#controls {
|
18 |
-
top: 10px;
|
19 |
-
left: 10px;
|
20 |
-
max-width: 320px;
|
21 |
-
max-height: 90vh;
|
22 |
-
overflow-y: auto;
|
23 |
-
}
|
24 |
-
#controls label { display: block; margin-top: 8px; }
|
25 |
-
#controls input, #controls select, #controls button {
|
26 |
-
width: 100%; padding: 6px; margin-top: 4px; box-sizing: border-box;
|
27 |
-
}
|
28 |
-
#buttonContainer {
|
29 |
display: flex;
|
30 |
justify-content: space-between;
|
31 |
-
|
|
|
32 |
}
|
33 |
-
#
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
display: none;
|
40 |
}
|
41 |
-
#
|
42 |
-
#
|
43 |
-
|
44 |
-
|
|
|
|
|
45 |
}
|
46 |
-
|
47 |
-
#
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
}
|
53 |
-
#
|
54 |
-
#
|
55 |
-
#
|
56 |
-
width: 100%; padding: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
}
|
58 |
-
|
59 |
-
#
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
}
|
65 |
-
#
|
66 |
-
#
|
|
|
|
|
|
|
67 |
#constraintEditPanel input, #constraintEditPanel select, #constraintEditPanel button {
|
68 |
-
width: 100%; padding: 6px;
|
69 |
-
}
|
70 |
</style>
|
71 |
</head>
|
72 |
<body>
|
73 |
-
<!--
|
74 |
-
<div id="
|
75 |
-
<
|
76 |
-
|
77 |
-
<
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
<button id="connectSystems">Connect Systems</button>
|
89 |
-
<button id="reset">Reset Simulation</button>
|
90 |
</div>
|
91 |
</div>
|
92 |
|
93 |
-
<!--
|
94 |
-
<div id="
|
|
|
|
|
95 |
|
96 |
-
<!-- Connection
|
97 |
-
<div id="
|
98 |
-
<
|
99 |
-
<
|
100 |
-
|
101 |
-
<
|
102 |
-
<
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
<select id="connMode">
|
107 |
-
<option value="element">Element</option>
|
108 |
-
<option value="system">System</option>
|
109 |
-
</select>
|
110 |
-
<!-- Show endpoint options only for springMass systems -->
|
111 |
-
<div id="sourceEndpointDiv">
|
112 |
-
<label for="sourceEndpoint">Source Endpoint:</label>
|
113 |
-
<select id="sourceEndpoint">
|
114 |
-
<option value="mass">Mass</option>
|
115 |
-
<option value="fixed">Fixed</option>
|
116 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
</div>
|
118 |
-
<button id="cancelConn">Cancel</button>
|
119 |
-
<p style="font-size: 12px; color: #555;">Now click on the target element to attach.</p>
|
120 |
</div>
|
121 |
|
122 |
-
<!--
|
123 |
-
<div id="
|
124 |
-
<
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
<
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
<
|
136 |
-
<
|
137 |
-
<
|
138 |
-
|
139 |
-
|
140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
</div>
|
142 |
|
143 |
-
<!-- Matter.js Library -->
|
144 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
|
145 |
<script>
|
146 |
-
|
147 |
-
const Engine = Matter.Engine,
|
148 |
-
Render = Matter.Render,
|
149 |
-
Runner = Matter.Runner,
|
150 |
-
World = Matter.World,
|
151 |
-
Bodies = Matter.Bodies,
|
152 |
-
Body = Matter.Body,
|
153 |
-
Constraint = Matter.Constraint,
|
154 |
-
Mouse = Matter.Mouse,
|
155 |
-
MouseConstraint = Matter.MouseConstraint,
|
156 |
-
Events = Matter.Events;
|
157 |
-
|
158 |
const engine = Engine.create();
|
159 |
const world = engine.world;
|
|
|
160 |
const render = Render.create({
|
161 |
element: document.body,
|
162 |
engine: engine,
|
@@ -164,350 +191,265 @@
|
|
164 |
width: window.innerWidth,
|
165 |
height: window.innerHeight,
|
166 |
wireframes: false,
|
167 |
-
background: '#
|
168 |
}
|
169 |
});
|
170 |
Render.run(render);
|
171 |
const runner = Runner.create();
|
172 |
Runner.run(runner, engine);
|
173 |
|
174 |
-
// Global boundaries (to be updated on resize)
|
175 |
let floor, wallLeft, wallRight;
|
176 |
function updateBoundaries() {
|
177 |
-
if (floor && wallLeft && wallRight)
|
178 |
-
|
179 |
-
}
|
180 |
-
|
181 |
-
isStatic: true,
|
182 |
-
render: { fillStyle: '#060a19' }
|
183 |
-
});
|
184 |
-
wallLeft = Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, { isStatic: true });
|
185 |
-
wallRight = Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, { isStatic: true });
|
186 |
World.add(world, [floor, wallLeft, wallRight]);
|
187 |
}
|
188 |
updateBoundaries();
|
189 |
|
190 |
-
|
191 |
-
|
192 |
-
let mouseConstraint = MouseConstraint.create(engine, {
|
193 |
-
mouse: mouse,
|
194 |
-
constraint: { stiffness: 0.2, render: { visible: false } }
|
195 |
-
});
|
196 |
World.add(world, mouseConstraint);
|
197 |
render.mouse = mouse;
|
198 |
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
|
207 |
-
|
208 |
-
function getRandomColor() {
|
209 |
-
return '#' + Math.floor(Math.random()*16777215).toString(16);
|
210 |
-
}
|
211 |
|
212 |
-
/* ---------- Dynamic Suboptions for Adding Elements ---------- */
|
213 |
function updateSubOptions() {
|
214 |
const simulationSelect = document.getElementById('simulationSelect');
|
215 |
const subOptionsContainer = document.getElementById('subOptionsContainer');
|
216 |
-
|
217 |
let html = '';
|
218 |
-
if(selected === 'bouncingBall'){
|
219 |
-
|
220 |
-
<label for="ballInitialX">Initial X:</label>
|
221 |
-
<input type="number" id="
|
222 |
-
<label for="
|
223 |
-
<input type="number" id="
|
224 |
-
<label for="
|
225 |
-
<input type="
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
<input type="number" id="
|
230 |
-
<label for="
|
231 |
-
<input type="
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
<input type="number" id="
|
239 |
-
<label for="
|
240 |
-
<input type="number" id="
|
241 |
-
<label for="
|
242 |
-
<input type="number" id="
|
243 |
-
<label for="
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
<label for="
|
251 |
-
<input type="number" id="
|
252 |
-
<label for="
|
253 |
-
<input type="number" id="
|
254 |
-
<label for="
|
255 |
-
<input type="number" id="
|
256 |
-
<label for="
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
<label for="
|
261 |
-
<input type="
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
<label for="
|
266 |
-
<input type="
|
267 |
-
|
268 |
-
<input type="number" id="rampHeight" value="20">
|
269 |
-
<label for="rampAngle">Ramp Angle (°):</label>
|
270 |
-
<input type="number" id="rampAngle" value="30">
|
271 |
-
<label for="rampX">Ramp X:</label>
|
272 |
-
<input type="number" id="rampX" value="400">
|
273 |
-
<label for="rampY">Ramp Y:</label>
|
274 |
-
<input type="number" id="rampY" value="${window.innerHeight-150}">
|
275 |
-
<label for="rampColor">Ramp Color:</label>
|
276 |
-
<input type="color" id="rampColor" value="#8e44ad">
|
277 |
-
<hr>
|
278 |
-
<label for="blockWidth">Block Width:</label>
|
279 |
-
<input type="number" id="blockWidth" value="40">
|
280 |
-
<label for="blockHeight">Block Height:</label>
|
281 |
-
<input type="number" id="blockHeight" value="40">
|
282 |
-
<label for="blockFriction">Block Friction:</label>
|
283 |
-
<input type="number" id="blockFriction" value="0.05" step="0.01" min="0" max="1">
|
284 |
-
<label for="blockColor">Block Color:</label>
|
285 |
-
<input type="color" id="blockColor" value="#1abc9c">
|
286 |
-
`;
|
287 |
-
} else if(selected === 'springMass'){
|
288 |
-
html += `
|
289 |
-
<label for="springMassRadius">Mass Radius:</label>
|
290 |
-
<input type="number" id="springMassRadius" value="25">
|
291 |
-
<label for="springMassRestitution">Restitution:</label>
|
292 |
-
<input type="number" id="springMassRestitution" value="0.8" step="0.1" min="0" max="1">
|
293 |
-
<label for="fixedPointX">Fixed X:</label>
|
294 |
-
<input type="number" id="fixedPointX" value="${Math.floor(window.innerWidth/2)}">
|
295 |
-
<label for="fixedPointY">Fixed Y:</label>
|
296 |
-
<input type="number" id="fixedPointY" value="100">
|
297 |
-
<label for="springLength">Spring Length:</label>
|
298 |
-
<input type="number" id="springLength" value="200">
|
299 |
-
<label for="springStiffness">Stiffness:</label>
|
300 |
-
<input type="number" id="springStiffness" value="0.02" step="0.01" min="0" max="1">
|
301 |
-
<label for="springMassColor">Color:</label>
|
302 |
-
<input type="color" id="springMassColor" value="#f39c12">
|
303 |
-
`;
|
304 |
-
} else if(selected === 'rectangleBlock'){
|
305 |
-
html += `
|
306 |
-
<label for="blockInitX">Initial X:</label>
|
307 |
-
<input type="number" id="blockInitX" value="150">
|
308 |
-
<label for="blockInitY">Initial Y:</label>
|
309 |
-
<input type="number" id="blockInitY" value="150">
|
310 |
-
<label for="blockWidth">Width:</label>
|
311 |
-
<input type="number" id="blockWidth" value="80">
|
312 |
-
<label for="blockHeight">Height:</label>
|
313 |
-
<input type="number" id="blockHeight" value="40">
|
314 |
-
<label for="blockFriction">Friction:</label>
|
315 |
-
<input type="number" id="blockFriction" value="0.1" step="0.01" min="0" max="1">
|
316 |
-
<label for="blockRestitution">Restitution:</label>
|
317 |
-
<input type="number" id="blockRestitution" value="0.3" step="0.1" min="0" max="1">
|
318 |
-
<label for="blockColor">Color:</label>
|
319 |
-
<input type="color" id="blockColor" value="#2ecc71">
|
320 |
-
`;
|
321 |
}
|
322 |
subOptionsContainer.innerHTML = html;
|
|
|
323 |
}
|
324 |
updateSubOptions();
|
325 |
document.getElementById('simulationSelect').addEventListener('change', updateSubOptions);
|
326 |
|
327 |
-
|
328 |
-
function addBouncingBall(){
|
329 |
const x = parseFloat(document.getElementById("ballInitialX").value);
|
330 |
const y = parseFloat(document.getElementById("ballInitialY").value);
|
331 |
const radius = parseFloat(document.getElementById("ballRadius").value);
|
332 |
const restitution = parseFloat(document.getElementById("ballRestitution").value);
|
333 |
const friction = parseFloat(document.getElementById("ballFriction").value);
|
334 |
-
const color = document.getElementById("ballColor").value
|
335 |
-
const ball = Bodies.circle(x, y, radius, { restitution, friction, render: { fillStyle: color } });
|
336 |
ball.elementType = "bouncingBall";
|
337 |
-
ball.customOptions = { x, y, radius, restitution, friction, color
|
338 |
World.add(world, ball);
|
339 |
}
|
340 |
-
|
|
|
341 |
const pivotX = parseFloat(document.getElementById("pendulumPivotX").value);
|
342 |
const pivotY = parseFloat(document.getElementById("pendulumPivotY").value);
|
343 |
const bobRadius = parseFloat(document.getElementById("pendulumBobRadius").value);
|
344 |
-
const
|
345 |
const stiffness = parseFloat(document.getElementById("pendulumStiffness").value);
|
346 |
-
const
|
347 |
-
const bob = Bodies.circle(pivotX, pivotY+
|
348 |
-
|
349 |
-
bob.customOptions = { pivotX, pivotY, bobRadius, pendulumLength, stiffness, bobColor, isSystemRoot: false };
|
350 |
-
const pendulumConstraint = Constraint.create({
|
351 |
pointA: { x: pivotX, y: pivotY },
|
352 |
bodyB: bob,
|
353 |
-
length
|
354 |
-
stiffness
|
355 |
-
render: { strokeStyle: '#
|
356 |
});
|
357 |
-
|
|
|
|
|
358 |
}
|
359 |
-
|
|
|
360 |
const x = parseFloat(document.getElementById("projectileInitialX").value);
|
361 |
const y = parseFloat(document.getElementById("projectileInitialY").value);
|
362 |
const radius = parseFloat(document.getElementById("projectileRadius").value);
|
363 |
const velX = parseFloat(document.getElementById("projectileVelX").value);
|
364 |
const velY = parseFloat(document.getElementById("projectileVelY").value);
|
365 |
-
const color = document.getElementById("projectileColor").value
|
366 |
-
const proj = Bodies.circle(x, y, radius, { restitution: 0.8, frictionAir: 0.
|
367 |
-
proj.elementType = "projectile";
|
368 |
-
proj.customOptions = { x, y, radius, velX, velY, color, isSystemRoot: false };
|
369 |
Body.setVelocity(proj, { x: velX, y: velY });
|
|
|
|
|
370 |
World.add(world, proj);
|
371 |
}
|
372 |
-
|
|
|
373 |
const rampWidth = parseFloat(document.getElementById("rampWidth").value);
|
374 |
const rampHeight = parseFloat(document.getElementById("rampHeight").value);
|
375 |
const angleDeg = parseFloat(document.getElementById("rampAngle").value);
|
376 |
-
const angle = angleDeg * Math.PI/180;
|
377 |
const rampX = parseFloat(document.getElementById("rampX").value);
|
378 |
const rampY = parseFloat(document.getElementById("rampY").value);
|
379 |
-
const rampColor = document.getElementById("rampColor").value
|
380 |
const ramp = Bodies.rectangle(rampX, rampY, rampWidth, rampHeight, {
|
381 |
isStatic: true,
|
382 |
-
angle
|
|
|
383 |
render: { fillStyle: rampColor }
|
384 |
});
|
385 |
-
ramp.elementType = "inclinedPlane_ramp";
|
386 |
-
ramp.customOptions = { rampWidth, rampHeight, angleDeg, rampX, rampY, rampColor, isSystemRoot: false };
|
387 |
const blockWidth = parseFloat(document.getElementById("blockWidth").value);
|
388 |
const blockHeight = parseFloat(document.getElementById("blockHeight").value);
|
389 |
const blockFriction = parseFloat(document.getElementById("blockFriction").value);
|
390 |
-
const blockColor = document.getElementById("blockColor").value
|
391 |
-
const
|
|
|
|
|
392 |
friction: blockFriction,
|
393 |
-
render: { fillStyle: blockColor }
|
|
|
394 |
});
|
|
|
|
|
395 |
block.elementType = "inclinedPlane_block";
|
396 |
-
block.customOptions = { blockWidth, blockHeight, blockFriction, blockColor
|
397 |
World.add(world, [ramp, block]);
|
398 |
}
|
399 |
-
|
400 |
-
|
401 |
-
const
|
402 |
-
const
|
403 |
-
const
|
404 |
-
const
|
405 |
-
const
|
406 |
-
const
|
407 |
-
const
|
408 |
-
|
409 |
-
density: 0.004,
|
410 |
-
render: { fillStyle: massColor }
|
411 |
-
});
|
412 |
-
mass.elementType = "springMass";
|
413 |
-
mass.customOptions = { massRadius, massRestitution, fixedPointX, fixedPointY, springLength, springStiffness, massColor, isSystemRoot: false };
|
414 |
-
mass.fixedPoint = { x: fixedPointX, y: fixedPointY };
|
415 |
const spring = Constraint.create({
|
416 |
-
pointA: { x:
|
417 |
bodyB: mass,
|
418 |
-
length
|
419 |
-
stiffness
|
420 |
damping: 0.05,
|
421 |
-
render: { strokeStyle: '#
|
422 |
});
|
|
|
|
|
|
|
423 |
World.add(world, [mass, spring]);
|
424 |
}
|
425 |
-
|
426 |
-
|
427 |
-
const y = parseFloat(document.getElementById("blockInitY").value);
|
428 |
-
const width = parseFloat(document.getElementById("blockWidth").value);
|
429 |
-
const height = parseFloat(document.getElementById("blockHeight").value);
|
430 |
-
const friction = parseFloat(document.getElementById("blockFriction").value);
|
431 |
-
const restitution = parseFloat(document.getElementById("blockRestitution").value);
|
432 |
-
const color = document.getElementById("blockColor").value || getRandomColor();
|
433 |
-
const block = Bodies.rectangle(x, y, width, height, { friction, restitution, render: { fillStyle: color } });
|
434 |
-
block.elementType = "rectangleBlock";
|
435 |
-
block.customOptions = { x, y, width, height, friction, restitution, color, isSystemRoot: false };
|
436 |
-
World.add(world, block);
|
437 |
-
}
|
438 |
-
document.getElementById('addElement').addEventListener('click', function(){
|
439 |
const selected = document.getElementById('simulationSelect').value;
|
440 |
-
if(selected === 'bouncingBall') addBouncingBall();
|
441 |
-
else if(selected === 'pendulum') addPendulum();
|
442 |
-
else if(selected === 'projectile') addProjectile();
|
443 |
-
else if(selected === 'inclinedPlane') addInclinedPlane();
|
444 |
-
else if(selected === 'springMass') addSpringMass();
|
445 |
-
else if(selected === 'rectangleBlock') addRectangleBlock();
|
446 |
-
});
|
447 |
-
// "Connect Systems" button (sets attach mode in SYSTEM mode)
|
448 |
-
document.getElementById('connectSystems').addEventListener('click', function(){
|
449 |
-
attachMode = true;
|
450 |
-
attachFrom = null; // will be set on first click
|
451 |
-
connectionPanel.style.display = "block";
|
452 |
-
// Set connection mode to system by default
|
453 |
-
document.getElementById('connMode').value = "system";
|
454 |
-
alert("System connection mode activated. Click on the first system element (must be marked as system root) then the target system element.");
|
455 |
});
|
456 |
-
|
|
|
457 |
World.clear(world);
|
458 |
Engine.clear(engine);
|
459 |
updateBoundaries();
|
460 |
-
|
461 |
-
|
462 |
-
World.add(world,
|
463 |
-
render.mouse =
|
464 |
hideEditPanel();
|
465 |
hideConnectionPanel();
|
466 |
hideConstraintEditPanel();
|
|
|
467 |
});
|
468 |
|
469 |
-
/* ---------- Edit Panel (opens on double-click) ---------- */
|
470 |
function openEditPanel(body) {
|
471 |
selectedBody = body;
|
|
|
472 |
let html = `<h3>Edit Element</h3>
|
473 |
-
<label for="editPosX">Position X:</label>
|
474 |
-
<input type="number" id="
|
475 |
-
<label for="
|
476 |
-
<input type="
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
<label for="editBallFriction">Friction:</label>
|
490 |
-
<input type="number" id="editBallFriction" value="${opts.friction}" step="0.001" min="0" max="1">`;
|
491 |
}
|
492 |
-
|
493 |
-
html += `<div style="margin-top:10px;">
|
494 |
<button id="updateElement">Update</button>
|
495 |
<button id="deleteElement">Delete</button>
|
|
|
496 |
</div>`;
|
497 |
editPanel.innerHTML = html;
|
498 |
editPanel.style.display = "block";
|
|
|
499 |
document.getElementById("updateElement").addEventListener("click", updateElementFromEditPanel);
|
500 |
-
document.getElementById("deleteElement").addEventListener("click",
|
501 |
-
|
502 |
-
hideEditPanel();
|
503 |
-
});
|
504 |
-
}
|
505 |
-
function hideEditPanel() {
|
506 |
-
editPanel.style.display = "none";
|
507 |
-
selectedBody = null;
|
508 |
}
|
509 |
-
|
510 |
-
|
|
|
|
|
|
|
|
|
|
|
511 |
const newX = parseFloat(document.getElementById("editPosX").value);
|
512 |
const newY = parseFloat(document.getElementById("editPosY").value);
|
513 |
const newAngle = parseFloat(document.getElementById("editAngle").value);
|
@@ -515,193 +457,131 @@
|
|
515 |
Body.setPosition(selectedBody, { x: newX, y: newY });
|
516 |
Body.setAngle(selectedBody, newAngle);
|
517 |
selectedBody.render.fillStyle = newColor;
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
if(newRadius && oldRadius && newRadius !== oldRadius){
|
527 |
-
const scaleFactor = newRadius / oldRadius;
|
528 |
-
Body.scale(selectedBody, scaleFactor, scaleFactor);
|
529 |
-
}
|
530 |
-
selectedBody.restitution = newRestitution;
|
531 |
-
selectedBody.friction = newFriction;
|
532 |
-
selectedBody.customOptions = { x: newX, y: newY, radius: newRadius, restitution: newRestitution, friction: newFriction, color: newColor, isSystemRoot: isSysRoot };
|
533 |
}
|
534 |
hideEditPanel();
|
535 |
}
|
536 |
|
537 |
-
|
538 |
-
render.canvas.addEventListener("dblclick", function(event){
|
539 |
const rect = render.canvas.getBoundingClientRect();
|
540 |
const mousePos = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
541 |
const bodies = Matter.Composite.allBodies(world);
|
542 |
const clicked = Matter.Query.point(bodies, mousePos);
|
543 |
-
if(clicked.length > 0)
|
544 |
-
|
545 |
-
} else {
|
546 |
-
hideEditPanel();
|
547 |
-
}
|
548 |
});
|
549 |
|
550 |
-
|
551 |
-
|
552 |
-
Events.on(mouseConstraint, 'mouseup',
|
553 |
const mousePos = event.mouse.position;
|
554 |
const bodies = Matter.Composite.allBodies(world);
|
555 |
const clickedBodies = Matter.Query.point(bodies, mousePos);
|
556 |
-
if(clickedBodies.length > 0){
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
}
|
587 |
-
// Use the system root's position (we assume the element itself is the root)
|
588 |
-
}
|
589 |
-
// For springMass endpoints, get endpoint options if needed
|
590 |
-
let sourceEndpoint = "mass";
|
591 |
-
if(attachFrom.elementType === "springMass"){
|
592 |
-
sourceEndpoint = document.getElementById('sourceEndpoint').value;
|
593 |
-
}
|
594 |
-
let targetEndpoint = "mass";
|
595 |
-
if(clickedBody.elementType === "springMass"){
|
596 |
-
targetEndpoint = prompt("For target springMass, choose 'fixed' or 'mass' (default: mass):", "mass") || "mass";
|
597 |
-
if(targetEndpoint !== "fixed") { targetEndpoint = "mass"; }
|
598 |
-
}
|
599 |
-
// Determine attachment points
|
600 |
-
let pointA = { x: 0, y: 0 }, bodyA = attachFrom;
|
601 |
-
if(attachFrom.elementType === "springMass" && sourceEndpoint === "fixed"){
|
602 |
-
bodyA = null;
|
603 |
-
pointA = attachFrom.fixedPoint;
|
604 |
-
}
|
605 |
-
let pointB = { x: 0, y: 0 }, bodyB = clickedBody;
|
606 |
-
if(clickedBody.elementType === "springMass" && targetEndpoint === "fixed"){
|
607 |
-
bodyB = null;
|
608 |
-
pointB = clickedBody.fixedPoint || { x: clickedBody.position.x, y: clickedBody.position.y };
|
609 |
-
}
|
610 |
-
// If connection mode is "system", override to use the elements as-is
|
611 |
-
if(connMode === "system") {
|
612 |
-
bodyA = attachFrom;
|
613 |
-
bodyB = clickedBody;
|
614 |
-
pointA = { x: 0, y: 0 };
|
615 |
-
pointB = { x: 0, y: 0 };
|
616 |
-
}
|
617 |
-
let posA = bodyA ? attachFrom.position : pointA;
|
618 |
-
let posB = bodyB ? clickedBody.position : pointB;
|
619 |
-
let dx = posB.x - posA.x, dy = posB.y - posA.y;
|
620 |
-
let length = Math.sqrt(dx*dx + dy*dy);
|
621 |
-
let stiffness = (connType === "spring") ? 0.05 : 1;
|
622 |
-
const newConstraint = Constraint.create({
|
623 |
-
bodyA: bodyA,
|
624 |
-
pointA: pointA,
|
625 |
-
bodyB: bodyB,
|
626 |
-
pointB: pointB,
|
627 |
-
length: length,
|
628 |
-
stiffness: stiffness,
|
629 |
-
render: { strokeStyle: '#000', lineWidth: 2 }
|
630 |
-
});
|
631 |
-
World.add(world, newConstraint);
|
632 |
-
attachMode = false;
|
633 |
-
attachFrom = null;
|
634 |
-
connectionPanel.style.display = "none";
|
635 |
-
alert("Attachment created using " + connType + " connection in " + connMode + " mode.");
|
636 |
-
}
|
637 |
-
} else {
|
638 |
-
// Check if near a constraint line for editing…
|
639 |
-
const constraints = Matter.Composite.allConstraints(world);
|
640 |
-
for(let cons of constraints){
|
641 |
-
let posA = cons.bodyA ? { x: cons.bodyA.position.x + cons.pointA.x, y: cons.bodyA.position.y + cons.pointA.y } : cons.pointA;
|
642 |
-
let posB = cons.bodyB ? { x: cons.bodyB.position.x + cons.pointB.x, y: cons.bodyB.position.y + cons.pointB.y } : cons.pointB;
|
643 |
-
let dist = distanceToSegment(mousePos, posA, posB);
|
644 |
-
if(dist < 5){
|
645 |
-
showConstraintEditPanel(cons);
|
646 |
-
return;
|
647 |
-
}
|
648 |
-
}
|
649 |
-
hideConstraintEditPanel();
|
650 |
}
|
651 |
});
|
652 |
-
|
653 |
-
function distanceToSegment(p, v, w){
|
654 |
-
|
655 |
-
if(l2 === 0) return Math.hypot(p.x - v.x, p.y - v.y);
|
656 |
-
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
|
657 |
-
|
658 |
-
let proj = { x: v.x + t*(w.x-v.x), y: v.y + t*(w.y-v.y) };
|
659 |
return Math.hypot(p.x - proj.x, p.y - proj.y);
|
660 |
}
|
661 |
|
662 |
-
|
663 |
-
const constraintEditPanel = document.getElementById('constraintEditPanel');
|
664 |
-
function showConstraintEditPanel(cons){
|
665 |
selectedConstraint = cons;
|
666 |
-
let posA = cons.bodyA ?
|
667 |
-
let posB = cons.bodyB ?
|
668 |
-
document.getElementById('consA_X').value = posA.x.toFixed(
|
669 |
-
document.getElementById('consA_Y').value = posA.y.toFixed(
|
670 |
-
document.getElementById('consB_X').value = posB.x.toFixed(
|
671 |
-
document.getElementById('consB_Y').value = posB.y.toFixed(
|
672 |
-
|
673 |
-
document.getElementById('consType').value = connType;
|
674 |
constraintEditPanel.style.display = "block";
|
675 |
}
|
676 |
-
|
677 |
-
|
678 |
-
selectedConstraint
|
679 |
-
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
let connType = document.getElementById('consType').value;
|
689 |
-
selectedConstraint.stiffness = (connType === "spring") ? 0.05 : 1;
|
690 |
hideConstraintEditPanel();
|
691 |
}
|
692 |
});
|
693 |
-
|
694 |
-
|
|
|
695 |
World.remove(world, selectedConstraint);
|
696 |
hideConstraintEditPanel();
|
697 |
}
|
698 |
});
|
699 |
|
700 |
-
|
701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
702 |
Render.lookAt(render, { min: { x: 0, y: 0 }, max: { x: window.innerWidth, y: window.innerHeight } });
|
703 |
updateBoundaries();
|
704 |
});
|
705 |
</script>
|
706 |
</body>
|
707 |
-
</html>
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
+
<title>Physics Simulator for IIT Aspirants</title>
|
6 |
<style>
|
7 |
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
|
8 |
+
canvas { display: block; width: 100%; height: 100vh; }
|
9 |
+
|
10 |
+
/* Top Navbar for Controls */
|
11 |
+
#navbar {
|
12 |
+
position: fixed;
|
13 |
+
top: 0;
|
14 |
+
left: 0;
|
15 |
+
width: 100%;
|
16 |
+
background: rgba(255, 255, 255, 0.95);
|
17 |
+
padding: 10px 20px;
|
18 |
z-index: 1000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
display: flex;
|
20 |
justify-content: space-between;
|
21 |
+
align-items: center;
|
22 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
23 |
}
|
24 |
+
#controls {
|
25 |
+
display: flex;
|
26 |
+
align-items: center;
|
27 |
+
gap: 15px;
|
28 |
+
flex-wrap: wrap;
|
29 |
+
max-width: 70%;
|
|
|
30 |
}
|
31 |
+
#controls label { margin: 0 5px 0 0; font-size: 14px; }
|
32 |
+
#controls select, #controls button {
|
33 |
+
padding: 6px;
|
34 |
+
font-size: 14px;
|
35 |
+
border-radius: 4px;
|
36 |
+
border: 1px solid #ccc;
|
37 |
}
|
38 |
+
#controls select { min-width: 150px; }
|
39 |
+
#buttonContainer { display: flex; gap: 10px; }
|
40 |
+
|
41 |
+
/* Suboptions Sidebar (Left) */
|
42 |
+
#subOptionsSidebar {
|
43 |
+
position: fixed;
|
44 |
+
top: 60px;
|
45 |
+
left: 0;
|
46 |
+
width: 250px;
|
47 |
+
height: calc(100vh - 60px);
|
48 |
+
background: rgba(245, 245, 245, 0.95);
|
49 |
+
padding: 15px;
|
50 |
+
z-index: 900;
|
51 |
+
overflow-y: auto;
|
52 |
+
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
|
53 |
+
transition: transform 0.3s ease;
|
54 |
}
|
55 |
+
#subOptionsSidebar.hidden { transform: translateX(-100%); }
|
56 |
+
#subOptionsContainer label { display: block; margin: 10px 0 5px; font-size: 14px; }
|
57 |
+
#subOptionsContainer input, #subOptionsContainer select {
|
58 |
+
width: 100%; padding: 6px; font-size: 14px; border-radius: 4px; border: 1px solid #ccc; }
|
59 |
+
|
60 |
+
/* Right Sidebar for Edit and Connection Panels */
|
61 |
+
#rightSidebar {
|
62 |
+
position: fixed;
|
63 |
+
top: 60px;
|
64 |
+
right: 0;
|
65 |
+
width: 300px;
|
66 |
+
height: calc(100vh - 60px);
|
67 |
+
background: rgba(245, 245, 245, 0.95);
|
68 |
+
padding: 15px;
|
69 |
+
z-index: 900;
|
70 |
+
overflow-y: auto;
|
71 |
+
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
|
72 |
+
transition: transform 0.3s ease;
|
73 |
}
|
74 |
+
#rightSidebar.hidden { transform: translateX(100%); }
|
75 |
+
#editPanel, #connectionPanel { margin-bottom: 20px; }
|
76 |
+
#editPanel h3, #connectionPanel h3 { margin: 0 0 10px; font-size: 16px; }
|
77 |
+
#editPanel label, #connectionPanel label { display: block; margin: 10px 0 5px; font-size: 14px; }
|
78 |
+
#editPanel input, #editPanel select, #editPanel button,
|
79 |
+
#connectionPanel input, #connectionPanel select, #connectionPanel button {
|
80 |
+
width: 100%; padding: 6px; font-size: 14px; border-radius: 4px; border: 1px solid #ccc; }
|
81 |
+
#editPanel button, #connectionPanel button { margin-top: 10px; }
|
82 |
+
|
83 |
+
/* Bottom Bar for Real-Time Data and Constraint Edit */
|
84 |
+
#bottomBar {
|
85 |
+
position: fixed;
|
86 |
+
bottom: 0;
|
87 |
+
left: 0;
|
88 |
+
width: 100%;
|
89 |
+
background: rgba(255, 255, 255, 0.95);
|
90 |
+
padding: 10px 20px;
|
91 |
+
z-index: 1000;
|
92 |
+
display: flex;
|
93 |
+
justify-content: space-between;
|
94 |
+
align-items: center;
|
95 |
+
box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
|
96 |
}
|
97 |
+
#dataDisplay { flex: 1; font-size: 14px; }
|
98 |
+
#dataDisplay h3 { margin: 0 0 5px; font-size: 16px; }
|
99 |
+
#constraintEditPanel { flex: 0 0 300px; display: none; }
|
100 |
+
#constraintEditPanel h3 { margin: 0 0 10px; font-size: 16px; }
|
101 |
+
#constraintEditPanel label { display: block; margin: 10px 0 5px; font-size: 14px; }
|
102 |
#constraintEditPanel input, #constraintEditPanel select, #constraintEditPanel button {
|
103 |
+
width: 100%; padding: 6px; font-size: 14px; border-radius: 4px; border: 1px solid #ccc; }
|
104 |
+
#constraintEditPanel button { margin-top: 10px; }
|
105 |
</style>
|
106 |
</head>
|
107 |
<body>
|
108 |
+
<!-- Top Navbar -->
|
109 |
+
<div id="navbar">
|
110 |
+
<div id="controls">
|
111 |
+
<label for="simulationSelect">Select Simulation:</label>
|
112 |
+
<select id="simulationSelect">
|
113 |
+
<option value="bouncingBall">Bouncing Ball</option>
|
114 |
+
<option value="pendulum">Pendulum</option>
|
115 |
+
<option value="projectile">Projectile Motion</option>
|
116 |
+
<option value="inclinedPlane">Inclined Plane</option>
|
117 |
+
<option value="springMass">Spring-Mass System</option>
|
118 |
+
</select>
|
119 |
+
<div id="buttonContainer">
|
120 |
+
<button id="addElement">Add Element</button>
|
121 |
+
<button id="reset">Reset</button>
|
122 |
+
</div>
|
|
|
|
|
123 |
</div>
|
124 |
</div>
|
125 |
|
126 |
+
<!-- Left Sidebar for Suboptions -->
|
127 |
+
<div id="subOptionsSidebar">
|
128 |
+
<div id="subOptionsContainer"></div>
|
129 |
+
</div>
|
130 |
|
131 |
+
<!-- Right Sidebar for Edit and Connection Panels -->
|
132 |
+
<div id="rightSidebar" class="hidden">
|
133 |
+
<div id="editPanel"></div>
|
134 |
+
<div id="connectionPanel">
|
135 |
+
<h3>Connect Elements</h3>
|
136 |
+
<label for="connType">Connection Type:</label>
|
137 |
+
<select id="connType">
|
138 |
+
<option value="string">String (Inextensible)</option>
|
139 |
+
<option value="spring">Spring (Elastic)</option>
|
140 |
+
<option value="stick">Rigid Stick</option>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
</select>
|
142 |
+
<div id="sourceEndpointDiv">
|
143 |
+
<label for="sourceEndpoint">Source Endpoint:</label>
|
144 |
+
<select id="sourceEndpoint">
|
145 |
+
<option value="mass">Mass</option>
|
146 |
+
<option value="fixed">Fixed Point</option>
|
147 |
+
</select>
|
148 |
+
</div>
|
149 |
+
<button id="cancelConn">Cancel</button>
|
150 |
+
<p style="font-size: 12px; color: #555;">Click the target element to connect.</p>
|
151 |
</div>
|
|
|
|
|
152 |
</div>
|
153 |
|
154 |
+
<!-- Bottom Bar for Data and Constraints -->
|
155 |
+
<div id="bottomBar">
|
156 |
+
<div id="dataDisplay">
|
157 |
+
<h3>Real-Time Data</h3>
|
158 |
+
<p id="dataContent">Select an element to view data.</p>
|
159 |
+
</div>
|
160 |
+
<div id="constraintEditPanel">
|
161 |
+
<h3>Edit Constraint</h3>
|
162 |
+
<label for="consA_X">Endpoint A X:</label>
|
163 |
+
<input type="number" id="consA_X" step="0.1">
|
164 |
+
<label for="consA_Y">Endpoint A Y:</label>
|
165 |
+
<input type="number" id="consA_Y" step="0.1">
|
166 |
+
<label for="consB_X">Endpoint B X:</label>
|
167 |
+
<input type="number" id="consB_X" step="0.1">
|
168 |
+
<label for="consB_Y">Endpoint B Y:</label>
|
169 |
+
<input type="number" id="consB_Y" step="0.1">
|
170 |
+
<label for="consType">Connection Type:</label>
|
171 |
+
<select id="consType">
|
172 |
+
<option value="string">String</option>
|
173 |
+
<option value="spring">Spring</option>
|
174 |
+
<option value="stick">Stick</option>
|
175 |
+
</select>
|
176 |
+
<button id="updateConstraint">Update</button>
|
177 |
+
<button id="deleteConstraint">Delete</button>
|
178 |
+
</div>
|
179 |
</div>
|
180 |
|
|
|
181 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
|
182 |
<script>
|
183 |
+
const { Engine, Render, Runner, World, Bodies, Body, Constraint, Mouse, MouseConstraint, Events, Vector } = Matter;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
const engine = Engine.create();
|
185 |
const world = engine.world;
|
186 |
+
engine.gravity.scale = 0.001;
|
187 |
const render = Render.create({
|
188 |
element: document.body,
|
189 |
engine: engine,
|
|
|
191 |
width: window.innerWidth,
|
192 |
height: window.innerHeight,
|
193 |
wireframes: false,
|
194 |
+
background: '#e9ecef'
|
195 |
}
|
196 |
});
|
197 |
Render.run(render);
|
198 |
const runner = Runner.create();
|
199 |
Runner.run(runner, engine);
|
200 |
|
|
|
201 |
let floor, wallLeft, wallRight;
|
202 |
function updateBoundaries() {
|
203 |
+
if (floor && wallLeft && wallRight) World.remove(world, [floor, wallLeft, wallRight]);
|
204 |
+
floor = Bodies.rectangle(window.innerWidth / 2, window.innerHeight + 50, window.innerWidth * 2, 100, { isStatic: true, render: { fillStyle: '#343a40' } });
|
205 |
+
wallLeft = Bodies.rectangle(-50, window.innerHeight / 2, 100, window.innerHeight * 2, { isStatic: true, render: { fillStyle: '#343a40' } });
|
206 |
+
wallRight = Bodies.rectangle(window.innerWidth + 50, window.innerHeight / 2, 100, window.innerHeight * 2, { isStatic: true, render: { fillStyle: '#343a40' } });
|
|
|
|
|
|
|
|
|
|
|
207 |
World.add(world, [floor, wallLeft, wallRight]);
|
208 |
}
|
209 |
updateBoundaries();
|
210 |
|
211 |
+
const mouse = Mouse.create(render.canvas);
|
212 |
+
const mouseConstraint = MouseConstraint.create(engine, { mouse: mouse, constraint: { stiffness: 0.2, render: { visible: false } } });
|
|
|
|
|
|
|
|
|
213 |
World.add(world, mouseConstraint);
|
214 |
render.mouse = mouse;
|
215 |
|
216 |
+
let selectedBody = null, selectedConstraint = null, attachMode = false, attachFrom = null;
|
217 |
+
const subOptionsSidebar = document.getElementById('subOptionsSidebar'),
|
218 |
+
rightSidebar = document.getElementById('rightSidebar'),
|
219 |
+
editPanel = document.getElementById('editPanel'),
|
220 |
+
connectionPanel = document.getElementById('connectionPanel'),
|
221 |
+
constraintEditPanel = document.getElementById('constraintEditPanel'),
|
222 |
+
dataContent = document.getElementById('dataContent');
|
223 |
|
224 |
+
function getRandomColor() { return '#' + Math.floor(Math.random() * 16777215).toString(16); }
|
|
|
|
|
|
|
225 |
|
|
|
226 |
function updateSubOptions() {
|
227 |
const simulationSelect = document.getElementById('simulationSelect');
|
228 |
const subOptionsContainer = document.getElementById('subOptionsContainer');
|
229 |
+
const selected = simulationSelect.value;
|
230 |
let html = '';
|
231 |
+
if (selected === 'bouncingBall') {
|
232 |
+
html = `
|
233 |
+
<label for="ballInitialX">Initial X (m):</label><input type="number" id="ballInitialX" value="200">
|
234 |
+
<label for="ballInitialY">Initial Y (m):</label><input type="number" id="ballInitialY" value="50">
|
235 |
+
<label for="ballRadius">Radius (m):</label><input type="number" id="ballRadius" value="20" min="5">
|
236 |
+
<label for="ballRestitution">Restitution:</label><input type="number" id="ballRestitution" value="0.9" step="0.1" min="0" max="1">
|
237 |
+
<label for="ballFriction">Friction:</label><input type="number" id="ballFriction" value="0.01" step="0.01" min="0" max="1">
|
238 |
+
<label for="ballColor">Color:</label><input type="color" id="ballColor" value="#3498db">
|
239 |
+
`;
|
240 |
+
} else if (selected === 'pendulum') {
|
241 |
+
html = `
|
242 |
+
<label for="pendulumPivotX">Pivot X (m):</label><input type="number" id="pendulumPivotX" value="${Math.floor(window.innerWidth / 2)}">
|
243 |
+
<label for="pendulumPivotY">Pivot Y (m):</label><input type="number" id="pendulumPivotY" value="50">
|
244 |
+
<label for="pendulumBobRadius">Bob Radius (m):</label><input type="number" id="pendulumBobRadius" value="30" min="5">
|
245 |
+
<label for="pendulumLength">Length (m):</label><input type="number" id="pendulumLength" value="300" min="50">
|
246 |
+
<label for="pendulumStiffness">Stiffness:</label><input type="number" id="pendulumStiffness" value="1" step="0.1" min="0" max="1">
|
247 |
+
<label for="pendulumBobColor">Color:</label><input type="color" id="pendulumBobColor" value="#e74c3c">
|
248 |
+
`;
|
249 |
+
} else if (selected === 'projectile') {
|
250 |
+
html = `
|
251 |
+
<label for="projectileInitialX">Initial X (m):</label><input type="number" id="projectileInitialX" value="100">
|
252 |
+
<label for="projectileInitialY">Initial Y (m):</label><input type="number" id="projectileInitialY" value="${window.innerHeight - 150}">
|
253 |
+
<label for="projectileRadius">Radius (m):</label><input type="number" id="projectileRadius" value="15" min="5">
|
254 |
+
<label for="projectileVelX">Velocity X (m/s):</label><input type="number" id="projectileVelX" value="20">
|
255 |
+
<label for="projectileVelY">Velocity Y (m/s):</label><input type="number" id="projectileVelY" value="-25">
|
256 |
+
<label for="projectileColor">Color:</label><input type="color" id="projectileColor" value="#f1c40f">
|
257 |
+
`;
|
258 |
+
} else if (selected === 'inclinedPlane') {
|
259 |
+
html = `
|
260 |
+
<label for="rampWidth">Ramp Width (m):</label><input type="number" id="rampWidth" value="400" min="100">
|
261 |
+
<label for="rampHeight">Ramp Height (m):</label><input type="number" id="rampHeight" value="20" min="10">
|
262 |
+
<label for="rampAngle">Angle (°):</label><input type="number" id="rampAngle" value="30" min="0" max="90">
|
263 |
+
<label for="rampX">Ramp X (m):</label><input type="number" id="rampX" value="400">
|
264 |
+
<label for="rampY">Ramp Y (m):</label><input type="number" id="rampY" value="${window.innerHeight - 100}">
|
265 |
+
<label for="rampColor">Ramp Color:</label><input type="color" id="rampColor" value="#8e44ad">
|
266 |
+
<label for="blockWidth">Block Width (m):</label><input type="number" id="blockWidth" value="50" min="10">
|
267 |
+
<label for="blockHeight">Block Height (m):</label><input type="number" id="blockHeight" value="50" min="10">
|
268 |
+
<label for="blockFriction">Block Friction:</label><input type="number" id="blockFriction" value="0.3" step="0.1" min="0" max="1">
|
269 |
+
<label for="blockColor">Block Color:</label><input type="color" id="blockColor" value="#2ecc71">
|
270 |
+
`;
|
271 |
+
} else if (selected === 'springMass') {
|
272 |
+
html = `
|
273 |
+
<label for="springMassRadius">Mass Radius (m):</label><input type="number" id="springMassRadius" value="25" min="5">
|
274 |
+
<label for="springMassRestitution">Restitution:</label><input type="number" id="springMassRestitution" value="0.5" step="0.1" min="0" max="1">
|
275 |
+
<label for="fixedPointX">Fixed X (m):</label><input type="number" id="fixedPointX" value="${Math.floor(window.innerWidth / 2)}">
|
276 |
+
<label for="fixedPointY">Fixed Y (m):</label><input type="number" id="fixedPointY" value="100">
|
277 |
+
<label for="springLength">Spring Length (m):</label><input type="number" id="springLength" value="200" min="50">
|
278 |
+
<label for="springStiffness">Stiffness:</label><input type="number" id="springStiffness" value="0.05" step="0.01" min="0" max="1">
|
279 |
+
<label for="springMassColor">Color:</label><input type="color" id="springMassColor" value="#e67e22">
|
280 |
+
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
}
|
282 |
subOptionsContainer.innerHTML = html;
|
283 |
+
subOptionsSidebar.classList.remove('hidden');
|
284 |
}
|
285 |
updateSubOptions();
|
286 |
document.getElementById('simulationSelect').addEventListener('change', updateSubOptions);
|
287 |
|
288 |
+
function addBouncingBall() {
|
|
|
289 |
const x = parseFloat(document.getElementById("ballInitialX").value);
|
290 |
const y = parseFloat(document.getElementById("ballInitialY").value);
|
291 |
const radius = parseFloat(document.getElementById("ballRadius").value);
|
292 |
const restitution = parseFloat(document.getElementById("ballRestitution").value);
|
293 |
const friction = parseFloat(document.getElementById("ballFriction").value);
|
294 |
+
const color = document.getElementById("ballColor").value;
|
295 |
+
const ball = Bodies.circle(x, y, radius, { restitution, friction, render: { fillStyle: color }, density: 0.001 });
|
296 |
ball.elementType = "bouncingBall";
|
297 |
+
ball.customOptions = { x, y, radius, restitution, friction, color };
|
298 |
World.add(world, ball);
|
299 |
}
|
300 |
+
|
301 |
+
function addPendulum() {
|
302 |
const pivotX = parseFloat(document.getElementById("pendulumPivotX").value);
|
303 |
const pivotY = parseFloat(document.getElementById("pendulumPivotY").value);
|
304 |
const bobRadius = parseFloat(document.getElementById("pendulumBobRadius").value);
|
305 |
+
const length = parseFloat(document.getElementById("pendulumLength").value);
|
306 |
const stiffness = parseFloat(document.getElementById("pendulumStiffness").value);
|
307 |
+
const color = document.getElementById("pendulumBobColor").value;
|
308 |
+
const bob = Bodies.circle(pivotX, pivotY + length, bobRadius, { restitution: 0.9, density: 0.001, render: { fillStyle: color } });
|
309 |
+
const constraint = Constraint.create({
|
|
|
|
|
310 |
pointA: { x: pivotX, y: pivotY },
|
311 |
bodyB: bob,
|
312 |
+
length,
|
313 |
+
stiffness,
|
314 |
+
render: { strokeStyle: '#333', lineWidth: 2 }
|
315 |
});
|
316 |
+
bob.elementType = "pendulum";
|
317 |
+
bob.customOptions = { pivotX, pivotY, bobRadius, length, stiffness, color };
|
318 |
+
World.add(world, [bob, constraint]);
|
319 |
}
|
320 |
+
|
321 |
+
function addProjectile() {
|
322 |
const x = parseFloat(document.getElementById("projectileInitialX").value);
|
323 |
const y = parseFloat(document.getElementById("projectileInitialY").value);
|
324 |
const radius = parseFloat(document.getElementById("projectileRadius").value);
|
325 |
const velX = parseFloat(document.getElementById("projectileVelX").value);
|
326 |
const velY = parseFloat(document.getElementById("projectileVelY").value);
|
327 |
+
const color = document.getElementById("projectileColor").value;
|
328 |
+
const proj = Bodies.circle(x, y, radius, { restitution: 0.8, frictionAir: 0.005, render: { fillStyle: color }, density: 0.001 });
|
|
|
|
|
329 |
Body.setVelocity(proj, { x: velX, y: velY });
|
330 |
+
proj.elementType = "projectile";
|
331 |
+
proj.customOptions = { x, y, radius, velX, velY, color };
|
332 |
World.add(world, proj);
|
333 |
}
|
334 |
+
|
335 |
+
function addInclinedPlane() {
|
336 |
const rampWidth = parseFloat(document.getElementById("rampWidth").value);
|
337 |
const rampHeight = parseFloat(document.getElementById("rampHeight").value);
|
338 |
const angleDeg = parseFloat(document.getElementById("rampAngle").value);
|
339 |
+
const angle = angleDeg * Math.PI / 180;
|
340 |
const rampX = parseFloat(document.getElementById("rampX").value);
|
341 |
const rampY = parseFloat(document.getElementById("rampY").value);
|
342 |
+
const rampColor = document.getElementById("rampColor").value;
|
343 |
const ramp = Bodies.rectangle(rampX, rampY, rampWidth, rampHeight, {
|
344 |
isStatic: true,
|
345 |
+
angle,
|
346 |
+
friction: 0.1,
|
347 |
render: { fillStyle: rampColor }
|
348 |
});
|
|
|
|
|
349 |
const blockWidth = parseFloat(document.getElementById("blockWidth").value);
|
350 |
const blockHeight = parseFloat(document.getElementById("blockHeight").value);
|
351 |
const blockFriction = parseFloat(document.getElementById("blockFriction").value);
|
352 |
+
const blockColor = document.getElementById("blockColor").value;
|
353 |
+
const blockX = rampX - rampWidth / 4 * Math.cos(angle);
|
354 |
+
const blockY = rampY - rampHeight / 2 - blockHeight / 2 - 10;
|
355 |
+
const block = Bodies.rectangle(blockX, blockY, blockWidth, blockHeight, {
|
356 |
friction: blockFriction,
|
357 |
+
render: { fillStyle: blockColor },
|
358 |
+
density: 0.001
|
359 |
});
|
360 |
+
ramp.elementType = "inclinedPlane_ramp";
|
361 |
+
ramp.customOptions = { rampWidth, rampHeight, angleDeg, rampX, rampY, rampColor };
|
362 |
block.elementType = "inclinedPlane_block";
|
363 |
+
block.customOptions = { blockWidth, blockHeight, blockFriction, blockColor };
|
364 |
World.add(world, [ramp, block]);
|
365 |
}
|
366 |
+
|
367 |
+
function addSpringMass() {
|
368 |
+
const radius = parseFloat(document.getElementById("springMassRadius").value);
|
369 |
+
const restitution = parseFloat(document.getElementById("springMassRestitution").value);
|
370 |
+
const fixedX = parseFloat(document.getElementById("fixedPointX").value);
|
371 |
+
const fixedY = parseFloat(document.getElementById("fixedPointY").value);
|
372 |
+
const length = parseFloat(document.getElementById("springLength").value);
|
373 |
+
const stiffness = parseFloat(document.getElementById("springStiffness").value);
|
374 |
+
const color = document.getElementById("springMassColor").value;
|
375 |
+
const mass = Bodies.circle(fixedX, fixedY + length, radius, { restitution, density: 0.001, render: { fillStyle: color } });
|
|
|
|
|
|
|
|
|
|
|
|
|
376 |
const spring = Constraint.create({
|
377 |
+
pointA: { x: fixedX, y: fixedY },
|
378 |
bodyB: mass,
|
379 |
+
length,
|
380 |
+
stiffness,
|
381 |
damping: 0.05,
|
382 |
+
render: { strokeStyle: '#333', lineWidth: 2 }
|
383 |
});
|
384 |
+
mass.elementType = "springMass";
|
385 |
+
mass.customOptions = { radius, restitution, fixedX, fixedY, length, stiffness, color };
|
386 |
+
mass.fixedPoint = { x: fixedX, y: fixedY };
|
387 |
World.add(world, [mass, spring]);
|
388 |
}
|
389 |
+
|
390 |
+
document.getElementById('addElement').addEventListener('click', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
391 |
const selected = document.getElementById('simulationSelect').value;
|
392 |
+
if (selected === 'bouncingBall') addBouncingBall();
|
393 |
+
else if (selected === 'pendulum') addPendulum();
|
394 |
+
else if (selected === 'projectile') addProjectile();
|
395 |
+
else if (selected === 'inclinedPlane') addInclinedPlane();
|
396 |
+
else if (selected === 'springMass') addSpringMass();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
});
|
398 |
+
|
399 |
+
document.getElementById('reset').addEventListener('click', () => {
|
400 |
World.clear(world);
|
401 |
Engine.clear(engine);
|
402 |
updateBoundaries();
|
403 |
+
const newMouse = Mouse.create(render.canvas);
|
404 |
+
const newMouseConstraint = MouseConstraint.create(engine, { mouse: newMouse, constraint: { stiffness: 0.2, render: { visible: false } } });
|
405 |
+
World.add(world, newMouseConstraint);
|
406 |
+
render.mouse = newMouse;
|
407 |
hideEditPanel();
|
408 |
hideConnectionPanel();
|
409 |
hideConstraintEditPanel();
|
410 |
+
dataContent.textContent = "Select an element to view data.";
|
411 |
});
|
412 |
|
|
|
413 |
function openEditPanel(body) {
|
414 |
selectedBody = body;
|
415 |
+
rightSidebar.classList.remove('hidden');
|
416 |
let html = `<h3>Edit Element</h3>
|
417 |
+
<label for="editPosX">Position X (m):</label><input type="number" id="editPosX" value="${body.position.x.toFixed(1)}" step="0.1">
|
418 |
+
<label for="editPosY">Position Y (m):</label><input type="number" id="editPosY" value="${body.position.y.toFixed(1)}" step="0.1">
|
419 |
+
<label for="editAngle">Angle (rad):</label><input type="number" id="editAngle" value="${body.angle.toFixed(2)}" step="0.01">
|
420 |
+
<label for="editColor">Color:</label><input type="color" id="editColor" value="${body.render.fillStyle}">`;
|
421 |
+
if (body.elementType === "bouncingBall") {
|
422 |
+
const opts = body.customOptions;
|
423 |
+
html += `<label for="editBallRadius">Radius (m):</label><input type="number" id="editBallRadius" value="${opts.radius}" min="5">
|
424 |
+
<label for="editBallRestitution">Restitution:</label><input type="number" id="editBallRestitution" value="${opts.restitution}" step="0.1" min="0" max="1">
|
425 |
+
<label for="editBallFriction">Friction:</label><input type="number" id="editBallFriction" value="${opts.friction}" step="0.01" min="0" max="1">`;
|
426 |
+
} else if (body.elementType === "pendulum") {
|
427 |
+
const opts = body.customOptions;
|
428 |
+
html += `<label for="editPendulumPivotX">Pivot X (m):</label><input type="number" id="editPendulumPivotX" value="${opts.pivotX}">
|
429 |
+
<label for="editPendulumPivotY">Pivot Y (m):</label><input type="number" id="editPendulumPivotY" value="${opts.pivotY}">
|
430 |
+
<label for="editPendulumBobRadius">Radius (m):</label><input type="number" id="editPendulumBobRadius" value="${opts.bobRadius}" min="5">
|
431 |
+
<label for="editPendulumLength">Length (m):</label><input type="number" id="editPendulumLength" value="${opts.length}" min="50">
|
432 |
+
<label for="editPendulumStiffness">Stiffness:</label><input type="number" id="editPendulumStiffness" value="${opts.stiffness}" step="0.1" min="0" max="1">`;
|
|
|
|
|
433 |
}
|
434 |
+
html += `<div style="margin-top:15px;">
|
|
|
435 |
<button id="updateElement">Update</button>
|
436 |
<button id="deleteElement">Delete</button>
|
437 |
+
<button id="connectElement">Connect</button>
|
438 |
</div>`;
|
439 |
editPanel.innerHTML = html;
|
440 |
editPanel.style.display = "block";
|
441 |
+
connectionPanel.style.display = "none";
|
442 |
document.getElementById("updateElement").addEventListener("click", updateElementFromEditPanel);
|
443 |
+
document.getElementById("deleteElement").addEventListener("click", () => { World.remove(world, selectedBody); hideEditPanel(); });
|
444 |
+
document.getElementById("connectElement").addEventListener("click", () => { attachMode = true; attachFrom = body; editPanel.style.display = "none"; connectionPanel.style.display = "block"; });
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
}
|
446 |
+
|
447 |
+
function hideEditPanel() { editPanel.style.display = "none"; connectionPanel.style.display = "none"; rightSidebar.classList.add('hidden'); selectedBody = null; }
|
448 |
+
function hideConnectionPanel() { connectionPanel.style.display = "none"; attachMode = false; attachFrom = null; if (!selectedBody) rightSidebar.classList.add('hidden'); }
|
449 |
+
function hideConstraintEditPanel() { constraintEditPanel.style.display = "none"; selectedConstraint = null; }
|
450 |
+
|
451 |
+
function updateElementFromEditPanel() {
|
452 |
+
if (!selectedBody) return;
|
453 |
const newX = parseFloat(document.getElementById("editPosX").value);
|
454 |
const newY = parseFloat(document.getElementById("editPosY").value);
|
455 |
const newAngle = parseFloat(document.getElementById("editAngle").value);
|
|
|
457 |
Body.setPosition(selectedBody, { x: newX, y: newY });
|
458 |
Body.setAngle(selectedBody, newAngle);
|
459 |
selectedBody.render.fillStyle = newColor;
|
460 |
+
if (selectedBody.elementType === "bouncingBall") {
|
461 |
+
const newRadius = parseFloat(document.getElementById("editBallRadius").value);
|
462 |
+
const newRestitution = parseFloat(document.getElementById("editBallRestitution").value);
|
463 |
+
const newFriction = parseFloat(document.getElementById("editBallFriction").value);
|
464 |
+
Body.scale(selectedBody, newRadius / selectedBody.circleRadius, newRadius / selectedBody.circleRadius);
|
465 |
+
selectedBody.restitution = newRestitution;
|
466 |
+
selectedBody.friction = newFriction;
|
467 |
+
selectedBody.customOptions = { ...selectedBody.customOptions, x: newX, y: newY, radius: newRadius, restitution: newRestitution, friction: newFriction, color: newColor };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
}
|
469 |
hideEditPanel();
|
470 |
}
|
471 |
|
472 |
+
render.canvas.addEventListener("dblclick", (event) => {
|
|
|
473 |
const rect = render.canvas.getBoundingClientRect();
|
474 |
const mousePos = { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
475 |
const bodies = Matter.Composite.allBodies(world);
|
476 |
const clicked = Matter.Query.point(bodies, mousePos);
|
477 |
+
if (clicked.length > 0 && !clicked[0].isStatic) openEditPanel(clicked[0]);
|
478 |
+
else hideEditPanel();
|
|
|
|
|
|
|
479 |
});
|
480 |
|
481 |
+
document.getElementById('cancelConn').addEventListener('click', hideConnectionPanel);
|
482 |
+
|
483 |
+
Events.on(mouseConstraint, 'mouseup', (event) => {
|
484 |
const mousePos = event.mouse.position;
|
485 |
const bodies = Matter.Composite.allBodies(world);
|
486 |
const clickedBodies = Matter.Query.point(bodies, mousePos);
|
487 |
+
if (attachMode && clickedBodies.length > 0 && attachFrom && clickedBodies[0] !== attachFrom) {
|
488 |
+
const connType = document.getElementById('connType').value;
|
489 |
+
const sourceEndpoint = attachFrom.elementType === "springMass" ? document.getElementById('sourceEndpoint').value : "mass";
|
490 |
+
const targetBody = clickedBodies[0];
|
491 |
+
let pointA = sourceEndpoint === "fixed" ? attachFrom.fixedPoint : attachFrom.position;
|
492 |
+
let pointB = targetBody.elementType === "springMass" && confirm("Connect to fixed point?") ? targetBody.fixedPoint : targetBody.position;
|
493 |
+
const length = Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y);
|
494 |
+
const stiffness = connType === "spring" ? 0.05 : (connType === "stick" ? 1 : 0.9);
|
495 |
+
const constraint = Constraint.create({
|
496 |
+
bodyA: sourceEndpoint === "mass" ? attachFrom : null,
|
497 |
+
pointA: sourceEndpoint === "fixed" ? pointA : { x: 0, y: 0 },
|
498 |
+
bodyB: targetBody.elementType === "springMass" && pointB === targetBody.fixedPoint ? null : targetBody,
|
499 |
+
pointB: targetBody.elementType === "springMass" && pointB === targetBody.fixedPoint ? pointB : { x: 0, y: 0 },
|
500 |
+
length,
|
501 |
+
stiffness,
|
502 |
+
render: { strokeStyle: '#333', lineWidth: 2 }
|
503 |
+
});
|
504 |
+
World.add(world, constraint);
|
505 |
+
hideConnectionPanel();
|
506 |
+
} else if (!clickedBodies.length) {
|
507 |
+
const constraints = Matter.Composite.allConstraints(world);
|
508 |
+
for (let cons of constraints) {
|
509 |
+
let posA = cons.bodyA ? Vector.add(cons.bodyA.position, cons.pointA) : cons.pointA;
|
510 |
+
let posB = cons.bodyB ? Vector.add(cons.bodyB.position, cons.pointB) : cons.pointB;
|
511 |
+
if (distanceToSegment(mousePos, posA, posB) < 5) {
|
512 |
+
showConstraintEditPanel(cons);
|
513 |
+
return;
|
514 |
+
}
|
515 |
+
}
|
516 |
+
hideConstraintEditPanel();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
}
|
518 |
});
|
519 |
+
|
520 |
+
function distanceToSegment(p, v, w) {
|
521 |
+
const l2 = Vector.magnitudeSquared(Vector.sub(w, v));
|
522 |
+
if (l2 === 0) return Math.hypot(p.x - v.x, p.y - v.y);
|
523 |
+
let t = Math.max(0, Math.min(1, ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2));
|
524 |
+
const proj = Vector.add(v, Vector.mult(Vector.sub(w, v), t));
|
|
|
525 |
return Math.hypot(p.x - proj.x, p.y - proj.y);
|
526 |
}
|
527 |
|
528 |
+
function showConstraintEditPanel(cons) {
|
|
|
|
|
529 |
selectedConstraint = cons;
|
530 |
+
let posA = cons.bodyA ? Vector.add(cons.bodyA.position, cons.pointA) : cons.pointA;
|
531 |
+
let posB = cons.bodyB ? Vector.add(cons.bodyB.position, cons.pointB) : cons.pointB;
|
532 |
+
document.getElementById('consA_X').value = posA.x.toFixed(1);
|
533 |
+
document.getElementById('consA_Y').value = posA.y.toFixed(1);
|
534 |
+
document.getElementById('consB_X').value = posB.x.toFixed(1);
|
535 |
+
document.getElementById('consB_Y').value = posB.y.toFixed(1);
|
536 |
+
document.getElementById('consType').value = cons.stiffness < 0.1 ? "spring" : (cons.stiffness > 0.95 ? "stick" : "string");
|
|
|
537 |
constraintEditPanel.style.display = "block";
|
538 |
}
|
539 |
+
|
540 |
+
document.getElementById('updateConstraint').addEventListener('click', () => {
|
541 |
+
if (selectedConstraint) {
|
542 |
+
const aX = parseFloat(document.getElementById('consA_X').value);
|
543 |
+
const aY = parseFloat(document.getElementById('consA_Y').value);
|
544 |
+
const bX = parseFloat(document.getElementById('consB_X').value);
|
545 |
+
const bY = parseFloat(document.getElementById('consB_Y').value);
|
546 |
+
const connType = document.getElementById('consType').value;
|
547 |
+
if (!selectedConstraint.bodyA) selectedConstraint.pointA = { x: aX, y: aY };
|
548 |
+
if (!selectedConstraint.bodyB) selectedConstraint.pointB = { x: bX, y: bY };
|
549 |
+
selectedConstraint.stiffness = connType === "spring" ? 0.05 : (connType === "stick" ? 1 : 0.9);
|
550 |
+
selectedConstraint.length = Math.hypot(bX - aX, bY - aY);
|
|
|
|
|
551 |
hideConstraintEditPanel();
|
552 |
}
|
553 |
});
|
554 |
+
|
555 |
+
document.getElementById('deleteConstraint').addEventListener('click', () => {
|
556 |
+
if (selectedConstraint) {
|
557 |
World.remove(world, selectedConstraint);
|
558 |
hideConstraintEditPanel();
|
559 |
}
|
560 |
});
|
561 |
|
562 |
+
Events.on(engine, 'afterUpdate', () => {
|
563 |
+
if (selectedBody && !selectedBody.isStatic) {
|
564 |
+
const v = selectedBody.velocity;
|
565 |
+
const ke = 0.5 * selectedBody.mass * (v.x * v.x + v.y * v.y);
|
566 |
+
const pe = selectedBody.elementType === "springMass" ?
|
567 |
+
0.5 * selectedBody.constraints[0].stiffness * Math.pow(Vector.magnitude(Vector.sub(selectedBody.position, selectedBody.fixedPoint)) - selectedBody.customOptions.length, 2) :
|
568 |
+
selectedBody.mass * engine.gravity.y * (window.innerHeight - selectedBody.position.y);
|
569 |
+
dataContent.innerHTML = `
|
570 |
+
Position: (${selectedBody.position.x.toFixed(1)}, ${selectedBody.position.y.toFixed(1)}) m<br>
|
571 |
+
Velocity: (${v.x.toFixed(1)}, ${v.y.toFixed(1)}) m/s<br>
|
572 |
+
Kinetic Energy: ${ke.toFixed(2)} J<br>
|
573 |
+
Potential Energy: ${pe.toFixed(2)} J<br>
|
574 |
+
Total Energy: ${(ke + pe).toFixed(2)} J
|
575 |
+
`;
|
576 |
+
}
|
577 |
+
});
|
578 |
+
|
579 |
+
window.addEventListener('resize', () => {
|
580 |
+
render.canvas.width = window.innerWidth;
|
581 |
+
render.canvas.height = window.innerHeight;
|
582 |
Render.lookAt(render, { min: { x: 0, y: 0 }, max: { x: window.innerWidth, y: window.innerHeight } });
|
583 |
updateBoundaries();
|
584 |
});
|
585 |
</script>
|
586 |
</body>
|
587 |
+
</html>
|