🎯 Building an Interactive Newton's Cradle

Drag the balls or click the buttons below:

What You'll Learn:

Step 1: HTML Structure

You can download the entire code here: cradle.htm or copy-paste from the steps below.

Start with a simple HTML structure containing a canvas element and control buttons:

<canvas id="canvas" width="600" height="400"></canvas> <div class="controls"> <button onclick="startSwing(1)">Swing 1 Ball</button> <button onclick="startSwing(2)">Swing 2 Balls</button> <button onclick="reset()">Reset</button> </div>

Step 2: Initialize Canvas and Constants

Set up the canvas context and define the physical parameters of your cradle:

const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const numBalls = 5; // Number of balls in cradle const ballRadius = 20; // Radius of each ball const stringLength = 150; // Length of pendulum string const frameY = 50; // Y position of top frame const spacing = ballRadius * 2 + 2; // Space between balls const centerX = canvas.width / 2; // Center of canvas let balls = []; let dragging = null; // Track which ball is being dragged
💡 Tip: The spacing between balls is slightly more than their diameter to prevent them from overlapping when at rest.

Step 3: Create the Ball Class

Ball Properties

Each ball is a pendulum with an anchor point, angle, and velocity:

class Ball { constructor(index) { this.index = index; this.anchorX = centerX + (index - 2) * spacing; this.anchorY = frameY; this.angle = 0; // Current angle from vertical this.velocity = 0; // Angular velocity this.mass = 1; this.stringLength = stringLength; } }

Calculate Position

Convert polar coordinates (angle) to Cartesian coordinates (x, y):

get x() { return this.anchorX + Math.sin(this.angle) * this.stringLength; } get y() { return this.anchorY + Math.cos(this.angle) * this.stringLength; }
Physics Concept: We use sine and cosine to convert the pendulum's angle into screen coordinates. The angle of 0 represents the ball hanging straight down.

Step 4: Implement Pendulum Physics

The Update Method

Apply the physics of a simple pendulum to update the ball's position each frame:

update(dt) { const gravity = 0.5; const damping = 0.999; // Calculate acceleration from gravity let acceleration = (-gravity / this.stringLength) * Math.sin(this.angle); // Update velocity and apply damping this.velocity += acceleration * dt; this.velocity *= damping; // Update angle this.angle += this.velocity * dt; }
Pendulum Formula:
acceleration = -(g / L) × sin(θ)
Breaking it down:

Step 5: Render the Balls

Draw each ball and its string on the canvas:

draw() { // Draw string ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.anchorX, this.anchorY); ctx.lineTo(this.x, this.y); ctx.stroke(); // Draw ball ctx.fillStyle = '#4a5568'; ctx.beginPath(); ctx.arc(this.x, this.y, ballRadius, 0, Math.PI * 2); ctx.fill(); // Add highlight for 3D effect ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.beginPath(); ctx.arc(this.x - 5, this.y - 5, ballRadius / 3, 0, Math.PI * 2); ctx.fill(); }

Step 6: Collision Detection

Detect when balls collide and transfer momentum between them:

function detectCollisions() { for (let i = 0; i < balls.length - 1; i++) { const b1 = balls[i]; const b2 = balls[i + 1]; // Calculate distance between ball centers const dx = b2.x - b1.x; const dy = b2.y - b1.y; const dist = Math.sqrt(dx * dx + dy * dy); // If balls are touching if (dist < ballRadius * 2) { // Swap velocities (elastic collision) const temp = b1.velocity; b1.velocity = b2.velocity; b2.velocity = temp; } } }
Physics Concept: In a perfectly elastic collision between objects of equal mass, they simply exchange velocities. This is what creates the iconic Newton's cradle effect!

Step 7: Animation Loop

Create the main animation loop that updates and renders everything:

function animate() { const dt = 0.5; // Time step for physics // Update all balls (except the one being dragged) balls.forEach(ball => { if (dragging !== ball) { ball.update(dt); } }); // Check for collisions detectCollisions(); // Render everything draw(); // Request next frame requestAnimationFrame(animate); }
💡 Tip: requestAnimationFrame automatically syncs with the browser's refresh rate (typically 60 FPS) for smooth animation.

Step 8: Drawing Function

Clear the canvas and draw all elements:

function draw() { // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw top frame ctx.strokeStyle = '#2d3748'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(50, frameY); ctx.lineTo(canvas.width - 50, frameY); ctx.stroke(); // Draw all balls balls.forEach(ball => ball.draw()); }

Step 9: Mouse Interaction

Mouse Down - Start Dragging

canvas.addEventListener('mousedown', e => { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; // Check if mouse is over any ball for (let ball of balls) { const dx = mx - ball.x; const dy = my - ball.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < ballRadius) { dragging = ball; ball.velocity = 0; // Stop its motion break; } } });

Mouse Move - Update Ball Position

canvas.addEventListener('mousemove', e => { if (dragging) { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; // Calculate angle from anchor to mouse const dx = mx - dragging.anchorX; const dy = my - dragging.anchorY; dragging.angle = Math.atan2(dx, dy); } });

Mouse Up - Release Ball

canvas.addEventListener('mouseup', () => { dragging = null; }); canvas.addEventListener('mouseleave', () => { dragging = null; });

Step 10: Touch Support

Add touchscreen support with similar logic to mouse events:

canvas.addEventListener('touchstart', e => { e.preventDefault(); // Prevent scrolling const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; const mx = touch.clientX - rect.left; const my = touch.clientY - rect.top; for (let ball of balls) { const dx = mx - ball.x; const dy = my - ball.y; if (Math.sqrt(dx * dx + dy * dy) < ballRadius) { dragging = ball; ball.velocity = 0; break; } } }); canvas.addEventListener('touchmove', e => { e.preventDefault(); if (dragging) { const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; const mx = touch.clientX - rect.left; const my = touch.clientY - rect.top; const dx = mx - dragging.anchorX; const dy = my - dragging.anchorY; dragging.angle = Math.atan2(dx, dy); } }); canvas.addEventListener('touchend', () => { dragging = null; }); canvas.addEventListener('touchcancel', () => { dragging = null; });
💡 Important: Always call e.preventDefault() on touch events to prevent the page from scrolling while dragging.

Step 11: Helper Functions

Initialize the Cradle

function init() { balls = []; for (let i = 0; i < numBalls; i++) { balls.push(new Ball(i)); } }

Start Automatic Swing

function startSwing(count) { reset(); for (let i = 0; i < count; i++) { balls[i].angle = -0.6; // Pull back by 0.6 radians } }

Reset All Balls

function reset() { balls.forEach(ball => { ball.angle = 0; ball.velocity = 0; }); }

Step 12: Start the Simulation

Finally, initialize the balls and start the animation loop:

init(); animate();

🎓 Key Concepts Review

Physics Simulation

Canvas Techniques

Interaction

🚀 Enhancement Ideas

More JavaScript tutorials

Spinning squares - visual effect (25 lines)

Oldschool fire effect (20 lines)

Fireworks (60 lines)

Animated fractal (32 lines)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21 lines)

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70 lines)

Sine scroller (30 lines)

Interactive animated sprites

Image transition effect (16 lines)