• 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
  • §

    Emitter factory

    Creates 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.

  • §

    Imports

    import { artefact, constructors, world } from '../core/library.js';
    
    import { correctForZero, doCreate, isa_fn, isa_obj, mergeOver, pushUnique, xta, λnull, Ωempty } from '../helper/utilities.js';
    
    import { currentGroup } from './canvas.js';
    
    import { releaseParticle, requestParticle } from './particle.js';
    
    import { releaseCell,  requestCell } from '../untracked-factory/cell-fragment.js';
    
    import { makeVector, releaseVector, requestVector } from '../untracked-factory/vector.js';
    
    import { releaseCoordinate, requestCoordinate } from '../untracked-factory/coordinate.js';
    
    import { makeColor } from './color.js';
    
    import baseMix from '../mixin/base.js';
    import entityMix from '../mixin/entity.js';
  • §

    Shared constants

    import { _abs, _floor, _isArray, _isFinite, _now, _piDouble, _random, _tick, BLACK, ENTITY, EULER, MOUSE, PARTICLE, T_WORLD } from '../helper/shared-vars.js';
  • §

    Local constants

    const T_EMITTER = 'Emitter',
        NEWEST = 'newest';
  • §

    Emitter constructor

    const Emitter = function (items = Ωempty) {
    
        this.makeName(items.name);
        this.register();
        this.initializePositions();
        this.set(this.defs);
  • §

    The entity has a hit zone which can be used for drag-and-drop, and other user interactions. Thus the onXYZ UI functions remain relevant.

        this.onEnter = λnull;
        this.onLeave = λnull;
        this.onDown = λnull;
        this.onUp = λnull;
  • §

    Each instantiated entity will include two color factories - one for creating random fillStyle color Strings for generated particles, the other for generating strokeStyle colors.

        this.fillColorFactory = makeColor({ name: `${this.name}-fillColorFactory`});
        this.strokeColorFactory = makeColor({ name: `${this.name}-strokeColorFactory`});
  • §

    The range attributes use Vector objects in which to hold their data.

        this.range = makeVector();
        this.rangeFrom = makeVector();
        this.minimumVelocity = makeVector();
  • §

    As part of its stamp functionality the Emitter entity will invoke three user-defined xyzAction functions. If none of these functions are supplied to the entity, then it will not display anything on the canvas.

        this.preAction = λnull;
        this.stampAction = λnull;
        this.postAction = λnull;
  • §

    Setup the particle store, including the arrays used for winnowing out and killing dead particles

        this.particleStore = [];
        this.deadParticles = [];
        this.liveParticles = [];
    
        this.forces = [];
        this.filters = [];
        this.currentFilters = [];
        this.dirtyFilters = false;
        this.dirtyFiltersCache = false;
        this.dirtyImageSubscribers = false;
    
        this.generatorChoke = 0;
        this.lastUpdated = 0;
    
        if (!items.group) items.group = currentGroup;
    
        this.set(items);
    
        return this;
    };
  • §

    Emitter prototype

    const P = Emitter.prototype = doCreate();
    P.type = T_EMITTER;
    P.lib = ENTITY;
    P.isArtefact = true;
    P.isAsset = false;
  • §

    Mixins

    baseMix(P);
    entityMix(P);
  • §

    Emitter attributes

    const defaultAttributes = {
  • §

    world - World object; can be set using the String name of a World object, or the World object itself.

        world: null,
  • §

    artefact - In theory, any Scrawl-canvas object whose isArtefact flag is set to true can be assigned to this attribute. However this has not been tested on non-entity artefacts. For now, stick to Scrawl-canvas entity objects.

    • Can be set using the String name of an artefact object, or the artefact object itself.
        artefact: null,
  • §

    range and rangeFrom - Vector objects with some convenience pseudo-attributes to make setting them a bit easier: rangeX, rangeY, rangeZ, rangeFromX, rangeFromY, rangeFromZ.

    • These attributes set each generated particle’s initial velocity; their values represent the distance travelled in the x, y and z directions, as measured in pixels-per-second.
    • The rangeFrom attributes (float Numbers that can be negative) the lowest value in that dimension that will be generated. This value is local to the particle thus negative values are to the left (x) or above (y) or behind (z) the particle’s initial position.
    • The range attributes (again, float Numbers that can be negative) are the maximum (or least maximum) random value which will be added to the rangeFrom value.
    • All particles are assigned a (constrained) random velocity in this manner when they are generated.
        range: null,
        rangeFrom: null,
  • §

    minimumVelocity - Vector object with some convenience pseudo-attributes to make setting it a bit easier: _minimumVelocityX, minimumVelocityY, minimumVelocityZ. Sometimes we want to make sure a given velocity has a bit of speed to it. This vector gets used to make sure velocities near zero get rejected

        minimumVelocity: null,
  • §

    generationRate - positive integer Number - Emitter entitys use ephemeral particles to produce their visual effects, generating a steady stream of particles over time and then killing them off in various ways. Attribute sets the maximum number of particles that the Emitter will generate every second.

        generationRate: 0,
  • §

    particleCount - positive integer Number - attribute sets the maximum number of particles that the Emitter will manage and display at any one time.

        particleCount: 0,
  • §

    generateAlongPath, generateInArea - Object-based flags (default: false) - to set the flags, assign an entity object to them

    • the default action is for the Emitter to generate its particles from a single coordinate which can be determined from the Emitter’s lockTo attribute - thus the coordinate can be the absolute/relative start coordinates, or a path/pivot/mimic reference entity, or a Net particle, or the mouse cursor.
    • If we set the generateAlongPath attribute to a path-based entity then the Emitter will use that path to set the initial coordinate for all its generated particles
    • If we set the generateInArea attribute to any entity then the Emitter will use that entity’s area to set the initial coordinate for all its generated particles
    • generateInArea takes precedence over generateAlongPath, which in turn takes precedence over the default coordinate behaviour
        generateAlongPath: null,
        generateInArea: null,
        generateFromExistingParticles: false,
        generateFromExistingParticleHistories: false,
        limitDirectionToAngleMultiples: 0,
  • §

    generationChoke - Number measuring milliseconds (default: 15) - because both generateAlongPath and generateInArea functionalities use a while loop, we need a way to break out of those loops should they fail to generate an acceptable coordinate within a given amount of time. This attribute sets the maximum time the entity will spend on generating semi-random coordinates during any one Display cycle loop.

        generationChoke: 15,
  • §

    Emitter entitys will continuously generate new particles (up to the limit set in the particleCount attribute). The killAfterTime, killRadius and killBeyondCanvas attributes set out the circumstances in which existing particles will be removed from the entity’s particleStore attribute

    • killAfterTime - a positive float Number - sets the maximum time (measured in seconds) that a particle will live before it is killed and removed. This time is set on particle generation and is not updatable. We can add some randomness to the time through the killAfterTimeVariation attribute.
    • killRadius - a positive float Number - sets the maximum distance (measurted in pixels) from its initial position that a particle can move. If it moves beyond that distance, it will be killed. Again, some variation can be introduced through the killRadiusVariation attribute.
    • killBeyondCanvas - a Boolean flag (default: false) - when set, any particle that moves beyond its host Cell’s canvas dimensions will be killed and removed.
        killAfterTime: 0,
        killAfterTimeVariation: 0,
    
        killRadius: 0,
        killRadiusVariation: 0,
    
        killBeyondCanvas: false,
  • §

    historyLength - positive integer Number - every Particle will keep a record of its recent state, in a set of ParticleHistory arrays stored in the Particle’s history Array. The Emitter entity will set the maximum permitted length of the history array whenever it generates a new Particle.

        historyLength: 1,
  • §

    Emitter entitys will, as part of the Display cycle, apply any force objects assigned to a Particle. The initial forces assigned to every new Particle will be in line with the Force objects included in the Emitter’s forces Array.

    • To set the Array, supply a new Array containing Force objects, and/or the name Strings of those Force objects, to the forces attribute.
        forces: null,
  • §

    mass, massVariation - positive float Number - the initial mass assigned to each Particle when it is generated.

    • The mass attribute is used by the pre-defined gravity Force
        mass: 1,
        massVariation: 0,
  • §

    Physics calculations are handled by the Emitter entity’s physics engine which must be a String value of either euler (the default engine), improved-euler or runge-kutta.

        engine: EULER,
  • §

    Note that the hitRadius attribute is tied directly to the width and height attributes (which are effectively meaningless for this entity)

    • This attribute is absolute - unlike other Scrawl-canvas radius attributes it cannot be set using a percentage String value
        hitRadius: 10,
  • §

    We can tell the entity to display its hit zone by setting the showHitRadius flag. The hit zone outline color attribute hitRadiusColor accepts any valid CSS color String value

        showHitRadius: false,
        hitRadiusColor: BLACK,
  • §

    resetAfterBlur - positive float Number (measuring seconds) - physics simulations can be brittle, particularly if they are forced to calculate Particle loads (accelerations), velocities and speeds over a large time step. Rather than manage that time step in cases where the user may neglect or navigate away from the browser tab containing the physics animation, Scrawl-canvas will stop, clear, and recreate the scene if the time it takes the user to return to (re-focus on) the web page is greater than the value set in this attribute.

        resetAfterBlur: 3,
  • §

    stampFirst - The order in which particles get stamped during each iteration of the Display cycle. Takes a string argument, the values of which can be: oldest (default), newest.

        stampFirst: 'oldest',
  • §
    Not defined in the defs object, but set up in the constructor and setters
  • §

    particleStore - an Array where all the Emitter’s current particles will be stored. To render the entity, we need to iterate through these particles and use them to repeatedly stamp the Emitter’s artefact - or perform equivalent <canvas> context engine instructions - onto the host Cell. These actions will be defined in the stampAction function.

  • §

    The user-defined stamp functions preAction, stampAction and postAction are invoked in turn one each tick of the Display cycle. By default these functions do nothing, meaning nothing gets drawn to the canvas

    • preAction and postAction - these functions receive a single argument, a Cell wrapper on which we can draw additional graphics (if needed) - see Demo Particles 006 for a working example
    • stampAction - define all major rendering actions in this function. The function receives the following arguments: (artefact, particle, host) - where artefact is the Emitter entity’s artefact object (if any has been defined/set); particle is the current Particle object whose history needs to be rendered onto the canvas; and host is the Cell wrapper on which we will draw our graphics
  • §

    fillColorFactory and strokeColorFactory - Color objects - there will never be a need to define these attributes as this is done as part of the factory’s object build functionality. Used to generate fill and stroke colors for each newly generated particle

    };
    P.defs = mergeOver(P.defs, defaultAttributes);
  • §

    Packet management

    P.packetExclusions = pushUnique(P.packetExclusions, ['forces', 'particleStore', 'deadParticles', 'liveParticles', 'fillColorFactory', 'strokeColorFactory']);
    P.packetObjects = pushUnique(P.packetObjects, ['world', 'artefact', 'generateInArea', 'generateAlongPath']);
    P.packetFunctions = pushUnique(P.packetFunctions, ['preAction', 'stampAction', 'postAction']);
    
    P.finalizePacketOut = function (copy, items) {
    
        const forces = items.forces || this.forces || false;
        if (forces) {
    
            const tempForces = [];
            forces.forEach(f => {
    
                if (f.substring) tempForces.push(f);
                else if (isa_obj(f) && f.name) tempForces.push(f.name);
            });
            copy.forces = tempForces;
        }
    
        const tempParticles = [];
        this.particleStore.forEach(p => tempParticles.push(p.saveAsPacket()));
        copy.particleStore = tempParticles;
    
        return copy;
    };
  • §

    Clone management

    P.postCloneAction = function(clone) {
    
        return clone;
    };
  • §

    Kill management

    P.factoryKill = function (killArtefact, killWorld) {
    
        this.isRunning = false;
    
        if (killArtefact) this.artefact.kill();
    
        if (killWorld) this.world.kill();
    
        this.fillColorFactory.kill();
        this.strokeColorFactory.kill();
    
        this.deadParticles.forEach(p => p.kill());
        this.liveParticles.forEach(p => p.kill());
        this.particleStore.forEach(p => p.kill());
    };
  • §

    Get, Set, deltaSet

    const S = P.setters,
        D = P.deltaSetters;
    
    S.rangeX = function (val) { this.range.x = val; };
    S.rangeY = function (val) { this.range.y = val; };
    S.rangeZ = function (val) { this.range.z = val; };
    S.range = function (item) { this.range.set(item); };
    
    S.rangeFromX = function (val) { this.rangeFrom.x = val; };
    S.rangeFromY = function (val) { this.rangeFrom.y = val; };
    S.rangeFromZ = function (val) { this.rangeFrom.z = val; };
    S.rangeFrom = function (item) { this.rangeFrom.set(item); };
    
    S.minimumVelocityX = function (val) { this.minimumVelocity.x = val; };
    S.minimumVelocityY = function (val) { this.minimumVelocity.y = val; };
    S.minimumVelocityZ = function (val) { this.minimumVelocity.z = val; };
    S.minimumVelocity = function (item) { this.minimumVelocity.set(item); };
    
    S.preAction = function (item) {
    
        if (isa_fn(item)) {
    
            this.preAction = item;
            this.dirtyFilterIdentifier = true;
        }
    };
    S.stampAction = function (item) {
    
        if (isa_fn(item)) {
    
            this.stampAction = item;
            this.dirtyFilterIdentifier = true;
        }
    };
    S.postAction = function (item) {
    
        if (isa_fn(item)) {
    
            this.postAction = item;
            this.dirtyFilterIdentifier = true;
        }
    };
    
    
    S.world = function (item) {
    
        let w;
    
        if (item.substring) w = world[item];
        else if (isa_obj(item) && item.type === T_WORLD) w = item;
    
        if (w) {
    
            this.world = w;
        }
    };
    
    S.artefact = function (item) {
    
        let art;
    
        if (item.substring) art = artefact[item];
        else if (isa_obj(item) && item.isArtefact) art = item;
    
        if (art) {
    
            this.artefact = art;
            this.dirtyFilterIdentifier = true;
        }
    };
  • §

    To generate along a path, or in an area, we set the generateAlongPath or generateInArea attributes to the (path-based) artefact we shall be using for the template. This can be the artefact’s String name, or the artefact object itself

    S.generateAlongPath = function (item) {
    
        let art;
    
        if (item.substring) art = artefact[item];
        else if (isa_obj(item) && item.isArtefact) art = item;
    
        if (art && art.useAsPath) this.generateAlongPath = art;
        else this.generateAlongPath = false;
    
        this.dirtyFilterIdentifier = true;
    };
    
    S.generateInArea = function (item) {
    
        let art;
    
        if (item.substring) art = artefact[item];
        else if (isa_obj(item) && item.isArtefact) art = item;
    
        if (art) this.generateInArea = art;
        else this.generateInArea = false;
    
        this.dirtyFilterIdentifier = true;
    };
  • §

    Color management - we can set these attributes (fillColor fillMinimumColor fillMaximumColor, strokeColor strokeMinimumColor strokeMaximumColor) on the Emitter object - the setter functions pass the color value onto the appropriate color factory for processing and update

    S.fillColor = function (item) {
    
        this.fillColorFactory.set({color: item});
        this.dirtyFilterIdentifier = true;
    };
    S.fillMinimumColor = function (item) {
    
        this.fillColorFactory.set({minimumColor: item});
        this.dirtyFilterIdentifier = true;
    };
    S.fillMaximumColor = function (item) {
    
        this.fillColorFactory.set({maximumColor: item});
        this.dirtyFilterIdentifier = true;
    };
    
    S.strokeColor = function (item) {
    
        this.strokeColorFactory.set({color: item});
        this.dirtyFilterIdentifier = true;
    };
    S.strokeMinimumColor = function (item) {
    
        this.strokeColorFactory.set({minimumColor: item});
        this.dirtyFilterIdentifier = true;
    };
    S.strokeMaximumColor = function (item) {
    
        this.strokeColorFactory.set({maximumColor: item});
        this.dirtyFilterIdentifier = true;
    };
    
    S.hitRadius = function (item) {
    
        if (item.toFixed) {
    
            this.hitRadius = item;
            this.width = this.height = item * 2;
        }
    };
    D.hitRadius = function (item) {
    
        if (item.toFixed) {
    
            this.hitRadius += item;
            this.width = this.height = this.hitRadius * 2;
        }
    };
    S.width = function (item) {
    
        if (item.toFixed) {
    
            this.hitRadius = item / 2;
            this.width = this.height = item;
        }
    };
    D.width = function (item) {
    
        if (item.toFixed) {
    
            this.hitRadius = item / 2;
            this.width = this.height = item;
        }
    };
    S.height = S.width;
    D.height = D.width;
  • §

    Prototype functions

  • §

    prepareStamp - internal - overwrites the entity mixin function

    P.prepareStamp = function () {
    
        if (this.dirtyHost) {
    
            this.dirtyHost = false;
            this.dirtyDimensions = true;
        }
    
        if (this.dirtyScale || this.dirtyDimensions || this.dirtyStart || this.dirtyOffset || this.dirtyHandle) this.dirtyPathObject = true;
    
        if (this.dirtyScale) this.cleanScale();
    
        if (this.dirtyDimensions) this.cleanDimensions();
    
        if (this.dirtyLock) this.cleanLock();
    
        if (this.dirtyStart) this.cleanStart();
    
        if (this.dirtyOffset) this.cleanOffset();
    
        if (this.dirtyHandle) this.cleanHandle();
    
        if (this.dirtyRotation) this.cleanRotation();
    
        if (this.lockTo.includes(MOUSE) || this.lockTo.includes(PARTICLE)) {
    
            this.dirtyStampPositions = true;
            this.dirtyStampHandlePositions = true;
        }
    
        if (this.dirtyStampPositions) this.cleanStampPositions();
        if (this.dirtyStampHandlePositions) this.cleanStampHandlePositions();
  • §

    Functionality specific to Emitter entitys

        const now = _now();
    
        const {particleStore, deadParticles, liveParticles, particleCount, generationRate, resetAfterBlur} = this;
    
        let generatorChoke = this.generatorChoke;
  • §

    Create the generator choke, if necessary

        if (!generatorChoke) {
    
            this.generatorChoke = generatorChoke = now;
        }
  • §

    Check through particles, removing all particles that have completed their lives

        particleStore.forEach(p => {
    
            if (p.isRunning) liveParticles.push(p);
            else deadParticles.push(p);
        });
        particleStore.length = 0;
    
        deadParticles.forEach(d => releaseParticle(d));
        deadParticles.length = 0;
    
        particleStore.push(...liveParticles);
        liveParticles.length = 0;
  • §

    Determine how many new particles need to be generated

        let elapsed = now - generatorChoke;
  • §

    Need to prevent generation of new particles if the elapsed time is due to the user focussing on another tab in the browser before returning to the tab running this Scrawl-canvas animation

        if ((elapsed / 1000) > resetAfterBlur) {
    
            elapsed = 0;
            this.generatorChoke = now;
        }
    
        if (elapsed > 0 && generationRate) {
    
            let canGenerate = _floor((generationRate / 1000) * elapsed);
    
            if (particleCount) {
    
                const reqParticles = particleCount - particleStore.length;
    
                if (reqParticles <= 0) canGenerate = 0;
                else if (reqParticles < canGenerate) canGenerate = reqParticles;
            }
    
            if (canGenerate) {
    
                this.addParticles(canGenerate);
  • §

    We only update the choke value after particles have been generated

    • Ensures that if we only want 2 particles a second, our requirement will be respected
                this.generatorChoke = now;
            }
        }
  • §

    prepareStampTabsHelper is defined in the mixin/hidden-dom-elements.js file - handles updates to anchor and button objects

        this.prepareStampTabsHelper();
    };
  • §

    addParticles - internal function called by prepareStamp … if you are not a fan of overly-complex functions, look away now.

    We can add particles to an emitter in a number of different ways, determined by the setting of two flag attributes on the emitter. The flags are actioned in the following order:

    • generateInArea - when this flag attribute is set to an artefact object, the emitter will use that artefact’s outline to decide where new particles will be added to the scene
    • generateAlongPath - similarly, when this flag attribute is set to a shape-based entity (with its useAsPath attribute flag set to true), the emitter will use the path to dettermine wshere the new particle will be added.

    If neither of the above flags has been set, then the emitter will add particles from a single coordinate. This coordinate will be calculated according to the values set In the lockTo attribute (which can also be set using the lockXTo and lockYTo pseudo-attributes):

    • start - use the emitter entity’s start/handle/offset coordinates - which can be absolute px Number or relative % String values
    • pivot - use a pivot entity to calculate the emitter’s reference coordinate
    • mimic - use a mimic entity to calculate the emitter’s reference coordinate
    • path - use a Shape-based entity’s path to determine the emitter’s reference coordinate
    • mouse - use the mouse/touch/pointer cursor value as the emitter’s coordinate
    P.addParticles = function (req) {
  • §

    internal helper functions, used when creating the particle

        const calc = function (item, itemVar) {
            return correctForZero(item + ((_random() * itemVar * 2) - itemVar));
        };
    
        const velocityCalc = function (item, itemVar, min) {
  • §

    Fast path

            if (min <= 0) return correctForZero(item + (_random() * itemVar));
    
            let val = 0,
                attempts = 0,
                sign;
    
            do {
    
                val = correctForZero(item + (_random() * itemVar));
    
                attempts += 1;
  • §

    Fallback: guarantee at least |min|, randomize sign

                if (attempts > 20) {
    
                    sign = _random() < 0.5 ? -1 : 1;
                    return sign * min;
                }
            } while (_abs(val) < min);
    
            return val;
        };
    
        let i, p, cx, cy, timeKill, radiusKill;
    
        const timeChoke = _now();
  • §

    The emitter object retains details of the initial values required for eachg particle it generates

        const {historyLength, engine, forces, mass, massVariation, fillColorFactory, strokeColorFactory, range, rangeFrom, minimumVelocity, currentStampPosition, particleStore, killAfterTime, killAfterTimeVariation, killRadius, killRadiusVariation, killBeyondCanvas, currentRotation, generateAlongPath, generateInArea, generateFromExistingParticles, generateFromExistingParticleHistories, limitDirectionToAngleMultiples, generationChoke} = this;
    
        const {x, y, z} = range;
        const {x:fx, y:fy, z:fz} = rangeFrom;
  • §

    Use an artefact’s current area location to determine where the particle will be generated

        if (generateInArea) {
    
            const host = this.currentHost;
    
            if (host) {
    
                const hostCanvas = host.element;
    
                const {width, height} = hostCanvas;
    
                if (!generateInArea.pathObject || generateInArea.dirtyPathObject) generateInArea.cleanPathObject();
    
                const testCell = requestCell(),
                    testEngine = testCell.engine,
                    coord = requestCoordinate();
    
                const {pathObject, winding, currentStart} = generateInArea;
    
                [cx, cy] = currentStart;
    
                const test = (item) => testEngine.isPointInPath(pathObject, ...item, winding);
    
                testCell.rotateDestination(testEngine, cx, cy, generateInArea);
    
                GenerateInAreaLoops:
                for (i = 0; i < req; i++) {
    
                    let coordFlag = false;
    
                    while (!coordFlag) {
    
                        if (timeChoke + generationChoke < _now()) break GenerateInAreaLoops;
    
                        coord.set(_random() * width, _random() * height);
    
                        if (test(coord)) coordFlag = true;
                    }
    
                    p = requestParticle();
    
                    p.set({
                        positionX: coord[0],
                        positionY: coord[1],
                        positionZ: 0,
    
                        velocityX: velocityCalc(fx, x, minimumVelocity.x),
                        velocityY: velocityCalc(fy, y, minimumVelocity.y),
                        velocityZ: velocityCalc(fz, z, minimumVelocity.z),
    
                        historyLength,
                        engine,
                        forces,
  • §

    mass: calc(mass, massVariation),

                        mass: _abs(calc(mass, massVariation)) || 1e-6,
    
                        fill: fillColorFactory.getRangeColor(_random()),
                        stroke: strokeColorFactory.getRangeColor(_random()),
                    });
    
                    timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                    radiusKill = _abs(calc(killRadius, killRadiusVariation));
    
                    p.run(timeKill, radiusKill, killBeyondCanvas);
    
                    particleStore.push(p);
                }
                releaseCell(testCell);
                releaseCoordinate(coord);
            }
        }
  • §

    Use an Shape-based entity’s path to determine where the particle will be generated

        else if (generateAlongPath) {
    
            if (generateAlongPath.useAsPath) {
    
                if (!generateAlongPath.pathObject || generateAlongPath.dirtyPathObject) generateAlongPath.cleanPathObject();
    
                GenerateAlongPathLoops:
                for (i = 0; i < req; i++) {
    
                    let coord = false,
                        coordFlag = false;
    
                    while (!coordFlag) {
    
                        if (timeChoke + generationChoke < _now()) break GenerateAlongPathLoops;
    
                        coord = generateAlongPath.getPathPositionData(_random(), true);
    
                        if (coord) coordFlag = true;
                    }
    
                    p = requestParticle();
    
                    p.set({
                        positionX: coord.x,
                        positionY: coord.y,
                        positionZ: 0,
    
                        velocityX: velocityCalc(fx, x, minimumVelocity.x),
                        velocityY: velocityCalc(fy, y, minimumVelocity.y),
                        velocityZ: velocityCalc(fz, z, minimumVelocity.z),
    
                        historyLength,
                        engine,
                        forces,
    
                        mass: calc(mass, massVariation),
    
                        fill: fillColorFactory.getRangeColor(_random()),
                        stroke: strokeColorFactory.getRangeColor(_random()),
                    });
    
                    timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                    radiusKill = _abs(calc(killRadius, killRadiusVariation));
    
                    p.run(timeKill, radiusKill, killBeyondCanvas);
    
                    particleStore.push(p);
                }
            }
        }
  • §

    TODO: documentation

        else if (generateFromExistingParticleHistories) {
    
            const len = particleStore.length,
                res = requestVector();
    
            let r, parent, history, startval;
    
            for (i = 0; i < req; i++) {
    
                if (len) {
    
                    parent = particleStore[_floor(_random() * len)];
                    history = parent.history;
    
                    if (history && history.length > 1) {
    
                        [, , ...startval] = history[_floor(_random() * history.length)];
    
                        if (startval) res.setFromArray(startval);
                        else res.setFromVector(parent.position);
                    }
                    else res.setFromVector(parent.position);
                }
                else res.setFromArray(currentStampPosition);
    
                p = requestParticle();
    
                p.set({
                    positionX: correctForZero(res.x),
                    positionY: correctForZero(res.y),
                    positionZ: correctForZero(res.z),
    
                    historyLength,
                    engine,
                    forces,
    
                    mass: calc(mass, massVariation),
    
                    fill: fillColorFactory.getRangeColor(_random()),
                    stroke: strokeColorFactory.getRangeColor(_random()),
                });
    
                if (limitDirectionToAngleMultiples) {
    
                    res.zero();
                    r = _floor(360 / limitDirectionToAngleMultiples);
                    res.x = velocityCalc(fx, x, minimumVelocity.x);
                    res.rotate((_floor(_random() * r)) * limitDirectionToAngleMultiples);
    
                    p.set({
                        velocityX: correctForZero(res.x),
                        velocityY: correctForZero(res.y),
                        velocityZ: velocityCalc(fz, z, minimumVelocity.z),
                    });
                }
                else {
    
                    p.set({
                        velocityX: velocityCalc(fx, x, minimumVelocity.x),
                        velocityY: velocityCalc(fy, y, minimumVelocity.y),
                        velocityZ: velocityCalc(fz, z, minimumVelocity.z),
                    });
                }
                p.velocity.rotate(currentRotation);
    
                timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                radiusKill = _abs(calc(killRadius, killRadiusVariation));
    
                p.run(timeKill, radiusKill, killBeyondCanvas);
    
                particleStore.push(p);
            }
            releaseVector(res);
        }
  • §

    TODO: documentation

        else if (generateFromExistingParticles) {
    
            const len = particleStore.length,
                res = requestVector();
    
            let r, parent;
    
            for (i = 0; i < req; i++) {
    
                if (len) {
    
                    parent = particleStore[_floor(_random() * len)];
                    res.setFromVector(parent.position);
                }
                else res.setFromArray(currentStampPosition);
    
                p = requestParticle();
    
                p.set({
                    positionX: correctForZero(res.x),
                    positionY: correctForZero(res.y),
                    positionZ: correctForZero(res.z),
    
                    historyLength,
                    engine,
                    forces,
    
                    mass: calc(mass, massVariation),
    
                    fill: fillColorFactory.getRangeColor(_random()),
                    stroke: strokeColorFactory.getRangeColor(_random()),
                });
    
                if (limitDirectionToAngleMultiples) {
    
                    res.zero();
                    r = _floor(360 / limitDirectionToAngleMultiples);
                    res.x = velocityCalc(fx, x, minimumVelocity.x);
                    res.rotate((_floor(_random() * r)) * limitDirectionToAngleMultiples);
    
                    p.set({
                        velocityX: correctForZero(res.x),
                        velocityY: correctForZero(res.y),
                        velocityZ: velocityCalc(fz, z, minimumVelocity.z),
                    });
                }
                else {
    
                    p.set({
                        velocityX: velocityCalc(fx, x, minimumVelocity.x),
                        velocityY: velocityCalc(fy, y, minimumVelocity.y),
                        velocityZ: velocityCalc(fz, z, minimumVelocity.z),
                    });
                }
                p.velocity.rotate(currentRotation);
    
                timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                radiusKill = _abs(calc(killRadius, killRadiusVariation));
    
                p.run(timeKill, radiusKill, killBeyondCanvas);
    
                particleStore.push(p);
            }
            releaseVector(res);
        }
  • §

    Generate the particle using the emitter’s start coordinate, or a reference artifact’s coordinate

        else {
    
            [cx, cy] = currentStampPosition;
    
            for (i = 0; i < req; i++) {
    
                p = requestParticle();
    
                p.set({
                    positionX: cx,
                    positionY: cy,
                    positionZ: 0,
    
                    velocityX: velocityCalc(fx, x, minimumVelocity.x),
                    velocityY: velocityCalc(fy, y, minimumVelocity.y),
                    velocityZ: velocityCalc(fz, z, minimumVelocity.z),
    
                    historyLength,
                    engine,
                    forces,
    
                    mass: calc(mass, massVariation),
    
                    fill: fillColorFactory.getRangeColor(_random()),
                    stroke: strokeColorFactory.getRangeColor(_random()),
                });
    
                p.velocity.rotate(currentRotation);
    
                timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                radiusKill = _abs(calc(killRadius, killRadiusVariation));
    
                p.run(timeKill, radiusKill, killBeyondCanvas);
    
                particleStore.push(p);
            }
        }
    };
  • §

    regularStamp - overwriters the functionality defined in the entity.js mixin

    P.regularStamp = function () {
    
        const {world, artefact, particleStore, preAction, stampAction, postAction, lastUpdated, resetAfterBlur, showHitRadius, hitRadius, hitRadiusColor, currentStampPosition, stampFirst} = this;
    
        const host = this.currentHost;
    
        let deltaTime = _tick;
    
        const now = _now();
    
        if (lastUpdated) deltaTime = (now - lastUpdated) / 1000;
  • §

    If the user has focussed on another tab in the browser before returning to the tab running this Scrawl-canvas animation, then we risk breaking the page by continuing the animation with the existing particles - simplest solution is to remove all the particles and, in effect, restarting the emitter’s animation.

        if (deltaTime > resetAfterBlur) {
    
            particleStore.forEach(p => releaseParticle(p));
            particleStore.length = 0;
            deltaTime = _tick;
        }
    
        particleStore.forEach(p => p.applyForces(world, host));
        particleStore.forEach(p => p.update(deltaTime, world));
  • §

    Perform canvas drawing before the main (developer-defined) stampAction function

        preAction.call(this, host);
    
        if (NEWEST === stampFirst) {
    
            particleStore.toReversed().forEach(p => {
    
                p.manageHistory(deltaTime, host);
                stampAction.call(this, artefact, p, host);
            });
        }
        else {
    
            particleStore.forEach(p => {
    
                p.manageHistory(deltaTime, host);
                stampAction.call(this, artefact, p, host);
            });
        }
  • §

    Perform further canvas drawing after the main (developer-defined) stampAction function

        postAction.call(this, host);
    
        if (showHitRadius) {
    
            const engine = host.engine;
    
            engine.save();
            engine.lineWidth = 1;
            engine.strokeStyle = hitRadiusColor;
    
            engine.resetTransform();
            engine.beginPath();
            engine.arc(currentStampPosition[0], currentStampPosition[1], hitRadius, 0, _piDouble);
            engine.stroke();
    
            engine.restore();
        }
    
        this.lastUpdated = now;
    };
  • §

    checkHit - overwrites the function defined in mixin/position.js

    • The Emitter entity’s hit area is a circle centred on the entity’s rotation/reflection (start) position or, where the entity’s position is determined by reference (pivot, mimic, path, etc), the reference’s current position.
    • Emitter entitys can be dragged and dropped around a canvas display like any other Scrawl-canvas artefact.
    P.checkHit = function (items = []) {
    
        if (this.noUserInteraction) return false;
    
        const tests = (!_isArray(items)) ?  [items] : items;
        const currentStampPosition = this.currentStampPosition;
    
        let res = false,
            tx, ty;
    
        if (tests.some(test => {
    
            if (_isArray(test)) {
    
                tx = test[0];
                ty = test[1];
            }
            else if (xta(test, test.x, test.y)) {
    
                tx = test.x;
                ty = test.y;
            }
            else return false;
    
            if (!_isFinite(tx) || !_isFinite(ty)) return false;
    
            const v = requestVector(currentStampPosition).vectorSubtract(test);
    
            if (v.getMagnitude() < this.hitRadius) res = true;
    
            releaseVector(v);
    
            return res;
    
        }, this)) {
    
            return this.checkHitReturn(tx, ty);
        }
        return false;
    };
  • §

    Factory

    let myWorld = scrawl.makeWorld({
    
        name: 'demo-world',
        tickMultiplier: 2,
        userAttributes: [
            {
                key: 'particleColor',
                defaultValue: '#F0F8FF',
            },
            {
                key: 'alphaDecay',
                defaultValue: 6,
            },
        ],
    });
    
    scrawl.makeEmitter({
    
        name: 'use-raw-2d-context',
        world: myWorld,
        start: ['center', 'center'],
    
        generationRate: 60,
        killAfterTime: 5,
    
        historyLength: 50,
    
        rangeX: 40,
        rangeFromX: -20,
        rangeY: 40,
        rangeFromY: -20,
        rangeZ: -1,
        rangeFromZ: -0.2,
    
        stampAction: function (artefact, particle, host) {
    
            let engine = host.engine,
                history = particle.history,
                remaining, radius, alpha, x, y, z,
                endRad = Math.PI * 2;
    
            engine.save();
            engine.fillStyle = myWorld.get('particleColor');
            engine.beginPath();
            history.forEach((p, index) => {
                [remaining, z, x, y] = p;
                radius = 6 * (1 + (z / 3));
                alpha = remaining / myWorld.alphaDecay;
                if (radius > 0 && alpha > 0) {
                    engine.moveTo(x, y);
                    engine.arc(x, y, radius, 0, endRad);
                }
            });
            engine.globalAlpha = alpha;
            engine.fill();
            engine.restore();
        },
    });
    
    export const makeEmitter = function (items) {
    
        if (!items) return false;
        return new Emitter(items);
    };
    
    constructors.Emitter = Emitter;