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(); });