-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcanvas.js
423 lines (344 loc) · 14.3 KB
/
canvas.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
const cleanBtn = document.getElementById('clean-btn');
const removeCharcoalBtn = document.getElementById('remove-charcoal-btn');
const purityMarker = document.getElementById('purity-concentration-marker');
const infoBtn = document.getElementById('info-btn');
const info = document.querySelector('.info');
const charcoalMarker = document.getElementById('charcoal-concentration-marker');
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
// check for devices smaller than 700px
if(innerWidth < 700){
canvas.width = innerWidth-20;
canvas.height = innerHeight*0.75;
}else{
canvas.width = innerWidth-20;
canvas.height = innerHeight*0.9;
}
//Gradient generator for canvas background
function gradientGenerator(){
let my_gradient = c.createLinearGradient(0, 0, 0, 1500);
my_gradient.addColorStop(0, "#0082c8");
my_gradient.addColorStop(1, "white");
c.fillStyle = my_gradient;
c.fillRect(0, 0, canvas.width, canvas.height);
}
let colors = [
'#fda403',
'#e8751a',
'#c51350',
'#8a1253'
]
//initialize mouse object with undefined coordinates
let mouse = {
x: undefined,
y: undefined
}
//add event to start cleaning process
cleanBtn.addEventListener('click',() =>{
startCleaningProcess();
})
//remove show-info class when click upon canvas
canvas.addEventListener('click',() =>{
if(info.classList.contains('show-info')){
info.classList.remove('show-info');
}
})
//show info-box on click event
infoBtn.addEventListener('click',() =>{
info.classList.toggle('show-info');
})
// keep track of screen size changes and fire up init() function each time so as molecules could be regenerated
addEventListener('resize',() =>{
if(innerWidth < 600){
canvas.width = innerWidth-20;
canvas.height = innerHeight*0.75;
}else{
canvas.width = innerWidth-20;
canvas.height = innerHeight*0.9;
}
init();
})
// mousedown event so as mouse object is updated with current mouse position and then becomes undefined on mouseup position
canvas.addEventListener('mousedown',(e) =>{
mouse.x = e.x;
mouse.y = e.y;
//generate new impurity molecules on mouse down event anywhere on the canvas
generateMolecules();
})
// mouseup event in affect
canvas.addEventListener('mouseup',(e) =>{
mouse.x = undefined;
mouse.y = undefined;
})
//utility function to generate a random integer between any 2 provided integers.
function randomIntFromRange(min,max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
//utility function to generate a random color from the colors array
function getRandomColors(){
let k = Math.floor(Math.random()*colors.length);
return colors[k];
}
// find the distance between 2 molecules or a molecule and the canvas walls
function collisionDist(x1,y1,x2,y2){
let xDist = x2-x1;
let yDist = y2-y1;
return Math.sqrt(Math.pow(xDist,2)+Math.pow(yDist,2));
}
// utility function helpfull in rotating the x-y plane once newtonion theorem is applied in 1-D
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) - velocity.y * Math.sin(angle),
y: velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveInelasticCollision(particle, otherParticle){
const xVelocityDiff = particle.velocity.x - otherParticle.velocity.x;
const yVelocityDiff = particle.velocity.y - otherParticle.velocity.y;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
// Prevent accidental overlap of particles
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
// Grab angle between the two colliding particles
const angle = -Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
// Store mass in var for better readability in collision equation
const m1 = particle.mass;
const m2 = otherParticle.mass;
// Velocity before equation
const u1 = rotate(particle.velocity, angle);
const u2 = rotate(otherParticle.velocity, angle);
// Velocity after 1d inelastic collision equation
const v1 = { x: (m1*u1.x + m2*u2.x )/ (m1+m2), y: u1.y };
const v2 = { x: (m1*u1.x + m2*u2.x )/ (m1+m2), y: u2.y };
// Final velocity after rotating axis back to original location
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
// Swap particle velocities for realistic bounce effect
particle.velocity.x = vFinal2.x;
particle.velocity.y = vFinal2.y;
otherParticle.velocity.x = vFinal1.x;
otherParticle.velocity.y = vFinal1.y;
}
}
// Apply newtonian theorem in 1-D so as elastic collision could be acheived. Energy throughout the system remains constant.
// Energy of initial particles(pure water molecules) changes when impurity/charcoal is induced.
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.velocity.x - otherParticle.velocity.x;
const yVelocityDiff = particle.velocity.y - otherParticle.velocity.y;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
// Prevent accidental overlap of particles
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
// Grab angle between the two colliding particles
const angle = -Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
// Store mass in var for better readability in collision equation
const m1 = particle.mass;
const m2 = otherParticle.mass;
// Velocity before equation
const u1 = rotate(particle.velocity, angle);
const u2 = rotate(otherParticle.velocity, angle);
// Velocity after 1d collision equation
const v1 = { x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2), y: u1.y };
const v2 = { x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2), y: u2.y };
// Final velocity after rotating axis back to original location
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
// Swap particle velocities for realistic bounce effect
particle.velocity.x = vFinal1.x;
particle.velocity.y = vFinal1.y;
otherParticle.velocity.x = vFinal2.x;
otherParticle.velocity.y = vFinal2.y;
}
}
// class responsible for drawing an updating water,impurity and charcoal molecules
class Molecule{
constructor(x,y,velocity,radius,color){
this.x = x;
this.y = y;
this.velocity = velocity;
this.radius = radius;
this.color = color;
this.mass = 2;
}
//The following function is responsible for drawing static water and impurity molecules
draw(){
c.beginPath()
c.arc(this.x,this.y,this.radius,0,Math.PI * 2,false);
let my_gradient = c.createLinearGradient(0, 0, 0, 1500);
my_gradient.addColorStop(0, this.color);
my_gradient.addColorStop(1, "white");
c.fillStyle = my_gradient;
c.fill();
c.closePath();
}
//The following function is responsible for drawing static charcoal balls only
drawCharcoalBall(){
c.beginPath()
c.arc(this.x,this.y,this.radius,0,Math.PI * 2,false);
let my_gradient = c.createLinearGradient(0, 0, 0, 1500);
my_gradient.addColorStop(0, this.color);
my_gradient.addColorStop(1, "white");
c.fillStyle = my_gradient;
c.fill();
c.strokeStyle = 'red';
c.stroke();
c.closePath();
}
//The following function updates water molecules/impurity molecules and hence provides movement to them in random directions
update(){
//check for collision with left-right canvas walls
if(this.x+this.radius >= canvas.width || this.x-this.radius <= 0){
this.velocity.x = -this.velocity.x;
}
//check for collision with top-bottom canvas walls
if(this.y+this.radius >= canvas.height || this.y-this.radius <= 0){
this.velocity.y = -this.velocity.y;
}
// check for collision of water/impurity molecules with one another except themselves
moleculeArray.forEach(molecule =>{
if(this !== molecule){
let collisionDistVal = collisionDist(this.x,this.y,molecule.x,molecule.y);
if(collisionDistVal - (this.radius+molecule.radius) < 0){
resolveCollision(this,molecule);
}
}
})
// increment velocity in x and y directions
this.x += this.velocity.x;
this.y += this.velocity.y;
// draw the molecules at newly generated coordinates
this.draw();
}
//The following function updates charcoal molecules and hence provides movement to them in random directions
updateCharcoalBall(){
//check for collision with left-right canvas walls
if(this.x+this.radius >= canvas.width || this.x-this.radius <= 0){
this.velocity.x = -this.velocity.x;
}
//check for collision with top-bottom canvas walls
if(this.y+this.radius >= canvas.height || this.y-this.radius <= 0){
this.velocity.y = -this.velocity.y;
}
// check for collision of charcoal molecules with one another except themselves
charcoalBallsArray.forEach((ball,index) =>{
if(this !== ball){
let collisionDistVal = collisionDist(this.x,this.y,ball.x,ball.y);
if(collisionDistVal - (this.radius+ball.radius) < 0){
resolveCollision(this,ball);
}
}
// check if index of molecule array is less than starting molecules than resolve the collision otherwise at all other indexes
// impurity particles are present and thus when charcoal comes in contact with them, remove them from array
// Charcoal molecules size increases on coming in contact with impurity molecules and after their radius becomes more than 20px
// remove them from the charcoal-array
moleculeArray.forEach((molecule,i) =>{
let collisionDistVal = collisionDist(this.x,this.y,molecule.x,molecule.y);
if(collisionDistVal - (this.radius+molecule.radius) < 0){
if(i<startingMolecules){
resolveCollision(this,molecule)
}
else{
resolveInelasticCollision(this,molecule)
if(molecule.radius > 0 ){
molecule.radius -= 1;
}
else{
moleculeArray.splice(i,1);
}
}
}
})
})
// remove charcoalballs slowly after all impurity molecules are removed
if(moleculeArray.length == startingMolecules){
if(this.radius > 0.05){
this.radius -= 0.05;
}
else{
charcoalBallsArray = [];
}
}
// increment velocity of charcoal molecules in x and y directions
this.x += this.velocity.x;
this.y += this.velocity.y;
// draw the charcoal molecules at newly generated coordinates
this.drawCharcoalBall();
}
}
let molecule;
let moleculeArray = [];
let charcoalBallsArray = [];
let startingMolecules;
let purityConcentrationValue = 0;
let charcoalConcentrationValue = 0;
// The following function is responsible for creating multiple new objects for Molecule class so as only water-molecules could be generated
// on the start
function init(){
//re-initialize the molecule array to an empty array so that water molecules do not increase so much when window is resized
moleculeArray = [];
startingMolecules = 30;
for(i = 0;i < startingMolecules; i++){
let radius = randomIntFromRange(5,20);
let x = randomIntFromRange(radius , canvas.width - radius);
let y = randomIntFromRange(radius , canvas.height - radius);
let velocity = {
x: Math.random(),
y: Math.random()
}
// check if water molecules are not generated together and hence prevent 2 molecules getting joined up
if( i !== 0){
for(let j=0; j < moleculeArray.length; j++){
if(collisionDist(x,y,moleculeArray[j].x,moleculeArray[j].y) - radius - moleculeArray[j].radius < 0){
x = Math.random() * canvas.width;
y = Math.random() * canvas.height;
j = -1;
}
}
}
moleculeArray.push(new Molecule(x,y,velocity,radius,'blue'));
}
}
// The generateMolecules() function generates objects for new impurity molecules when clicked on canvas anywhere
function generateMolecules(){
for(i = 0;i < randomIntFromRange(1,5); i++){
let radius = randomIntFromRange(5,20);
let x = mouse.x;
let y = mouse.y
let velocity = {
x: randomIntFromRange(-8,8),
y: randomIntFromRange(-8,8)
}
moleculeArray.push(new Molecule(x,y,velocity,radius,getRandomColors()));
}
}
// Object for charcoal molecules are created when cleanBtn is clicked
function startCleaningProcess(){
for(i = 0;i < 5; i++){
let radius = 10;
let genX = 50;
let genY = 50;
let velocity = {
x: Math.random() * randomIntFromRange(3,8) ,
y: Math.random() * randomIntFromRange(3,8)
}
charcoalBallsArray.push(new Molecule(genX,genY,velocity,radius,'#484848'));
}
}
// Animation loop keeps the functions running at all time
function animate(){
requestAnimationFrame(animate);
gradientGenerator();
moleculeArray.forEach(molecule =>{
molecule.update();
})
charcoalBallsArray.forEach(ball =>{
ball.updateCharcoalBall();
})
//insert concentration values in DOM. The more the value, higher is the concentration
purityMarker.innerHTML = 'Impurity Concentration: ' + (moleculeArray.length - startingMolecules);
charcoalMarker.innerHTML = 'Charcoal Concentration: ' + charcoalBallsArray.length;
}
gradientGenerator();
init();
animate();