UNPKG

scrawl-canvas

Version:

Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun

488 lines (326 loc) 17.5 kB
// # Particle factory // Particle objects represent a 3-dimensional coordinate and include a history of recent positions which we can use to determine how to display that particle on screen. // #### Imports import { constructors, force, spring, springnames } from '../core/library.js'; import { doCreate, mergeOver, pushUnique, λnull, Ωempty } from '../helper/utilities.js'; import { releaseParticleHistory, requestParticleHistory } from '../untracked-factory/particle-history.js'; import { makeVector, releaseVector, requestVector } from '../untracked-factory/vector.js'; // The Particle object uses the base mixin, thus it supports all the normal Scrawl-canvas functionality such as `get`, `set`, `setDelta`, `clone`, `kill`, etc. import baseMix from '../mixin/base.js'; // Shared constants import { _isArray, BLACK, EULER, PARTICLE, T_PARTICLE } from '../helper/shared-vars.js'; // Local constants (none defined) // #### Particle constructor const Particle = function (items = Ωempty) { this.makeName(items.name); this.register(); this.set(this.defs); this.initializePositions(); this.hasLifetime = false; this.distanceLimit = 0; this.killBeyondCanvas = false; this.isBeingDragged = false; this.dragOffset = null; this.set(items); return this; }; // #### Particle prototype const P = Particle.prototype = doCreate(); // Particles have their own section in the Scrawl-canvas library. They are not artefacts or assets. P.type = T_PARTICLE; P.lib = PARTICLE; P.isArtefact = false; P.isAsset = false; // #### Mixins baseMix(P); // #### Particle attributes const defaultAttributes = { // The __position__ attribute represents a particle's world coordinate, and is held in an `{x:value, y:value, z:value}` Vector object. The default values are `{x:0, y:0, z:0}`, placing the artifact at the Cell canvas's top-left corner. We can set the position using the __positionX__, __positionY__ and __positionZ__ pseudo-attributes. position: null, // __velocity__ - Vector object, generally used internally as part of the particle physics calculation. We can give a particle an initial velocity using the __velocityX__, __velocityY__ and __velocityZ__ pseudo-attributes. velocity: null, // __load__ - Vector object used internally as part of the particle physics calculation. Never attempt to amend this attribute as it gets reset to zero at the start of every Display cycle. load: null, // __history__ - Array used to hold ParticleHistory arrays, which in turn include data on the particles position at a given time, and the time remaining for that particle to live. The latest history arrays are added to the start of the array, with the oldest history arrays at the end of the array. history: null, // __historyLength__ - Number - we control how many ParticleHistory arrays the Particle will retain. historyLength: 1, // __engine__ - a String value naming the physics engine to be used to calculate this Particle's movement in response to all the forces applied to it. Scrawl-canvas comes with three in-built engines: //+ __'euler'__ - the simplest, quickest and least stable engine (default) //+ __'runge-kutta'__ - the most complex, slowest and most stable engine //+ __'improved-euler'__ - an engine that sits between the other two engines in terms of complexity, speed and stability. engine: EULER, // __forces__ - an Array to hold Force objects that will be applied to this Particle. forces: null, // __mass__ - a Number value representing the Particle's mass (in kg) - this value is used in the gravity force calculation. mass: 1, // __fill__ and __stroke__ - CSS color values which can be used to display the Particle during the animation. fill: BLACK, stroke: BLACK, }; P.defs = mergeOver(P.defs, defaultAttributes); // #### Packet management P.packetExclusionsByRegex = pushUnique(P.packetExclusionsByRegex, ['^(local|dirty|current)']); P.packetObjects = pushUnique(P.packetObjects, ['position', 'velocity', 'acceleration']); // #### Clone management // In general we don't need to create or clone Particles objects ourselves; their generation is managed behind the scenes by the physics-related entitys. // #### Kill management P.factoryKill = function () { this.history.forEach(h => releaseParticleHistory(h)); const deadSprings = []; let s; springnames.forEach(name => { s = spring[name]; if (s.particleFrom && s.particleFrom.name === this.name) deadSprings.push(s); else if (s.particleTo && s.particleTo.name === this.name) deadSprings.push(s); }); deadSprings.forEach(s => s.kill()); }; // #### Get, Set, deltaSet const G = P.getters, S = P.setters, D = P.deltaSetters; // __positionX__, __positionY__, __positionZ__ G.positionX = function () { return this.position.x; }; G.positionY = function () { return this.position.y; }; G.positionZ = function () { return this.position.z; }; // We return the __position__ value as an `[x, y, z]` Array rather than as an object G.position = function () { const s = this.position; return [s.x, s.y, s.z]; }; S.positionX = function (coord) { this.position.x = coord; }; S.positionY = function (coord) { this.position.y = coord; }; S.positionZ = function (coord) { this.position.z = coord; }; S.position = function (item) { this.position.set(item); }; D.positionX = function (coord) { this.position.x += coord; }; D.positionY = function (coord) { this.position.y += coord; }; D.positionZ = function (coord) { this.position.z += coord; }; D.position = λnull; // __velocity__, __velocityX__, __velocityY__, __velocityZ__ // + There should be no need to access/amend these values G.velocityX = function () { return this.velocity.x; }; G.velocityY = function () { return this.velocity.y; }; G.velocityZ = function () { return this.velocity.z; }; G.velocity = function () { const s = this.velocity; return [s.x, s.y, s.z]; }; S.velocityX = function (coord) { this.velocity.x = coord; }; S.velocityY = function (coord) { this.velocity.y = coord; }; S.velocityZ = function (coord) { this.velocity.z = coord; }; S.velocity = function (x, y, z) { this.velocity.set(x, y, z); }; D.velocityX = function (coord) { this.velocity.x += coord; }; D.velocityY = function (coord) { this.velocity.y += coord; }; D.velocityZ = function (coord) { this.velocity.z += coord; }; D.velocity = λnull; // __forces__ - generally no need to add forces to Particles ourselves as this is handled by the physics-based entitys S.forces = function (item) { if (item) { if (_isArray(item)) { this.forces.length = 0; this.forces = this.forces.concat(item); } else this.forces.push(item); } }; // Remove certain attributes from the set/deltaSet functionality S.load = λnull; S.history = λnull; D.load = λnull; // #### Prototype functions // `initializePositions` - internal function called by all particle factories // + Setup initial Arrays and Objects. P.initializePositions = function () { this.initialPosition = makeVector(); this.position = makeVector(); this.velocity = makeVector(); this.load = makeVector(); this.forces = []; this.history = []; // __isRunning__ - a Boolean flag used as part of internal Particle lifetime management this.isRunning = false; }; // `applyForces` - internal function used to calculate the particles's load vector // + Requires both a __world__ object and a __host__ (Cell) object as arguments P.applyForces = function (world, host) { this.load.zero(); let f; if (!this.isBeingDragged) { this.forces.forEach(key => { f = force[key]; if (f && f.action) f.action(this, world, host); }); } }; // `update` - internal function used to calculate the Particles's position vector from its load and velocity vectors // + Requires both a __tick__ Number (measured in seconds) and a __host__ (Cell) object as arguments P.update = function (tick, world) { if (this.isBeingDragged) this.position.setFromVector(this.isBeingDragged).vectorAdd(this.dragOffset); else particleEngines[this.engine].call(this, tick * world.tickMultiplier); }; // `manageHistory` - internal function. Every particle can retain a history of its previous time and position moments, held in a ParticleHistory Array. P.manageHistory = function (tick, host) { const {history, remainingTime, position, historyLength, hasLifetime, distanceLimit, initialPosition, killBeyondCanvas} = this; let addHistoryFlag = true, remaining = 0; // A particle can have a lifetime value - a float Number measured in seconds, stored in the `remainingTime` attribute. This is flagged for action in the `hasLifetime` attribute. The particle has, in effect, three states: // + ___alive___ - on each tick a ParticleHistory object will be generated and added to the particle's `history` attribute array; if this addition takes the history array over its permitted length (as detailed in the particle's `historyLength` attribute) then the oldest ParticleHistory object is removed from the history array // + ___dying___ - if the particle has existed for longer than its alotted time - as detailed in its `remainingTime` attribute - then it enters a post-life phase where history objects are no longer generated on each tick, but the oldest ParticleHistory object continues to be removed from the history array // + ___dead___ - when the particle has existed for longer than its alotted time, and its history array is finally empty, then its `isRunning` flag can be set to false. // // Particle lifetime values are set by the emitter when creating the particles, based on the emitter's `killAfterTime` and `killAfterTimeVariation` attributes if (hasLifetime) { remaining = remainingTime - tick; if (remaining <= 0) { const last = history.pop(); releaseParticleHistory(last); addHistoryFlag = false; if (!history.length) this.isRunning = false; } else this.remainingTime = remaining; } // A particle can be killed off under the following additional circumstances: // + If we set the emitter's `killBeyondCanvas` flag to `true` // + If we set a kill radius - a distance from the particle's initial position beyond which the particle will be removed - defined in the emitter's `killRadius` and `killRadiusVariation` attributes const oldest = history[history.length - 1]; if (oldest) { const [, oz, ox, oy] = oldest; if (killBeyondCanvas) { const w = host.element.width, h = host.element.height; if (ox < 0 || oy < 0 || ox > w || oy > h) { addHistoryFlag = false; this.isRunning = false; } } if (distanceLimit) { const test = requestVector(initialPosition); test.vectorSubtractArray([ox, oy, oz]); if (test.getMagnitude() > distanceLimit) { addHistoryFlag = false; this.isRunning = false; } releaseVector(test); } } // Generate a new ParticleHistory object, if required, and remove any old ParticleHistory object beyond the history array's permitted length (as defined in the emitter's `historyLength` attribute) if (addHistoryFlag) { const {x, y, z} = position; // We add a pooled particleHistory object const h = requestParticleHistory(); h[0] = remaining; h[1] = z; h[2] = x; h[3] = y; history.unshift(h); if (history.length > historyLength) { const old = history.splice(historyLength); // We only release the particleHistory objects when we've finished with them old.forEach(item => releaseParticleHistory(item)); } } }; // `run` - internal function. We define the triggers that will kill the particle at the same time as we start it running. This function should only be called by an physics entity (Emitter, Net, Tracer). Note that there is no equivalent `halt` function; instead, we set the particle's `isRunning` attribute to false to get it removed from the system. P.run = function (timeKill, radiusKill, killBeyondCanvas) { // We can kill a Particle if it has lasted longer than its alloted lifetime. Lifetime (if required) is assigned to the Particle by its entity when generated. this.hasLifetime = false; if (timeKill) { this.remainingTime = timeKill; this.hasLifetime = true; } // We can kill a Particle if it has passed a certain distance beyond its initial position. Kill radius value (if required) is assigned to the Particle by its entity when generated. this.distanceLimit = 0; if (radiusKill) { this.initialPosition.set(this.position); this.distanceLimit = radiusKill; } // We can kill a Particle if it has moved beyond the Cell's canvas's dimensions. This boolean is set on the Particle by its entity when generated. this.killBeyondCanvas = killBeyondCanvas; this.isRunning = true; }; // #### Factory // Scrawl-canvas does not expose the particle factory functions in the scrawl object. Instead, particles are consumed by the physics-based entitys: [Tracer](./tracer.html); [Emitter](./emitter.html); [Net](./net.html). export const makeParticle = function (items) { if (!items) return false; return new Particle(items); }; constructors.Particle = Particle; // #### Particle pool // An attempt to reuse Particle objects rather than constantly creating and deleting them const particlePool = []; // `exported function` - retrieve a Particle from the particle pool export const requestParticle = function (items) { if (!particlePool.length) particlePool.push(new Particle()); const v = particlePool.shift(); v.set(items); return v }; // `exported function` - return a Particle to the particle pool. Failing to return Particles to the pool may lead to more inefficient code and possible memory leaks. export const releaseParticle = function (item) { if (item && item.type === T_PARTICLE) { item.history.forEach(h => releaseParticleHistory(h)); item.history.length = 0; item.set(item.defs); particlePool.push(item); // Do not keep excessive numbers of under-utilised particle objects in the pool if (particlePool.length > 50) { const temp = [...particlePool]; particlePool.length = 0; temp.forEach(p => p.kill()); } } }; // #### Particle physics engines // These functions are called by the `update` function which assigns the Particle object as `this` as part of the call. The engines calculate particle acceleration and apply it to particle velocity and then, taking into account the time elapsed since the previous tick, particle position. const particleEngines = { // __euler__ - the simplest and quickest engine, and the least accurate 'euler': function (tick) { const {position, velocity, load, mass} = this; const acc = requestVector(), vel = requestVector(velocity); acc.setFromVector(load).scalarDivide(mass); vel.vectorAdd(acc.scalarMultiply(tick)); velocity.setFromVector(vel); position.vectorAdd(vel.scalarMultiply(tick)); releaseVector(acc, vel); }, // __improved-euler__ is more accurate than the euler engine, but takes longer to calculate 'improved-euler': function (tick) { const {position, velocity, load, mass} = this; const acc1 = requestVector(), acc2 = requestVector(), acc3 = requestVector(), vel = requestVector(velocity); acc1.setFromVector(load).scalarDivide(mass).scalarMultiply(tick); acc2.setFromVector(load).vectorAdd(acc1).scalarDivide(mass).scalarMultiply(tick); acc3.setFromVector(acc1).vectorAdd(acc2).scalarDivide(2); vel.vectorAdd(acc3); velocity.setFromVector(vel); position.vectorAdd(vel.scalarMultiply(tick)); releaseVector(acc1, acc2, acc3, vel); }, // __runge-kutta__ is very accurate, but also a lot more computationally expensive 'runge-kutta': function (tick) { const {position, velocity, load, mass} = this; const acc1 = requestVector(), acc2 = requestVector(), acc3 = requestVector(), acc4 = requestVector(), acc5 = requestVector(), vel = requestVector(velocity); acc1.setFromVector(load).scalarDivide(mass).scalarMultiply(tick).scalarDivide(2); acc2.setFromVector(load).vectorAdd(acc1).scalarDivide(mass).scalarMultiply(tick).scalarDivide(2); acc3.setFromVector(load).vectorAdd(acc2).scalarDivide(mass).scalarMultiply(tick).scalarDivide(2); acc4.setFromVector(load).vectorAdd(acc3).scalarDivide(mass).scalarMultiply(tick).scalarDivide(2); acc2.scalarMultiply(2); acc3.scalarMultiply(2); acc5.setFromVector(acc1).vectorAdd(acc2).vectorAdd(acc3).vectorAdd(acc4).scalarDivide(6); vel.vectorAdd(acc5); velocity.setFromVector(vel); position.vectorAdd(vel.scalarMultiply(tick)); releaseVector(acc1, acc2, acc3, acc4, acc5, vel); }, };