Spaces:
Running
Running
Download script.js from SA34764/bouncebox-simulator: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/SA34764/bouncebox-simulator/resolve/main/script.js
- Command line
-
hf download hf://spaces/SA34764/bouncebox-simulator/script.js
-
curl -L -o script.js https://huggingface.co/spaces/SA34764/bouncebox-simulator/resolve/main/script.js
14.6 kB
| class BounceSimulator { | |
| constructor() { | |
| this.canvas = document.getElementById('simulationCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.instructions = document.getElementById('instructions'); | |
| this.bounceCount = document.getElementById('bounceCount'); | |
| // Simulation state | |
| this.isRunning = false; | |
| this.bounces = 0; | |
| this.trailPoints = []; | |
| this.maxTrailLength = 50; | |
| this.isDragging = false; | |
| // Object properties | |
| this.object = { | |
| x: 0, | |
| y: 0, | |
| size: 20, | |
| shape: 'ball', | |
| color: '#3b82f6', | |
| velocityX: 0, | |
| velocityY: 0, | |
| speed: 5 | |
| }; | |
| // Container properties | |
| this.container = { | |
| shape: 'rectangle', | |
| width: 0, | |
| height: 0, | |
| radius: 0 | |
| }; | |
| this.init(); | |
| this.setupEventListeners(); | |
| } | |
| init() { | |
| this.resizeCanvas(); | |
| window.addEventListener('resize', () => this.resizeCanvas()); | |
| this.setupDragEvents(); | |
| this.resetSimulation(); | |
| } | |
| resizeCanvas() { | |
| const container = this.canvas.parentElement; | |
| this.canvas.width = container.clientWidth; | |
| this.canvas.height = 384; // h-96 equivalent | |
| this.container.width = this.canvas.width - 40; | |
| this.container.height = this.canvas.height - 40; | |
| this.container.radius = Math.min(this.container.width, this.container.height) / 2; | |
| this.object.x = this.container.width / 2; | |
| this.object.y = this.container.height / 2; | |
| } | |
| setupEventListeners() { | |
| // Container shape buttons | |
| document.getElementById('rectContainer').addEventListener('click', () => this.setContainerShape('rectangle')); | |
| document.getElementById('circleContainer').addEventListener('click', () => this.setContainerShape('circle')); | |
| // Object shape buttons | |
| document.getElementById('ballObject').addEventListener('click', () => this.setObjectShape('ball')); | |
| document.getElementById('squareObject').addEventListener('click', () => this.setObjectShape('square')); | |
| // Angle control | |
| document.getElementById('angleControl').addEventListener('input', (e) => { | |
| const angle = parseInt(e.target.value); | |
| document.getElementById('angleValue').textContent = `${angle}°`; | |
| }); | |
| // Speed control | |
| document.getElementById('speedControl').addEventListener('input', (e) => { | |
| const speed = parseInt(e.target.value); | |
| document.getElementById('speedValue').textContent = speed; | |
| this.object.speed = speed; | |
| }); | |
| // Trail toggle | |
| document.getElementById('trailToggle').addEventListener('change', (e) => { | |
| if (!e.target.checked) { | |
| this.trailPoints = []; | |
| } | |
| }); | |
| // Action buttons | |
| document.getElementById('startBtn').addEventListener('click', () => this.startSimulation()); | |
| document.getElementById('resetBtn').addEventListener('click', () => this.resetSimulation()); | |
| } | |
| setupDragEvents() { | |
| this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e)); | |
| this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e)); | |
| this.canvas.addEventListener('mouseup', () => this.handleMouseUp()); | |
| this.canvas.addEventListener('mouseleave', () => this.handleMouseUp()); | |
| // Touch events for mobile | |
| this.canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e)); | |
| this.canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e)); | |
| this.canvas.addEventListener('touchend', () => this.handleMouseUp()); | |
| } | |
| handleMouseDown(e) { | |
| if (this.isRunning) return; | |
| const rect = this.canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| // Check if click is on the object | |
| const dx = x - this.object.x; | |
| const dy = y - this.object.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance <= this.object.size / 2) { | |
| this.isDragging = true; | |
| this.canvas.style.cursor = 'grabbing'; | |
| } | |
| } | |
| handleMouseMove(e) { | |
| if (!this.isDragging || this.isRunning) return; | |
| const rect = this.canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| // Update object position while dragging | |
| this.object.x = x; | |
| this.object.y = y; | |
| // Keep object within container bounds | |
| if (this.container.shape === 'rectangle') { | |
| this.constrainToRectangle(x, y); | |
| } else { | |
| this.constrainToCircle(x, y); | |
| } | |
| this.draw(); | |
| } | |
| handleTouchStart(e) { | |
| if (this.isRunning) return; | |
| e.preventDefault(); | |
| const rect = this.canvas.getBoundingClientRect(); | |
| const touch = e.touches[0]; | |
| const x = touch.clientX - rect.left; | |
| const y = touch.clientY - rect.top; | |
| const dx = x - this.object.x; | |
| const dy = y - this.object.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance <= this.object.size / 2) { | |
| this.isDragging = true; | |
| } | |
| } | |
| handleTouchMove(e) { | |
| if (!this.isDragging || this.isRunning) return; | |
| e.preventDefault(); | |
| const rect = this.canvas.getBoundingClientRect(); | |
| const touch = e.touches[0]; | |
| const x = touch.clientX - rect.left; | |
| const y = touch.clientY - rect.top; | |
| this.object.x = x; | |
| this.object.y = y; | |
| // Keep object within container bounds | |
| if (this.container.shape === 'rectangle') { | |
| this.constrainToRectangle(x, y); | |
| } else { | |
| this.constrainToCircle(x, y); | |
| } | |
| this.draw(); | |
| } | |
| handleMouseUp() { | |
| if (this.isDragging) { | |
| this.isDragging = false; | |
| this.canvas.style.cursor = 'crosshair'; | |
| } | |
| } | |
| constrainToRectangle(x, y) { | |
| const left = 20 + this.object.size / 2; | |
| const right = this.container.width + 20 - this.object.size / 2; | |
| const top = 20 + this.object.size / 2; | |
| const bottom = this.container.height + 20 - this.object.size / 2; | |
| this.object.x = Math.max(left, Math.min(right, x)); | |
| this.object.y = Math.max(top, Math.min(bottom, y)); | |
| } | |
| constrainToCircle(x, y) { | |
| const centerX = this.container.width / 2 + 20; | |
| const centerY = this.container.height / 2 + 20; | |
| const maxDistance = this.container.radius - this.object.size / 2; | |
| const dx = x - centerX; | |
| const dy = y - centerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > maxDistance) { | |
| const angle = Math.atan2(dy, dx); | |
| this.object.x = centerX + Math.cos(angle) * maxDistance; | |
| this.object.y = centerY + Math.sin(angle) * maxDistance; | |
| } else { | |
| this.object.x = x; | |
| this.object.y = y; | |
| } | |
| } | |
| setContainerShape(shape) { | |
| this.container.shape = shape; | |
| this.updateActiveButton('rectContainer', 'circleContainer', shape === 'rectangle'); | |
| this.resetSimulation(); | |
| } | |
| setObjectShape(shape) { | |
| this.object.shape = shape; | |
| this.updateActiveButton('ballObject', 'squareObject', shape === 'ball'); | |
| this.resetSimulation(); | |
| } | |
| updateActiveButton(btn1Id, btn2Id, isFirstActive) { | |
| const btn1 = document.getElementById(btn1Id); | |
| const btn2 = document.getElementById(btn2Id); | |
| btn1.classList.toggle('active', isFirstActive); | |
| btn2.classList.toggle('active', !isFirstActive); | |
| } | |
| startSimulation() { | |
| if (this.isRunning) return; | |
| this.isRunning = true; | |
| this.instructions.style.display = 'none'; | |
| this.canvas.style.cursor = 'crosshair'; | |
| const angle = parseInt(document.getElementById('angleControl').value); | |
| const radians = angle * Math.PI / 180; | |
| this.object.velocityX = Math.cos(radians) * this.object.speed; | |
| this.object.velocityY = Math.sin(radians) * this.object.speed; | |
| this.animate(); | |
| } | |
| resetSimulation() { | |
| this.isRunning = false; | |
| this.bounces = 0; | |
| this.isDragging = false; | |
| this.canvas.style.cursor = 'crosshair'; | |
| if (!document.getElementById('trailToggle').checked) { | |
| this.trailPoints = []; | |
| } | |
| this.bounceCount.textContent = `Bounces: ${this.bounces}`; | |
| this.instructions.style.display = 'flex'; | |
| // Don't reset object position - keep it where it was | |
| this.object.velocityX = 0; | |
| this.object.velocityY = 0; | |
| this.draw(); | |
| } | |
| animate() { | |
| if (!this.isRunning) return; | |
| this.update(); | |
| this.draw(); | |
| requestAnimationFrame(() => this.animate()); | |
| } | |
| update() { | |
| if (this.isDragging) return; | |
| // Add current position to trail | |
| if (document.getElementById('trailToggle').checked) { | |
| this.trailPoints.push({ x: this.object.x, y: this.object.y }); | |
| } | |
| // Update object position | |
| this.object.x += this.object.velocityX; | |
| this.object.y += this.object.velocityY; | |
| // Handle collisions based on container shape | |
| if (this.container.shape === 'rectangle') { | |
| this.handleRectangleCollisions(); | |
| } else { | |
| this.handleCircleCollisions(); | |
| } | |
| } | |
| handleRectangleCollisions() { | |
| const left = 20; | |
| const right = this.container.width + 20; | |
| const top = 20; | |
| const bottom = this.container.height + 20; | |
| // Check left/right boundaries | |
| if (this.object.x - this.object.size / 2 <= left || this.object.x + this.object.size / 2 >= right) { | |
| this.object.velocityX *= -1; | |
| this.bounce(); | |
| } | |
| // Check top/bottom boundaries | |
| if (this.object.y - this.object.size / 2 <= top || this.object.y + this.object.size / 2 >= bottom) { | |
| this.object.velocityY *= -1; | |
| this.bounce(); | |
| } | |
| // Keep object within bounds | |
| this.object.x = Math.max(left + this.object.size / 2, Math.min(right - this.object.size / 2, this.object.x)); | |
| this.object.y = Math.max(top + this.object.size / 2, Math.min(bottom - this.object.size / 2, this.object.y)); | |
| } | |
| handleCircleCollisions() { | |
| const centerX = this.container.width / 2 + 20; | |
| const centerY = this.container.height / 2 + 20; | |
| // Calculate distance from center | |
| const dx = this.object.x - centerX; | |
| const dy = this.object.y - centerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| // Check if object hits circle boundary | |
| if (distance + this.object.size / 2 >= this.container.radius) { | |
| // Calculate normal vector | |
| const nx = dx / distance; | |
| const ny = dy / distance; | |
| // Calculate dot product | |
| const dotProduct = this.object.velocityX * nx + this.object.velocityY * ny; | |
| // Reflect velocity | |
| this.object.velocityX = this.object.velocityX - 2 * dotProduct * nx; | |
| this.object.velocityY = this.object.velocityY - 2 * dotProduct * ny; | |
| this.bounce(); | |
| // Adjust position to prevent sticking | |
| const overlap = distance + this.object.size / 2 - this.container.radius; | |
| this.object.x -= nx * overlap; | |
| this.object.y -= ny * overlap; | |
| } | |
| } | |
| bounce() { | |
| this.bounces++; | |
| this.bounceCount.textContent = `Bounces: ${this.bounces}`; | |
| this.bounceCount.classList.add('bounce-animation'); | |
| setTimeout(() => this.bounceCount.classList.remove('bounce-animation'), 600); | |
| // Change color on bounce (rainbow effect) | |
| const colors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6']; | |
| this.object.color = colors[this.bounces % colors.length]; | |
| } | |
| draw() { | |
| // Clear canvas | |
| this.ctx.fillStyle = '#111827'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| // Draw container | |
| this.ctx.strokeStyle = '#4b5563'; | |
| this.ctx.lineWidth = 2; | |
| if (this.container.shape === 'rectangle') { | |
| this.ctx.strokeRect(20, 20, this.container.width, this.container.height); | |
| } else { | |
| this.ctx.beginPath(); | |
| this.ctx.arc( | |
| this.container.width / 2 + 20, | |
| this.container.height / 2 + 20, | |
| this.container.radius, | |
| 0, | |
| 2 * Math.PI | |
| ); | |
| this.ctx.stroke(); | |
| } | |
| // Draw trail | |
| if (document.getElementById('trailToggle').checked && this.trailPoints.length > 1) { | |
| this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; | |
| this.ctx.lineWidth = 2; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(this.trailPoints[0].x, this.trailPoints[0].y); | |
| for (let i = 1; i < this.trailPoints.length; i++) { | |
| this.ctx.lineTo(this.trailPoints[i].x, this.trailPoints[i].y); | |
| } | |
| this.ctx.stroke(); | |
| } | |
| // Draw object | |
| this.ctx.fillStyle = this.object.color; | |
| if (this.object.shape === 'ball') { | |
| this.ctx.beginPath(); | |
| this.ctx.arc(this.object.x, this.object.y, this.object.size / 2, 0, 2 * Math.PI); | |
| this.ctx.fill(); | |
| // Add glow effect | |
| this.ctx.shadowColor = this.object.color; | |
| this.ctx.shadowBlur = 10; | |
| this.ctx.fill(); | |
| this.ctx.shadowBlur = 0; | |
| } else { | |
| this.ctx.fillRect( | |
| this.object.x - this.object.size / 2, | |
| this.object.y - this.object.size / 2, | |
| this.object.size, | |
| this.object.size | |
| ); | |
| } | |
| } | |
| } | |
| // Initialize simulator when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new BounceSimulator(); | |
| }); |