Spaces:
Running
Running
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Advanced Physics Simulator</title> | |
<style> | |
:root { | |
--primary: #00aaff; | |
--background: #0a0a0a; | |
--surface: #1a1a1a; | |
--text: #ffffff; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
user-select: none; | |
} | |
body { | |
background: var(--background); | |
color: var(--text); | |
font-family: system-ui, -apple-system, sans-serif; | |
overflow: hidden; | |
} | |
#app { | |
display: grid; | |
grid-template-columns: 1fr 300px; | |
height: 100vh; | |
} | |
#viewport { | |
position: relative; | |
overflow: hidden; | |
} | |
#canvas { | |
background: #000; | |
position: absolute; | |
} | |
#overlay { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
background: rgba(0,0,0,0.8); | |
padding: 10px; | |
border-radius: 4px; | |
font-family: monospace; | |
} | |
#controls { | |
background: var(--surface); | |
padding: 20px; | |
overflow-y: auto; | |
} | |
.panel { | |
background: rgba(255,255,255,0.05); | |
border-radius: 8px; | |
padding: 15px; | |
margin-bottom: 15px; | |
} | |
.panel h3 { | |
color: var(--primary); | |
margin-bottom: 15px; | |
} | |
.control-row { | |
display: flex; | |
align-items: center; | |
margin: 8px 0; | |
gap: 10px; | |
} | |
label { | |
flex: 1; | |
} | |
input[type="range"] { | |
width: 120px; | |
-webkit-appearance: none; | |
height: 4px; | |
background: #333; | |
border-radius: 2px; | |
} | |
input[type="range"]::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
width: 16px; | |
height: 16px; | |
background: var(--primary); | |
border-radius: 50%; | |
cursor: pointer; | |
} | |
input[type="number"] { | |
width: 70px; | |
padding: 4px; | |
background: #333; | |
border: 1px solid #444; | |
color: #fff; | |
border-radius: 4px; | |
} | |
button { | |
background: var(--primary); | |
color: #fff; | |
border: none; | |
padding: 8px 16px; | |
border-radius: 4px; | |
cursor: pointer; | |
font-weight: 500; | |
} | |
button:hover { | |
filter: brightness(1.1); | |
} | |
button:active { | |
transform: translateY(1px); | |
} | |
.button-group { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 8px; | |
margin-top: 10px; | |
} | |
#graphs { | |
display: grid; | |
grid-template-columns: 1fr; | |
gap: 10px; | |
margin-top: 10px; | |
} | |
.graph { | |
height: 100px; | |
background: #111; | |
border-radius: 4px; | |
} | |
@media (max-width: 768px) { | |
#app { | |
grid-template-columns: 1fr; | |
} | |
#controls { | |
position: fixed; | |
bottom: 0; | |
width: 100%; | |
height: 200px; | |
padding: 10px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div id="app"> | |
<div id="viewport"> | |
<canvas id="canvas"></canvas> | |
<div id="overlay"> | |
<div>FPS: <span id="fps">60</span></div> | |
<div>Objects: <span id="objectCount">0</span></div> | |
<div>Time Scale: <span id="timeScale">1.0x</span></div> | |
</div> | |
</div> | |
<div id="controls"> | |
<div class="panel"> | |
<h3>Simulation Controls</h3> | |
<div class="button-group"> | |
<button id="playPause">Pause</button> | |
<button id="reset">Reset</button> | |
<button id="slowMotion">Slow Motion</button> | |
<button id="step">Step Frame</button> | |
</div> | |
</div> | |
<div class="panel"> | |
<h3>Physics Settings</h3> | |
<div class="control-row"> | |
<label>Gravity (m/sΒ²)</label> | |
<input type="range" id="gravity" min="0" max="20" step="0.1" value="9.8"> | |
<span id="gravityValue">9.8</span> | |
</div> | |
<div class="control-row"> | |
<label>Air Resistance</label> | |
<input type="range" id="airResistance" min="0" max="1" step="0.01" value="0.02"> | |
<span id="airValue">0.02</span> | |
</div> | |
<div class="control-row"> | |
<label>Elasticity</label> | |
<input type="range" id="elasticity" min="0" max="1" step="0.1" value="0.8"> | |
<span id="elasticityValue">0.8</span> | |
</div> | |
</div> | |
<div class="panel"> | |
<h3>Object Properties</h3> | |
<div class="control-row"> | |
<label>Mass (kg)</label> | |
<input type="number" id="mass" value="1" min="0.1" step="0.1"> | |
</div> | |
<div class="control-row"> | |
<label>Initial Velocity X</label> | |
<input type="number" id="velocityX" value="0" step="0.1"> | |
</div> | |
<div class="control-row"> | |
<label>Initial Velocity Y</label> | |
<input type="number" id="velocityY" value="0" step="0.1"> | |
</div> | |
<div class="button-group"> | |
<button id="addObject">Add Object</button> | |
<button id="clearAll">Clear All</button> | |
</div> | |
</div> | |
<div class="panel"> | |
<h3>Visualization</h3> | |
<div class="button-group"> | |
<button id="toggleVectors">Toggle Vectors</button> | |
<button id="toggleTrails">Toggle Trails</button> | |
<button id="toggleGraph">Toggle Graphs</button> | |
</div> | |
<div id="graphs"> | |
<canvas class="graph" id="velocityGraph"></canvas> | |
<canvas class="graph" id="energyGraph"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Physics Engine Module | |
const PhysicsEngine = { | |
gravity: 9.8, | |
airResistance: 0.02, | |
elasticity: 0.8, | |
timeScale: 1.0, | |
paused: false, | |
update(objects, dt) { | |
if (this.paused) return; | |
dt *= this.timeScale; | |
objects.forEach(obj => { | |
if (!obj.static) { | |
// Apply forces | |
obj.vy += this.gravity * dt; | |
obj.vx *= (1 - this.airResistance); | |
obj.vy *= (1 - this.airResistance); | |
// Update position | |
obj.x += obj.vx * dt; | |
obj.y += obj.vy * dt; | |
// Rotation | |
obj.angle += obj.angularVelocity * dt; | |
// Store trail points | |
if (obj.trails) { | |
obj.trails.push({x: obj.x, y: obj.y}); | |
if (obj.trails.length > 50) obj.trails.shift(); | |
} | |
} | |
}); | |
// Collision detection | |
this.handleCollisions(objects); | |
}, | |
handleCollisions(objects) { | |
for (let i = 0; i < objects.length; i++) { | |
const obj1 = objects[i]; | |
// Wall collisions | |
if (obj1.x < obj1.radius) { | |
obj1.x = obj1.radius; | |
obj1.vx *= -this.elasticity; | |
} | |
if (obj1.x > canvas.width - obj1.radius) { | |
obj1.x = canvas.width - obj1.radius; | |
obj1.vx *= -this.elasticity; | |
} | |
if (obj1.y < obj1.radius) { | |
obj1.y = obj1.radius; | |
obj1.vy *= -this.elasticity; | |
} | |
if (obj1.y > canvas.height - obj1.radius) { | |
obj1.y = canvas.height - obj1.radius; | |
obj1.vy *= -this.elasticity; | |
} | |
// Object collisions | |
for (let j = i + 1; j < objects.length; j++) { | |
const obj2 = objects[j]; | |
const dx = obj2.x - obj1.x; | |
const dy = obj2.y - obj1.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < obj1.radius + obj2.radius) { | |
const angle = Math.atan2(dy, dx); | |
const sin = Math.sin(angle); | |
const cos = Math.cos(angle); | |
// Elastic collision response | |
const v1 = Math.sqrt(obj1.vx * obj1.vx + obj1.vy * obj1.vy); | |
const v2 = Math.sqrt(obj2.vx * obj2.vx + obj2.vy * obj2.vy); | |
obj1.vx = ((obj1.mass - obj2.mass) * v1 + 2 * obj2.mass * v2) / | |
(obj1.mass + obj2.mass) * cos; | |
obj1.vy = ((obj1.mass - obj2.mass) * v1 + 2 * obj2.mass * v2) / | |
(obj1.mass + obj2.mass) * sin; | |
obj2.vx = ((obj2.mass - obj1.mass) * v2 + 2 * obj1.mass * v1) / | |
(obj1.mass + obj2.mass) * -cos; | |
obj2.vy = ((obj2.mass - obj1.mass) * v2 + 2 * obj1.mass * v1) / | |
(obj1.mass + obj2.mass) * -sin; | |
} | |
} | |
} | |
} | |
}; | |
// Renderer Module | |
const Renderer = { | |
showVectors: true, | |
showTrails: true, | |
clear(ctx) { | |
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); | |
}, | |
drawObject(ctx, obj) { | |
// Draw trails | |
if (this.showTrails && obj.trails) { | |
ctx.beginPath(); | |
obj.trails.forEach((pos, i) => { | |
if (i === 0) { | |
ctx.moveTo(pos.x, pos.y); | |
} else { | |
ctx.lineTo(pos.x, pos.y); | |
} | |
}); | |
ctx.strokeStyle = obj.color + '40'; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
} | |
// Draw object | |
ctx.save(); | |
ctx.translate(obj.x, obj.y); | |
ctx.rotate(obj.angle); | |
ctx.beginPath(); | |
ctx.arc(0, 0, obj.radius, 0, Math.PI * 2); | |
ctx.fillStyle = obj.color; | |
ctx.fill(); | |
// Draw direction indicator | |
ctx.beginPath(); | |
ctx.moveTo(0, 0); | |
ctx.lineTo(obj.radius, 0); | |
ctx.strokeStyle = '#fff'; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
ctx.restore(); | |
// Draw vectors | |
if (this.showVectors) { | |
ctx.beginPath(); | |
ctx.moveTo(obj.x, obj.y); | |
ctx.lineTo(obj.x + obj.vx * 5, obj.y + obj.vy * 5); | |
ctx.strokeStyle = '#ff0'; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
} | |
}, | |
drawGraph(ctx, data, color) { | |
ctx.beginPath(); | |
data.forEach((value, i) => { | |
ctx.lineTo(i * 2, ctx.canvas.height - value); | |
}); | |
ctx.strokeStyle = color; | |
ctx.stroke(); | |
} | |
}; | |
// Main Application | |
class PhysicsSimulator { | |
constructor() { | |
this.canvas = document.getElementById('canvas'); | |
this.ctx = this.canvas.getContext('2d'); | |
this.objects = []; | |
this.velocityData = []; | |
this.energyData = []; | |
this.setupCanvas(); | |
this.setupEventListeners(); | |
this.startAnimation(); | |
} | |
setupCanvas() { | |
const resize = () => { | |
const container = this.canvas.parentElement; | |
this.canvas.width = container.offsetWidth; | |
this.canvas.height = container.offsetHeight; | |
}; | |
window.addEventListener('resize', resize); | |
resize(); | |
} | |
setupEventListeners() { | |
// Physics controls | |
document.getElementById('gravity').oninput = (e) => { | |
PhysicsEngine.gravity = parseFloat(e.target.value); | |
document.getElementById('gravityValue').textContent = e.target.value; | |
}; | |
document.getElementById('airResistance').oninput = (e) => { | |
PhysicsEngine.airResistance = parseFloat(e.target.value); | |
document.getElementById('airValue').textContent = e.target.value; | |
}; | |
document.getElementById('elasticity').oninput = (e) => { | |
PhysicsEngine.elasticity = parseFloat(e.target.value); | |
document.getElementById('elasticityValue').textContent = e.target.value; | |
}; | |
// Simulation controls | |
document.getElementById('playPause').onclick = () => { | |
PhysicsEngine.paused = !PhysicsEngine.paused; | |
document.getElementById('playPause').textContent = | |
PhysicsEngine.paused ? 'Play' : 'Pause'; | |
}; | |
document.getElementById('slowMotion').onclick = () => { | |
PhysicsEngine.timeScale = PhysicsEngine.timeScale === 1 ? 0.2 : 1; | |
document.getElementById('timeScale').textContent = | |
PhysicsEngine.timeScale + 'x'; | |
}; | |
// Object controls | |
this.canvas.onclick = (e) => { | |
const rect = this.canvas.getBoundingClientRect(); | |
const x = e.clientX - rect.left; | |
const y = e.clientY - rect.top; | |
this.addObject(x, y); | |
}; | |
document.getElementById('addObject').onclick = () => { | |
this.addObject( | |
Math.random() * this.canvas.width, | |
Math.random() * this.canvas.height | |
); | |
}; | |
document.getElementById('clearAll').onclick = () => { | |
this.objects = []; | |
}; | |
// Visualization controls | |
document.getElementById('toggleVectors').onclick = () => { | |
Renderer.showVectors = !Renderer.showVectors; | |
}; | |
document.getElementById('toggleTrails').onclick = () => { | |
Renderer.showTrails = !Renderer.showTrails; | |
}; | |
} | |
addObject(x, y) { | |
const mass = parseFloat(document.getElementById('mass').value); | |
const vx = parseFloat(document.getElementById('velocityX').value); | |
const vy = parseFloat(document.getElementById('velocityY').value); | |
this.objects.push({ | |
x: x, | |
y: y, | |
vx: vx, | |
vy: vy, | |
mass: mass, | |
radius: Math.sqrt(mass) * 10, | |
angle: 0, | |
angularVelocity: Math.random() * 2 - 1, | |
color: `hsl(${Math.random() * 360}, 70%, 50%)`, | |
trails: [] | |
}); | |
} | |
update(dt) { | |
PhysicsEngine.update(this.objects, dt); | |
// Update statistics | |
document.getElementById('objectCount').textContent = this.objects.length; | |
// Store data for graphs | |
const totalVelocity = this.objects.reduce((sum, obj) => | |
sum + Math.sqrt(obj.vx * obj.vx + obj.vy * obj.vy), 0); | |
const totalEnergy = this.objects.reduce((sum, obj) => | |
sum + 0.5 * obj.mass * (obj.vx * obj.vx + obj.vy * obj.vy), 0); | |
this.velocityData.push(totalVelocity); | |
this.energyData.push(totalEnergy); | |
if (this.velocityData.length > 100) this.velocityData.shift(); | |
if (this.energyData.length > 100) this.energyData.shift(); | |
} | |
render() { | |
Renderer.clear(this.ctx); | |
this.objects.forEach(obj => Renderer.drawObject(this.ctx, obj)); | |
} | |
startAnimation() { | |
let lastTime = performance.now(); | |
let frames = 0; | |
let fpsTime = 0; | |
const animate = (currentTime) => { | |
const dt = (currentTime - lastTime) / 1000; | |
lastTime = currentTime; | |
// Calculate FPS | |
frames++; | |
if (currentTime - fpsTime > 1000) { | |
document.getElementById('fps').textContent = frames; | |
frames = 0; | |
fpsTime = currentTime; | |
} | |
this.update(dt); | |
this.render(); | |
requestAnimationFrame(animate); | |
}; | |
requestAnimationFrame(animate); | |
} | |
} | |
// Initialize application | |
const app = new PhysicsSimulator(); | |
</script> | |
</body> | |
</html><script async data-explicit-opt-in="true" data-cookie-opt-in="true" src="https://vercel.live/_next-live/feedback/feedback.js"></script> |