|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Bouncing Balls in Circular Boundary</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<style> |
|
body { |
|
overflow: hidden; |
|
background-color: #1a202c; |
|
} |
|
#canvas { |
|
display: block; |
|
margin: 0 auto; |
|
background-color: #2d3748; |
|
} |
|
.controls { |
|
position: absolute; |
|
bottom: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
background-color: rgba(255, 255, 255, 0.1); |
|
padding: 10px; |
|
border-radius: 8px; |
|
backdrop-filter: blur(5px); |
|
} |
|
.ball-count { |
|
position: absolute; |
|
top: 20px; |
|
left: 20px; |
|
color: white; |
|
font-family: 'Arial', sans-serif; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
padding: 8px 12px; |
|
border-radius: 20px; |
|
} |
|
.fps-counter { |
|
position: absolute; |
|
top: 20px; |
|
right: 20px; |
|
color: white; |
|
font-family: 'Arial', sans-serif; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
padding: 8px 12px; |
|
border-radius: 20px; |
|
} |
|
</style> |
|
</head> |
|
<body class="flex items-center justify-center h-screen"> |
|
<div class="relative"> |
|
<canvas id="canvas"></canvas> |
|
<div class="ball-count">Balls: <span id="ballCount">8</span></div> |
|
<div class="fps-counter">FPS: <span id="fpsCounter">0</span></div> |
|
<div class="controls"> |
|
<button id="addBall" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded mr-2 transition">Add Ball</button> |
|
<button id="removeBall" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded mr-2 transition">Remove Ball</button> |
|
<button id="reset" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded transition">Reset</button> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
const canvas = document.getElementById('canvas'); |
|
const ctx = canvas.getContext('2d'); |
|
const ballCountDisplay = document.getElementById('ballCount'); |
|
const fpsCounter = document.getElementById('fpsCounter'); |
|
const addBallBtn = document.getElementById('addBall'); |
|
const removeBallBtn = document.getElementById('removeBall'); |
|
const resetBtn = document.getElementById('reset'); |
|
|
|
|
|
canvas.width = 800; |
|
canvas.height = 800; |
|
|
|
|
|
const boundaryRadius = 350; |
|
const boundaryCenter = { x: canvas.width / 2, y: canvas.height / 2 }; |
|
const minRadius = 15; |
|
const maxRadius = 25; |
|
const minSpeed = 1; |
|
const maxSpeed = 4; |
|
const colors = [ |
|
'#FF5252', '#FF4081', '#E040FB', '#7C4DFF', |
|
'#536DFE', '#448AFF', '#40C4FF', '#18FFFF', |
|
'#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41', |
|
'#FFFF00', '#FFD740', '#FFAB40', '#FF6E40' |
|
]; |
|
|
|
|
|
const friction = 0.995; |
|
const bounceFactor = 0.95; |
|
|
|
|
|
class Ball { |
|
constructor(x, y, radius, color, dx, dy) { |
|
this.x = x; |
|
this.y = y; |
|
this.radius = radius; |
|
this.color = color; |
|
this.dx = dx; |
|
this.dy = dy; |
|
this.mass = radius * radius; |
|
} |
|
|
|
update() { |
|
|
|
this.dx *= friction; |
|
this.dy *= friction; |
|
|
|
|
|
this.x += this.dx; |
|
this.y += this.dy; |
|
|
|
|
|
const distanceFromCenter = Math.sqrt( |
|
Math.pow(this.x - boundaryCenter.x, 2) + |
|
Math.pow(this.y - boundaryCenter.y, 2) |
|
); |
|
|
|
if (distanceFromCenter + this.radius > boundaryRadius) { |
|
|
|
const nx = (this.x - boundaryCenter.x) / distanceFromCenter; |
|
const ny = (this.y - boundaryCenter.y) / distanceFromCenter; |
|
|
|
|
|
const dotProduct = this.dx * nx + this.dy * ny; |
|
|
|
|
|
this.dx = (this.dx - 2 * dotProduct * nx) * bounceFactor; |
|
this.dy = (this.dy - 2 * dotProduct * ny) * bounceFactor; |
|
|
|
|
|
const correction = boundaryRadius - this.radius; |
|
this.x = boundaryCenter.x + nx * correction; |
|
this.y = boundaryCenter.y + ny * correction; |
|
} |
|
} |
|
|
|
draw() { |
|
ctx.beginPath(); |
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); |
|
ctx.fillStyle = this.color; |
|
ctx.fill(); |
|
ctx.closePath(); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.arc(this.x - this.radius/3, this.y - this.radius/3, this.radius/3, 0, Math.PI * 2); |
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; |
|
ctx.fill(); |
|
ctx.closePath(); |
|
} |
|
} |
|
|
|
|
|
let balls = []; |
|
|
|
|
|
function initBalls(count) { |
|
balls = []; |
|
for (let i = 0; i < count; i++) { |
|
addRandomBall(); |
|
} |
|
ballCountDisplay.textContent = balls.length; |
|
} |
|
|
|
|
|
function addRandomBall() { |
|
const radius = minRadius + Math.random() * (maxRadius - minRadius); |
|
|
|
|
|
let angle = Math.random() * Math.PI * 2; |
|
let distance = Math.random() * (boundaryRadius - radius); |
|
const x = boundaryCenter.x + Math.cos(angle) * distance; |
|
const y = boundaryCenter.y + Math.sin(angle) * distance; |
|
|
|
|
|
const speed = minSpeed + Math.random() * (maxSpeed - minSpeed); |
|
angle = Math.random() * Math.PI * 2; |
|
const dx = Math.cos(angle) * speed; |
|
const dy = Math.sin(angle) * speed; |
|
|
|
|
|
const color = colors[Math.floor(Math.random() * colors.length)]; |
|
|
|
balls.push(new Ball(x, y, radius, color, dx, dy)); |
|
ballCountDisplay.textContent = balls.length; |
|
} |
|
|
|
|
|
function checkCollisions() { |
|
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) { |
|
|
|
const angle = Math.atan2(dy, dx); |
|
|
|
|
|
const velocity1 = Math.sqrt(ball1.dx * ball1.dx + ball1.dy * ball1.dy); |
|
const velocity2 = Math.sqrt(ball2.dx * ball2.dx + ball2.dy * ball2.dy); |
|
|
|
const direction1 = Math.atan2(ball1.dy, ball1.dx); |
|
const direction2 = Math.atan2(ball2.dy, ball2.dx); |
|
|
|
const velocityX1 = velocity1 * Math.cos(direction1 - angle); |
|
const velocityY1 = velocity1 * Math.sin(direction1 - angle); |
|
const velocityX2 = velocity2 * Math.cos(direction2 - angle); |
|
const velocityY2 = velocity2 * Math.sin(direction2 - angle); |
|
|
|
|
|
const finalVelocityX1 = ((ball1.mass - ball2.mass) * velocityX1 + 2 * ball2.mass * velocityX2) / (ball1.mass + ball2.mass); |
|
const finalVelocityX2 = ((ball2.mass - ball1.mass) * velocityX2 + 2 * ball1.mass * velocityX1) / (ball1.mass + ball2.mass); |
|
|
|
|
|
ball1.dx = Math.cos(angle) * finalVelocityX1 + Math.cos(angle + Math.PI/2) * velocityY1; |
|
ball1.dy = Math.sin(angle) * finalVelocityX1 + Math.sin(angle + Math.PI/2) * velocityY1; |
|
ball2.dx = Math.cos(angle) * finalVelocityX2 + Math.cos(angle + Math.PI/2) * velocityY2; |
|
ball2.dy = Math.sin(angle) * finalVelocityX2 + Math.sin(angle + Math.PI/2) * velocityY2; |
|
|
|
|
|
const overlap = (ball1.radius + ball2.radius - distance) / 2; |
|
ball1.x -= overlap * Math.cos(angle); |
|
ball1.y -= overlap * Math.sin(angle); |
|
ball2.x += overlap * Math.cos(angle); |
|
ball2.y += overlap * Math.sin(angle); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
let lastTime = 0; |
|
let fps = 0; |
|
function animate(currentTime) { |
|
|
|
if (lastTime) { |
|
fps = Math.round(1000 / (currentTime - lastTime)); |
|
} |
|
lastTime = currentTime; |
|
fpsCounter.textContent = fps; |
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.arc(boundaryCenter.x, boundaryCenter.y, boundaryRadius, 0, Math.PI * 2); |
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; |
|
ctx.lineWidth = 2; |
|
ctx.stroke(); |
|
|
|
|
|
ctx.beginPath(); |
|
ctx.arc(boundaryCenter.x, boundaryCenter.y, 3, 0, Math.PI * 2); |
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; |
|
ctx.fill(); |
|
|
|
|
|
checkCollisions(); |
|
balls.forEach(ball => { |
|
ball.update(); |
|
ball.draw(); |
|
}); |
|
|
|
requestAnimationFrame(animate); |
|
} |
|
|
|
|
|
initBalls(8); |
|
|
|
|
|
animate(); |
|
|
|
|
|
addBallBtn.addEventListener('click', () => { |
|
if (balls.length < 30) { |
|
addRandomBall(); |
|
} |
|
}); |
|
|
|
removeBallBtn.addEventListener('click', () => { |
|
if (balls.length > 1) { |
|
balls.pop(); |
|
ballCountDisplay.textContent = balls.length; |
|
} |
|
}); |
|
|
|
resetBtn.addEventListener('click', () => { |
|
initBalls(8); |
|
}); |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
const size = Math.min(window.innerWidth, window.innerHeight) * 0.9; |
|
canvas.width = size; |
|
canvas.height = size; |
|
boundaryCenter.x = canvas.width / 2; |
|
boundaryCenter.y = canvas.height / 2; |
|
}); |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=aryansrk/traul" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |