Spaces:
Running
Running
| <!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; } | |
| /* The canvas fills the viewport */ | |
| canvas { display: block; } | |
| /* Top horizontal menu bar (outside the canvas) */ | |
| 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; | |
| } | |
| /* A container below the menu for additional options */ | |
| #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); | |
| } | |
| /* Edit Panel (floating, triggered on double-click) */ | |
| #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); | |
| } | |
| /* Connection and Constraint panels (similar styling) */ | |
| #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; } | |
| /* Common styling for labels/inputs */ | |
| 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> | |
| <!-- Top horizontal menu bar (outside the canvas) --> | |
| <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> | |
| <!-- Container for dynamic suboptions --> | |
| <div id="subOptionsContainer"></div> | |
| <!-- Edit Panel (opens on double-click on an element) --> | |
| <div id="editPanel"></div> | |
| <!-- Connection Options Panel --> | |
| <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> | |
| <!-- Constraint Edit Panel --> | |
| <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> | |
| <!-- Matter.js Library --> | |
| <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> | |