diff --git a/index.html b/index.html new file mode 100644 index 0000000..2715fa5 --- /dev/null +++ b/index.html @@ -0,0 +1,169 @@ + + + + + + + + Collision Simulation + + + + +
+
+

Welcome to the collision simulator!

+

A Project by: Johnathon Slightham

+

Visit the wiki on github for more information on how this works!

+

To use the simulator:

+ + +
+
+

Collision Simulator

+

Project by Johnathon Slightham

+
+ +
+
+ Your browser does not support HTML 5
+
+
+
+

Modifiers

+ Coefficient of Kinetic Friction:
+ + Collision Effectiveness (100% - No energy lost): %
+ Draw Velocity Vector: +

Placed Particles

+ + + + + + + + + +
ID #MassX VelocityY VelocityX AccelerationY Acceleration
+ Total Kinetic Energy: J +

Add Particles

+
+ Mass: X-Velocity: Y-Velocity: +
+

Modify Graph

+ Graph X Axis: + + + Graph Y Axis: + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..0115d93 --- /dev/null +++ b/main.js @@ -0,0 +1,625 @@ +class Particle{ + constructor(id, x, y, vx, vy, m){ + this.id = id; + this.x = x; + this.y = y; + this.vx = vx; + this.vy = vy; + this.m = m; + this.r = getRadius(m); + this.v = Math.sqrt(Math.pow(this.vx, 2) + Math.pow(this.vy, 2)); + this.ek = []; + this.ek.push([0, 0]); + this.ax = 0 + this.ay = 0; + } +} + +// Functions to display or hide the welcome screen +function on() { + document.getElementById("playButton").style.display = "block"; +} + +function off() { + document.getElementById("playButton").style.display = "none"; +} + +var particles = []; +var ctx; +var canvas; +var table; +var a = 0; +var sa; +var ek = []; +var eff = 0.85; +var shot = new Audio("shot.mp3"); +var wallhit = new Audio("wallhit.mp3"); +var selector = document.getElementById('graph_type'); +var sfr = 0; +var fr = 0; +var drawVelocityVector = false; +var measurementData = []; +var chartX = "measurement"; +var chartY = "ekt" +measurementData.push('Measurement Number'); +measurementData.push(0); + +ek.push('Kientic Energy (J)'); +ek.push(0); + +var measurement = 0; + +// Load the google charts API +google.charts.load('current', {'packages':['corechart']}); + +function main(elem){ + off(); + canvas = document.getElementById(elem); + ctx = canvas.getContext("2d"); + table = document.getElementById("particles"); + + fr = document.getElementById("fr").value; + //sfr = document.getElementById("sfr").value; + + drawVelocityVector = document.getElementById("drawvelocity").checked; + + reloadDropdowns(); + + // Set a callback to run when the charts API is loaded + google.charts.setOnLoadCallback(drawKEChart); + + //canvas.addEventListener("mousedown", doMouseDown, false); + //canvas.addEventListener("onmouseup", doMouseUp, false); + + // Loop through each particle every 10 milliseconds, create measurements every 1 second + setInterval(loop, 10); + setInterval(measureData, 1000); +} + +// The main loop function that handles each particle +function loop(){ + + // Set volumes of sounds according to % energy transferred + eff = document.getElementById("effe").value/100; + shot.volume = 1 - eff; + wallhit.volume = 1 - eff; + + // Acceleration = 9.8*uk (derived from F=ma, -uk*g*m=m*a) + a = -(9.8)*fr; + sa = -(9.8)*sfr; + + chartX = document.getElementById("graphx").value; + chartY = document.getElementById("graphy").value; + drawVelocityVector = document.getElementById("drawvelocity").checked; + + // Write the current kinetic energy, then reset to 0 for recalculation + document.getElementById("ek").innerHTML = ek[ek.length-1]; + + // Draw board, then process collisions. + draw(); + separate(); + detectCollisions(); + + // Iterate through each particle, to change it's velocity from acceleration, and position from velocity + particles.forEach((elem, i) => { + + // Determine velocity vector and store for later use + elem.v = Math.sqrt(Math.pow(elem.vx, 2) + Math.pow(elem.vy, 2)); + + // Check for collisions with wall, and on collision set to - velocity + if(elem.x + elem.vx > canvas.width - elem.r || elem.x + elem.vx < elem.r){ + elem.vx = -eff*elem.vx; + let wallHitAudio = wallhit.cloneNode(); + wallHitAudio.volume = 1 - eff; + wallHitAudio.play() + + // Fix bug of balls getting stuck in walls, if a ball is in a wall move it out + if(elem.x + elem.vx > canvas.width - elem.r){ + elem.x = canvas.width - elem.r; + } + + if(elem.x + elem.vx < elem.r){ + elem.x = elem.r; + } + + } + if(elem.y + elem.vy > canvas.height - elem.r || elem.y + elem.vy < elem.r){ + elem.vy = -eff*elem.vy; + let wallHitAudio = wallhit.cloneNode(); + wallHitAudio.volume = 1 - eff; + wallHitAudio.play() + + // Fix bug of balls getting stuck in walls, if a ball is in a wall move it out + if(elem.y + elem.vy < elem.r){ + elem.y = elem.r; + } + if(elem.y + elem.vy > canvas.width - elem.r){ + elem.y = canvas.height - elem.r; + } + } + + // Calculate Theta, and acceleration values based on the direction of the velocity vector + elem.theta = Math.atan2(elem.vx,elem.vy); + elem.sax = sa*Math.abs(Math.sin(elem.theta)); + elem.say = sa*Math.abs(Math.cos(elem.theta)); + + elem.ax = a*Math.abs(Math.sin(elem.theta)); + elem.ay = a*Math.abs(Math.cos(elem.theta)); + + // If the particle has enough velocity to be decelerated, decelerate it + if(!Math.abs(elem.vx) < Math.abs(elem.ax) || !Math.abs(elem.vy) < Math.abs(elem.ay)){ + // Add acceleration to velocity each iteration + if(elem.vx > 0){ + elem.vx += elem.ax; + } + else{ + elem.vx -= elem.ax; + } + + // Add acceleration to velocity each iteration + if(elem.vy > 0){ + elem.vy += elem.ay; + } + else{ + elem.vy -= elem.ay; + } + + }else{ + elem.ax = 0; + elem.ay = 0; + + } + // Set velocity to zero if the acceleration is larger than it + if(Math.abs(elem.ay) > Math.abs(elem.vy) || Math.abs(elem.ax) > Math.abs(elem.vx)){ + elem.ax = 0; + elem.ay = 0; + elem.vx = 0; + elem.vy = 0; + }else{ + if((elem.vx == 0 || elem.vy == 0) && (Math.abs(elem.say) > Math.abs(elem.vy) || Math.abs(elem.sax) > Math.abs(elem.vx))){ + elem.ax = 0; + elem.ay = 0; + elem.vx = 0; + elem.vy = 0; + }else{ + // Each iteration add the velocity to the position + elem.x += elem.vx; + elem.y += elem.vy; + } + + } + + // Display data in the table + table.rows[i+1].cells[0].innerHTML = elem.id; + table.rows[i+1].cells[1].innerHTML = elem.m + " kg"; + table.rows[i+1].cells[2].innerHTML = Math.abs(elem.vx.toPrecision(2)); + table.rows[i+1].cells[3].innerHTML = Math.abs(elem.vy.toPrecision(2)); + table.rows[i+1].cells[4].innerHTML = elem.ax.toPrecision(2); + table.rows[i+1].cells[5].innerHTML = elem.ay.toPrecision(2); + }) +} + +// Function to draw all particles on the canvas, and draw velocity vectors +function draw(){ + ctx.clearRect(0, 0, canvas.width, canvas.height); + particles.forEach((elem, i) => + { + // Draw actual particle + ctx.beginPath(); + ctx.fillStyle = "black"; + ctx.strokeStyle = "black"; + ctx.arc(elem.x, elem.y, elem.r, 0, 2*Math.PI); + ctx.fill(); + ctx.stroke(); + + // Center mass in particle + ctx.font = elem.m*0.55 + 'px serif'; + ctx.fillStyle = "white"; + ctx.textAlign = "center"; + ctx.fillText(elem.m+"kg", elem.x, elem.y + elem.r/4); + + // Draw Velocity Vector, if this option is selected + if(drawVelocityVector){ + ctx.beginPath(); + ctx.strokeStyle = "red"; + ctx.moveTo(elem.x, elem.y); + ctx.lineTo(elem.x + elem.v*Math.sin(elem.theta)* 10, elem.y + elem.v*Math.cos(elem.theta) * 10); + ctx.stroke(); + } + + }); + +} + +// Function that returns the distance between the two particles given +function getDistance(p1, p2){ + let xDist = Math.abs(p1.x - p2.x); + let yDist = Math.abs(p1.y - p2.y); + + return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2)); +} + +var collisions = []; + +// Function that will separate all particles on the canvas +function separate(){ + collisions = []; + let noOverlap = false; + while(!noOverlap){ + noOverlap = true; + particles.forEach((elem1, i) => { + for(var j = i+1; j < particles.length; j++){ + elem2 = particles[j]; + // Make sure not comparing the same element + if(elem1 != elem2){ + // If the particles are overlapping + if(getDistance(elem1, elem2) < elem1.r + elem2.r){ + collisions.push({element1: elem1, element2: elem2}); + console.log("overlap"); + + // Move the particle back to its previous position, only if doing so will not put it inside of a wall + if(!elem1.x - elem1.vx > canvas.width - elem1.r || !elem1.x - elem1.vx < elem1.r) + elem1.x -= elem1.vx; + + if(!elem2.x - elem2.vx > canvas.width - elem2.r || !elem2.x - elem2.vx < elem2.r) + elem2.x -= elem2.vx; + + if(!elem1.y - elem1.vy > canvas.height - elem1.r || !elem1.y - elem1.vy < elem1.r) + elem1.y -= elem1.vy; + + if(!elem2.y - elem2.vy > canvas.height - elem2.r || !elem2.y - elem2.vy < elem2.r) + elem2.y -= elem2.vy; + + noOverlap = false; + + draw(); + } + } + } + }) + } +} + +// Function that detects, and handles all collisions. +function detectCollisions(){ + + collisions.forEach((collision) => { + let shotAudio = shot.cloneNode() + shotAudio.volume = 1 - eff; + shotAudio.play() + elem1 = collision.element1; + elem2 = collision.element2; + + // Check if the particles being compared are the same, if they are don't run collision detection or particles will collide with themselves. + if(elem1 != elem2){ + elem1.px = elem1.x; + elem1.py = elem1.y; + elem2.px = elem2.x; + elem2.py = elem2.y; + + // Store previous velocities, this will be used later when detecting static friction + elem1.pvx = elem1.vx; + elem1.pvy = elem1.vy; + elem2.pvx = elem2.vx; + elem2.pvy = elem2.vy; + + // If the particles are still overlapping, separate them again + if(getDistance(elem1, elem2) < elem1.r + elem2.r){ + console.log("overlap"); + elem1.x -= elem1.vx; + elem1.y -= elem1.vy; + elem2.x -= elem2.vx; + elem2.y -= elem2.vy; + } + + // Theta = angle of velocity + let theta1 = Math.atan2(elem1.vx,elem1.vy); + let theta2 = Math.atan2(elem2.vx,elem2.vy); + + // Phi = angle of collision + let phi = Math.atan2((elem2.px - elem1.px),(elem2.py - elem1.py)); + + // Find rotated x & y components, with x axis parallel to the contact normal vector + let v1xr = elem1.v*Math.cos(theta1 - phi); + let v1yr = elem1.v*Math.sin(theta1 - phi); + let v2xr = elem2.v*Math.cos(theta2 - phi); + let v2yr = elem2.v*Math.sin(theta2 - phi); + + // Use the rotated components to solve a 1 dimensional collision. + let v1fxr = eff*(v1xr*(elem1.m - elem2.m) + 2*elem2.m*v2xr)/(elem1.m + elem2.m); + let v2fxr = eff*(v2xr*(elem2.m - elem1.m) + 2*elem1.m*v1xr)/(elem2.m + elem1.m); + + elem1.vy = v1fxr*Math.cos(phi) + v1yr*Math.cos(phi + Math.PI/2); + elem1.vx = v1fxr*Math.sin(phi) + v1yr*Math.sin(phi + Math.PI/2); + elem2.vy = v2fxr*Math.cos(phi) + v2yr*Math.cos(phi + Math.PI/2); + elem2.vx = v2fxr*Math.sin(phi) + v2yr*Math.sin(phi + Math.PI/2); + + // Calculate new theta, and static acceleration for static friction checking + elem1.theta = Math.atan2(elem1.vx,elem1.vy); + elem1.sax = sa*Math.abs(Math.sin(elem1.theta)); + elem1.say = sa*Math.abs(Math.cos(elem1.theta)); + + elem2.theta = Math.atan2(elem2.vx,elem2.vy); + elem2.sax = sa*Math.abs(Math.sin(elem2.theta)); + elem2.say = sa*Math.abs(Math.cos(elem2.theta)); + + if((elem2.sax > elem2.vx || elem2.say > elem2.vy) && (elem2.pvx == 0 || elem2.pvy == 0)){ + //elem2.vx = 0; + //elem2.vy = 0; + } + + if(elem1.sax > elem1.vx || elem1.say > elem1.vy && (elem1.pvx == 0 || elem1.pvy == 0)){ + //elem1.vx = 0; + //elem1.vy = 0; + } + + } + }); +} + +// Function called when the add particle button is submitted +function addParticle(){ + var form = document.getElementById("add"); + var errMsg = "Errors: "; // String to store all errors with entered data + // Mass cannot be less than 1 error + if(form["mass"].value < 1){ + errMsg += "Cannot Have Negative, or Zero Mass! " + } + // Mass is too large for the canvas size error + else if(getRadius(form["mass"].value)*2 + 10 > canvas.width || getRadius(form["mass"].value)*2 + 10 > canvas.height){ + errMsg += "That mass is too large for this canvas! " + }else if(!form["xvel"].value || !form["yvel"].value || !form["mass"].value){ + errMsg += "One or more values is not defined! " + } + // If no errors, add rows to the table and insert the particle into the array + else{ + + + var xv = parseInt(form["xvel"].value) + var yv = parseInt(form["yvel"].value) + + // Check if the acceleration is larger than the velocity, if so set velocity to 0 and not negative. + + let n = 1; + if(particles.length > 0){ + n = particles[particles.length-1].id +1; + } + + // Find a position that is not ontop of another ball, or inside of a wall. This will randomize the position 9 times or until a successful location is discovered. + let validPos = false; + let numtries = 0; + let tx = 0; let ty = 0; + while(!validPos && numtries < 9){ + numtries ++; + validPos = true; + tx = Math.round(Math.random()*((canvas.width - getRadius(parseInt(form["mass"].value))) - getRadius(parseInt(form["mass"].value)) - 5) + getRadius(parseInt(form["mass"].value)) + 5); + ty = Math.round(Math.random()*((canvas.height - getRadius(parseInt(form["mass"].value))) - getRadius(parseInt(form["mass"].value)) - 5) + getRadius(parseInt(form["mass"].value)) + 5); + let temp = new Particle(n, tx, ty, xv, yv, parseInt(form["mass"].value)+5); + particles.forEach(particle =>{ + if(getDistance(particle, temp) < particle.r + temp.r){ + validPos = false; + } + }) + } + // if a valid position was found create it + if(validPos){ + table.insertRow(); + table.rows[table.rows.length -1].insertCell(); + table.rows[table.rows.length -1].insertCell(); + table.rows[table.rows.length -1].insertCell(); + table.rows[table.rows.length -1].insertCell(); + table.rows[table.rows.length -1].insertCell(); + table.rows[table.rows.length -1].insertCell(); + particles.push(new Particle(n, tx, ty, xv, yv, parseInt(form["mass"].value))); + }else{ + errMsg += "Not enough room for that particle on the canvas! " + } + + reloadDropdowns(); + } + if(errMsg != "Errors: "){ + alert(errMsg); + } +} + +// Function that returns a radius, by multiplying the given mass by a constant +function getRadius(mass){ + return mass*0.8; +} + +// Function to remove a particle from the array by id +function remParticle(id){ + alert("remove"); + id = parseInt(id, 10); + + particles = particles.filter(function(element, i){ + eid = element.id; + + if(id == eid){ + table.deleteRow(i+1); + } + return id != eid; + }); + console.log(particles); +} + +// Function that adds the data at the current time to their respective array +function measureData(){ + measurement++; + let ekt = 0; + particles.forEach((elem, i) => { + ekt += 0.5*Math.pow(elem.v, 2)*elem.m; + // Ek=0.5mv^2, add to total + elem.ek.push(0.5*Math.pow(elem.v, 2)*elem.m); + measurementData.push(measurement); + + }) + ek.push(ekt.toPrecision(6)); + if(measurement > 100){ + measurement = 0; + measurementData = []; + measurementData.push('Time (s)'); + measurementData.push(0); + ek = []; + ek.push('Kientic Energy (J)'); + ek.push(0); + + particles.forEach((elem, i) => { + elem.ek = []; + elem.ek.push('Kientic Energy (J)'); + }) + } + drawKEChart(); +} + +// Function to draw the graph based on the selected values +function drawKEChart() { + //console.log(chartX); + let x = []; + if(chartX == "measurement"){ + x = [...measurementData]; + }else if(chartX == "ekt"){ + x = [...ek]; + }else{ + x = [...particles[parseInt(chartX, 10)-1].ek]; + } + + let y = []; + if(chartY == "measurement"){ + y = [...measurementData]; + }else if(chartY == "ekt"){ + y = [...ek]; + }else{ + y = [...particles[parseInt(chartY, 10)-1].ek]; + } + + var arr = []; + x.forEach((n, i) =>{ + arr.push([n, y[i]]); + }) + + var data = google.visualization.arrayToDataTable(arr); + + var options = { + title: 'Graph (Change X and Y Axis in the above form):', + legend: { position: 'bottom' } + }; + + var chart = new google.visualization.LineChart(document.getElementById('curve_chart')); + + chart.draw(data, options); + +} + +// Function called when static friction is changed, to make sure a valid number is selected +function sfrChange(){ + let element = document.getElementById("sfr"); + if(element.value >= 1){ + alert("The coefficient of static friction cannot be more than 1 for a billiard ball!"); + element.value = sfr; + }else if(element.value <= fr){ + alert("The coefficient of kinetic friction cannot be more than the coefficient of static friction!"); + element.value = sfr; + } + else{ + sfr = document.getElementById("sfr").value; + } +} + +// Function called when kinetic friction is changed, to make sure a valid number is selected +function frChange(){ + let element = document.getElementById("fr"); + if(element.value >= 1){ + alert("The coefficient of kinetic friction cannot be more than 1 for a billiard ball!"); + element.value = fr; + } + //else if(element.value >= sfr){ + // alert("The coefficient of kinetic friction cannot be more than the coefficient of static friction!"); + // element.value = fr; + //} + else{ + fr = element.value; + } +} + +// Function called when % effectiveness is changed, to make sure a valid number is selected +function effeChange(){ + let element = document.getElementById("effe"); + if(element.value > 100){ + alert("There cannot be more than 100% efficient collisions! "); + element.value = eff*100; + } + else if(element.value < 0){ + alert("There cannot be less than 0% effecient collisions! "); + element.value = eff*100; + }else{ + eff = element.value/100; + } +} + +// Function called to reset the dropdown menus when particles are added +function reloadDropdowns(){ + + let xSelect = document.getElementById("graphx"); + for(let i = 0; i < xSelect.options.length; i++){ + xSelect.options.remove(i); + i--; + } + + let ySelect = document.getElementById("graphy"); + for(let j = 0; j < ySelect.options.length; j++){ + ySelect.options.remove(j); + j--; + } + + let xtOption = document.createElement("option"); + xtOption.text = "Time" + xtOption.value = "measurement" + + let xeOption = document.createElement("option"); + xeOption.text = "Total Kinetic Energy" + xeOption.value = "ekt" + + let ytOption = document.createElement("option"); + ytOption.text = "Time" + ytOption.value = "measurement" + + let yeOption = document.createElement("option"); + yeOption.text = "Total Kinetic Energy" + yeOption.value = "ekt" + + xSelect.options.add(xtOption, 0); + xSelect.options.add(xeOption, 1); + + ySelect.options.add(ytOption, 1); + ySelect.options.add(yeOption, 0); + + particles.forEach((elem, i) =>{ + let temp = document.createElement("option"); + let temp2 = document.createElement("option"); + temp.text = "Particle " + (i+1).toString() + " Kinetic Energy" + temp.value = i+1; + temp2.text = "Particle " + (i+1).toString() + " Kinetic Energy" + temp2.value = i+1; + xSelect.options.add(temp, xSelect.options.length-1); + ySelect.options.add(temp2, ySelect.options.length-1); + }) +} + +function setVelocity(event){ + let x = event.clientX-25; + let y = event.clientX-25; + while(mouseIsDown){ + console.log(x); + } +} + +function doMouseDown(event){ + mouseIsDown = true; + setVelocity(event); +} + +function doMouseUp(event){ + alert(); + mouseIsDown = false; +} \ No newline at end of file diff --git a/shot.mp3 b/shot.mp3 new file mode 100644 index 0000000..58ab220 Binary files /dev/null and b/shot.mp3 differ diff --git a/wallhit.mp3 b/wallhit.mp3 new file mode 100644 index 0000000..da98b90 Binary files /dev/null and b/wallhit.mp3 differ