• Jump To … +
    ./source/asset-management/image-asset.js ./source/asset-management/noise-asset.js ./source/asset-management/raw-asset.js ./source/asset-management/reaction-diffusion-asset.js ./source/asset-management/sprite-asset.js ./source/asset-management/video-asset.js ./source/core/animation-loop.js ./source/core/display-cycle.js ./source/core/document.js ./source/core/events.js ./source/core/init.js ./source/core/library.js ./source/core/snippets.js ./source/core/user-interaction.js ./source/factory/action.js ./source/factory/anchor.js ./source/factory/animation.js ./source/factory/bezier.js ./source/factory/block.js ./source/factory/button.js ./source/factory/canvas.js ./source/factory/cell.js ./source/factory/cog.js ./source/factory/color.js ./source/factory/conic-gradient.js ./source/factory/crescent.js ./source/factory/element.js ./source/factory/emitter.js ./source/factory/enhanced-label.js ./source/factory/filter.js ./source/factory/gradient.js ./source/factory/grid.js ./source/factory/group.js ./source/factory/label.js ./source/factory/line-spiral.js ./source/factory/line.js ./source/factory/loom.js ./source/factory/mesh.js ./source/factory/net.js ./source/factory/oval.js ./source/factory/particle-force.js ./source/factory/particle-spring.js ./source/factory/particle-world.js ./source/factory/particle.js ./source/factory/pattern.js ./source/factory/picture.js ./source/factory/polygon.js ./source/factory/polyline.js ./source/factory/quadratic.js ./source/factory/radial-gradient.js ./source/factory/rectangle.js ./source/factory/render-animation.js ./source/factory/shape.js ./source/factory/spiral.js ./source/factory/stack.js ./source/factory/star.js ./source/factory/tetragon.js ./source/factory/ticker.js ./source/factory/tracer.js ./source/factory/tween.js ./source/factory/unstacked-element.js ./source/factory/wheel.js ./source/helper/array-pool.js ./source/helper/color-engine.js ./source/helper/document-root-elements.js ./source/helper/filter-engine-bluenoise-data.js ./source/helper/filter-engine.js ./source/helper/random-seed.js ./source/helper/shape-path-calculation.js ./source/helper/shared-vars.js ./source/helper/system-flags.js ./source/helper/utilities.js ./source/helper/workstore.js ./source/mixin/anchor.js ./source/mixin/asset-advanced-functionality.js ./source/mixin/asset-consumer.js ./source/mixin/asset.js ./source/mixin/base.js ./source/mixin/button.js ./source/mixin/cascade.js ./source/mixin/cell-key-functions.js ./source/mixin/delta.js ./source/mixin/display-shape.js ./source/mixin/dom.js ./source/mixin/entity.js ./source/mixin/filter.js ./source/mixin/hidden-dom-elements.js ./source/mixin/mimic.js ./source/mixin/path.js ./source/mixin/pattern.js ./source/mixin/pivot.js ./source/mixin/position.js ./source/mixin/shape-basic.js ./source/mixin/shape-curve.js ./source/mixin/styles.js ./source/mixin/text.js ./source/mixin/tween.js ./source/scrawl.js ./source/untracked-factory/cell-fragment.js ./source/untracked-factory/coordinate.js ./source/untracked-factory/drag-zone.js ./source/untracked-factory/keyboard-zone.js ./source/untracked-factory/observe-update.js ./source/untracked-factory/palette.js ./source/untracked-factory/particle-history.js ./source/untracked-factory/quaternion.js ./source/untracked-factory/state.js ./source/untracked-factory/text-style.js ./source/untracked-factory/vector.js
  • §

    Loom factory

    A Loom offers functionality to render an image onto a <canvas> element, where the image is not a rectangle - it can have curved borders. It can also offer the illusion of flat 3D images in the canvas, giving them perspective.

    Loom entitys are composite entitys - an entity that relies on other entitys for its basic functionality.

    • Every Loom object requires two (or one) path-enabled Shape entitys to act as its left and right tracks.
    • A Loom entity also requires a Picture entity to act as its image source.
  • §

    Imports

    import { artefact, constructors, group } from '../core/library.js';
    
    import { addStrings, doCreate, mergeDiscard, mergeOver, pushUnique, removeItem, xta, λnull, λcloneError, Ωempty } from '../helper/utilities.js';
    
    import { currentCorePosition } from '../core/user-interaction.js';
    
    import { makeState } from '../untracked-factory/state.js';
    
    import { releaseCell, requestCell } from '../untracked-factory/cell-fragment.js';
    
    import { currentGroup } from './canvas.js';
    
    import baseMix from '../mixin/base.js';
    import deltaMix from '../mixin/delta.js';
    import hiddenElementsMix from '../mixin/hidden-dom-elements.js';
    import anchorMix from '../mixin/anchor.js';
    import buttonMix from '../mixin/button.js';
  • §

    Shared constants

    import { _atan2, _ceil, _cos, _floor, _hypot, _isArray, _isFinite, _keys, _max, _min, _parse, _piHalf, _sin, BLACK, DESTINATION_OUT, ENTITY, FILL, GOOD_HOST, NAME, SOURCE_OVER, STATE_KEYS, T_GROUP, T_PICTURE, UNDEF, ZERO_STR } from '../helper/shared-vars.js';
  • §

    Local constants

    const T_LOOM = 'Loom';
  • §

    Loom constructor

    const Loom = function (items = Ωempty) {
    
        this.makeName(items.name);
        this.register();
    
        this.state = makeState(Ωempty);
    
        this.modifyConstructorInputForAnchorButton(items);
    
        this.set(this.defs);
    
        if (!items.group) items.group = currentGroup;
    
        this.onEnter = λnull;
        this.onLeave = λnull;
        this.onDown = λnull;
        this.onUp = λnull;
    
        this.delta = {};
    
        this.boundingBox = [];
        this.currentHost = null;
        this.currentPathData = null;
        this.dirtyDimensions = true;
        this.dirtyHost = true;
        this.dirtyInput = true;
        this.dirtyOutput = true;
        this.dirtyTargetImage = true;
        this.fromPathSteps = 1;
        this.output = null;
        this.pathTests = null;
        this.sourceDimension = 0;
        this.sourceImageData = null;
        this.toPathSteps = 1;
        this.watchFromPath = true;
    
        this.set(items);
    
        this.fromPathData = [];
        this.toPathData = [];
    
        this.watchIndex = -1;
        this.engineInstructions = [];
        this.engineDeltaLengths = [];
    
        return this;
    };
  • §

    Loom prototype

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

    Mixins

    baseMix(P);
    deltaMix(P);
    hiddenElementsMix(P);
    anchorMix(P);
    buttonMix(P);
  • §

    Loom attributes

    const defaultAttributes = {
  • §

    fromPath, toPath - A Loom entity uses 2 Shape paths to construct a frame between which the image will be redrawn. These attributes can be set using the Shapes’ name-String, or the Shape objects themselves

    • The positioning, scaling etc of each strut is set in the constituent Shape entitys, not the Loom entity.
    • We can use a single path for both attributes, though nothing will show if the start/end attributs do not have differing values.
        fromPath: null,
        toPath: null,
  • §

    fromPathStart, fromPathEnd and toPathStart, toPathEnd - float Numbers generally between 0.0 - 1.0 - The Loom entity can set the start and end cursors on each of its path struts, between which the image will be drawn.

    • These can be animated to allow the image to flow between one part of the display and another, changing its shape as it moves.
        fromPathStart: 0,
        fromPathEnd: 1,
    
        toPathStart: 0,
        toPathEnd: 1,
  • §

    synchronizePathCursors - Boolean flag - To make sure the from... and to... strut start points, and end points, have the same value set the attribute to true (default).

    • Setting it to false allows the cursors to be set independently on each strut … which in turn may lead to unexpected display consequences.
        synchronizePathCursors: true,
  • §

    loopPathCursors - Boolean flag - For animation purposes, the image will move between the struts with the bottom of the page appearing again at the top of the Loom as it moves down (and vice versa).

    • To change this functionality - so that the image slowly disappears as it animates up and down past the ends of the struts, set the attribute to false.
        loopPathCursors: true,
        constantSpeedAlongPath: true,
  • §

    isHorizontalCopy - Boolean flag - Copying the source image to the output happens, by default, by rows - which effectively means the struts are on the left-hand and right-hand edges of the image.

    • To change this to columns (which sets the struts to the top and bottom edges of the image) set the attribute to false
        isHorizontalCopy: true,
  • §

    showBoundingBox (Boolean), boundingBoxColor (CSS color String) - Mainly for library development/testing work - shows the loom entity’s bounding box - which is calculated from the constituent Shape entitys’ current bounding boxes.

        showBoundingBox: false,
        boundingBoxColor: BLACK,
  • §

    source - The Picture entity source for this loom. For initialization and/or set, we can supply either the Picture entity itself, or its name-String value.

    • The content image displayed by the Loom entity are set in the Picture entity, not the Loom, and can be any artefact supported by the Picture (image, video, sprite, or a Cell artefact).
    • Note that any filters should be applied to the Picture entity; Loom entitys do not support filter functionality but will apply a Picture’s filters to the source image as-and-where appropriate.
        source: null,
  • §

    sourceIsVideoOrSprite - Boolean flag - If the Picture entity is hosting a video or sprite asset, we need to update the input on every frame.

    • It’s easier to tell the Loom entity to do this using a flag, rather than get the Picture entity to update all its Loom subscribers on every display cycle.
    • For Pictures using image assets the flag must be set to false (the default); setting the flag to true will significantly degrade display and animation performance.
        sourceIsVideoOrSprite: false,
  • §

    The current Frame drawing process often leads to moiré interference patterns appearing in the resulting image. Scrawl-canvas uses a resize trick to blur out these patterns.

    interferenceLoops (positive integer Number), interferenceFactor (positive float Number) - The interferenceFactor attribute sets the resizing ratio; while he interferenceLoops attribute sets the number of times the image gets resized.

    • If inteference patterns still appear in the final image, tweak these values to see if a better output can be achieved.
        interferenceLoops: 2,
        interferenceFactor: 1.03,
  • §

    sourceExpansionFactor (positive integer Number) - Unpainted lines sometimes appear in the output. The solution appears to be to expand the source picture by a given factor.

        sourceExpansionFactor: 1,
  • §

    The Loom entity does not use the position or entity mixins (used by most other entitys) as its positioning is entirely dependent on the position, rotation, scale etc of its constituent Shape path entity struts.

    It does, however, use these attributes (alongside their setters and getters): visibility, order, delta, host, group, anchor.

        visibility: true,
        calculateOrder: 0,
        stampOrder: 0,
        host: null,
        group: null,
        anchor: null,
  • §

    noCanvasEngineUpdates - Boolean flag - Canvas engine updates are required for the Loom’s border - strokeStyle and line styling; if a Loom is to be drawn without a border, then setting this flag to true may help improve rendering efficiency.

        noCanvasEngineUpdates: false,
  • §

    noDeltaUpdates - Boolean flag - Loom entitys support delta animation - achieved by updating the ...path attributes by appropriate (and small!) values. If the Loom is not going to be animated by delta values, setting the flag to true may help improve rendering efficiency.

        noDeltaUpdates: false,
  • §

    onEnter, onLeave, onDown, onUp - Loom entitys support collision detection, reporting a hit when a test coordinate falls within the Loom’s output image. As a result, Looms can also accept and act on the four on functions - see entity event listener functions for more details.

        onEnter: null,
        onLeave: null,
        onDown: null,
        onUp: null,
  • §

    noUserInteraction - Boolean flag - To switch off collision detection for a Loom entity - which might help improve rendering efficiency - set the flag to true.

        noUserInteraction: false,
  • §

    Anchor objects can be assigned to Loom entitys, meaning the following attributes are supported:

    • anchorDescription
    • anchorType
    • anchorTarget
    • anchorRel
    • anchorReferrerPolicy
    • anchorPing
    • anchorHreflang
    • anchorHref
    • anchorDownload

    And the anchor attributes can also be supplied as a key:value object assigned to the anchor attribute:

        anchor: {
            description
            download
            href
            hreflang
            ping
            referrerpolicy
            rel:
            target:
            anchorType
            clickAction:
        }
    

    Note that Loom entitys DO NOT SUPPORT the sensor component of the Scrawl-canvas collisions system and will return an empty array when asked to supply sensor coordinates for testing against other artefacts.

  • §

    method - All normal Scrawl-canvas entity stamping methods are supported.

        method: FILL,
  • §

    Loom entitys support appropriate styling attributes, mainly for their stroke styles (used with the draw, drawAndFill, fillAndDraw, drawThenFill and fillThenDraw stamping methods).

    • These state attributes are stored directly on the object, rather than in a separate State object.

    The following attributes are thus supported:

    Alpha and Composite operations will be applied to both the Loom entity’s border (the Shape entitys, with connecting lines between their paths’ start and end points) and fill (the image displayed between the Loom’s struts)

    • globalAlpha
    • globalCompositeOperation

    All line attributes are supported

    • lineWidth
    • lineCap
    • lineJoin
    • lineDash
    • lineDashOffset
    • miterLimit

    The Loom entity’s strokeStyle can be any style supported by Scrawl-canvas - color strings, gradient objects, and pattern objects

    • strokeStyle

    The shadow attributes will only be applied to the stroke (border), not to the Loom’s fill (image)

    • shadowOffsetX
    • shadowOffsetY
    • shadowBlur
    • shadowColor
    };
    P.defs = mergeOver(P.defs, defaultAttributes);
  • §

    Packet management

    P.packetExclusions = pushUnique(P.packetExclusions, ['pathObject', 'state']);
    P.packetExclusionsByRegex = pushUnique(P.packetExclusionsByRegex, ['^(local|dirty|current)', 'Subscriber$']);
    P.packetObjects = pushUnique(P.packetObjects, ['group', 'fromPath', 'toPath', 'source']);
    P.packetFunctions = pushUnique(P.packetFunctions, ['onEnter', 'onLeave', 'onDown', 'onUp']);
    
    P.processPacketOut = function (key, value, incs) {
    
        let result = true;
    
        if(!incs.includes(key) && value === this.defs[key]) result = false;
    
        return result;
    };
    
    P.finalizePacketOut = function (copy, items) {
    
        const stateCopy = _parse(this.state.saveAsPacket(items))[3];
        copy = mergeOver(copy, stateCopy);
    
        copy = this.handlePacketAnchor(copy, items);
    
        return copy;
    };
    
    P.handlePacketAnchor = function (copy, items) {
    
        if (this.anchor) {
    
            const a = _parse(this.anchor.saveAsPacket(items))[3];
            copy.anchor = a;
        }
        return copy;
    }
  • §

    Clone management

    TODO - this functionality is currently disabled, need to enable it and make it work properly

    P.clone = λcloneError;
  • §

    Kill management

    No additional kill functionality required

  • §

    Get, Set, deltaSet

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

    get - copied over from the entity mixin

    P.get = function (item) {
    
        const getter = this.getters[item];
    
        if (getter) return getter.call(this);
    
        else {
    
            const state = this.state;
    
            let def = this.defs[item],
                val;
    
            if (def != null) {
    
                val = this[item];
                return (typeof val !== UNDEF) ? val : def;
            }
    
            def = state.defs[item];
    
            if (def != null) {
    
                val = state[item];
                return (typeof val !== UNDEF) ? val : def;
            }
            return null;
        }
    };
  • §

    set - copied over from the entity mixin.

    P.set = function (items = Ωempty) {
    
        const keys = _keys(items),
            keysLen = keys.length;
    
        if (keysLen) {
    
            const setters = this.setters,
                defs = this.defs,
                state = this.state;
    
            const stateSetters = (state) ? state.setters : Ωempty;
            const stateDefs = (state) ? state.defs : Ωempty;
    
            let fn, i, key, value;
    
            for (i = 0; i < keysLen; i++) {
    
                key = keys[i];
                value = items[key];
    
                if (key && key !== NAME && value != null) {
    
                    if (!STATE_KEYS.includes(key)) {
    
                        fn = setters[key];
    
                        if (fn) fn.call(this, value);
                        else if (typeof defs[key] !== UNDEF) this[key] = value;
                    }
                    else {
    
                        fn = stateSetters[key];
    
                        if (fn) fn.call(state, value);
                        else if (typeof stateDefs[key] !== UNDEF) state[key] = value;
                    }
                }
            }
        }
        return this;
    };
  • §

    setDelta - copied over from the entity mixin.

    P.setDelta = function (items = Ωempty) {
    
        const keys = _keys(items),
            keysLen = keys.length;
    
        if (keysLen) {
    
            const setters = this.deltaSetters,
                defs = this.defs,
                state = this.state;
    
            const stateSetters = (state) ? state.deltaSetters : Ωempty;
            const stateDefs = (state) ? state.defs : Ωempty;
    
            let fn, i, key, value;
    
            for (i = 0; i < keysLen; i++) {
    
                key = keys[i];
                value = items[key];
    
                if (key && key !== NAME && value != null) {
    
                    if (!STATE_KEYS.includes(key)) {
    
                        fn = setters[key];
    
                        if (fn) fn.call(this, value);
                        else if (typeof defs[key] !== UNDEF) this[key] = addStrings(this[key], value);
                    }
                    else {
    
                        fn = stateSetters[key];
    
                        if (fn) fn.call(state, value);
                        else if (typeof stateDefs[key] !== UNDEF) state[key] = addStrings(state[key], value);
                    }
                }
            }
        }
        return this;
    };
  • §

    host, getHost - copied over from the position mixin.

    S.host = function (item) {
    
        if (item) {
    
            const host = artefact[item];
    
            if (host && host.here) this.host = host.name;
            else this.host = item;
        }
        else this.host = ZERO_STR;
    };
  • §

    group - copied over from the position mixin.

    G.group = function () {
    
        return (this.group) ? this.group.name : ZERO_STR;
    };
    S.group = function (item) {
    
        let g;
    
        if (item) {
    
            if (this.group && this.group.type === T_GROUP) this.group.removeArtefacts(this.name);
    
            if (item.substring) {
    
                g = group[item];
    
                if (g) this.group = g;
                else this.group = item;
            }
            else this.group = item;
        }
    
        if (this.group && this.group.type === T_GROUP) this.group.addArtefacts(this.name);
    };
  • §

    getHere - returns current core position.

    P.getHere = function () {
    
        return currentCorePosition;
    };
  • §

    delta - copied over from the position mixin.

    S.delta = function (items) {
    
        if (items) this.delta = mergeDiscard(this.delta, items);
    };
  • §

    fromPath

    S.fromPath = function (item) {
    
        if (item) {
    
            const oldPath = this.fromPath,
                newPath = (item.substring) ? artefact[item] : item,
                name = this.name;
    
            if (newPath && newPath.name && newPath.useAsPath) {
    
                if (oldPath && oldPath.name !== newPath.name) removeItem(oldPath.pathed, name);
    
                pushUnique(newPath.pathed, name);
    
                this.fromPath = newPath;
    
                this.dirtyStart = true;
            }
        }
    };
  • §

    toPath

    S.toPath = function (item) {
    
        if (item) {
    
            const oldPath = this.toPath,
                newPath = (item.substring) ? artefact[item] : item,
                name = this.name;
    
            if (newPath && newPath.name && newPath.useAsPath) {
    
                if (oldPath && oldPath.name !== newPath.name) removeItem(oldPath.pathed, name);
    
                pushUnique(newPath.pathed, name);
    
                this.toPath = newPath;
    
                this.dirtyStart = true;
            }
        }
    };
  • §

    source

    S.source = function (item) {
    
        item = (item.substring) ? artefact[item] : item;
    
        if (item && item.type === T_PICTURE) {
    
            const src = this.source;
    
            if (src && src.type === T_PICTURE) src.imageUnsubscribe(this.name);
    
            this.source = item;
            item.imageSubscribe(this.name);
            this.dirtyInput = true;
        }
    };
  • §

    isHorizontalCopy

    S.isHorizontalCopy = function (item) {
    
        this.isHorizontalCopy = (item) ? true : false;
        this.dirtyPathData = true;
    };
  • §

    synchronizePathCursors

    S.synchronizePathCursors = function (item) {
    
        this.synchronizePathCursors = (item) ? true : false;
    
        if (item) {
    
            this.toPathStart = this.fromPathStart;
            this.toPathEnd = this.fromPathEnd;
        }
    
        this.dirtyPathData = true;
    };
  • §

    loopPathCursors

    S.loopPathCursors = function (item) {
    
        this.loopPathCursors = (item) ? true : false;
    
        if (item) {
    
            let c = this.fromPathStart;
            if (c < 0 || c > 1) this.fromPathStart = c - _floor(c);
    
            c = this.fromPathEnd
            if (c < 0 || c > 1) this.fromPathEnd = c - _floor(c);
    
            c = this.toPathStart
            if (c < 0 || c > 1) this.toPathStart = c - _floor(c);
    
            c = this.toPathEnd
            if (c < 0 || c > 1) this.toPathEnd = c - _floor(c);
        }
    
        this.dirtyOutput = true;
    };
  • §

    fromPathStart

    S.fromPathStart = function (item) {
    
        if (this.loopPathCursors && (item < 0 || item > 1)) item = item - _floor(item);
        this.fromPathStart = item;
        if (this.synchronizePathCursors) this.toPathStart = item;
        this.dirtyPathData = true;
    };
    D.fromPathStart = function (item) {
    
        let val = this.fromPathStart += item;
    
        if (this.loopPathCursors && (val < 0 || val > 1)) val = val - _floor(val);
        this.fromPathStart = val;
        if (this.synchronizePathCursors) this.toPathStart = val;
        this.dirtyPathData = true;
    };
  • §

    fromPathEnd

    S.fromPathEnd = function (item) {
    
        if (this.loopPathCursors && (item < 0 || item > 1)) item = item - _floor(item);
        this.fromPathEnd = item;
        if (this.synchronizePathCursors) this.toPathEnd = item;
        this.dirtyPathData = true;
    };
    D.fromPathEnd = function (item) {
    
        let val = this.fromPathEnd += item;
    
        if (this.loopPathCursors && (val < 0 || val > 1)) val = val - _floor(val);
        this.fromPathEnd = val;
        if (this.synchronizePathCursors) this.toPathEnd = val;
        this.dirtyPathData = true;
    };
  • §

    toPathStart

    S.toPathStart = function (item) {
    
        if (this.loopPathCursors && (item < 0 || item > 1)) item = item - _floor(item);
        this.toPathStart = item;
        if (this.synchronizePathCursors) this.fromPathStart = item;
        this.dirtyPathData = true;
    };
    D.toPathStart = function (item) {
    
        let val = this.toPathStart += item;
    
        if (this.loopPathCursors && (val < 0 || val > 1)) val = val - _floor(val);
        this.toPathStart = val;
        if (this.synchronizePathCursors) this.fromPathStart = val;
        this.dirtyPathData = true;
    };
  • §

    toPathEnd

    S.toPathEnd = function (item) {
    
        if (this.loopPathCursors && (item < 0 || item > 1)) item = item - _floor(item);
        this.toPathEnd = item;
        if (this.synchronizePathCursors) this.fromPathEnd = item;
        this.dirtyPathData = true;
    };
    D.toPathEnd = function (item) {
    
        let val = this.toPathEnd += item;
    
        if (this.loopPathCursors && (val < 0 || val > 1)) val = val - _floor(val);
        this.toPathEnd = val;
        if (this.synchronizePathCursors) this.fromPathEnd = val;
        this.dirtyPathData = true;
    };
  • §

    Prototype functions

  • §

    getHost - copied over from the position mixin.

    P.getHost = function () {
    
        if (this.currentHost) return this.currentHost;
        else if (this.host) {
    
            const host = artefact[this.host];
    
            if (host) {
    
                this.currentHost = host;
                this.dirtyHost = true;
                return this.currentHost;
            }
        }
        return currentCorePosition;
    };
  • §

    Invalidate mid-init functionality

    P.midInitActions = λnull;
  • §

    Force the Loom entity to update

    • Because it doesn’t automatically keep check of changes in its picture source
    P.update = function () {
    
        this.dirtyInput = true;
        this.dirtyOutput = true;
    };
  • §

    Display cycle functionality

  • §

    prepareStamp - function called as part of the Display cycle compile step.

    • This is where we need to check whether we need to recalculate the path data which we’ll use later to build the Loom entity’s output image.
    • We only need to recalculate the path data on the initial render, and afterwards when the dirtyPathData flag has been set.
    • If we perform the recalculation, then we need to make sure to set the dirtyOutput flag, which will trigger the output image build.

    prepareStamp - function called as part of the Display cycle compile step.

    P.prepareStamp = function () {
    
        const fPath = this.fromPath,
            tPath = this.toPath;
  • §

    Sanity check 1 — also recalculates bounding box if needed

        const [startX, startY] = this.getBoundingBox();
  • §

    Sanity check 2 — if end points moved, mark path data dirty

        if (!this.dirtyPathData && fPath && tPath) {
    
            const { x: fSx, y: fSy } = fPath.getPathPositionData(0);
            const { x: fEx, y: fEy } = fPath.getPathPositionData(1);
            const { x: tSx, y: tSy } = tPath.getPathPositionData(0);
            const { x: tEx, y: tEy } = tPath.getPathPositionData(1);
    
            const localPathTests = [fSx, fSy, fEx, fEy, tSx, tSy, tEx, tEy];
    
            if (!this.pathTests || this.pathTests.some((v, i) => v !== localPathTests[i])) {
    
                this.pathTests = localPathTests;
                this.dirtyPathData = true;
            }
        }
    
        if (this.dirtyPathData || !this.fromPathData.length) {
    
            this.dirtyPathData = false;
  • §

    Invalidate cached render instructions

            this.watchIndex = -1;
            this.engineInstructions.length = 0;
            this.engineDeltaLengths.length = 0;
    
            const fromPathData = this.fromPathData,
                toPathData   = this.toPathData;
    
            fromPathData.length = 0;
            toPathData.length   = 0;
    
            if (fPath && tPath) {
  • §

    Decide table resolution (and input size).

    • Sample both paths uniformly at ‘pathSteps’ points.
                const fPathLength = _ceil(fPath.length);
                const tPathLength = _ceil(tPath.length);
    
                const pathSteps = this.setSourceDimension(_max(fPathLength, tPathLength) * this.sourceExpansionFactor);
  • §

    Guard: ensure at least 2 samples so we can include both ends cleanly

                const steps = (pathSteps > 1) ? pathSteps : 2,
                    step  = 1 / (steps - 1);
    
                const pathSpeed = this.constantSpeedAlongPath;
  • §

    Build sampling tables with exactly steps entries.

                for (let i = 0; i < steps; i++) {
  • §

    Ensure the last sample is exactly 1.0 to avoid FP drift

                    const cursor = (i === steps - 1) ? 1 : i * step;
    
                    let p = fPath.getPathPositionData(cursor, pathSpeed);
                    fromPathData.push([p.x - startX, p.y - startY]);
    
                    p = tPath.getPathPositionData(cursor, pathSpeed);
                    toPathData.push([p.x - startX, p.y - startY]);
                }
  • §

    Compute the fractional span (0..1) required to traverse on each path.

    • Stash these as per-row increments (cleanOutput adds them in table-index space).
                const fStart = this.fromPathStart, fEnd = this.fromPathEnd,
                    tStart = this.toPathStart,   tEnd = this.toPathEnd
    
                let fPartial = (fEnd >= fStart) ? (fEnd - fStart) : (1 - fStart + fEnd),
                    tPartial = (tEnd >= tStart) ? (tEnd - tStart) : (1 - tStart + tEnd);
    
                if (fPartial < 0.005) fPartial = 0.005;
                if (tPartial < 0.005) tPartial = 0.005;
    
                this.fromPathSteps = fPartial;
                this.toPathSteps   = tPartial;
  • §

    Which path reaches the end first?

                this.watchFromPath = (fPartial <= tPartial);
    
                this.dirtyOutput = true;
            }
            else this.dirtyPathData = true;
        }
  • §

    Hidden DOM elements housekeeping

        this.prepareStampTabsHelper();
    };
  • §

    setSourceDimension - internal function called by prepareStamp.

    • We make the source dimensions a square of the longest path length
    • This way, we can do a horizontal scan, or a vertical scan with no further calculation
    P.setSourceDimension = function (val) {
  • §
    • prepareStamp does the dimension calculation itself, then supplies the new value
    • we just need to update this.sourceDimension and set the dirtyInput flag
        if (val) {
    
            if (this.sourceDimension !== val) {
    
                this.sourceDimension = val;
                this.dirtyInput = true;
            }
        }
  • §

    if other functions call setSourceDimension, they will do it without supplying a new value

    • in which case we can calculate and update it here
    • other functions do it as a sanity check
        else {
    
            const fPath = this.fromPath,
                tPath = this.toPath;
    
            if(fPath && tPath) {
    
                const fPathLength = _ceil(fPath.length),
                    tPathLength = _ceil(tPath.length);
    
                const steps = _max(fPathLength, tPathLength);
    
                if (this.sourceDimension !== steps) this.sourceDimension = steps;
            }
            else this.sourceDimension = 0;
        }
        return this.sourceDimension;
    };
  • §

    simpleStamp - Simple stamping is entirely synchronous

    • TODO: we may have to disable this functionality for the Loom entity, if we use a Web Assembly module for either the prepareStamp calculations, or to build the output image itself
    P.simpleStamp = function (host, changes) {
    
        if (host && GOOD_HOST.includes(host.type)) {
    
            this.currentHost = host;
    
            if (changes) {
    
                this.set(changes);
                this.prepareStamp();
            }
            this.regularStamp();
        }
    };
  • §

    stamp - All entity stamping, except for simple stamps, goes through this function.

    • While other entitys have to worry about applying filters as part of the stamping process, this is not an issue for Loom entitys because filters are defined on, and applied to, the source Picture entity, not the Loom itself

    Here we check which dirty flags need actioning, and call a range of different functions to process the work. These flags are:

    • dirtyInput - the Picture entity has reported a change in its source, or copy attributes)
    • dirtyOutput - to render the cleaned input, or take account that the Loom paths’ cursors have changed)
    P.stamp = function (force = false, host, changes) {
    
        if (force) {
    
            if (host && GOOD_HOST.includes(host.type)) this.currentHost = host;
    
            if (changes) {
    
                this.set(changes);
                this.prepareStamp();
            }
            return this.regularStamp();
        }
    
        else if (this.visibility) {
  • §

    if (this.sourceIsVideoOrSprite || this.dirtyInput) this.sourceImageData = this.cleanInput();

            if (this.sourceIsVideoOrSprite || this.dirtyInput) this.cleanInput();
    
            if (this.dirtyOutput) this.output = this.cleanOutput();
    
            this.regularStamp();
        }
    };
  • §

    Clean functions

  • §

    cleanInput - internal function called by stamp

    P.cleanInput = function () {
    
        this.dirtyInput = false;
    
        const sourceDimension = this.sourceDimension;
    
        if (!sourceDimension) {
    
            this.dirtyInput = true;
            return false;
        }
    
        const mycell = requestCell(),
            engine = mycell.engine,
            canvas = mycell.element;
    
        canvas.width = sourceDimension;
        canvas.height = sourceDimension;
        engine.resetTransform();
    
        this.source.stamp(true, mycell, {
            startX: 0,
            startY: 0,
            handleX: 0,
            handleY: 0,
            offsetX: 0,
            offsetY: 0,
            roll: 0,
            scale: 1,
    
            width: sourceDimension,
            height: sourceDimension,
    
            method: FILL,
        });
    
        this.sourceImageData = engine.getImageData(0, 0, sourceDimension, sourceDimension);
    
        releaseCell(mycell);
    };
  • §

    cleanOutput - internal function called by stamp

    • If you’re not a fan of big functions, please look away now.
    P.cleanOutput = function () {
    
        this.dirtyOutput = false;
    
        const sourceDimension = this.sourceDimension,
            sourceData = this.sourceImageData;
    
        if (sourceDimension && sourceData) {
    
            const fromPathData = this.fromPathData,
                toPathData = this.toPathData,
    
                dataLen = fromPathData.length,
    
                fPathStart = this.fromPathStart,
                fStep = this.fromPathSteps || 1,
    
                tPathStart = this.toPathStart,
                tStep = this.toPathSteps || 1,
    
                magicVerticalPi = _piHalf - 1.5708,
    
                isHorizontalCopy = this.isHorizontalCopy,
                loop = this.loopPathCursors,
    
                watchFromPath = this.watchFromPath,
                engineInstructions = this.engineInstructions,
                engineDeltaLengths = this.engineDeltaLengths;
    
            let fCursor = fPathStart * dataLen,
                tCursor = tPathStart * dataLen,
                watchIndex = this.watchIndex,
                fx, fy, tx, ty, dx, dy, dLength, dAngle, cos, sin, i, j,
                instruction;
    
            let [, , outputWidth, outputHeight] = this.getBoundingBox();
    
            if (outputWidth && outputHeight) {
    
                outputWidth = ~~outputWidth;
                outputHeight = ~~outputHeight;
    
                const inputCell = requestCell(),
                    inputEngine = inputCell.engine,
                    inputCanvas = inputCell.element;
    
                inputCanvas.width = sourceDimension;
                inputCanvas.height = sourceDimension;
                inputEngine.resetTransform();
                inputEngine.putImageData(sourceData, 0, 0);
    
                const outputCell = requestCell(),
                    outputEngine = outputCell.engine,
                    outputCanvas = outputCell.element;
    
                outputCanvas.width = outputWidth;
                outputCanvas.height = outputHeight;
                outputEngine.globalAlpha = this.state.globalAlpha;
                outputEngine.resetTransform();
    
                if(!engineInstructions.length) {
    
                    for (i = 0; i < sourceDimension; i++) {
    
                        if (watchIndex < 0) {
    
                            if (watchFromPath && fCursor < 1) watchIndex = i;
                            else if (!watchFromPath && tCursor < 1) watchIndex = i;
                        }
    
                        if (fCursor < dataLen && tCursor < dataLen && fCursor >= 0 && tCursor >= 0) {
    
                            [fx, fy] = fromPathData[_floor(fCursor)];
                            [tx, ty] = toPathData[_floor(tCursor)];
    
                            dx = tx - fx;
                            dy = ty - fy;
    
                            dLength = _hypot(dx, dy);
    
                            if (isHorizontalCopy) {
    
                                dAngle = -_atan2(dx, dy) + _piHalf;
                                cos = _cos(dAngle);
                                sin = _sin(dAngle);
    
                                engineInstructions.push([cos, sin, -sin, cos, fx, fy]);
                                engineDeltaLengths.push(dLength);
                            }
                            else {
    
                                dAngle = -_atan2(dx, dy) + magicVerticalPi;
                                cos = _cos(dAngle);
                                sin = _sin(dAngle);
    
                                engineInstructions.push([cos, sin, -sin, cos, fx, fy, dLength]);
                                engineDeltaLengths.push(dLength);
                            }
                        }
                        else {
    
                            engineInstructions.push(false);
                            engineDeltaLengths.push(false);
                        }
    
                        fCursor += fStep;
                        tCursor += tStep;
    
                        if (loop) {
    
                            if (fCursor >= dataLen) fCursor -= dataLen;
                            if (tCursor >= dataLen) tCursor -= dataLen;
                        }
                    }
                    if (watchIndex < 0) watchIndex = 0;
                    this.watchIndex = watchIndex;
                }
    
                if (isHorizontalCopy) {
    
                    for (i = 0; i < sourceDimension; i++) {
    
                        instruction = engineInstructions[watchIndex];
    
                        if (instruction) {
    
                            outputEngine.setTransform(...instruction);
                            outputEngine.drawImage(inputCanvas, 0, ~~watchIndex, ~~sourceDimension, 1, 0, 0, ~~engineDeltaLengths[watchIndex], 1);
                        }
                        watchIndex++;
    
                        if (watchIndex >= sourceDimension) watchIndex = 0;
                    }
                }
                else {
    
                    for (i = 0; i < sourceDimension; i++) {
    
                        instruction = engineInstructions[watchIndex];
    
                        if (instruction) {
    
                            outputEngine.setTransform(...instruction);
                            outputEngine.drawImage(inputCanvas, ~~watchIndex, 0, 1, ~~sourceDimension, 0, 0, 1, ~~engineDeltaLengths[watchIndex]);
                        }
                        watchIndex++;
    
                        if (watchIndex >= sourceDimension) watchIndex = 0;
                    }
                }
    
                const iFactor = this.interferenceFactor,
                    iLoops = this.interferenceLoops,
    
                    iWidth = ~~(outputWidth * iFactor) + 1,
                    iHeight = ~~(outputHeight * iFactor) + 1;
    
                inputCanvas.width = iWidth;
                inputCanvas.height = iHeight;
    
                outputEngine.resetTransform();
                inputEngine.resetTransform();
    
                for (j = 0; j < iLoops; j++) {
    
                    inputEngine.drawImage(outputCanvas, 0, 0, outputWidth, outputHeight, 0, 0, iWidth, iHeight);
                    outputEngine.drawImage(inputCanvas, 0, 0, iWidth, iHeight, 0, 0, outputWidth, outputHeight);
                }
    
                const outputData = outputEngine.getImageData(0, 0, outputWidth, outputHeight);
    
                releaseCell(inputCell, outputCell);
    
                this.dirtyTargetImage = true;
    
                return outputData;
            }
            else this.dirtyOutput = true;
        }
        return false;
    };
  • §

    regularStamp - internal function called by stamp

    P.regularStamp = function () {
    
        const dest = this.currentHost;
    
        if (dest) {
    
            const engine = dest.engine;
    
            if (!this.noCanvasEngineUpdates) dest.setEngine(this);
    
            this[this.method](engine);
        }
    };
  • §

    getBoundingBox - internal function called by prepareStamp and cleanOutput functions, as well as the various method functions.

    • Loom calculates its bounding box from the Shape path entitys associated with it
    • This function recalculates when presented with a dirtyStart flag - we rely on the Shape entitys to tell us when their paths have changed/updated
    • Results get stashed in the boundingBox attribute for easier access, but all the method functions call this function just in case the box needs recalculating.
    P.getBoundingBox = function () {
    
        const fPath = this.fromPath,
            tPath = this.toPath;
    
        if(fPath && tPath) {
    
            if (this.dirtyStart) {
    
                this.boundingBox.length = 0;
    
                if (fPath.getBoundingBox && tPath.getBoundingBox) {
    
                    this.dirtyStart = false;
    
    /* eslint-disable-next-line */
                    let [lsx, lsy, sw, sh, sx, sy] = fPath.getBoundingBox();
    /* eslint-disable-next-line */
                    let [lex, ley, ew, eh, ex, ey] = tPath.getBoundingBox();
    
                    if (!_isFinite(lsx) || !_isFinite(lsy) || !_isFinite(sw) || !_isFinite(sh) || !_isFinite(sx) || !_isFinite(sy) || !_isFinite(lex) || !_isFinite(ley) || !_isFinite(ew) || !_isFinite(eh) || !_isFinite(ex) || !_isFinite(ey)) this.dirtyStart = true;
    
                    if (lsx === lex && lsy === ley && sw === ew && sh === eh && sx === ex && sy === ey) this.dirtyStart = true;
    
                    lsx += sx;
                    lsy += sy;
                    lex += ex;
                    ley += ey;
    
                    const minX = _min(lsx, lex),
                        maxX = _max(lsx + sw, lex + ew),
                        minY = _min(lsy, ley),
                        maxY = _max(lsy + sh, ley + eh);
    
                    this.boundingBox.push(minX, minY, maxX - minX, maxY - minY);
    
                    this.dirtyPathData = true;
                }
                else this.boundingBox.push(0, 0, 0, 0);
            }
        }
        else if (!this.boundingBox.length) this.boundingBox.push(0, 0, 0, 0);
    
        return this.boundingBox;
    };
  • §
    Stamp methods

    These ‘method’ functions stamp the Loom entity onto the canvas context supplied to them in the engine argument.

  • §

    fill

    P.fill = function (engine) {
    
    
        this.doFill(engine);
    
        if (this.showBoundingBox) this.drawBoundingBox(engine);
    
    };
  • §

    draw

    P.draw = function (engine) {
    
        this.doStroke(engine);
    
        if (this.showBoundingBox) this.drawBoundingBox(engine);
    };
  • §

    drawAndFill

    P.drawAndFill = function (engine) {
    
        this.doStroke(engine);
        this.currentHost.clearShadow();
        this.doFill(engine);
    
        if (this.showBoundingBox) this.drawBoundingBox(engine);
    };
  • §

    fillAndDraw

    P.fillAndDraw = function (engine) {
    
        this.doFill(engine);
        this.currentHost.clearShadow();
        this.doStroke(engine);
    
        if (this.showBoundingBox) this.drawBoundingBox(engine);
    };
  • §

    drawThenFill

    P.drawThenFill = function (engine) {
    
        this.doStroke(engine);
        this.doFill(engine);
    
        if (this.showBoundingBox) this.drawBoundingBox(engine);
    };
  • §

    fillThenDraw

    P.fillThenDraw = function (engine) {
    
        this.doFill(engine);
        this.doStroke(engine);
    
        if (this.showBoundingBox) this.drawBoundingBox(engine);
    };
  • §

    clear

    P.clear = function (engine) {
    
        const output = this.output,
            canvas = (this.currentHost) ? this.currentHost.element : false,
            gco = engine.globalCompositeOperation;
    
        if (output && canvas) {
    
            const tempCell = requestCell(),
                tempEngine = tempCell.engine,
                tempCanvas = tempCell.element;
    
            let [x, y, w, h] = this.getBoundingBox();
    
            x = ~~x;
            y = ~~y;
            w = ~~w;
            h = ~~h;
    
            tempCanvas.width = w;
            tempCanvas.height = h;
    
            tempEngine.putImageData(output, 0, 0);
            engine.resetTransform();
            engine.globalCompositeOperation = DESTINATION_OUT;
            engine.drawImage(tempCanvas, 0, 0, w, h, x, y, w, h);
            engine.globalCompositeOperation = gco;
    
            releaseCell(tempCell);
            if (this.showBoundingBox) this.drawBoundingBox(engine);
        }
    };
  • §

    none, clip

    P.none = λnull;
    P.clip = λnull;
  • §

    These stroke and fill functions handle most of the stuff that the method functions require to stamp the Loom entity onto a canvas cell.

  • §

    doStroke

    P.doStroke = function (engine) {
    
        const fPath = this.fromPath,
            tPath = this.toPath;
    
        if(fPath && fPath.getBoundingBox && tPath && tPath.getBoundingBox) {
    
            const host = this.currentHost;
    
            if (host) {
    
                const fStart = fPath.currentStampPosition,
                    fEnd = fPath.getPathPositionData(1),
                    tStart = tPath.currentStampPosition,
                    tEnd = tPath.getPathPositionData(1);
    
                host.rotateDestination(engine, fStart[0], fStart[1], fPath);
                engine.stroke(fPath.pathObject);
    
                host.rotateDestination(engine, tStart[0], tStart[1], tPath);
                engine.stroke(tPath.pathObject);
    
                engine.resetTransform();
                engine.beginPath()
                engine.moveTo(fEnd.x, fEnd.y);
                engine.lineTo(tEnd.x, tEnd.y);
                engine.moveTo(...tStart);
                engine.lineTo(...fStart);
                engine.closePath();
                engine.stroke();
            }
        }
    };
  • §

    doFill

    • Canvas API’s putImageData function turns transparent pixels in the output into transparent in the host canvas - which is not what we want
    • Problem solved by putting output into a pool cell, then drawing it from there to the host cell
    P.doFill = function (engine) {
    
        const output = this.output,
            canvas = (this.currentHost) ? this.currentHost.element : false;
    
        if (output && canvas) {
    
            const tempCell = requestCell(),
                tempEngine = tempCell.engine,
                tempCanvas = tempCell.element;
    
            let [x, y, w, h] = this.getBoundingBox();
    
            x = ~~x;
            y = ~~y;
            w = ~~w;
            h = ~~h;
    
            tempCanvas.width = w;
            tempCanvas.height = h;
    
            tempEngine.putImageData(output, 0, 0);
            engine.resetTransform();
            engine.drawImage(tempCanvas, 0, 0, w, h, x, y, w, h);
    
            releaseCell(tempCell);
        }
    };
  • §

    drawBoundingBox

    • Usually only need to draw the bounding box during development work, to make sure the getBoundingBox calculation is operating correctly
    P.drawBoundingBox = function (engine) {
    
        if (this.dirtyStart) this.getBoundingBox();
    
        engine.save();
    
        const t = engine.getTransform();
        engine.resetTransform();
    
        engine.strokeStyle = this.boundingBoxColor;
        engine.lineWidth = 1;
        engine.globalCompositeOperation = SOURCE_OVER;
        engine.globalAlpha = 1;
        engine.shadowOffsetX = 0;
        engine.shadowOffsetY = 0;
        engine.shadowBlur = 0;
    
        engine.strokeRect(...this.boundingBox);
    
        engine.restore();
        engine.setTransform(t);
    };
  • §

    Collision functionality

  • §

    The checkHit functionality can be used in the same way it is for other entitys (and groups)

    • One difference is that this function checks hits against an ImageData object, thus doesn’t need to be supplied with a pool canvas so that it can do its job
    • Note: will check for the dirtyTargetImage flag, which normally gets checked as part of the rendering cycle
    P.checkHit = function (items = []) {
    
        if (this.noUserInteraction) return false;
    
        const tests = (!_isArray(items)) ?  [items] : items,
            targetData = (this.output && this.output.data) ? this.output.data : false;
    
        let tx, ty, cx, cy, index;
    
        if (targetData) {
    
            const [x, y, w, h] = this.getBoundingBox();
    
            if (tests.some(test => {
    
                if (_isArray(test)) {
    
                    tx = test[0];
                    ty = test[1];
                }
                else if (xta(test, test.x, test.y)) {
    
                    tx = test.x;
                    ty = test.y;
                }
                else return false;
    
                if (!_isFinite(tx) || !_isFinite(ty)) return false;
    
                cx = tx - x;
                cy = ty - y;
    
                if (cx < 0 || cx > w || cy < 0 || cy > h) return false;
    
                index = (((cy * w) + cx) * 4) + 3;
    
                if (targetData) return (targetData[index] > 0) ? true : false;
                else return false;
    
            }, this)) {
    
                return {
                    x: tx,
                    y: ty,
                    artefact: this
                };
            }
        }
        return false;
    };
  • §

    Factory

    scrawl.makeQuadratic({
    
        name: 'my-quad',
        // [... rest of definition]
    });
    
    let myBez = scrawl.makeBezier({
    
        name: 'my-bezier',
        // [... rest of definition]
    });
    
    scrawl.makePicture({
    
        name: 'myFlower',
    
        // Loom will respect the Picture's copy attributes when creating its output
        copyStartX: 0,
        copyStartY: 0,
    
        copyWidth: '100%',
        copyHeight: '100%',
    
        // Best practice - set visibility to false
        visibility: false,
    
        // [... rest of definition]
    });
    
    let myLoom = scrawl.makeLoom({
    
        name: 'display-loom',
    
        fromPath: 'my-quad',
        toPath: myBez,
    
        source: 'myFlower',
    
        lineWidth: 2,
        lineCap: 'round',
        strokeStyle: 'orange',
    
        boundingBoxColor: 'red',
        showBoundingBox: true,
    
        method: 'fillThenDraw',
    });
    
    export const makeLoom = function (items) {
    
        if (!items) return false;
        return new Loom(items);
    };
    
    constructors.Loom = Loom;