Physics-Playground / index.html
fantos's picture
Update index.html
362d9b7 verified
<!DOCTYPE html>
<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>