├── 01 - Drawing a Ball
├── index.html
└── script.js
├── 02 - Moving the Ball
├── index.html
└── script.js
├── 03 - The Ball Class
├── index.html
└── script.js
├── 04 - Ball Acceleration
├── index.html
└── script.js
├── 05 - Introducing Vectors
├── index.html
└── script.js
├── 06 - Unit Vector, Dot Product
├── index.html
└── script.js
├── 07 - Ball to Ball Collision
├── index.html
└── script.js
├── 08 - Collision Response
├── index.html
└── script.js
├── 09 - Mass and Elasticity
├── index.html
└── script.js
├── 10 - Adding the Walls
├── index.html
└── script.js
├── 11 - Rotation Matrix
├── index.html
└── script.js
├── 12 - Steering a Capsule
├── index.html
└── script.js
├── 13 - Capsule to Capsule
├── index.html
└── script.js
├── 14 - Rotational Collision
├── index.html
└── script.js
├── 15 - Separating Axis Theorem
├── index.html
└── script.js
├── 16 - Minimum Translation Vector
├── index.html
└── script.js
├── 17 - Bodies and Shapes
├── index.html
└── script.js
├── 18 - Collision Manifold
├── index.html
└── script.js
├── 19 - A Star Is Born
├── index.html
└── script.js
├── 20 - Ready for Games
├── game.js
├── index.html
├── mocorgo.js
└── userInput.js
├── Game Template
├── game_template.js
├── index.html
├── mocorgo.js
└── userInput.js
├── LICENSE
└── README.md
/01 - Drawing a Ball/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Drawing the Ball
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/01 - Drawing a Ball/script.js:
--------------------------------------------------------------------------------
1 | //store the canvas element in a variable
2 | const canvas = document.getElementById('canvas');
3 | //define the rendering context
4 | const ctx = canvas.getContext('2d');
5 |
6 | //a function that draws a circle and fills it with a color
7 | function drawBall(x, y, r){
8 | ctx.beginPath();
9 | ctx.arc(x, y, r, 0, 2*Math.PI);
10 | ctx.strokeStyle = "black";
11 | ctx.stroke();
12 | ctx.fillStyle = "red";
13 | ctx.fill();
14 | ctx.closePath();
15 | }
16 |
17 | //calling the function twice
18 | drawBall(100, 100, 20);
19 | drawBall(200, 200, 30);
20 |
--------------------------------------------------------------------------------
/02 - Moving the Ball/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Moving the Ball
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/02 - Moving the Ball/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | let x = 100;
5 | let y = 100;
6 |
7 | //boolean variables - if true, ball moves in the direction
8 | let LEFT, UP, RIGHT, DOWN;
9 |
10 | function drawBall(x, y, r){
11 | ctx.beginPath();
12 | ctx.arc(x, y, r, 0, 2*Math.PI);
13 | ctx.strokeStyle = "black";
14 | ctx.stroke();
15 | ctx.fillStyle = "red";
16 | ctx.fill();
17 | ctx.closePath();
18 | }
19 |
20 | //direction booleans become true if the arrow key is pressed
21 | canvas.addEventListener('keydown', function(e){
22 | if(e.keyCode === 37){
23 | LEFT = true;
24 | }
25 | if(e.keyCode === 38){
26 | UP = true;
27 | }
28 | if(e.keyCode === 39){
29 | RIGHT = true;
30 | }
31 | if(e.keyCode === 40){
32 | DOWN = true;
33 | }
34 | });
35 |
36 | //direction booleans become true if the arrow key is released
37 | canvas.addEventListener('keyup', function(e){
38 | if(e.keyCode === 37){
39 | LEFT = false;
40 | }
41 | if(e.keyCode === 38){
42 | UP = false;
43 | }
44 | if(e.keyCode === 39){
45 | RIGHT = false;
46 | }
47 | if(e.keyCode === 40){
48 | DOWN = false;
49 | }
50 | });
51 |
52 | //changes the x or y position depending on the boolean values
53 | function move(){
54 | if(LEFT){
55 | x--;
56 | }
57 | if(UP){
58 | y--;
59 | }
60 | if(RIGHT){
61 | x++;
62 | }
63 | if(DOWN){
64 | y++;
65 | }
66 | }
67 |
68 | //main loop that runs around 60 times per second
69 | function mainLoop() {
70 | ctx.clearRect(0, 0, 640, 480);
71 | move();
72 | drawBall(x, y, 20);
73 | requestAnimationFrame(mainLoop);
74 | }
75 | requestAnimationFrame(mainLoop);
76 |
77 |
--------------------------------------------------------------------------------
/03 - The Ball Class/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | The Ball Class
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/03 - The Ball Class/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | //array that stores all the Ball objects
5 | const BALLZ = [];
6 |
7 | let LEFT, UP, RIGHT, DOWN;
8 |
9 | //creating a Ball class
10 | class Ball{
11 | //special method that gets called at the instantiation
12 | constructor(x, y, r){
13 | this.x = x;
14 | this.y = y;
15 | this.r = r;
16 | this.player = false;
17 | //pushing the Ball object into the BALLZ array
18 | BALLZ.push(this);
19 | }
20 |
21 | drawBall(){
22 | ctx.beginPath();
23 | ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
24 | ctx.strokeStyle = "black";
25 | ctx.stroke();
26 | ctx.fillStyle = "red";
27 | ctx.fill();
28 | ctx.closePath();
29 | }
30 | }
31 |
32 | function keyControl(b){
33 | canvas.addEventListener('keydown', function(e){
34 | if(e.keyCode === 37){
35 | LEFT = true;
36 | }
37 | if(e.keyCode === 38){
38 | UP = true;
39 | }
40 | if(e.keyCode === 39){
41 | RIGHT = true;
42 | }
43 | if(e.keyCode === 40){
44 | DOWN = true;
45 | }
46 | });
47 |
48 | canvas.addEventListener('keyup', function(e){
49 | if(e.keyCode === 37){
50 | LEFT = false;
51 | }
52 | if(e.keyCode === 38){
53 | UP = false;
54 | }
55 | if(e.keyCode === 39){
56 | RIGHT = false;
57 | }
58 | if(e.keyCode === 40){
59 | DOWN = false;
60 | }
61 | });
62 |
63 | if(LEFT){
64 | b.x--;
65 | }
66 | if(UP){
67 | b.y--;
68 | }
69 | if(RIGHT){
70 | b.x++;
71 | }
72 | if(DOWN){
73 | b.y++;
74 | }
75 |
76 | }
77 |
78 | function mainLoop() {
79 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
80 | //iterating through the BALLZ array
81 | BALLZ.forEach((b) => {
82 | b.drawBall();
83 | if (b.player){
84 | keyControl(b);
85 | }
86 | });
87 | requestAnimationFrame(mainLoop);
88 | }
89 |
90 | //create two Ball objects
91 | let Ball1 = new Ball(200, 200, 30);
92 | let Ball2 = new Ball(300, 300, 20);
93 | Ball1.player = true;
94 |
95 | requestAnimationFrame(mainLoop);
96 |
97 |
--------------------------------------------------------------------------------
/04 - Ball Acceleration/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Ball Acceleration
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/04 - Ball Acceleration/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 |
6 | let LEFT, UP, RIGHT, DOWN;
7 |
8 | //velocity gets multiplied by (1-friction)
9 | let friction = 0.1;
10 |
11 | class Ball{
12 | constructor(x, y, r){
13 | this.x = x;
14 | this.y = y;
15 | this.r = r;
16 | this.vel_x = 0;
17 | this.vel_y = 0;
18 | this.acc_x = 0;
19 | this.acc_y = 0;
20 | this.acceleration = 1;
21 | this.player = false;
22 | BALLZ.push(this);
23 | }
24 |
25 | drawBall(){
26 | ctx.beginPath();
27 | ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
28 | ctx.strokeStyle = "black";
29 | ctx.stroke();
30 | ctx.fillStyle = "red";
31 | ctx.fill();
32 | ctx.closePath();
33 | }
34 |
35 | //displaying the current acceleration and the velocity of the ball
36 | display(){
37 | ctx.beginPath();
38 | ctx.moveTo(this.x, this.y);
39 | ctx.lineTo(this.x + this.acc_x*100, this.y + this.acc_y*100);
40 | ctx.strokeStyle = "green";
41 | ctx.stroke();
42 | ctx.closePath();
43 | ctx.beginPath();
44 | ctx.moveTo(this.x, this.y);
45 | ctx.lineTo(this.x + this.vel_x*10, this.y + this.vel_y*10);
46 | ctx.strokeStyle = "blue";
47 | ctx.stroke();
48 | ctx.closePath();
49 | }
50 | }
51 |
52 | function keyControl(b){
53 | canvas.addEventListener('keydown', function(e){
54 | if(e.keyCode === 37){
55 | LEFT = true;
56 | }
57 | if(e.keyCode === 38){
58 | UP = true;
59 | }
60 | if(e.keyCode === 39){
61 | RIGHT = true;
62 | }
63 | if(e.keyCode === 40){
64 | DOWN = true;
65 | }
66 | });
67 |
68 | canvas.addEventListener('keyup', function(e){
69 | if(e.keyCode === 37){
70 | LEFT = false;
71 | }
72 | if(e.keyCode === 38){
73 | UP = false;
74 | }
75 | if(e.keyCode === 39){
76 | RIGHT = false;
77 | }
78 | if(e.keyCode === 40){
79 | DOWN = false;
80 | }
81 | });
82 |
83 | //if true, the accelertion component gets a certain value
84 | if(LEFT){
85 | b.acc_x = -b.acceleration;
86 | }
87 | if(UP){
88 | b.acc_y = -b.acceleration;
89 | }
90 | if(RIGHT){
91 | b.acc_x = b.acceleration;
92 | }
93 | if(DOWN){
94 | b.acc_y = b.acceleration;
95 | }
96 | if(!UP && !DOWN){
97 | b.acc_y = 0;
98 | }
99 | if(!RIGHT && !LEFT){
100 | b.acc_x = 0;
101 | }
102 |
103 | //acceleration values added to the velocity components
104 | b.vel_x += b.acc_x;
105 | b.vel_y += b.acc_y;
106 | //velocity gets multiplied by a number between 0 and 1
107 | b.vel_x *= 1-friction;
108 | b.vel_y *= 1-friction;
109 | //velocity values added to the current x, y position
110 | b.x += b.vel_x;
111 | b.y += b.vel_y;
112 |
113 | }
114 |
115 | function mainLoop() {
116 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
117 | BALLZ.forEach((b) => {
118 | b.drawBall();
119 | if (b.player){
120 | keyControl(b);
121 | }
122 | b.display();
123 | });
124 | requestAnimationFrame(mainLoop);
125 | }
126 |
127 | let Ball1 = new Ball(200, 200, 100);
128 | Ball1.player = true;
129 |
130 | requestAnimationFrame(mainLoop);
131 |
132 |
--------------------------------------------------------------------------------
/05 - Introducing Vectors/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Introducing Vectors
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/05 - Introducing Vectors/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 |
6 | let LEFT, UP, RIGHT, DOWN;
7 | let friction = 0.1;
8 |
9 | //a class Vector with basic vector operations
10 | class Vector{
11 | constructor(x, y){
12 | this.x = x;
13 | this.y = y;
14 | }
15 |
16 | add(v){
17 | return new Vector(this.x+v.x, this.y+v.y);
18 | }
19 |
20 | subtr(v){
21 | return new Vector(this.x-v.x, this.y-v.y);
22 | }
23 |
24 | mag(){
25 | return Math.sqrt(this.x**2 + this.y**2)
26 | }
27 |
28 | mult(n){
29 | return new Vector(this.x*n, this.y*n);
30 | }
31 |
32 | drawVec(start_x, start_y, n, color){
33 | ctx.beginPath();
34 | ctx.moveTo(start_x, start_y);
35 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
36 | ctx.strokeStyle = color;
37 | ctx.stroke();
38 | ctx.closePath();
39 | }
40 | }
41 |
42 | class Ball{
43 | constructor(x, y, r){
44 | this.x = x;
45 | this.y = y;
46 | this.r = r;
47 | this.vel = new Vector(0,0);
48 | this.acc = new Vector(0,0);
49 | this.acceleration = 1;
50 | this.player = false;
51 | BALLZ.push(this);
52 | }
53 |
54 | drawBall(){
55 | ctx.beginPath();
56 | ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
57 | ctx.strokeStyle = "black";
58 | ctx.stroke();
59 | ctx.fillStyle = "red";
60 | ctx.fill();
61 | ctx.closePath();
62 | }
63 |
64 | display(){
65 | this.vel.drawVec(this.x, this.y, 10, "green");
66 | this.acc.drawVec(this.x, this.y, 100, "blue");
67 | }
68 | }
69 |
70 | function keyControl(b){
71 | canvas.addEventListener('keydown', function(e){
72 | if(e.keyCode === 37){
73 | LEFT = true;
74 | }
75 | if(e.keyCode === 38){
76 | UP = true;
77 | }
78 | if(e.keyCode === 39){
79 | RIGHT = true;
80 | }
81 | if(e.keyCode === 40){
82 | DOWN = true;
83 | }
84 | });
85 |
86 | canvas.addEventListener('keyup', function(e){
87 | if(e.keyCode === 37){
88 | LEFT = false;
89 | }
90 | if(e.keyCode === 38){
91 | UP = false;
92 | }
93 | if(e.keyCode === 39){
94 | RIGHT = false;
95 | }
96 | if(e.keyCode === 40){
97 | DOWN = false;
98 | }
99 | });
100 |
101 | if(LEFT){
102 | b.acc.x = -b.acceleration;
103 | }
104 | if(UP){
105 | b.acc.y = -b.acceleration;
106 | }
107 | if(RIGHT){
108 | b.acc.x = b.acceleration;
109 | }
110 | if(DOWN){
111 | b.acc.y = b.acceleration;
112 | }
113 | if(!LEFT && !RIGHT){
114 | b.acc.x = 0;
115 | }
116 | if(!UP && !DOWN){
117 | b.acc.y = 0;
118 | }
119 |
120 | //acceleration vector gets added to the velocity vector
121 | b.vel = b.vel.add(b.acc);
122 | b.vel = b.vel.mult(1-friction);
123 | b.x += b.vel.x;
124 | b.y += b.vel.y;
125 | }
126 |
127 | function mainLoop(timestamp) {
128 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
129 | BALLZ.forEach((b) => {
130 | b.drawBall();
131 | if (b.player){
132 | keyControl(b);
133 | }
134 | b.display();
135 | })
136 | requestAnimationFrame(mainLoop);
137 | }
138 |
139 | let Ball1 = new Ball(200, 200, 30);
140 | Ball1.player = true;
141 |
142 | requestAnimationFrame(mainLoop);
143 |
144 |
--------------------------------------------------------------------------------
/06 - Unit Vector, Dot Product/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Unit Vector, Dot Product
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/06 - Unit Vector, Dot Product/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 |
6 | let LEFT, UP, RIGHT, DOWN;
7 | let friction = 0.1;
8 |
9 | class Vector{
10 | constructor(x, y){
11 | this.x = x;
12 | this.y = y;
13 | }
14 |
15 | add(v){
16 | return new Vector(this.x+v.x, this.y+v.y);
17 | }
18 |
19 | subtr(v){
20 | return new Vector(this.x-v.x, this.y-v.y);
21 | }
22 |
23 | mag(){
24 | return Math.sqrt(this.x**2 + this.y**2);
25 | }
26 |
27 | mult(n){
28 | return new Vector(this.x*n, this.y*n);
29 | }
30 |
31 | //returns a perpendicular normal vector
32 | normal(){
33 | return new Vector(-this.y, this.x).unit();
34 | }
35 |
36 | //returns a vector with same direction and 1 length
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath()
52 | }
53 |
54 | //returns the length of a vectors projection onto the other one
55 | static dot(v1, v2){
56 | return v1.x*v2.x + v1.y*v2.y;
57 | }
58 | }
59 |
60 | class Ball{
61 | constructor(x, y, r){
62 | this.x = x;
63 | this.y = y;
64 | this.r = r;
65 | this.vel = new Vector(0,0);
66 | this.acc = new Vector(0,0);
67 | this.acceleration = 1;
68 | this.player = false;
69 | BALLZ.push(this);
70 | }
71 |
72 | drawBall(){
73 | ctx.beginPath();
74 | ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
75 | ctx.strokeStyle = "black";
76 | ctx.stroke();
77 | ctx.fillStyle = "red";
78 | ctx.fill();
79 | ctx.closePath();
80 | }
81 |
82 | display(){
83 | this.vel.drawVec(550, 400, 10, "green");
84 | this.acc.unit().drawVec(550, 400, 50, "blue");
85 | ctx.beginPath();
86 | ctx.arc(550, 400, 50, 0, 2*Math.PI);
87 | ctx.strokeStyle = "black";
88 | ctx.stroke();
89 | ctx.closePath();
90 | }
91 | }
92 |
93 | function keyControl(b){
94 | canvas.addEventListener('keydown', function(e){
95 | if(e.keyCode === 37){
96 | LEFT = true;
97 | }
98 | if(e.keyCode === 38){
99 | UP = true;
100 | }
101 | if(e.keyCode === 39){
102 | RIGHT = true;
103 | }
104 | if(e.keyCode === 40){
105 | DOWN = true;
106 | }
107 | });
108 |
109 | canvas.addEventListener('keyup', function(e){
110 | if(e.keyCode === 37){
111 | LEFT = false;
112 | }
113 | if(e.keyCode === 38){
114 | UP = false;
115 | }
116 | if(e.keyCode === 39){
117 | RIGHT = false;
118 | }
119 | if(e.keyCode === 40){
120 | DOWN = false;
121 | }
122 | });
123 |
124 | if(LEFT){
125 | b.acc.x = -b.acceleration;
126 | }
127 | if(UP){
128 | b.acc.y = -b.acceleration;
129 | }
130 | if(RIGHT){
131 | b.acc.x = b.acceleration;
132 | }
133 | if(DOWN){
134 | b.acc.y = b.acceleration;
135 | }
136 | if(!LEFT && !RIGHT){
137 | b.acc.x = 0;
138 | }
139 | if(!UP && !DOWN){
140 | b.acc.y = 0;
141 | }
142 |
143 | b.acc = b.acc.unit().mult(b.acceleration);
144 | b.vel = b.vel.add(b.acc);
145 | b.vel = b.vel.mult(1-friction);
146 | b.x += b.vel.x;
147 | b.y += b.vel.y;
148 | }
149 |
150 | function mainLoop(timestamp) {
151 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
152 | BALLZ.forEach((b) => {
153 | b.drawBall();
154 | if (b.player){
155 | keyControl(b);
156 | }
157 | b.display();
158 | })
159 | requestAnimationFrame(mainLoop);
160 | }
161 |
162 | let Ball1 = new Ball(200, 200, 30);
163 | Ball1.player = true;
164 |
165 | requestAnimationFrame(mainLoop);
166 |
167 |
--------------------------------------------------------------------------------
/07 - Ball to Ball Collision/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Ball to Ball Collision
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/07 - Ball to Ball Collision/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 |
6 | let LEFT, UP, RIGHT, DOWN;
7 | let friction = 0.1;
8 |
9 | class Vector{
10 | constructor(x, y){
11 | this.x = x;
12 | this.y = y;
13 | }
14 |
15 | add(v){
16 | return new Vector(this.x+v.x, this.y+v.y);
17 | }
18 |
19 | subtr(v){
20 | return new Vector(this.x-v.x, this.y-v.y);
21 | }
22 |
23 | mag(){
24 | return Math.sqrt(this.x**2 + this.y**2);
25 | }
26 |
27 | mult(n){
28 | return new Vector(this.x*n, this.y*n);
29 | }
30 |
31 | normal(){
32 | return new Vector(-this.y, this.x).unit();
33 | }
34 |
35 | unit(){
36 | if(this.mag() === 0){
37 | return new Vector(0,0);
38 | } else {
39 | return new Vector(this.x/this.mag(), this.y/this.mag());
40 | }
41 | }
42 |
43 | drawVec(start_x, start_y, n, color){
44 | ctx.beginPath();
45 | ctx.moveTo(start_x, start_y);
46 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
47 | ctx.strokeStyle = color;
48 | ctx.stroke();
49 | ctx.closePath();
50 | }
51 |
52 | static dot(v1, v2){
53 | return v1.x*v2.x + v1.y*v2.y;
54 | }
55 | }
56 |
57 | class Ball{
58 | constructor(x, y, r){
59 | this.pos = new Vector(x,y);
60 | this.r = r;
61 | this.vel = new Vector(0,0);
62 | this.acc = new Vector(0,0);
63 | this.acceleration = 1;
64 | this.player = false;
65 | BALLZ.push(this);
66 | }
67 |
68 | drawBall(){
69 | ctx.beginPath();
70 | ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2*Math.PI);
71 | ctx.strokeStyle = "black";
72 | ctx.stroke();
73 | ctx.fillStyle = "red";
74 | ctx.fill();
75 | ctx.closePath();
76 | }
77 |
78 | display(){
79 | this.vel.drawVec(550, 400, 10, "green");
80 | this.acc.unit().drawVec(550, 400, 50, "blue");
81 | ctx.beginPath();
82 | ctx.arc(550, 400, 50, 0, 2*Math.PI);
83 | ctx.strokeStyle = "black";
84 | ctx.stroke();
85 | ctx.closePath();
86 | }
87 | }
88 |
89 | function keyControl(b){
90 | canvas.addEventListener('keydown', function(e){
91 | if(e.keyCode === 37){
92 | LEFT = true;
93 | }
94 | if(e.keyCode === 38){
95 | UP = true;
96 | }
97 | if(e.keyCode === 39){
98 | RIGHT = true;
99 | }
100 | if(e.keyCode === 40){
101 | DOWN = true;
102 | }
103 | });
104 |
105 | canvas.addEventListener('keyup', function(e){
106 | if(e.keyCode === 37){
107 | LEFT = false;
108 | }
109 | if(e.keyCode === 38){
110 | UP = false;
111 | }
112 | if(e.keyCode === 39){
113 | RIGHT = false;
114 | }
115 | if(e.keyCode === 40){
116 | DOWN = false;
117 | }
118 | });
119 |
120 | if(LEFT){
121 | b.acc.x = -b.acceleration;
122 | }
123 | if(UP){
124 | b.acc.y = -b.acceleration;
125 | }
126 | if(RIGHT){
127 | b.acc.x = b.acceleration;
128 | }
129 | if(DOWN){
130 | b.acc.y = b.acceleration;
131 | }
132 | if(!LEFT && !RIGHT){
133 | b.acc.x = 0;
134 | }
135 | if(!UP && !DOWN){
136 | b.acc.y = 0;
137 | }
138 |
139 | b.acc = b.acc.unit().mult(b.acceleration);
140 | b.vel = b.vel.add(b.acc);
141 | b.vel = b.vel.mult(1-friction);
142 | b.pos = b.pos.add(b.vel);
143 | }
144 |
145 | function round(number, precision){
146 | let factor = 10**precision;
147 | return Math.round(number * factor) / factor;
148 | }
149 |
150 | //collision detection between two balls
151 | function coll_det_bb(b1, b2){
152 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
153 | return true;
154 | } else {
155 | return false;
156 | }
157 | }
158 |
159 | //penetration resolution
160 | //repositions the balls based on the penetration depth and the collision normal
161 | function pen_res_bb(b1, b2){
162 | let dist = b1.pos.subtr(b2.pos);
163 | let pen_depth = b1.r + b2.r - dist.mag();
164 | let pen_res = dist.unit().mult(pen_depth/2);
165 | b1.pos = b1.pos.add(pen_res);
166 | b2.pos = b2.pos.add(pen_res.mult(-1));
167 | }
168 |
169 | function mainLoop(timestamp) {
170 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
171 | BALLZ.forEach((b, index) => {
172 | b.drawBall();
173 | if (b.player){
174 | keyControl(b);
175 | }
176 |
177 | //calls the collision detection for each ball pairs
178 | for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Collision Response
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/08 - Collision Response/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 |
6 | let LEFT, UP, RIGHT, DOWN;
7 | let friction = 0.0;
8 |
9 | class Vector{
10 | constructor(x, y){
11 | this.x = x;
12 | this.y = y;
13 | }
14 |
15 | add(v){
16 | return new Vector(this.x+v.x, this.y+v.y);
17 | }
18 |
19 | subtr(v){
20 | return new Vector(this.x-v.x, this.y-v.y);
21 | }
22 |
23 | mag(){
24 | return Math.sqrt(this.x**2 + this.y**2);
25 | }
26 |
27 | mult(n){
28 | return new Vector(this.x*n, this.y*n);
29 | }
30 |
31 | normal(){
32 | return new Vector(-this.y, this.x).unit();
33 | }
34 |
35 | unit(){
36 | if(this.mag() === 0){
37 | return new Vector(0,0);
38 | } else {
39 | return new Vector(this.x/this.mag(), this.y/this.mag());
40 | }
41 | }
42 |
43 | drawVec(start_x, start_y, n, color){
44 | ctx.beginPath();
45 | ctx.moveTo(start_x, start_y);
46 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
47 | ctx.strokeStyle = color;
48 | ctx.stroke();
49 | ctx.closePath();
50 | }
51 |
52 | static dot(v1, v2){
53 | return v1.x*v2.x + v1.y*v2.y;
54 | }
55 | }
56 |
57 | class Ball{
58 | constructor(x, y, r){
59 | this.pos = new Vector(x,y);
60 | this.r = r;
61 | this.vel = new Vector(0,0);
62 | this.acc = new Vector(0,0);
63 | this.acceleration = 1;
64 | this.player = false;
65 | BALLZ.push(this);
66 | }
67 |
68 | drawBall(){
69 | ctx.beginPath();
70 | ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2*Math.PI);
71 | ctx.strokeStyle = "black";
72 | ctx.stroke();
73 | ctx.fillStyle = "red";
74 | ctx.fill();
75 | ctx.closePath();
76 | }
77 |
78 | display(){
79 | this.vel.drawVec(550, 400, 10, "green");
80 | this.acc.unit().drawVec(550, 400, 50, "blue");
81 | ctx.beginPath();
82 | ctx.arc(550, 400, 50, 0, 2*Math.PI);
83 | ctx.strokeStyle = "black";
84 | ctx.stroke();
85 | ctx.closePath();
86 | }
87 |
88 | reposition(){
89 | this.acc = this.acc.unit().mult(this.acceleration);
90 | this.vel = this.vel.add(this.acc);
91 | this.vel = this.vel.mult(1-friction);
92 | this.pos = this.pos.add(this.vel);
93 | }
94 | }
95 |
96 | function keyControl(b){
97 | canvas.addEventListener('keydown', function(e){
98 | if(e.keyCode === 37){
99 | LEFT = true;
100 | }
101 | if(e.keyCode === 38){
102 | UP = true;
103 | }
104 | if(e.keyCode === 39){
105 | RIGHT = true;
106 | }
107 | if(e.keyCode === 40){
108 | DOWN = true;
109 | }
110 | });
111 |
112 | canvas.addEventListener('keyup', function(e){
113 | if(e.keyCode === 37){
114 | LEFT = false;
115 | }
116 | if(e.keyCode === 38){
117 | UP = false;
118 | }
119 | if(e.keyCode === 39){
120 | RIGHT = false;
121 | }
122 | if(e.keyCode === 40){
123 | DOWN = false;
124 | }
125 | });
126 |
127 | if(LEFT){
128 | b.acc.x = -b.acceleration;
129 | }
130 | if(UP){
131 | b.acc.y = -b.acceleration;
132 | }
133 | if(RIGHT){
134 | b.acc.x = b.acceleration;
135 | }
136 | if(DOWN){
137 | b.acc.y = b.acceleration;
138 | }
139 | if(!LEFT && !RIGHT){
140 | b.acc.x = 0;
141 | }
142 | if(!UP && !DOWN){
143 | b.acc.y = 0;
144 | }
145 | }
146 |
147 | function round(number, precision){
148 | let factor = 10**precision;
149 | return Math.round(number * factor) / factor;
150 | }
151 |
152 | function coll_det_bb(b1, b2){
153 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
154 | return true;
155 | } else {
156 | return false;
157 | }
158 | }
159 |
160 | function pen_res_bb(b1, b2){
161 | let dist = b1.pos.subtr(b2.pos);
162 | let pen_depth = b1.r + b2.r - dist.mag();
163 | let pen_res = dist.unit().mult(pen_depth/2);
164 | b1.pos = b1.pos.add(pen_res);
165 | b2.pos = b2.pos.add(pen_res.mult(-1));
166 | }
167 |
168 | //collision resolution
169 | //calculates the balls new velocity vectors after the collision
170 | function coll_res_bb(b1, b2){
171 | //collision normal vector
172 | let normal = b1.pos.subtr(b2.pos).unit();
173 | //relative velocity vector
174 | let relVel = b1.vel.subtr(b2.vel);
175 | //separating velocity - relVel projected onto the collision normal vector
176 | let sepVel = Vector.dot(relVel, normal);
177 | //the projection value after the collision (multiplied by -1)
178 | let new_sepVel = -sepVel;
179 | //collision normal vector with the magnitude of the new_sepVel
180 | let sepVelVec = normal.mult(new_sepVel);
181 |
182 | //adding the separating velocity vector to the original vel. vector
183 | b1.vel = b1.vel.add(sepVelVec);
184 | //adding its opposite to the other balls original vel. vector
185 | b2.vel = b2.vel.add(sepVelVec.mult(-1));
186 | }
187 |
188 | function momentum_display(){
189 | let momentum = Ball1.vel.add(Ball2.vel).mag();
190 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
191 | }
192 |
193 | function mainLoop(timestamp) {
194 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
195 | BALLZ.forEach((b, index) => {
196 | b.drawBall();
197 | if (b.player){
198 | keyControl(b);
199 | }
200 | for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Mass and Elasticity
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/09 - Mass and Elasticity/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 |
6 | let LEFT, UP, RIGHT, DOWN;
7 | let friction = 0.05;
8 |
9 | class Vector{
10 | constructor(x, y){
11 | this.x = x;
12 | this.y = y;
13 | }
14 |
15 | add(v){
16 | return new Vector(this.x+v.x, this.y+v.y);
17 | }
18 |
19 | subtr(v){
20 | return new Vector(this.x-v.x, this.y-v.y);
21 | }
22 |
23 | mag(){
24 | return Math.sqrt(this.x**2 + this.y**2);
25 | }
26 |
27 | mult(n){
28 | return new Vector(this.x*n, this.y*n);
29 | }
30 |
31 | normal(){
32 | return new Vector(-this.y, this.x).unit();
33 | }
34 |
35 | unit(){
36 | if(this.mag() === 0){
37 | return new Vector(0,0);
38 | } else {
39 | return new Vector(this.x/this.mag(), this.y/this.mag());
40 | }
41 | }
42 |
43 | drawVec(start_x, start_y, n, color){
44 | ctx.beginPath();
45 | ctx.moveTo(start_x, start_y);
46 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
47 | ctx.strokeStyle = color;
48 | ctx.stroke();
49 | ctx.closePath();
50 | }
51 |
52 | static dot(v1, v2){
53 | return v1.x*v2.x + v1.y*v2.y;
54 | }
55 | }
56 |
57 | class Ball{
58 | constructor(x, y, r, m){
59 | this.pos = new Vector(x,y);
60 | this.r = r;
61 | this.m = m;
62 | if (this.m === 0){
63 | this.inv_m = 0;
64 | } else {
65 | this.inv_m = 1 / this.m;
66 | }
67 | this.elasticity = 1;
68 | this.vel = new Vector(0,0);
69 | this.acc = new Vector(0,0);
70 | this.acceleration = 1;
71 | this.player = false;
72 | BALLZ.push(this);
73 | }
74 |
75 | drawBall(){
76 | ctx.beginPath();
77 | ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2*Math.PI);
78 | ctx.strokeStyle = "black";
79 | ctx.stroke();
80 | ctx.fillStyle = "red";
81 | ctx.fill();
82 | ctx.closePath();
83 | }
84 |
85 | display(){
86 | this.vel.drawVec(this.pos.x, this.pos.y, 10, "green");
87 | ctx.fillStyle = "black";
88 | ctx.fillText("m = "+this.m, this.pos.x-10, this.pos.y-5);
89 | ctx.fillText("e = "+this.elasticity, this.pos.x-10, this.pos.y+5);
90 | }
91 |
92 | reposition(){
93 | this.acc = this.acc.unit().mult(this.acceleration);
94 | this.vel = this.vel.add(this.acc);
95 | this.vel = this.vel.mult(1-friction);
96 | this.pos = this.pos.add(this.vel);
97 | }
98 | }
99 |
100 | function keyControl(b){
101 | canvas.addEventListener('keydown', function(e){
102 | if(e.keyCode === 37){
103 | LEFT = true;
104 | }
105 | if(e.keyCode === 38){
106 | UP = true;
107 | }
108 | if(e.keyCode === 39){
109 | RIGHT = true;
110 | }
111 | if(e.keyCode === 40){
112 | DOWN = true;
113 | }
114 | });
115 |
116 | canvas.addEventListener('keyup', function(e){
117 | if(e.keyCode === 37){
118 | LEFT = false;
119 | }
120 | if(e.keyCode === 38){
121 | UP = false;
122 | }
123 | if(e.keyCode === 39){
124 | RIGHT = false;
125 | }
126 | if(e.keyCode === 40){
127 | DOWN = false;
128 | }
129 | });
130 |
131 | if(LEFT){
132 | b.acc.x = -b.acceleration;
133 | }
134 | if(UP){
135 | b.acc.y = -b.acceleration;
136 | }
137 | if(RIGHT){
138 | b.acc.x = b.acceleration;
139 | }
140 | if(DOWN){
141 | b.acc.y = b.acceleration;
142 | }
143 | if(!LEFT && !RIGHT){
144 | b.acc.x = 0;
145 | }
146 | if(!UP && !DOWN){
147 | b.acc.y = 0;
148 | }
149 | }
150 |
151 | function round(number, precision){
152 | let factor = 10**precision;
153 | return Math.round(number * factor) / factor;
154 | }
155 |
156 | function randInt(min, max){
157 | return Math.floor(Math.random() * (max-min+1)) + min;
158 | }
159 |
160 | function coll_det_bb(b1, b2){
161 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
162 | return true;
163 | } else {
164 | return false;
165 | }
166 | }
167 |
168 | function pen_res_bb(b1, b2){
169 | let dist = b1.pos.subtr(b2.pos);
170 | let pen_depth = b1.r + b2.r - dist.mag();
171 | //dividing the penetration depth in the ratio of the inverse masses
172 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
173 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
174 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
175 | }
176 |
177 | function coll_res_bb(b1, b2){
178 | let normal = b1.pos.subtr(b2.pos).unit();
179 | let relVel = b1.vel.subtr(b2.vel);
180 | let sepVel = Vector.dot(relVel, normal);
181 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
182 |
183 | //the difference between the new and the original sep.velocity value
184 | let vsep_diff = new_sepVel - sepVel;
185 |
186 | //dividing the impulse value in the ration of the inverse masses
187 | //and adding the impulse vector to the original vel. vectors
188 | //according to their inverse mass
189 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
190 | let impulseVec = normal.mult(impulse);
191 |
192 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
193 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
194 | }
195 |
196 | function momentum_display(){
197 | let momentum = Ball1.vel.add(Ball2.vel).mag();
198 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
199 | }
200 |
201 | function mainLoop(timestamp) {
202 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
203 | BALLZ.forEach((b, index) => {
204 | b.drawBall();
205 | if (b.player){
206 | keyControl(b);
207 | }
208 | for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Adding the Walls
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/10 - Adding the Walls/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 |
7 | let LEFT, UP, RIGHT, DOWN;
8 | let friction = 0.05;
9 |
10 | class Vector{
11 | constructor(x, y){
12 | this.x = x;
13 | this.y = y;
14 | }
15 |
16 | add(v){
17 | return new Vector(this.x+v.x, this.y+v.y);
18 | }
19 |
20 | subtr(v){
21 | return new Vector(this.x-v.x, this.y-v.y);
22 | }
23 |
24 | mag(){
25 | return Math.sqrt(this.x**2 + this.y**2);
26 | }
27 |
28 | mult(n){
29 | return new Vector(this.x*n, this.y*n);
30 | }
31 |
32 | normal(){
33 | return new Vector(-this.y, this.x).unit();
34 | }
35 |
36 | unit(){
37 | if(this.mag() === 0){
38 | return new Vector(0,0);
39 | } else {
40 | return new Vector(this.x/this.mag(), this.y/this.mag());
41 | }
42 | }
43 |
44 | drawVec(start_x, start_y, n, color){
45 | ctx.beginPath();
46 | ctx.moveTo(start_x, start_y);
47 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
48 | ctx.strokeStyle = color;
49 | ctx.stroke();
50 | ctx.closePath();
51 | }
52 |
53 | static dot(v1, v2){
54 | return v1.x*v2.x + v1.y*v2.y;
55 | }
56 | }
57 |
58 | class Ball{
59 | constructor(x, y, r, m){
60 | this.pos = new Vector(x,y);
61 | this.r = r;
62 | this.m = m;
63 | if (this.m === 0){
64 | this.inv_m = 0;
65 | } else {
66 | this.inv_m = 1 / this.m;
67 | }
68 | this.elasticity = 1;
69 | this.vel = new Vector(0,0);
70 | this.acc = new Vector(0,0);
71 | this.acceleration = 1;
72 | this.player = false;
73 | BALLZ.push(this);
74 | }
75 |
76 | drawBall(){
77 | ctx.beginPath();
78 | ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2*Math.PI);
79 | ctx.strokeStyle = "black";
80 | ctx.stroke();
81 | ctx.fillStyle = "red";
82 | ctx.fill();
83 | ctx.closePath();
84 | }
85 |
86 | display(){
87 | this.vel.drawVec(this.pos.x, this.pos.y, 10, "green");
88 | ctx.fillStyle = "black";
89 | ctx.fillText("m = "+this.m, this.pos.x-10, this.pos.y-5);
90 | ctx.fillText("e = "+this.elasticity, this.pos.x-10, this.pos.y+5);
91 | }
92 |
93 | reposition(){
94 | this.acc = this.acc.unit().mult(this.acceleration);
95 | this.vel = this.vel.add(this.acc);
96 | this.vel = this.vel.mult(1-friction);
97 | this.pos = this.pos.add(this.vel);
98 | }
99 | }
100 |
101 | //Walls are line segments between two points
102 | class Wall{
103 | constructor(x1, y1, x2, y2){
104 | this.start = new Vector(x1, y1);
105 | this.end = new Vector(x2, y2);
106 | WALLZ.push(this);
107 | }
108 |
109 | drawWall(){
110 | ctx.beginPath();
111 | ctx.moveTo(this.start.x, this.start.y);
112 | ctx.lineTo(this.end.x, this.end.y);
113 | ctx.strokeStyle = "black";
114 | ctx.stroke();
115 | ctx.closePath();
116 | }
117 |
118 | wallUnit(){
119 | return this.end.subtr(this.start).unit();
120 | }
121 | }
122 |
123 | function keyControl(b){
124 | canvas.addEventListener('keydown', function(e){
125 | if(e.keyCode === 37){
126 | LEFT = true;
127 | }
128 | if(e.keyCode === 38){
129 | UP = true;
130 | }
131 | if(e.keyCode === 39){
132 | RIGHT = true;
133 | }
134 | if(e.keyCode === 40){
135 | DOWN = true;
136 | }
137 | });
138 |
139 | canvas.addEventListener('keyup', function(e){
140 | if(e.keyCode === 37){
141 | LEFT = false;
142 | }
143 | if(e.keyCode === 38){
144 | UP = false;
145 | }
146 | if(e.keyCode === 39){
147 | RIGHT = false;
148 | }
149 | if(e.keyCode === 40){
150 | DOWN = false;
151 | }
152 | });
153 |
154 | if(LEFT){
155 | b.acc.x = -b.acceleration;
156 | }
157 | if(UP){
158 | b.acc.y = -b.acceleration;
159 | }
160 | if(RIGHT){
161 | b.acc.x = b.acceleration;
162 | }
163 | if(DOWN){
164 | b.acc.y = b.acceleration;
165 | }
166 | if(!LEFT && !RIGHT){
167 | b.acc.x = 0;
168 | }
169 | if(!UP && !DOWN){
170 | b.acc.y = 0;
171 | }
172 | }
173 |
174 | function round(number, precision){
175 | let factor = 10**precision;
176 | return Math.round(number * factor) / factor;
177 | }
178 |
179 | function randInt(min, max){
180 | return Math.floor(Math.random() * (max-min+1)) + min;
181 | }
182 |
183 | //returns with the closest point on a line segment to a given point
184 | function closestPointBW(b1, w1){
185 | let ballToWallStart = w1.start.subtr(b1.pos);
186 | if(Vector.dot(w1.wallUnit(), ballToWallStart) > 0){
187 | return w1.start;
188 | }
189 |
190 | let wallEndToBall = b1.pos.subtr(w1.end);
191 | if(Vector.dot(w1.wallUnit(), wallEndToBall) > 0){
192 | return w1.end;
193 | }
194 |
195 | let closestDist = Vector.dot(w1.wallUnit(), ballToWallStart);
196 | let closestVect = w1.wallUnit().mult(closestDist);
197 | return w1.start.subtr(closestVect);
198 | }
199 |
200 | function coll_det_bb(b1, b2){
201 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
202 | return true;
203 | } else {
204 | return false;
205 | }
206 | }
207 |
208 | //collision detection between ball and wall
209 | function coll_det_bw(b1, w1){
210 | let ballToClosest = closestPointBW(b1, w1).subtr(b1.pos);
211 | if (ballToClosest.mag() <= b1.r){
212 | return true;
213 | }
214 | }
215 |
216 | function pen_res_bb(b1, b2){
217 | let dist = b1.pos.subtr(b2.pos);
218 | let pen_depth = b1.r + b2.r - dist.mag();
219 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
220 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
221 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
222 | }
223 |
224 | //penetration resolution between ball and wall
225 | function pen_res_bw(b1, w1){
226 | let penVect = b1.pos.subtr(closestPointBW(b1, w1));
227 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
228 | }
229 |
230 | function coll_res_bb(b1, b2){
231 | let normal = b1.pos.subtr(b2.pos).unit();
232 | let relVel = b1.vel.subtr(b2.vel);
233 | let sepVel = Vector.dot(relVel, normal);
234 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
235 |
236 | let vsep_diff = new_sepVel - sepVel;
237 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
238 | let impulseVec = normal.mult(impulse);
239 |
240 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
241 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
242 | }
243 |
244 | //collision response between ball and wall
245 | function coll_res_bw(b1, w1){
246 | let normal = b1.pos.subtr(closestPointBW(b1, w1)).unit();
247 | let sepVel = Vector.dot(b1.vel, normal);
248 | let new_sepVel = -sepVel * b1.elasticity;
249 | let vsep_diff = sepVel - new_sepVel;
250 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
251 | }
252 |
253 | function momentum_display(){
254 | let momentum = Ball1.vel.add(Ball2.vel).mag();
255 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
256 | }
257 |
258 | function mainLoop(timestamp) {
259 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
260 | BALLZ.forEach((b, index) => {
261 | b.drawBall();
262 | if (b.player){
263 | keyControl(b);
264 | }
265 | //each ball object iterates through each wall object
266 | WALLZ.forEach((w) => {
267 | if(coll_det_bw(BALLZ[index], w)){
268 | pen_res_bw(BALLZ[index], w);
269 | coll_res_bw(BALLZ[index], w);
270 | }
271 | })
272 | for(let i = index+1; i {
284 | w.drawWall();
285 | })
286 |
287 | requestAnimationFrame(mainLoop);
288 | }
289 |
290 | for (let i = 0; i < 10; i++){
291 | let newBall = new Ball(randInt(100,500), randInt(50,400), randInt(20,50), randInt(0,10));
292 | newBall.elasticity = randInt(0,10) / 10;
293 | }
294 | let Wall2 = new Wall(300, 400, 550, 200);
295 |
296 | //walls along the canvas edges
297 | let edge1 = new Wall(0, 0, canvas.clientWidth, 0);
298 | let edge2 = new Wall(canvas.clientWidth, 0, canvas.clientWidth, canvas.clientHeight);
299 | let edge3 = new Wall(canvas.clientWidth, canvas.clientHeight, 0, canvas.clientHeight);
300 | let edge4 = new Wall(0, canvas.clientHeight, 0, 0);
301 | BALLZ[0].player = true;
302 |
303 | //intro if canvas 1138x640
304 | // let Wall1 = new Wall(1000, 350, 1100, 500);
305 | // let Wall2 = new Wall(700, 50, 800, 50);
306 | // let Ball1 = new Ball(-4000, 400, 80, 10);
307 | // let Ball2 = new Ball(960, 585, 35, 2);
308 | // let Ball3 = new Ball(1005, 610, 10, 2);
309 | // let Ball4 = new Ball(1120, 590, 10, 1);
310 | // let Ball45= new Ball(500, 340, 30, 2);
311 | // Ball1.vel.x = 290;
312 |
313 |
314 |
315 | requestAnimationFrame(mainLoop);
316 |
317 |
--------------------------------------------------------------------------------
/11 - Rotation Matrix/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Rotation Matrix
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/11 - Rotation Matrix/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 |
7 | let LEFT, UP, RIGHT, DOWN;
8 | let friction = 0.05;
9 |
10 | class Vector{
11 | constructor(x, y){
12 | this.x = x;
13 | this.y = y;
14 | }
15 |
16 | add(v){
17 | return new Vector(this.x+v.x, this.y+v.y);
18 | }
19 |
20 | subtr(v){
21 | return new Vector(this.x-v.x, this.y-v.y);
22 | }
23 |
24 | mag(){
25 | return Math.sqrt(this.x**2 + this.y**2);
26 | }
27 |
28 | mult(n){
29 | return new Vector(this.x*n, this.y*n);
30 | }
31 |
32 | normal(){
33 | return new Vector(-this.y, this.x).unit();
34 | }
35 |
36 | unit(){
37 | if(this.mag() === 0){
38 | return new Vector(0,0);
39 | } else {
40 | return new Vector(this.x/this.mag(), this.y/this.mag());
41 | }
42 | }
43 |
44 | drawVec(start_x, start_y, n, color){
45 | ctx.beginPath();
46 | ctx.moveTo(start_x, start_y);
47 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
48 | ctx.strokeStyle = color;
49 | ctx.stroke();
50 | ctx.closePath();
51 | }
52 |
53 | static dot(v1, v2){
54 | return v1.x*v2.x + v1.y*v2.y;
55 | }
56 | }
57 |
58 |
59 | //Matrix gets its values set to 0 as default
60 | class Matrix{
61 | constructor(rows, cols){
62 | this.rows = rows;
63 | this.cols = cols;
64 | this.data = [];
65 |
66 | for (let i = 0; i 0){
248 | return w1.start;
249 | }
250 |
251 | let wallEndToBall = b1.pos.subtr(w1.end);
252 | if(Vector.dot(w1.wallUnit(), wallEndToBall) > 0){
253 | return w1.end;
254 | }
255 |
256 | let closestDist = Vector.dot(w1.wallUnit(), ballToWallStart);
257 | let closestVect = w1.wallUnit().mult(closestDist);
258 | return w1.start.subtr(closestVect);
259 | }
260 |
261 | function coll_det_bb(b1, b2){
262 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
263 | return true;
264 | } else {
265 | return false;
266 | }
267 | }
268 |
269 | function coll_det_bw(b1, w1){
270 | let ballToClosest = closestPointBW(b1, w1).subtr(b1.pos);
271 | if (ballToClosest.mag() <= b1.r){
272 | return true;
273 | }
274 | }
275 |
276 | function pen_res_bb(b1, b2){
277 | let dist = b1.pos.subtr(b2.pos);
278 | let pen_depth = b1.r + b2.r - dist.mag();
279 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
280 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
281 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
282 | }
283 |
284 | function pen_res_bw(b1, w1){
285 | let penVect = b1.pos.subtr(closestPointBW(b1, w1));
286 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
287 | }
288 |
289 | function coll_res_bb(b1, b2){
290 | let normal = b1.pos.subtr(b2.pos).unit();
291 | let relVel = b1.vel.subtr(b2.vel);
292 | let sepVel = Vector.dot(relVel, normal);
293 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
294 |
295 | let vsep_diff = new_sepVel - sepVel;
296 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
297 | let impulseVec = normal.mult(impulse);
298 |
299 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
300 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
301 | }
302 |
303 | function coll_res_bw(b1, w1){
304 | let normal = b1.pos.subtr(closestPointBW(b1, w1)).unit();
305 | let sepVel = Vector.dot(b1.vel, normal);
306 | let new_sepVel = -sepVel * b1.elasticity;
307 | let vsep_diff = sepVel - new_sepVel;
308 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
309 | }
310 |
311 | function momentum_display(){
312 | let momentum = Ball1.vel.add(Ball2.vel).mag();
313 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
314 | }
315 |
316 | function mainLoop(timestamp) {
317 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
318 | userInput();
319 | BALLZ.forEach((b, index) => {
320 | b.drawBall();
321 | if (b.player){
322 | b.keyControl();
323 | }
324 | WALLZ.forEach((w) => {
325 | if(coll_det_bw(BALLZ[index], w)){
326 | pen_res_bw(BALLZ[index], w);
327 | coll_res_bw(BALLZ[index], w);
328 | }
329 | })
330 | for(let i = index+1; i {
341 | w.drawWall();
342 | w.keyControl();
343 | w.reposition();
344 | })
345 |
346 | requestAnimationFrame(mainLoop);
347 | }
348 |
349 | let Wall1 = new Wall (200, 200, 400, 200);
350 |
351 |
352 |
353 | requestAnimationFrame(mainLoop);
354 |
355 |
--------------------------------------------------------------------------------
/12 - Steering a Capsule/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Steering a Capsule
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/12 - Steering a Capsule/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 | const CAPS = [];
7 |
8 | let LEFT, UP, RIGHT, DOWN;
9 | let friction = 0.05;
10 |
11 | class Vector{
12 | constructor(x, y){
13 | this.x = x;
14 | this.y = y;
15 | }
16 |
17 | add(v){
18 | return new Vector(this.x+v.x, this.y+v.y);
19 | }
20 |
21 | subtr(v){
22 | return new Vector(this.x-v.x, this.y-v.y);
23 | }
24 |
25 | mag(){
26 | return Math.sqrt(this.x**2 + this.y**2);
27 | }
28 |
29 | mult(n){
30 | return new Vector(this.x*n, this.y*n);
31 | }
32 |
33 | normal(){
34 | return new Vector(-this.y, this.x).unit();
35 | }
36 |
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath();
52 | }
53 |
54 | static dot(v1, v2){
55 | return v1.x*v2.x + v1.y*v2.y;
56 | }
57 |
58 | static cross(v1, v2){
59 | return v1.x*v2.y - v1.y*v2.x;
60 | }
61 | }
62 |
63 | class Matrix{
64 | constructor(rows, cols){
65 | this.rows = rows;
66 | this.cols = cols;
67 | this.data = [];
68 |
69 | for (let i = 0; i 0){
167 | this.refAngle *= -1;
168 | }
169 | CAPS.push(this);
170 | }
171 |
172 | draw(){
173 | ctx.beginPath();
174 | ctx.arc(this.start.x, this.start.y, this.r, this.refAngle+this.angle+Math.PI/2, this.refAngle+this.angle+3*Math.PI/2);
175 | ctx.arc(this.end.x, this.end.y, this.r, this.refAngle+this.angle-Math.PI/2, this.refAngle+this.angle+Math.PI/2);
176 | ctx.closePath();
177 | ctx.strokeStyle = "black";
178 | ctx.stroke();
179 | ctx.fillStyle = "lightgreen";
180 | ctx.fill();
181 | }
182 |
183 | keyControl(){
184 | if(UP){
185 | this.acc = this.dir.mult(-this.acceleration);;
186 | }
187 | if(DOWN){
188 | this.acc = this.dir.mult(this.acceleration);;
189 | }
190 | if(LEFT){
191 | this.angVel = -0.1;
192 | }
193 | if(RIGHT){
194 | this.angVel = 0.1;
195 | }
196 | if(!UP && !DOWN){
197 | this.acc = new Vector(0,0);
198 | }
199 | }
200 |
201 | reposition(){
202 | this.acc = this.acc.unit().mult(this.acceleration);
203 | this.vel = this.vel.add(this.acc);
204 | this.vel = this.vel.mult(1-friction);
205 | this.pos = this.pos.add(this.vel);
206 | this.angle += this.angVel;
207 | this.angVel *= 0.95;
208 | let rotMat = rotMx(this.angle);
209 | this.dir = rotMat.multiplyVec(this.refDir);
210 | this.start = this.pos.add(this.dir.mult(-this.length/2));
211 | this.end = this.pos.add(this.dir.mult(this.length/2));
212 | }
213 | }
214 |
215 | class Wall{
216 | constructor(x1, y1, x2, y2){
217 | this.start = new Vector(x1, y1);
218 | this.end = new Vector(x2, y2);
219 | this.center = this.start.add(this.end).mult(0.5);
220 | this.length = this.end.subtr(this.start).mag();
221 | this.refStart = new Vector(x1, y1);
222 | this.refEnd = new Vector(x2, y2);
223 | this.refUnit = this.end.subtr(this.start).unit();
224 | this.angVel = 0;
225 | this.angle = 0;
226 | WALLZ.push(this);
227 | }
228 |
229 | drawWall(){
230 | let rotMat = rotMx(this.angle);
231 | let newDir = rotMat.multiplyVec(this.refUnit);
232 | this.start = this.center.add(newDir.mult(-this.length/2));
233 | this.end = this.center.add(newDir.mult(this.length/2));
234 | ctx.beginPath();
235 | ctx.moveTo(this.start.x, this.start.y);
236 | ctx.lineTo(this.end.x, this.end.y);
237 | ctx.strokeStyle = "black";
238 | ctx.stroke();
239 | ctx.closePath();
240 | }
241 |
242 | keyControl(){
243 | if(LEFT){
244 | this.angVel = -0.1;
245 | }
246 | if(RIGHT){
247 | this.angVel = 0.1;
248 | }
249 | }
250 |
251 | reposition(){
252 | this.angle += this.angVel;
253 | this.angVel *= 0.99;
254 | }
255 |
256 | wallUnit(){
257 | return this.end.subtr(this.start).unit();
258 | }
259 | }
260 |
261 | function userInput(){
262 | canvas.addEventListener('keydown', function(e){
263 | if(e.keyCode === 37){
264 | LEFT = true;
265 | }
266 | if(e.keyCode === 38){
267 | UP = true;
268 | }
269 | if(e.keyCode === 39){
270 | RIGHT = true;
271 | }
272 | if(e.keyCode === 40){
273 | DOWN = true;
274 | }
275 | });
276 |
277 | canvas.addEventListener('keyup', function(e){
278 | if(e.keyCode === 37){
279 | LEFT = false;
280 | }
281 | if(e.keyCode === 38){
282 | UP = false;
283 | }
284 | if(e.keyCode === 39){
285 | RIGHT = false;
286 | }
287 | if(e.keyCode === 40){
288 | DOWN = false;
289 | }
290 | });
291 | }
292 |
293 | function round(number, precision){
294 | let factor = 10**precision;
295 | return Math.round(number * factor) / factor;
296 | }
297 |
298 | function randInt(min, max){
299 | return Math.floor(Math.random() * (max-min+1)) + min;
300 | }
301 |
302 | function rotMx(angle){
303 | let mx = new Matrix(2,2);
304 | mx.data[0][0] = Math.cos(angle);
305 | mx.data[0][1] = -Math.sin(angle);
306 | mx.data[1][0] = Math.sin(angle);
307 | mx.data[1][1] = Math.cos(angle);
308 | return mx;
309 | }
310 |
311 | function closestPointBW(b1, w1){
312 | let ballToWallStart = w1.start.subtr(b1.pos);
313 | if(Vector.dot(w1.wallUnit(), ballToWallStart) > 0){
314 | return w1.start;
315 | }
316 |
317 | let wallEndToBall = b1.pos.subtr(w1.end);
318 | if(Vector.dot(w1.wallUnit(), wallEndToBall) > 0){
319 | return w1.end;
320 | }
321 |
322 | let closestDist = Vector.dot(w1.wallUnit(), ballToWallStart);
323 | let closestVect = w1.wallUnit().mult(closestDist);
324 | return w1.start.subtr(closestVect);
325 | }
326 |
327 | function coll_det_bb(b1, b2){
328 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
329 | return true;
330 | } else {
331 | return false;
332 | }
333 | }
334 |
335 | function coll_det_bw(b1, w1){
336 | let ballToClosest = closestPointBW(b1, w1).subtr(b1.pos);
337 | if (ballToClosest.mag() <= b1.r){
338 | return true;
339 | }
340 | }
341 |
342 | function pen_res_bb(b1, b2){
343 | let dist = b1.pos.subtr(b2.pos);
344 | let pen_depth = b1.r + b2.r - dist.mag();
345 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
346 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
347 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
348 | }
349 |
350 | function pen_res_bw(b1, w1){
351 | let penVect = b1.pos.subtr(closestPointBW(b1, w1));
352 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
353 | }
354 |
355 | function coll_res_bb(b1, b2){
356 | let normal = b1.pos.subtr(b2.pos).unit();
357 | let relVel = b1.vel.subtr(b2.vel);
358 | let sepVel = Vector.dot(relVel, normal);
359 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
360 |
361 | let vsep_diff = new_sepVel - sepVel;
362 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
363 | let impulseVec = normal.mult(impulse);
364 |
365 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
366 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
367 | }
368 |
369 | function coll_res_bw(b1, w1){
370 | let normal = b1.pos.subtr(closestPointBW(b1, w1)).unit();
371 | let sepVel = Vector.dot(b1.vel, normal);
372 | let new_sepVel = -sepVel * b1.elasticity;
373 | let vsep_diff = sepVel - new_sepVel;
374 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
375 | }
376 |
377 | function momentum_display(){
378 | let momentum = Ball1.vel.add(Ball2.vel).mag();
379 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
380 | }
381 |
382 | function mainLoop(timestamp) {
383 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
384 | userInput();
385 | BALLZ.forEach((b, index) => {
386 | b.drawBall();
387 | if (b.player){
388 | b.keyControl();
389 | }
390 | WALLZ.forEach((w) => {
391 | if(coll_det_bw(BALLZ[index], w)){
392 | pen_res_bw(BALLZ[index], w);
393 | coll_res_bw(BALLZ[index], w);
394 | }
395 | })
396 | for(let i = index+1; i {
407 | w.drawWall();
408 | w.keyControl();
409 | w.reposition();
410 | })
411 |
412 | CAPS.forEach((c) => {
413 | c.draw();
414 | c.keyControl();
415 | c.reposition();
416 | })
417 |
418 | requestAnimationFrame(mainLoop);
419 | }
420 |
421 | let Caps1 = new Capsule(200, 200, 300, 300, 40);
422 |
423 |
424 |
425 | requestAnimationFrame(mainLoop);
426 |
427 |
--------------------------------------------------------------------------------
/13 - Capsule to Capsule/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Capsule to Capsule
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/13 - Capsule to Capsule/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 | const CAPS = [];
7 |
8 | let LEFT, UP, RIGHT, DOWN;
9 | let friction = 0.05;
10 |
11 | class Vector{
12 | constructor(x, y){
13 | this.x = x;
14 | this.y = y;
15 | }
16 |
17 | add(v){
18 | return new Vector(this.x+v.x, this.y+v.y);
19 | }
20 |
21 | subtr(v){
22 | return new Vector(this.x-v.x, this.y-v.y);
23 | }
24 |
25 | mag(){
26 | return Math.sqrt(this.x**2 + this.y**2);
27 | }
28 |
29 | mult(n){
30 | return new Vector(this.x*n, this.y*n);
31 | }
32 |
33 | normal(){
34 | return new Vector(-this.y, this.x).unit();
35 | }
36 |
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath();
52 | }
53 |
54 | static dot(v1, v2){
55 | return v1.x*v2.x + v1.y*v2.y;
56 | }
57 |
58 | static cross(v1, v2){
59 | return v1.x*v2.y - v1.y*v2.x;
60 | }
61 | }
62 |
63 | class Matrix{
64 | constructor(rows, cols){
65 | this.rows = rows;
66 | this.cols = cols;
67 | this.data = [];
68 |
69 | for (let i = 0; i 0){
172 | this.refAngle *= -1;
173 | }
174 | this.player = false;
175 | CAPS.push(this);
176 | }
177 |
178 | draw(){
179 | ctx.beginPath();
180 | ctx.arc(this.start.x, this.start.y, this.r, this.refAngle+this.angle+Math.PI/2, this.refAngle+this.angle+3*Math.PI/2);
181 | ctx.arc(this.end.x, this.end.y, this.r, this.refAngle+this.angle-Math.PI/2, this.refAngle+this.angle+Math.PI/2);
182 | ctx.closePath();
183 | ctx.moveTo(this.start.x, this.start.y);
184 | ctx.lineTo(this.end.x, this.end.y);
185 | ctx.strokeStyle = "black";
186 | ctx.stroke();
187 | //ctx.fillStyle = "lightgreen";
188 | //ctx.fill();
189 | }
190 |
191 | keyControl(){
192 | if(UP){
193 | this.acc = this.dir.mult(-this.acceleration);;
194 | }
195 | if(DOWN){
196 | this.acc = this.dir.mult(this.acceleration);;
197 | }
198 | if(LEFT){
199 | this.angVel = -0.1;
200 | }
201 | if(RIGHT){
202 | this.angVel = 0.1;
203 | }
204 | if(!UP && !DOWN){
205 | this.acc = new Vector(0,0);
206 | }
207 | }
208 |
209 | reposition(){
210 | this.acc = this.acc.unit().mult(this.acceleration);
211 | this.vel = this.vel.add(this.acc);
212 | this.vel = this.vel.mult(1-friction);
213 | this.pos = this.pos.add(this.vel);
214 | this.angle += this.angVel;
215 | this.angVel *= 0.95;
216 | let rotMat = rotMx(this.angle);
217 | this.dir = rotMat.multiplyVec(this.refDir);
218 | this.start = this.pos.add(this.dir.mult(-this.length/2));
219 | this.end = this.pos.add(this.dir.mult(this.length/2));
220 | }
221 | }
222 |
223 | class Wall{
224 | constructor(x1, y1, x2, y2){
225 | this.start = new Vector(x1, y1);
226 | this.end = new Vector(x2, y2);
227 | this.dir = this.end.subtr(this.start).unit();
228 | this.center = this.start.add(this.end).mult(0.5);
229 | this.length = this.end.subtr(this.start).mag();
230 | this.refStart = new Vector(x1, y1);
231 | this.refEnd = new Vector(x2, y2);
232 | this.refUnit = this.end.subtr(this.start).unit();
233 | this.angVel = 0;
234 | this.angle = 0;
235 | WALLZ.push(this);
236 | }
237 |
238 | drawWall(){
239 | let rotMat = rotMx(this.angle);
240 | let newDir = rotMat.multiplyVec(this.refUnit);
241 | this.start = this.center.add(newDir.mult(-this.length/2));
242 | this.end = this.center.add(newDir.mult(this.length/2));
243 | ctx.beginPath();
244 | ctx.moveTo(this.start.x, this.start.y);
245 | ctx.lineTo(this.end.x, this.end.y);
246 | ctx.strokeStyle = "black";
247 | ctx.stroke();
248 | ctx.closePath();
249 | }
250 |
251 | keyControl(){
252 | if(LEFT){
253 | this.angVel = -0.1;
254 | }
255 | if(RIGHT){
256 | this.angVel = 0.1;
257 | }
258 | }
259 |
260 | reposition(){
261 | this.angle += this.angVel;
262 | this.angVel *= 0.99;
263 | }
264 | }
265 |
266 | function userInput(){
267 | canvas.addEventListener('keydown', function(e){
268 | if(e.keyCode === 37){
269 | LEFT = true;
270 | }
271 | if(e.keyCode === 38){
272 | UP = true;
273 | }
274 | if(e.keyCode === 39){
275 | RIGHT = true;
276 | }
277 | if(e.keyCode === 40){
278 | DOWN = true;
279 | }
280 | });
281 |
282 | canvas.addEventListener('keyup', function(e){
283 | if(e.keyCode === 37){
284 | LEFT = false;
285 | }
286 | if(e.keyCode === 38){
287 | UP = false;
288 | }
289 | if(e.keyCode === 39){
290 | RIGHT = false;
291 | }
292 | if(e.keyCode === 40){
293 | DOWN = false;
294 | }
295 | });
296 | }
297 |
298 | function round(number, precision){
299 | let factor = 10**precision;
300 | return Math.round(number * factor) / factor;
301 | }
302 |
303 | function randInt(min, max){
304 | return Math.floor(Math.random() * (max-min+1)) + min;
305 | }
306 |
307 | function rotMx(angle){
308 | let mx = new Matrix(2,2);
309 | mx.data[0][0] = Math.cos(angle);
310 | mx.data[0][1] = -Math.sin(angle);
311 | mx.data[1][0] = Math.sin(angle);
312 | mx.data[1][1] = Math.cos(angle);
313 | return mx;
314 | }
315 |
316 | function closestPointOnLS(p, w1){
317 | let ballToWallStart = w1.start.subtr(p);
318 | if(Vector.dot(w1.dir, ballToWallStart) > 0){
319 | return w1.start;
320 | }
321 |
322 | let wallEndToBall = p.subtr(w1.end);
323 | if(Vector.dot(w1.dir, wallEndToBall) > 0){
324 | return w1.end;
325 | }
326 |
327 | let closestDist = Vector.dot(w1.dir, ballToWallStart);
328 | let closestVect = w1.dir.mult(closestDist);
329 | return w1.start.subtr(closestVect);
330 | }
331 |
332 | function closestPointsBetweenLS(c1, c2){
333 | let shortestDist = closestPointOnLS(c1.start, c2).subtr(c1.start).mag();
334 | let closestPoints = [c1.start, closestPointOnLS(c1.start, c2)];
335 | if(closestPointOnLS(c1.end, c2).subtr(c1.end).mag() < shortestDist){
336 | shortestDist = closestPointOnLS(c1.end, c2).subtr(c1.end).mag();
337 | closestPoints = [c1.end, closestPointOnLS(c1.end, c2)];
338 | }
339 | if(closestPointOnLS(c2.start, c1).subtr(c2.start).mag() < shortestDist){
340 | shortestDist = closestPointOnLS(c2.start, c1).subtr(c2.start).mag();
341 | closestPoints = [closestPointOnLS(c2.start, c1), c2.start];
342 | }
343 | if(closestPointOnLS(c2.end, c1).subtr(c2.end).mag() < shortestDist){
344 | shortestDist = closestPointOnLS(c2.end, c1).subtr(c2.end).mag();
345 | closestPoints = [closestPointOnLS(c2.end, c1), c2.end];
346 | }
347 | ctx.strokeStyle = "red";
348 | ctx.beginPath();
349 | ctx.moveTo(closestPoints[0].x, closestPoints[0].y);
350 | ctx.lineTo(closestPoints[1].x, closestPoints[1].y);
351 | ctx.closePath();
352 | ctx.stroke();
353 | ctx.beginPath();
354 | ctx.arc(closestPoints[0].x, closestPoints[0].y, c1.r, 0, 2*Math.PI);
355 | ctx.closePath();
356 | ctx.stroke();
357 | ctx.beginPath();
358 | ctx.arc(closestPoints[1].x, closestPoints[1].y, c2.r, 0, 2*Math.PI);
359 | ctx.closePath();
360 | ctx.stroke();
361 | return closestPoints;
362 | }
363 |
364 | function coll_det_bb(b1, b2){
365 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
366 | return true;
367 | } else {
368 | return false;
369 | }
370 | }
371 |
372 | function coll_det_bw(b1, w1){
373 | let ballToClosest = closestPointOnLS(b1.pos, w1).subtr(b1.pos);
374 | if (ballToClosest.mag() <= b1.r){
375 | return true;
376 | }
377 | }
378 |
379 | //Capsule - Capsule collision detection
380 | function coll_det_cc(c1, c2){
381 | if(c1.r + c2.r >= closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).mag()){
382 | return true;
383 | } else {
384 | return false;
385 | }
386 | }
387 |
388 | function pen_res_bb(b1, b2){
389 | let dist = b1.pos.subtr(b2.pos);
390 | let pen_depth = b1.r + b2.r - dist.mag();
391 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
392 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
393 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
394 | }
395 |
396 | function pen_res_bw(b1, w1){
397 | let penVect = b1.pos.subtr(closestPointOnLS(b1.pos, w1));
398 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
399 | }
400 |
401 | //Capsule - Capsule penetration resolution
402 | function pen_res_cc(c1, c2){
403 | let dist = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]);
404 | let pen_depth = c1.r + c2.r - dist.mag();
405 | let pen_res = dist.unit().mult(pen_depth / (c1.inv_m + c2.inv_m));
406 | c1.pos = c1.pos.add(pen_res.mult(c1.inv_m));
407 | c2.pos = c2.pos.add(pen_res.mult(-c2.inv_m));
408 | }
409 |
410 | function coll_res_bb(b1, b2){
411 | let normal = b1.pos.subtr(b2.pos).unit();
412 | let relVel = b1.vel.subtr(b2.vel);
413 | let sepVel = Vector.dot(relVel, normal);
414 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
415 |
416 | let vsep_diff = new_sepVel - sepVel;
417 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
418 | let impulseVec = normal.mult(impulse);
419 |
420 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
421 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
422 | }
423 |
424 | function coll_res_bw(b1, w1){
425 | let normal = b1.pos.subtr(closestPointOnLS(b1.pos, w1)).unit();
426 | let sepVel = Vector.dot(b1.vel, normal);
427 | let new_sepVel = -sepVel * b1.elasticity;
428 | let vsep_diff = sepVel - new_sepVel;
429 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
430 | }
431 |
432 | //Capsule - Capsule linear collision response
433 | function coll_res_cc(c1, c2){
434 | let normal = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).unit();
435 | let relVel = c1.vel.subtr(c2.vel);
436 | let sepVel = Vector.dot(relVel, normal);
437 | let new_sepVel = -sepVel * Math.min(c1.elasticity, c2.elasticity);
438 |
439 | let vsep_diff = new_sepVel - sepVel;
440 | let impulse = vsep_diff / (c1.inv_m + c2.inv_m);
441 | let impulseVec = normal.mult(impulse);
442 |
443 | c1.vel = c1.vel.add(impulseVec.mult(c1.inv_m));
444 | c2.vel = c2.vel.add(impulseVec.mult(-c2.inv_m));
445 | }
446 |
447 | function momentum_display(){
448 | let momentum = Ball1.vel.add(Ball2.vel).mag();
449 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
450 | }
451 |
452 | function mainLoop(timestamp) {
453 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
454 | userInput();
455 | BALLZ.forEach((b, index) => {
456 | b.drawBall();
457 | if (b.player){
458 | b.keyControl();
459 | }
460 | WALLZ.forEach((w) => {
461 | if(coll_det_bw(BALLZ[index], w)){
462 | pen_res_bw(BALLZ[index], w);
463 | coll_res_bw(BALLZ[index], w);
464 | }
465 | })
466 | for(let i = index+1; i {
477 | w.drawWall();
478 | w.keyControl();
479 | w.reposition();
480 | })
481 |
482 | CAPS.forEach((c, index) => {
483 | c.draw();
484 | if(c.player === true){
485 | c.keyControl();
486 | }
487 | for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Rotational Collision
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/14 - Rotational Collision/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 | const CAPS = [];
7 |
8 | let LEFT, UP, RIGHT, DOWN;
9 | let friction = 0.05;
10 |
11 | class Vector{
12 | constructor(x, y){
13 | this.x = x;
14 | this.y = y;
15 | }
16 |
17 | add(v){
18 | return new Vector(this.x+v.x, this.y+v.y);
19 | }
20 |
21 | subtr(v){
22 | return new Vector(this.x-v.x, this.y-v.y);
23 | }
24 |
25 | mag(){
26 | return Math.sqrt(this.x**2 + this.y**2);
27 | }
28 |
29 | mult(n){
30 | return new Vector(this.x*n, this.y*n);
31 | }
32 |
33 | normal(){
34 | return new Vector(-this.y, this.x).unit();
35 | }
36 |
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath();
52 | }
53 |
54 | static dot(v1, v2){
55 | return v1.x*v2.x + v1.y*v2.y;
56 | }
57 |
58 | static cross(v1, v2){
59 | return v1.x*v2.y - v1.y*v2.x;
60 | }
61 | }
62 |
63 | class Matrix{
64 | constructor(rows, cols){
65 | this.rows = rows;
66 | this.cols = cols;
67 | this.data = [];
68 |
69 | for (let i = 0; i 0){
179 | this.refAngle *= -1;
180 | }
181 | this.player = false;
182 | CAPS.push(this);
183 | }
184 |
185 | draw(){
186 | ctx.beginPath();
187 | ctx.arc(this.start.x, this.start.y, this.r, this.refAngle+this.angle+Math.PI/2, this.refAngle+this.angle+3*Math.PI/2);
188 | ctx.arc(this.end.x, this.end.y, this.r, this.refAngle+this.angle-Math.PI/2, this.refAngle+this.angle+Math.PI/2);
189 | ctx.closePath();
190 | //ctx.moveTo(this.start.x, this.start.y);
191 | //ctx.lineTo(this.end.x, this.end.y);
192 | ctx.strokeStyle = "black";
193 | ctx.stroke();
194 | ctx.fillStyle = "lightgreen";
195 | ctx.fill();
196 | }
197 |
198 | keyControl(){
199 | if(UP){
200 | this.acc = this.dir.mult(-this.acceleration);;
201 | }
202 | if(DOWN){
203 | this.acc = this.dir.mult(this.acceleration);;
204 | }
205 | if(LEFT){
206 | this.angVel = -0.1;
207 | }
208 | if(RIGHT){
209 | this.angVel = 0.1;
210 | }
211 | if(!UP && !DOWN){
212 | this.acc = new Vector(0,0);
213 | }
214 | }
215 |
216 | reposition(){
217 | this.acc = this.acc.unit().mult(this.acceleration);
218 | this.vel = this.vel.add(this.acc);
219 | this.vel = this.vel.mult(1-friction);
220 | this.pos = this.pos.add(this.vel);
221 | this.angle += this.angVel;
222 | this.angVel *= 0.95;
223 | let rotMat = rotMx(this.angle);
224 | this.dir = rotMat.multiplyVec(this.refDir);
225 | this.start = this.pos.add(this.dir.mult(-this.length/2));
226 | this.end = this.pos.add(this.dir.mult(this.length/2));
227 | }
228 | }
229 |
230 | class Wall{
231 | constructor(x1, y1, x2, y2){
232 | this.start = new Vector(x1, y1);
233 | this.end = new Vector(x2, y2);
234 | this.dir = this.end.subtr(this.start).unit();
235 | this.center = this.start.add(this.end).mult(0.5);
236 | this.length = this.end.subtr(this.start).mag();
237 | this.refStart = new Vector(x1, y1);
238 | this.refEnd = new Vector(x2, y2);
239 | this.refUnit = this.end.subtr(this.start).unit();
240 | this.angVel = 0;
241 | this.angle = 0;
242 | WALLZ.push(this);
243 | }
244 |
245 | drawWall(){
246 | let rotMat = rotMx(this.angle);
247 | let newDir = rotMat.multiplyVec(this.refUnit);
248 | this.start = this.center.add(newDir.mult(-this.length/2));
249 | this.end = this.center.add(newDir.mult(this.length/2));
250 | ctx.beginPath();
251 | ctx.moveTo(this.start.x, this.start.y);
252 | ctx.lineTo(this.end.x, this.end.y);
253 | ctx.strokeStyle = "black";
254 | ctx.stroke();
255 | ctx.closePath();
256 | }
257 |
258 | keyControl(){
259 | if(LEFT){
260 | this.angVel = -0.1;
261 | }
262 | if(RIGHT){
263 | this.angVel = 0.1;
264 | }
265 | }
266 |
267 | reposition(){
268 | this.angle += this.angVel;
269 | this.angVel *= 0.99;
270 | }
271 | }
272 |
273 | function userInput(){
274 | canvas.addEventListener('keydown', function(e){
275 | if(e.keyCode === 37){
276 | LEFT = true;
277 | }
278 | if(e.keyCode === 38){
279 | UP = true;
280 | }
281 | if(e.keyCode === 39){
282 | RIGHT = true;
283 | }
284 | if(e.keyCode === 40){
285 | DOWN = true;
286 | }
287 | });
288 |
289 | canvas.addEventListener('keyup', function(e){
290 | if(e.keyCode === 37){
291 | LEFT = false;
292 | }
293 | if(e.keyCode === 38){
294 | UP = false;
295 | }
296 | if(e.keyCode === 39){
297 | RIGHT = false;
298 | }
299 | if(e.keyCode === 40){
300 | DOWN = false;
301 | }
302 | });
303 | }
304 |
305 | function round(number, precision){
306 | let factor = 10**precision;
307 | return Math.round(number * factor) / factor;
308 | }
309 |
310 | function randInt(min, max){
311 | return Math.floor(Math.random() * (max-min+1)) + min;
312 | }
313 |
314 | function rotMx(angle){
315 | let mx = new Matrix(2,2);
316 | mx.data[0][0] = Math.cos(angle);
317 | mx.data[0][1] = -Math.sin(angle);
318 | mx.data[1][0] = Math.sin(angle);
319 | mx.data[1][1] = Math.cos(angle);
320 | return mx;
321 | }
322 |
323 | function closestPointOnLS(p, w1){
324 | let ballToWallStart = w1.start.subtr(p);
325 | if(Vector.dot(w1.dir, ballToWallStart) > 0){
326 | return w1.start;
327 | }
328 |
329 | let wallEndToBall = p.subtr(w1.end);
330 | if(Vector.dot(w1.dir, wallEndToBall) > 0){
331 | return w1.end;
332 | }
333 |
334 | let closestDist = Vector.dot(w1.dir, ballToWallStart);
335 | let closestVect = w1.dir.mult(closestDist);
336 | return w1.start.subtr(closestVect);
337 | }
338 |
339 | function closestPointsBetweenLS(c1, c2){
340 | let shortestDist = closestPointOnLS(c1.start, c2).subtr(c1.start).mag();
341 | let closestPoints = [c1.start, closestPointOnLS(c1.start, c2)];
342 | if(closestPointOnLS(c1.end, c2).subtr(c1.end).mag() < shortestDist){
343 | shortestDist = closestPointOnLS(c1.end, c2).subtr(c1.end).mag();
344 | closestPoints = [c1.end, closestPointOnLS(c1.end, c2)];
345 | }
346 | if(closestPointOnLS(c2.start, c1).subtr(c2.start).mag() < shortestDist){
347 | shortestDist = closestPointOnLS(c2.start, c1).subtr(c2.start).mag();
348 | closestPoints = [closestPointOnLS(c2.start, c1), c2.start];
349 | }
350 | if(closestPointOnLS(c2.end, c1).subtr(c2.end).mag() < shortestDist){
351 | shortestDist = closestPointOnLS(c2.end, c1).subtr(c2.end).mag();
352 | closestPoints = [closestPointOnLS(c2.end, c1), c2.end];
353 | }
354 | ctx.strokeStyle = "red";
355 | ctx.beginPath();
356 | ctx.moveTo(closestPoints[0].x, closestPoints[0].y);
357 | ctx.lineTo(closestPoints[1].x, closestPoints[1].y);
358 | ctx.closePath();
359 | ctx.stroke();
360 | ctx.beginPath();
361 | ctx.arc(closestPoints[0].x, closestPoints[0].y, c1.r, 0, 2*Math.PI);
362 | ctx.closePath();
363 | ctx.stroke();
364 | ctx.beginPath();
365 | ctx.arc(closestPoints[1].x, closestPoints[1].y, c2.r, 0, 2*Math.PI);
366 | ctx.closePath();
367 | ctx.stroke();
368 | return closestPoints;
369 | }
370 |
371 | function coll_det_bb(b1, b2){
372 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
373 | return true;
374 | } else {
375 | return false;
376 | }
377 | }
378 |
379 | function coll_det_bw(b1, w1){
380 | let ballToClosest = closestPointOnLS(b1.pos, w1).subtr(b1.pos);
381 | if (ballToClosest.mag() <= b1.r){
382 | return true;
383 | }
384 | }
385 |
386 | function coll_det_cc(c1, c2){
387 | if(c1.r + c2.r >= closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).mag()){
388 | return true;
389 | } else {
390 | return false;
391 | }
392 | }
393 |
394 | function pen_res_bb(b1, b2){
395 | let dist = b1.pos.subtr(b2.pos);
396 | let pen_depth = b1.r + b2.r - dist.mag();
397 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
398 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
399 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
400 | }
401 |
402 | function pen_res_bw(b1, w1){
403 | let penVect = b1.pos.subtr(closestPointOnLS(b1.pos, w1));
404 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
405 | }
406 |
407 | function pen_res_cc(c1, c2){
408 | let dist = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]);
409 | let pen_depth = c1.r + c2.r - dist.mag();
410 | let pen_res = dist.unit().mult(pen_depth / (c1.inv_m + c2.inv_m));
411 | c1.pos = c1.pos.add(pen_res.mult(c1.inv_m));
412 | c2.pos = c2.pos.add(pen_res.mult(-c2.inv_m));
413 | }
414 |
415 | function coll_res_bb(b1, b2){
416 | let normal = b1.pos.subtr(b2.pos).unit();
417 | let relVel = b1.vel.subtr(b2.vel);
418 | let sepVel = Vector.dot(relVel, normal);
419 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
420 |
421 | let vsep_diff = new_sepVel - sepVel;
422 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
423 | let impulseVec = normal.mult(impulse);
424 |
425 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
426 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
427 | }
428 |
429 | function coll_res_bw(b1, w1){
430 | let normal = b1.pos.subtr(closestPointOnLS(b1.pos, w1)).unit();
431 | let sepVel = Vector.dot(b1.vel, normal);
432 | let new_sepVel = -sepVel * b1.elasticity;
433 | let vsep_diff = sepVel - new_sepVel;
434 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
435 | }
436 |
437 | //Capsule - Capsule collision response, rotation included
438 | function coll_res_cc(c1, c2){
439 | let normal = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).unit();
440 |
441 | //1. Closing velocity
442 | let collArm1 = closestPointsBetweenLS(c1, c2)[0].subtr(c1.pos).add(normal.mult(c1.r));
443 | let rotVel1 = new Vector(-c1.angVel * collArm1.y, c1.angVel * collArm1.x);
444 | let closVel1 = c1.vel.add(rotVel1);
445 | let collArm2 = closestPointsBetweenLS(c1, c2)[1].subtr(c2.pos).add(normal.mult(-c2.r));
446 | let rotVel2= new Vector(-c2.angVel * collArm2.y, c2.angVel * collArm2.x);
447 | let closVel2 = c2.vel.add(rotVel2);
448 |
449 | //2. Impulse augmentation
450 | let impAug1 = Vector.cross(collArm1, normal);
451 | impAug1 = impAug1 * c1.inv_inertia * impAug1;
452 | let impAug2 = Vector.cross(collArm2, normal);
453 | impAug2 = impAug2 * c2.inv_inertia * impAug2;
454 |
455 | let relVel = closVel1.subtr(closVel2);
456 | let sepVel = Vector.dot(relVel, normal);
457 | let new_sepVel = -sepVel * Math.min(c1.elasticity, c2.elasticity);
458 | let vsep_diff = new_sepVel - sepVel;
459 |
460 | let impulse = vsep_diff / (c1.inv_m + c2.inv_m + impAug1 + impAug2);
461 | let impulseVec = normal.mult(impulse);
462 |
463 | //3. Changing the velocities
464 | c1.vel = c1.vel.add(impulseVec.mult(c1.inv_m));
465 | c2.vel = c2.vel.add(impulseVec.mult(-c2.inv_m));
466 |
467 | c1.angVel += c1.inv_inertia * Vector.cross(collArm1, impulseVec);
468 | c2.angVel -= c2.inv_inertia * Vector.cross(collArm2, impulseVec);
469 | }
470 |
471 | function momentum_display(){
472 | let momentum = Ball1.vel.add(Ball2.vel).mag();
473 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
474 | }
475 |
476 | function mainLoop(timestamp) {
477 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
478 | userInput();
479 | BALLZ.forEach((b, index) => {
480 | b.drawBall();
481 | if (b.player){
482 | b.keyControl();
483 | }
484 | WALLZ.forEach((w) => {
485 | if(coll_det_bw(BALLZ[index], w)){
486 | pen_res_bw(BALLZ[index], w);
487 | coll_res_bw(BALLZ[index], w);
488 | }
489 | })
490 | for(let i = index+1; i {
501 | w.drawWall();
502 | w.keyControl();
503 | w.reposition();
504 | })
505 |
506 | CAPS.forEach((c, index) => {
507 | c.draw();
508 | if(c.player === true){
509 | c.keyControl();
510 | }
511 | for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Separating Axis Theorem
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/15 - Separating Axis Theorem/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 | const CAPS = [];
7 |
8 | let LEFT, UP, RIGHT, DOWN;
9 | let friction = 0.05;
10 |
11 | class Vector{
12 | constructor(x, y){
13 | this.x = x;
14 | this.y = y;
15 | }
16 |
17 | add(v){
18 | return new Vector(this.x+v.x, this.y+v.y);
19 | }
20 |
21 | subtr(v){
22 | return new Vector(this.x-v.x, this.y-v.y);
23 | }
24 |
25 | mag(){
26 | return Math.sqrt(this.x**2 + this.y**2);
27 | }
28 |
29 | mult(n){
30 | return new Vector(this.x*n, this.y*n);
31 | }
32 |
33 | normal(){
34 | return new Vector(-this.y, this.x).unit();
35 | }
36 |
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath();
52 | }
53 |
54 | static dot(v1, v2){
55 | return v1.x*v2.x + v1.y*v2.y;
56 | }
57 |
58 | static cross(v1, v2){
59 | return v1.x*v2.y - v1.y*v2.x;
60 | }
61 | }
62 |
63 | class Matrix{
64 | constructor(rows, cols){
65 | this.rows = rows;
66 | this.cols = cols;
67 | this.data = [];
68 |
69 | for (let i = 0; i 0){
179 | this.refAngle *= -1;
180 | }
181 | this.player = false;
182 | CAPS.push(this);
183 | }
184 |
185 | draw(){
186 | ctx.beginPath();
187 | ctx.arc(this.start.x, this.start.y, this.r, this.refAngle+this.angle+Math.PI/2, this.refAngle+this.angle+3*Math.PI/2);
188 | ctx.arc(this.end.x, this.end.y, this.r, this.refAngle+this.angle-Math.PI/2, this.refAngle+this.angle+Math.PI/2);
189 | ctx.closePath();
190 | //ctx.moveTo(this.start.x, this.start.y);
191 | //ctx.lineTo(this.end.x, this.end.y);
192 | ctx.strokeStyle = "black";
193 | ctx.stroke();
194 | ctx.fillStyle = "lightgreen";
195 | ctx.fill();
196 | }
197 |
198 | keyControl(){
199 | if(UP){
200 | this.acc = this.dir.mult(-this.acceleration);;
201 | }
202 | if(DOWN){
203 | this.acc = this.dir.mult(this.acceleration);;
204 | }
205 | if(LEFT){
206 | this.angVel = -0.1;
207 | }
208 | if(RIGHT){
209 | this.angVel = 0.1;
210 | }
211 | if(!UP && !DOWN){
212 | this.acc = new Vector(0,0);
213 | }
214 | }
215 |
216 | reposition(){
217 | this.acc = this.acc.unit().mult(this.acceleration);
218 | this.vel = this.vel.add(this.acc);
219 | this.vel = this.vel.mult(1-friction);
220 | this.pos = this.pos.add(this.vel);
221 | this.angle += this.angVel;
222 | this.angVel *= 0.95;
223 | let rotMat = rotMx(this.angle);
224 | this.dir = rotMat.multiplyVec(this.refDir);
225 | this.start = this.pos.add(this.dir.mult(-this.length/2));
226 | this.end = this.pos.add(this.dir.mult(this.length/2));
227 | }
228 | }
229 |
230 | //creating a new class
231 | class Box{
232 | constructor(x1, y1, x2, y2, w, m){
233 | this.vertex = [];
234 | this.vertex[0] = new Vector(x1, y1);
235 | this.vertex[1] = new Vector(x2, y2);
236 | this.edge = this.vertex[1].subtr(this.vertex[0]);
237 | this.length = this.edge.mag();
238 | this.dir = this.edge.unit();
239 | this.refDir = this.edge.unit();
240 | this.width = w;
241 | this.vertex[2] = this.vertex[1].add(this.dir.normal().mult(this.width));
242 | this.vertex[3] = this.vertex[2].add(this.dir.mult(-this.length));
243 |
244 | this.m = m;
245 | if (this.m === 0){
246 | this.inv_m = 0;
247 | } else {
248 | this.inv_m = 1 / this.m;
249 | }
250 | this.inertia = this.m * (this.width**2 +(this.length+2*this.width)**2) / 12;
251 | if (this.m === 0){
252 | this.inv_inertia = 0;
253 | } else {
254 | this.inv_inertia = 1 / this.inertia;
255 | }
256 | this.elasticity = 1;
257 | this.pos = this.vertex[0].add(this.dir.mult(this.length/2)).add(this.dir.normal().mult(this.width/2));
258 | this.vel = new Vector(0,0);
259 | this.acc = new Vector(0,0);
260 | this.acceleration = 1;
261 | this.angVel = 0;
262 | this.angle = 0;
263 | this.player = false;
264 | }
265 |
266 | draw(){
267 | ctx.beginPath();
268 | ctx.moveTo(this.vertex[0].x, this.vertex[0].y);
269 | ctx.lineTo(this.vertex[1].x, this.vertex[1].y);
270 | ctx.lineTo(this.vertex[2].x, this.vertex[2].y);
271 | ctx.lineTo(this.vertex[3].x, this.vertex[3].y);
272 | ctx.lineTo(this.vertex[0].x, this.vertex[0].y);
273 | ctx.strokeStyle = "black";
274 | ctx.stroke();
275 | ctx.closePath();
276 | testCircle(this.pos.x, this.pos.y);
277 | }
278 |
279 | keyControl(){
280 | if(UP){
281 | this.acc = this.dir.mult(-this.acceleration);;
282 | }
283 | if(DOWN){
284 | this.acc = this.dir.mult(this.acceleration);;
285 | }
286 | if(LEFT){
287 | this.angVel = -0.1;
288 | }
289 | if(RIGHT){
290 | this.angVel = 0.1;
291 | }
292 | if(!UP && !DOWN){
293 | this.acc = new Vector(0,0);
294 | }
295 | }
296 |
297 | reposition(){
298 | this.acc = this.acc.unit().mult(this.acceleration);
299 | this.vel = this.vel.add(this.acc);
300 | this.vel = this.vel.mult(1-friction);
301 | this.pos = this.pos.add(this.vel);
302 | this.angle += this.angVel;
303 | this.angVel *= 0.95;
304 | let rotMat = rotMx(this.angle);
305 | this.dir = rotMat.multiplyVec(this.refDir);
306 | this.vertex[0] = this.pos.add(this.dir.mult(-this.length/2)).add(this.dir.normal().mult(this.width/2));
307 | this.vertex[1] = this.pos.add(this.dir.mult(-this.length/2)).add(this.dir.normal().mult(-this.width/2));
308 | this.vertex[2] = this.pos.add(this.dir.mult(this.length/2)).add(this.dir.normal().mult(-this.width/2));
309 | this.vertex[3] = this.pos.add(this.dir.mult(this.length/2)).add(this.dir.normal().mult(this.width/2));
310 | }
311 | }
312 |
313 | class Wall{
314 | constructor(x1, y1, x2, y2){
315 | this.start = new Vector(x1, y1);
316 | this.end = new Vector(x2, y2);
317 | this.vertex = [this.start, this.end];
318 | this.dir = this.end.subtr(this.start).unit();
319 | this.center = this.start.add(this.end).mult(0.5);
320 | this.length = this.end.subtr(this.start).mag();
321 | this.refStart = new Vector(x1, y1);
322 | this.refEnd = new Vector(x2, y2);
323 | this.refUnit = this.end.subtr(this.start).unit();
324 | this.angVel = 0;
325 | this.angle = 0;
326 | WALLZ.push(this);
327 | }
328 |
329 | draw(){
330 | let rotMat = rotMx(this.angle);
331 | let newDir = rotMat.multiplyVec(this.refUnit);
332 | this.start = this.center.add(newDir.mult(-this.length/2));
333 | this.end = this.center.add(newDir.mult(this.length/2));
334 | ctx.beginPath();
335 | ctx.moveTo(this.start.x, this.start.y);
336 | ctx.lineTo(this.end.x, this.end.y);
337 | ctx.strokeStyle = "black";
338 | ctx.stroke();
339 | ctx.closePath();
340 | }
341 |
342 | keyControl(){
343 | if(LEFT){
344 | this.angVel = -0.1;
345 | }
346 | if(RIGHT){
347 | this.angVel = 0.1;
348 | }
349 | }
350 |
351 | reposition(){
352 | this.angle += this.angVel;
353 | this.angVel *= 0.99;
354 | }
355 | }
356 |
357 | function userInput(){
358 | canvas.addEventListener('keydown', function(e){
359 | if(e.keyCode === 37){
360 | LEFT = true;
361 | }
362 | if(e.keyCode === 38){
363 | UP = true;
364 | }
365 | if(e.keyCode === 39){
366 | RIGHT = true;
367 | }
368 | if(e.keyCode === 40){
369 | DOWN = true;
370 | }
371 | });
372 |
373 | canvas.addEventListener('keyup', function(e){
374 | if(e.keyCode === 37){
375 | LEFT = false;
376 | }
377 | if(e.keyCode === 38){
378 | UP = false;
379 | }
380 | if(e.keyCode === 39){
381 | RIGHT = false;
382 | }
383 | if(e.keyCode === 40){
384 | DOWN = false;
385 | }
386 | });
387 | }
388 |
389 | function round(number, precision){
390 | let factor = 10**precision;
391 | return Math.round(number * factor) / factor;
392 | }
393 |
394 | function randInt(min, max){
395 | return Math.floor(Math.random() * (max-min+1)) + min;
396 | }
397 |
398 | function testCircle(x, y, color="black"){
399 | ctx.beginPath();
400 | ctx.arc(x, y, 10, 0, 2*Math.PI);
401 | ctx.strokeStyle = color;
402 | ctx.stroke();
403 | ctx.closePath();
404 | }
405 |
406 | function rotMx(angle){
407 | let mx = new Matrix(2,2);
408 | mx.data[0][0] = Math.cos(angle);
409 | mx.data[0][1] = -Math.sin(angle);
410 | mx.data[1][0] = Math.sin(angle);
411 | mx.data[1][1] = Math.cos(angle);
412 | return mx;
413 | }
414 |
415 | function closestPointOnLS(p, w1){
416 | let ballToWallStart = w1.start.subtr(p);
417 | if(Vector.dot(w1.dir, ballToWallStart) > 0){
418 | return w1.start;
419 | }
420 |
421 | let wallEndToBall = p.subtr(w1.end);
422 | if(Vector.dot(w1.dir, wallEndToBall) > 0){
423 | return w1.end;
424 | }
425 |
426 | let closestDist = Vector.dot(w1.dir, ballToWallStart);
427 | let closestVect = w1.dir.mult(closestDist);
428 | return w1.start.subtr(closestVect);
429 | }
430 |
431 | function closestPointsBetweenLS(c1, c2){
432 | let shortestDist = closestPointOnLS(c1.start, c2).subtr(c1.start).mag();
433 | let closestPoints = [c1.start, closestPointOnLS(c1.start, c2)];
434 | if(closestPointOnLS(c1.end, c2).subtr(c1.end).mag() < shortestDist){
435 | shortestDist = closestPointOnLS(c1.end, c2).subtr(c1.end).mag();
436 | closestPoints = [c1.end, closestPointOnLS(c1.end, c2)];
437 | }
438 | if(closestPointOnLS(c2.start, c1).subtr(c2.start).mag() < shortestDist){
439 | shortestDist = closestPointOnLS(c2.start, c1).subtr(c2.start).mag();
440 | closestPoints = [closestPointOnLS(c2.start, c1), c2.start];
441 | }
442 | if(closestPointOnLS(c2.end, c1).subtr(c2.end).mag() < shortestDist){
443 | shortestDist = closestPointOnLS(c2.end, c1).subtr(c2.end).mag();
444 | closestPoints = [closestPointOnLS(c2.end, c1), c2.end];
445 | }
446 | ctx.strokeStyle = "red";
447 | ctx.beginPath();
448 | ctx.moveTo(closestPoints[0].x, closestPoints[0].y);
449 | ctx.lineTo(closestPoints[1].x, closestPoints[1].y);
450 | ctx.closePath();
451 | ctx.stroke();
452 | ctx.beginPath();
453 | ctx.arc(closestPoints[0].x, closestPoints[0].y, c1.r, 0, 2*Math.PI);
454 | ctx.closePath();
455 | ctx.stroke();
456 | ctx.beginPath();
457 | ctx.arc(closestPoints[1].x, closestPoints[1].y, c2.r, 0, 2*Math.PI);
458 | ctx.closePath();
459 | ctx.stroke();
460 | return closestPoints;
461 | }
462 |
463 | function coll_det_bb(b1, b2){
464 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
465 | return true;
466 | } else {
467 | return false;
468 | }
469 | }
470 |
471 | function coll_det_bw(b1, w1){
472 | let ballToClosest = closestPointOnLS(b1.pos, w1).subtr(b1.pos);
473 | if (ballToClosest.mag() <= b1.r){
474 | return true;
475 | }
476 | }
477 |
478 | function coll_det_cc(c1, c2){
479 | if(c1.r + c2.r >= closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).mag()){
480 | return true;
481 | } else {
482 | return false;
483 | }
484 | }
485 |
486 | function pen_res_bb(b1, b2){
487 | let dist = b1.pos.subtr(b2.pos);
488 | let pen_depth = b1.r + b2.r - dist.mag();
489 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
490 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
491 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
492 | }
493 |
494 | function pen_res_bw(b1, w1){
495 | let penVect = b1.pos.subtr(closestPointOnLS(b1.pos, w1));
496 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
497 | }
498 |
499 | function pen_res_cc(c1, c2){
500 | let dist = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]);
501 | let pen_depth = c1.r + c2.r - dist.mag();
502 | let pen_res = dist.unit().mult(pen_depth / (c1.inv_m + c2.inv_m));
503 | c1.pos = c1.pos.add(pen_res.mult(c1.inv_m));
504 | c2.pos = c2.pos.add(pen_res.mult(-c2.inv_m));
505 | }
506 |
507 | function coll_res_bb(b1, b2){
508 | let normal = b1.pos.subtr(b2.pos).unit();
509 | let relVel = b1.vel.subtr(b2.vel);
510 | let sepVel = Vector.dot(relVel, normal);
511 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
512 |
513 | let vsep_diff = new_sepVel - sepVel;
514 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
515 | let impulseVec = normal.mult(impulse);
516 |
517 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
518 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
519 | }
520 |
521 | function coll_res_bw(b1, w1){
522 | let normal = b1.pos.subtr(closestPointOnLS(b1.pos, w1)).unit();
523 | let sepVel = Vector.dot(b1.vel, normal);
524 | let new_sepVel = -sepVel * b1.elasticity;
525 | let vsep_diff = sepVel - new_sepVel;
526 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
527 | }
528 |
529 | //Capsule - Capsule collision response, rotation included
530 | function coll_res_cc(c1, c2){
531 | let normal = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).unit();
532 |
533 | //1. Closing velocity
534 | let collArm1 = closestPointsBetweenLS(c1, c2)[0].subtr(c1.pos).add(normal.mult(c1.r));
535 | let rotVel1 = new Vector(-c1.angVel * collArm1.y, c1.angVel * collArm1.x);
536 | let closVel1 = c1.vel.add(rotVel1);
537 | let collArm2 = closestPointsBetweenLS(c1, c2)[1].subtr(c2.pos).add(normal.mult(-c2.r));
538 | let rotVel2= new Vector(-c2.angVel * collArm2.y, c2.angVel * collArm2.x);
539 | let closVel2 = c2.vel.add(rotVel2);
540 |
541 | //2. Impulse augmentation
542 | let impAug1 = Vector.cross(collArm1, normal);
543 | impAug1 = impAug1 * c1.inv_inertia * impAug1;
544 | let impAug2 = Vector.cross(collArm2, normal);
545 | impAug2 = impAug2 * c2.inv_inertia * impAug2;
546 |
547 | let relVel = closVel1.subtr(closVel2);
548 | let sepVel = Vector.dot(relVel, normal);
549 | let new_sepVel = -sepVel * Math.min(c1.elasticity, c2.elasticity);
550 | let vsep_diff = new_sepVel - sepVel;
551 |
552 | let impulse = vsep_diff / (c1.inv_m + c2.inv_m + impAug1 + impAug2);
553 | let impulseVec = normal.mult(impulse);
554 |
555 | //3. Changing the velocities
556 | c1.vel = c1.vel.add(impulseVec.mult(c1.inv_m));
557 | c2.vel = c2.vel.add(impulseVec.mult(-c2.inv_m));
558 |
559 | c1.angVel += c1.inv_inertia * Vector.cross(collArm1, impulseVec);
560 | c2.angVel -= c2.inv_inertia * Vector.cross(collArm2, impulseVec);
561 | }
562 |
563 | //applying the separating axis theorem on two objects
564 | function sat(o1, o2){
565 | axes1 = [];
566 | axes2 = [];
567 | axes1.push(o1.dir.normal());
568 | axes1.push(o1.dir);
569 | axes2.push(o2.dir.normal());
570 | axes2.push(o2.dir);
571 | let proj1, proj2 = 0;
572 |
573 | for(let i=0; imax){
604 | max = p;
605 | }
606 | }
607 | return {
608 | min: min,
609 | max: max
610 | }
611 | }
612 |
613 | function momentum_display(){
614 | let momentum = Ball1.vel.add(Ball2.vel).mag();
615 | ctx.fillText("Momentum: "+round(momentum, 4), 500, 330);
616 | }
617 |
618 | function mainLoop(timestamp) {
619 | ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
620 | userInput();
621 | // BALLZ.forEach((b, index) => {
622 | // b.drawBall();
623 | // if (b.player){
624 | // b.keyControl();
625 | // }
626 | // WALLZ.forEach((w) => {
627 | // if(coll_det_bw(BALLZ[index], w)){
628 | // pen_res_bw(BALLZ[index], w);
629 | // coll_res_bw(BALLZ[index], w);
630 | // }
631 | // })
632 | // for(let i = index+1; i {
643 | // w.drawWall();
644 | // w.keyControl();
645 | // w.reposition();
646 | // })
647 |
648 | // CAPS.forEach((c, index) => {
649 | // c.draw();
650 | // if(c.player === true){
651 | // c.keyControl();
652 | // }
653 | // for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Minimum Translation Vector
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/16 - Minimum Translation Vector/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 | const CAPS = [];
7 |
8 | let LEFT, UP, RIGHT, DOWN;
9 | let friction = 0.05;
10 |
11 | class Vector{
12 | constructor(x, y){
13 | this.x = x;
14 | this.y = y;
15 | }
16 |
17 | add(v){
18 | return new Vector(this.x+v.x, this.y+v.y);
19 | }
20 |
21 | subtr(v){
22 | return new Vector(this.x-v.x, this.y-v.y);
23 | }
24 |
25 | mag(){
26 | return Math.sqrt(this.x**2 + this.y**2);
27 | }
28 |
29 | mult(n){
30 | return new Vector(this.x*n, this.y*n);
31 | }
32 |
33 | normal(){
34 | return new Vector(-this.y, this.x).unit();
35 | }
36 |
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath();
52 | }
53 |
54 | static dot(v1, v2){
55 | return v1.x*v2.x + v1.y*v2.y;
56 | }
57 |
58 | static cross(v1, v2){
59 | return v1.x*v2.y - v1.y*v2.x;
60 | }
61 | }
62 |
63 | class Matrix{
64 | constructor(rows, cols){
65 | this.rows = rows;
66 | this.cols = cols;
67 | this.data = [];
68 |
69 | for (let i = 0; i 0){
180 | this.refAngle *= -1;
181 | }
182 | this.player = false;
183 | CAPS.push(this);
184 | }
185 |
186 | draw(){
187 | ctx.beginPath();
188 | ctx.arc(this.start.x, this.start.y, this.r, this.refAngle+this.angle+Math.PI/2, this.refAngle+this.angle+3*Math.PI/2);
189 | ctx.arc(this.end.x, this.end.y, this.r, this.refAngle+this.angle-Math.PI/2, this.refAngle+this.angle+Math.PI/2);
190 | ctx.closePath();
191 | //ctx.moveTo(this.start.x, this.start.y);
192 | //ctx.lineTo(this.end.x, this.end.y);
193 | ctx.strokeStyle = "black";
194 | ctx.stroke();
195 | ctx.fillStyle = "lightgreen";
196 | ctx.fill();
197 | }
198 |
199 | keyControl(){
200 | if(UP){
201 | this.acc = this.dir.mult(-this.acceleration);;
202 | }
203 | if(DOWN){
204 | this.acc = this.dir.mult(this.acceleration);;
205 | }
206 | if(LEFT){
207 | this.angVel = -0.1;
208 | }
209 | if(RIGHT){
210 | this.angVel = 0.1;
211 | }
212 | if(!UP && !DOWN){
213 | this.acc = new Vector(0,0);
214 | }
215 | }
216 |
217 | reposition(){
218 | this.acc = this.acc.unit().mult(this.acceleration);
219 | this.vel = this.vel.add(this.acc);
220 | this.vel = this.vel.mult(1-friction);
221 | this.pos = this.pos.add(this.vel);
222 | this.angle += this.angVel;
223 | this.angVel *= 0.95;
224 | let rotMat = rotMx(this.angle);
225 | this.dir = rotMat.multiplyVec(this.refDir);
226 | this.start = this.pos.add(this.dir.mult(-this.length/2));
227 | this.end = this.pos.add(this.dir.mult(this.length/2));
228 | }
229 | }
230 |
231 | //creating a new class
232 | class Box{
233 | constructor(x1, y1, x2, y2, w, m){
234 | this.vertex = [];
235 | this.vertex[0] = new Vector(x1, y1);
236 | this.vertex[1] = new Vector(x2, y2);
237 | this.edge = this.vertex[1].subtr(this.vertex[0]);
238 | this.length = this.edge.mag();
239 | this.dir = this.edge.unit();
240 | this.refDir = this.edge.unit();
241 | this.width = w;
242 | this.vertex[2] = this.vertex[1].add(this.dir.normal().mult(this.width));
243 | this.vertex[3] = this.vertex[2].add(this.dir.mult(-this.length));
244 |
245 | this.m = m;
246 | if (this.m === 0){
247 | this.inv_m = 0;
248 | } else {
249 | this.inv_m = 1 / this.m;
250 | }
251 | this.inertia = this.m * (this.width**2 +(this.length+2*this.width)**2) / 12;
252 | if (this.m === 0){
253 | this.inv_inertia = 0;
254 | } else {
255 | this.inv_inertia = 1 / this.inertia;
256 | }
257 | this.elasticity = 1;
258 | this.pos = this.vertex[0].add(this.dir.mult(this.length/2)).add(this.dir.normal().mult(this.width/2));
259 | this.vel = new Vector(0,0);
260 | this.acc = new Vector(0,0);
261 | this.acceleration = 1;
262 | this.angVel = 0;
263 | this.angle = 0;
264 | this.player = false;
265 | }
266 |
267 | draw(){
268 | ctx.beginPath();
269 | ctx.moveTo(this.vertex[0].x, this.vertex[0].y);
270 | ctx.lineTo(this.vertex[1].x, this.vertex[1].y);
271 | ctx.lineTo(this.vertex[2].x, this.vertex[2].y);
272 | ctx.lineTo(this.vertex[3].x, this.vertex[3].y);
273 | ctx.lineTo(this.vertex[0].x, this.vertex[0].y);
274 | ctx.strokeStyle = "black";
275 | ctx.stroke();
276 | ctx.closePath();
277 | testCircle(this.pos.x, this.pos.y);
278 | }
279 |
280 | keyControl(){
281 | if(UP){
282 | this.acc = this.dir.mult(-this.acceleration);;
283 | }
284 | if(DOWN){
285 | this.acc = this.dir.mult(this.acceleration);;
286 | }
287 | if(LEFT){
288 | this.angVel = -0.1;
289 | }
290 | if(RIGHT){
291 | this.angVel = 0.1;
292 | }
293 | if(!UP && !DOWN){
294 | this.acc = new Vector(0,0);
295 | }
296 | }
297 |
298 | reposition(){
299 | this.acc = this.acc.unit().mult(this.acceleration);
300 | this.vel = this.vel.add(this.acc);
301 | this.vel = this.vel.mult(1-friction);
302 | this.pos = this.pos.add(this.vel);
303 | this.angle += this.angVel;
304 | this.angVel *= 0.6;
305 | let rotMat = rotMx(this.angle);
306 | this.dir = rotMat.multiplyVec(this.refDir);
307 | this.vertex[0] = this.pos.add(this.dir.mult(-this.length/2)).add(this.dir.normal().mult(this.width/2));
308 | this.vertex[1] = this.pos.add(this.dir.mult(-this.length/2)).add(this.dir.normal().mult(-this.width/2));
309 | this.vertex[2] = this.pos.add(this.dir.mult(this.length/2)).add(this.dir.normal().mult(-this.width/2));
310 | this.vertex[3] = this.pos.add(this.dir.mult(this.length/2)).add(this.dir.normal().mult(this.width/2));
311 | }
312 | }
313 |
314 | class Wall{
315 | constructor(x1, y1, x2, y2){
316 | this.start = new Vector(x1, y1);
317 | this.end = new Vector(x2, y2);
318 | this.vertex = [this.start, this.end];
319 | this.dir = this.end.subtr(this.start).unit();
320 | this.center = this.start.add(this.end).mult(0.5);
321 | this.length = this.end.subtr(this.start).mag();
322 | this.refStart = new Vector(x1, y1);
323 | this.refEnd = new Vector(x2, y2);
324 | this.refUnit = this.end.subtr(this.start).unit();
325 | this.angVel = 0;
326 | this.angle = 0;
327 | WALLZ.push(this);
328 | }
329 |
330 | draw(){
331 | let rotMat = rotMx(this.angle);
332 | let newDir = rotMat.multiplyVec(this.refUnit);
333 | this.start = this.center.add(newDir.mult(-this.length/2));
334 | this.end = this.center.add(newDir.mult(this.length/2));
335 | ctx.beginPath();
336 | ctx.moveTo(this.start.x, this.start.y);
337 | ctx.lineTo(this.end.x, this.end.y);
338 | ctx.strokeStyle = "black";
339 | ctx.stroke();
340 | ctx.closePath();
341 | }
342 |
343 | keyControl(){
344 | if(LEFT){
345 | this.angVel = -0.1;
346 | }
347 | if(RIGHT){
348 | this.angVel = 0.1;
349 | }
350 | }
351 |
352 | reposition(){
353 | this.angle += this.angVel;
354 | this.angVel *= 0.99;
355 | }
356 | }
357 |
358 | function userInput(){
359 | canvas.addEventListener('keydown', function(e){
360 | if(e.keyCode === 37){
361 | LEFT = true;
362 | }
363 | if(e.keyCode === 38){
364 | UP = true;
365 | }
366 | if(e.keyCode === 39){
367 | RIGHT = true;
368 | }
369 | if(e.keyCode === 40){
370 | DOWN = true;
371 | }
372 | });
373 |
374 | canvas.addEventListener('keyup', function(e){
375 | if(e.keyCode === 37){
376 | LEFT = false;
377 | }
378 | if(e.keyCode === 38){
379 | UP = false;
380 | }
381 | if(e.keyCode === 39){
382 | RIGHT = false;
383 | }
384 | if(e.keyCode === 40){
385 | DOWN = false;
386 | }
387 | });
388 | }
389 |
390 | function round(number, precision){
391 | let factor = 10**precision;
392 | return Math.round(number * factor) / factor;
393 | }
394 |
395 | function randInt(min, max){
396 | return Math.floor(Math.random() * (max-min+1)) + min;
397 | }
398 |
399 | function testCircle(x, y, color="black"){
400 | ctx.beginPath();
401 | ctx.arc(x, y, 10, 0, 2*Math.PI);
402 | ctx.strokeStyle = color;
403 | ctx.stroke();
404 | ctx.closePath();
405 | }
406 |
407 | function rotMx(angle){
408 | let mx = new Matrix(2,2);
409 | mx.data[0][0] = Math.cos(angle);
410 | mx.data[0][1] = -Math.sin(angle);
411 | mx.data[1][0] = Math.sin(angle);
412 | mx.data[1][1] = Math.cos(angle);
413 | return mx;
414 | }
415 |
416 | function closestPointOnLS(p, w1){
417 | let ballToWallStart = w1.start.subtr(p);
418 | if(Vector.dot(w1.dir, ballToWallStart) > 0){
419 | return w1.start;
420 | }
421 |
422 | let wallEndToBall = p.subtr(w1.end);
423 | if(Vector.dot(w1.dir, wallEndToBall) > 0){
424 | return w1.end;
425 | }
426 |
427 | let closestDist = Vector.dot(w1.dir, ballToWallStart);
428 | let closestVect = w1.dir.mult(closestDist);
429 | return w1.start.subtr(closestVect);
430 | }
431 |
432 | function closestPointsBetweenLS(c1, c2){
433 | let shortestDist = closestPointOnLS(c1.start, c2).subtr(c1.start).mag();
434 | let closestPoints = [c1.start, closestPointOnLS(c1.start, c2)];
435 | if(closestPointOnLS(c1.end, c2).subtr(c1.end).mag() < shortestDist){
436 | shortestDist = closestPointOnLS(c1.end, c2).subtr(c1.end).mag();
437 | closestPoints = [c1.end, closestPointOnLS(c1.end, c2)];
438 | }
439 | if(closestPointOnLS(c2.start, c1).subtr(c2.start).mag() < shortestDist){
440 | shortestDist = closestPointOnLS(c2.start, c1).subtr(c2.start).mag();
441 | closestPoints = [closestPointOnLS(c2.start, c1), c2.start];
442 | }
443 | if(closestPointOnLS(c2.end, c1).subtr(c2.end).mag() < shortestDist){
444 | shortestDist = closestPointOnLS(c2.end, c1).subtr(c2.end).mag();
445 | closestPoints = [closestPointOnLS(c2.end, c1), c2.end];
446 | }
447 | ctx.strokeStyle = "red";
448 | ctx.beginPath();
449 | ctx.moveTo(closestPoints[0].x, closestPoints[0].y);
450 | ctx.lineTo(closestPoints[1].x, closestPoints[1].y);
451 | ctx.closePath();
452 | ctx.stroke();
453 | ctx.beginPath();
454 | ctx.arc(closestPoints[0].x, closestPoints[0].y, c1.r, 0, 2*Math.PI);
455 | ctx.closePath();
456 | ctx.stroke();
457 | ctx.beginPath();
458 | ctx.arc(closestPoints[1].x, closestPoints[1].y, c2.r, 0, 2*Math.PI);
459 | ctx.closePath();
460 | ctx.stroke();
461 | return closestPoints;
462 | }
463 |
464 | function coll_det_bb(b1, b2){
465 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
466 | return true;
467 | } else {
468 | return false;
469 | }
470 | }
471 |
472 | function coll_det_bw(b1, w1){
473 | let ballToClosest = closestPointOnLS(b1.pos, w1).subtr(b1.pos);
474 | if (ballToClosest.mag() <= b1.r){
475 | return true;
476 | }
477 | }
478 |
479 | function coll_det_cc(c1, c2){
480 | if(c1.r + c2.r >= closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).mag()){
481 | return true;
482 | } else {
483 | return false;
484 | }
485 | }
486 |
487 | function pen_res_bb(b1, b2){
488 | let dist = b1.pos.subtr(b2.pos);
489 | let pen_depth = b1.r + b2.r - dist.mag();
490 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
491 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
492 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
493 | }
494 |
495 | function pen_res_bw(b1, w1){
496 | let penVect = b1.pos.subtr(closestPointOnLS(b1.pos, w1));
497 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
498 | }
499 |
500 | function pen_res_cc(c1, c2){
501 | let dist = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]);
502 | let pen_depth = c1.r + c2.r - dist.mag();
503 | let pen_res = dist.unit().mult(pen_depth / (c1.inv_m + c2.inv_m));
504 | c1.pos = c1.pos.add(pen_res.mult(c1.inv_m));
505 | c2.pos = c2.pos.add(pen_res.mult(-c2.inv_m));
506 | }
507 |
508 | function coll_res_bb(b1, b2){
509 | let normal = b1.pos.subtr(b2.pos).unit();
510 | let relVel = b1.vel.subtr(b2.vel);
511 | let sepVel = Vector.dot(relVel, normal);
512 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
513 |
514 | let vsep_diff = new_sepVel - sepVel;
515 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
516 | let impulseVec = normal.mult(impulse);
517 |
518 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
519 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
520 | }
521 |
522 | function coll_res_bw(b1, w1){
523 | let normal = b1.pos.subtr(closestPointOnLS(b1.pos, w1)).unit();
524 | let sepVel = Vector.dot(b1.vel, normal);
525 | let new_sepVel = -sepVel * b1.elasticity;
526 | let vsep_diff = sepVel - new_sepVel;
527 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
528 | }
529 |
530 | function coll_res_cc(c1, c2){
531 | let normal = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).unit();
532 |
533 | //1. Closing velocity
534 | let collArm1 = closestPointsBetweenLS(c1, c2)[0].subtr(c1.pos).add(normal.mult(c1.r));
535 | let rotVel1 = new Vector(-c1.angVel * collArm1.y, c1.angVel * collArm1.x);
536 | let closVel1 = c1.vel.add(rotVel1);
537 | let collArm2 = closestPointsBetweenLS(c1, c2)[1].subtr(c2.pos).add(normal.mult(-c2.r));
538 | let rotVel2= new Vector(-c2.angVel * collArm2.y, c2.angVel * collArm2.x);
539 | let closVel2 = c2.vel.add(rotVel2);
540 |
541 | //2. Impulse augmentation
542 | let impAug1 = Vector.cross(collArm1, normal);
543 | impAug1 = impAug1 * c1.inv_inertia * impAug1;
544 | let impAug2 = Vector.cross(collArm2, normal);
545 | impAug2 = impAug2 * c2.inv_inertia * impAug2;
546 |
547 | let relVel = closVel1.subtr(closVel2);
548 | let sepVel = Vector.dot(relVel, normal);
549 | let new_sepVel = -sepVel * Math.min(c1.elasticity, c2.elasticity);
550 | let vsep_diff = new_sepVel - sepVel;
551 |
552 | let impulse = vsep_diff / (c1.inv_m + c2.inv_m + impAug1 + impAug2);
553 | let impulseVec = normal.mult(impulse);
554 |
555 | //3. Changing the velocities
556 | c1.vel = c1.vel.add(impulseVec.mult(c1.inv_m));
557 | c2.vel = c2.vel.add(impulseVec.mult(-c2.inv_m));
558 |
559 | c1.angVel += c1.inv_inertia * Vector.cross(collArm1, impulseVec);
560 | c2.angVel -= c2.inv_inertia * Vector.cross(collArm2, impulseVec);
561 | }
562 |
563 | //applying the separating axis theorem on two objects
564 | function sat(o1, o2){
565 | let minOverlap = null;
566 | let smallestAxis;
567 | let vertexObj;
568 |
569 | let axes = findAxes(o1, o2);
570 | let proj1, proj2 = 0;
571 | let firstShapeAxes = getFirstShapeAxes(o1);
572 |
573 | for(let i=0; i proj2.max && proj1.min < proj2.min) ||
582 | (proj1.max < proj2.max && proj1.min > proj2.min)){
583 | let mins = Math.abs(proj1.min - proj2.min);
584 | let maxs = Math.abs(proj1.max - proj2.max);
585 | if (mins < maxs){
586 | overlap += mins;
587 | } else {
588 | overlap += maxs;
589 | axes[i] = axes[i].mult(-1);
590 | }
591 | }
592 |
593 | if (overlap < minOverlap || minOverlap === null){
594 | minOverlap = overlap;
595 | smallestAxis = axes[i];
596 | if (i proj2.max){
599 | smallestAxis = axes[i].mult(-1);
600 | }
601 | } else {
602 | vertexObj = o1;
603 | if(proj1.max < proj2.max){
604 | smallestAxis = axes[i].mult(-1);
605 | }
606 | }
607 | }
608 | };
609 |
610 | let contactVertex = projShapeOntoAxis(smallestAxis, vertexObj).collVertex;
611 | smallestAxis.drawVec(contactVertex.x, contactVertex.y, minOverlap, "blue");
612 |
613 | return true;
614 | }
615 |
616 | //returns the min and max projection values of a shape onto an axis
617 | function projShapeOntoAxis(axis, obj){
618 | setBallVerticesAlongAxis(obj, axis);
619 | let min = Vector.dot(axis, obj.vertex[0]);
620 | let max = min;
621 | let collVertex = obj.vertex[0];
622 | for(let i=0; imax){
629 | max = p;
630 | }
631 | }
632 | return {
633 | min: min,
634 | max: max,
635 | collVertex: collVertex
636 | }
637 | }
638 |
639 | //finds the projection axes for the two objects
640 | function findAxes(o1, o2){
641 | let axes = [];
642 | if(o1 instanceof Ball && o2 instanceof Ball){
643 | axes.push(o2.pos.subtr(o1.pos).unit());
644 | return axes;
645 | }
646 | if(o1 instanceof Ball){
647 | axes.push(closestVertexToPoint(o2, o1.pos).subtr(o1.pos).unit());
648 | axes.push(o2.dir.normal());
649 | if (o2 instanceof Box){
650 | axes.push(o2.dir);
651 | }
652 | return axes;
653 | }
654 | if(o2 instanceof Ball){
655 | axes.push(closestVertexToPoint(o1, o2.pos).subtr(o2.pos).unit());
656 | axes.push(o1.dir.normal());
657 | if (o1 instanceof Box){
658 | axes.push(o1.dir);
659 | }
660 | return axes;
661 | }
662 | axes.push(o1.dir.normal());
663 | axes.push(o1.dir);
664 | axes.push(o2.dir.normal());
665 | axes.push(o2.dir);
666 | return axes;
667 | }
668 |
669 | //iterates through an objects vertices and returns the one that is the closest to the given point
670 | function closestVertexToPoint(obj, p){
671 | let closestVertex;
672 | let minDist = null;
673 | for(let i=0; i {
708 | // b.drawBall();
709 | // if (b.player){
710 | // b.keyControl();
711 | // }
712 | // WALLZ.forEach((w) => {
713 | // if(coll_det_bw(BALLZ[index], w)){
714 | // pen_res_bw(BALLZ[index], w);
715 | // coll_res_bw(BALLZ[index], w);
716 | // }
717 | // })
718 | // for(let i = index+1; i {
729 | // w.drawWall();
730 | // w.keyControl();
731 | // w.reposition();
732 | // })
733 |
734 | // CAPS.forEach((c, index) => {
735 | // c.draw();
736 | // if(c.player === true){
737 | // c.keyControl();
738 | // }
739 | // for(let i = index+1; i
2 |
3 |
4 |
5 |
6 |
14 | Bodies and Shapes
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/17 - Bodies and Shapes/script.js:
--------------------------------------------------------------------------------
1 | const canvas = document.getElementById('canvas');
2 | const ctx = canvas.getContext('2d');
3 |
4 | const BALLZ = [];
5 | const WALLZ = [];
6 | const CAPS = [];
7 |
8 | let LEFT, UP, RIGHT, DOWN;
9 | let friction = 0.05;
10 |
11 | class Vector{
12 | constructor(x, y){
13 | this.x = x;
14 | this.y = y;
15 | }
16 |
17 | add(v){
18 | return new Vector(this.x+v.x, this.y+v.y);
19 | }
20 |
21 | subtr(v){
22 | return new Vector(this.x-v.x, this.y-v.y);
23 | }
24 |
25 | mag(){
26 | return Math.sqrt(this.x**2 + this.y**2);
27 | }
28 |
29 | mult(n){
30 | return new Vector(this.x*n, this.y*n);
31 | }
32 |
33 | normal(){
34 | return new Vector(-this.y, this.x).unit();
35 | }
36 |
37 | unit(){
38 | if(this.mag() === 0){
39 | return new Vector(0,0);
40 | } else {
41 | return new Vector(this.x/this.mag(), this.y/this.mag());
42 | }
43 | }
44 |
45 | drawVec(start_x, start_y, n, color){
46 | ctx.beginPath();
47 | ctx.moveTo(start_x, start_y);
48 | ctx.lineTo(start_x + this.x * n, start_y + this.y * n);
49 | ctx.strokeStyle = color;
50 | ctx.stroke();
51 | ctx.closePath();
52 | }
53 |
54 | static dot(v1, v2){
55 | return v1.x*v2.x + v1.y*v2.y;
56 | }
57 |
58 | static cross(v1, v2){
59 | return v1.x*v2.y - v1.y*v2.x;
60 | }
61 | }
62 |
63 | class Matrix{
64 | constructor(rows, cols){
65 | this.rows = rows;
66 | this.cols = cols;
67 | this.data = [];
68 |
69 | for (let i = 0; i 0){
450 | return w1.start;
451 | }
452 |
453 | let wallEndToBall = p.subtr(w1.end);
454 | if(Vector.dot(w1.dir, wallEndToBall) > 0){
455 | return w1.end;
456 | }
457 |
458 | let closestDist = Vector.dot(w1.dir, ballToWallStart);
459 | let closestVect = w1.dir.mult(closestDist);
460 | return w1.start.subtr(closestVect);
461 | }
462 |
463 | function closestPointsBetweenLS(c1, c2){
464 | let shortestDist = closestPointOnLS(c1.start, c2).subtr(c1.start).mag();
465 | let closestPoints = [c1.start, closestPointOnLS(c1.start, c2)];
466 | if(closestPointOnLS(c1.end, c2).subtr(c1.end).mag() < shortestDist){
467 | shortestDist = closestPointOnLS(c1.end, c2).subtr(c1.end).mag();
468 | closestPoints = [c1.end, closestPointOnLS(c1.end, c2)];
469 | }
470 | if(closestPointOnLS(c2.start, c1).subtr(c2.start).mag() < shortestDist){
471 | shortestDist = closestPointOnLS(c2.start, c1).subtr(c2.start).mag();
472 | closestPoints = [closestPointOnLS(c2.start, c1), c2.start];
473 | }
474 | if(closestPointOnLS(c2.end, c1).subtr(c2.end).mag() < shortestDist){
475 | shortestDist = closestPointOnLS(c2.end, c1).subtr(c2.end).mag();
476 | closestPoints = [closestPointOnLS(c2.end, c1), c2.end];
477 | }
478 | ctx.strokeStyle = "red";
479 | ctx.beginPath();
480 | ctx.moveTo(closestPoints[0].x, closestPoints[0].y);
481 | ctx.lineTo(closestPoints[1].x, closestPoints[1].y);
482 | ctx.closePath();
483 | ctx.stroke();
484 | ctx.beginPath();
485 | ctx.arc(closestPoints[0].x, closestPoints[0].y, c1.r, 0, 2*Math.PI);
486 | ctx.closePath();
487 | ctx.stroke();
488 | ctx.beginPath();
489 | ctx.arc(closestPoints[1].x, closestPoints[1].y, c2.r, 0, 2*Math.PI);
490 | ctx.closePath();
491 | ctx.stroke();
492 | return closestPoints;
493 | }
494 |
495 | function coll_det_bb(b1, b2){
496 | if(b1.r + b2.r >= b2.pos.subtr(b1.pos).mag()){
497 | return true;
498 | } else {
499 | return false;
500 | }
501 | }
502 |
503 | function coll_det_bw(b1, w1){
504 | let ballToClosest = closestPointOnLS(b1.pos, w1).subtr(b1.pos);
505 | if (ballToClosest.mag() <= b1.r){
506 | return true;
507 | }
508 | }
509 |
510 | function coll_det_cc(c1, c2){
511 | if(c1.r + c2.r >= closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).mag()){
512 | return true;
513 | } else {
514 | return false;
515 | }
516 | }
517 |
518 | function pen_res_bb(b1, b2){
519 | let dist = b1.pos.subtr(b2.pos);
520 | let pen_depth = b1.r + b2.r - dist.mag();
521 | let pen_res = dist.unit().mult(pen_depth / (b1.inv_m + b2.inv_m));
522 | b1.pos = b1.pos.add(pen_res.mult(b1.inv_m));
523 | b2.pos = b2.pos.add(pen_res.mult(-b2.inv_m));
524 | }
525 |
526 | function pen_res_bw(b1, w1){
527 | let penVect = b1.pos.subtr(closestPointOnLS(b1.pos, w1));
528 | b1.pos = b1.pos.add(penVect.unit().mult(b1.r-penVect.mag()));
529 | }
530 |
531 | function pen_res_cc(c1, c2){
532 | let dist = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]);
533 | let pen_depth = c1.r + c2.r - dist.mag();
534 | let pen_res = dist.unit().mult(pen_depth / (c1.inv_m + c2.inv_m));
535 | c1.pos = c1.pos.add(pen_res.mult(c1.inv_m));
536 | c2.pos = c2.pos.add(pen_res.mult(-c2.inv_m));
537 | }
538 |
539 | function coll_res_bb(b1, b2){
540 | let normal = b1.pos.subtr(b2.pos).unit();
541 | let relVel = b1.vel.subtr(b2.vel);
542 | let sepVel = Vector.dot(relVel, normal);
543 | let new_sepVel = -sepVel * Math.min(b1.elasticity, b2.elasticity);
544 |
545 | let vsep_diff = new_sepVel - sepVel;
546 | let impulse = vsep_diff / (b1.inv_m + b2.inv_m);
547 | let impulseVec = normal.mult(impulse);
548 |
549 | b1.vel = b1.vel.add(impulseVec.mult(b1.inv_m));
550 | b2.vel = b2.vel.add(impulseVec.mult(-b2.inv_m));
551 | }
552 |
553 | function coll_res_bw(b1, w1){
554 | let normal = b1.pos.subtr(closestPointOnLS(b1.pos, w1)).unit();
555 | let sepVel = Vector.dot(b1.vel, normal);
556 | let new_sepVel = -sepVel * b1.elasticity;
557 | let vsep_diff = sepVel - new_sepVel;
558 | b1.vel = b1.vel.add(normal.mult(-vsep_diff));
559 | }
560 |
561 | function coll_res_cc(c1, c2){
562 | let normal = closestPointsBetweenLS(c1, c2)[0].subtr(closestPointsBetweenLS(c1, c2)[1]).unit();
563 |
564 | //1. Closing velocity
565 | let collArm1 = closestPointsBetweenLS(c1, c2)[0].subtr(c1.pos).add(normal.mult(c1.r));
566 | let rotVel1 = new Vector(-c1.angVel * collArm1.y, c1.angVel * collArm1.x);
567 | let closVel1 = c1.vel.add(rotVel1);
568 | let collArm2 = closestPointsBetweenLS(c1, c2)[1].subtr(c2.pos).add(normal.mult(-c2.r));
569 | let rotVel2= new Vector(-c2.angVel * collArm2.y, c2.angVel * collArm2.x);
570 | let closVel2 = c2.vel.add(rotVel2);
571 |
572 | //2. Impulse augmentation
573 | let impAug1 = Vector.cross(collArm1, normal);
574 | impAug1 = impAug1 * c1.inv_inertia * impAug1;
575 | let impAug2 = Vector.cross(collArm2, normal);
576 | impAug2 = impAug2 * c2.inv_inertia * impAug2;
577 |
578 | let relVel = closVel1.subtr(closVel2);
579 | let sepVel = Vector.dot(relVel, normal);
580 | let new_sepVel = -sepVel * Math.min(c1.elasticity, c2.elasticity);
581 | let vsep_diff = new_sepVel - sepVel;
582 |
583 | let impulse = vsep_diff / (c1.inv_m + c2.inv_m + impAug1 + impAug2);
584 | let impulseVec = normal.mult(impulse);
585 |
586 | //3. Changing the velocities
587 | c1.vel = c1.vel.add(impulseVec.mult(c1.inv_m));
588 | c2.vel = c2.vel.add(impulseVec.mult(-c2.inv_m));
589 |
590 | c1.angVel += c1.inv_inertia * Vector.cross(collArm1, impulseVec);
591 | c2.angVel -= c2.inv_inertia * Vector.cross(collArm2, impulseVec);
592 | }
593 |
594 | //applying the separating axis theorem on two objects
595 | function sat(o1, o2){
596 | let minOverlap = null;
597 | let smallestAxis;
598 | let vertexObj;
599 |
600 | let axes = findAxes(o1, o2);
601 | let proj1, proj2 = 0;
602 | let firstShapeAxes = getShapeAxes(o1);
603 |
604 | for(let i=0; i proj2.max && proj1.min < proj2.min) ||
613 | (proj1.max < proj2.max && proj1.min > proj2.min)){
614 | let mins = Math.abs(proj1.min - proj2.min);
615 | let maxs = Math.abs(proj1.max - proj2.max);
616 | if (mins < maxs){
617 | overlap += mins;
618 | } else {
619 | overlap += maxs;
620 | axes[i] = axes[i].mult(-1);
621 | }
622 | }
623 |
624 | if (overlap < minOverlap || minOverlap === null){
625 | minOverlap = overlap;
626 | smallestAxis = axes[i];
627 | if (i proj2.max){
630 | smallestAxis = axes[i].mult(-1);
631 | }
632 | } else {
633 | vertexObj = o1;
634 | if(proj1.max < proj2.max){
635 | smallestAxis = axes[i].mult(-1);
636 | }
637 | }
638 | }
639 | };
640 |
641 | let contactVertex = projShapeOntoAxis(smallestAxis, vertexObj).collVertex;
642 | //smallestAxis.drawVec(contactVertex.x, contactVertex.y, minOverlap, "blue");
643 |
644 | return {
645 | pen: minOverlap,
646 | axis: smallestAxis,
647 | vertex: contactVertex
648 | }
649 | }
650 |
651 | //returns the min and max projection values of a shape onto an axis
652 | function projShapeOntoAxis(axis, obj){
653 | setBallVerticesAlongAxis(obj, axis);
654 | let min = Vector.dot(axis, obj.vertex[0]);
655 | let max = min;
656 | let collVertex = obj.vertex[0];
657 | for(let i=0; imax){
664 | max = p;
665 | }
666 | }
667 | return {
668 | min: min,
669 | max: max,
670 | collVertex: collVertex
671 | }
672 | }
673 |
674 | //finds the projection axes for the two objects
675 | function findAxes(o1, o2){
676 | let axes = [];
677 | if(o1 instanceof Circle && o2 instanceof Circle){
678 | axes.push(o2.pos.subtr(o1.pos).unit());
679 | return axes;
680 | }
681 | if(o1 instanceof Circle){
682 | axes.push(closestVertexToPoint(o2, o1.pos).subtr(o1.pos).unit());
683 | axes.push(o2.dir.normal());
684 | if (o2 instanceof Rectangle){
685 | axes.push(o2.dir);
686 | }
687 | return axes;
688 | }
689 | if(o2 instanceof Circle){
690 | axes.push(o1.dir.normal());
691 | if (o1 instanceof Rectangle){
692 | axes.push(o1.dir);
693 | }
694 | axes.push(closestVertexToPoint(o1, o2.pos).subtr(o2.pos).unit());
695 | return axes;
696 | }
697 | axes.push(o1.dir.normal());
698 | if (o1 instanceof Rectangle){
699 | axes.push(o1.dir);
700 | }
701 | axes.push(o2.dir.normal());
702 | if (o2 instanceof Rectangle){
703 | axes.push(o2.dir);
704 | }
705 | return axes;
706 | }
707 |
708 | //iterates through an objects vertices and returns the one that is the closest to the given point
709 | function closestVertexToPoint(obj, p){
710 | let closestVertex;
711 | let minDist = null;
712 | for(let i=0; i {
748 | // b.drawBall();
749 | // if (b.player){
750 | // b.keyControl();
751 | // }
752 | // WALLZ.forEach((w) => {
753 | // if(coll_det_bw(BALLZ[index], w)){
754 | // pen_res_bw(BALLZ[index], w);
755 | // coll_res_bw(BALLZ[index], w);
756 | // }
757 | // })
758 | // for(let i = index+1; i {
769 | // w.drawWall();
770 | // w.keyControl();
771 | // w.reposition();
772 | // })
773 |
774 | // CAPS.forEach((c, index) => {
775 | // c.draw();
776 | // if(c.player === true){
777 | // c.keyControl();
778 | // }
779 | // for(let i = index+1; i bestSat.pen){
804 | bestSat = sat(testCaps.comp[o1comp], Caps1.comp[o2comp]);
805 | ctx.fillText("COLLISION", 500, 400);
806 | }
807 | }
808 | }
809 |
810 | if(bestSat.pen !== null){
811 | bestSat.axis.drawVec(bestSat.vertex.x, bestSat.vertex.y, bestSat.pen, "blue");
812 | }
813 |
814 | requestAnimationFrame(mainLoop);
815 | }
816 |
817 | let testCaps = new Capsule(200, 100, 100, 200, 50, 3);
818 | //let testBall = new Ball(200, 200, 80, 6);
819 | //let Box2 = new Box(300, 180, 250, 50, 80, 5);
820 | //let Ball1 = new Ball(300, 100, 50, 5);
821 | //let Wall1 = new Wall(300, 100, 250, 400);
822 | let Caps1 = new Capsule(350, 100, 300, 400, 60, 3);
823 |
824 | //Caps1.player = true;
825 |
826 |
827 |
828 | requestAnimationFrame(mainLoop);
829 |
830 |
--------------------------------------------------------------------------------
/18 - Collision Manifold/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Collision Manifold
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/19 - A Star Is Born/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | A Star Is Born
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/20 - Ready for Games/game.js:
--------------------------------------------------------------------------------
1 | //STEP 1: setting up the environment
2 | const canvas = document.getElementById('canvas');
3 | const ctx = canvas.getContext('2d');
4 |
5 | putWallsAround(0, 0, canvas.clientWidth, canvas.clientHeight);
6 | let randomObjects = [];
7 |
8 | //Creating 10 body object with random arguments
9 | for(let addBody = 0; addBody < 10; addBody++){
10 | let x0 = randInt(100, canvas.clientWidth-100);
11 | let y0 = randInt(100, canvas.clientHeight-100);
12 | let x1 = x0 + randInt(-50, 50);
13 | let y1 = y0 + randInt(-50, 50);
14 | let r = randInt(10, 30);
15 | let m = randInt(0, 10);
16 | if(addBody%4 === 0){
17 | let ballObj = new Ball(x0, y0, r, m);
18 | ballObj.setPosition(100, 100);
19 | ballObj.color = "red";
20 | ballObj.layer = 1;
21 | randomObjects.push(ballObj);
22 | }
23 | if(addBody%4 === 1){
24 | let boxObj = new Box(x0, y0, x1, y1, r, m);
25 | boxObj.setPosition(200, 200);
26 | boxObj.color = "blue";
27 | boxObj.layer = 2;
28 | randomObjects.push(boxObj);
29 | }
30 | if(addBody%4 === 2){
31 | let capsObj = new Capsule(x0, y0, x1, y1, r, m);
32 | capsObj.setPosition(300, 300);
33 | capsObj.color = "lightgreen";
34 | capsObj.layer = 3;
35 | randomObjects.push(capsObj);
36 | }
37 | if(addBody%4 === 3){
38 | let starObj = new Star(x0, y0, r+20, m);
39 | starObj.setPosition(400, 400);
40 | starObj.color = "yellow";
41 | starObj.layer = 4;
42 | randomObjects.push(starObj);
43 | }
44 | };
45 |
46 | //setting the initial velocities
47 | for (let i in randomObjects){
48 | if(randomObjects[i].m !== 0){
49 | randomObjects[i].vel.set(Math.random()*4-2, Math.random()*4-2);
50 | }
51 | }
52 |
53 | //creating the player
54 | let playerBall = new Ball(320, 240, 10, 5);
55 | playerBall.player = true;
56 | playerBall.maxSpeed = 3;
57 | playerBall.color = "#5B2C6F";
58 |
59 |
60 | //STEP 2: defining the game logic
61 | function gameLogic(){
62 | for (let i in randomObjects){
63 | if(collide(randomObjects[i], playerBall)){
64 | //playerBall.remove();
65 | randomObjects[i].remove();
66 | randomObjects.splice(i, 1);
67 | }
68 | }
69 | }
70 |
71 | //STEP 3: handling the user input and the game loop
72 | userInput(playerBall);
73 | requestAnimationFrame(mainLoop);
--------------------------------------------------------------------------------
/20 - Ready for Games/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Ready for Games
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/20 - Ready for Games/userInput.js:
--------------------------------------------------------------------------------
1 | //Event listeners for the arrow keys
2 | function userInput(obj){
3 | canvas.addEventListener('keydown', function(e){
4 | if(e.keyCode === 37){
5 | obj.left = true;
6 | }
7 | if(e.keyCode === 38){
8 | obj.up = true;
9 | }
10 | if(e.keyCode === 39){
11 | obj.right = true;
12 | }
13 | if(e.keyCode === 40){
14 | obj.down = true;
15 | }
16 | if(e.keyCode === 32){
17 | obj.action = true;
18 | }
19 | });
20 |
21 | canvas.addEventListener('keyup', function(e){
22 | if(e.keyCode === 37){
23 | obj.left = false;
24 | }
25 | if(e.keyCode === 38){
26 | obj.up = false;
27 | }
28 | if(e.keyCode === 39){
29 | obj.right = false;
30 | }
31 | if(e.keyCode === 40){
32 | obj.down = false;
33 | }
34 | if(e.keyCode === 32){
35 | obj.action = false;
36 | }
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/Game Template/game_template.js:
--------------------------------------------------------------------------------
1 | // STEP 1: setting up the environment
2 | // creating the starting objects and variables before starting the main loop
3 | // for example:
4 | const canvas = document.getElementById('canvas');
5 | const ctx = canvas.getContext('2d');
6 | canvas.focus();
7 |
8 | putWallsAround(0, 0, canvas.clientWidth, canvas.clientHeight);
9 | let player = new Ball(100, 100, 30, 5);
10 | player.setColor("red");
11 | player.maxSpeed = 5;
12 |
13 | // STEP 2: defining the game logic
14 | function gameLogic(){
15 | // this gets called periodically as part of the main loop
16 | // define the rules here
17 | }
18 |
19 | // handling the user input and the game loop
20 | userInput(player);
21 | requestAnimationFrame(mainLoop);
--------------------------------------------------------------------------------
/Game Template/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 | Game Template
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Game Template/userInput.js:
--------------------------------------------------------------------------------
1 | //Event listeners for the arrow keys
2 | function userInput(obj){
3 | canvas.addEventListener('keydown', function(e){
4 | if(e.code === "ArrowLeft"){
5 | obj.left = true;
6 | }
7 | if(e.code === "ArrowUp"){
8 | obj.up = true;
9 | }
10 | if(e.code === "ArrowRight"){
11 | obj.right = true;
12 | }
13 | if(e.code === "ArrowDown"){
14 | obj.down = true;
15 | }
16 | if(e.code === "Space"){
17 | obj.action = true;
18 | }
19 | });
20 |
21 | canvas.addEventListener('keyup', function(e){
22 | if(e.code === "ArrowLeft"){
23 | obj.left = false;
24 | }
25 | if(e.code === "ArrowUp"){
26 | obj.up = false;
27 | }
28 | if(e.code === "ArrowRight"){
29 | obj.right = false;
30 | }
31 | if(e.code === "ArrowDown"){
32 | obj.down = false;
33 | }
34 | if(e.code === "Space"){
35 | obj.action = false;
36 | }
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 danielszabo88
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mocorgo
2 | Building a physics engine\
3 | Youtube - https://www.youtube.com/channel/UCORSpAcEfLWPRnbD2AAizSg
4 |
5 | Newest version is always in the Game Template folder
6 |
--------------------------------------------------------------------------------