Last active
August 29, 2015 14:09
-
-
Save chemplexity/2f6a6195cbe89d10f5f4 to your computer and use it in GitHub Desktop.
Interactive Mass Spectrometer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<style> | |
body { | |
margin:10; | |
padding:10; | |
} | |
canvas { | |
background-color: white; | |
} | |
p { | |
font-family: "Verdana"; | |
font-size: 14px; | |
} | |
div#strength { | |
position:absolute; | |
width:300px; | |
height:100px; | |
left:50px; | |
top:210px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="strength"> | |
<table> | |
<tr><td align="right"><p>Acceleration Voltage</p></td><td> </td> | |
<td align="middle"><input type="range" min="2.4" max="4.2" step="0.01" onchange="voltageSlider(this.value)"/></td></tr> | |
<tr><td align="right"><p>Ion Optics</p></td><td> </td> | |
<td align="middle"><input type="range" min="-0.9" max="2.1" step="0.01" onchange="opticsSlider(this.value)"/></td></tr> | |
<tr><td align="right"><p>Magnetic Sector</p></td><td> </td> | |
<td align="middle"><input type="range" min="-200" max="-50" step="0.01" onchange="magnetSlider(this.value)"/></td></tr> | |
</table> | |
</div> | |
<canvas></canvas> | |
</body> | |
</html> | |
<script> | |
"use strict"; | |
// Global Variables | |
var maxParticles = 2500, | |
particleSize = 3, | |
emissionRate = 9, | |
objectSize = 5, | |
fieldValue = -160; | |
// Counter Variables | |
var counterA = 0, | |
counterB = 0, | |
counterC = 0, | |
counterIons = 0; | |
// Position Variables | |
var startX = 40, | |
startY = 40, | |
cableX = startX+125, | |
cableY = startY+120; | |
// Text Variables | |
var maxDisplay = 0, | |
maxIon = 0; | |
// Canvas Properties | |
var canvas = document.querySelector('canvas'); | |
var ctx = canvas.getContext('2d'); | |
canvas.width = 400; | |
canvas.height = 400; | |
// Particle Object | |
function Particle(point, velocity, acceleration) { | |
this.position = point || new Vector(0, 0); | |
this.velocity = velocity || new Vector(0, 0); | |
this.acceleration = acceleration || new Vector(0, 0); | |
} | |
// Magnetic Field Calculations | |
Particle.prototype.submitToFields = function (fields) { | |
// Particle Acceleration | |
var totalAccelerationX = 0; | |
var totalAccelerationY = 0; | |
// Calculations | |
for (var i = 0; i < fields.length; i++) { | |
var field = fields[i]; | |
// Distance to Magnet | |
var vectorX = field.position.x - this.position.x; | |
var vectorY = field.position.y - this.position.y; | |
// Force of Magnet | |
var force = field.mass / Math.pow(vectorX * vectorX + vectorY * vectorY, 1.5); | |
totalAccelerationX += vectorX * force; | |
totalAccelerationY += vectorY * force; | |
} | |
// Update Acceleration | |
this.acceleration = new Vector(totalAccelerationX, totalAccelerationY); | |
}; | |
// Particle Properties | |
Particle.prototype.move = function () { | |
this.velocity.add(this.acceleration); | |
this.position.add(this.velocity); | |
}; | |
// Field Position | |
function Field(point, mass) { | |
this.position = point; | |
this.setMass(mass); | |
} | |
// Field Properties | |
Field.prototype.setMass = function(mass) { | |
this.mass = mass || 10; | |
this.drawColor = mass < 0 ? "#f00" : "#0f0"; | |
} | |
// Emitter Properties | |
function Vector(x, y) {this.x = x || 0; this.y = y || 0;} | |
Vector.prototype.add = function(vector) { | |
this.x += vector.x; | |
this.y += vector.y;} | |
Vector.prototype.getMagnitude = function () { | |
return Math.sqrt(this.x * this.x + this.y * this.y)} | |
Vector.prototype.getAngle = function () { | |
return Math.atan2(this.y, this.x)} | |
Vector.fromAngle = function (angle, magnitude) { | |
return new Vector(magnitude * Math.cos(angle), magnitude * Math.sin(angle))} | |
// Create Emitter | |
function Emitter(point, velocity, spread) { | |
// Emitter Properties | |
this.position = point; | |
this.velocity = velocity; | |
this.spread = spread || Math.PI / 360; | |
this.drawColor = "#202020"; | |
} | |
// Update Emitter | |
Emitter.prototype.emitParticle = function() { | |
// Particle Properties | |
var angle = this.velocity.getAngle() + this.spread - (Math.random() * this.spread * 3.5); | |
var initial = this.velocity.getMagnitude(); | |
var magnitude = initial + (Math.random() * initial * 0.08); | |
var position = new Vector(this.position.x, this.position.y); | |
var velocity = Vector.fromAngle(angle, magnitude); | |
return new Particle(position,velocity); | |
} | |
// Create Particle | |
function addNewParticles() { | |
// Stop Filter | |
if (particles.length > maxParticles) return; | |
// Emitter | |
for (var i = 0; i < emitters.length; i++) { | |
// Particle Array | |
for (var j = 0; j < emissionRate; j++) { | |
particles.push(emitters[i].emitParticle()); | |
} | |
} | |
} | |
// Particle Animation | |
function plotParticles(boundsX, boundsY) { | |
// Particle Array | |
var currentParticles = []; | |
// Particle Movement | |
for (var i = 0; i < particles.length; i++) { | |
var particle = particles[i]; | |
var pos = particle.position; | |
// Out of Bounds | |
if (pos.x < 0 || pos.x > boundsX || pos.y < 0 || pos.y > boundsY) continue; | |
// Update Detector | |
if (pos.x <= startX+290 && pos.x >= startX+281 && pos.y >= startY+55 && pos.y <= startY+55+7) { | |
counterA += 1; | |
}; | |
// Particle Trajectory | |
particle.submitToFields(fields); | |
particle.move(); | |
currentParticles.push(particle); | |
} | |
// Particle Array | |
particles = currentParticles; | |
} | |
// Draw Particles | |
function drawParticles() { | |
// Particle Color | |
ctx.fillStyle = "rgba(10, 70, 250, 0.25)"; | |
// Particle Properties | |
for (var i = 0; i < particles.length; i++) { | |
var position = particles[i].position; | |
ctx.fillRect(position.x, position.y, particleSize, particleSize); | |
} | |
} | |
// Draw Fields | |
function drawCircle(object) { | |
//Fields | |
ctx.fillStyle = object.drawColor; | |
ctx.beginPath(); | |
ctx.arc(object.position.x, object.position.y, objectSize, 0, Math.PI * 3); | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
// Draw Detector | |
function drawDetector() { | |
// Save Canvas | |
ctx.save(); | |
// Position | |
var x = ctx.width, | |
y = ctx.height, | |
detectorX = 280, | |
detectorY = 135, | |
deg = 33; | |
// Detector | |
ctx.beginPath(); | |
ctx.translate(x, y); | |
ctx.rotate(deg * Math.PI / 180); | |
ctx.rect(startX+detectorX, startY-detectorY-2.5, 15, 5.5); | |
ctx.fillStyle = "rgba(0, 0, 0, 0.9)" | |
ctx.fill(); | |
// Top Line | |
ctx.beginPath(); | |
ctx.translate(x, y); | |
ctx.rotate(0 * Math.PI / 180); | |
ctx.rect(startX+detectorX-7, startY-detectorY-5, 22, 3); | |
ctx.fillStyle = "rgba(0, 0, 0, 0.5)" | |
ctx.fill(); | |
// Bottom Line | |
ctx.beginPath(); | |
ctx.translate(x, y); | |
ctx.rotate(0 * Math.PI / 180); | |
ctx.rect(startX+detectorX-7, startY-detectorY+3, 22, 3); | |
ctx.fillStyle = "rgba(0, 0, 0, 0.5)" | |
ctx.fill(); | |
// Restor Canvas | |
ctx.restore(); | |
// Cable Position | |
var fromX = startX+detectorX+4; | |
var fromY = startY+detectorY-70.5; | |
var toX = startX+125; | |
var toY = startY+120; | |
var cp1X = startX+220; | |
var cp1Y = startY+200; | |
var cp2X = startX+230; | |
var cp2Y = startY+20; | |
// Cable | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.moveTo(fromX, fromY); | |
ctx.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, toX, toY); | |
ctx.stroke(); | |
ctx.closePath(); | |
} | |
// Draw Computer | |
function drawComputer() { | |
// Computer Position | |
var computerX = startX+123; | |
var computerY = startY+80; | |
var detectorX = 310; | |
var detectorY = 130; | |
var fromX = startX+detectorX+2; | |
var fromY = startY+detectorY-64; | |
var toX = startX+125; | |
var toY = startY+120; | |
var cp1X = startX+220; | |
var cp1Y = startY+200; | |
var cp2X = startX+250; | |
var cp2Y = startY+20; | |
// Computer Screen | |
ctx.beginPath(); | |
ctx.rect(computerX-80, computerY, 50, 40); | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)" | |
ctx.lineWidth = 4; | |
ctx.stroke(); | |
// Computer Stand | |
ctx.beginPath(); | |
ctx.rect(computerX-65, computerY+45, 20, 2); | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)" | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
// Computer Base | |
ctx.beginPath(); | |
ctx.rect(computerX-20, computerY, 20, 47); | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)" | |
ctx.lineWidth = 4; | |
ctx.stroke(); | |
// Computer Button Bottom | |
ctx.beginPath(); | |
ctx.rect(computerX-10.5, computerY + 34, 2, 4); | |
ctx.strokeStyle = powerColor; | |
ctx.lineWidth = 2.5; | |
ctx.stroke(); | |
// Computer Button Top | |
ctx.beginPath(); | |
ctx.rect(computerX-13.2, computerY + 7, 7, 0.5); | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)" | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
// Computer Button Middle | |
ctx.beginPath(); | |
ctx.rect(computerX-14, computerY + 12, 8.5, 0.5); | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.7)" | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
// Ions/Second | |
ctx.font = '7pt Verdana'; | |
ctx.textAlign = 'center'; | |
ctx.fillStyle = 'black'; | |
ctx.fillText("ions/sec", computerX-55, computerY+32); | |
} | |
// Update Computer | |
function updateComputer() { | |
// Computer Position | |
var computerX = startX+123; | |
var computerY = startY+80; | |
var count = 0; | |
if (maxDisplay == 0) {count = counterIons} | |
else if (maxDisplay == 1) {count = maxIon} | |
// Ions/Second | |
ctx.font = '10pt Verdana'; | |
ctx.textAlign = 'center'; | |
ctx.fillStyle = 'black'; | |
ctx.fillText(count.toString(), computerX-55, computerY+20); | |
} | |
// Timer | |
function timedCount() { | |
// Average Ions (over 3 seconds) | |
counterIons = Math.round((counterA + counterB + counterC) / 3); | |
// Pause | |
setTimeout(function(){timedCount()}, 1000); | |
// Update Text | |
updateComputer(); | |
// Update Counter | |
counterC = counterB; | |
counterB = counterA; | |
counterA = 0; | |
//Update Max | |
if (counterIons > maxIon) {maxIon = counterIons} | |
} | |
// Intialize Timer | |
timedCount(); | |
// Draw Mass Spectrometer | |
function drawMS() { | |
// Emitter | |
ctx.beginPath(); | |
ctx.rect(startX-10, startY-1, 24, 9); | |
ctx.fillStyle = "rgba(0, 0, 0, 0.4)" | |
ctx.fill(); | |
ctx.lineWidth = 0.5; | |
ctx.stroke(); | |
// Magnetic Sector | |
var sectorX1 = startX + 150; | |
var sectorX2 = sectorX1; | |
var sectorY1 = startY + 175; | |
var sectorY2 = sectorY1 - 20; | |
var sectorLine = 7; | |
var radius = 190; | |
var startAngle = 1.5 * Math.PI; | |
var endAngle = 1.67 * Math.PI; | |
var counterClockwise = false; | |
// Top Arch | |
ctx.beginPath(); | |
ctx.arc(sectorX1, sectorY1, radius, startAngle, endAngle, counterClockwise); | |
ctx.strokeStyle = "rgba(20, 20, 20, 0.7)" | |
ctx.lineWidth = sectorLine; | |
ctx.stroke(); | |
// Bottom Arch | |
ctx.beginPath(); | |
ctx.arc(sectorX2, sectorY2, radius-(0.3 * radius), startAngle, endAngle, counterClockwise); | |
ctx.strokeStyle = "rgba(20, 20, 20, 0.7)" | |
ctx.lineWidth = sectorLine; | |
ctx.stroke(); | |
// Top Line | |
ctx.beginPath(); | |
ctx.rect(startX+42.5, startY+14, 80, 6.0); | |
ctx.fillStyle = "rgba(10, 10, 10, 0.35)" | |
ctx.lineWidth = 0.75; | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.4)" | |
ctx.stroke(); | |
ctx.fill(); | |
// Bottom Line | |
ctx.beginPath(); | |
ctx.rect(startX+42.5, startY-13, 80, 6.0); | |
ctx.fillStyle = "rgba(10, 10, 10, 0.35)" | |
ctx.fill(); | |
ctx.strokeStyle = "rgba(0, 0, 0, 0.4)" | |
ctx.lineWidth = 0.75; | |
ctx.stroke(); | |
} | |
// Magnetic Sector Slider | |
function magnetSlider(force) { | |
fields[0].mass = -force; | |
} | |
// Ion Optics Slider | |
function opticsSlider(force) { | |
// Sharpen Condition | |
//if (force < 0.5 && force > 0) {force = 0.1} | |
emitters[0].spread = (Math.PI / 360) * force * force | |
emitters[1].spread = (Math.PI / 360) * force * force | |
} | |
// Acceleration Voltage Slider | |
function voltageSlider(force) { | |
emitters[0].velocity.x = force - 0.1 | |
emitters[1].velocity.x = force | |
} | |
// Particle Array | |
var particles = []; | |
// Position | |
var midX = canvas.width / 2; | |
var midY = canvas.height / 2; | |
// Emitters | |
var emitters = [new Emitter(new Vector(midX - 150, startY + 2), Vector.fromAngle(-0.0, 3.3)), | |
new Emitter(new Vector(midX - 150, startY + 2), Vector.fromAngle(-0.0, 3.4))]; | |
// Fields //50 27 50 45 130 -10 | |
var fields = [new Field(new Vector(midX + 34, startY + 45), -fieldValue)]; | |
// Animation | |
function loop() { | |
clear(); | |
update(); | |
draw(); | |
queue(); | |
} | |
// Clear | |
function clear() { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
} | |
// Update | |
function update() { | |
addNewParticles(); | |
plotParticles(canvas.width, canvas.height/2); | |
} | |
// Draw | |
function draw() { | |
drawParticles(); | |
drawDetector(); | |
drawMS(); | |
drawComputer(); | |
updateComputer(); | |
//fields.forEach(drawCircle); | |
//emitters.forEach(drawCircle); | |
} | |
// Wait | |
function queue() { | |
window.requestAnimationFrame(loop); | |
} | |
// Start | |
loop(); | |
// Mouse Coordinates | |
function getMousePos(canvas, evt) { | |
// Boundary | |
var rect = canvas.getBoundingClientRect(); | |
// XY Values | |
return {x: evt.clientX - rect.left, | |
y: evt.clientY - rect.top}; | |
} | |
// Mouse Position | |
var mousePos = [] | |
// Position Listener | |
canvas.addEventListener('mousemove', function(evt) { | |
mousePos = getMousePos(canvas, evt) | |
if (mousePos.x >= startX+43 && mousePos.x <= startX+43+50 && | |
mousePos.y >= startY+80 && mousePos.y <= startY+80+40) { | |
maxDisplay = 1} | |
else {maxDisplay = 0} | |
}, false); | |
// Computer Screen Position | |
var screen = {left: startX+43, top: startY+80, right: startX+43+50, bottom: startY+80+40}; | |
var toggleLow = 0, | |
togglePower = 0, | |
maxParticlesLast = maxParticles, | |
emissionRateLast = emissionRate, | |
powerColor = ""; | |
if (togglePower == 0) {powerColor = "rgba(20, 230, 20, 0.9)"} | |
else if(togglePower == 1) {powerColor = "rgba(230, 20, 20, 0.9)"} | |
canvas.addEventListener('click', function(){ | |
if (mousePos.x >= startX+43 && mousePos.x <= startX+43+50 && | |
mousePos.y >= startY+80 && mousePos.y <= startY+80+40 && | |
toggleLow == 0) { | |
toggleLow = 1 | |
togglePower = 0 | |
maxParticles = 500 | |
emissionRate = 2 | |
powerColor = "rgba(20, 230, 20, 0.9)"} | |
else if (mousePos.x >= startX+43 && mousePos.x <= startX+43+50 && | |
mousePos.y >= startY+80 && mousePos.y <= startY+80+40 && | |
toggleLow == 1) { | |
toggleLow = 0 | |
togglePower = 0 | |
maxParticles = 2500 | |
emissionRate = 9 | |
powerColor = "rgba(20, 230, 20, 0.9)" | |
} | |
else if (mousePos.x >= startX+123-20 && mousePos.x <= startX+123-20+20 && | |
mousePos.y >= startY+80 && mousePos.y <= startY+80+47 && | |
togglePower == 0) { | |
togglePower = 1 | |
maxParticlesLast = maxParticles | |
maxParticles = 0 | |
emissionRateLast = emissionRate | |
emissionRate = 0 | |
powerColor = "rgba(230, 20, 20, 0.9)" | |
} | |
else if (mousePos.x >= startX+123-20 && mousePos.x <= startX+123-20+20 && | |
mousePos.y >= startY+80 && mousePos.y <= startY+80+47 && | |
togglePower == 1) { | |
togglePower = 0 | |
maxParticles = maxParticlesLast | |
emissionRate = emissionRateLast | |
powerColor = "rgba(20, 230, 20, 0.9)" | |
} | |
}); | |
// Misc. Code | |
function misc(){ | |
// Text | |
ctx.font = '10pt Verdana'; | |
ctx.textAlign = 'center'; | |
ctx.fillStyle = 'black'; | |
ctx.fillText('Source', 30, textY); | |
// Computer Color | |
ctx.beginPath(); | |
ctx.rect(computerX-80, computerY, 50, 40); | |
ctx.fillStyle = "rgba(62, 219, 67, 0.7"; | |
ctx.fill(); | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment