(function ($, window) { function Vector2d(x, y) { this.x = x; this.y = y; } Vector2d.prototype.dot = function(vector) { return this.x * vector.x + this.y * vector.y; } Vector2d.prototype.add = function(vector) { return new Vector2d(this.x + vector.x, this.y + vector.y); } Vector2d.prototype.subtract = function(vector) { return new Vector2d(this.x - vector.x, this.y - vector.y); } Vector2d.prototype.length = function() { return Math.sqrt(this.x*this.x + this.y*this.y); } Vector2d.prototype.multiply = function(scaleFactor) { return new Vector2d(this.x * scaleFactor, this.y * scaleFactor); } Vector2d.prototype.normalize = function() { var len = this.length(); if (len == 0) { this.x = 0; this.y = 0; return this; } else { this.x = this.x / len; this.y = this.y / len; return this; } } function Ball(pos, vel, radius, mass, color) { this.pos = pos; this.vel = vel; this.radius = radius; this.color = color; this.mass = mass; } Ball.prototype.render = function(ctx) { ctx.fillStyle = this.color; ctx.strokeStyle = this.color; ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI*2, false); ctx.fill(); } Ball.prototype.resolveCollision = function(ball) { var delta = this.pos.subtract(ball.pos); var r = this.radius + ball.radius; var dist2 = delta.dot(delta); if (dist2 > r*r) { return; /* not colliding */ } var d = delta.length(); var mtd; if (d != 0) { mtd = delta.multiply(((this.radius + ball.radius)-d)/d); } else { // special case, balls are exactly on top of eachother d = ball.radius + this.radius - 1.0; delta = new Vector2d(this.radius + ball.radius, 0); mtd = delta.multiply(((this.radius + ball.radius)-d)/d); } // resolve intersection var im1 = 1 / this.mass; var im2 = 1 / ball.mass; // push/pull them apart this.pos = this.pos.add(mtd.multiply(im1 / (im1 + im2))); ball.pos = ball.pos.subtract(mtd.multiply( im2 / (im1 + im2))); // impact speed var v = this.vel.subtract(ball.vel); var vn = v.dot(mtd.normalize()); // sphere intersecting but moving away from each other already if (vn > 0.0) { return; } var i = (-(1.0 + Bouncer.RESTITUTION) * vn) / (im1 + im2); var impulse = mtd.multiply(i); this.vel = this.vel.add(impulse.multiply(im1)); ball.vel = ball.vel.subtract(impulse.multiply(im2)); } function Bouncer(ctx) { this.ctx = ctx; this.balls = []; this.now = undefined; this.last = undefined; this.launchBall = undefined; } Bouncer.GRAVITY = 700; /* pixels per second */ Bouncer.RESTITUTION = 0.75; Bouncer.prototype.init = function() { var self = this; $(self.ctx.canvas).bind('mousedown', function(e) { var x = e.pageX-$(self.ctx.canvas).offset().left; var y = e.pageY-$(self.ctx.canvas).offset().top; self.launchBall = new Ball(new Vector2d(x, y), new Vector2d(x, y), 10, 1, "#00FF00"); }); $(document).bind('mouseup', function(e) { if (self.launchBall) { var x = e.pageX-$(self.ctx.canvas).offset().left; var y = e.pageY-$(self.ctx.canvas).offset().top; self.addBall(new Vector2d(self.launchBall.pos.x, self.launchBall.pos.y), new Vector2d((self.launchBall.vel.x - self.launchBall.pos.x) * 5, (self.launchBall.vel.y - self.launchBall.pos.y) * 5), self.launchBall.radius, 1, "#FF0000"); self.launchBall = undefined; } }); $(document).bind('mousemove', function(e) { if (self.launchBall) { var x1 = self.launchBall.pos.x; var y1 = self.launchBall.pos.y; var x2 = e.pageX-$('#canvas').offset().left; var y2 = e.pageY-$('#canvas').offset().top; var dx = Math.abs(x2 - x1); var dy = Math.abs(y2 - y1); //console.log("x1:" + x1 + " y1:" + y1 + " xV2:" + self.launchBall.xVel + " yV2:" + self.launchBall.yVel); //console.log("x2:" + x2 + " y2: " + y2 + " dx:" + dx + " dy:" + dy); if ((x2 - x1) < 0) { self.launchBall.vel.x = x1 + dx; } else { self.launchBall.vel.x = x1 - dx; } if ((y2 - y1) < 0) { self.launchBall.vel.y = y1 + dy; } else { self.launchBall.vel.y = y1 - dy; } } }); this.last = new Date(); this.step(); } Bouncer.prototype.render = function() { // Modify canvas dimensions this.ctx.canvas.width = window.innerWidth - 50; this.ctx.canvas.height = window.innerHeight - 50; // Clear this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); // Render balls for(var i = 0; i < this.balls.length; i++) { this.balls[i].render(this.ctx); } // Render launching ball if (this.launchBall) { this.launchBall.render(this.ctx); this.ctx.moveTo(this.launchBall.pos.x, this.launchBall.pos.y); this.ctx.lineTo(this.launchBall.vel.x, this.launchBall.vel.y); this.ctx.strokeStyle = "#FF6633"; this.ctx.stroke(); } } Bouncer.prototype.step = function() { this.now = + new Date; var dt = (this.now - this.last) / 1000; this.last = this.now; this.tick(dt); this.render(); var self = this; setTimeout(function() { self.step() }, 0); } Bouncer.prototype.tick = function(dt) { for(var i = 0; i < this.balls.length; i++) { this.balls[i].vel.y += Bouncer.GRAVITY * dt; this.balls[i].pos.y += this.balls[i].vel.y * dt; this.balls[i].pos.x += this.balls[i].vel.x * dt; } this.checkWallCollisions(); this.checkBallCollisions(); } Bouncer.prototype.checkBallCollisions = function() { for(var i = 0; i < this.balls.length; i++) { for(var j = i; j < this.balls.length; j++) { this.balls[i].resolveCollision(this.balls[j]); } } } Bouncer.prototype.checkWallCollisions = function() { for(var i = 0; i < this.balls.length; i++) { if ((this.balls[i].pos.x- this.balls[i].radius) < 0) { this.balls[i].pos.x = this.balls[i].radius; this.balls[i].vel.x = -this.balls[i].vel.x * Bouncer.RESTITUTION; } if ((this.balls[i].pos.x + this.balls[i].radius) > this.ctx.canvas.width) { this.balls[i].pos.x = this.ctx.canvas.width - this.balls[i].radius; this.balls[i].vel.x = -this.balls[i].vel.x * Bouncer.RESTITUTION; } if ((this.balls[i].pos.y - this.balls[i].radius) < 0) { this.balls[i].pos.y = this.balls[i].radius; this.balls[i].vel.y = -this.balls[i].vel.y * Bouncer.RESTITUTION; } if ((this.balls[i].pos.y + this.balls[i].radius) > this.ctx.canvas.height) { this.balls[i].pos.y = this.ctx.canvas.height - this.balls[i].radius; this.balls[i].vel.y = -this.balls[i].vel.y * Bouncer.RESTITUTION; } } } Bouncer.prototype.addBall = function(pos, vel, radius, mass, color) { this.balls.push(new Ball(pos, vel, radius, mass, color)); } $(function() { var canvas = $('#canvas')[0]; var ctx = canvas.getContext('2d'); var bouncer = new Bouncer(ctx, canvas); bouncer.init(); }); }(jQuery, window));
<canvas id="canvas" width="600" height="600" />