Spaces:
Running
Running
<html lang="hi"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Responsive Physics Simulator with Flexible Attachments</title> | |
<style> | |
body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; } | |
/* Full-page canvas */ | |
canvas { display: block; } | |
/* Controls Panel – always on top */ | |
#controls { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
z-index: 1000; | |
background: rgba(255,255,255,0.95); | |
padding: 10px; | |
border-radius: 5px; | |
max-width: 320px; | |
max-height: 90vh; | |
overflow-y: auto; | |
} | |
#controls label { display: block; margin-top: 8px; } | |
#controls input, #controls select, #controls button { | |
width: 100%; padding: 6px; margin-top: 4px; box-sizing: border-box; | |
} | |
/* Edit Panel – appears on double–click */ | |
#editPanel { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
z-index: 1000; | |
background: rgba(255,255,255,0.95); | |
padding: 10px; | |
border-radius: 5px; | |
max-width: 300px; | |
max-height: 90vh; | |
overflow-y: auto; | |
display: none; | |
} | |
#editPanel h3 { margin: 0 0 5px 0; } | |
#editPanel label { display: block; margin-top: 8px; } | |
#editPanel input, #editPanel select, #editPanel button { | |
width: 100%; padding: 6px; margin-top: 4px; box-sizing: border-box; | |
} | |
/* Connection Panel – for attachments */ | |
#connectionPanel { | |
position: absolute; | |
top: 150px; | |
right: 10px; | |
z-index: 1000; | |
background: rgba(255,255,255,0.95); | |
padding: 10px; | |
border-radius: 5px; | |
max-width: 250px; | |
display: none; | |
} | |
#connectionPanel h3 { margin: 0 0 5px 0; } | |
#connectionPanel label { display: block; margin-top: 8px; } | |
#connectionPanel input, #connectionPanel select, #connectionPanel button { | |
width: 100%; padding: 6px; margin-top: 4px; box-sizing: border-box; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- Controls Panel --> | |
<div id="controls"> | |
<label for="elementType">Element Type:</label> | |
<select id="elementType"> | |
<option value="block">Block (Rectangle)</option> | |
<option value="circle">Circle</option> | |
</select> | |
<label for="initX">Initial X:</label> | |
<input type="number" id="initX" value="150"> | |
<label for="initY">Initial Y:</label> | |
<input type="number" id="initY" value="100"> | |
<!-- For block --> | |
<div id="blockOptions"> | |
<label for="blockWidth">Width:</label> | |
<input type="number" id="blockWidth" value="80"> | |
<label for="blockHeight">Height:</label> | |
<input type="number" id="blockHeight" value="60"> | |
</div> | |
<!-- For circle --> | |
<div id="circleOptions" style="display:none;"> | |
<label for="circleRadius">Radius:</label> | |
<input type="number" id="circleRadius" value="30"> | |
</div> | |
<label for="initColor">Color:</label> | |
<input type="color" id="initColor" value="#3498db"> | |
<button id="addElement">Add Element</button> | |
<button id="reset">Reset Simulation</button> | |
</div> | |
<!-- Edit Panel (opens on double–click) --> | |
<div id="editPanel"></div> | |
<!-- Connection Panel (for attachments) --> | |
<div id="connectionPanel"> | |
<h3>Attachment Options</h3> | |
<label for="connType">Connection Type:</label> | |
<select id="connType"> | |
<option value="spring">Spring</option> | |
<option value="string">String</option> | |
</select> | |
<button id="cancelConn">Cancel Attachment</button> | |
<p style="font-size:12px; color:#555;">Ab target element par click karein.</p> | |
</div> | |
<!-- Matter.js Library --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script> | |
<script> | |
// Module aliases | |
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; | |
// Create engine and world | |
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); | |
// Global boundaries (will be updated on resize) | |
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 for drag & drop | |
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 & attachment | |
let selectedBody = null; | |
let attachMode = false; | |
let attachFrom = null; | |
const editPanel = document.getElementById("editPanel"); | |
const connectionPanel = document.getElementById("connectionPanel"); | |
// Helper: random color | |
function getRandomColor() { | |
return '#' + Math.floor(Math.random()*16777215).toString(16); | |
} | |
// Toggle suboptions based on element type | |
const elementTypeSelect = document.getElementById("elementType"); | |
elementTypeSelect.addEventListener("change", function(){ | |
if(this.value === "block"){ | |
document.getElementById("blockOptions").style.display = "block"; | |
document.getElementById("circleOptions").style.display = "none"; | |
} else { | |
document.getElementById("blockOptions").style.display = "none"; | |
document.getElementById("circleOptions").style.display = "block"; | |
} | |
}); | |
// Functions to add elements | |
function addBlock(){ | |
const x = parseFloat(document.getElementById("initX").value); | |
const y = parseFloat(document.getElementById("initY").value); | |
const width = parseFloat(document.getElementById("blockWidth").value); | |
const height = parseFloat(document.getElementById("blockHeight").value); | |
const color = document.getElementById("initColor").value || getRandomColor(); | |
const block = Bodies.rectangle(x, y, width, height, { | |
restitution: 0.1, | |
friction: 0.5, | |
render: { fillStyle: color } | |
}); | |
// Store creation options for later editing | |
block.customOptions = { x, y, width, height, color }; | |
block.elementType = "block"; | |
World.add(world, block); | |
} | |
function addCircle(){ | |
const x = parseFloat(document.getElementById("initX").value); | |
const y = parseFloat(document.getElementById("initY").value); | |
const radius = parseFloat(document.getElementById("circleRadius").value); | |
const color = document.getElementById("initColor").value || getRandomColor(); | |
const circle = Bodies.circle(x, y, radius, { | |
restitution: 0.9, | |
friction: 0.005, | |
render: { fillStyle: color } | |
}); | |
circle.customOptions = { x, y, radius, color }; | |
circle.elementType = "circle"; | |
World.add(world, circle); | |
} | |
document.getElementById("addElement").addEventListener("click", function(){ | |
if(elementTypeSelect.value === "block") addBlock(); | |
else addCircle(); | |
}); | |
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(); | |
}); | |
// Edit Panel functions – open on double–click | |
function openEditPanel(body) { | |
selectedBody = body; | |
let html = `<h3>Edit Element</h3> | |
<label>Position X:</label> | |
<input type="number" id="editPosX" value="${body.position.x.toFixed(2)}"> | |
<label>Position Y:</label> | |
<input type="number" id="editPosY" value="${body.position.y.toFixed(2)}"> | |
<label>Angle (radians):</label> | |
<input type="number" id="editAngle" value="${body.angle.toFixed(2)}"> | |
<label>Color:</label> | |
<input type="color" id="editColor" value="${body.render.fillStyle || '#ffffff'}"> | |
<button id="updateElement">Update</button> | |
<button id="deleteElement">Delete</button> | |
<button id="attachElement">Attach</button>`; | |
editPanel.innerHTML = html; | |
editPanel.style.display = "block"; | |
document.getElementById("updateElement").addEventListener("click", updateElementFromEditPanel); | |
document.getElementById("deleteElement").addEventListener("click", function(){ | |
World.remove(world, selectedBody); | |
hideEditPanel(); | |
}); | |
document.getElementById("attachElement").addEventListener("click", function(){ | |
attachMode = true; | |
attachFrom = selectedBody; | |
hideEditPanel(); | |
connectionPanel.style.display = "block"; | |
}); | |
} | |
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; | |
hideEditPanel(); | |
} | |
// Open edit panel on double–click (using canvas dblclick) | |
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(); } | |
}); | |
// Attachment (Connection) creation: | |
// When attachMode is true and user clicks on a target body, | |
// create a constraint between attachFrom and that body using selected connection type. | |
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(attachMode && attachFrom && clickedBodies.length > 0) { | |
const target = clickedBodies[0]; | |
if(target === attachFrom) return; | |
const connType = document.getElementById("connType").value; | |
// Default endpoints: attach from centers | |
const pointA = { x: 0, y: 0 }; | |
const pointB = { x: 0, y: 0 }; | |
// Compute current distance between centers | |
const dx = target.position.x - attachFrom.position.x; | |
const dy = target.position.y - attachFrom.position.y; | |
const length = Math.sqrt(dx*dx + dy*dy); | |
// Set stiffness based on connection type: | |
let stiffness = (connType === "spring") ? 0.05 : 1; | |
let damping = (connType === "spring") ? 0.05 : 0; | |
const newConstraint = Constraint.create({ | |
bodyA: attachFrom, | |
pointA: pointA, | |
bodyB: target, | |
pointB: pointB, | |
length: length, | |
stiffness: stiffness, | |
damping: damping, | |
render: { strokeStyle: '#000', lineWidth: 2 } | |
}); | |
World.add(world, newConstraint); | |
attachMode = false; | |
attachFrom = null; | |
connectionPanel.style.display = "none"; | |
alert("Attachment created (" + connType + ")."); | |
} | |
}); | |
document.getElementById("cancelConn").addEventListener("click", function(){ | |
connectionPanel.style.display = "none"; | |
attachMode = false; | |
attachFrom = null; | |
}); | |
// Update boundaries and renderer on window resize | |
window.addEventListener("resize", function(){ | |
Render.lookAt(render, { min: { x: 0, y: 0 }, max: { x: window.innerWidth, y: window.innerHeight } }); | |
updateBoundaries(); | |
}); | |
</script> | |
</body> | |
</html> | |