• Jump To … +
    ./source/asset-management/image-asset.js ./source/asset-management/noise-asset.js ./source/asset-management/raw-asset.js ./source/asset-management/reaction-diffusion-asset.js ./source/asset-management/sprite-asset.js ./source/asset-management/video-asset.js ./source/core/animation-loop.js ./source/core/display-cycle.js ./source/core/document.js ./source/core/events.js ./source/core/init.js ./source/core/library.js ./source/core/snippets.js ./source/core/user-interaction.js ./source/factory/action.js ./source/factory/anchor.js ./source/factory/animation.js ./source/factory/bezier.js ./source/factory/block.js ./source/factory/button.js ./source/factory/canvas.js ./source/factory/cell.js ./source/factory/cog.js ./source/factory/color.js ./source/factory/conic-gradient.js ./source/factory/crescent.js ./source/factory/element.js ./source/factory/emitter.js ./source/factory/enhanced-label.js ./source/factory/filter.js ./source/factory/gradient.js ./source/factory/grid.js ./source/factory/group.js ./source/factory/label.js ./source/factory/line-spiral.js ./source/factory/line.js ./source/factory/loom.js ./source/factory/mesh.js ./source/factory/net.js ./source/factory/oval.js ./source/factory/particle-force.js ./source/factory/particle-spring.js ./source/factory/particle-world.js ./source/factory/particle.js ./source/factory/pattern.js ./source/factory/picture.js ./source/factory/polygon.js ./source/factory/polyline.js ./source/factory/quadratic.js ./source/factory/radial-gradient.js ./source/factory/rectangle.js ./source/factory/render-animation.js ./source/factory/shape.js ./source/factory/spiral.js ./source/factory/stack.js ./source/factory/star.js ./source/factory/tetragon.js ./source/factory/ticker.js ./source/factory/tracer.js ./source/factory/tween.js ./source/factory/unstacked-element.js ./source/factory/wheel.js ./source/helper/array-pool.js ./source/helper/color-engine.js ./source/helper/document-root-elements.js ./source/helper/filter-engine-bluenoise-data.js ./source/helper/filter-engine.js ./source/helper/random-seed.js ./source/helper/shape-path-calculation.js ./source/helper/shared-vars.js ./source/helper/system-flags.js ./source/helper/utilities.js ./source/helper/workstore.js ./source/mixin/anchor.js ./source/mixin/asset-advanced-functionality.js ./source/mixin/asset-consumer.js ./source/mixin/asset.js ./source/mixin/base.js ./source/mixin/button.js ./source/mixin/cascade.js ./source/mixin/cell-key-functions.js ./source/mixin/delta.js ./source/mixin/display-shape.js ./source/mixin/dom.js ./source/mixin/entity.js ./source/mixin/filter.js ./source/mixin/hidden-dom-elements.js ./source/mixin/mimic.js ./source/mixin/path.js ./source/mixin/pattern.js ./source/mixin/pivot.js ./source/mixin/position.js ./source/mixin/shape-basic.js ./source/mixin/shape-curve.js ./source/mixin/styles.js ./source/mixin/text.js ./source/mixin/tween.js ./source/scrawl.js ./source/untracked-factory/cell-fragment.js ./source/untracked-factory/coordinate.js ./source/untracked-factory/drag-zone.js ./source/untracked-factory/keyboard-zone.js ./source/untracked-factory/observe-update.js ./source/untracked-factory/palette.js ./source/untracked-factory/particle-history.js ./source/untracked-factory/quaternion.js ./source/untracked-factory/state.js ./source/untracked-factory/text-style.js ./source/untracked-factory/vector.js
  • §

    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', 'load']);
  • §

    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 = (key && key.type === 'Force') ? key : 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; Emitter; Net.

    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.pop();
    
        v.isRunning = false;
        v.hasLifetime = false;
        v.distanceLimit = 0;
        v.killBeyondCanvas = false;
        v.isBeingDragged = false;
        v.dragOffset = null;
    
        v.position.zero();
        v.velocity.zero();
        v.load.zero();
        v.initialPosition.zero();
        v.history.length = 0;
        v.forces.length = 0;
    
        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);
        },
    };