<![CDATA[Rates of Reactions with Concentration of Reactant 1 (Blue), Temperature and Amount and Surface Area of Reactant 2 ]]> false false true false ]]> ./rateofreactants/Screenshot 2022-10-17 at 8.55.55 PM (2).png ./1authorlookangphoto5050.png; DESCRIPTION_EDITOR Intro Page true false _default_ Intro Page false

This will be a EJSS-based particle physics simulation

]]>
20 5 false VARIABLE_EDITOR Var Table true false VARIABLE_EDITOR User Configuration true false VARIABLE_EDITOR lookang true false VARIABLE_EDITOR interfaceOptions true false CODE_EDITOR undefined false false CODE_EDITOR User-Editable Particle Definitions true false 0) ? opts[0] : ""; // selected option let idx = redConcentrationNames.indexOf(option); //idx = redConcentrationNames.indexOf(option); //alert(option) //idx = 0 if selected //idx = -1 else if (idx !== -1) { redConcentrationSetting = idx; } opts = _view.temperature.getProperty("SelectedOptions"); // array of options option = (opts.length > 0) ? opts[0] : ""; // selected option idx = temperatureNames.indexOf(option); if (idx !== -1) { temperatureSetting = idx; } opts = _view.surfaceArea.getProperty("SelectedOptions"); // array of options option = (opts.length > 0) ? opts[0] : ""; // selected option idx = surfaceAreaNames.indexOf(option); if (idx !== -1) { surfaceAreaSetting = idx; } opts = _view.blueConcentration.getProperty("SelectedOptions"); // array of options option = (opts.length > 0) ? opts[0] : ""; // selected option idx = blueConcentrationNames.indexOf(option); if (idx !== -1) { blueConcentrationSetting = idx; } let temperature = temperatures[temperatureSetting]; let reactant2Separation = surfaceAreaNumbers[surfaceAreaSetting]; let reactant2Radius = 8; let reactant1Radius = 8; // Define the types of particles let r1 = new ParticleType("reactant1"); let r2 = new ParticleType("reactant2"); let pr = new ParticleType("product"); r1.setFillColor("blue"); r1.setRadius(8); r1.setRestitution(1); r1.setFriction(0); r2.setFillColor("red"); r2.setRadius(reactant2Radius); r2.setRestitution(1); r2.setDensity(10); r2.setFriction(0); pr.setFillColor("green"); pr.setRadius(12); pr.setDensity(5); pr.setRestitution(1); pr.setFriction(0); // When reactant1 collides with reactant2, reactant1 becomes product and reactant2 gets destroyed r1.setCollisionHandler("reactant2", function (r2Part) { this.vel.scale(this.mass); this.vel.addScaled(r2Part.vel, r2Part.mass); this.changeType("product"); this.vel.scale(this.invmass); r2Part.destroy(); return false; }); // Spawn reactant2 let nx, ny; nx = ny = redConcentrationNumbers[redConcentrationSetting]; for (let x = -(nx - 1) * (reactant2Radius + reactant2Separation); x <= (nx - 1) * (reactant2Radius + reactant2Separation); x += (reactant2Radius + reactant2Separation) * 2) { for (let y = -(ny - 1) * (reactant2Radius + reactant2Separation); y <= (ny - 1) * (reactant2Radius + reactant2Separation); y += (reactant2Radius + reactant2Separation) * 2) { particleSystem.addParticle(new Particle("reactant2", x, y)); } } // Spawn reactant1 and set initial velocity let v = new Vector(temperature + 30, 0); let hasMult = false; for (let i = 0; i < blueConcentrationNumbers[blueConcentrationSetting]; i++) { let x, y, s; s = 0; do { x = randomRange(-width / 2 + reactant1Radius, width / 2 - + reactant1Radius); y = randomRange(-height / 2 + reactant1Radius, height / 2 - reactant1Radius); s += 1; } while (!particleSystem.isPositionFree(x, y, reactant1Radius + 5) && s < 50); let p = new Particle("reactant1", x, y); if (!hasMult) { hasMult = true; v.scale(p.mass); } p.applyImpulse(v.rotate(Math.random() * 2 * Math.PI)); particleSystem.addParticle(p); } particleSystem.setWallEnergy(temperature); }; ]]> CODE_EDITOR Post-initialization true false CODE_EDITOR lookang true false CODE_EDITOR lookangtextset true false ODE_EDITOR Evol Page true false t 0.05 Euler 10000 0.00001 false false false false CODE_EDITOR fixedRel true false CODE_EDITOR user true false LIBRARY_EDITOR PartSim false false = 1 && vector instanceof Vector)) { vector = new Vector(); } return vector.set(this).normalize(); }; Vector.prototype.rotate = function(radians) { let c = Math.cos(radians), s = Math.sin(radians); let x = this.x; this.x = c * this.x - s * this.y; this.y = s * x + c * this.y; return this; }; Vector.prototype.addScaled = function(vector, scalar) { if (!("x" in vector && "y" in vector)) { throw new TypeError(`x and y are not members of vector`); } this.x += vector.x * scalar; this.y += vector.y * scalar; }; function randomRange(min, max) { return min + (max - min) * Math.random(); } function checkIfIsValidCssColor(color) { return CSS.supports("color", color); }; function ParticleType(name) { if (name in ParticleType.types) { debug.warn(`Particle type "${name}" already exists! Overriding`); } this.name = name; this.minRadius = 16; this.maxRadius = 16; this.minDensity = 1; this.maxDensity = 1; this.isOutlineVisible = true; this.outlineColor = "black"; this.outlineThickness = 1; this.isFillVisible = true; this.fillColor = "white"; this.minRestitution = 0.5; this.maxRestitution = 0.5; this.minFriction = 0; this.maxFriction = 0; this.minLinearDamping = 0; this.maxLinearDamping = 0; this.minAngularDamping = 0; this.maxAngularDamping = 0; this.collisionHandlers = Object.create(null); ParticleType.types[name] = this; }; ParticleType.types = Object.create(null); ParticleType.get = function(name) { if (name in ParticleType.types) { return ParticleType.types[name]; } else { return null; } } ParticleType.prototype.getName = function () { return this.name; }; ParticleType.prototype.setRadius = function (value) { if (value < 0) { debug.warn(`setRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.minRadius = value; this.maxRadius = value; }; ParticleType.prototype.getRadius = function () { return randomRange(this.minRadius, this.maxRadius); }; ParticleType.prototype.setMinRadius = function (value) { if (value < 0) { debug.warn(`setMinRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.minRadius = value; if (this.maxRadius < this.minRadius) { debug.info(`Setting minimum radius to ${value}, which is larger than the type's current maximum radius (${this.maxRadius})`); this.maxRadius = this.minRadius; } }; ParticleType.prototype.getMinRadius = function () { return this.minRadius; }; ParticleType.prototype.setMaxRadius = function (value) { if (value < 0) { debug.warn(`setMaxRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxRadius = value; if (this.minRadius > this.maxRadius) { debug.info(`Setting maximum radius to ${value}, which is smaller than the type's current minimum radius (${this.minRadius})`); this.minRadius = this.maxRadius; } }; ParticleType.prototype.getMaxRadius = function () { return this.maxRadius; }; ParticleType.prototype.setDensity = function (value) { if (value < 0) { debug.warn(`setDensity called with parameter value=${value}, which is less than 0!`); value = 0; } this.minDensity = value; this.maxDensity = value; }; ParticleType.prototype.getDensity = function () { return randomRange(this.minDensity, this.maxDensity); }; ParticleType.prototype.setMinDensity = function (value) { if (value < 0) { debug.warn(`setMinDensity called with parameter value=${value}, which is less than 0!`); value = 0; } if (value === 0) { debug.info('Setting minimum density to 0. Objects with density 0 behave as if they had infinite mass'); } this.minDensity = value; if (this.maxDensity < this.minDensity) { debug.info(`Setting minimum density to ${value}, which is larger than the type's current maximum density (${this.maxDensity})`); this.maxDensity = this.minDensity; } }; ParticleType.prototype.getMinDensity = function () { return this.minDensity; }; ParticleType.prototype.setMaxDensity = function (value) { if (value < 0) { debug.warn(`setMaxDensity called with parameter value=${value}, which is less than 0!`); value = 0; } if (value === 0) { debug.info("Setting maximum density to 0. All particles of this type will not be affected by collisions"); } this.maxDensity = value; if (this.minDensity > this.maxDensity) { this.minDensity = this.maxDensity; } }; ParticleType.prototype.getMaxDensity = function () { return this.maxDensity; }; ParticleType.prototype.setIsOutlineVisible = function (value) { if (value !== true && value !== false) { debug.info(`setIsOutlineVisible called with parameter value=${value}, which is not a boolean. This value is interpreted by JavaScript as a ${value ? "truthy" : "falsy"} value.`) } this.isOutlineVisible = value; }; ParticleType.prototype.getIsOutlineVisible = function () { return this.isOutlineVisible; }; ParticleType.prototype.setOutlineColor = function (value) { if (checkIfIsValidCssColor(value)) { this.outlineColor = value; } else { debug.warn(`setOutlineColor called with parameter value=${value}, which is not a valid color! Did nothing instead`); } }; ParticleType.prototype.getOutlineColor = function () { return this.outlineColor; }; ParticleType.prototype.setOutlineThickness = function (value) { if (value < 0) { debug.warn(`setOutlineThickness called with parameter value=${value}, which is less than 0!`); value = 0; } this.outlineThickness = value; }; ParticleType.prototype.getOutlineThickness = function () { return this.outlineThickness; }; ParticleType.prototype.setIsFillVisible = function (value) { if (value !== true && value !== false) { debug.info(`setIsFillVisible called with parameter value=${value}, which is not a boolean. This value is interpreted by JavaScript as a ${value ? "truthy" : "falsy"} value.`) } this.isFillVisible = value; }; ParticleType.prototype.getIsFillVisible = function () { return this.isFillVisible; }; ParticleType.prototype.setFillColor = function (value) { if (checkIfIsValidCssColor(value)) { this.fillColor = value; } else { debug.warn(`setFillColor called with parameter value=${value}, which is not a valid color! Did nothing instead`); } }; ParticleType.prototype.getFillColor = function () { return this.fillColor; }; ParticleType.prototype.setRestitution = function (value) { if (value < 0) { debug.warn(`setRestitution called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setRestitution called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minRestitution = value; this.maxRestitution = value; }; ParticleType.prototype.getRestitution = function () { return randomRange(this.minRestitution, this.maxRestitution); }; ParticleType.prototype.setMinRestitution = function (value) { if (value < 0) { debug.warn(`setMinRestitution called with parameter value=${value}, which is less than 0!`) value = 0; } if (value > 1) { debug.warn(`setMinRestitution called with parameter value=${value}, which is greater than 1!`) value = 1; } this.minRestitution = value; if (this.maxRestitution < this.minRestitution) { debug.info(`Setting minimum restitution to ${value}, which is greater than the type's current maximum restitution`) this.maxRestitution = this.minRestitution; } }; ParticleType.prototype.getMinRestitution = function () { return this.minRestitution; }; ParticleType.prototype.setMaxRestitution = function (value) { if (value < 0) { debug.warn(`setMaxRestitution called with parameter value=${value}, which is less than 0!`) value = 0; } if (value > 1) { debug.warn(`setMaxRestitution called with parameter value=${value}, which is greater than 1!`) value = 1; } this.maxRestitution = value; if (this.minRestitution > this.maxRestitution) { debug.info(`Setting maximum restitution to ${value}, which is less than the type's current minimum restitution`) this.minRestitution = this.maxRestitution; } }; ParticleType.prototype.getMaxRestitution = function () { return this.maxRestitution; }; ParticleType.prototype.setFriction = function (value) { if (value < 0) { debug.warn(`setFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minFriction = value; this.maxFriction = value; }; ParticleType.prototype.getFriction = function () { return randomRange(this.minFriction, this.maxFriction); }; ParticleType.prototype.setMinFriction = function (value) { if (value < 0) { debug.warn(`setMinFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setMinFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minFriction = value; if (this.maxFriction < this.minFriction) { debug.info(`Setting minimum friction to ${value}, which is greater than the type's current maximum friction`); this.maxFriction = this.minFriction; } }; ParticleType.prototype.getMinFriction = function () { return this.minFriction; }; ParticleType.prototype.setMaxFriction = function (value) { if (value < 0) { debug.warn(`setMaxFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setMaxFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.maxFriction = value; if (this.minFriction > this.maxFriction) { debug.info(`Setting maximum friction to ${value}, which is less than the type's current minimum friction`); this.minFriction = this.maxFriction; } }; ParticleType.prototype.getMaxFriction = function () { return this.maxFriction; }; ParticleType.prototype.setLinearDamping = function (value) { if (value < 0) { debug.warn(`setLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minLinearDamping = value; this.maxLinearDamping = value; }; ParticleType.prototype.getLinearDamping = function () { return randomRange(this.minLinearDamping, this.maxLinearDamping); }; ParticleType.prototype.setMinLinearDamping = function (value) { if (value < 0) { debug.warn(`setMinLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minLinearDamping = value; if (this.maxLinearDamping < this.minLinearDamping) { debug.info(`Setting minimum linear damping to ${value}, which is greater than the type's current maximum linear damping`); this.maxLinearDamping = this.minLinearDamping } }; ParticleType.prototype.getMinLinearDamping = function () { return this.minLinearDamping; }; ParticleType.prototype.setMaxLinearDamping = function (value) { if (value < 0) { debug.warn(`setMaxLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxLinearDamping = value; if (this.minLinearDamping > this.maxLinearDamping) { debug.info(`Setting maximum linear damping to ${value}, which is less than the type's current minimum linear damping`); } }; ParticleType.prototype.getMaxLinearDamping = function () { return this.maxLinearDamping; }; ParticleType.prototype.setAngularDamping = function (value) { if (value < 0) { debug.warn(`setAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minAngularDamping = value; this.maxAngularDamping = value; }; ParticleType.prototype.getAngularDamping = function () { return randomRange(this.minAngularDamping, this.maxAngularDamping); }; ParticleType.prototype.setMinAngularDamping = function (value) { if (value < 0) { debug.warn(`setMinAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minAngularDamping = value; if (this.maxAngularDamping < this.minAngularDamping) { debug.info(`Setting minimum angular damping to ${value}, which is greater than the type's current maximum angular damping`); } }; ParticleType.prototype.getMinAngularDamping = function () { return this.minAngularDamping; }; ParticleType.prototype.setMaxAngularDamping = function (value) { if (value < 0) { debug.warn(`setMaxAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxAngularDamping = value; if (this.minAngularDamping > this.maxAngularDamping) { debug.info(`Setting maximum angular damping to ${value}, which is less than the type's current minimum angular damping`); } }; ParticleType.prototype.getMaxAngularDamping = function () { return this.maxAngularDamping; }; // Return truthy values in any callback to prevent collisions from having physics-based effects ParticleType.prototype.setCollisionHandler = function (name, callbackOrCallbackArray) { let callbackArray = callbackOrCallbackArray; if (typeof callbackArray === "function") { callbackArray = [callbackArray]; } this.collisionHandlers[name] = callbackArray; }; ParticleType.prototype.addCollisionHandler = function (name, callbackOrCallbackArray) { let callbackArray = callbackOrCallbackArray; if (!(name in this.collisionHandlers)) { this.collisionHandlers[name] = []; } if (typeof callbackArray === "function") { callbackArray = [callbackArray]; } this.collisionHandlers[name].push(...callbackArray); }; function ParticleCollection() { // For now, naively just throw all elements in an array this.__allElements = []; this.index = 0; this.gravity = new Vector(gravityX, gravityY); this.wallEnergy = 0; this.hasWallEnergy = false; this.typeCounts = Object.create(null) if (ParticleCollection.particleSystem !== null) { throw "Only one singleton ParticleCollection may exist"; } xList = []; yList = []; pixelPosList = []; wList = []; hList = []; pixelSizeList = []; shapeTypeList = []; outlineColorList = []; outlineThicknessList = []; drawOutlineList = []; fillColorList = []; drawFillList = []; particleCount = 0; } ParticleCollection.prototype.getTypeCount = function(typeOrTypeName) { let typeName; if (typeof typeOrTypeName === "string" || typeOrTypeName instanceof String) { typeName = typeOrTypeName; } else if (typeOrTypeName instanceof ParticleType) { typeName = typeOrTypeName.getName(); } else { debug.error(`The parameter was not a string or ParticleType`); return 0; } if (typeName in this.typeCounts) { return this.typeCounts[typeName]; } else { return 0; } }; ParticleCollection.prototype.disableWallEnergy = function() { this.hasWallEnergy = false; }; ParticleCollection.prototype.setWallEnergy = function(energy) { this.wallEnergy = energy; this.hasWallEnergy = true; }; ParticleCollection.particleSystem = null; ParticleCollection.getCollection = function() { if (ParticleCollection.particleSystem === null) { ParticleCollection.particleSystem = new ParticleCollection(); } return ParticleCollection.particleSystem; }; ParticleCollection.prototype.registerTypeChange = function(fromType, toType) { let fromTypeName = fromType.getName(); let toTypeName = toType.getName(); if (fromTypeName in this.typeCounts) { this.typeCounts[fromTypeName] -= 1; } else { debug.error(`Type "${typeName}" does not exist! Particle count display may be inaccurate`); } if (!(toTypeName in this.typeCounts)) { this.typeCounts[toTypeName] = 0; } this.typeCounts[toTypeName] += 1; }; ParticleCollection.prototype.pruneDeletedElements = function() { for (let i = 0; i < this.__allElements.length; i++) { while (i < this.__allElements.length && this.__allElements[i].isRegisteredForDeletion) { let replacementElement = this.__allElements.pop(); let typeName = this.__allElements[i].type.getName(); if (typeName in this.typeCounts) { this.typeCounts[typeName] -= 1; } else { debug.error(`Type "${typeName}" does not exist! Particle count display may be inaccurate`); } if (i < this.__allElements.length) { this.__allElements[i] = replacementElement; } } } }; ParticleCollection.prototype.elementsTouching = function(x, y, radius, output=[]) { output.length = 0; // clear array // Naively let idx = 0; for (let i = 0; i < this.__allElements.length; i++) { let c = this.__allElements[i]; let p = c.pos; if ((p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) <= (radius + c.radius) * (radius + c.radius)) { output[idx] = c; idx += 1 } } output.length = idx; return output; }; ParticleCollection.prototype.isPositionFree = function(x, y, radius) { // Naively for (let i = 0; i < this.__allElements.length; i++) { let c = this.__allElements[i]; let p = c.pos; if ((p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) <= (radius + c.radius) * (radius + c.radius)) { return false; } } return true; } ParticleCollection.prototype.addParticle = function(particle) { this.__allElements.push(particle); particle._owner = new WeakRef(this); let typeName = particle.type.getName(); if (!(typeName in this.typeCounts)) { this.typeCounts[typeName] = 0; } this.typeCounts[typeName] += 1; particleCount += 1; }; ParticleCollection.prototype.spawnFreeParticles = function(particleType, count) { for (let i = 0; i < count; i++) { let tries = 0; let p = new Particle(particleType); do { p.pos.x = randomRange(-width/2 + p.radius, width/2 - p.radius); p.pos.y = randomRange(-height/2 + p.radius, height/2 - p.radius); tries += 1; } while (tries < 100 && !this.isPositionFree(p.pos.x, p.pos.y, p.radius)) this.addParticle(p); } }; ParticleCollection.prototype.getAllParticles = function() { return this.__allElements; }; ParticleCollection.prototype.handleWallCollisions = function(particle) { if (particle.pos.x < particle.radius - width/2) { particle.pos.x = particle.radius - width / 2; if (particle.vel.x < 0) { particle.vel.x = -particle.vel.x * particle.restitution; if (this.hasWallEnergy) { particle.vel.x = (particle.vel.x + this.wallEnergy) / 2; } } } if (particle.pos.x > width/2 - particle.radius) { particle.pos.x = width/2 - particle.radius; if (particle.vel.x > 0) { particle.vel.x = -particle.vel.x * particle.restitution; if (this.hasWallEnergy) { particle.vel.x = (particle.vel.x - this.wallEnergy) / 2; } } } if (particle.pos.y < particle.radius - height/2) { particle.pos.y = particle.radius - height / 2; if (particle.vel.y < 0) { particle.vel.y = -particle.vel.y * particle.restitution; if (this.hasWallEnergy) { particle.vel.y = (particle.vel.y + this.wallEnergy) / 2; } } } if (particle.pos.y > height/2 - particle.radius) { particle.pos.y = height/2 - particle.radius; if (particle.vel.y > 0) { particle.vel.y = -particle.vel.y * particle.restitution; if (this.hasWallEnergy) { particle.vel.y = (particle.vel.y - this.wallEnergy) / 2; } } } } ParticleCollection.prototype.onTimeStep = function(deltaT) { this.pruneDeletedElements(); let particles = this.getAllParticles(); if (this.index >= particles.length) { this.index = 0; } if (particles.length > 0) { particles[this.index].angle %= 2 * Math.PI; this.index += 1; } for (let i = 0; i < particles.length; i++) { particles[i]._handleCollisions(); } this.gravity.scale(deltaT); for (let i = 0; i < particles.length; i++) { particles[i].applyImpulse(this.gravity.scale(particles[i].mass)); this.gravity.scale(particles[i].invmass); particles[i]._timeStep(deltaT); this.handleWallCollisions(particles[i]); } this.render(); this.gravity.scale(1/deltaT); }; ParticleCollection.prototype.clear = function() { this.__allElements.length = 0; this.typeCounts = Object.create(null); }; ParticleCollection.prototype.render = function() { let particles = this.getAllParticles(); particleCount = particles.length; xList.length = particleCount; yList.length = particleCount; pixelPosList.length = particleCount; wList.length = particleCount; hList.length = particleCount; pixelSizeList.length = particleCount; shapeTypeList.length = particleCount; outlineColorList.length = particleCount; outlineThicknessList.length = particleCount; drawOutlineList.length = particleCount; fillColorList.length = particleCount; drawFillList.length = particleCount; for (let i = 0; i < particles.length; i++) { particles[i].render(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, i); } }; function Particle(type, x, y) { this.isRegisteredForDeletion = false; this.type = null; this.changeType(type); this.pos = new Vector(x, y); this.vel = new Vector(); this.angle = 0; this.angleV = 0; this._deltaAngleV = 0; this._deltaV = new Vector(); this._owner = null; // just to give a distinction to easily decide which particle gets to handle each pair of collisions this._collisionHandlePriority = Math.random(); }; Particle.normal = new Vector(); Particle.forceBuilder = new Vector(); Particle.prototype.changeType = function(type) { let fromType = this.type; if (typeof type === "string" || type instanceof String) { this.type = ParticleType.get(type); if (this.type === null) { throw TypeError("type did not correspond to an existing particle type"); } } else if (type instanceof ParticleType) { this.type = type; } else { throw TypeError("type was not a ParticleType or a type name"); } let owner if (fromType !== null && this._owner !== null && (owner = this._owner.deref()) !== undefined) { owner.registerTypeChange(fromType, this.type); } this.radius = this.type.getRadius(); this.density = this.type.getDensity(); this.restitution = this.type.getRestitution(); this.friction = this.type.getFriction(); this.linearDamping = this.type.getLinearDamping(); this.angularDamping = this.type.getAngularDamping(); if (this.density === 0) { this.mass = 0; this.invmass = 0; this.massMomentOfInertia = 0; this.invmmi = 0; } else { this.mass = this.density * Math.PI * this.radius * this.radius; this.invmass = 1.0 / this.mass; this.massMomentOfInertia = (this.radius * this.radius * this.mass) * 0.5; this.invmmi = 1/this.massMomentOfInertia; } }; Particle.prototype.destroy = function () { this.isRegisteredForDeletion = true; }; Particle.prototype.applyImpulse = function (impulse, offset = null) { // Applies an impulse at an offset relative to the center of the particle this._deltaV.addScaled(impulse, this.invmass); if (offset !== null) { this.applyMoment(offset.crossZ(impulse)); } }; Particle.prototype.applyMoment = function (moment) { this._deltaAngleV += moment * this.invmmi; }; Particle.prototype._handleCollisions = function (particleCollection=null) { if (this.isRegisteredForDeletion) { return; } if (particleCollection === null) { particleCollection = this._owner.deref(); } if (particleCollection !== undefined) { let toCollideWith = particleCollection.elementsTouching(this.pos.x, this.pos.y, this.radius); for (let i = 0; i < toCollideWith.length; i++) { let other = toCollideWith[i]; // The first condition excludes collisions with self if (this === other) continue; // The second condition ensures that the types' collision handlers are called in a consistent order if (this.type.getName() < other.type.getName()) continue; // The third condition just makes sure that only one particle will handle collision in each pair if (this.type.getName() === other.type.getName() && this._collisionHandlePriority < other._collisionHandlePriority) continue; // The fourth option is to skip processing deleted particles if (other.isRegisteredForDeletion) continue; let willIgnoreCollision = this._runCollisionHandlersWith(other); if (!willIgnoreCollision) { if (this.pos.x === other.pos.x && this.pos.y === other.pos.y) { this.pos.addScaled(this.vel, -1/this.vel.mag()); } this._resolveCollisionWith(other); } } } }; Particle.prototype._runCollisionHandlersWith = function (particle) { let willIgnoreCollision = false; if (particle.type.getName() in this.type.collisionHandlers) { let handlers = this.type.collisionHandlers[particle.type.getName()], len = handlers.length; for (let i = 0; i < len; i++) { if (handlers[i].call(this, particle)) { willIgnoreCollision = true; } } } if (this.type.getName() in particle.type.collisionHandlers) { let handlers = particle.type.collisionHandlers[this.type.getName()], len = handlers.length; for (let i = 0; i < len; i++) { if (handlers[i].call(particle, this)) { willIgnoreCollision = true; } } } return willIgnoreCollision; } Particle.prototype._resolveCollisionWith = function (particle) { Particle.normal.set(particle.pos).sub(this.pos); let penetration = this.radius + particle.radius - Particle.normal.mag(); Particle.normal.normalize(); Particle.forceBuilder.set(particle.vel).sub(this.vel); // Start with the velocity of particle relative to self let velApart = Particle.normal.dot(Particle.forceBuilder); Particle.forceBuilder.addScaled(Particle.normal, velApart); // It's now the tangental velocity this._separateIfOverlapping(particle, penetration); if (velApart > 0) return; let coefficientOfRestitution = Math.min(this.restitution, particle.restitution); let friction = Math.max(this.friction, particle.friction); Particle.forceBuilder.scale(friction); Particle.forceBuilder.addScaled(Particle.normal, coefficientOfRestitution * velApart); Particle.forceBuilder.scale(Math.min(this.mass, particle.mass)); this.applyImpulse(Particle.forceBuilder, Particle.normal.scale(this.radius)); Particle.normal.scale(1/this.radius); Particle.forceBuilder.scale(-1); particle.applyImpulse(Particle.forceBuilder, Particle.normal.scale(-particle.radius)); }; Particle.prototype._separateIfOverlapping = function (particle, penetration) { const separateAmount = 0.9; const activationAmount = 0.01; if (penetration > activationAmount) { let massRatio = particle.mass / (this.mass + particle.mass); this.pos.addScaled(Particle.normal, -penetration * separateAmount * massRatio); particle.pos.addScaled(Particle.normal, penetration * separateAmount * (1 - massRatio)); } }; Particle.prototype._timeStep = function (deltaT) { this.vel.add(this._deltaV); this.angleV += this._deltaAngleV; this._deltaV.set(); this._deltaAngleV = 0; this.pos.addScaled(this.vel, deltaT); this.angle += this.angleV * deltaT; this.vel.scale(Math.pow(1.0 - this.linearDamping, deltaT)); this.angleV *= Math.pow(1.0 - this.angularDamping, deltaT); }; Particle.prototype.render = function(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, index=-1) { if (index === -1) { index = xList.length; } xList[index] = this.pos.x; yList[index] = this.pos.y; pixelPosList[index] = true; wList[index] = this.radius * 2; hList[index] = this.radius * 2; pixelSizeList[index] = true; shapeTypeList[index] = "ELLIPSE"; outlineColorList[index] = this.type.getOutlineColor(); outlineThicknessList[index] = this.type.getOutlineThickness(); drawOutlineList[index] = this.type.getIsOutlineVisible(); fillColorList[index] = this.type.getFillColor(); drawFillList[index] = this.type.getIsFillVisible(); }; ]]> LIBRARY_EDITOR fullscreen true false LIBRARY_EDITOR PartSim from StatesofMatter false false = 1 && vector instanceof Vector)) { vector = new Vector(); } return vector.set(this).normalize(); }; Vector.prototype.rotate = function(radians) { let c = Math.cos(radians), s = Math.sin(radians); let x = this.x; this.x = c * this.x - s * this.y; this.y = s * x + c * this.y; return this; }; Vector.prototype.addScaled = function(vector, scalar) { if (!("x" in vector && "y" in vector)) { throw new TypeError(`x and y are not members of vector`); } this.x += vector.x * scalar; this.y += vector.y * scalar; }; function randomRange(min, max) { return min + (max - min) * Math.random(); } function checkIfIsValidCssColor(color) { return CSS.supports("color", color); }; function ParticleType(name) { if (name in ParticleType.types) { debug.warn(`Particle type "${name}" already exists! Overriding`); } this.name = name; this.minRadius = 16; this.maxRadius = 16; this.minDensity = 1; this.maxDensity = 1; this.isOutlineVisible = true; this.outlineColor = "black"; this.outlineThickness = 1; this.isFillVisible = true; this.fillColor = "white"; this.minRestitution = 0.5; this.maxRestitution = 0.5; this.minFriction = 0; this.maxFriction = 0; this.minLinearDamping = 0; this.maxLinearDamping = 0; this.minAngularDamping = 0; this.maxAngularDamping = 0; this.collisionHandlers = Object.create(null); ParticleType.types[name] = this; }; ParticleType.types = Object.create(null); ParticleType.get = function(name) { if (name in ParticleType.types) { return ParticleType.types[name]; } else { return null; } } ParticleType.prototype.getName = function () { return this.name; }; ParticleType.prototype.setRadius = function (value) { if (value < 0) { debug.warn(`setRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.minRadius = value; this.maxRadius = value; }; ParticleType.prototype.getRadius = function () { return randomRange(this.minRadius, this.maxRadius); }; ParticleType.prototype.setMinRadius = function (value) { if (value < 0) { debug.warn(`setMinRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.minRadius = value; if (this.maxRadius < this.minRadius) { debug.info(`Setting minimum radius to ${value}, which is larger than the type's current maximum radius (${this.maxRadius})`); this.maxRadius = this.minRadius; } }; ParticleType.prototype.getMinRadius = function () { return this.minRadius; }; ParticleType.prototype.setMaxRadius = function (value) { if (value < 0) { debug.warn(`setMaxRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxRadius = value; if (this.minRadius > this.maxRadius) { debug.info(`Setting maximum radius to ${value}, which is smaller than the type's current minimum radius (${this.minRadius})`); this.minRadius = this.maxRadius; } }; ParticleType.prototype.getMaxRadius = function () { return this.maxRadius; }; ParticleType.prototype.setDensity = function (value) { if (value < 0) { debug.warn(`setDensity called with parameter value=${value}, which is less than 0!`); value = 0; } this.minDensity = value; this.maxDensity = value; }; ParticleType.prototype.getDensity = function () { return randomRange(this.minDensity, this.maxDensity); }; ParticleType.prototype.setMinDensity = function (value) { if (value < 0) { debug.warn(`setMinDensity called with parameter value=${value}, which is less than 0!`); value = 0; } if (value === 0) { debug.info('Setting minimum density to 0. Objects with density 0 behave as if they had infinite mass'); } this.minDensity = value; if (this.maxDensity < this.minDensity) { debug.info(`Setting minimum density to ${value}, which is larger than the type's current maximum density (${this.maxDensity})`); this.maxDensity = this.minDensity; } }; ParticleType.prototype.getMinDensity = function () { return this.minDensity; }; ParticleType.prototype.setMaxDensity = function (value) { if (value < 0) { debug.warn(`setMaxDensity called with parameter value=${value}, which is less than 0!`); value = 0; } if (value === 0) { debug.info("Setting maximum density to 0. All particles of this type will not be affected by collisions"); } this.maxDensity = value; if (this.minDensity > this.maxDensity) { this.minDensity = this.maxDensity; } }; ParticleType.prototype.getMaxDensity = function () { return this.maxDensity; }; ParticleType.prototype.setIsOutlineVisible = function (value) { if (value !== true && value !== false) { debug.info(`setIsOutlineVisible called with parameter value=${value}, which is not a boolean. This value is interpreted by JavaScript as a ${value ? "truthy" : "falsy"} value.`) } this.isOutlineVisible = value; }; ParticleType.prototype.getIsOutlineVisible = function () { return this.isOutlineVisible; }; ParticleType.prototype.setOutlineColor = function (value) { if (checkIfIsValidCssColor(value)) { this.outlineColor = value; } else { debug.warn(`setOutlineColor called with parameter value=${value}, which is not a valid color! Did nothing instead`); } }; ParticleType.prototype.getOutlineColor = function () { return this.outlineColor; }; ParticleType.prototype.setOutlineThickness = function (value) { if (value < 0) { debug.warn(`setOutlineThickness called with parameter value=${value}, which is less than 0!`); value = 0; } this.outlineThickness = value; }; ParticleType.prototype.getOutlineThickness = function () { return this.outlineThickness; }; ParticleType.prototype.setIsFillVisible = function (value) { if (value !== true && value !== false) { debug.info(`setIsFillVisible called with parameter value=${value}, which is not a boolean. This value is interpreted by JavaScript as a ${value ? "truthy" : "falsy"} value.`) } this.isFillVisible = value; }; ParticleType.prototype.getIsFillVisible = function () { return this.isFillVisible; }; ParticleType.prototype.setFillColor = function (value) { if (checkIfIsValidCssColor(value)) { this.fillColor = value; } else { debug.warn(`setFillColor called with parameter value=${value}, which is not a valid color! Did nothing instead`); } }; ParticleType.prototype.getFillColor = function () { return this.fillColor; }; ParticleType.prototype.setRestitution = function (value) { if (value < 0) { debug.warn(`setRestitution called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setRestitution called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minRestitution = value; this.maxRestitution = value; }; ParticleType.prototype.getRestitution = function () { return randomRange(this.minRestitution, this.maxRestitution); }; ParticleType.prototype.setMinRestitution = function (value) { if (value < 0) { debug.warn(`setMinRestitution called with parameter value=${value}, which is less than 0!`) value = 0; } if (value > 1) { debug.warn(`setMinRestitution called with parameter value=${value}, which is greater than 1!`) value = 1; } this.minRestitution = value; if (this.maxRestitution < this.minRestitution) { debug.info(`Setting minimum restitution to ${value}, which is greater than the type's current maximum restitution`) this.maxRestitution = this.minRestitution; } }; ParticleType.prototype.getMinRestitution = function () { return this.minRestitution; }; ParticleType.prototype.setMaxRestitution = function (value) { if (value < 0) { debug.warn(`setMaxRestitution called with parameter value=${value}, which is less than 0!`) value = 0; } if (value > 1) { debug.warn(`setMaxRestitution called with parameter value=${value}, which is greater than 1!`) value = 1; } this.maxRestitution = value; if (this.minRestitution > this.maxRestitution) { debug.info(`Setting maximum restitution to ${value}, which is less than the type's current minimum restitution`) this.minRestitution = this.maxRestitution; } }; ParticleType.prototype.getMaxRestitution = function () { return this.maxRestitution; }; ParticleType.prototype.setFriction = function (value) { if (value < 0) { debug.warn(`setFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minFriction = value; this.maxFriction = value; }; ParticleType.prototype.getFriction = function () { return randomRange(this.minFriction, this.maxFriction); }; ParticleType.prototype.setMinFriction = function (value) { if (value < 0) { debug.warn(`setMinFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setMinFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minFriction = value; if (this.maxFriction < this.minFriction) { debug.info(`Setting minimum friction to ${value}, which is greater than the type's current maximum friction`); this.maxFriction = this.minFriction; } }; ParticleType.prototype.getMinFriction = function () { return this.minFriction; }; ParticleType.prototype.setMaxFriction = function (value) { if (value < 0) { debug.warn(`setMaxFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setMaxFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.maxFriction = value; if (this.minFriction > this.maxFriction) { debug.info(`Setting maximum friction to ${value}, which is less than the type's current minimum friction`); this.minFriction = this.maxFriction; } }; ParticleType.prototype.getMaxFriction = function () { return this.maxFriction; }; ParticleType.prototype.setLinearDamping = function (value) { if (value < 0) { debug.warn(`setLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minLinearDamping = value; this.maxLinearDamping = value; }; ParticleType.prototype.getLinearDamping = function () { return randomRange(this.minLinearDamping, this.maxLinearDamping); }; ParticleType.prototype.setMinLinearDamping = function (value) { if (value < 0) { debug.warn(`setMinLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minLinearDamping = value; if (this.maxLinearDamping < this.minLinearDamping) { debug.info(`Setting minimum linear damping to ${value}, which is greater than the type's current maximum linear damping`); this.maxLinearDamping = this.minLinearDamping } }; ParticleType.prototype.getMinLinearDamping = function () { return this.minLinearDamping; }; ParticleType.prototype.setMaxLinearDamping = function (value) { if (value < 0) { debug.warn(`setMaxLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxLinearDamping = value; if (this.minLinearDamping > this.maxLinearDamping) { debug.info(`Setting maximum linear damping to ${value}, which is less than the type's current minimum linear damping`); } }; ParticleType.prototype.getMaxLinearDamping = function () { return this.maxLinearDamping; }; ParticleType.prototype.setAngularDamping = function (value) { if (value < 0) { debug.warn(`setAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minAngularDamping = value; this.maxAngularDamping = value; }; ParticleType.prototype.getAngularDamping = function () { return randomRange(this.minAngularDamping, this.maxAngularDamping); }; ParticleType.prototype.setMinAngularDamping = function (value) { if (value < 0) { debug.warn(`setMinAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minAngularDamping = value; if (this.maxAngularDamping < this.minAngularDamping) { debug.info(`Setting minimum angular damping to ${value}, which is greater than the type's current maximum angular damping`); } }; ParticleType.prototype.getMinAngularDamping = function () { return this.minAngularDamping; }; ParticleType.prototype.setMaxAngularDamping = function (value) { if (value < 0) { debug.warn(`setMaxAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxAngularDamping = value; if (this.minAngularDamping > this.maxAngularDamping) { debug.info(`Setting maximum angular damping to ${value}, which is less than the type's current minimum angular damping`); } }; ParticleType.prototype.getMaxAngularDamping = function () { return this.maxAngularDamping; }; // Return truthy values in any callback to prevent collisions from having physics-based effects ParticleType.prototype.setCollisionHandler = function (name, callbackOrCallbackArray) { let callbackArray = callbackOrCallbackArray; if (typeof callbackArray === "function") { callbackArray = [callbackArray]; } this.collisionHandlers[name] = callbackArray; }; ParticleType.prototype.addCollisionHandler = function (name, callbackOrCallbackArray) { let callbackArray = callbackOrCallbackArray; if (!(name in this.collisionHandlers)) { this.collisionHandlers[name] = []; } if (typeof callbackArray === "function") { callbackArray = [callbackArray]; } this.collisionHandlers[name].push(...callbackArray); }; function ParticleCollection() { // For now, naively just throw all elements in an array this.__allElements = []; this.index = 0; this.gravity = new Vector(gravityX, gravityY); this.wallEnergy = 0; this.hasWallEnergy = false; this.typeCounts = Object.create(null) if (ParticleCollection.particleSystem !== null) { throw "Only one singleton ParticleCollection may exist"; } xList = []; yList = []; pixelPosList = []; wList = []; hList = []; pixelSizeList = []; shapeTypeList = []; outlineColorList = []; outlineThicknessList = []; drawOutlineList = []; fillColorList = []; drawFillList = []; particleCount = 0; } ParticleCollection.prototype.getTypeCount = function(typeOrTypeName) { let typeName; if (typeof typeOrTypeName === "string" || typeOrTypeName instanceof String) { typeName = typeOrTypeName; } else if (typeOrTypeName instanceof ParticleType) { typeName = typeOrTypeName.getName(); } else { debug.error(`The parameter was not a string or ParticleType`); return 0; } if (typeName in this.typeCounts) { return this.typeCounts[typeName]; } else { return 0; } }; ParticleCollection.prototype.disableWallEnergy = function() { this.hasWallEnergy = false; }; ParticleCollection.prototype.setWallEnergy = function(energy) { this.wallEnergy = energy; this.hasWallEnergy = true; }; ParticleCollection.particleSystem = null; ParticleCollection.getCollection = function() { if (ParticleCollection.particleSystem === null) { ParticleCollection.particleSystem = new ParticleCollection(); } return ParticleCollection.particleSystem; }; ParticleCollection.prototype.registerTypeChange = function(fromType, toType) { let fromTypeName = fromType.getName(); let toTypeName = toType.getName(); if (fromTypeName in this.typeCounts) { this.typeCounts[fromTypeName] -= 1; } else { debug.error(`Type "${typeName}" does not exist! Particle count display may be inaccurate`); } if (!(toTypeName in this.typeCounts)) { this.typeCounts[toTypeName] = 0; } this.typeCounts[toTypeName] += 1; }; ParticleCollection.prototype.pruneDeletedElements = function() { for (let i = 0; i < this.__allElements.length; i++) { while (i < this.__allElements.length && this.__allElements[i].isRegisteredForDeletion) { let replacementElement = this.__allElements.pop(); let typeName = this.__allElements[i].type.getName(); if (typeName in this.typeCounts) { this.typeCounts[typeName] -= 1; } else { debug.error(`Type "${typeName}" does not exist! Particle count display may be inaccurate`); } if (i < this.__allElements.length) { this.__allElements[i] = replacementElement; } } } }; ParticleCollection.prototype.elementsTouching = function(x, y, radius, output=[]) { output.length = 0; // clear array // Naively let idx = 0; for (let i = 0; i < this.__allElements.length; i++) { let c = this.__allElements[i]; let p = c.pos; if ((p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) <= (radius + c.radius) * (radius + c.radius)) { output[idx] = c; idx += 1 } } output.length = idx; return output; }; ParticleCollection.prototype.isPositionFree = function(x, y, radius) { // Naively for (let i = 0; i < this.__allElements.length; i++) { let c = this.__allElements[i]; let p = c.pos; if ((p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) <= (radius + c.radius) * (radius + c.radius)) { return false; } } return true; } ParticleCollection.prototype.addParticle = function(particle) { this.__allElements.push(particle); particle._owner = new WeakRef(this); let typeName = particle.type.getName(); if (!(typeName in this.typeCounts)) { this.typeCounts[typeName] = 0; } this.typeCounts[typeName] += 1; particleCount += 1; }; ParticleCollection.prototype.spawnFreeParticles = function(particleType, count) { for (let i = 0; i < count; i++) { let tries = 0; let p = new Particle(particleType); do { p.pos.x = randomRange(-width/2 + p.radius, width/2 - p.radius); p.pos.y = randomRange(-height/2 + p.radius, height/2 - p.radius); tries += 1; } while (tries < 100 && !this.isPositionFree(p.pos.x, p.pos.y, p.radius)) this.addParticle(p); } }; ParticleCollection.prototype.getAllParticles = function() { return this.__allElements; }; ParticleCollection.prototype.handleWallCollisions = function(particle) { if (particle.pos.x < particle.radius - width/2) { particle.pos.x = particle.radius - width / 2; if (particle.vel.x < 0) { particle.vel.x = -particle.vel.x * particle.restitution; if (this.hasWallEnergy) { particle.vel.x = (particle.vel.x + this.wallEnergy) / 2; } } } if (particle.pos.x > width/2 - particle.radius) { particle.pos.x = width/2 - particle.radius; if (particle.vel.x > 0) { particle.vel.x = -particle.vel.x * particle.restitution; if (this.hasWallEnergy) { particle.vel.x = (particle.vel.x - this.wallEnergy) / 2; } } } if (particle.pos.y < particle.radius - height/2) { particle.pos.y = particle.radius - height / 2; if (particle.vel.y < 0) { particle.vel.y = -particle.vel.y * particle.restitution; if (this.hasWallEnergy) { particle.vel.y = (particle.vel.y + this.wallEnergy) / 2; } } } if (particle.pos.y > height/2 - particle.radius) { particle.pos.y = height/2 - particle.radius; if (particle.vel.y > 0) { particle.vel.y = -particle.vel.y * particle.restitution; if (this.hasWallEnergy) { particle.vel.y = (particle.vel.y - this.wallEnergy) / 2; } } } } ParticleCollection.prototype.onTimeStep = function(deltaT) { this.pruneDeletedElements(); let particles = this.getAllParticles(); if (this.index >= particles.length) { this.index = 0; } if (particles.length > 0) { particles[this.index].angle %= 2 * Math.PI; this.index += 1; } for (let i = 0; i < particles.length; i++) { particles[i]._handleCollisions(); } this.gravity.scale(deltaT); for (let i = 0; i < particles.length; i++) { particles[i].applyImpulse(this.gravity.scale(particles[i].mass)); this.gravity.scale(particles[i].invmass); particles[i]._timeStep(deltaT); this.handleWallCollisions(particles[i]); particles[i].render(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, i); } this.gravity.scale(1/deltaT); }; ParticleCollection.prototype.clear = function() { this.__allElements.length = 0; this.typeCounts = Object.create(null); }; ParticleCollection.prototype.render = function() { let particles = this.getAllParticles(); particleCount = particles.length; xList.length = particleCount; yList.length = particleCount; pixelPosList.length = particleCount; wList.length = particleCount; hList.length = particleCount; pixelSizeList.length = particleCount; shapeTypeList.length = particleCount; outlineColorList.length = particleCount; outlineThicknessList.length = particleCount; drawOutlineList.length = particleCount; fillColorList.length = particleCount; drawFillList.length = particleCount; for (let i = 0; i < particles.length; i++) { particles[i].render(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, i); } }; function Particle(type, x, y) { this.isRegisteredForDeletion = false; this.type = null; this.changeType(type); this.pos = new Vector(x, y); this.vel = new Vector(); this.angle = 0; this.angleV = 0; this._deltaAngleV = 0; this._deltaV = new Vector(); this._owner = null; // just to give a distinction to easily decide which particle gets to handle each pair of collisions this._collisionHandlePriority = Math.random(); }; Particle.normal = new Vector(); Particle.forceBuilder = new Vector(); Particle.prototype.changeType = function(type) { let fromType = this.type; if (typeof type === "string" || type instanceof String) { this.type = ParticleType.get(type); if (this.type === null) { throw TypeError("type did not correspond to an existing particle type"); } } else if (type instanceof ParticleType) { this.type = type; } else { throw TypeError("type was not a ParticleType or a type name"); } let owner if (fromType !== null && this._owner !== null && (owner = this._owner.deref()) !== undefined) { owner.registerTypeChange(fromType, this.type); } this.radius = this.type.getRadius(); this.density = this.type.getDensity(); this.restitution = this.type.getRestitution(); this.friction = this.type.getFriction(); this.linearDamping = this.type.getLinearDamping(); this.angularDamping = this.type.getAngularDamping(); if (this.density === 0) { this.mass = 0; this.invmass = 0; this.massMomentOfInertia = 0; this.invmmi = 0; } else { this.mass = this.density * Math.PI * this.radius * this.radius; this.invmass = 1.0 / this.mass; this.massMomentOfInertia = (this.radius * this.radius * this.mass) * 0.5; this.invmmi = 1/this.massMomentOfInertia; } }; Particle.prototype.destroy = function () { this.isRegisteredForDeletion = true; }; Particle.prototype.applyImpulse = function (impulse, offset = null) { // Applies an impulse at an offset relative to the center of the particle this._deltaV.addScaled(impulse, this.invmass); if (offset !== null) { this.applyMoment(offset.crossZ(impulse)); } }; Particle.prototype.applyMoment = function (moment) { this._deltaAngleV += moment * this.invmmi; }; Particle.prototype._handleCollisions = function (particleCollection=null) { if (this.isRegisteredForDeletion) { return; } if (particleCollection === null) { particleCollection = this._owner.deref(); } if (particleCollection !== undefined) { let toCollideWith = particleCollection.elementsTouching(this.pos.x, this.pos.y, this.radius); for (let i = 0; i < toCollideWith.length; i++) { let other = toCollideWith[i]; // The first condition excludes collisions with self if (this === other) continue; // The second condition ensures that the types' collision handlers are called in a consistent order if (this.type.getName() < other.type.getName()) continue; // The third condition just makes sure that only one particle will handle collision in each pair if (this.type.getName() === other.type.getName() && this._collisionHandlePriority < other._collisionHandlePriority) continue; // The fourth option is to skip processing deleted particles if (other.isRegisteredForDeletion) continue; let willIgnoreCollision = this._runCollisionHandlersWith(other); if (!willIgnoreCollision) { if (this.pos.x === other.pos.x && this.pos.y === other.pos.y) { this.pos.addScaled(this.vel, -1/this.vel.mag()); } this._resolveCollisionWith(other); } } } }; Particle.prototype._runCollisionHandlersWith = function (particle) { let willIgnoreCollision = false; if (particle.type.getName() in this.type.collisionHandlers) { let handlers = this.type.collisionHandlers[particle.type.getName()], len = handlers.length; for (let i = 0; i < len; i++) { if (handlers[i].call(this, particle)) { willIgnoreCollision = true; } } } if (this.type.getName() in particle.type.collisionHandlers) { let handlers = particle.type.collisionHandlers[this.type.getName()], len = handlers.length; for (let i = 0; i < len; i++) { if (handlers[i].call(particle, this)) { willIgnoreCollision = true; } } } return willIgnoreCollision; } Particle.prototype._resolveCollisionWith = function (particle) { Particle.normal.set(particle.pos).sub(this.pos); let penetration = this.radius + particle.radius - Particle.normal.mag(); Particle.normal.normalize(); Particle.forceBuilder.set(particle.vel).sub(this.vel); // Start with the velocity of particle relative to self let velApart = Particle.normal.dot(Particle.forceBuilder); Particle.forceBuilder.addScaled(Particle.normal, velApart); // It's now the tangental velocity this._separateIfOverlapping(particle, penetration); if (velApart > 0) return; let coefficientOfRestitution = (Math.min(this.restitution, particle.restitution)); // weird multiplication for some reason let friction = Math.max(this.friction, particle.friction); Particle.forceBuilder.scale(friction); Particle.forceBuilder.addScaled(Particle.normal, coefficientOfRestitution * velApart); Particle.forceBuilder.scale(Math.min(this.mass, particle.mass)); this.applyImpulse(Particle.forceBuilder, Particle.normal.scale(this.radius)); Particle.normal.scale(1/this.radius); Particle.forceBuilder.scale(-1); particle.applyImpulse(Particle.forceBuilder, Particle.normal.scale(-particle.radius)); }; Particle.prototype._separateIfOverlapping = function (particle, penetration) { const separateAmount = 0.9; const activationAmount = 0.01; if (penetration > activationAmount) { let massRatio = particle.mass / (this.mass + particle.mass); this.pos.addScaled(Particle.normal, -penetration * separateAmount * massRatio); particle.pos.addScaled(Particle.normal, penetration * separateAmount * (1 - massRatio)); } }; Particle.prototype._timeStep = function (deltaT) { this.vel.add(this._deltaV); this.angleV += this._deltaAngleV; this._deltaV.set(); this._deltaAngleV = 0; this.pos.addScaled(this.vel, deltaT); this.angle += this.angleV * deltaT; this.vel.scale(Math.pow(1.0 - this.linearDamping, deltaT)); this.angleV *= Math.pow(1.0 - this.angularDamping, deltaT); }; Particle.prototype.render = function(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, index=-1) { if (index === -1) { index = xList.length; } xList[index] = this.pos.x; yList[index] = this.pos.y; vxList[index] = this.vel.x * 0.25; // lookang velocity vyList[index] = this.vel.y * 0.25; // lookang velocity dvxList[index] = this._deltaV.x; dvyList[index] = this._deltaV.y; pixelPosList[index] = true; wList[index] = this.radius * 2; hList[index] = this.radius * 2; pixelSizeList[index] = true; shapeTypeList[index] = "ELLIPSE"; outlineColorList[index] = this.type.getOutlineColor(); outlineThicknessList[index] = this.type.getOutlineThickness(); drawOutlineList[index] = this.type.getIsOutlineVisible(); fillColorList[index] = this.type.getFillColor(); drawFillList[index] = this.type.getIsFillVisible(); }; ]]> LIBRARY_EDITOR updated PArtSim true false = 1 && vector instanceof Vector)) { vector = new Vector(); } return vector.set(this).normalize(); }; Vector.prototype.rotate = function(radians) { let c = Math.cos(radians), s = Math.sin(radians); let x = this.x; this.x = c * this.x - s * this.y; this.y = s * x + c * this.y; return this; }; Vector.prototype.addScaled = function(vector, scalar) { if (!("x" in vector && "y" in vector)) { throw new TypeError(`x and y are not members of vector`); } this.x += vector.x * scalar; this.y += vector.y * scalar; }; function randomRange(min, max) { return min + (max - min) * Math.random(); } function checkIfIsValidCssColor(color) { return CSS.supports("color", color); }; function ParticleType(name) { if (name in ParticleType.types) { debug.warn(`Particle type "${name}" already exists! Overriding`); } this.name = name; this.minRadius = 16; this.maxRadius = 16; this.minDensity = 1; this.maxDensity = 1; this.isOutlineVisible = true; this.outlineColor = "black"; this.outlineThickness = 1; this.isFillVisible = true; this.fillColor = "white"; this.minRestitution = 0.5; this.maxRestitution = 0.5; this.minFriction = 0; this.maxFriction = 0; this.minLinearDamping = 0; this.maxLinearDamping = 0; this.minAngularDamping = 0; this.maxAngularDamping = 0; this.collisionHandlers = Object.create(null); ParticleType.types[name] = this; }; ParticleType.types = Object.create(null); ParticleType.get = function(name) { if (name in ParticleType.types) { return ParticleType.types[name]; } else { return null; } } ParticleType.prototype.getName = function () { return this.name; }; ParticleType.prototype.setRadius = function (value) { if (value < 0) { debug.warn(`setRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.minRadius = value; this.maxRadius = value; }; ParticleType.prototype.getRadius = function () { return randomRange(this.minRadius, this.maxRadius); }; ParticleType.prototype.setMinRadius = function (value) { if (value < 0) { debug.warn(`setMinRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.minRadius = value; if (this.maxRadius < this.minRadius) { debug.info(`Setting minimum radius to ${value}, which is larger than the type's current maximum radius (${this.maxRadius})`); this.maxRadius = this.minRadius; } }; ParticleType.prototype.getMinRadius = function () { return this.minRadius; }; ParticleType.prototype.setMaxRadius = function (value) { if (value < 0) { debug.warn(`setMaxRadius called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxRadius = value; if (this.minRadius > this.maxRadius) { debug.info(`Setting maximum radius to ${value}, which is smaller than the type's current minimum radius (${this.minRadius})`); this.minRadius = this.maxRadius; } }; ParticleType.prototype.getMaxRadius = function () { return this.maxRadius; }; ParticleType.prototype.setDensity = function (value) { if (value < 0) { debug.warn(`setDensity called with parameter value=${value}, which is less than 0!`); value = 0; } this.minDensity = value; this.maxDensity = value; }; ParticleType.prototype.getDensity = function () { return randomRange(this.minDensity, this.maxDensity); }; ParticleType.prototype.setMinDensity = function (value) { if (value < 0) { debug.warn(`setMinDensity called with parameter value=${value}, which is less than 0!`); value = 0; } if (value === 0) { debug.info('Setting minimum density to 0. Objects with density 0 behave as if they had infinite mass'); } this.minDensity = value; if (this.maxDensity < this.minDensity) { debug.info(`Setting minimum density to ${value}, which is larger than the type's current maximum density (${this.maxDensity})`); this.maxDensity = this.minDensity; } }; ParticleType.prototype.getMinDensity = function () { return this.minDensity; }; ParticleType.prototype.setMaxDensity = function (value) { if (value < 0) { debug.warn(`setMaxDensity called with parameter value=${value}, which is less than 0!`); value = 0; } if (value === 0) { debug.info("Setting maximum density to 0. All particles of this type will not be affected by collisions"); } this.maxDensity = value; if (this.minDensity > this.maxDensity) { this.minDensity = this.maxDensity; } }; ParticleType.prototype.getMaxDensity = function () { return this.maxDensity; }; ParticleType.prototype.setIsOutlineVisible = function (value) { if (value !== true && value !== false) { debug.info(`setIsOutlineVisible called with parameter value=${value}, which is not a boolean. This value is interpreted by JavaScript as a ${value ? "truthy" : "falsy"} value.`) } this.isOutlineVisible = value; }; ParticleType.prototype.getIsOutlineVisible = function () { return this.isOutlineVisible; }; ParticleType.prototype.setOutlineColor = function (value) { if (checkIfIsValidCssColor(value)) { this.outlineColor = value; } else { debug.warn(`setOutlineColor called with parameter value=${value}, which is not a valid color! Did nothing instead`); } }; ParticleType.prototype.getOutlineColor = function () { return this.outlineColor; }; ParticleType.prototype.setOutlineThickness = function (value) { if (value < 0) { debug.warn(`setOutlineThickness called with parameter value=${value}, which is less than 0!`); value = 0; } this.outlineThickness = value; }; ParticleType.prototype.getOutlineThickness = function () { return this.outlineThickness; }; ParticleType.prototype.setIsFillVisible = function (value) { if (value !== true && value !== false) { debug.info(`setIsFillVisible called with parameter value=${value}, which is not a boolean. This value is interpreted by JavaScript as a ${value ? "truthy" : "falsy"} value.`) } this.isFillVisible = value; }; ParticleType.prototype.getIsFillVisible = function () { return this.isFillVisible; }; ParticleType.prototype.setFillColor = function (value) { if (checkIfIsValidCssColor(value)) { this.fillColor = value; } else { debug.warn(`setFillColor called with parameter value=${value}, which is not a valid color! Did nothing instead`); } }; ParticleType.prototype.getFillColor = function () { return this.fillColor; }; ParticleType.prototype.setRestitution = function (value) { if (value < 0) { debug.warn(`setRestitution called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setRestitution called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minRestitution = value; this.maxRestitution = value; }; ParticleType.prototype.getRestitution = function () { return randomRange(this.minRestitution, this.maxRestitution); }; ParticleType.prototype.setMinRestitution = function (value) { if (value < 0) { debug.warn(`setMinRestitution called with parameter value=${value}, which is less than 0!`) value = 0; } if (value > 1) { debug.warn(`setMinRestitution called with parameter value=${value}, which is greater than 1!`) value = 1; } this.minRestitution = value; if (this.maxRestitution < this.minRestitution) { debug.info(`Setting minimum restitution to ${value}, which is greater than the type's current maximum restitution`) this.maxRestitution = this.minRestitution; } }; ParticleType.prototype.getMinRestitution = function () { return this.minRestitution; }; ParticleType.prototype.setMaxRestitution = function (value) { if (value < 0) { debug.warn(`setMaxRestitution called with parameter value=${value}, which is less than 0!`) value = 0; } if (value > 1) { debug.warn(`setMaxRestitution called with parameter value=${value}, which is greater than 1!`) value = 1; } this.maxRestitution = value; if (this.minRestitution > this.maxRestitution) { debug.info(`Setting maximum restitution to ${value}, which is less than the type's current minimum restitution`) this.minRestitution = this.maxRestitution; } }; ParticleType.prototype.getMaxRestitution = function () { return this.maxRestitution; }; ParticleType.prototype.setFriction = function (value) { if (value < 0) { debug.warn(`setFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minFriction = value; this.maxFriction = value; }; ParticleType.prototype.getFriction = function () { return randomRange(this.minFriction, this.maxFriction); }; ParticleType.prototype.setMinFriction = function (value) { if (value < 0) { debug.warn(`setMinFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setMinFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.minFriction = value; if (this.maxFriction < this.minFriction) { debug.info(`Setting minimum friction to ${value}, which is greater than the type's current maximum friction`); this.maxFriction = this.minFriction; } }; ParticleType.prototype.getMinFriction = function () { return this.minFriction; }; ParticleType.prototype.setMaxFriction = function (value) { if (value < 0) { debug.warn(`setMaxFriction called with parameter value=${value}, which is less than 0!`); value = 0; } if (value > 1) { debug.warn(`setMaxFriction called with parameter value=${value}, which is greater than 1!`); value = 1; } this.maxFriction = value; if (this.minFriction > this.maxFriction) { debug.info(`Setting maximum friction to ${value}, which is less than the type's current minimum friction`); this.minFriction = this.maxFriction; } }; ParticleType.prototype.getMaxFriction = function () { return this.maxFriction; }; ParticleType.prototype.setLinearDamping = function (value) { if (value < 0) { debug.warn(`setLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minLinearDamping = value; this.maxLinearDamping = value; }; ParticleType.prototype.getLinearDamping = function () { return randomRange(this.minLinearDamping, this.maxLinearDamping); }; ParticleType.prototype.setMinLinearDamping = function (value) { if (value < 0) { debug.warn(`setMinLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minLinearDamping = value; if (this.maxLinearDamping < this.minLinearDamping) { debug.info(`Setting minimum linear damping to ${value}, which is greater than the type's current maximum linear damping`); this.maxLinearDamping = this.minLinearDamping } }; ParticleType.prototype.getMinLinearDamping = function () { return this.minLinearDamping; }; ParticleType.prototype.setMaxLinearDamping = function (value) { if (value < 0) { debug.warn(`setMaxLinearDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxLinearDamping = value; if (this.minLinearDamping > this.maxLinearDamping) { debug.info(`Setting maximum linear damping to ${value}, which is less than the type's current minimum linear damping`); } }; ParticleType.prototype.getMaxLinearDamping = function () { return this.maxLinearDamping; }; ParticleType.prototype.setAngularDamping = function (value) { if (value < 0) { debug.warn(`setAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minAngularDamping = value; this.maxAngularDamping = value; }; ParticleType.prototype.getAngularDamping = function () { return randomRange(this.minAngularDamping, this.maxAngularDamping); }; ParticleType.prototype.setMinAngularDamping = function (value) { if (value < 0) { debug.warn(`setMinAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.minAngularDamping = value; if (this.maxAngularDamping < this.minAngularDamping) { debug.info(`Setting minimum angular damping to ${value}, which is greater than the type's current maximum angular damping`); } }; ParticleType.prototype.getMinAngularDamping = function () { return this.minAngularDamping; }; ParticleType.prototype.setMaxAngularDamping = function (value) { if (value < 0) { debug.warn(`setMaxAngularDamping called with parameter value=${value}, which is less than 0!`); value = 0; } this.maxAngularDamping = value; if (this.minAngularDamping > this.maxAngularDamping) { debug.info(`Setting maximum angular damping to ${value}, which is less than the type's current minimum angular damping`); } }; ParticleType.prototype.getMaxAngularDamping = function () { return this.maxAngularDamping; }; // Return truthy values in any callback to prevent collisions from having physics-based effects ParticleType.prototype.setCollisionHandler = function (name, callbackOrCallbackArray) { let callbackArray = callbackOrCallbackArray; if (typeof callbackArray === "function") { callbackArray = [callbackArray]; } this.collisionHandlers[name] = callbackArray; }; ParticleType.prototype.addCollisionHandler = function (name, callbackOrCallbackArray) { let callbackArray = callbackOrCallbackArray; if (!(name in this.collisionHandlers)) { this.collisionHandlers[name] = []; } if (typeof callbackArray === "function") { callbackArray = [callbackArray]; } this.collisionHandlers[name].push(...callbackArray); }; function ParticleCollection() { // For now, naively just throw all elements in an array this.__allElements = []; this.index = 0; this.gravity = new Vector(gravityX, gravityY); this.wallEnergy = 0; this.hasWallEnergy = false; this.typeCounts = Object.create(null) if (ParticleCollection.particleSystem !== null) { throw "Only one singleton ParticleCollection may exist"; } xList = []; yList = []; pixelPosList = []; wList = []; hList = []; pixelSizeList = []; shapeTypeList = []; outlineColorList = []; outlineThicknessList = []; drawOutlineList = []; fillColorList = []; drawFillList = []; particleCount = 0; } ParticleCollection.prototype.getTypeCount = function(typeOrTypeName) { let typeName; if (typeof typeOrTypeName === "string" || typeOrTypeName instanceof String) { typeName = typeOrTypeName; } else if (typeOrTypeName instanceof ParticleType) { typeName = typeOrTypeName.getName(); } else { debug.error(`The parameter was not a string or ParticleType`); return 0; } if (typeName in this.typeCounts) { return this.typeCounts[typeName]; } else { return 0; } }; ParticleCollection.prototype.disableWallEnergy = function() { this.hasWallEnergy = false; }; ParticleCollection.prototype.setWallEnergy = function(energy) { this.wallEnergy = energy; this.hasWallEnergy = true; }; ParticleCollection.particleSystem = null; ParticleCollection.getCollection = function() { if (ParticleCollection.particleSystem === null) { ParticleCollection.particleSystem = new ParticleCollection(); } return ParticleCollection.particleSystem; }; ParticleCollection.prototype.registerTypeChange = function(fromType, toType) { let fromTypeName = fromType.getName(); let toTypeName = toType.getName(); if (fromTypeName in this.typeCounts) { this.typeCounts[fromTypeName] -= 1; } else { debug.error(`Type "${typeName}" does not exist! Particle count display may be inaccurate`); } if (!(toTypeName in this.typeCounts)) { this.typeCounts[toTypeName] = 0; } this.typeCounts[toTypeName] += 1; }; ParticleCollection.prototype.pruneDeletedElements = function() { for (let i = 0; i < this.__allElements.length; i++) { while (i < this.__allElements.length && this.__allElements[i].isRegisteredForDeletion) { let replacementElement = this.__allElements.pop(); let typeName = this.__allElements[i].type.getName(); if (typeName in this.typeCounts) { this.typeCounts[typeName] -= 1; } else { debug.error(`Type "${typeName}" does not exist! Particle count display may be inaccurate`); } if (i < this.__allElements.length) { this.__allElements[i] = replacementElement; } } } }; ParticleCollection.prototype.elementsTouching = function(x, y, radius, output=[]) { output.length = 0; // clear array // Naively let idx = 0; for (let i = 0; i < this.__allElements.length; i++) { let c = this.__allElements[i]; let p = c.pos; if ((p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) <= (radius + c.radius) * (radius + c.radius)) { output[idx] = c; idx += 1 } } output.length = idx; return output; }; ParticleCollection.prototype.isPositionFree = function(x, y, radius) { // Naively for (let i = 0; i < this.__allElements.length; i++) { let c = this.__allElements[i]; let p = c.pos; if ((p.x - x) * (p.x - x) + (p.y - y) * (p.y - y) <= (radius + c.radius) * (radius + c.radius)) { return false; } } return true; } ParticleCollection.prototype.addParticle = function(particle) { this.__allElements.push(particle); particle._owner = new WeakRef(this); let typeName = particle.type.getName(); if (!(typeName in this.typeCounts)) { this.typeCounts[typeName] = 0; } this.typeCounts[typeName] += 1; particleCount += 1; }; ParticleCollection.prototype.spawnFreeParticles = function(particleType, count) { for (let i = 0; i < count; i++) { let tries = 0; let p = new Particle(particleType); do { p.pos.x = randomRange(-width/2 + p.radius, width/2 - p.radius); p.pos.y = randomRange(-height/2 + p.radius, height/2 - p.radius); tries += 1; } while (tries < 100 && !this.isPositionFree(p.pos.x, p.pos.y, p.radius)) this.addParticle(p); } }; ParticleCollection.prototype.getAllParticles = function() { return this.__allElements; }; ParticleCollection.prototype.handleWallCollisions = function(particle) { if (particle.pos.x < particle.radius - width/2) { particle.pos.x = particle.radius - width / 2; if (particle.vel.x < 0) { particle.vel.x = -particle.vel.x * particle.restitution; if (this.hasWallEnergy) { particle.vel.x = (particle.vel.x + this.wallEnergy) / 2; } } } if (particle.pos.x > width/2 - particle.radius) { particle.pos.x = width/2 - particle.radius; if (particle.vel.x > 0) { particle.vel.x = -particle.vel.x * particle.restitution; if (this.hasWallEnergy) { particle.vel.x = (particle.vel.x - this.wallEnergy) / 2; } } } if (particle.pos.y < particle.radius - height/2) { particle.pos.y = particle.radius - height / 2; if (particle.vel.y < 0) { particle.vel.y = -particle.vel.y * particle.restitution; if (this.hasWallEnergy) { particle.vel.y = (particle.vel.y + this.wallEnergy) / 2; } } } if (particle.pos.y > height/2 - particle.radius) { particle.pos.y = height/2 - particle.radius; if (particle.vel.y > 0) { particle.vel.y = -particle.vel.y * particle.restitution; if (this.hasWallEnergy) { particle.vel.y = (particle.vel.y - this.wallEnergy) / 2; } } } } ParticleCollection.prototype.onTimeStep = function(deltaT) { this.pruneDeletedElements(); let particles = this.getAllParticles(); if (this.index >= particles.length) { this.index = 0; } if (particles.length > 0) { particles[this.index].angle %= 2 * Math.PI; this.index += 1; } for (let i = 0; i < particles.length; i++) { particles[i]._handleCollisions(); } this.gravity.scale(deltaT); for (let i = 0; i < particles.length; i++) { particles[i].applyImpulse(this.gravity.scale(particles[i].mass)); this.gravity.scale(particles[i].invmass); particles[i]._timeStep(deltaT); this.handleWallCollisions(particles[i]); } this.render(); this.gravity.scale(1/deltaT); }; ParticleCollection.prototype.clear = function() { this.__allElements.length = 0; this.typeCounts = Object.create(null); }; ParticleCollection.prototype.render = function() { let particles = this.getAllParticles(); particleCount = particles.length; xList.length = particleCount; yList.length = particleCount; pixelPosList.length = particleCount; wList.length = particleCount; hList.length = particleCount; pixelSizeList.length = particleCount; shapeTypeList.length = particleCount; outlineColorList.length = particleCount; outlineThicknessList.length = particleCount; drawOutlineList.length = particleCount; fillColorList.length = particleCount; drawFillList.length = particleCount; for (let i = 0; i < particles.length; i++) { particles[i].render(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, i); } }; function Particle(type, x, y) { this.isRegisteredForDeletion = false; this.type = null; this.changeType(type); this.pos = new Vector(x, y); this.vel = new Vector(); this.angle = 0; this.angleV = 0; this._deltaAngleV = 0; this._deltaV = new Vector(); this._owner = null; // just to give a distinction to easily decide which particle gets to handle each pair of collisions this._collisionHandlePriority = Math.random(); }; Particle.normal = new Vector(); Particle.forceBuilder = new Vector(); Particle.prototype.changeType = function(type) { let fromType = this.type; if (typeof type === "string" || type instanceof String) { this.type = ParticleType.get(type); if (this.type === null) { throw TypeError("type did not correspond to an existing particle type"); } } else if (type instanceof ParticleType) { this.type = type; } else { throw TypeError("type was not a ParticleType or a type name"); } let owner if (fromType !== null && this._owner !== null && (owner = this._owner.deref()) !== undefined) { owner.registerTypeChange(fromType, this.type); } this.radius = this.type.getRadius(); this.density = this.type.getDensity(); this.restitution = this.type.getRestitution(); this.friction = this.type.getFriction(); this.linearDamping = this.type.getLinearDamping(); this.angularDamping = this.type.getAngularDamping(); if (this.density === 0) { this.mass = 0; this.invmass = 0; this.massMomentOfInertia = 0; this.invmmi = 0; } else { this.mass = this.density * Math.PI * this.radius * this.radius; this.invmass = 1.0 / this.mass; this.massMomentOfInertia = (this.radius * this.radius * this.mass) * 0.5; this.invmmi = 1/this.massMomentOfInertia; } }; Particle.prototype.destroy = function () { this.isRegisteredForDeletion = true; }; Particle.prototype.applyImpulse = function (impulse, offset = null) { // Applies an impulse at an offset relative to the center of the particle this._deltaV.addScaled(impulse, this.invmass); if (offset !== null) { this.applyMoment(offset.crossZ(impulse)); } }; Particle.prototype.applyMoment = function (moment) { this._deltaAngleV += moment * this.invmmi; }; Particle.prototype._handleCollisions = function (particleCollection=null) { if (this.isRegisteredForDeletion) { return; } if (particleCollection === null) { particleCollection = this._owner.deref(); } if (particleCollection !== undefined) { let toCollideWith = particleCollection.elementsTouching(this.pos.x, this.pos.y, this.radius); for (let i = 0; i < toCollideWith.length; i++) { let other = toCollideWith[i]; // The first condition excludes collisions with self if (this === other) continue; // The second condition ensures that the types' collision handlers are called in a consistent order if (this.type.getName() < other.type.getName()) continue; // The third condition just makes sure that only one particle will handle collision in each pair if (this.type.getName() === other.type.getName() && this._collisionHandlePriority < other._collisionHandlePriority) continue; // The fourth option is to skip processing deleted particles if (other.isRegisteredForDeletion) continue; let willIgnoreCollision = this._runCollisionHandlersWith(other); if (!willIgnoreCollision) { if (this.pos.x === other.pos.x && this.pos.y === other.pos.y) { this.pos.addScaled(this.vel, -1/this.vel.mag()); } this._resolveCollisionWith(other); } } } }; Particle.prototype._runCollisionHandlersWith = function (particle) { let willIgnoreCollision = false; if (particle.type.getName() in this.type.collisionHandlers) { let handlers = this.type.collisionHandlers[particle.type.getName()], len = handlers.length; for (let i = 0; i < len; i++) { if (handlers[i].call(this, particle)) { willIgnoreCollision = true; } } } if (this.type.getName() in particle.type.collisionHandlers) { let handlers = particle.type.collisionHandlers[this.type.getName()], len = handlers.length; for (let i = 0; i < len; i++) { if (handlers[i].call(particle, this)) { willIgnoreCollision = true; } } } return willIgnoreCollision; } Particle.prototype._resolveCollisionWith = function (particle) { Particle.normal.set(particle.pos).sub(this.pos); let penetration = this.radius + particle.radius - Particle.normal.mag(); Particle.normal.normalize(); Particle.forceBuilder.set(particle.vel).sub(this.vel); // Start with the velocity of particle relative to self let velApart = Particle.normal.dot(Particle.forceBuilder); Particle.forceBuilder.addScaled(Particle.normal, velApart); // It's now the tangental velocity this._separateIfOverlapping(particle, penetration); if (velApart > 0) return; let coefficientOfRestitution = Math.min(this.restitution, particle.restitution); let friction = Math.max(this.friction, particle.friction); Particle.forceBuilder.scale(friction); Particle.forceBuilder.addScaled(Particle.normal, coefficientOfRestitution * velApart); Particle.forceBuilder.scale(Math.min(this.mass, particle.mass)); this.applyImpulse(Particle.forceBuilder, Particle.normal.scale(this.radius)); Particle.normal.scale(1/this.radius); Particle.forceBuilder.scale(-1); particle.applyImpulse(Particle.forceBuilder, Particle.normal.scale(-particle.radius)); }; Particle.prototype._separateIfOverlapping = function (particle, penetration) { const separateAmount = 0.9; const activationAmount = 0.01; if (penetration > activationAmount) { let massRatio = particle.mass / (this.mass + particle.mass); this.pos.addScaled(Particle.normal, -penetration * separateAmount * massRatio); particle.pos.addScaled(Particle.normal, penetration * separateAmount * (1 - massRatio)); } }; Particle.prototype._timeStep = function (deltaT) { this.vel.add(this._deltaV); this.angleV += this._deltaAngleV; this._deltaV.set(); this._deltaAngleV = 0; this.pos.addScaled(this.vel, deltaT); this.angle += this.angleV * deltaT; this.vel.scale(Math.pow(1.0 - this.linearDamping, deltaT)); this.angleV *= Math.pow(1.0 - this.angularDamping, deltaT); }; Particle.prototype.render = function(xList, yList, pixelPosList, wList, hList, pixelSizeList, shapeTypeList, outlineColorList, outlineThicknessList, drawOutlineList, fillColorList, drawFillList, index=-1) { if (index === -1) { index = xList.length; } xList[index] = this.pos.x; yList[index] = this.pos.y; vxList[index] = this.vel.x * 0.25; // lookang velocity vyList[index] = this.vel.y * 0.25; // lookang velocity dvxList[index] = this._deltaV.x; dvyList[index] = this._deltaV.y; pixelPosList[index] = true; wList[index] = this.radius * 2; hList[index] = this.radius * 2; pixelSizeList[index] = true; shapeTypeList[index] = "ELLIPSE"; outlineColorList[index] = this.type.getOutlineColor(); outlineThicknessList[index] = this.type.getOutlineThickness(); drawOutlineList[index] = this.type.getIsOutlineVisible(); fillColorList[index] = this.type.getFillColor(); drawFillList[index] = this.type.getIsFillVisible(); }; ]]> LIBRARY_EDITOR initialize true false HTML_VIEW_EDITOR view1 true false true Elements.Panel true Elements.Panel true Elements.Panel Elements.CheckBox Elements.CheckBox Elements.ComboBox Elements.ComboBox Elements.ComboBox Elements.ComboBox true Elements.Panel Elements.CheckBox Elements.TwoStateButton Elements.Button Elements.Button Elements.Button true Elements.Panel true Elements.Panel true Elements.Panel Elements.Label Elements.TextField true Elements.Panel Elements.Label Elements.TextField true Elements.Panel Elements.Label Elements.TextField Elements.Panel true Elements.PlottingPanel Elements.ShapeSet2D true Elements.Panel Elements.HtmlArea This simulation illustrates the kinetic theory of particles and shows how it can be used to explain rates of reaction between 2 reactants.

Variables include

Reactant 1 in blue color

Temperature of surroundings

Reactant 2 in red color , its surface area depicted by square shape

Reactant 2 in red color, its amount, a little or a lot

The Product in green color (after 2 reactants collide) speed at which the product is produced is called the rate of reaction

]]>