| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Soft-Body Physics Sandbox</title> |
| <style> |
| body { margin: 0; overflow: hidden; background: #000; } |
| canvas { display: block; background: #f5f5f5; } |
| .toolbar { position: absolute; top: 10px; left: 10px; display: flex; gap: 8px; } |
| .toolbar button { padding: 8px 16px; border: none; border-radius: 6px; background: #4CAF50; color: white; cursor: pointer; } |
| .toolbar button:hover { background: #45a049; } |
| </style> |
| </head> |
| <body> |
| <canvas id="canvas"></canvas> |
| <div class="toolbar"> |
| <button id="add-circle">Add Circle</button> |
| <button id="clear">Clear</button> |
| <button id="toggle-walls">Toggle Walls</button> |
| <button id="gravity-dir">Gravity Direction</button> |
| </div> |
|
|
| <script> |
| const canvas = document.getElementById('canvas'); |
| const ctx = canvas.getContext('2d'); |
| let width = window.innerWidth; |
| let height = window.innerHeight; |
| canvas.width = width; |
| canvas.height = height; |
| |
| let circles = []; |
| let gravity = { x: 0, y: 0.3 }; |
| let walls = true; |
| let dragCircle = null; |
| let dragOffset = { x: 0, y: 0 }; |
| |
| const pastelColors = ['#FFB3BA', '#FFDFBA', '#FFFFBA', '#BAFFC9', '#BAE1FF', '#E6BAFF']; |
| |
| class Circle { |
| constructor(x, y, radius, color) { |
| this.x = x; |
| this.y = y; |
| this.radius = radius; |
| this.color = color; |
| this.vx = 0; |
| this.vy = 0; |
| this.deformation = 0; |
| } |
| |
| update(dt) { |
| this.vy += gravity.y * dt; |
| this.vx += gravity.x * dt; |
| this.x += this.vx * dt; |
| this.y += this.vy * dt; |
| |
| if (walls) { |
| this.handleWallCollisions(); |
| } |
| |
| this.deformation = Math.max(0, this.deformation - 0.1 * dt); |
| } |
| |
| handleWallCollisions() { |
| if (this.x - this.radius < 0) { |
| this.x = this.radius; |
| this.vx *= -0.8; |
| } |
| if (this.x + this.radius > width) { |
| this.x = width - this.radius; |
| this.vx *= -0.8; |
| } |
| if (this.y - this.radius < 0) { |
| this.y = this.radius; |
| this.vy *= -0.8; |
| } |
| if (this.y + this.radius > height) { |
| this.y = height - this.radius; |
| this.vy *= -0.8; |
| } |
| } |
| |
| draw() { |
| ctx.beginPath(); |
| ctx.arc(this.x, this.y, this.radius + this.deformation, 0, Math.PI * 2); |
| ctx.fillStyle = this.color; |
| ctx.fill(); |
| ctx.closePath(); |
| } |
| } |
| |
| function handleCircleCollisions() { |
| for (let i = 0; i < circles.length; i++) { |
| for (let j = i + 1; j < circles.length; j++) { |
| const dx = circles[j].x - circles[i].x; |
| const dy = circles[j].y - circles[i].y; |
| const dist = Math.sqrt(dx * dx + dy * dy); |
| if (dist < circles[i].radius + circles[j].radius) { |
| const nx = dx / dist; |
| const ny = dy / dist; |
| const overlap = (circles[i].radius + circles[j].radius) - dist; |
| const separation = overlap / 2; |
| circles[i].x -= nx * separation; |
| circles[i].y -= ny * separation; |
| circles[j].x += nx * separation; |
| circles[j].y += ny * separation; |
| |
| const v1 = { x: circles[i].vx, y: circles[i].vy }; |
| const v2 = { x: circles[j].vx, y: circles[j].vy }; |
| const dot = v1.x * nx + v1.y * ny; |
| const dot2 = v2.x * nx + v2.y * ny; |
| const impulse = 2 * (dot - dot2) / (circles[i].radius + circles[j].radius); |
| circles[i].vx -= impulse * nx; |
| circles[i].vy -= impulse * ny; |
| circles[j].vx += impulse * nx; |
| circles[j].vy += impulse * ny; |
| |
| circles[i].deformation += 5; |
| circles[j].deformation += 5; |
| } |
| } |
| } |
| } |
| |
| function addCircle() { |
| const radius = 20 + Math.random() * 15; |
| const color = pastelColors[Math.floor(Math.random() * pastelColors.length)]; |
| const x = Math.random() * (width - radius * 2) + radius; |
| const y = Math.random() * (height - radius * 2) + radius; |
| circles.push(new Circle(x, y, radius, color)); |
| } |
| |
| function clearCircles() { |
| circles = []; |
| } |
| |
| function toggleWalls() { |
| walls = !walls; |
| } |
| |
| function rotateGravity() { |
| const angle = Math.atan2(gravity.y, gravity.x) + Math.PI / 2; |
| gravity.x = Math.cos(angle) * 0.3; |
| gravity.y = Math.sin(angle) * 0.3; |
| } |
| |
| canvas.addEventListener('mousedown', (e) => { |
| const rect = canvas.getBoundingClientRect(); |
| const mx = e.clientX - rect.left; |
| const my = e.clientY - rect.top; |
| for (let circle of circles) { |
| if (Math.hypot(mx - circle.x, my - circle.y) < circle.radius) { |
| dragCircle = circle; |
| dragOffset.x = circle.x - mx; |
| dragOffset.y = circle.y - my; |
| break; |
| } |
| } |
| }); |
| |
| canvas.addEventListener('mousemove', (e) => { |
| if (dragCircle) { |
| const rect = canvas.getBoundingClientRect(); |
| const mx = e.clientX - rect.left; |
| const my = e.clientY - rect.top; |
| dragCircle.x = mx + dragOffset.x; |
| dragCircle.y = my + dragOffset.y; |
| dragCircle.vx = 0; |
| dragCircle.vy = 0; |
| } |
| }); |
| |
| canvas.addEventListener('mouseup', () => { |
| if (dragCircle) { |
| dragCircle.vx = (dragCircle.x - dragCircle.x) * 50; |
| dragCircle.vy = (dragCircle.y - dragCircle.y) * 50; |
| dragCircle = null; |
| } |
| }); |
| |
| document.getElementById('add-circle').addEventListener('click', addCircle); |
| document.getElementById('clear').addEventListener('click', clearCircles); |
| document.getElementById('toggle-walls').addEventListener('click', toggleWalls); |
| document.getElementById('gravity-dir').addEventListener('click', rotateGravity); |
| |
| function animate(timestamp) { |
| ctx.clearRect(0, 0, width, height); |
| |
| for (let circle of circles) { |
| circle.update(0.016); |
| } |
| |
| handleCircleCollisions(); |
| |
| for (let circle of circles) { |
| circle.draw(); |
| } |
| |
| requestAnimationFrame(animate); |
| } |
| |
| function init() { |
| for (let i = 0; i < 80; i++) { |
| addCircle(); |
| } |
| animate(); |
| } |
| |
| window.addEventListener('resize', () => { |
| width = window.innerWidth; |
| height = window.innerHeight; |
| canvas.width = width; |
| canvas.height = height; |
| }); |
| |
| init(); |
| </script> |
| </body> |
| </html> |