• Jump To … +
    ./source/core/animationloop.js ./source/core/component.js ./source/core/document.js ./source/core/events.js ./source/core/init.js ./source/core/library.js ./source/core/userInteraction.js ./source/core/utilities.js ./source/factory/action.js ./source/factory/anchor.js ./source/factory/animation.js ./source/factory/bezier.js ./source/factory/block.js ./source/factory/canvas.js ./source/factory/cell.js ./source/factory/cog.js ./source/factory/color.js ./source/factory/coordinate.js ./source/factory/element.js ./source/factory/emitter.js ./source/factory/filter.js ./source/factory/fontAttributes.js ./source/factory/gradient.js ./source/factory/grid.js ./source/factory/group.js ./source/factory/imageAsset.js ./source/factory/line.js ./source/factory/loom.js ./source/factory/mesh.js ./source/factory/net.js ./source/factory/noise.js ./source/factory/oval.js ./source/factory/palette.js ./source/factory/particle.js ./source/factory/particleForce.js ./source/factory/particleHistory.js ./source/factory/particleSpring.js ./source/factory/particleWorld.js ./source/factory/pattern.js ./source/factory/phrase.js ./source/factory/picture.js ./source/factory/polygon.js ./source/factory/polyline.js ./source/factory/quadratic.js ./source/factory/quaternion.js ./source/factory/radialGradient.js ./source/factory/rectangle.js ./source/factory/renderAnimation.js ./source/factory/shape.js ./source/factory/spiral.js ./source/factory/spriteAsset.js ./source/factory/stack.js ./source/factory/star.js ./source/factory/state.js ./source/factory/tetragon.js ./source/factory/ticker.js ./source/factory/tracer.js ./source/factory/tween.js ./source/factory/unstackedElement.js ./source/factory/vector.js ./source/factory/videoAsset.js ./source/factory/wheel.js ./source/mixin/anchor.js ./source/mixin/asset.js ./source/mixin/assetConsumer.js ./source/mixin/base.js ./source/mixin/cascade.js ./source/mixin/delta.js ./source/mixin/displayShape.js ./source/mixin/dom.js ./source/mixin/entity.js ./source/mixin/filter.js ./source/mixin/mimic.js ./source/mixin/path.js ./source/mixin/pattern.js ./source/mixin/pivot.js ./source/mixin/position.js ./source/mixin/shapeBasic.js ./source/mixin/shapeCurve.js ./source/mixin/shapePathCalculation.js ./source/mixin/styles.js ./source/mixin/tween.js ./source/worker/filter-string.js ./source/worker/filter.js
  • ¶

    Particle factory

    The Scrawl-canvas particle physics engine is a simple system designed to allow developers a way to add particle-based effects to their canvas animation scenes. The physics engine is built on top of the following components:

    • Particle objects, which represent a 3-dimensional coordinate - based on a Scrawl-canvas Vector object - and include a history of recent positions which we can use to determine how to display that particle on screen.
    • History arrays which can be pooled (reused) to cut down on Array creation and distruction during the animation.
    • Force objects which define the general and occasional forces to be applied to each particle in the system as the animation progresses - a gravity force object is pre-defined by Scrawl-canvas.
    • Spring objects used to define a constraint (connection) between two particles in a system.
    • World objects where we can store attributes and values used by various objects; these attributes can be set up so that they will be inherited by clones of the World object. We can also influence the speed of the physics animation here.

    We do not have to handle particle generation and manipulation ourselves. Instead, Scrawl-canvas gives us three dedicated entitys which we use to add particle animation effects to the canvas scene. These entitys are:

    • Tracer - this entity generates a single non-recycled (in other words: long lasting) particle with a history, which we can use to display trace effects in the animation.
    • Emitter - an entity which generates a stream of short-lived, recycled particles, each with its own history. Emitters are highly versatile entitys which can generate a wide range of effects.
    • Net - a (generally) larger entity which uses both forces and springs to manage the animation of its non-recycled particles. Note that other artefacts can use Net particles as a reference for their own positioning.

    Particle physics

    The Scrawl-canvas particle physics engine system is based on a fairly classical understanding of particle kinetics (applying forces and constraints to a small, spherical object in 3D space) and kinematics (the movement of the small object in response to the forces and constraints applied to it).

    Particles are represented in the system by a simple Scrawl-canvas object which wraps a set of Scrawl-canvas Vector objects describing the particle’s position, velocity and load. The object also includes a history array detailing the recent particle’s most recent movements, an array of Force objects, a mass attribute (for gravity calculations), and an engine attribute - particles can use different engines to calculate their movements, depending on the needs of the animation; more precise engines are slower, but more stable.

    • Note that all Particles are positioned using ___absolute__ (pixel-based) coordinates; they cannot be positioned relatively (using String% values), or by reference to an artefact or another Particle.
    • Entitys that use Particles for their display - Emitter, Net, Tracer - can, however, use all the normal entity positioning strategies.
    • All artefacts (including all entitys) can position themselves by reference to a named (non-recycled) Particle.

    The particle pool

    As part of a particle animation many thousands of Particle objects may need to be generated, used and then discarded. Scrawl-canvas attempts to make this more efficient by pooling particle objects so that they can be reused as the animation progresses.

  • ¶

    Demos:

    • particles-001 - Emitter entity, and Particle World, basic functionality
    • particles-002 - Emitter using artefacts
    • particles-003 - Position Emitter entity: start; pivot; mimic; path; mouse; drag-and-drop
    • particles-004 - Emit particles along the length of a path
    • particles-005 - Emit particles from inside an artefact’s area
    • particles-006 - Fixed number of particles in a field; preAction and postAction functionality
    • particles-007 - Particle Force objects: generation and functionality
    • particles-008 - Net entity: generation and basic functionality, including Spring objects
    • particles-009 - Net particles: drag-and-drop functionality
    • particles-010 - Net entity: using a shape path as a net template
    • particles-011 - Tracer entity: basic functionality
    • particles-012 - Use Net entity particles as reference coordinates for other artefacts
  • ¶

    Imports

    import { constructors, force, spring, springnames } from '../core/library.js';
    import { mergeOver, pushUnique, λnull } from '../core/utilities.js';
    
    import { requestParticleHistoryObject, releaseParticleHistoryObject } from './particleHistory.js';
    import { requestVector, releaseVector, makeVector } from './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';
  • ¶

    Particle constructor

    const Particle = function (items = {}) {
    
        this.makeName(items.name);
        this.register();
    
        this.set(this.defs);
        this.initializePositions();
        this.set(items);
    
        return this;
    };
  • ¶

    Particle prototype

    let P = Particle.prototype = Object.create(Object.prototype);
  • ¶

    Particles have their own section in the Scrawl-canvas library. They are not artefacts or assets.

    P.type = 'Particle';
    P.lib = 'particle';
    P.isArtefact = false;
    P.isAsset = false;
  • ¶

    Mixins

    P = baseMix(P);
  • ¶

    Particle attributes

    • Attributes defined in the base mixin: name.
    let 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 Cdell 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: '#000000',
        stroke: '#000000',
    };
    P.defs = mergeOver(P.defs, defaultAttributes);
  • ¶

    Packet management

    P.packetExclusions = pushUnique(P.packetExclusions, []);
    P.packetExclusionsByRegex = pushUnique(P.packetExclusionsByRegex, ['^(local|dirty|current)']);
    P.packetCoordinates = pushUnique(P.packetCoordinates, []);
    P.packetObjects = pushUnique(P.packetObjects, ['position', 'velocity', 'acceleration']);
    P.packetFunctions = pushUnique(P.packetFunctions, []);
  • ¶

    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 => releaseParticleHistoryObject(h));
    
        let deadSprings = [];
    
        springnames.forEach(name => {
    
            let 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

    let 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 () {
    
        let 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 () {
    
        let 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 (Array.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();
    
        if (!this.isBeingDragged) {
    
            this.forces.forEach(key => {
    
                let 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) {
    
        let {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) {
    
                let last = history.pop();
    
                releaseParticleHistoryObject(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
        let oldest = history[history.length - 1];
    
        if (oldest) {
    
            let [or, oz, ox, oy] = oldest;
    
            if (killBeyondCanvas) {
    
                let w = host.element.width,
                    h = host.element.height;
    
                if (ox < 0 || oy < 0 || ox > w || oy > h) {
    
                    addHistoryFlag = false;
                    this.isRunning = false;
                }
            }
    
            if (distanceLimit) {
    
                let 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) {
    
            let {x, y, z} = position;
    
            let h = requestParticleHistoryObject();
    
            h.push(remaining, z, x, y);
    
            history.unshift(h);
    
            if (history.length > historyLength) {
    
                let old = history.splice(historyLength);
    
                old.forEach(item => releaseParticleHistoryObject(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.

    const makeParticle = function (items) {
        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

    const requestParticle = function (items) {
    
        if (!particlePool.length) particlePool.push(new Particle());
    
        let 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.

    const releaseParticle = function (item) {
    
        if (item && item.type === 'Particle') {
    
            item.history.forEach(h => releaseParticleHistoryObject(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) {
    
                let temp = [].concat(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) {
    
            let {position, velocity, load, mass} = this;
    
            let 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) {
    
            let {position, velocity, load, mass} = this;
    
            let 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) {
    
            let {position, velocity, load, mass} = this;
    
            let 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);
        },
    };
  • ¶

    Exports

    export {
        makeParticle,
    
        requestParticle,
        releaseParticle,
    };