- 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

- §

  # Base mixin

  This mixin sets up much of the basic functionality of a Scrawl-canvas Object:

  - defs - every Scrawl-canvas object comes with a set of pre-defined attributes with default values; the specific attributes will vary between factories, but all include a ‘name’ attribute.
  - getters - to retrieve values from a given attribute.
  - setters - to update a range of attribute values in a Scrawl-canvas object.
  - deltaSetters - to add supplied values to a range of attribute values in a Scrawl-canvas object.
  - name attribute functionality - all Scrawl-canvas objects are given a name attribute, either supplied by the developer or computer-generated by the system.
  - Object registration functionality - most factory-generated objects get registered in the Scrawl-canvas library object.
  - packet generation and use - Scrawl-canvas objects can deliver and consume text-based packets to build new objects or update existing objects.
  - clone functionality - most Scrawl-canvas objects can be cloned, to make the development of a canvas (or stack) scene easier.
  - kill functionality - remove objects from the Scrawl-canvas system - and, where required, the DOM - in a safe way.

  Given the fundamental nature of the above functionality the base mixin should always, when coding a Scrawl-canvas factory, be the first to be applied to that factory function’s prototype.

- §

  #### Imports

      import * as library from '../core/library.js';

      import { addStrings, generateUniqueString, isa_boolean, isa_obj, mergeOver, pushUnique, removeItem, xt, xta, λnull, Ωempty } from '../core/utilities.js';

      import { _entries, _isArray, _keys, _parse, _string, ARG_SPLITTER, BAD_PACKET_CHECK, HAS_PACKET_CHECK, NAME, NATIVE_CODE, PACKET_DIVIDER, TYPE_EXCLUSIONS, UNDEF, ZERO_STR } from '../core/shared-vars.js'

- §

  #### Export function

      export default function (P = Ωempty) {

- §

  #### Get, Set, deltaSet

  The defs object supplies default values for a Scrawl-canvas object. Setter functions will check to see that a related defs attribute exists before allowing users to update an object attribute. Similarly the getter function will use the defs object to supply default values for an attribute that has not otherwise been set, or has been deleted by a user.

          P.defs = {};

- §

  The getters object holds a suite of functions for given factory object attributes that need to have their values processed before they can be returned to the user.

          P.getters = {};

- §

  The setters object holds a suite of functions for given factory object attributes that need to process a new value before setting it to the attribute.

          P.setters = {};

- §

  The deltaSetters object holds a suite of functions for given factory object attributes that need to process a new value before adding it to the attribute’s existing value.

          P.deltaSetters = {};

- §

  `get` - Retrieve an attribute value using the get function. While many attributes can be retrieved directly - for example, scrawl.artefact.myelement.scale - some attributes should only ever be retrieved using get:

      scrawl.artefact.myelement.get('startX');
      -> 200

          P.get = function (item) {

              let val, fn;

              if (xt(item)) {

                  fn = this.getters[item];

                  if (fn) return fn.call(this);

                  else {

                      const def = this.defs[item];

                      if (typeof def != UNDEF) {

                          val = this[item];
                          return (typeof val != UNDEF) ? val : def;
                      }
                  }
              }
              return null;
          };

- §

  `set` - Set an attribute value using the set function. It is extremely important that all factory object attributes are set using the set function; setting an attribute directly will lead to unexpected behaviour! The set function takes a single object as its argument.

      scrawl.artefact.myelement.set({
          startX: 50,
          startY: 200,
          scale: 1.5,
          roll: 90,
      });

          P.set = function (items = Ωempty) {

              let i, key, val, fn;

              const keys = _keys(items),
                  keysLen = keys.length;

              if (keysLen) {

                  const setters = this.setters,
                      defs = this.defs;

                  for (i = 0; i < keysLen; i++) {

                      key = keys[i];
                      val = items[key];

                      if (key && key != NAME && val != null) {

                          fn = setters[key];

                          if (fn) fn.call(this, val);
                          else if (typeof defs[key] != UNDEF) this[key] = val;
                      }
                  }
              }
              return this;
          };

- §

  `setDelta` - Add a value to an existing attribute value using the setDelta function. It is extremely important that all factory object attributes are updated using the setDelta function; updating an attribute directly will lead to unexpected behaviour! The setDelta function takes a single object as its argument.

      scrawl.artefact.myelement.setDelta({
          startX: 10,
          startY: -20,
          scale: 0.05,
          roll: 5,
      });

          P.setDelta = function (items = Ωempty) {

              let i, key, val, fn;

              const keys = _keys(items),
                  keysLen = keys.length;

              if (keysLen) {

                  const setters = this.deltaSetters,
                      defs = this.defs;

                  for (i = 0; i < keysLen; i++) {

                      key = keys[i];
                      val = items[key];

                      if (key && key != NAME && val != null) {

                          fn = setters[key];

                          if (fn) fn.call(this, val);
                          else if (typeof defs[key] != UNDEF) this[key] = addStrings(this[key], val);
                      }
                  }
              }
              return this;
          };

- §

  #### Shared attributes

          const defaultAttributes = {

- §

  Scrawl-canvas relies on unique name values being present in a factory object for a wide range of functionalities. Most of the library sections store an object by its name value, for example: scrawl.artefact.myelement

              name: '',
          };
          P.defs = mergeOver(P.defs, defaultAttributes);

          const G = P.getters;

          G.type = function () {
              return this.type;
          };
          G.isArtefact = function () {
              return this.isArtefact;
          };
          G.isAsset = function () {
              return this.isAsset;
          };

- §

  #### Packet management

  Packets are Scrawl-canvas’s way of generating and consuming SC object data, both locally and over a network. A packet is a formatted JSON String which can be captured in a variable or saved to a text file.

  The packet format is as follows:

      "[object-name, object-type, object's-library-section, {object-data-key:value-pairs}]"

  - Use saveAsPacket() to get a stringified representation of a Scrawl-canvas object, which can then be consumed by other SC objects at a later date.
  - Use actionPacket(‘packet-string’) to apply a previously saved packet to an existing SC object, or to create a new SC object from the packet if it doesn’t exist already
  - Use importPacket(url) to retrieve a packet from a text file (normally kept remotely on a server); the function’s argument can be either the packet file’s url address, or an Array of such addresses.

  Using JSON.stringify(object), or object.toString(), to generate String representations of SC objects is not recommended as they will forego the pre-processing that saveAsPacket performs to generate the packet String.

  Example:

      let canvas = scrawl.library.canvas.mycanvas;
      canvas.setAsCurrentCanvas();

      let box = scrawl.makeBlock({

          name: 'my-box',

          startX: 10,
          startY: 10,

          width: 100,
          height: 50,

          fillStyle: 'red',

          onEnter: function () {
              box.set({
                  fillStyle: 'pink',
              });
          },

          onLeave: function () {
              box.set({
                  fillStyle: 'red',
              });
          }
      });

      let boxPacket = box.saveAsPacket();

      console.log(boxPacket);
      | [
      |     "my-box",
      |     "Block",
      |     "entity",
      |     {
      |         "name":"my-box",
      |         "dimensions":[100,50],
      |         "start":[10,10],
      |         "delta":{},
      |         "onEnter":"function () {\n\t\tbox.set({\n\t\t\tfillStyle: 'pink',\n\t\t});\n\t}",
      |         "onLeave":"function () {\n\t\tbox.set({\n\t\t\tfillStyle: 'red',\n\t\t});\n\t}",
      |         "group":"mycanvas_base",
      |         "fillStyle":"black"
      |     }
      | ]

      // delete the box
      box.kill()

      // recreate the entity - we can invoke the function on any handy SC object without affecting that object
      let resurrectedBox = canvas.actionPacket(boxPacket);

      // recreate the entity - if we had saved the packet to a file on a server
      let fetchedBox = canvas.importPacket('https://example.com/path/to/boxPacket.txt');

- §

  Internal processing Arrays

          P.packetExclusions = [];
          P.packetExclusionsByRegex = [];
          P.packetCoordinates = [];
          P.packetObjects = [];
          P.packetFunctions = [];

- §

  `saveAsPacket` - accepts an items {key:value} object argument with the following (optional) attributes:

  - includeDefaults - either a boolean (default: false), or an array of attribute strings listing those attributes whose values should be included in the packet even if those values are the default values; setting this attribute to boolean true will include all of the Scrawl-canvas object’s attribute values in the (much larger) packet.

  Note: if the argument is supplied as a boolean ‘true’, code will create an items object with an attribute ‘includeDefaults’ set to true.

          P.saveAsPacket = function (items = Ωempty) {

              if (isa_boolean(items) && items) items = {
                  includeDefaults: true,
              }

              const defs = this.defs,
                  defKeys = _keys(defs),
                  packetExclusions = this.packetExclusions,
                  packetExclusionsByRegex = this.packetExclusionsByRegex,
                  packetCoordinates = this.packetCoordinates,
                  packetObjects = this.packetObjects,
                  packetFunctions = this.packetFunctions;

              let packetDefaultInclusions = items.includeDefaults || false;

              let copy = {};

              if (packetDefaultInclusions && !_isArray(packetDefaultInclusions)) {

                  packetDefaultInclusions = _keys(defs);
              }
              else if (!packetDefaultInclusions) packetDefaultInclusions = [];

              _entries(this).forEach(([key, val]) => {

                  let flag = true,
                      test;

                  if (!defKeys.includes(key)) flag = false;

                  if (flag && packetExclusions.includes(key)) flag = false;

                  if (flag) {

                      test = packetExclusionsByRegex.some(reg => new RegExp(reg).test(key));
                      if (test) flag = false;
                  }

                  if (flag) {

                      if (packetFunctions.includes(key)) {

                          if (xt(val) && val != null) {

                              const func = this.stringifyFunction(val);

                              if (func && func.length) copy[key] = func;
                          }
                      }

                      else if (packetObjects.includes(key) && this[key] && this[key].name) copy[key] = this[key].name;

                      else if (packetCoordinates.includes(key)) {

                          if (packetDefaultInclusions.includes(key)) copy[key] = val;
                          else if (val[0] || val[1]) copy[key] = val;
                      }

                      else {

- §

  Start cascade down to factory to pick up factory-specific exclusions

                          test = this.processPacketOut(key, val, packetDefaultInclusions);
                          if (test) copy[key] = val;
                      }
                  }
              }, this);

- §

  Start cascade down to factory to complete the copy object’s build

              copy = this.finalizePacketOut(copy, items);

- §

  Return a JSON string

              return _string([this.name, this.type, this.lib, copy]);
          };

- §

  Helper functions that get defined in various mixins and factories - localizing the functionality to meet the specific needs of that factory’s object instances

- §

  `stringifyFunction`

          P.stringifyFunction = function (val) {

- §

  The dotAll /s regex flag currently not supported by Firefox let matches = val.toString().match(/((.?)).?{(.*)}/s);

              const matches = val.toString().match(/\(([\s\S]*?)\)[\s\S]*?\{([\s\S]*)\}/),
                  vars = matches[1],
                  func = matches[2];

              return (xta(vars, func)) ? `${vars}${PACKET_DIVIDER}${func}` : false;
          };

- §

  `processPacketOut`

          P.processPacketOut = function (key, value, incl) {

              let result = true;

              if (!incl.includes(key) && value === this.defs[key]) result = false;

              return result;
          };

- §

  `finalizePacketOut`

          P.finalizePacketOut = function (copy) {

              return copy;
          };

- §

  `importPacket` - Import and unpack a string representation of a factory object using the saveAsPacket function.

  - Uses fetch, thus is an asynchronous process and returns a promise
  - Once we have the packet, we can further action it using actionPacket()

          P.importPacket = function (items) {

              const self = this;

              const getPacket = function(url) {

                  return new Promise((resolve, reject) => {

                      let report;

                      if (!url.substring) reject(new Error('Packet url supplied for import is not a string'));

                      if (url[0] == HAS_PACKET_CHECK) {

- §

  Looks like we already have a packet for processing

                          report = self.actionPacket(url);
                          if (report && report.lib) resolve(report);
                          else reject(report);
                      }

- §

  This is not much of a test …

                      else if (url.includes(BAD_PACKET_CHECK)) {

- §

  Looks like we have a packet for processing, but it’s malformed

                          reject(new Error('Bad packet supplied for import'));
                      }
                      else {

- §

  Attempt to fetch the packet from a remote server

                          fetch(url)
                          .then(res => {

                              if (!res.ok) throw new Error(`Packet import from server failed - ${res.status}: ${res.statusText} - ${res.url}`);
                              return res.text();
                          })
                          .then(packet => {

                              report = self.actionPacket(packet);
                              if (report && report.lib) resolve(report);
                              else throw report;
                          })
                          .catch(error => reject(error));
                      }
                  });
              };

              if (_isArray(items)) {

                  const promises = [];

                  items.forEach(item => promises.push(getPacket(item)));

                  return new Promise((resolve, reject) => {

                      Promise.all(promises)
                      .then(res => resolve(res))
                      .catch(error => reject(error));
                  });
              }
              else if (items.substring) return getPacket(items);
              else Promise.reject(new Error('Argument supplied for packet import is not a string or array of strings'));
          };

- §

  `actionPacket` - This function:

  1. Unpacks the packet
  2. Determines whether an artefact/asset/style/tween/etc of this packet’s name already exists
  3. If we have existence, set the artefact/asset/style/tween/etc with the packet’s data
  4. If we have no existence, create the artefact/asset/style/tween/etc
  5. Returns the affected artefact/asset/style/tween/etc on success; false otherwise

  The function can be called directly on any Scrawl-canvas object that uses the base.js mixin - which means that all differing functionality for various types of object have to remain here, in base.js

          P.actionPacket = function (packet) {

              try {

                  if (packet && packet.substring) {

                      if (packet[0] == HAS_PACKET_CHECK) {

                          let name, type, lib, update;

                          try {

                              [name, type, lib, update] = _parse(packet);
                          }
                          catch (e) {

                              throw new Error(`Failed to process packet due to JSON parsing error - ${e.message}`);
                          }

                          if (xta(name, type, lib, update)) {

                              if (TYPE_EXCLUSIONS.includes(type)) {

                                  throw new Error(`Failed to process packet - Stacks, Canvases and visual assets are excluded from the packet system`);
                              }

                              let obj = library[lib][name];

                              if (obj) obj.set(update);
                              else {

- §

  Stack-based artefacts need a DOM element that they can pass into the factory

                                  if (update.outerHTML && update.host) {

                                      const myParent = document.querySelector(`#${update.host}`);

                                      if (myParent) {

                                          const tempEl = document.createElement('div');

                                          tempEl.innerHTML = update.outerHTML;

                                          const myEl = tempEl.firstElementChild;

                                          if (myEl) {

                                              myEl.id = name;

                                              myParent.appendChild(myEl);

                                              update.domElement = myEl;
                                          }
                                      }
                                  }

                                  obj = new library.constructors[type](update);

                                  if (!obj) throw new Error('Failed to create Scrawl-canvas object from supplied packet');
                              }

- §

  For the main object

                              obj.packetFunctions.forEach(item => this.actionPacketFunctions(obj, item));

- §

  For artefact anchors - I know that anchors only have the one function to worry about, but doing it this way so I don’t forget how to approach it eg for SC sub-objects that have more than one user-settable function (eg timeline actions)

                              if (update.anchor && obj.anchor) {

                                  obj.anchor.packetFunctions.forEach(item => {

- §

  Anchor.setters.clickAction(arg) explicitly checks that the supplied arg is a function - if it isn’t (like in packet cases) then the attribute doesn’t get updated when we invoke obj.set(update); earlier in this function.

                                      obj.anchor[item] = update.anchor[item];
                                      this.actionPacketFunctions(obj.anchor, item)

- §

  Anchors are a bit of an exception case because they add a user-interactive and yet otherwise untracked DOM element to the page, which has to be updated in its own sweet, special way…

                                      obj.anchor.build();
                                  });
                              }

- §

  Specific to Phrase entitys, which doesn’t include a simple way to set or update glyphStyle objects

                              if (update.glyphStyles && obj.glyphStyles) {

                                  update.glyphStyles.forEach((gStyle, index) => {

                                      if (isa_obj(gStyle)) obj.setGlyphStyles(gStyle, index);
                                  });
                              }

                              if (obj) return obj;
                              else throw new Error('Failed to process supplied packet');
                          }
                          else throw new Error('Failed to process packet - JSON string holds incomplete data');
                      }
                      else throw new Error('Failed to process packet - JSON string does not represent an array');
                  }
                  else throw new Error('Failed to process packet - not a JSON string');
              }
              catch (e) { console.log(e); return e }
          };

- §

  `actionPacketFunctions` - internal helper function - creates functions from Strings

          P.actionPacketFunctions = function(obj, item) {

              const fItem = obj[item];

              if (xt(fItem) && fItem != null && fItem.substring) {

                  if (fItem === PACKET_DIVIDER) obj[item] = λnull;
                  else {

                      let args, func, f;

      /* eslint-disable-next-line */
                      [args, func] = fItem.split(PACKET_DIVIDER);

                      args = args.split(ARG_SPLITTER);
                      args = args.map(a => a.trim());

- §

  Native code raises non-terminal errors (because it is native code!) - so we dodge that bullet.

                      if (!func.includes(NATIVE_CODE)) {

                          f = new Function(...args, func);

                          obj[item] = f.bind(obj);
                      }
                      else obj[item] = λnull;
                  }
              }
          };

- §

  #### Clone management

  Most Scrawl-canvas factory objects can be copied using the clone function. The result will be an exact copy of the original, additionally set with values supplied in the argument object.

      scrawl.artefact.myelement.clone({
          name: 'myclonedelement',
          startY: 60,
      });

- §

  `clone`

          P.clone = function (items = Ωempty) {

              const myName = this.name;

              let myPacket, myTicker;

              this.name = items.name || ZERO_STR;

- §

  Tickers are specific to Tween and Action objects

              if (items.useNewTicker) {

                  myTicker = this.ticker;
                  this.ticker = null;
                  myPacket = this.saveAsPacket();
                  this.ticker = myTicker;
              }

- §

  Everything else

              else myPacket = this.saveAsPacket();

              this.name = myName;

              let clone = this.actionPacket(myPacket);

              this.packetFunctions.forEach(func => {

                  if (this[func]) clone[func] = this[func];
              });

              clone = this.postCloneAction(clone, items);

              clone.set(items);

              return clone;
          };

- §

  `postCloneAction` - overwritten by a variety of mixins and factories

          P.postCloneAction = function (clone) {

              return clone;
          };

- §

  #### Kill management

- §

  `kill` - overwritten by various mixins and factories

          P.kill = function () {

              return this.deregister();
          }

- §

  #### Prototype functions

  `makeName` - If the developer doesn’t supply a name value for a factory function, then Scrawl-canvas will generate a random name for the object to use.

          P.makeName = function (item) {

              if (item && item.substring && !library[`${this.lib}names`].includes(item)) this.name = item;
              else this.name = generateUniqueString();

              return this;
          };

- §

  `register` - Many (but not all) factory functions will register their result objects in the scrawl lobrary. The section where the object is stored is dependent on the factory function’s type value.

  - Some objects are stored in more than one place - for example the artefacts section will include Stack, Element and Canvas objects in addition to various Entity objects

          P.register = function () {

              if (!xt(this.name)) throw new Error(`core/base error - register() name not set: ${this}`);

              const arr = library[`${this.lib}names`],
                  mylib = library[this.lib];

              if(this.isArtefact){

                  pushUnique(library.artefactnames, this.name);
                  library.artefact[this.name] = this;
              }

              if(this.isAsset){

                  pushUnique(library.assetnames, this.name);
                  library.asset[this.name] = this;
              }

              pushUnique(arr, this.name);
              mylib[this.name] = this;

              return this;
          };

- §

  `deregister` - Reverse what register() does

          P.deregister = function () {

              if (!xt(this.name)) throw new Error(`core/base error - deregister() name not set: ${this}`);

              const arr = library[`${this.lib}names`],
                  mylib = library[this.lib];

              if(this.isArtefact){

                  removeItem(library.artefactnames, this.name);
                  delete library.artefact[this.name];
              }

              if(this.isAsset){

                  removeItem(library.assetnames, this.name);
                  delete library.asset[this.name];
              }

              removeItem(arr, this.name);
              delete mylib[this.name];

              return this;
          };
      }