bouncebox-simulator / script.js
SA34764's picture
just add that we can put the ball where ever we want before the start
7fed41f verified
Raw History Blame Contribute Delete
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();
});