The simulation is two-dimensional: the table and balls are seen from straight above. Drawing the balls as simple circles produces a very un-realistic effect though; to improve things it is necessary to render in some way the rotation of the balls. The easiest way to do this is to add some feature to a ball surface – like a white circle – and change its position and size as the ball rotates.

A point on the surface of a ball is identified by two angles \((\varphi, \vartheta)\):

As the balls moves, it rotates by an angle \(\delta = \Delta s / r\), where \(r\) is the ball radius and \(\Delta s = \sqrt{\Delta x^2 + \Delta y^2}\) is the distance travelled. The direction of the movement is the direction of the ball velocity, so this is the situation as seen ‘from above’:

where \(\alpha = \arctan(w/v)\). Let’s rotate the \(x, y\) axes by the angle \(\alpha\), so that the movement is along the new \(\bar x\) axis: to do it simply subtract \(\alpha\) from \(\varphi\). Using these new coordinates the situation as seen ‘from the side’ is:

(the \(\bar y\) axis goes away from you ‘through’ the screen). The initial coordinate of the point are:

$$ \bar x = r \sin \vartheta cos (\varphi-\alpha) $$

$$ \bar y = r \sin \vartheta sin (\varphi-\alpha) $$

$$ z = r \cos \vartheta $$

after the ball moves the \(\bar y\) coordinate is the same, whereas \(\bar x\) and \(z\) are rotated by \(\delta\) clockwise:

$$ \bar{x}’ = \bar x \cos\delta + z \sin\delta = r \sin \vartheta cos (\varphi-\alpha) \cos\delta + r \cos \vartheta \sin\delta $$

$$ \bar y’ = r \sin \vartheta sin (\varphi-\alpha) $$

$$ z’ = –\bar x \sin\delta + z \cos\delta = –r \sin \vartheta cos (\varphi-\alpha) \sin\delta + r \cos \vartheta \cos\delta $$

The angles after the ball moves are then:

$$ \varphi’ = \alpha + \arctan \frac{\bar y’}{\bar x’} = \alpha + \arctan \frac {\sin \vartheta sin (\varphi-\alpha)} {\sin \vartheta cos (\varphi-\alpha) \cos\delta + \cos \vartheta \sin\delta} $$

$$ \vartheta’ = \arccos \frac{z’}{r} = \arccos(-\sin \vartheta cos (\varphi-\alpha) \sin\delta + \cos \vartheta \cos\delta) $$

and here is the corresponding code:

/** * Updates the ball position and orientation, applying the current velocity for a specified time interval * @param t the time interval to use. The velocity is considered constant, so the time interval must be * small compared to the rate of change of the velocity */ updatePosition(t: number) { var dx = this.v * t; var dy = this.w * t // Update the ball position this.x += dx; this.y += dy; var ds2 = dx * dx + dy * dy; if (ds2 > 0) { // Update the ball orientation var delta = Math.sqrt(ds2) / this.radius; var sinDelta = Math.sin(delta); var cosDelta = Math.cos(delta); var alpha = Math.atan2(this.w, this.v); var sinTheta = Math.sin(this.theta); var cosTheta = Math.cos(this.theta); var phiAlpha = this.phi - alpha; var sinPhiAlpha = Math.sin(phiAlpha); var cosPhiAlpha = Math.cos(phiAlpha); this.phi = alpha + Math.atan2(sinTheta * sinPhiAlpha, sinTheta * cosPhiAlpha * cosDelta + cosTheta * sinDelta); this.theta = Math.acos(-sinTheta * cosPhiAlpha * sinDelta + cosTheta * cosDelta); } } // updatePosition

A circle drawn on the surface of the ball that is centered at the point \((\varphi, \vartheta)\) looks like a circle when \(\vartheta=0\), and then get progressively ‘squashed’ as \(\vartheta\) increases, until is disappears when \(\vartheta>\pi/2\):

The circle is actually a spherical one, that should be projected on a horizontal plane to compute the exact shape to be drawn; approximately though the circle becomes an ellipse as \(\vartheta\) increases – with the axis in the direction from its center to the center of the ball decreasing with \(\cos \vartheta\). Using this approximation the code to draw the ball is this:

/** * draw: draws the ball * @param ctx the canvas rendering context to use to draw the ball */ draw(ctx: CanvasRenderingContext2D) { ctx.save(); // Move the coordinates to the center of the ball - simplifies everything else ctx.translate(this.x, this.y); // Gradient from the ball color to black, used to shade the ball color to give an illusion of depth var ballColorGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.radius*3); ballColorGradient.addColorStop(0, this.color); ballColorGradient.addColorStop(1, "black"); // Gradient from white to black, used to shade the white circle to give an illusion of depth var whiteGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.radius * 3); whiteGradient.addColorStop(0, "white"); whiteGradient.addColorStop(1, "black"); // Draw the ball: a circle filled with the ball color shading to black ctx.fillStyle = ballColorGradient ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI*2); ctx.fill(); if (this.theta < Math.PI / 2) { // Draw the white circle if it is visible var d = this.radius * Math.sin(this.theta); var cosTheta = Math.cos(this.theta); var s = this.circleRadius * cosTheta; if (d - s < this.radius) { var cosPhi = Math.cos(this.phi); var sinPhi = Math.sin(this.phi); // Clip to the ball's circle - do not want to draw parts of the white circle that fall outside the ball borders ctx.clip(); // Move the coordinates to the center of the white circle ctx.translate(d * cosPhi, d * sinPhi); // Compress the coordinates by cosTheta in the direction between the center of the white circle and the center of the ball ctx.rotate(this.phi); ctx.scale(cosTheta, 1); // Draw the white circle ctx.fillStyle = whiteGradient; ctx.beginPath(); ctx.arc(0, 0, this.circleRadius, 0, 2 * Math.PI); ctx.fill(); } } ctx.restore(); } // draw

Note how the code uses gradients to fill the ball and the white circle to give a more ‘tridimensional’ look.

Aside: these first experiences of using the HTML canvas are very positive: the API is complete, easy to use and works well in multiple ‘modern’ browsers