- Jump To … +
  ./source/core/animation-loop.js ./source/core/display-cycle.js ./source/core/document-root-elements.js ./source/core/document.js ./source/core/events.js ./source/core/init.js ./source/core/library.js ./source/core/random-seed.js ./source/core/shared-vars.js ./source/core/snippets.js ./source/core/system-flags.js ./source/core/user-interaction.js ./source/core/utilities.js ./source/factory/action.js ./source/factory/anchor.js ./source/factory/animation.js ./source/factory/array-pool.js ./source/factory/bezier.js ./source/factory/block.js ./source/factory/canvas.js ./source/factory/cell-fragment.js ./source/factory/cell.js ./source/factory/cog.js ./source/factory/color.js ./source/factory/conic-gradient.js ./source/factory/coordinate.js ./source/factory/crescent.js ./source/factory/drag-zone.js ./source/factory/element.js ./source/factory/emitter.js ./source/factory/filter-engine-bluenoise-data.js ./source/factory/filter-engine.js ./source/factory/filter.js ./source/factory/font-attributes.js ./source/factory/gradient.js ./source/factory/grid.js ./source/factory/group.js ./source/factory/image-asset.js ./source/factory/keyboard-zone.js ./source/factory/line-spiral.js ./source/factory/line.js ./source/factory/loom.js ./source/factory/mesh.js ./source/factory/net.js ./source/factory/noise-asset.js ./source/factory/observe-update.js ./source/factory/oval.js ./source/factory/palette.js ./source/factory/particle-force.js ./source/factory/particle-history.js ./source/factory/particle-spring.js ./source/factory/particle-world.js ./source/factory/particle.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/radial-gradient.js ./source/factory/raw-asset.js ./source/factory/reaction-diffusion-asset.js ./source/factory/rectangle.js ./source/factory/render-animation.js ./source/factory/shape.js ./source/factory/spiral.js ./source/factory/sprite-asset.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/unstacked-element.js ./source/factory/vector.js ./source/factory/video-asset.js ./source/factory/wheel.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/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/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/shape-path-calculation.js ./source/mixin/styles.js ./source/mixin/tween.js

- §

  # Emitter 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.
- §

  #### 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-012 - Use Net entity particles as reference coordinates for other artefacts
  - delaunator-001 - Delauney triangulation and Voronoi cell visualisation
- §

  #### Imports

      import { artefact, constructors, world } from '../core/library.js';

      import { doCreate, isa_fn, isa_obj, mergeOver, pushUnique, xta, λnull, Ωempty } from '../core/utilities.js';

      import { currentGroup } from './canvas.js';

      import { releaseParticle, requestParticle } from './particle.js';

      import { releaseCell,  requestCell } from './cell-fragment.js';

      import { makeVector, releaseVector, requestVector } from './vector.js';

      import { releaseCoordinate, requestCoordinate } from './coordinate.js';

      import { makeColor } from './color.js';

      import baseMix from '../mixin/base.js';
      import entityMix from '../mixin/entity.js';

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

- §

  #### 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();

- §

  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 = [];

          if (!items.group) items.group = currentGroup;

          this.set(items);

          if (this.purge) this.purgeArtefact(this.purge);

          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

  - Attributes defined in the base mixin: name.
  - Attributes defined in the position mixin: group, visibility, order, calculateOrder, stampOrder, start, startX, startY, handle, handleX, handleY, offset, offsetX, offsetY, dimensions, width, height, pivoted, mimicked, lockTo, lockXTo, lockYTo, scale, roll, noUserInteraction, noPositionDependencies, noCanvasEngineUpdates, noFilters, noPathUpdates, purge, bringToFrontOnDrag.
  - Attributes defined in the delta mixin: delta, noDeltaUpdates.
  - Attributes defined in the pivot mixin: pivot, pivotCorner, addPivotHandle, addPivotOffset, addPivotRotation.
  - Attributes defined in the mimic mixin: mimic, useMimicDimensions, useMimicScale, useMimicStart, useMimicHandle, useMimicOffset, useMimicRotation, useMimicFlip, addOwnDimensionsToMimic, addOwnScaleToMimic, addOwnStartToMimic, addOwnHandleToMimic, addOwnOffsetToMimic, addOwnRotationToMimic.
  - Attributes defined in the path mixin: path, pathPosition, addPathHandle, addPathOffset, addPathRotation, constantPathSpeed.
  - Attributes defined in the entity mixin: method, pathObject, winding, flipReverse, flipUpend, scaleOutline, lockFillStyleToEntity, lockStrokeStyleToEntity, onEnter, onLeave, onDown, onUp, fillStyle, strokeStyle, globalAlpha, globalCompositeOperation, lineWidth, lineCap, lineJoin, lineDash, lineDashOffset, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, filter.
  - Attributes defined in the anchor mixin: anchor.
  - Attributes defined in the filter mixin: filters, isStencil.

      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’sinitial 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,

- §

  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,

- §

  ##### 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.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 thew 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;
              }
          }
      };

- §

  `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 item + ((_random() * itemVar * 2) - itemVar);
          };

          const velocityCalc = function (item, itemVar) {
              return item + (_random() * itemVar);
          };

          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, 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),
                          velocityY: velocityCalc(fy, y),
                          velocityZ: velocityCalc(fz, 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);
                  }
                  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),
                          velocityY: velocityCalc(fy, y),
                          velocityZ: velocityCalc(fz, 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: res.x,
                      positionY: res.y,
                      positionZ: 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);
                      res.rotate((_floor(_random() * r)) * limitDirectionToAngleMultiples);

                      p.set({
                          velocityX: res.x,
                          velocityY: res.y,
                          velocityZ: velocityCalc(fz, z),
                      });
                  }
                  else {

                      p.set({
                          velocityX: velocityCalc(fx, x),
                          velocityY: velocityCalc(fy, y),
                          velocityZ: velocityCalc(fz, z),
                      });
                  }
                  releaseVector(res);

                  p.velocity.rotate(currentRotation);

                  timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                  radiusKill = _abs(calc(killRadius, killRadiusVariation));

                  p.run(timeKill, radiusKill, killBeyondCanvas);

                  particleStore.push(p);
              }
          }

- §

  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: res.x,
                      positionY: res.y,
                      positionZ: 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);
                      res.rotate((_floor(_random() * r)) * limitDirectionToAngleMultiples);

                      p.set({
                          velocityX: res.x,
                          velocityY: res.y,
                          velocityZ: velocityCalc(fz, z),
                      });
                  }
                  else {

                      p.set({
                          velocityX: velocityCalc(fx, x),
                          velocityY: velocityCalc(fy, y),
                          velocityZ: velocityCalc(fz, z),
                      });
                  }
                  releaseVector(res);

                  p.velocity.rotate(currentRotation);

                  timeKill = _abs(calc(killAfterTime, killAfterTimeVariation));
                  radiusKill = _abs(calc(killRadius, killRadiusVariation));

                  p.run(timeKill, radiusKill, killBeyondCanvas);

                  particleStore.push(p);
              }
          }

- §

  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),
                      velocityY: velocityCalc(fy, y),
                      velocityZ: velocityCalc(fz, 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} = 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);

          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.setTransform(1, 0, 0, 1, 0, 0);
              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 = [], mycell) {

          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 (!tx.toFixed || !ty.toFixed || isNaN(tx) || isNaN(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, mycell);
          }
          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;