Chapter 1 : Introduction
1.1 Introduction of the tutorial
At the coming chapters, we would build a simple physics engine that simulates ball collision (circle-circle collision) using HTML canvas and Javascript step by step. While there are already many tutorials about how to build a physics engine in scratch, we would like to implement it with our angle of view so that everyone could have one more way to understand the principle of circle collision. We would implement it with the following features:
- Implementing without using vectors, but using rotation formulae directly
- Implementing without defining new Javascript classes (just use objects directly)
- Using trigonometry (sinθ, cosθ and tanθ) and square root as an intemidate, but would eliminate all trigonometric functions and square root functions finally
- Keep all the codes in a single .html file
To be simple, we would implement elastic collision only and the objects would have no friction. Also, the tutorial would keep the codes in a single .html, hope it would help beginners to view and understand the codes more easily!
The final product contains the following features:
- Movable circle
- Fixed circle
- Fixed polygon
- Line segment
1.2 Product preview
The code has both debug and non-debug version, which the debug version has some extra drawings such as positions and velocities. The non debug final product would look like the following:
The code of the canvas above (non-debug version):
<canvas id="myCanvas" width="400px" height="400px"></canvas>
<script>
const circleArray = [
{
cx:80,
cy:80,
r:30,
m:Math.PI*30*30, //mass = area
vx:90,
vy:30
},
{
cx:-40,
cy:-80,
r:40,
m:Math.PI*40*40, //mass = area
vx:90,
vy:-30
},
{
cx:0,
cy:80,
r:10,
m:Math.PI*10*10, //mass = area
vx:0,
vy:0
}
];
const fixedCircleArray = [
{
cx:0,
cy:0,
r:200
},
{
cx:0,
cy:-150,
r:20
}
];
const fixedPolygonArray = [
{
x:[140,90,60,110],
y:[10,40,-10,-40],
isClosed:true
},
{
x:[-90,-150,-90],
y:[60,0,-60],
isClosed:false
}
];
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
ctx.font="12px sans-serif";
ctx.textAlign="center";
const frameRate=60;
const intervalId=setInterval(function(){
//circle position update
for(const c of circleArray){
c.cx += c.vx * 1 / frameRate;
c.cy += c.vy * 1 / frameRate;
}
for(const fp of fixedPolygonArray){
//fixed point and circle check collision
for(let i=0;i < fp.x.length;i++){
for(const c2 of circleArray){
const dx=c2.cx-fp.x[i];
const dy=c2.cy-fp.y[i];
const vxFactor = c2.vx * dx + c2.vy * dy;
if( dx * dx + dy * dy <= c2.r * c2.r && vxFactor < 0){ //apply collision only if 2 circles touch and the direction of speed is correct
c2.vx -= 2 * dx * vxFactor / (dx * dx + dy * dy);
c2.vy -= 2 * dy * vxFactor / (dx * dx + dy * dy);
}
}
}
//line segment and circle collision
for(let i = 0 , j = 1 ; fp.isClosed ? i < fp.x.length : i < fp.x.length - 1 ; i++ , j = (i+1) % fp.x.length){
const lx = fp.x[j] - fp.x[i];
const ly = fp.y[j] - fp.y[i];
for(const c2 of circleArray){
if((c2.cx - fp.x[i]) * lx > -(c2.cy - fp.y[i]) * ly && (c2.cx - fp.x[j]) * lx < -(c2.cy - fp.y[j]) * ly){
const cxx1 = (c2.cx - fp.x[i]) * ly - (c2.cy - fp.y[i]) * lx;
if(cxx1 * cxx1 <= c2.r * c2.r * (lx * lx + ly * ly)){ //check if the circle touches the line segment
const relVx = c2.vx * ly - c2.vy * lx;
if((cxx1 > 0 && relVx < 0) || (0 > cxx1 && relVx > 0)){ //check if the circle is approaching to the line segment
c2.vx -= 2 * ly * relVx / (ly * ly + lx * lx);
c2.vy += 2 * lx * relVx / (ly * ly + lx * lx);
}
}
}
}
}
}
//fixed circle and circle check collision
for(const fc of fixedCircleArray){
for(const c2 of circleArray){
const dx=c2.cx-fc.cx;
const dy=c2.cy-fc.cy;
const vxFactor = c2.vx * dx + c2.vy * dy;
if( (dx * dx + dy * dy >= fc.r * fc.r && dx * dx + dy * dy <= (fc.r + c2.r)*(fc.r + c2.r) && vxFactor < 0) || (dx * dx + dy * dy <= fc.r * fc.r && dx * dx + dy * dy >= (fc.r - c2.r)*(fc.r - c2.r) && vxFactor > 0)){ //apply collision only if 2 circles touch and the direction of speed is correct
c2.vx -= 2 * dx * vxFactor / (dx * dx + dy * dy);
c2.vy -= 2 * dy * vxFactor / (dx * dx + dy * dy);
}
}
}
//circle check collision : find if d is smaller than (or equals to) r1+r2
for(let i = 0; i < circleArray.length ; i++){
const c1 = circleArray[i];
for(let j = i; j < circleArray.length ; j++){
const c2 = circleArray[j];
const dx=c2.cx-c1.cx;
const dy=c2.cy-c1.cy;
if(dx * dx + dy * dy <= (c1.r + c2.r)*(c1.r + c2.r)){ //find if d is smaller than (or equals to) r1+r2
const rel_c2vx = c2.vx - c1.vx;
const rel_c2vy = c2.vy - c1.vy;
if(rel_c2vx * dx + rel_c2vy * dy < 0){ //apply collision only when 2 circles are colliding instead of separating
c1.vx += 2 * dx * c2.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
c1.vy += 2 * dy * c2.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
c2.vx -= 2 * dx * c1.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
c2.vy -= 2 * dy * c1.m * (rel_c2vx * dx + rel_c2vy * dy) / (dx * dx + dy * dy) / (c1.m + c2.m);
}
}
}
}
//clear all drawings
ctx.clearRect(0,0,canvas.width,canvas.height);
//draw x and y axis
ctx.strokeStyle="lightgray";
ctx.beginPath();
ctx.moveTo(0,canvas.height/2);
ctx.lineTo(canvas.width,canvas.height/2);
ctx.moveTo(canvas.width/2,0);
ctx.lineTo(canvas.width/2,canvas.height);
ctx.stroke();
ctx.strokeStyle="black";
//draw border
ctx.beginPath();
ctx.rect(0,0,canvas.width,canvas.height);
ctx.stroke();
//draw fixed point and line segment
for(const fp of fixedPolygonArray){
//draw fixed point
for(let i=0;i < fp.x.length;i++){
ctx.beginPath();
ctx.fillStyle=ctx.strokeStyle="gray";
ctx.arc(canvas.width/2+fp.x[i],canvas.height/2-fp.y[i],2,0,2*Math.PI);
ctx.fill();
}
//draw line segment
for(let i = 0 , j = 1 ; fp.isClosed ? i < fp.x.length : i < fp.x.length - 1 ; i++ , j = (i+1) % fp.x.length){
ctx.beginPath();
ctx.moveTo(canvas.width/2+fp.x[i],canvas.height/2-fp.y[i]);
ctx.lineTo(canvas.width/2+fp.x[j],canvas.height/2-fp.y[j]);
ctx.stroke();
}
}
//draw fixed circle
for(let i=0;i < fixedCircleArray.length;i++){
const fc=fixedCircleArray[i];
ctx.fillStyle=ctx.strokeStyle="gray";
ctx.beginPath();
ctx.arc(canvas.width/2+fc.cx,canvas.height/2-fc.cy,fc.r,0,2*Math.PI);
ctx.stroke();
}
//draw circle
for(let i=0;i < circleArray.length;i++){
const c=circleArray[i];
ctx.fillStyle=ctx.strokeStyle="black";
ctx.beginPath();
ctx.arc(canvas.width/2+c.cx,canvas.height/2-c.cy,c.r,0,2*Math.PI);
ctx.stroke();
}
},1000/frameRate);
</script>
That looks funny, right? Lets move to next chapters to start coding!
|