|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>Advanced Physics Simulator – Broad Elements Menu</title> |
|
<style> |
|
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; } |
|
|
|
canvas { display: block; } |
|
|
|
canvas { |
|
display: block; |
|
margin-top: 94px; |
|
} |
|
#topMenu { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
background: transparent; |
|
border-bottom: 1px solid #ccc; |
|
padding: 5px; |
|
z-index: 2000; |
|
display: flex; |
|
align-items: center; |
|
gap: 15px; |
|
backdrop-filter: blur(10px); |
|
} |
|
#topMenu select, #topMenu button { |
|
padding: 5px 10px; |
|
font-size: 14px; |
|
} |
|
|
|
#subOptionsContainer { |
|
position: fixed; |
|
top: 45px; |
|
left: 0; |
|
z-index: 2000; |
|
background: transparent; |
|
padding: 5px; |
|
border: 1px solid #ccc; |
|
border-radius: 0; |
|
display: flex |
|
; |
|
width: -webkit-fill-available; |
|
align-items: baseline; |
|
backdrop-filter: blur(10px); |
|
} |
|
|
|
#editPanel { |
|
position: fixed; |
|
top: 45px; |
|
right: 0; |
|
z-index: 2000; |
|
background: transparent; |
|
padding: 10px; |
|
border: 1px solid #ccc; |
|
border-radius: 4px; |
|
max-width: 100%; |
|
max-height: 72vh; |
|
overflow-y: auto; |
|
display: none; |
|
backdrop-filter: blur(10px); |
|
} |
|
|
|
#connectionPanel, #constraintEditPanel { |
|
position: fixed; |
|
z-index: 2000; |
|
background: rgba(255,255,255,0.95); |
|
padding: 10px; |
|
border: 1px solid #ccc; |
|
border-radius: 4px; |
|
max-width: 250px; |
|
display: none; |
|
} |
|
#connectionPanel { top: 45px; left: 50%; transform: translateX(-50%); } |
|
#constraintEditPanel { bottom: 10px; right: 10px; } |
|
|
|
label { display: block; margin-top: 8px; font-size: 13px; } |
|
input, select, button { |
|
width: fit-content; |
|
padding: 5px; |
|
margin-top: 4px; |
|
font-size: 13px; |
|
margin: 4px; |
|
} |
|
input[type="color" i] { |
|
width: 42px; |
|
height: 28px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<div id="topMenu"> |
|
<select id="simulationSelect"> |
|
<option value="bouncingBall">Bouncing Ball</option> |
|
<option value="pendulum">Pendulum</option> |
|
<option value="projectile">Projectile Motion</option> |
|
<option value="inclinedPlane">Inclined Plane</option> |
|
<option value="springMass">Spring–Mass System</option> |
|
<option value="rectangleBlock">Rectangle Block</option> |
|
<option value="rotatingRectangle">Rotating Rectangle</option> |
|
<option value="triangle">Triangle</option> |
|
</select> |
|
<button id="addElement">Add Element</button> |
|
<button id="connectSystems">Connect Systems</button> |
|
<button id="reset">Reset Simulation</button> |
|
</div> |
|
|
|
|
|
<div id="subOptionsContainer"></div> |
|
|
|
|
|
<div id="editPanel"></div> |
|
|
|
|
|
<div id="connectionPanel"> |
|
<h3>Connection Options</h3> |
|
<label for="connType">Connection Type:</label> |
|
<select id="connType"> |
|
<option value="string">String</option> |
|
<option value="spring">Spring</option> |
|
<option value="stick">Stick</option> |
|
</select> |
|
<label for="connMode">Connection Mode:</label> |
|
<select id="connMode"> |
|
<option value="element">Element</option> |
|
<option value="system">System</option> |
|
</select> |
|
<div id="sourceEndpointDiv"> |
|
<label for="sourceEndpoint">Source Endpoint:</label> |
|
<select id="sourceEndpoint"> |
|
<option value="mass">Mass</option> |
|
<option value="fixed">Fixed</option> |
|
</select> |
|
</div> |
|
<button id="cancelConn">Cancel</button> |
|
<p style="font-size: 12px; color: #555;">Click on the target element to attach.</p> |
|
</div> |
|
|
|
|
|
<div id="constraintEditPanel"> |
|
<h3>Edit Constraint</h3> |
|
<label for="consA_X">Endpoint A X:</label> |
|
<input type="number" id="consA_X" step="1"> |
|
<label for="consA_Y">Endpoint A Y:</label> |
|
<input type="number" id="consA_Y" step="1"> |
|
<label for="consB_X">Endpoint B X:</label> |
|
<input type="number" id="consB_X" step="1"> |
|
<label for="consB_Y">Endpoint B Y:</label> |
|
<input type="number" id="consB_Y" step="1"> |
|
<label for="consType">Connection Type:</label> |
|
<select id="consType"> |
|
<option value="string">String</option> |
|
<option value="spring">Spring</option> |
|
<option value="stick">Stick</option> |
|
</select> |
|
<button id="updateConstraint">Update Constraint</button> |
|
<button id="deleteConstraint">Delete Constraint</button> |
|
</div> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script> |
|
<script> |
|
// Module aliases and engine setup |
|
const Engine = Matter.Engine, |
|
Render = Matter.Render, |
|
Runner = Matter.Runner, |
|
World = Matter.World, |
|
Bodies = Matter.Bodies, |
|
Body = Matter.Body, |
|
Constraint = Matter.Constraint, |
|
Mouse = Matter.Mouse, |
|
MouseConstraint = Matter.MouseConstraint, |
|
Events = Matter.Events; |
|
|
|
const engine = Engine.create(); |
|
const world = engine.world; |
|
const render = Render.create({ |
|
element: document.body, |
|
engine: engine, |
|
options: { |
|
width: window.innerWidth, |
|
height: window.innerHeight, |
|
wireframes: false, |
|
background: '#f0f0f0' |
|
} |
|
}); |
|
Render.run(render); |
|
const runner = Runner.create(); |
|
Runner.run(runner, engine); |
|
|
|
// Boundaries (responsive) |
|
let floor, wallLeft, wallRight; |
|
function updateBoundaries() { |
|
if(floor && wallLeft && wallRight) { |
|
World.remove(world, [floor, wallLeft, wallRight]); |
|
} |
|
floor = Bodies.rectangle(window.innerWidth/2, window.innerHeight - 50, window.innerWidth, 100, { |
|
isStatic: true, |
|
render: { fillStyle: '#060a19' } |
|
}); |
|
wallLeft = Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, { isStatic: true }); |
|
wallRight = Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, { isStatic: true }); |
|
World.add(world, [floor, wallLeft, wallRight]); |
|
} |
|
updateBoundaries(); |
|
|
|
// Mouse control |
|
let mouse = Mouse.create(render.canvas); |
|
let mouseConstraint = MouseConstraint.create(engine, { |
|
mouse: mouse, |
|
constraint: { stiffness: 0.2, render: { visible: false } } |
|
}); |
|
World.add(world, mouseConstraint); |
|
render.mouse = mouse; |
|
|
|
// Global variables for editing and connection |
|
let selectedBody = null; |
|
let selectedConstraint = null; |
|
let attachMode = false; |
|
let attachFrom = null; |
|
const connectionPanel = document.getElementById('connectionPanel'); |
|
const editPanel = document.getElementById('editPanel'); |
|
|
|
// Helper: random color |
|
function getRandomColor() { |
|
return '#' + Math.floor(Math.random()*16777215).toString(16); |
|
} |
|
|
|
/* ---------- Dynamic Suboptions for Adding Elements ---------- */ |
|
function updateSubOptions() { |
|
const simulationSelect = document.getElementById('simulationSelect'); |
|
const subOptionsContainer = document.getElementById('subOptionsContainer'); |
|
let selected = simulationSelect.value; |
|
let html = ''; |
|
if(selected === 'bouncingBall'){ |
|
html += ` |
|
<label for="ballInitialX">Initial X:</label> |
|
<input type="number" id="ballInitialX" value="100"> |
|
<label for="ballInitialY">Initial Y:</label> |
|
<input type="number" id="ballInitialY" value="50"> |
|
<label for="ballRadius">Radius:</label> |
|
<input type="number" id="ballRadius" value="30"> |
|
<label for="ballRestitution">Restitution:</label> |
|
<input type="number" id="ballRestitution" value="0.9" step="0.1" min="0" max="1"> |
|
<label for="ballFriction">Friction:</label> |
|
<input type="number" id="ballFriction" value="0.005" step="0.001" min="0" max="1"> |
|
<label for="ballColor">Color:</label> |
|
<input type="color" id="ballColor" value="#3498db"> |
|
`; |
|
} else if(selected === 'pendulum'){ |
|
html += ` |
|
<label for="pendulumPivotX">Pivot X:</label> |
|
<input type="number" id="pendulumPivotX" value="${window.innerWidth-200}"> |
|
<label for="pendulumPivotY">Pivot Y:</label> |
|
<input type="number" id="pendulumPivotY" value="50"> |
|
<label for="pendulumBobRadius">Bob Radius:</label> |
|
<input type="number" id="pendulumBobRadius" value="40"> |
|
<label for="pendulumLength">Length:</label> |
|
<input type="number" id="pendulumLength" value="250"> |
|
<label for="pendulumStiffness">Stiffness:</label> |
|
<input type="number" id="pendulumStiffness" value="1" step="0.1" min="0" max="1"> |
|
<label for="pendulumBobColor">Color:</label> |
|
<input type="color" id="pendulumBobColor" value="#ff0000"> |
|
`; |
|
} else if(selected === 'projectile'){ |
|
html += ` |
|
<label for="projectileInitialX">Initial X:</label> |
|
<input type="number" id="projectileInitialX" value="100"> |
|
<label for="projectileInitialY">Initial Y:</label> |
|
<input type="number" id="projectileInitialY" value="300"> |
|
<label for="projectileRadius">Radius:</label> |
|
<input type="number" id="projectileRadius" value="20"> |
|
<label for="projectileVelX">Velocity X:</label> |
|
<input type="number" id="projectileVelX" value="15"> |
|
<label for="projectileVelY">Velocity Y:</label> |
|
<input type="number" id="projectileVelY" value="-15"> |
|
<label for="projectileColor">Color:</label> |
|
<input type="color" id="projectileColor" value="#e67e22"> |
|
`; |
|
} else if(selected === 'inclinedPlane'){ |
|
html += ` |
|
<label for="rampWidth">Ramp Width:</label> |
|
<input type="number" id="rampWidth" value="300"> |
|
<label for="rampHeight">Ramp Height:</label> |
|
<input type="number" id="rampHeight" value="20"> |
|
<label for="rampAngle">Ramp Angle (°):</label> |
|
<input type="number" id="rampAngle" value="30"> |
|
<label for="rampX">Ramp X:</label> |
|
<input type="number" id="rampX" value="400"> |
|
<label for="rampY">Ramp Y:</label> |
|
<input type="number" id="rampY" value="${window.innerHeight-150}"> |
|
<label for="rampColor">Ramp Color:</label> |
|
<input type="color" id="rampColor" value="#8e44ad"> |
|
<hr> |
|
<label for="blockWidth">Block Width:</label> |
|
<input type="number" id="blockWidth" value="40"> |
|
<label for="blockHeight">Block Height:</label> |
|
<input type="number" id="blockHeight" value="40"> |
|
<label for="blockFriction">Block Friction:</label> |
|
<input type="number" id="blockFriction" value="0.05" step="0.01" min="0" max="1"> |
|
<label for="blockColor">Block Color:</label> |
|
<input type="color" id="blockColor" value="#1abc9c"> |
|
`; |
|
} else if(selected === 'springMass'){ |
|
html += ` |
|
<label for="springMassRadius">Mass Radius:</label> |
|
<input type="number" id="springMassRadius" value="25"> |
|
<label for="springMassRestitution">Restitution:</label> |
|
<input type="number" id="springMassRestitution" value="0.8" step="0.1" min="0" max="1"> |
|
<label for="fixedPointX">Fixed X:</label> |
|
<input type="number" id="fixedPointX" value="${Math.floor(window.innerWidth/2)}"> |
|
<label for="fixedPointY">Fixed Y:</label> |
|
<input type="number" id="fixedPointY" value="100"> |
|
<label for="springLength">Spring Length:</label> |
|
<input type="number" id="springLength" value="200"> |
|
<label for="springStiffness">Stiffness:</label> |
|
<input type="number" id="springStiffness" value="0.02" step="0.01" min="0" max="1"> |
|
<label for="springMassColor">Color:</label> |
|
<input type="color" id="springMassColor" value="#f39c12"> |
|
`; |
|
} else if(selected === 'rectangleBlock'){ |
|
html += ` |
|
<label for="blockInitX">Initial X:</label> |
|
<input type="number" id="blockInitX" value="150"> |
|
<label for="blockInitY">Initial Y:</label> |
|
<input type="number" id="blockInitY" value="150"> |
|
<label for="blockWidth">Width:</label> |
|
<input type="number" id="blockWidth" value="80"> |
|
<label for="blockHeight">Height:</label> |
|
<input type="number" id="blockHeight" value="40"> |
|
<label for="blockFriction">Friction:</label> |
|
<input type="number" id="blockFriction" value="0.1" step="0.01" min="0" max="1"> |
|
<label for="blockRestitution">Restitution:</label> |
|
<input type="number" id="blockRestitution" value="0.3" step="0.1" min="0" max="1"> |
|
<label for="blockColor">Color:</label> |
|
<input type="color" id="blockColor" value="#2ecc71"> |
|
`; |
|
} else if(selected === 'rotatingRectangle'){ |
|
html += ` |
|
<label for="rotRectInitX">Initial X:</label> |
|
<input type="number" id="rotRectInitX" value="200"> |
|
<label for="rotRectInitY">Initial Y:</label> |
|
<input type="number" id="rotRectInitY" value="200"> |
|
<label for="rotRectWidth">Width:</label> |
|
<input type="number" id="rotRectWidth" value="100"> |
|
<label for="rotRectHeight">Height:</label> |
|
<input type="number" id="rotRectHeight" value="50"> |
|
<label for="rotRectRotationSpeed">Rotation Speed (rad/s):</label> |
|
<input type="number" id="rotRectRotationSpeed" value="0.05" step="0.01"> |
|
<label for="rotRectFriction">Friction:</label> |
|
<input type="number" id="rotRectFriction" value="0.1" step="0.01" min="0" max="1"> |
|
<label for="rotRectRestitution">Restitution:</label> |
|
<input type="number" id="rotRectRestitution" value="0.3" step="0.1" min="0" max="1"> |
|
<label for="rotRectColor">Color:</label> |
|
<input type="color" id="rotRectColor" value="#9b59b6"> |
|
`; |
|
} else if(selected === 'triangle'){ |
|
html += ` |
|
<label for="triInitX">Initial X:</label> |
|
<input type="number" id="triInitX" value="300"> |
|
<label for="triInitY">Initial Y:</label> |
|
<input type="number" id="triInitY" value="300"> |
|
<label for="triSize">Size (radius):</label> |
|
<input type="number" id="triSize" value="40"> |
|
<label for="triFriction">Friction:</label> |
|
<input type="number" id="triFriction" value="0.1" step="0.01" min="0" max="1"> |
|
<label for="triRestitution">Restitution:</label> |
|
<input type="number" id="triRestitution" value="0.3" step="0.1" min="0" max="1"> |
|
<label for="triColor">Color:</label> |
|
<input type="color" id="triColor" value="#e74c3c"> |
|
`; |
|
} |
|
subOptionsContainer.innerHTML = html; |
|
} |
|
updateSubOptions(); |
|
document.getElementById('simulationSelect').addEventListener('change', updateSubOptions); |
|
|
|
/* ---------- Functions to Add Simulation Elements ---------- */ |
|
function addBouncingBall(){ |
|
const x = parseFloat(document.getElementById("ballInitialX").value); |
|
const y = parseFloat(document.getElementById("ballInitialY").value); |
|
const radius = parseFloat(document.getElementById("ballRadius").value); |
|
const restitution = parseFloat(document.getElementById("ballRestitution").value); |
|
const friction = parseFloat(document.getElementById("ballFriction").value); |
|
const color = document.getElementById("ballColor").value || getRandomColor(); |
|
const ball = Bodies.circle(x, y, radius, { restitution, friction, render: { fillStyle: color } }); |
|
ball.elementType = "bouncingBall"; |
|
ball.customOptions = { x, y, radius, restitution, friction, color, isSystemRoot: false }; |
|
World.add(world, ball); |
|
} |
|
function addPendulum(){ |
|
const pivotX = parseFloat(document.getElementById("pendulumPivotX").value); |
|
const pivotY = parseFloat(document.getElementById("pendulumPivotY").value); |
|
const bobRadius = parseFloat(document.getElementById("pendulumBobRadius").value); |
|
const pendulumLength = parseFloat(document.getElementById("pendulumLength").value); |
|
const stiffness = parseFloat(document.getElementById("pendulumStiffness").value); |
|
const bobColor = document.getElementById("pendulumBobColor").value || "#ff0000"; |
|
const bob = Bodies.circle(pivotX, pivotY+pendulumLength, bobRadius, { restitution: 1, density: 0.005, render: { fillStyle: bobColor } }); |
|
bob.elementType = "pendulum"; |
|
bob.customOptions = { pivotX, pivotY, bobRadius, pendulumLength, stiffness, bobColor, isSystemRoot: false }; |
|
const pendulumConstraint = Constraint.create({ |
|
pointA: { x: pivotX, y: pivotY }, |
|
bodyB: bob, |
|
length: pendulumLength, |
|
stiffness: stiffness, |
|
render: { strokeStyle: '#000', lineWidth: 2 } |
|
}); |
|
World.add(world, [bob, pendulumConstraint]); |
|
} |
|
function addProjectile(){ |
|
const x = parseFloat(document.getElementById("projectileInitialX").value); |
|
const y = parseFloat(document.getElementById("projectileInitialY").value); |
|
const radius = parseFloat(document.getElementById("projectileRadius").value); |
|
const velX = parseFloat(document.getElementById("projectileVelX").value); |
|
const velY = parseFloat(document.getElementById("projectileVelY").value); |
|
const color = document.getElementById("projectileColor").value || getRandomColor(); |
|
const proj = Bodies.circle(x, y, radius, { restitution: 0.8, frictionAir: 0.001, render: { fillStyle: color } }); |
|
proj.elementType = "projectile"; |
|
proj.customOptions = { x, y, radius, velX, velY, color, isSystemRoot: false }; |
|
Body.setVelocity(proj, { x: velX, y: velY }); |
|
World.add(world, proj); |
|
} |
|
function addInclinedPlane(){ |
|
const rampWidth = parseFloat(document.getElementById("rampWidth").value); |
|
const rampHeight = parseFloat(document.getElementById("rampHeight").value); |
|
const angleDeg = parseFloat(document.getElementById("rampAngle").value); |
|
const angle = angleDeg * Math.PI/180; |
|
const rampX = parseFloat(document.getElementById("rampX").value); |
|
const rampY = parseFloat(document.getElementById("rampY").value); |
|
const rampColor = document.getElementById("rampColor").value || "#8e44ad"; |
|
const ramp = Bodies.rectangle(rampX, rampY, rampWidth, rampHeight, { |
|
isStatic: true, |
|
angle: angle, |
|
render: { fillStyle: rampColor } |
|
}); |
|
ramp.elementType = "inclinedPlane_ramp"; |
|
ramp.customOptions = { rampWidth, rampHeight, angleDeg, rampX, rampY, rampColor, isSystemRoot: false }; |
|
const blockWidth = parseFloat(document.getElementById("blockWidth").value); |
|
const blockHeight = parseFloat(document.getElementById("blockHeight").value); |
|
const blockFriction = parseFloat(document.getElementById("blockFriction").value); |
|
const blockColor = document.getElementById("blockColor").value || "#1abc9c"; |
|
const block = Bodies.rectangle(rampX - rampWidth/4, rampY - 50, blockWidth, blockHeight, { |
|
friction: blockFriction, |
|
render: { fillStyle: blockColor } |
|
}); |
|
block.elementType = "inclinedPlane_block"; |
|
block.customOptions = { blockWidth, blockHeight, blockFriction, blockColor, isSystemRoot: false }; |
|
World.add(world, [ramp, block]); |
|
} |
|
function addSpringMass(){ |
|
const massRadius = parseFloat(document.getElementById("springMassRadius").value); |
|
const massRestitution = parseFloat(document.getElementById("springMassRestitution").value); |
|
const fixedPointX = parseFloat(document.getElementById("fixedPointX").value); |
|
const fixedPointY = parseFloat(document.getElementById("fixedPointY").value); |
|
const springLength = parseFloat(document.getElementById("springLength").value); |
|
const springStiffness = parseFloat(document.getElementById("springStiffness").value); |
|
const massColor = document.getElementById("springMassColor").value || getRandomColor(); |
|
const mass = Bodies.circle(fixedPointX, fixedPointY+springLength, massRadius, { |
|
restitution: massRestitution, |
|
density: 0.004, |
|
render: { fillStyle: massColor } |
|
}); |
|
mass.elementType = "springMass"; |
|
mass.customOptions = { massRadius, massRestitution, fixedPointX, fixedPointY, springLength, springStiffness, massColor, isSystemRoot: false }; |
|
mass.fixedPoint = { x: fixedPointX, y: fixedPointY }; |
|
const spring = Constraint.create({ |
|
pointA: { x: fixedPointX, y: fixedPointY }, |
|
bodyB: mass, |
|
length: springLength, |
|
stiffness: springStiffness, |
|
damping: 0.05, |
|
render: { strokeStyle: '#000', lineWidth: 2 } |
|
}); |
|
World.add(world, [mass, spring]); |
|
} |
|
function addRectangleBlock(){ |
|
const x = parseFloat(document.getElementById("blockInitX").value); |
|
const y = parseFloat(document.getElementById("blockInitY").value); |
|
const width = parseFloat(document.getElementById("blockWidth").value); |
|
const height = parseFloat(document.getElementById("blockHeight").value); |
|
const friction = parseFloat(document.getElementById("blockFriction").value); |
|
const restitution = parseFloat(document.getElementById("blockRestitution").value); |
|
const color = document.getElementById("blockColor").value || getRandomColor(); |
|
const block = Bodies.rectangle(x, y, width, height, { friction, restitution, render: { fillStyle: color } }); |
|
block.elementType = "rectangleBlock"; |
|
block.customOptions = { x, y, width, height, friction, restitution, color, isSystemRoot: false }; |
|
World.add(world, block); |
|
} |
|
function addRotatingRectangle(){ |
|
const x = parseFloat(document.getElementById("rotRectInitX").value); |
|
const y = parseFloat(document.getElementById("rotRectInitY").value); |
|
const width = parseFloat(document.getElementById("rotRectWidth").value); |
|
const height = parseFloat(document.getElementById("rotRectHeight").value); |
|
const rotationSpeed = parseFloat(document.getElementById("rotRectRotationSpeed").value); |
|
const friction = parseFloat(document.getElementById("rotRectFriction").value); |
|
const restitution = parseFloat(document.getElementById("rotRectRestitution").value); |
|
const color = document.getElementById("rotRectColor").value || getRandomColor(); |
|
const rect = Bodies.rectangle(x, y, width, height, { friction, restitution, render: { fillStyle: color } }); |
|
rect.elementType = "rotatingRectangle"; |
|
rect.customOptions = { x, y, width, height, rotationSpeed, friction, restitution, color, isSystemRoot: false }; |
|
// Continuously rotate the rectangle |
|
Events.on(engine, "beforeUpdate", function(){ |
|
Body.rotate(rect, rotationSpeed); |
|
}); |
|
World.add(world, rect); |
|
} |
|
function addTriangle(){ |
|
const x = parseFloat(document.getElementById("triInitX").value); |
|
const y = parseFloat(document.getElementById("triInitY").value); |
|
const radius = parseFloat(document.getElementById("triSize").value); |
|
const friction = parseFloat(document.getElementById("triFriction").value); |
|
const restitution = parseFloat(document.getElementById("triRestitution").value); |
|
const color = document.getElementById("triColor").value || getRandomColor(); |
|
const tri = Bodies.polygon(x, y, 3, radius, { friction, restitution, render: { fillStyle: color } }); |
|
tri.elementType = "triangle"; |
|
tri.customOptions = { x, y, radius, friction, restitution, color, isSystemRoot: false }; |
|
World.add(world, tri); |
|
} |
|
document.getElementById('addElement').addEventListener('click', function(){ |
|
const selected = document.getElementById('simulationSelect').value; |
|
if(selected === 'bouncingBall') addBouncingBall(); |
|
else if(selected === 'pendulum') addPendulum(); |
|
else if(selected === 'projectile') addProjectile(); |
|
else if(selected === 'inclinedPlane') addInclinedPlane(); |
|
else if(selected === 'springMass') addSpringMass(); |
|
else if(selected === 'rectangleBlock') addRectangleBlock(); |
|
else if(selected === 'rotatingRectangle') addRotatingRectangle(); |
|
else if(selected === 'triangle') addTriangle(); |
|
}); |
|
// "Connect Systems" button to initiate attach mode (for connecting elements or systems) |
|
document.getElementById('connectSystems').addEventListener('click', function(){ |
|
attachMode = true; |
|
attachFrom = null; |
|
connectionPanel.style.display = "block"; |
|
document.getElementById('connMode').value = "system"; |
|
alert("System connection mode activated. Click on the first system element (must be marked as system root) then the target system element."); |
|
}); |
|
document.getElementById('reset').addEventListener('click', function(){ |
|
World.clear(world); |
|
Engine.clear(engine); |
|
updateBoundaries(); |
|
mouse = Mouse.create(render.canvas); |
|
mouseConstraint = MouseConstraint.create(engine, { mouse: mouse, constraint: { stiffness: 0.2, render: { visible: false } } }); |
|
World.add(world, mouseConstraint); |
|
render.mouse = mouse; |
|
hideEditPanel(); |
|
hideConnectionPanel(); |
|
hideConstraintEditPanel(); |
|
}); |
|
|
|
/* ---------- Edit Panel Functions (Double-click on element) ---------- */ |
|
function openEditPanel(body) { |
|
selectedBody = body; |
|
let html = `<h3>Edit Element</h3> |
|
<label for="editPosX">Position X:</label> |
|
<input type="number" id="editPosX" value="${body.position.x.toFixed(2)}"> |
|
<label for="editPosY">Position Y:</label> |
|
<input type="number" id="editPosY" value="${body.position.y.toFixed(2)}"> |
|
<label for="editAngle">Angle (radians):</label> |
|
<input type="number" id="editAngle" value="${body.angle.toFixed(2)}"> |
|
<label for="editColor">Color:</label> |
|
<input type="color" id="editColor" value="${body.render.fillStyle || '#ffffff'}"> |
|
<label for="editSystemRoot">Is System Root?</label> |
|
<input type="checkbox" id="editSystemRoot" ${body.customOptions.isSystemRoot ? "checked" : ""}>`; |
|
if(body.elementType === "bouncingBall"){ |
|
const opts = body.customOptions; |
|
html += `<label for="editBallRadius">Radius:</label> |
|
<input type="number" id="editBallRadius" value="${opts.radius}"> |
|
<label for="editBallRestitution">Restitution:</label> |
|
<input type="number" id="editBallRestitution" value="${opts.restitution}" step="0.1" min="0" max="1"> |
|
<label for="editBallFriction">Friction:</label> |
|
<input type="number" id="editBallFriction" value="${opts.friction}" step="0.001" min="0" max="1">`; |
|
} |
|
// Additional type-specific options can be added similarly |
|
html += `<div style="margin-top:10px;"> |
|
<button id="updateElement">Update</button> |
|
<button id="deleteElement">Delete</button> |
|
</div>`; |
|
editPanel.innerHTML = html; |
|
editPanel.style.display = "block"; |
|
document.getElementById("updateElement").addEventListener("click", updateElementFromEditPanel); |
|
document.getElementById("deleteElement").addEventListener("click", function(){ |
|
World.remove(world, selectedBody); |
|
hideEditPanel(); |
|
}); |
|
} |
|
function hideEditPanel() { |
|
editPanel.style.display = "none"; |
|
selectedBody = null; |
|
} |
|
function updateElementFromEditPanel(){ |
|
if(!selectedBody) return; |
|
const newX = parseFloat(document.getElementById("editPosX").value); |
|
const newY = parseFloat(document.getElementById("editPosY").value); |
|
const newAngle = parseFloat(document.getElementById("editAngle").value); |
|
const newColor = document.getElementById("editColor").value; |
|
Body.setPosition(selectedBody, { x: newX, y: newY }); |
|
Body.setAngle(selectedBody, newAngle); |
|
selectedBody.render.fillStyle = newColor; |
|
const isSysRoot = document.getElementById("editSystemRoot").checked; |
|
selectedBody.customOptions.isSystemRoot = isSysRoot; |
|
if(selectedBody.elementType === "bouncingBall"){ |
|
const oldRadius = selectedBody.circleRadius; |
|
const newRadius = parseFloat(document.getElementById("editBallRadius").value); |
|
const newRestitution = parseFloat(document.getElementById("editBallRestitution").value); |
|
const newFriction = parseFloat(document.getElementById("editBallFriction").value); |
|
if(newRadius && oldRadius && newRadius !== oldRadius){ |
|
const scaleFactor = newRadius / oldRadius; |
|
Body.scale(selectedBody, scaleFactor, scaleFactor); |
|
} |
|
selectedBody.restitution = newRestitution; |
|
selectedBody.friction = newFriction; |
|
selectedBody.customOptions = { x: newX, y: newY, radius: newRadius, restitution: newRestitution, friction: newFriction, color: newColor, isSystemRoot: isSysRoot }; |
|
} |
|
hideEditPanel(); |
|
} |
|
render.canvas.addEventListener("dblclick", function(event){ |
|
const rect = render.canvas.getBoundingClientRect(); |
|
const mousePos = { x: event.clientX - rect.left, y: event.clientY - rect.top }; |
|
const bodies = Matter.Composite.allBodies(world); |
|
const clicked = Matter.Query.point(bodies, mousePos); |
|
if(clicked.length > 0){ |
|
openEditPanel(clicked[0]); |
|
} else { |
|
hideEditPanel(); |
|
} |
|
}); |
|
|
|
/* ---------- Connection / Attachment ---------- */ |
|
Events.on(mouseConstraint, 'mouseup', function(event){ |
|
const mousePos = event.mouse.position; |
|
const bodies = Matter.Composite.allBodies(world); |
|
const clickedBodies = Matter.Query.point(bodies, mousePos); |
|
if(clickedBodies.length > 0){ |
|
const clickedBody = clickedBodies[0]; |
|
if(attachMode){ |
|
if(!attachFrom){ |
|
attachFrom = clickedBody; |
|
alert("First element selected. Now click on the target element."); |
|
return; |
|
} else if(clickedBody === attachFrom){ |
|
alert("Please select a different element."); |
|
return; |
|
} |
|
const connType = document.getElementById('connType').value; |
|
const connMode = document.getElementById('connMode').value; |
|
if(connMode === "system"){ |
|
if(!attachFrom.customOptions.isSystemRoot){ |
|
alert("Source element is not marked as System Root. Please mark it in the edit panel."); |
|
attachMode = false; |
|
attachFrom = null; |
|
connectionPanel.style.display = "none"; |
|
return; |
|
} |
|
if(!clickedBody.customOptions.isSystemRoot){ |
|
alert("Target element is not marked as System Root. Please mark it in its edit panel."); |
|
attachMode = false; |
|
attachFrom = null; |
|
connectionPanel.style.display = "none"; |
|
return; |
|
} |
|
} |
|
let sourceEndpoint = "mass"; |
|
if(attachFrom.elementType === "springMass"){ |
|
sourceEndpoint = document.getElementById('sourceEndpoint').value; |
|
} |
|
let targetEndpoint = "mass"; |
|
if(clickedBody.elementType === "springMass"){ |
|
targetEndpoint = prompt("For target springMass, choose 'fixed' or 'mass' (default: mass):", "mass") || "mass"; |
|
if(targetEndpoint !== "fixed") { targetEndpoint = "mass"; } |
|
} |
|
let pointA = { x: 0, y: 0 }, bodyA = attachFrom; |
|
if(attachFrom.elementType === "springMass" && sourceEndpoint === "fixed"){ |
|
bodyA = null; |
|
pointA = attachFrom.fixedPoint; |
|
} |
|
let pointB = { x: 0, y: 0 }, bodyB = clickedBody; |
|
if(clickedBody.elementType === "springMass" && targetEndpoint === "fixed"){ |
|
bodyB = null; |
|
pointB = clickedBody.fixedPoint || { x: clickedBody.position.x, y: clickedBody.position.y }; |
|
} |
|
if(connMode === "system"){ |
|
bodyA = attachFrom; |
|
bodyB = clickedBody; |
|
pointA = { x: 0, y: 0 }; |
|
pointB = { x: 0, y: 0 }; |
|
} |
|
let posA = bodyA ? attachFrom.position : pointA; |
|
let posB = bodyB ? clickedBody.position : pointB; |
|
let dx = posB.x - posA.x, dy = posB.y - posA.y; |
|
let length = Math.sqrt(dx*dx + dy*dy); |
|
let stiffness = (connType === "spring") ? 0.05 : 1; |
|
const newConstraint = Constraint.create({ |
|
bodyA: bodyA, |
|
pointA: pointA, |
|
bodyB: bodyB, |
|
pointB: pointB, |
|
length: length, |
|
stiffness: stiffness, |
|
render: { strokeStyle: '#000', lineWidth: 2 } |
|
}); |
|
World.add(world, newConstraint); |
|
attachMode = false; |
|
attachFrom = null; |
|
connectionPanel.style.display = "none"; |
|
alert("Attachment created using " + connType + " connection in " + connMode + " mode."); |
|
} |
|
} else { |
|
// Check if near a constraint for editing |
|
const constraints = Matter.Composite.allConstraints(world); |
|
for(let cons of constraints){ |
|
let posA = cons.bodyA ? { x: cons.bodyA.position.x + cons.pointA.x, y: cons.bodyA.position.y + cons.pointA.y } : cons.pointA; |
|
let posB = cons.bodyB ? { x: cons.bodyB.position.x + cons.pointB.x, y: cons.bodyB.position.y + cons.pointB.y } : cons.pointB; |
|
let dist = distanceToSegment(mousePos, posA, posB); |
|
if(dist < 5){ |
|
showConstraintEditPanel(cons); |
|
return; |
|
} |
|
} |
|
hideConstraintEditPanel(); |
|
} |
|
}); |
|
function distanceToSegment(p, v, w){ |
|
let l2 = (w.x - v.x)**2 + (w.y - v.y)**2; |
|
if(l2 === 0) return Math.hypot(p.x - v.x, p.y - v.y); |
|
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; |
|
t = Math.max(0, Math.min(1, t)); |
|
let proj = { x: v.x + t*(w.x-v.x), y: v.y + t*(w.y-v.y) }; |
|
return Math.hypot(p.x - proj.x, p.y - proj.y); |
|
} |
|
|
|
/* ---------- Constraint Edit Panel ---------- */ |
|
const constraintEditPanel = document.getElementById('constraintEditPanel'); |
|
function showConstraintEditPanel(cons){ |
|
selectedConstraint = cons; |
|
let posA = cons.bodyA ? { x: cons.bodyA.position.x + cons.pointA.x, y: cons.bodyA.position.y + cons.pointA.y } : cons.pointA; |
|
let posB = cons.bodyB ? { x: cons.bodyB.position.x + cons.pointB.x, y: cons.bodyB.position.y + cons.pointB.y } : cons.pointB; |
|
document.getElementById('consA_X').value = posA.x.toFixed(2); |
|
document.getElementById('consA_Y').value = posA.y.toFixed(2); |
|
document.getElementById('consB_X').value = posB.x.toFixed(2); |
|
document.getElementById('consB_Y').value = posB.y.toFixed(2); |
|
let connType = (cons.stiffness < 0.1) ? "spring" : "string"; |
|
document.getElementById('consType').value = connType; |
|
constraintEditPanel.style.display = "block"; |
|
} |
|
function hideConstraintEditPanel(){ |
|
constraintEditPanel.style.display = "none"; |
|
selectedConstraint = null; |
|
} |
|
document.getElementById('updateConstraint').addEventListener('click', function(){ |
|
if(selectedConstraint){ |
|
let aX = parseFloat(document.getElementById('consA_X').value); |
|
let aY = parseFloat(document.getElementById('consA_Y').value); |
|
let bX = parseFloat(document.getElementById('consB_X').value); |
|
let bY = parseFloat(document.getElementById('consB_Y').value); |
|
if(!selectedConstraint.bodyA) { selectedConstraint.pointA = { x: aX, y: aY }; } |
|
if(!selectedConstraint.bodyB) { selectedConstraint.pointB = { x: bX, y: bY }; } |
|
let connType = document.getElementById('consType').value; |
|
selectedConstraint.stiffness = (connType === "spring") ? 0.05 : 1; |
|
hideConstraintEditPanel(); |
|
} |
|
}); |
|
document.getElementById('deleteConstraint').addEventListener('click', function(){ |
|
if(selectedConstraint){ |
|
World.remove(world, selectedConstraint); |
|
hideConstraintEditPanel(); |
|
} |
|
}); |
|
|
|
// Window resize: update renderer and boundaries |
|
window.addEventListener('resize', function(){ |
|
Render.lookAt(render, { min: { x: 0, y: 0 }, max: { x: window.innerWidth, y: window.innerHeight } }); |
|
updateBoundaries(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|