Created
April 9, 2025 10:03
-
-
Save shricodev/9ac15a07a3a35473b689819c5f64cf91 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html> | |
| <head> | |
| <title>Physics Ball Sandbox</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| touch-action: none; | |
| font-family: Arial, sans-serif; | |
| } | |
| canvas { | |
| display: block; | |
| background-color: #f0f0f0; | |
| } | |
| #controls { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background: rgba(255, 255, 255, 0.7); | |
| padding: 10px; | |
| border-radius: 5px; | |
| } | |
| #instructions { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| background: rgba(255, 255, 255, 0.7); | |
| padding: 10px; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="canvas"></canvas> | |
| <div id="controls"> | |
| <div> | |
| <label | |
| >Ball Size: | |
| <input type="range" id="sizeSlider" min="10" max="50" value="20" | |
| /></label> | |
| <span id="sizeValue">20</span> | |
| </div> | |
| <div> | |
| <label | |
| >Launch Speed: | |
| <input type="range" id="speedSlider" min="0" max="20" value="5" | |
| /></label> | |
| <span id="speedValue">5</span> | |
| </div> | |
| <button id="clearBtn">Clear All Balls</button> | |
| </div> | |
| <div id="instructions"> | |
| <strong>Instructions:</strong><br /> | |
| - Click to place a ball<br /> | |
| - Click + drag to launch a ball in that direction<br /> | |
| - Drag an existing ball to reposition it<br /> | |
| - Adjust sliders to change ball size and launch speed | |
| </div> | |
| <script> | |
| const canvas = document.getElementById("canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| const sizeSlider = document.getElementById("sizeSlider"); | |
| const sizeValue = document.getElementById("sizeValue"); | |
| const speedSlider = document.getElementById("speedSlider"); | |
| const speedValue = document.getElementById("speedValue"); | |
| const clearBtn = document.getElementById("clearBtn"); | |
| // Set canvas to full window size | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Physics parameters | |
| const gravity = 0.5; | |
| const friction = 0.99; | |
| const restitution = 0.8; // Bounciness | |
| // Ball collection | |
| let balls = []; | |
| let currentBall = null; | |
| let isDragging = false; | |
| let dragStart = { x: 0, y: 0 }; | |
| let selectedBallIndex = -1; | |
| // Initialize UI values | |
| sizeValue.textContent = sizeSlider.value; | |
| speedValue.textContent = speedSlider.value; | |
| // Ball class | |
| class Ball { | |
| constructor(x, y, radius) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = radius; | |
| this.vx = 0; | |
| this.vy = 0; | |
| this.color = `hsl(${Math.random() * 360}, 70%, 60%)`; | |
| } | |
| update() { | |
| // Apply gravity | |
| this.vy += gravity; | |
| // Apply friction | |
| this.vx *= friction; | |
| this.vy *= friction; | |
| // Update position | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| // Boundary collision | |
| if (this.x - this.radius < 0) { | |
| this.x = this.radius; | |
| this.vx *= -restitution; | |
| } else if (this.x + this.radius > canvas.width) { | |
| this.x = canvas.width - this.radius; | |
| this.vx *= -restitution; | |
| } | |
| if (this.y - this.radius < 0) { | |
| this.y = this.radius; | |
| this.vy *= -restitution; | |
| } else if (this.y + this.radius > canvas.height) { | |
| this.y = canvas.height - this.radius; | |
| this.vy *= -restitution; | |
| } | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = this.color; | |
| ctx.fill(); | |
| ctx.strokeStyle = "#333"; | |
| ctx.stroke(); | |
| } | |
| } | |
| // Handle ball-ball collisions | |
| function handleCollisions() { | |
| for (let i = 0; i < balls.length; i++) { | |
| for (let j = i + 1; j < balls.length; j++) { | |
| const ball1 = balls[i]; | |
| const ball2 = balls[j]; | |
| const dx = ball2.x - ball1.x; | |
| const dy = ball2.y - ball1.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < ball1.radius + ball2.radius) { | |
| // Collision detected | |
| const angle = Math.atan2(dy, dx); | |
| const sin = Math.sin(angle); | |
| const cos = Math.cos(angle); | |
| // Rotate velocities | |
| const vx1 = ball1.vx * cos + ball1.vy * sin; | |
| const vy1 = ball1.vy * cos - ball1.vx * sin; | |
| const vx2 = ball2.vx * cos + ball2.vy * sin; | |
| const vy2 = ball2.vy * cos - ball2.vx * sin; | |
| // Conservation of momentum | |
| const m1 = ball1.radius * ball1.radius; // mass proportional to area | |
| const m2 = ball2.radius * ball2.radius; | |
| const vx1Final = ((m1 - m2) * vx1 + 2 * m2 * vx2) / (m1 + m2); | |
| const vx2Final = ((m2 - m1) * vx2 + 2 * m1 * vx1) / (m1 + m2); | |
| // Update velocities | |
| ball1.vx = vx1Final * cos - vy1 * sin; | |
| ball1.vy = vy1 * cos + vx1Final * sin; | |
| ball2.vx = vx2Final * cos - vy2 * sin; | |
| ball2.vy = vy2 * cos + vx2Final * sin; | |
| // Prevent overlap | |
| const overlap = ball1.radius + ball2.radius - distance; | |
| const moveX = overlap * cos * 0.5; | |
| const moveY = overlap * sin * 0.5; | |
| ball1.x -= moveX; | |
| ball1.y -= moveY; | |
| ball2.x += moveX; | |
| ball2.y += moveY; | |
| } | |
| } | |
| } | |
| } | |
| // Animation loop | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Update and draw all balls | |
| balls.forEach((ball) => ball.update()); | |
| handleCollisions(); | |
| balls.forEach((ball) => ball.draw()); | |
| // Draw velocity vector when dragging | |
| if (currentBall && isDragging) { | |
| ctx.beginPath(); | |
| ctx.moveTo(currentBall.x, currentBall.y); | |
| ctx.lineTo(dragStart.x, dragStart.y); | |
| ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| currentBall.draw(); | |
| } | |
| requestAnimationFrame(animate); | |
| } | |
| // UI event handlers | |
| sizeSlider.addEventListener("input", () => { | |
| sizeValue.textContent = sizeSlider.value; | |
| }); | |
| speedSlider.addEventListener("input", () => { | |
| speedValue.textContent = speedSlider.value; | |
| }); | |
| clearBtn.addEventListener("click", () => { | |
| balls = []; | |
| currentBall = null; | |
| }); | |
| // Mouse/touch event handlers | |
| function getMousePos(canvas, evt) { | |
| const rect = canvas.getBoundingClientRect(); | |
| return { | |
| x: (evt.clientX || evt.touches[0].clientX) - rect.left, | |
| y: (evt.clientY || evt.touches[0].clientY) - rect.top, | |
| }; | |
| } | |
| canvas.addEventListener("mousedown", handleStart); | |
| canvas.addEventListener("touchstart", handleStart, { passive: false }); | |
| canvas.addEventListener("mousemove", handleMove); | |
| canvas.addEventListener("touchmove", handleMove, { passive: false }); | |
| canvas.addEventListener("mouseup", handleEnd); | |
| canvas.addEventListener("touchend", handleEnd); | |
| canvas.addEventListener("touchcancel", handleEnd); | |
| function handleStart(e) { | |
| e.preventDefault(); | |
| const pos = getMousePos(canvas, e); | |
| // Check if we're clicking on an existing ball | |
| for (let i = 0; i < balls.length; i++) { | |
| const ball = balls[i]; | |
| const dx = pos.x - ball.x; | |
| const dy = pos.y - ball.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < ball.radius) { | |
| // Start dragging existing ball | |
| selectedBallIndex = i; | |
| isDragging = true; | |
| return; | |
| } | |
| } | |
| // Otherwise, create a new ball | |
| const radius = parseInt(sizeSlider.value); | |
| currentBall = new Ball(pos.x, pos.y, radius); | |
| dragStart = { x: pos.x, y: pos.y }; | |
| isDragging = true; | |
| } | |
| function handleMove(e) { | |
| if (!isDragging) return; | |
| e.preventDefault(); | |
| const pos = getMousePos(canvas, e); | |
| if (selectedBallIndex >= 0) { | |
| // Move existing ball | |
| balls[selectedBallIndex].x = pos.x; | |
| balls[selectedBallIndex].y = pos.y; | |
| balls[selectedBallIndex].vx = 0; | |
| balls[selectedBallIndex].vy = 0; | |
| } else { | |
| // Update drag position for new ball | |
| dragStart = { x: pos.x, y: pos.y }; | |
| } | |
| } | |
| function handleEnd(e) { | |
| if (!isDragging) return; | |
| isDragging = false; | |
| if (selectedBallIndex >= 0) { | |
| // Finished dragging existing ball | |
| selectedBallIndex = -1; | |
| return; | |
| } | |
| if (!currentBall) return; | |
| // Calculate launch velocity | |
| const speed = parseInt(speedSlider.value); | |
| const angle = Math.atan2( | |
| currentBall.y - dragStart.y, | |
| currentBall.x - dragStart.x, | |
| ); | |
| currentBall.vx = Math.cos(angle) * speed; | |
| currentBall.vy = Math.sin(angle) * speed; | |
| // Add to balls array | |
| balls.push(currentBall); | |
| currentBall = null; | |
| } | |
| // Handle window resize | |
| window.addEventListener("resize", () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| // Start animation | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |
Author
Author
Prompt: Develop a Python sandbox simulation where users can add balls of different sizes, launch them with adjustable velocity, and interact by dragging to reposition. Include basic physics to simulate gravity and collisions between balls and boundaries.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output: https://youtu.be/uJktm9A0ROY