Adding an option (and checkbox) for wrapping the balls around the canvas edges.

$(function () {
    var canvas, context, width, height, x, y, radius = 25, clickX, clickY, drag = false;
	var total_dots = 10;
	var fps = 24;
	
    canvas = $("#canvas")[0];
    context = canvas.getContext("2d");
 	var dots = new Array();
	var drag_i = -1;
	var gravity = 0;
	var friction = .98;
	var bounce = -.96;
	var wrap = true;
	
	var this_dot = {};
	for (var i=0; i < total_dots; i++){
		createDot();
	}
	function createDot(x, y, r, vx, vy){
		var this_dot = {
			x: 		typeof(x) != 'undefined' ? x : Math.random()*canvas.width, 
			y: 		typeof(y) != 'undefined' ? y : Math.random()*canvas.height,
			radius: typeof(r) != 'undefined' ? r : Math.random()*20+10,
			vx: 	typeof(vx) != 'undefined' ? vx : Math.random()*30-10,
			vy: 	typeof(vy) != 'undefined' ? vy : Math.random()*30-10
		};
		dots.push(this_dot);
	}
    
	draw();
 	
    $("#canvas").mousedown(function (event) {
        var dx, dy, dist;
		for (var i=0; i < dots.length; i++){
			dx = event.pageX - this.offsetLeft - dots[i].x;
			dy = event.pageY - this.offsetTop - dots[i].y;
			dist = Math.sqrt(dx * dx + dy * dy);
			if(dist < radius) {
				drag = true;
				drag_i = i
				clickX = dx;
				clickY = dy;
				continue;
			}
		}
		//none clicked
		if (!drag) {
			createDot(event.pageX - this.offsetLeft, event.pageY - this.offsetTop);
		}
    });
 
    $("#canvas").mouseup(function (event) {
        drag = false;
		drag_i = -1;
    });
 
    $("#canvas").mousemove(function (event) {
        if(drag) {
            dots[drag_i].old_x = dots[drag_i].x;
            dots[drag_i].old_y = dots[drag_i].y;
            dots[drag_i].x = event.pageX - this.offsetLeft - clickX;
            dots[drag_i].y = event.pageY - this.offsetTop - clickY;
            dots[drag_i].vx = dots[drag_i].x - dots[drag_i].old_x;
            dots[drag_i].vy = dots[drag_i].y - dots[drag_i].old_y;
            draw();
        }
    });
 	function update(){
		for (var i=0; i < dots.length; i++){
			if (drag_i != i){
				var this_dot = dots[i];
				this_dot.vx *= friction;
				this_dot.vy = this_dot.vy * friction + gravity;
				this_dot.x += this_dot.vx;
				this_dot.y += this_dot.vy;
				if (wrap){
					if (this_dot.x > canvas.width + this_dot.radius){
							this_dot.x -= canvas.width + this_dot.radius*2;
					}
					else if(this_dot.x < 0 - this_dot.radius){
							this_dot.x += canvas.width + this_dot.radius*2;
					}
					if (this_dot.y > canvas.height + this_dot.radius){
							this_dot.y -= canvas.height + this_dot.radius*2;
					}
					else if(this_dot.y < 0 - this_dot.radius){
							this_dot.y += canvas.height + this_dot.radius*2;
					}
				}
				else if (!wrap) {
					if (this_dot.x > canvas.width - this_dot.radius){
						this_dot.x = canvas.width - this_dot.radius;
						this_dot.vx = this_dot.vx * bounce;
					}
					else if(this_dot.x < 0 + this_dot.radius){
						this_dot.x = this_dot.radius;
						this_dot.vx = this_dot.vx * bounce;
					}
					if (this_dot.y > canvas.height - this_dot.radius){
						this_dot.y = canvas.height - this_dot.radius;
						this_dot.vy = this_dot.vy * bounce;
					}
					else if(this_dot.y < 0 + this_dot.radius){
						this_dot.y = this_dot.radius;
						this_dot.vy = this_dot.vy * bounce;
					}
				}
			}
		}
	}
    function draw() {
        context.clearRect(0, 0, canvas.width, canvas.height);
		for (var i=0; i < dots.length; i++){
        	context.beginPath();
			context.arc(dots[i].x, dots[i].y, dots[i].radius, 0, Math.PI * 2, false);
        	context.fill();
			context.closePath();
		}
    }
	
	setInterval(function() {
    	update();
    	draw();
	}, 1000/fps);	

	$("#gravity").click(function(){
		if($("#gravity").is(':checked')){
			gravity = 2;	
		}
		else{
			gravity = 0;
		}
	});
	$("#wrap").click(function(){
		if($("#wrap").is(':checked')){
			wrap = true;	
		}
		else{
			wrap = false;
		}
	});
	
});

Back to the blog post