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> |