- 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

- §

  # Phrase factory

  Phrase entitys are graphical text rectangles rendered onto a DOM <canvas> element using the Canvas API’s CanvasRenderingContext2D interface - in particular the `fillRect`, `strokeRect`, `fillText` and `strokeText` methods.

  - Positioning functionality for the Phrase is supplied by the position mixin, while rendering functionality comes from the entity mixin.
  - Phrases can use CSS color Strings for their fillStyle and strokeStyle values, alongside Gradient, RadialGradient, Color and Pattern objects.
  - They will also accept Filter objects.
  - They can use Anchor objects for user navigation.
  - They can be rendered to the canvas by including them in a Cell object’s Group.
  - They can be animated directly, or using delta animation, or act as the target for Tween animations.
  - Phrases can be cloned, and killed.

  Phrase entity dimensions work differently to that of other entitys:

  - The `width` attribute is required for multi-line text. When set, Phrase entitys will automatically render text longer than its width in multiple lines on the canvas.
  - The `height` attribute is normally disregarded. Instead height is calculated as a combination of the font `size`, `lineheight`, and the number of lines of text that need to be rendered on the canvas - which itself depends on the text’s length and the entity’s `width` attribute.

  Be aware that text is always rendered as a graphic, not a block.

  - Scrawl-canvas ignores all attempts to set the canvas context engine’s `textAlign` and `textBaseline` attributes, which are permanently set to the defaults `left` and `top` respectively.
  - Each glyph (letter) is stamped separately onto the canvas - this allows us to include letterspacing and justification functionality, and to allow text to be styled on a per-glyph basis (overline/underline, highlight, color, bold/italics, etc).

  Phrase entitys use FontAttribute objects to help manage their text font:

  - More than one font family can be used in a single Phrase.
  - Font styles are also supported: a single Phrase can include multiple episodes of bold, italic, etc.
  - More than one font size can be displayed within a Phrase.
  - Letter spacing is supported, both across the entire text and within the text.
  - Beyond fonts, ranges of letters within the Phrase text can be background highlighted, or given overlines and/or underlines.

  Phrase entitys fully support text along a path at the Phrase, word and letter levels.

  Phrase entity text content is accessible to assistive technologies such as screen readers, by default.

- §

  #### Demos:

  - Canvas-015 - Phrase entity (make, clone, method, multiline)
  - Canvas-016 - Phrase entity position and font attributes; Block mimic functionality
  - Canvas-017 - Phrase entity - test lineHeight, letterSpacing and justify attributes; setSectionStyles() functionality
  - Canvas-018 - Phrase entity - text along a path
  - Canvas-029 - Phrase entitys and gradients
  - Snippets-001 - Scrawl-canvas DOM element snippets
  - Packets-002 - Scrawl-canvas packets - save and load a range of different entitys
  - Modules-001 - Scrawl-canvas modularized code - London crime charts
- §

  #### TODO:

  The CSS Font Loading API is beginning to stabilize across browsers, which means we can watch for font loads and then perform text measurements using the TextMetrics interface

  - Currently we need to calculate font/text height using temporary DOM elements - this is both ugly and less than accurate
  - Text metrics will give us much better height measurements, and also glyph width measurements (and thus advance values for eg text along a path)
  - However progress with the enhanced TextMetrics is slower across browsers, meaning that any code we develop is likely to bloat the code base as we cannot (yet) afford to discard existing code.
  - More details in Demo Canvas-029 - Phrase entitys and gradients

  interface TextMetrics { // x-direction readonly attribute double width; // advance width readonly attribute double actualBoundingBoxLeft; readonly attribute double actualBoundingBoxRight;

- §

  // y-direction readonly attribute double fontBoundingBoxAscent; readonly attribute double fontBoundingBoxDescent; readonly attribute double actualBoundingBoxAscent; readonly attribute double actualBoundingBoxDescent; readonly attribute double emHeightAscent; readonly attribute double emHeightDescent; readonly attribute double hangingBaseline; readonly attribute double alphabeticBaseline; readonly attribute double ideographicBaseline; };

  Upcoming new font attributes: enum CanvasFontKerning { “auto”, “normal”, “none” }; enum CanvasFontStretch { “ultra-condensed”, “extra-condensed”, “condensed”, “semi-condensed”, “normal”, “semi-expanded”, “expanded”, “extra-expanded”, “ultra-expanded” }; enum CanvasFontVariantCaps { “normal”, “small-caps”, “all-small-caps”, “petite-caps”, “all-petite-caps”, “unicase”, “titling-caps” }; enum CanvasTextRendering { “auto”, “optimizeSpeed”, “optimizeLegibility”, “geometricPrecision” };

- §

  #### Imports

      import { artefact, cell, cellnames, constructors, sectionClasses, styles, stylesnames } from '../core/library.js';

      import { scrawlCanvasHold } from '../core/document.js';

      import { addStrings, doCreate, isa_number, isa_obj, mergeOver, pushUnique, xt, xta, Ωempty } from '../core/utilities.js';

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

      import { releaseArray, requestArray } from './array-pool.js';

      import { makeFontAttributes } from './font-attributes.js';

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

      import { _abs, _parse, _ceil, _assign, _max, _values, _floor, ARIA_HIDDEN, ARIA_LIVE, AUTO, BLACK, BORDER_BOX, CENTER, CLASS_REGEX, CLIP, DEF_HIGHLIGHT, DEF_LINE_COLOR, DEF_SECTION_MARKERS, DEFAULT, DESTINATION_OUT, DIV, ENTITY, FAMILY, FULL, HALFTRANS, HANDLE, JUSTIFICATIONS, LEFT, LTR, NONE, POLITE, RIGHT, SIZE, SIZE_METRIC, SIZE_VALUE, SOURCE_OVER, SPACE, STRETCH, STYLE, T_CANVAS, T_CELL, T_PHRASE, T_SHAPE, TEXTAREA, TOP, TRUE, VARIANT, WEIGHT, ZERO_STR } from '../core/shared-vars.js';

- §

  Local constants

      const defaultTextForHeightMeasurements = '|/}ÁÅþ§¶¿∑ƒ⌈⌊qwertyd0123456789QWERTY';

      const fontHeightCalculator = document.createElement(DIV);
      fontHeightCalculator.style.padding = 0;
      fontHeightCalculator.style.border = 0;
      fontHeightCalculator.style.margin = 0;
      fontHeightCalculator.style.height = AUTO;
      fontHeightCalculator.style.lineHeight = 1;
      fontHeightCalculator.style.boxSizing = BORDER_BOX;
      fontHeightCalculator.innerHTML = defaultTextForHeightMeasurements;
      fontHeightCalculator.setAttribute(ARIA_HIDDEN, TRUE);
      scrawlCanvasHold.appendChild(fontHeightCalculator);

      const textEntityConverter = document.createElement(TEXTAREA);

- §

  ensureFloat - return the value provided as a floating point number of given precision; return 0 if not a number

      const ensureFloat = (val, precision) => {

          val = parseFloat(val);

          if (!isa_number(val)) val = 0;
          if (!isa_number(precision)) precision = 0;

          return parseFloat(val.toFixed(precision));
      };

- §

  ensurePositiveFloat - return the value provided as a positive floating point number of given precision; return 0 if not a number

      const ensurePositiveFloat = (val, precision) => {

          val = parseFloat(val);

          if (!isa_number(val)) val = 0;
          if (!isa_number(precision)) precision = 0;

          return _abs(parseFloat(val.toFixed(precision)));
      };

- §

  ensureString - return a String representation of the value

      const ensureString = (val) => {

          return (val.substring) ? val : val.toString;
      };

- §

  #### Phrase constructor

      const Phrase = function (items = Ωempty) {

          this.fontAttributes = makeFontAttributes(Ωempty);

          this.entityInit(items);

          this.dirtyDimensions = true;
          this.dirtyText = true;
          this.dirtyFont = true;
          this.dirtyPathObject = true;

          return this;
      };

- §

  #### Phrase prototype

      const P = Phrase.prototype = doCreate();
      P.type = T_PHRASE;
      P.lib = ENTITY;
      P.isArtefact = true;
      P.isAsset = false;

- §

  #### Mixins

  - base
  - entity

      baseMix(P);
      entityMix(P);

      P.midInitActions = function () {

          this.sectionStyles = [];
      };

- §

  #### Phrase 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 = {

- §

  text - the text String to be displayed by the Phrase

          text: ZERO_STR,

- §

  width - Number or String

  - In addition to normal dimensional values, Phrase entitys will accept the String label ‘auto’ (default). When set to ‘auto’ the width will be calculated as the natural, single line text length.

          width: AUTO,

- §

  exposeText - Boolean accessibility feature

  - When exposeText is set to true (default), Scrawl-canvas will create an element in the DOM and mirror its current text value in that element.
  - The element - a <div> - is attached to the canvas element’s textHold element, which immediately follows that element and has zero dimensions, so its contents don’t interfere with the flow of the rest of the DOM content.

          exposeText: true,

- §

  lineHeight - a positive float Number multiplier applied to the font height to add space between lines of text

          lineHeight: 1.15,

- §

  letterSpacing - a positive float Number representing a set number of pixels to place between each glyph (letter). Can be overridden for letter ranges using styling objects

          letterSpacing: 0,

- §

  justify - String value to indicate how the text should justify itself within its dimensions box.

  - Permitted values are: `left` (default), `center`, `right`, `full` (for ‘justified’ text).

          justify: LEFT,

- §

  ##### In-text styling

- §

  sectionClasses - Array of styling objects

  Glyphs (letters) can be individually styled by adding a styling object to the global `sectionClasses` Array (which is stored in the Library, not the Phrase prototype or entity), and then adding section style markup to the Phrase’s text String attribute. Subsequent glyphs will inherit those styles until a second style marker updating or terminating that particular style is encountered.

  The following font attributes can be modified on a per-glyph basis using section classes:

  - `style` - eg ‘italic’
  - `variant` - eg ‘small-caps’
  - `weight` - eg ‘bold’
  - `stretch`
  - `size` - any permitted font size value
  - `family` - font family
  - `space` - alter the letterSpacing values to spread or condense glyphs
  - `fill` - fillStyle to be applied to the glyph
  - `stroke` - strokeStyle to be applied to the glyph
  - `highlight` - boolean - whether highlight should be applied to the glyph
  - `underline` - boolean - whether underline should be applied to the glyph
  - `overline` - boolean - whether overline should be applied to the glyph
  - `defaults` - boolean - setting this to true will set the glyph (and subsequent glyphs) to the Phrase entity’s current font and fill/stroke style values

  Usage:

  1. Add styling objects to the `sectionClasses` Array - a number of styling objects are pre-defined for every Phrase entity; these can be added to, modified or deleted at any time using the `addSectionClass` and `removeSectionClass` functions.
  2. If necessary, update the entity’s `sectionClassMarker` delimiter attrbute. The attribute has to be a String, but that string can define a Regular Expression - for example setting the attribute to `[§<>]` will identify style markup delimeted by both `§marker-label§` and `<marker-label>`.
  3. Add section style markup to the Phrase entity’s text attribute value.

      let myPhrase = scrawl.makePhrase({
          name: 'my-phrase',
          text: 'This is §RED§some red text§DEFAULTS§ for demonstration.'
      });

      myPhrase.addSectionClass('RED', { fill: 'red' });

  The following classes are pre-defined for every Phrase entity:

  - `DEFAULTS` - remove all inline glyph styling from this point on
  - `b`, `/b`, `strong`, `/strong`, `BOLD`, `/BOLD` - add/remove bold styling
  - `i`, `/i`, `em`, `/em`, `ITALIC`, `/ITALIC` - add/remove italic styling
  - `SMALL-CAPS`, `/SMALL-CAPS` - add/remove small-caps styling
  - `HIGHLIGHT`, `/HIGHLIGHT` - add/remove glyph background highlight
  - `u`, `/u`, `UNDERLINE`, `/UNDERLINE` - add/remove glyph underline
  - `OVERLINE`, `/OVERLINE` - add/remove glyph overline

          sectionClassMarker: DEF_SECTION_MARKERS,

- §

  ##### Overlines, underlines, highlighting

  We set the position and style for overlines, underlines and background highlight on a per-Phrase entity level, then apply them to glyphs using sectionClasses styling objects.

  - over/underline decoration positions are float Numbers (generally) in the range `0 - 1` which represent where on the Phrase text line the decoration should appear. The values are relative to line heights, which in turn depend on font size and Phrase lineHeight attributes.
  - for strikethrough text, use appropriately positioned overlines.
  - over/underline decoration style values, and the highlight style value, can be any valid CSS color String.
- §

  overlinePosition, overlineStyle

          overlinePosition: -0.1,
          overlineStyle: DEF_LINE_COLOR,
          overlineWidth: 1,
          noOverlineGlyphs: ZERO_STR,

- §

  underlinePosition, underlineStyle

          underlinePosition: 0.6,
          underlineStyle: DEF_LINE_COLOR,
          underlineWidth: 1,
          noUnderlineGlyphs: ZERO_STR,

- §

  highlightStyle

          highlightStyle: DEF_HIGHLIGHT,

- §

  ##### Bounding box

  The bounding box represents the Phrase entity’s collision detection hit area. It contains all of the entity’s text, including line spacing.

- §

  boundingBoxColor

          boundingBoxColor: HALFTRANS,

- §

  showBoundingBox - Boolean flag indicating whether the Phrase entity’s bounding box should be displayed

          showBoundingBox: false,

- §

  ##### Text along a path

  Phrase entitys, alongside other artefacts, can use a Shape entity as a reference path to determine its location in the canvas display - achieved by setting the `path`, `pathPosition`, etc attributes as required.

  - In this case, the Phrase’s text will appear as boxed text, with straight lines of text.

  We have an additional use case for Phrase text: to map each letter along the length of a Shape entity’s path. For this, we have specific `textPath`, `textPathPosition`, etc attributes.

- §

  textPath - Shape entity to be used as the text path; can be supplied either as the entity object itself, or as the entity’s name-String.

          textPath: ZERO_STR,

- §

  textPathPosition - float Number value between `0.0 - 1.0` representing the text’s first character’s position on the path.

          textPathPosition: 0,

- §

  textPathLoop - Boolean flag - when true (default) the first character’s position will loop back to `0` when it passes `1` (and vice versa).

          textPathLoop: true,

- §

  addTextPathRoll - Boolean flag - when true (default) each glyph in the text will rotate to match its position’s tangent on the path.

          addTextPathRoll: true,

- §

  textPathDirection - String with values `ltr` or `rtl` - affects how the text glyphs will arrange themselves along the path.

          textPathDirection: LTR,

- §

  treatWordAsGlyph - Boolean flag - when true, Scrawl-canvas will treat each word in the text as if it was a glyph/letter; default: false.

          treatWordAsGlyph: false,
      };
      P.defs = mergeOver(P.defs, defaultAttributes);

- §

  #### Packet management

      P.packetExclusions = pushUnique(P.packetExclusions, ['textPositions', 'textLines', 'textLineWidths', 'textLineWords', 'textGlyphs', 'textGlyphWidths', 'fontAttributes']);

      P.finalizePacketOut = function (copy, items) {

          const stateCopy = _parse(this.state.saveAsPacket(items))[3];
          copy = mergeOver(copy, stateCopy);

          const fontAttributesCopy = _parse(this.fontAttributes.saveAsPacket(items))[3];
          delete fontAttributesCopy.name;
          copy = mergeOver(copy, fontAttributesCopy);

          copy = this.handlePacketAnchor(copy, items);

          return copy;
      };

- §

  #### Clone management

- §

  #### Kill management

      P.factoryKill = function () {

          if (this.exposedTextHold) this.exposedTextHold.remove();
      };

- §

  #### Get, Set, deltaSet

      const G = P.getters,
          S = P.setters,
          D = P.deltaSetters;

- §

  handle

      S.handleX = function (coord) {

          if (coord != null) {

              this.handle[0] = coord;
              this.dirtyHandle = true;
              this.dirtyText = true;
              this.dirtyPathObject = true;
          }
      };
      S.handleY = function (coord) {

          if (coord != null) {

              this.handle[1] = coord;
              this.dirtyHandle = true;
              this.dirtyText = true;
              this.dirtyPathObject = true;
          }
      };
      S.handle = function (x, y) {

          this.setCoordinateHelper(HANDLE, x, y);
          this.dirtyHandle = true;
          this.dirtyText = true;
          this.dirtyPathObject = true;
      };
      D.handleX = function (coord) {

          const c = this.handle;
          c[0] = addStrings(c[0], coord);
          this.dirtyHandle = true;
          this.dirtyText = true;
          this.dirtyPathObject = true;
      };
      D.handleY = function (coord) {

          const c = this.handle;
          c[1] = addStrings(c[1], coord);
          this.dirtyHandle = true;
          this.dirtyText = true;
          this.dirtyPathObject = true;
      };
      D.handle = function (x, y) {

          this.setDeltaCoordinateHelper(HANDLE, x, y);
          this.dirtyHandle = true;
          this.dirtyText = true;
          this.dirtyPathObject = true;
      };

- §

  text

      G.text = function () {

          return this.currentText || this.text || ZERO_STR;
      };
      S.text = function (item) {

          this.text = ensureString(item);

          this.dirtyText = true;
          this.dirtyPathObject = true;
          this.dirtyDimensions = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  justify

      S.justify = function (item) {

          if (JUSTIFICATIONS.includes(item)) this.justify = item;

          this.dirtyText = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  width

      S.width = function (item) {

          this.dimensions[0] = item;

          this.dirtyDimensions = true;
          this.dirtyHandle = true;
          this.dirtyPathObject = true;
          this.dirtyText = true;
      };
      D.width = function (item) {

          const c = this.dimensions;
          c[0] = addStrings(c[0], item);

          this.dirtyDimensions = true;
          this.dirtyHandle = true;
          this.dirtyPathObject = true;
          this.dirtyText = true;
      };

- §

  scale

      S.scale = function (item) {

          this.scale = item;

          this.dirtyDimensions = true;
          this.dirtyHandle = true;
          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyScale = true;
      };
      D.scale = function (item) {

          this.scale += item;

          this.dirtyDimensions = true;
          this.dirtyHandle = true;
          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyScale = true;
      };

- §

  lineHeight

      S.lineHeight = function (item) {

          this.lineHeight = ensurePositiveFloat(item, 3);

- §

  TODO: text display, entity height and pathObject fall out of sync at small/negative values

  - This is a temporary fix

          if (this.lineHeight < 0.5) this.lineHeight = 0.5;

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };
      D.lineHeight = function (item) {

          this.lineHeight += ensureFloat(item, 3);

- §

  TODO: text display, entity height and pathObject fall out of sync at small/negative values

  - This is a temporary fix

          if (this.lineHeight < 0.5) this.lineHeight = 0.5;

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  letterSpacing

      S.letterSpacing = function (item) {

          this.letterSpacing = ensurePositiveFloat(item, 3);

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };
      D.letterSpacing = function (item) {

          this.letterSpacing += ensureFloat(item, 3);
          if (this.letterSpacing < 0) this.letterSpacing = 0;

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  overlinePosition

      S.overlinePosition = function (item) {

          this.overlinePosition = ensureFloat(item, 3);

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };
      D.overlinePosition = function (item) {

          this.overlinePosition += ensureFloat(item, 3);

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  noOverlineGlyphs

      S.noOverlineGlyphs = function (item) {

          if (item.substring) this.noOverlineGlyphs = item;
      };

- §

  underlinePosition

      S.underlinePosition = function (item) {

          this.underlinePosition = ensureFloat(item, 3);

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };
      D.underlinePosition = function (item) {

          this.underlinePosition += ensureFloat(item, 3);

          this.dirtyPathObject = true;
          this.dirtyText = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  noUnderlineGlyphs

      S.noUnderlineGlyphs = function (item) {

          if (item.substring) this.noUnderlineGlyphs = item;
      };

- §

  textPath

      S.textPath = function (item) {

          this.textPath = item;

          this.dirtyHandle = true;
          this.dirtyText = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  textPathPosition

      S.textPathPosition = function (item) {

          if (this.textPathLoop) {

              if (item < 0) item = _abs(item);
              if (item > 1) item = item % 1;
              this.textPathPosition = parseFloat(item.toFixed(6));
          }
          else this.textPathPosition = item;
          this.dirtyFilterIdentifier = true;
      };
      D.textPathPosition = function (item) {

          let newVal = this.textPathPosition + item;

          if (this.textPathLoop) {

              if (newVal < 0) newVal += 1;
              if (newVal > 1) newVal = newVal % 1;
              this.textPathPosition = parseFloat(newVal.toFixed(6));
          }
          else this.textPathPosition = newVal;
          this.dirtyFilterIdentifier = true;
      };

- §

  ##### FontAttribute attributes

  Phrase entitys break down fonts into their constituent parts using FontAttribute objects.

  - The Canvas API standards for using fonts on a canvas (`font`, `textAlign`, `textBaseline`) are near-useless, and often lead to a sub-par display of text.
  - Thus Phrase entitys hide these from the developer, instead giving them functions to get/set/update fonts which align more closely with CSS standards.
  - Note that the Canvas API only supports a subset of possible CSS font-related values, and that the level of support for even these will vary between browsers/devices. The Phrase entity will do work to ensure the font strings passed to the Canvas API CanvasRenderingContext2D engine will be valid (thus avoiding unnecessary runtime errors), but this may not be the same as a developer specifies in their code.
- §

  font - the desired CSS font (`get` returns the actual font String being used)

  - The font String is not retained. Rather we break it down into its constituent parts, and rebuild the font String when needed.

      G.font = function () {

          return this.fontAttributes.getFontString();
      };
      S.font = function (item) {

          this.fontAttributes.set({font: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  style - CSS `font-style` String

      G.style = function () {

          return this.fontAttributes.get(STYLE);
      };
      S.style = function (item) {

          this.fontAttributes.set({style: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  variant - CSS `font-variant` String

      G.variant = function () {

          return this.fontAttributes.get(VARIANT);
      };
      S.variant = function (item) {

          this.fontAttributes.set({variant: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  weight - CSS `font-weight` String

      G.weight = function () {

          return this.fontAttributes.get(WEIGHT);
      };
      S.weight = function (item) {

          this.fontAttributes.set({weight: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  stretch - CSS `font-stretch` String

      G.stretch = function () {

          return this.fontAttributes.get(STRETCH);
      };
      S.stretch = function (item) {

          this.fontAttributes.set({stretch: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  size - CSS `font-size` String

      G.size = function () {

          return this.fontAttributes.get(SIZE);
      };
      S.size = function (item) {

          this.fontAttributes.set({size: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  sizeValue - the Number part of the font’s size value

      G.sizeValue = function () {

          return this.fontAttributes.get(SIZE_VALUE);
      };
      S.sizeValue = function (item) {

          this.fontAttributes.set({sizeValue: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };
      D.sizeValue = function (item) {

          this.fontAttributes.deltaSet({sizeValue: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  sizeMetric - the String metric part of the font’s size value

      G.sizeMetric = function () {

          return this.fontAttributes.get(SIZE_METRIC);
      };
      S.sizeMetric = function (item) {

          this.fontAttributes.set({sizeMetric: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  family - CSS `font-family` String

      G.family = function () {

          return this.fontAttributes.get(FAMILY);
      };
      S.family = function (item) {

          this.fontAttributes.set({family: item});

          this.dirtyFont = true;
          this.dirtyPathObject = true;
          this.dirtyFilterIdentifier = true;
      };

- §

  #### Prototype functions

- §

  `cleanDimensionsAdditionalActions` - local overwrite

      P.cleanDimensionsAdditionalActions = function () {

          this.fontAttributes.dirtyFont = true;
          this.fontAttributes.updateMetadata(this.scale, this.lineHeight, this.getHost());

          if (this.dimensions[0] == AUTO) {

              this.buildText();

              const myCell = requestCell(),
                  engine = myCell.engine;

              engine.font = this.fontAttributes.getFontString();

              this.currentDimensions[0] = _ceil(engine.measureText(this.currentText).width / this.scale);

              releaseCell(myCell);
          }

          if (this.textLines) this.currentDimensions[1] = _ceil((this.textHeight * this.textLines.length * this.lineHeight) / this.scale);
          else this.dirtyDimensions = true;
      };

- §

  `setSectionStyles` - internal function

      P.setSectionStyles = function (text) {

          const search = new RegExp(this.sectionClassMarker),
              parseArray = text.split(search),
              styles = this.sectionStyles,
              classes = sectionClasses;

          let parsedText = [],
              classObj, index, styleObj;

          styles.length = 0;

          parseArray.forEach(item => {

              classObj = classes[item];

              if (classObj) {

                  index = parsedText.length;
                  styleObj = styles[index];

                  if (!styleObj) styles[index] = _assign({}, classObj);
                  else _assign(styleObj, classObj);
              }
              else if (xt(item)) parsedText.push(...item);
          });
          return parsedText.join('');
      };

- §

  `addSectionClass`, `removeSectionClass` - add and remove section class definitions to the entity’s `sectionClasses` object.

  WARNING: the SectionClass object has been moved to the library, rather than created individually on each Phrase entity. This means that changes to the object will affect all phrases that make use of that class. Namespacing new classes is strongly recommended!

      P.addSectionClass = function (label, obj) {

          if (xta(label, obj) && label.substring && isa_obj(obj)) {

- §

  this.sectionClasses[label] = obj;

              sectionClasses[label] = obj;
          }
          this.dirtyText = true;
          this.dirtyPathObject = true;

          return this;
      };

      P.removeSectionClass = function (label) {

- §

  delete this.sectionClasses[label];

          delete sectionClasses[label];

          this.dirtyText = true;
          this.dirtyPathObject = true;

          return this;
      };

- §

  `getTextPath` - internal function

      P.getTextPath = function () {

          let path = this.textPath;

          if (path && path.substring) {

              path = this.textPath = artefact[this.textPath];

              if (path.type == T_SHAPE && path.useAsPath) path.pathed.push(this.name);
              else {

                  path = this.path = false;
              }
          }

          return path;
      };

- §

  #### Display cycle functionality

  Phrase entitys, because they handle graphical text which has its own special requirements and methods in the Canvas API, has to overwrite a substantial portion of the Display cycle functionality defined in the entity mixin.

- §

  `cleanPathObject` - overwrites mixin/entity.js functionality so that it can deal with the `dirtyFont`, `dirtyText` and `dirtyHandle` flags

  - Fonts don’t have accessible paths; Phrase entity pathObjects represent the bounding box around the entity’s text.

      P.cleanPathObject = function () {

          this.dirtyPathObject = false;

          if (!this.noPathUpdates || !this.pathObject) {

              if (this.dirtyFont && this.fontAttributes) {

                  this.dirtyFont = false;
                  this.dirtyText = true;
                  this.dirtyMimicDimensions = true;
                  this.dirtyPositionSubscribers = true;
              }
              if (this.dirtyText) this.buildText();

              if (this.dirtyHandle) this.cleanHandle();

              const p = this.pathObject = new Path2D();

              const handle = this.currentHandle,
                  dims = this.currentDimensions,

                  scale = this.currentScale,
                  x = -handle[0] * scale,
                  y = -handle[1] * scale,
                  w = dims[0] * scale,
                  h = dims[1] * scale;

              this.boxStartValues = [x, y];

              p.rect(x, y, w, h);
          }
      };

- §

  `buildText` - internal function called by `cleanPathObject`

      P.buildText = function () {

          this.dirtyText = false;

          let t = this.convertTextEntityCharacters(this.text);
          t = this.setSectionStyles(t);
          this.currentText = t;

          if (isNaN(this.currentDimensions[0])) this.dirtyText = true;
          else {

              this.calculateTextPositions(t);

              if (this.exposeText) {

                  if (!this.exposedTextHold) {

                      const myhold = document.createElement(DIV);
                      myhold.id = `${this.name}-text-hold`;
                      myhold.setAttribute(ARIA_LIVE, POLITE);
                      this.exposedTextHold = myhold;
                      this.exposedTextHoldAttached = false;
                  }

                  this.exposedTextHold.textContent = t;

                  if (!this.exposedTextHoldAttached) {

                      if (this.currentHost) {

                          const hold = this.getCanvasTextHold(this.currentHost);

                          if (hold && hold.textHold) {

                              hold.textHold.appendChild(this.exposedTextHold);
                              this.exposedTextHoldAttached = true;
                          }
                      }
                  }
              }
          }
      };

      P.getCanvasTextHold = function (item) {

          if (item && item.type == T_CELL && item.controller && item.controller.type == T_CANVAS && item.controller.textHold) return item.controller;

          if (item && item.type == T_CELL && item.currentHost) return this.getCanvasTextHold(item.currentHost);

          return false;
      };

- §

  `convertTextEntityCharacters` - internal function called by `buildText`

  - To convert any HTML entity (eg: < ε) in the text string into their required glyphs
  - Also removes excessive white space

      P.convertTextEntityCharacters = function (item) {

          let mytext = item.trim();
          mytext = mytext.replace(CLASS_REGEX, SPACE);
          textEntityConverter.innerHTML = mytext;
          mytext = textEntityConverter.value;
          return mytext;
      };

- §

  `calculateTextPositions` - internal function called by `buildText`

  - If you are not a fan of big, complex functions … look away now!

      P.calculateTextPositions = function (mytext) {

- §
  0. `strokeStyle` and `fillStyle` helper function

          const makeStyle = function (art) {

              if (!host) {

                  self.dirtyPathObject = true;
                  self.dirtyText = true;
                  return BLACK;
              }

              if (art.substring) {

                  let brokenStyle = false;

                  if (stylesnames.includes(art)) brokenStyle = styles[art];
                  else if (cellnames.includes(art)) brokenStyle = cell[art];

                  if (brokenStyle) return brokenStyle;
              }
              return art;
          };

- §
  1. Setup - get values for text? arrays, current?, highlight?, ?Attributes, etc

          const myCell = requestCell(),
              engine = myCell.engine;

          const self = this,
              host = (this.group && this.group.getHost) ? this.group.getHost() : false;

          const textGlyphWidths = requestArray(),
              textLines = requestArray(),
              textLineWidths = requestArray(),
              textLineWords = requestArray(),
              textPositions = requestArray();

          let gStyle, gPos, item,
              starts, ends, cursor, height,
              space, i, iz, j, jz;

          const singles = requestArray(),
              pairs = requestArray(),
              path = this.getTextPath();

          let fragment, len, glyphArr, glyph, nextGlyph, glyphWidth, lineLen, totalLen;

          const fontAttributes = this.fontAttributes,
              glyphAttributes = fontAttributes.clone({});

          const sectionStyles = this.sectionStyles;

          const state = this.state,
              fontLibrary = {},
              fontArray = requestArray();

          let scale = this.currentScale;

          const dims = this.currentDimensions,
              width = dims[0] * scale,
              treatWordAsGlyph = this.treatWordAsGlyph,
              lineHeight = this.lineHeight,
              justify = this.justify;

          fontAttributes.updateMetadata(scale, lineHeight, host);
          glyphAttributes.updateMetadata(scale, lineHeight, host);

          const defaultFont = fontAttributes.getFontString(),
              defaultFillStyle = makeStyle(state.fillStyle),
              defaultStrokeStyle = makeStyle(state.strokeStyle),
              defaultSpace = this.letterSpacing * scale;

          let currentFont = defaultFont,
              currentFillStyle = defaultFillStyle,
              currentStrokeStyle = defaultStrokeStyle,
              currentSpace = defaultSpace;

          if (this.highlightStyle) makeStyle(this.highlightStyle);
          if (this.underlineStyle) makeStyle(this.underlineStyle);
          if (this.overlineStyle) makeStyle(this.overlineStyle);

          let highlightFlag = false,
              underlineFlag = false,
              overlineFlag = false,
              maxHeight = 0;

- §
  2. Create `textGlyphs` array
  - also shove the default font into the `fontLibrary` array

          const textGlyphs = requestArray();
          if (treatWordAsGlyph) textGlyphs.push(...mytext.join('').split(SPACE));
          else textGlyphs.push(...mytext);

          fontArray.push(currentFont);

- §
  3. `textPositions` array will include an array of data for each glyph
  - `[font, strokeStyle, fillStyle, highlight, underline, overline, text, startX, startY, (pathData)]`
  - and populate `spacesArray` with space position data (for full justify calculations later)

          for (i = 0, iz = textGlyphs.length; i < iz; i++) {

              item = textGlyphs[i];

      /* eslint-disable-next-line */
              textPositions[i] = [, , , , , , item, 0, 0, 0];
          }

- §
  4. Process the `sectionStyles` array to start populating the `textPositions` arrays

          if (!sectionStyles[0]) sectionStyles[0] = {
              family: glyphAttributes.family,
              size: (glyphAttributes.sizeValue) ? `${glyphAttributes.sizeValue}${glyphAttributes.sizeMetric}` : glyphAttributes.sizeMetric,
              stretch: glyphAttributes.stretch,
              style: glyphAttributes.style,
              variant: glyphAttributes.variant,
              weight: glyphAttributes.weight,
          };

          for (i = 0, iz = sectionStyles.length; i < iz; i++) {

              gStyle = sectionStyles[i];

              if (gStyle) {

                  gPos = textPositions[i];

                  if (gPos) {

                      if (i === 0) {
                          gPos[0] = currentFont;
                          gPos[3] = highlightFlag;
                          gPos[4] = underlineFlag;
                          gPos[5] = overlineFlag;
                      }

                      if (gStyle.defaults) {
                          currentFont = glyphAttributes.update(fontAttributes);
                          currentStrokeStyle = defaultStrokeStyle;
                          currentFillStyle = defaultFillStyle;
                          currentSpace = defaultSpace;
                          gPos[0] = currentFont;
                          gPos[1] = currentStrokeStyle;
                          gPos[2] = currentFillStyle;
                          gPos[3] = highlightFlag;
                          gPos[4] = underlineFlag;
                          gPos[5] = overlineFlag;
                      }

                      item = gStyle.stroke;
                      if (item && item !== currentStrokeStyle) {

                          if (DEFAULT == item) currentStrokeStyle = defaultStrokeStyle;
                          else currentStrokeStyle = makeStyle(gStyle.stroke);
                          gPos[1] = currentStrokeStyle;
                      }

                      item = gStyle.fill;
                      if (item && item !== currentFillStyle) {

                          if (DEFAULT == item) currentFillStyle = defaultFillStyle;
                          else currentFillStyle = makeStyle(gStyle.fill);
                          gPos[2] = currentFillStyle;
                      }

                      item = gStyle.space;
                      if (xt(item) && item !== currentSpace) currentSpace = item * scale

                      item = gStyle.highlight;
                      if (xt(item) && item !== highlightFlag) {

                          highlightFlag = item;
                          gPos[3] = highlightFlag;
                      }

                      item = gStyle.underline;
                      if (xt(item) && item !== underlineFlag) {

                          underlineFlag = item;
                          gPos[4] = underlineFlag;
                      }

                      item = gStyle.overline;
                      if (xt(item) && item !== overlineFlag) {

                          overlineFlag = item;
                          gPos[5] = overlineFlag;
                      }

                      if (i !== 0 && (gStyle.variant || gStyle.weight || gStyle.style || gStyle.stretch || gStyle.size || gStyle.sizeValue || gStyle.sizeMetric || gStyle.family || gStyle.font)) {

                          item = glyphAttributes.update(gStyle);
                          if (item !== currentFont) {

                              currentFont = item;
                              gPos[0] = currentFont;

                              if (!fontArray.includes(currentFont)) fontArray.push(currentFont);
                          }
                      }
                  }
              }

- §

  Setup `textGlyphWidths` array, populating it with current letterSpacing values

              textGlyphWidths[i] = currentSpace;
          }

- §

  Finish populating `textGlyphWidths`

          for (i = 0, iz = textGlyphs.length; i < iz; i++) {

              if (xt(textGlyphWidths[i])) currentSpace = textGlyphWidths[i];

              textGlyphWidths[i] = currentSpace;
          }

- §
  5. Calculate the text `height` value
  - All lines in a multiline Phrase will use the maximum text height value, even if they don’t include the biggest value

          engine.save();
          fontArray.forEach(font => {

              engine.font = font;

              const f = engine.measureText(defaultTextForHeightMeasurements);

              if (f.actualBoundingBoxAscent != null && f.actualBoundingBoxDescent != null) {

                  fontLibrary[font] = _ceil(f.actualBoundingBoxAscent + f.actualBoundingBoxDescent);
              }
              else {

- §

  browsers differ in the value thy return for this measurement

                  fontHeightCalculator.style.font = font;
                  item = fontHeightCalculator.clientHeight;
                  fontLibrary[font] = item;
              }
          });
          engine.restore();

          maxHeight = _max(..._values(fontLibrary));

- §
  6. Calculate glyph `width` values
  - This is the tricky bit as, ideally, we need to take into account font kerning values
  - However kerning values go out of the window when font attributes (especially size) change in mid-text
  - And we need to remember that `letterSpacing` can also be different in different parts of the text
  - This is also the best place to populate the `textLine` arrays

          totalLen = lineLen = starts = ends = 0;

          for (i = 0, iz = textPositions.length; i < iz; i++) {

              glyphArr = textPositions[i];
              glyph = glyphArr[6];

              if (glyphArr[0]) engine.font = glyphArr[0];

              singles.push(engine.measureText(glyph).width);

              glyphArr = textPositions[i + 1];
              nextGlyph = (!treatWordAsGlyph && glyphArr) ? glyphArr[6] : false;

              len = (nextGlyph) ? engine.measureText(`${glyph}${nextGlyph}`).width : false;
              pairs.push(len);
          }

          for (i = 0, iz = pairs.length; i < iz; i++) {

              glyph = pairs[i];

              if (glyph) {

                  len = singles[i] + singles[i + 1];
                  gPos = textPositions[i + 1];

                  if (len > glyph && !gPos[0]) {

                      singles[i] -= (len - glyph);
                  }
              }
          }

- §

  Calculate text line arrays

          for (i = 0, iz = textPositions.length; i < iz; i++) {

              glyphArr = textPositions[i];
              glyph = glyphArr[6];

              glyphWidth = singles[i] + textGlyphWidths[i];
              textGlyphWidths[i] = glyphWidth;

              if (treatWordAsGlyph || glyph == SPACE) ends = i;

              lineLen += glyphWidth;
              totalLen += glyphWidth;

- §

  Need `starts` to be less than `ends`

  - This should make sure we pick up individual words that are longer than the Phrase entity’s width

              if (lineLen >= width && starts < ends) {

                  fragment = textGlyphs.slice(starts, ends).join(ZERO_STR);
                  textLines.push(fragment);
                  len = (treatWordAsGlyph) ? fragment.split(SPACE).length - 1 : fragment.split(SPACE).length;
                  textLineWords.push(len);

                  len = textGlyphWidths.slice(starts, ends).reduce((a, v) => a + v, 0);
                  textLineWidths.push(len);

                  lineLen -= len;
                  starts = ends + 1;
              }

- §

  Need to pick up the last (or only) line

              if (i + 1 == iz) {

- §

  Pick up single line

                  if (lineLen == totalLen) {

                      fragment = mytext;

                      textLines.push(fragment);
                      textLineWords.push((treatWordAsGlyph) ? fragment.split(SPACE).length - 1 : fragment.split(SPACE).length);
                      textLineWidths.push(totalLen);
                  }

- §

  Final line of multiline text

                  else {

                      fragment = textGlyphs.slice(starts).join(ZERO_STR);
                      textLines.push(fragment);
                      len = (treatWordAsGlyph) ? fragment.split(SPACE).length - 1 : fragment.split(SPACE).length;
                      textLineWords.push(len);

                      len = textGlyphWidths.slice(starts).reduce((a, v) => a + v, 0);
                      textLineWidths.push(len);
                  }
              }

- §

  … And complete the population of data for `highlight`, `overline`, `underline`

              if (xt(glyphArr[3])) highlightFlag = glyphArr[3];
              if (xt(glyphArr[4])) underlineFlag = glyphArr[4];
              if (xt(glyphArr[5])) overlineFlag = glyphArr[5];

              glyphArr[3] = highlightFlag;
              glyphArr[4] = underlineFlag;
              glyphArr[5] = overlineFlag;
          }

- §
  7. Calculate `localHeight`

          if (scale <= 0) scale = 1;

          dims[1] = _ceil((maxHeight * textLines.length * lineHeight) / scale);

          this.cleanHandle();
          this.dirtyHandle = false;

          const handle = this.currentHandle,
              handleX = -handle[0] * scale,
              handleY = -handle[1] * scale;

- §

  Handle path positioning (which we’ll assume will need to be done for every display cycle) separately during stamping

          if (!path) {

- §
  8. We should now be in a position where we can calculate each glyph’s `startXY` values
  - We have 2 non-path scenarios: full-justified text; and regular text
- §

  Scenario 1: `justify === 'full'`

              if (justify == FULL) {

                  cursor = 0;
                  height = handleY;

                  for (i = 0, iz = textLineWidths.length; i < iz; i++) {

                      len = handleX;

                      if (i < iz - 1 && textLineWords[i] > 1) space = (width - textLineWidths[i]) / (textLineWords[i] - 1);
                      else space = 0;

                      for (j = 0, jz = [...textLines[i]].length; j < jz; j++) {

                          item = textPositions[cursor];

                          if (item[6] == SPACE) textGlyphWidths[cursor] += space;

                          item[7] = _floor(len);
                          item[8] = _floor(height);
                          item[9] = textGlyphWidths[cursor];

                          len += textGlyphWidths[cursor];

                          cursor++;
                      }

                      cursor++;
                      height += (maxHeight * lineHeight);
                  }
              }

- §

  Scenario 2: regular text - `justify === 'left'`, or `'centre'`, or `'right'`

              else {

                  cursor = 0;
                  height = handleY;

                  for (i = 0, iz = textLineWidths.length; i < iz; i++) {

                      if (justify == RIGHT) len = (width - textLineWidths[i]) + handleX;
                      else if (justify == CENTER) len = ((width - textLineWidths[i]) / 2) + handleX;
                      else len = handleX;

                      for (j = 0, jz = [...textLines[i]].length; j < jz; j++) {

                          item = textPositions[cursor];

- §

  BUG: There’s an issue here which causes the function to fail when `treatWordAsGlyph` flag is set to true. Affects non-path-referencing Phrase entitys. This test to see if item exists is a temporary fix.

  - Question: do we only care about treating word as glyph when it references a path? Probably no - we need to care about attempts to add space between letters (glyphs) as that may have an unwanted effect on heavily kerned fonts, or fonts with a lot of ligatures between various glyphs.

                          if (item) {

                              item[7] = _floor(len);
                              item[8] = _floor(height);
                              item[9] = textGlyphWidths[cursor];
                          }
                          len += textGlyphWidths[cursor];
                          cursor++;
                      }

                      cursor++;
                      height += (maxHeight * lineHeight);
                  }
              }
          }

- §
  9. Clean up and exit

          if (!this.textGlyphs) this.textGlyphs = [];
          this.textGlyphs.length = 0;
          this.textGlyphs.push(...textGlyphs);
          releaseArray(textGlyphs);

          if (!this.textGlyphWidths) this.textGlyphWidths = [];
          this.textGlyphWidths.length = 0;
          this.textGlyphWidths.push(...textGlyphWidths);
          releaseArray(textGlyphWidths);

          if (!this.textLines) this.textLines = [];
          this.textLines.length = 0;
          this.textLines.push(...textLines);
          releaseArray(textLines);

          if (!this.textLineWidths) this.textLineWidths = [];
          this.textLineWidths.length = 0;
          this.textLineWidths.push(...textLineWidths);
          releaseArray(textLineWidths);

          if (!this.textLineWords) this.textLineWords = [];
          this.textLineWords.length = 0;
          this.textLineWords.push(...textLineWords);
          releaseArray(textLineWords);

          if (!this.textPositions) this.textPositions = [];
          this.textPositions.length = 0;
          this.textPositions.push(...textPositions);
          releaseArray(textPositions);

          this.textHeight = maxHeight;
          this.textLength = totalLen;

          this.fontLibrary = fontLibrary;

          releaseArray(fontArray);
          releaseArray(singles);
          releaseArray(pairs);

          releaseCell(myCell);
      };

- §

  ##### Stamping the entity onto a Cell wrapper <canvas> element

- §

  `regularStamp` - overwrites the mixin/entity.js function

      P.regularStamp = function () {

          if (this.currentHost) {

              const { currentHost, method, preStamper, stamper, addTextPathRoll, currentHandle } = this;

              const engine = currentHost.engine;

              let i, iz, pos, data;

              if (method == NONE) this.performRotation(engine);

- §

  Scrawl-canvas clips canvases to the Phrase’s hit area

  - To ‘clip’ to the text, use stamp order and globalCompositeOperation instead

              else if (method == CLIP) {

                  this.performRotation(engine);
                  engine.clip(this.pathObject, this.winding);
              }

              else if (this.textPath) {

                  if (!this.noCanvasEngineUpdates) currentHost.setEngine(this);

- §

  Needs investigating: for some reason when applying a filter to a phrase entity the pool cell gets its baseline reset to default, which displaces the filter effect upwards. These lines fix the immediate issue, but don’t solve the deeper mystery.

                  engine.textBaseline = TOP;
                  engine.textAlign = LEFT;

                  this.getTextPath();
                  this.calculateGlyphPathPositions();

                  pos = this.textPositions;

                  let p, pathData;

                  const cr = this.currentRotation;

- §

  TODO: is this still needed?

                  const aPR = this.addPathRotation;
                  this.addPathRotation = addTextPathRoll;

                  for (i = 0, iz = pos.length; i < iz; i++) {

                      p = pos[i];
                      pathData = p[10];

                      if (pathData) {

                          this.currentPathData = pathData;
                          if (addTextPathRoll) this.currentRotation = pathData.angle;

                          currentHost.rotateDestination(engine, pathData.x, pathData.y, this);

                          engine.translate(-currentHandle[0], -currentHandle[1]);

                          data = preStamper(currentHost, engine, this, p);
                          stamper[method](engine, this, data);
                      }
                  }

- §

  TODO: is this still needed?

                  this.addPathRotation = aPR;

                  this.currentRotation = cr;
              }
              else {

                  this.performRotation(engine);

                  if (!this.noCanvasEngineUpdates) currentHost.setEngine(this);

- §

  … See above

                  engine.textBaseline = TOP;
                  engine.textAlign = LEFT;

                  pos = this.textPositions;

                  for (i = 0, iz = pos.length; i < iz; i++) {

                      data = preStamper(currentHost, engine, this, pos[i]);
                      stamper[method](engine, this, data);
                  }
                  if (this.showBoundingBox) this.drawBoundingBox(engine);
              }
          }
      };

- §

  `calculateGlyphPathPositions` - internal helper function called by `regularStamp`

      P.calculateGlyphPathPositions = function () {

          const path = this.getTextPath(),
              len = path.length,
              textPos = this.textPositions,
              widths = this.textGlyphWidths,
              direction = (this.textPathDirection == LTR) ? true : false,
              justify = this.justify,
              loop = this.textPathLoop,
              pathSpeed = this.constantPathSpeed;

          let posArray, i, iz, width,
              pathPos = this.textPathPosition,
              localPathPos;

          for (i = 0, iz = textPos.length; i < iz; i++) {

- §

  textPathPosition Array indexes [ 0-font - font definition, or null 1-strokeStyle - Boolean 2-fillStyle - Boolean 3-highlight - Boolean 4-underline - Boolean 5-overline - Boolean 6-text - String 7-startX - Number - on paths, acts as a negative offset for justifying glyphs 8-startY - Number 9-pathData - Object {x: Number (px), y: Number (px), angle: Number (degrees)} ]

              posArray = textPos[i];
              width = widths[i];

              switch (justify) {

                  case CENTER :
                      localPathPos = pathPos + ((width / 2) / len);
                      posArray[7] = -width / 2;
                      break;

                  case RIGHT :
                      localPathPos = pathPos + (width / len);
                      posArray[7] = -width;
                      break;

                  default :
                      localPathPos = pathPos;
              }

              if (loop && (localPathPos > 1 || localPathPos < 0)) {

                  localPathPos = (localPathPos > 0.5) ? localPathPos - 1 : localPathPos + 1;
              }

              posArray[10] = (localPathPos <= 1 && localPathPos >= 0) ?
                  path.getPathPositionData(localPathPos, pathSpeed) :
                  false;

              posArray[9] = width;

              if (direction) pathPos += (width / len);
              else pathPos -= (width / len);

              if (loop && (pathPos > 1 || pathPos < 0)) {

                  pathPos = (pathPos > 0.5) ? pathPos - 1 : pathPos + 1;
              }
          }
      };

- §

  `preStamper` - internal helper function called by `regularStamp`

      P.preStamper = function (dest, engine, entity, args) {

          const makeStyle = function (s) {

              if (s.getData) return s.getData(entity, dest);

              return s;
          };

          const [font, strokeStyle, fillStyle, highlight, underline, overline, ...data] = args;

          if (font) engine.font = font;

          if (highlight || underline || overline) {

              const { highlightStyle, textHeight, underlineStyle, underlineWidth, underlinePosition, noUnderlineGlyphs, overlineStyle, overlineWidth, overlinePosition } = entity;

              engine.save();

- §

  data[0] - glyph data[1] - xpos data[2] - ypos data[3] - width

              if (highlight) {

                  engine.fillStyle = makeStyle(highlightStyle);
                  engine.fillRect(data[1], data[2], data[3], textHeight);
              }

              if (underline && !noUnderlineGlyphs.includes(data[0])) {

                  engine.fillStyle = makeStyle(underlineStyle);
                  engine.fillRect(data[1], data[2] + (textHeight * underlinePosition), data[3], underlineWidth);
              }

              if (overline) {

                  engine.fillStyle = makeStyle(overlineStyle);
                  engine.fillRect(data[1], data[2] + (textHeight * overlinePosition), data[3], overlineWidth);
              }
              engine.restore();
          }

          if (strokeStyle) engine.strokeStyle = makeStyle(strokeStyle);
          if (fillStyle) engine.fillStyle = makeStyle(fillStyle);

          return data;
      };

- §

  `stamper` - object holding stamp method functions - functions called by `regularStamp`

      P.stamper = {

- §

  `stamper.draw`

          draw: function (engine, entity, data) {

              engine.strokeText(...data);
          },

- §

  `stamper.fill`

          fill: function (engine, entity, data) {

              engine.fillText(...data);
          },

- §

  `stamper.drawAndFill`

          drawAndFill: function (engine, entity, data) {

              engine.strokeText(...data);
              engine.fillText(...data);
              entity.currentHost.clearShadow();
              engine.strokeText(...data);
              engine.fillText(...data);
              entity.currentHost.restoreShadow(entity);
          },

- §

  `stamper.fillAndDraw`

          fillAndDraw: function (engine, entity, data) {

              engine.fillText(...data);
              engine.strokeText(...data);
              entity.currentHost.clearShadow();
              engine.fillText(...data);
              engine.strokeText(...data);
              entity.currentHost.restoreShadow(entity);
          },

- §

  `stamper.drawThenFill`

          drawThenFill: function (engine, entity, data) {

              engine.strokeText(...data);
              engine.fillText(...data);
          },

- §

  `stamper.fillThenDraw`

          fillThenDraw: function (engine, entity, data) {

              engine.fillText(...data);
              engine.strokeText(...data);
          },

- §

  `stamper.clear`

          clear: function (engine, entity, data) {

              const gco = engine.globalCompositeOperation;
              engine.globalCompositeOperation = DESTINATION_OUT;
              engine.fillText(...data);
              engine.globalCompositeOperation = gco;
          },
      };

- §

  `drawBoundingBox` - internal helper function called by `regularStamp`

      P.drawBoundingBox = function (engine) {

          engine.save();
          engine.strokeStyle = this.boundingBoxColor;
          engine.lineWidth = 1;
          engine.globalCompositeOperation = SOURCE_OVER;
          engine.globalAlpha = 1;
          engine.shadowOffsetX = 0;
          engine.shadowOffsetY = 0;
          engine.shadowBlur = 0;
          engine.stroke(this.pathObject);
          engine.restore();
      };

- §

  `performRotation` - internal helper function called by `regularStamp`

  - When doing text along a path, we have to perform a rendering context transformation for every glyph
  - In other cases, we perform the action on a per-line basis

      P.performRotation = function (engine) {

          const dest = this.currentHost;

          if (dest) {

              const [x, y] = this.currentStampPosition;

              dest.rotateDestination(engine, x, y, this);
          }
      };

- §

  #### Factory

      scrawl.makePhrase({

          name: 'myphrase_fill',

          text: 'H&epsilon;lj&ouml;!',
          font: 'bold 40px Garamond, serif',

          width: 120,

          startX: '14%',
          startY: '28%',
          handleX: 'center',
          handleY: 'center',

          justify: 'center',
          lineHeight: 1,

          fillStyle: 'green',
          strokeStyle: 'gold',

          lineWidth: 2,
          lineJoin: 'round',

          shadowOffsetX: 2,
          shadowOffsetY: 2,
          shadowBlur: 2,
          shadowColor: 'black',

      }).clone({

          name: 'myphrase_draw',
          startX: '38%',
          method: 'draw',
      });

      export const makePhrase = function (items) {

          if (!items) return false;
          return new Phrase(items);
      };

      constructors.Phrase = Phrase;