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:
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:
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.