• Jump To … +
    ./source/core/animationloop.js ./source/core/component.js ./source/core/document.js ./source/core/events.js ./source/core/init.js ./source/core/library.js ./source/core/userInteraction.js ./source/core/utilities.js ./source/factory/action.js ./source/factory/anchor.js ./source/factory/animation.js ./source/factory/bezier.js ./source/factory/block.js ./source/factory/canvas.js ./source/factory/cell.js ./source/factory/cog.js ./source/factory/color.js ./source/factory/coordinate.js ./source/factory/element.js ./source/factory/emitter.js ./source/factory/filter.js ./source/factory/fontAttributes.js ./source/factory/gradient.js ./source/factory/grid.js ./source/factory/group.js ./source/factory/imageAsset.js ./source/factory/line.js ./source/factory/loom.js ./source/factory/mesh.js ./source/factory/net.js ./source/factory/noise.js ./source/factory/oval.js ./source/factory/palette.js ./source/factory/particle.js ./source/factory/particleForce.js ./source/factory/particleHistory.js ./source/factory/particleSpring.js ./source/factory/particleWorld.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/radialGradient.js ./source/factory/rectangle.js ./source/factory/renderAnimation.js ./source/factory/shape.js ./source/factory/spiral.js ./source/factory/spriteAsset.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/unstackedElement.js ./source/factory/vector.js ./source/factory/videoAsset.js ./source/factory/wheel.js ./source/mixin/anchor.js ./source/mixin/asset.js ./source/mixin/assetConsumer.js ./source/mixin/base.js ./source/mixin/cascade.js ./source/mixin/delta.js ./source/mixin/displayShape.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/shapeBasic.js ./source/mixin/shapeCurve.js ./source/mixin/shapePathCalculation.js ./source/mixin/styles.js ./source/mixin/tween.js ./source/worker/filter-string.js ./source/worker/filter.js
  • ¶

    Mesh factory

    The Scrawl-canvas Mesh entity applies a Net particle system to an image, allowing the image to be deformed by dragging particles around the canvas. This is a similar concept to Photoshop warp meshes and (more distantly) the Gimp’s cage tool.

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

    • Every Mesh object requires a Net entity create the grid that it uses for transforming its image.
    • A Mesh entity also requires a Picture entity to act as its image source.
    • Meshes can (in theory) use CSS color Strings for their strokeStyle values, alongside Gradient, RadialGradient, Color and Pattern objects.
    • They can (in theory) use Anchor objects for user navigation.
    • They can (in theory) be rendered to the canvas by including them in a Cell object’s Group.
    • They can (in theory) be animated directly, or using delta animation, or act as the target for Tween animations.
    • Meshes can (in theory) be cloned, and killed.

    Note that this is experimental technology!

    • The Mesh entity code base shares many similarities to that of the Loom entity; some of the code has been copied over from that file directly.
    • Current code does not use position or entity mixins, meaning much of the code here has been copied over from those mixins (DRY issue).
    • TODO: packet management, clone and kill functionality not yet tested. Much of the other functionality also lacks tests.
  • ¶

    Demos:

    • Particles-008 - Net entity: generation and basic functionality, including Spring objects
    • Particles-016 - Mesh entitys
  • ¶

    Imports

    import { constructors, artefact } from '../core/library.js';
    import { currentGroup } from '../core/document.js';
    import { mergeOver, mergeDiscard, pushUnique, λnull, λthis, xta } from '../core/utilities.js';
    
    import { makeState } from '../factory/state.js';
    import { requestCell, releaseCell } from '../factory/cell.js';
    
    import baseMix from '../mixin/base.js';
    import anchorMix from '../mixin/anchor.js';
  • ¶

    Mesh constructor

    const Mesh = function (items = {}) {
    
        this.makeName(items.name);
        this.register();
    
        this.set(this.defs);
    
        this.state = makeState();
    
        if (!items.group) items.group = currentGroup;
    
        this.onEnter = λnull;
        this.onLeave = λnull;
        this.onDown = λnull;
        this.onUp = λnull;
    
        this.delta = {};
    
        this.set(items);
    
        this.fromPathData = [];
        this.toPathData = [];
    
        this.watchFromPath = null;
        this.watchIndex = -1;
        this.engineInstructions = [];
        this.engineDeltaLengths = [];
    
        return this;
    };
  • ¶

    Mesh prototype

    let P = Mesh.prototype = Object.create(Object.prototype);
    P.type = 'Mesh';
    P.lib = 'entity';
    P.isArtefact = true;
    P.isAsset = false;
  • ¶

    Mixins

    P = baseMix(P);
    P = anchorMix(P);
  • ¶

    Mesh attributes

    • Attributes defined in the base mixin: name.
    • Attributes defined in the anchor mixin: anchor.
    let defaultAttributes = {
  • ¶

    net - A Mesh entity requires a Net entity, set to generate a weak or strong net, to supply Particle objects to act as its mapping coordinates.

        net: null,
  • ¶

    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,
  • ¶

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

    • The content image displayed by the Mesh entity are set in the Picture entity, not the Mesh, 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; Mesh 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 Mesh entity to do this using a flag, rather than get the Picture entity to update all its Mesh 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,
  • ¶

    The Mesh 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, collides.

        visibility: true,
        order: 0,
        delta: null,
        host: null,
        group: null,
        anchor: null,
  • ¶

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

        noCanvasEngineUpdates: false,
  • ¶

    noDeltaUpdates - Boolean flag - Mesh entitys support delta animation - achieved by updating the ...path attributes by appropriate (and small!) values. If the Mesh 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 - Mesh entitys support collision detection, reporting a hit when a test coordinate falls within the Mesh’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 Mesh entity - which might help improve rendering efficiency - set the flag to true.

        noUserInteraction: false,
  • ¶

    Anchor objects can be assigned to Mesh 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: 
        }

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

        method: 'fill',
  • ¶

    Mesh 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 Mesh entity’s border (the Shape entitys, with connecting lines between their paths’ start and end points) and fill (the image displayed between the Mesh’s struts)

    • globalAlpha
    • globalCompositeOperation

    All line attributes are supported

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

    The Mesh 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 Mesh’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.packetCoordinates = pushUnique(P.packetCoordinates, []);
    P.packetObjects = pushUnique(P.packetObjects, ['group', 'net', 'source']);
    P.packetFunctions = pushUnique(P.packetFunctions, ['onEnter', 'onLeave', 'onDown', 'onUp']);
    
    P.processPacketOut = function (key, value, includes) {
    
        let result = true;
    
        if(includes.indexOf(key) < 0 && value === this.defs[key]) result = false;
    
        return result;
    };
    
    P.finalizePacketOut = function (copy, items) {
    
        let stateCopy = JSON.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) {
    
            let a = JSON.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 = λthis;
  • ¶

    Kill management

    No additional kill functionality required

  • ¶

    Get, Set, deltaSet

    let G = P.getters,
        S = P.setters,
        D = P.deltaSetters;
  • ¶

    get - copied over from the entity mixin

    P.get = function (item) {
    
        let getter = this.getters[item];
    
        if (getter) return getter.call(this);
    
        else {
    
            let def = this.defs[item],
                state = this.state,
                val;
    
            if (typeof def != 'undefined') {
    
                val = this[item];
                return (typeof val != 'undefined') ? val : def;
            }
    
            def = state.defs[item];
    
            if (typeof def != 'undefined') {
    
                val = state[item];
                return (typeof val != 'undefined') ? val : def;
            }
            return undef;
        }
    };
  • ¶

    set - copied over from the entity mixin.

    P.set = function (items = {}) {
    
        if (Object.keys(items).length) {
    
            let setters = this.setters,
                defs = this.defs,
                state = this.state,
                stateSetters = (state) ? state.setters : {},
                stateDefs = (state) ? state.defs : {},
                predefined, stateFlag;
    
            Object.entries(items).forEach(([key, value]) => {
    
                if (key && key !== 'name' && value != null) {
    
                    predefined = setters[key];
                    stateFlag = false;
    
                    if (!predefined) {
    
                        predefined = stateSetters[key];
                        stateFlag = true;
                    }
    
                    if (predefined) predefined.call(stateFlag ? this.state : this, value);
                    else if (typeof defs[key] !== 'undefined') this[key] = value;
                    else if (typeof stateDefs[key] !== 'undefined') state[key] = value;
                }
            }, this);
        }
        return this;
    };
  • ¶

    setDelta - copied over from the entity mixin.

    P.setDelta = function (items = {}) {
    
        if (Object.keys(items).length) {
    
            let setters = this.deltaSetters,
                defs = this.defs,
                state = this.state,
                stateSetters = (state) ? state.deltaSetters : {},
                stateDefs = (state) ? state.defs : {},
                predefined, stateFlag;
    
            Object.entries(items).forEach(([key, value]) => {
    
                if (key && key !== 'name' && value != null) {
    
                    predefined = setters[key];
                    stateFlag = false;
    
                    if (!predefined) {
    
                        predefined = stateSetters[key];
                        stateFlag = true;
                    }
    
                    if (predefined) predefined.call(stateFlag ? this.state : this, value);
                    else if (typeof defs[key] !== 'undefined') this[key] = addStrings(this[key], value);
                    else if (typeof stateDefs[key] !== 'undefined') state[key] = addStrings(state[key], value);
                }
            }, this);
        }
        return this;
    };
  • ¶

    host, getHost - copied over from the position mixin.

    S.host = function (item) {
    
        if (item) {
    
            let host = artefact[item];
    
            if (host && host.here) this.host = host.name;
            else this.host = item;
        }
        else this.host = '';
    };
  • ¶

    group - copied over from the position mixin.

    G.group = function () {
    
        return (this.group) ? this.group.name : '';
    };
    S.group = function (item) {
    
        let g;
    
        if (item) {
    
            if (this.group && this.group.type === '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 === '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);
    };
  • ¶

    net

    S.net = function (item) {
    
        if (item) {
    
            item = (item.substring) ? artefact[item] : item;
    
            if (item && item.type === 'Net') {
    
                this.net = item;
                this.dirtyStart = true;
            }
        }
    };
  • ¶

    source

    S.source = function (item) {
    
        item = (item.substring) ? artefact[item] : item;
    
        if (item && item.type === 'Picture') {
    
            let src = this.source;
    
            if (src && src.type === '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;
    };
  • ¶

    Prototype functions

  • ¶

    getHost - copied over from the position mixin.

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

    updateByDelta, reverseByDelta - copied over from the position mixin.

    P.updateByDelta = function () {
    
        this.setDelta(this.delta);
    
        return this;
    };
    
    P.reverseByDelta = function () {
    
        let temp = {};
        
        Object.entries(this.delta).forEach(([key, val]) => {
    
            if (val.substring) val = -(parseFloat(val)) + '%';
            else val = -val;
    
            temp[key] = val;
        });
    
        this.setDelta(temp);
    
        return this;
    };
  • ¶

    setDeltaValues - copied over from the position mixin.

    P.setDeltaValues = function (items = {}) {
    
        let delta = this.delta, 
            oldVal, action;
    
        Object.entries(items).forEach(([key, requirement]) => {
    
            if (xt(delta[key])) {
    
                action = requirement;
    
                oldVal = delta[key];
    
                switch (action) {
    
                    case 'reverse' :
                        if (oldVal.toFixed) delta[key] = -oldVal;
  • ¶

    TODO: reverse String% (and em, etc) values

                        break;
    
                    case 'zero' :
                        if (oldVal.toFixed) delta[key] = 0;
  • ¶

    TODO: zero String% (and em, etc) values

                        break;
    
                    case 'add' :
                        break;
    
                    case 'subtract' :
                        break;
    
                    case 'multiply' :
                        break;
    
                    case 'divide' :
                        break;
                }
            }
        })
        return this;
    };
  • ¶

    Invalidate mid-init functionality

    P.midInitActions = λnull;
  • ¶

    Invalidating sensor functionality

    P.cleanCollisionData = function () {
    
        return [0, []];
    };
    P.getSensors = function () {
    
        return [];
    };
  • ¶

    Display cycle functionality

  • ¶

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

    • This function is called before we get into the entity stamp promise cascade (thus it’s a synchronous function). This is where we need to check whether we need to recalculate the path data which we’ll use later to build the Mesh 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.
    P.prepareStamp = function() {
    
        this.badNet = true;
        this.dirtyParticles = false;
    
        let {net, particlePositions} = this;
    
        if (net && net.particleStore && net.particleStore.length > 3) {
    
            let {rows, columns, particleStore} = net;
    
            if (rows && columns) {
    
                this.badNet = false;
                this.rows = rows;
                this.columns = columns;
    
                if (!particlePositions) particlePositions = [];
  • ¶

    Sanity check

    • We will recalculate stuff if any of the net particles have moved since the last check. This is most simply done by constructing a string of all current particle position values and comparing it to the previous string. If they are the same, then we can use the stashed image construct, otherwise we build and stash a new image construct
                let checkPositions = [];
    
                particleStore.forEach(p => {
    
                    let pos = p.position;
                    let {x, y} = pos;
    
                    checkPositions.push([x, y]);
                });
                let checkPositionsString = checkPositions.join(','),
                    particlePositionsString = particlePositions.join(',');
    
                if (particlePositionsString !== checkPositionsString) {
    
                    this.particlePositions = checkPositions;
                    this.dirtyInput = true;
                }
    
                if (this.sourceIsVideoOrSprite) this.dirtyInput = true;
            }
        }
    };
  • ¶

    setSourceDimension - internal function called by prepareStamp.

    • We make the source dimensions a square of the longest row ‘path’
    • This way, we can do a horizontal scan, or a vertical scan with no further calculation

    This function also:

    • Calculates the bounding box
    • Creates the perimeter path object
    • Stores the (relative) lengths of individual struts in each row

    TODO: consider drawing order of squares - is there a way we can predict which squares are going to be behind other squares …

    • For instance by adding together the particle z values for each square, then filling in the lowest square first?
    • May also be a way of calculating a cull of squares so that we don’t need to fill in squares entirely covered by other squares?
    P.setSourceDimension = function () {
    
        if (!this.badNet) {
    
            const {columns, rows, particlePositions} = this;
    
            const results = [],
                lengths = [],
                xPos = [],
                yPos = [],
                top = [],
                left = [],
                right = [],
                coords = [],
                bottom = [];
    
            let x, xz, y, res, pos, coord, x0, x1, y0, y1, dx, dy, len, l, i, iz, j, jz;
    
            for (y = 0; y < rows; y++) {
    
                res = 0;
                len = lengths[y] = [];
                coord = coords[y] = [];
    
                for (x = 0, xz = columns - 1; x < xz; x++) {
    
                    pos = (y * columns) + x; 
    
                    [x0, y0] = particlePositions[pos];
                    [x1, y1] = particlePositions[pos + 1];
    
                    coord.push([x0, y0, x1, y1]);
    
                    if (x === 0) {
    
                        left.push([x0, y0]);
                    }
                    else if (x === xz - 1) {
    
                        right.push([x1, y1]);
                    }
                    else if (y === 0) {
    
                        top.push([x0, y0]);
    
                        if (x === xz - 2) top.push([x1, y1]);
                    }
                    else if (y === rows - 1) {
    
                        bottom.push([x0, y0]);
    
                        if (x === xz - 2) bottom.push([x1, y1]);
                    }
    
                    xPos.push(x0, x1);
                    yPos.push(y0, y1);
    
                    dx = x1 - x0;
                    dy = y1 - y0;
    
                    l = Math.sqrt((dx * dx) + (dy * dy));
                    res += l;
                    len.push(l);
                }
                results.push(res);
            }
            this.sourceDimension = Math.max(...results);
  • ¶

    Sanity check - the particle system, when it breaks down, can create some massive dimension values!

            let host = this.currentHost || this.getHost();
            if (host) {
    
                let max = Math.max(...host.currentDimensions);
                if (this.sourceDimension > max) this.sourceDimension = max;
            }
    
            for (i = 0, iz = lengths.length; i < iz; i++) {
    
                l = results[i];
                len = lengths[i];
                coord = coords[i];
    
                for (j = 0, jz = len.length; j < jz; j++) {
    
                    if (l) len[j] = len[j] / l;
    
                    coord[j].push(len[j]);
                }
            }
    
            this.struts = coords;
    
            let xMin = Math.min(...xPos),
                yMin = Math.min(...yPos),
                xMax = Math.max(...xPos),
                yMax = Math.max(...yPos);
    
            this.boundingBox = [xMin, yMin, xMax - xMin, yMax - yMin];
    
            left.reverse();
            bottom.reverse();
    
            let p = `M${top[0][0]},${top[0][1]}L`;
    
            for (i = 1, iz = top.length; i < iz; i++) {
    
                [x, y] = top[i];
                p += `${x},${y} `;
            }
            for (i = 0, iz = right.length; i < iz; i++) {
    
                [x, y] = right[i];
                p += `${x},${y} `;
            }
            for (i = 0, iz = bottom.length; i < iz; i++) {
    
                [x, y] = bottom[i];
                p += `${x},${y} `;
            }
            for (i = 0, iz = left.length; i < iz; i++) {
    
                [x, y] = left[i];
                p += `${x},${y} `;
            }
            p += 'z';
    
            this.pathObject = new Path2D(p);
        }
    };
  • ¶

    simpleStamp - Simple stamping is entirely synchronous

    • TODO: we may have to disable this functionality for the Mesh entity, if we use a web worker for either the prepareStamp calculations, or to build the output image itself
    P.simpleStamp = function (host, changes = {}) {
    
        if (host && host.type === 'Cell') {
    
            this.currentHost = host;
            
            if (changes) {
    
                this.set(changes);
                this.prepareStamp();
            }
    
            this.regularStampSynchronousActions();
        }
    };
  • ¶

    stamp - All entity stamping, except for simple stamps, goes through this function, which needs to return a Promise which will resolve in due course.

    • While other entitys have to worry about applying filters as part of the stamping process, this is not an issue for Mesh entitys because filters are defined on, and applied to, the source Picture entity, not the Mesh 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)
    P.stamp = function (force = false, host, changes) {
    
        if (force) {
    
            if (host && host.type === 'Cell') this.currentHost = host;
    
            if (changes) {
    
                this.set(changes);
                this.prepareStamp();
            }
            return this.regularStamp();
        }
    
        if (this.visibility) {
    
            let self = this,
                dirtyInput = this.dirtyInput;
    
            if (dirtyInput) {
    
                return new Promise((resolve, reject) => {
    
                    self.cleanInput()
                    .catch(err => {
  • ¶

    We don’t need to completely reject if output is not clean

    • It should be enough to bale out of the stamp functionality and hope it resolves during the next RAF iteration
                        console.log(`${self.name} - cleanInput Error: source has a zero dimension`);
                        resolve(false);
                    })
                    .then(res => {
    
                        self.sourceImageData = res;
                        return self.cleanOutput();
                    })
                    .then(res => {
    
                        self.output = res;
                        return self.regularStamp();
                    })
                    .then(res => {
    
                        resolve(true);
                    })
                    .catch(err => {
                        
                        reject(err);
                    });
                })
            }
            else return this.regularStamp();
        }
        else return Promise.resolve(false);
    };
  • ¶

    Clean functions

  • ¶

    cleanInput - internal function called by stamp

    P.cleanInput = function () {
    
        let self = this;
    
        return new Promise((resolve, reject) => {
    
            self.dirtyInput = false;
    
            self.setSourceDimension();
    
            let sourceDimension = self.sourceDimension;
    
            if (!sourceDimension) {
    
                self.dirtyInput = true;
                reject();
            }
    
            let cell = requestCell(),
                engine = cell.engine,
                canvas = cell.element;
    
            canvas.width = sourceDimension;
            canvas.height = sourceDimension;
            engine.setTransform(1, 0, 0, 1, 0, 0);
    
            self.source.stamp(true, cell, { 
                startX: 0,
                startY: 0,
                handleX: 0,
                handleY: 0,
                offsetX: 0,
                offsetY: 0,
                roll: 0,
                scale: 1,
    
                width: sourceDimension,
                height: sourceDimension,
    
                method: 'fill',
            })
            .then(res => {
    
                let sourceImageData = engine.getImageData(0, 0, sourceDimension, sourceDimension);
    
                releaseCell(cell);
                resolve(sourceImageData);
            })
            .catch(err => {
    
                releaseCell(cell);
                reject(err);
            });
        });
    };
  • ¶

    cleanOutput - internal function called by stamp

    • If you’re not a fan of big functions, please look away now.
    P.cleanOutput = function () {
        
        let self = this;
    
        return new Promise((resolve, reject) => {
    
            const halfPi = Math.PI / 2;
    
            self.dirtyOutput = false;
    
            let {sourceDimension, sourceImageData, columns, rows, struts, boundingBox} = self;
    
            sourceDimension = Math.ceil(sourceDimension);
    
            if (sourceImageData && rows - 1 > 0) {
    
                let [startX, startY, outputWidth, outputHeight] = boundingBox;
    
                outputWidth += startX;
                outputHeight += startY;
    
                const inputCell = requestCell(),
                    inputEngine = inputCell.engine,
                    inputCanvas = inputCell.element;
    
                inputCanvas.width = sourceDimension;
                inputCanvas.height = sourceDimension;
                inputEngine.setTransform(1, 0, 0, 1, 0, 0);
                inputEngine.putImageData(sourceImageData, 0, 0);
    
                const outputCell = requestCell(),
                    outputEngine = outputCell.engine,
                    outputCanvas = outputCell.element;
    
                outputCanvas.width = outputWidth;
                outputCanvas.height = outputHeight;
                outputEngine.globalAlpha = self.state.globalAlpha;
                outputEngine.setTransform(1, 0, 0, 1, 0, 0);
    
                const inputStrutHeight = parseFloat((sourceDimension / (rows - 1)).toFixed(4)),
                    inputStrutWidth = parseFloat((sourceDimension / (columns - 1)).toFixed(4));
    
                let topStruts, baseStruts,
                    maxLen, tStep, bStep, iStep, xtStep, ytStep, xbStep, ybStep, tx, ty, bx, by, sx, sy,
                    xLen, yLen, stripLength, stripAngle,
                    c, cz, r, rz, i, iz;
    
                for (r = 0, rz = rows - 1; r < rz; r++) {
    
                    topStruts = struts[r];
                    baseStruts = struts[r + 1];
    
                    for (c = 0, cz = columns - 1; c < cz; c++) {
    
                        let [ltx, lty, rtx, rty, tLen] = topStruts[c];
                        let [lbx, lby, rbx, rby, bLen] = baseStruts[c];
    
                        tLen *= sourceDimension;
                        bLen *= sourceDimension;
    
                        maxLen = Math.max(tLen, bLen, inputStrutWidth);
    
                        tStep = tLen / maxLen;
                        bStep = bLen / maxLen;
                        iStep = inputStrutWidth / maxLen;
    
                        xtStep = (rtx - ltx) / maxLen;
                        ytStep = (rty - lty) / maxLen;
                        xbStep = (rbx - lbx) / maxLen;
                        ybStep = (rby - lby) / maxLen;
    
                        for (i = 0; i < maxLen; i++) {
    
                            tx = ltx + (xtStep * i);
                            ty = lty + (ytStep * i);
                            bx = lbx + (xbStep * i);
                            by = lby + (ybStep * i);
                            sy = r * inputStrutHeight;
                            sx = (c * inputStrutWidth) + (iStep * i);
    
                            xLen = tx - bx;
                            yLen = ty - by;
                            stripLength = Math.sqrt((xLen * xLen) + (yLen * yLen));
                            stripAngle = Math.atan2(yLen, xLen) + halfPi;
    
                            outputEngine.setTransform(1, 0, 0, 1, tx, ty);
                            outputEngine.rotate(stripAngle);
  • ¶

    Safari bugfix because we fall foul of of the Safari source-out-of-bounds bug

    • Stack Overflow question identifying the issue
                            let testHeight = (sy + inputStrutHeight > sourceDimension) ? sourceDimension - sy : inputStrutHeight;
    
                            outputEngine.drawImage(inputCanvas, sx, sy, 1, testHeight, 0, 0, 1, stripLength);
                        }
                    }
                }
    
                let iFactor = self.interferenceFactor,
                    iLoops = self.interferenceLoops,
    
                    iWidth = Math.ceil(outputWidth * iFactor),
                    iHeight = Math.ceil(outputHeight * iFactor);
    
                inputCanvas.width = iWidth;
                inputCanvas.height = iHeight;
    
                outputEngine.setTransform(1, 0, 0, 1, 0, 0);
                inputEngine.setTransform(1, 0, 0, 1, 0, 0);
    
                for (let 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);
                }
    
                let outputData = outputEngine.getImageData(0, 0, outputWidth, outputHeight);
    
                releaseCell(inputCell);
                releaseCell(outputCell);
    
                self.dirtyTargetImage = true;
    
                resolve(outputData);
            }
            else reject(new Error(`${this.name} - cleanOutput Error: source has a zero dimension, or no data`));
        });
    };
  • ¶

    regularStamp - internal function called by stamp

    P.regularStamp = function () {
    
        let self = this;
    
        return new Promise((resolve, reject) => {
    
            if (self.currentHost) {
    
                self.regularStampSynchronousActions();
                resolve(true);
            }
            reject(new Error(`${self.name} has no current host`));
        });
    };
  • ¶

    regularStampSynchronousActions - internal function called by regularStamp

    P.regularStampSynchronousActions = function () {
    
        let dest = this.currentHost;
    
        if (dest) {
    
            let engine = dest.engine;
    
            if (!this.noCanvasEngineUpdates) dest.setEngine(this);
    
            this[this.method](engine);
        }
    };
  • ¶
    Stamp methods

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

  • ¶

    fill

    P.fill = function (engine) {
    
        this.doFill(engine);
    };
  • ¶

    draw

    P.draw = function (engine) {
    
        this.doStroke(engine);
    };
  • ¶

    drawAndFill

    P.drawAndFill = function (engine) {
    
        this.doStroke(engine);
        this.currentHost.clearShadow();
        this.doFill(engine);
    };
  • ¶

    fillAndDraw

    P.fillAndDraw = function (engine) {
    
        this.doFill(engine);
        this.currentHost.clearShadow();
        this.doStroke(engine);
    };
  • ¶

    drawThenFill

    P.drawThenFill = function (engine) {
    
        this.doStroke(engine);
        this.doFill(engine);
    };
  • ¶

    fillThenDraw

    P.fillThenDraw = function (engine) {
    
        this.doFill(engine);
        this.doStroke(engine);
    };
  • ¶

    clear

    P.clear = function (engine) {
    
        let output = this.output,
            canvas = (this.currentHost) ? this.currentHost.element : false,
            gco = engine.globalCompositeOperation;
    
        if (output && canvas) {
    
            let tempCell = requestCell(),
                tempEngine = tempCell.engine,
                tempCanvas = tempCell.element;
    
            let w = canvas.width,
                h = canvas.height;
    
            tempCanvas.width = w;
            tempCanvas.height = h;
    
            tempEngine.putImageData(output, 0, 0);
            engine.setTransform(1, 0, 0, 1, 0, 0);
            engine.globalCompositeOperation = 'destination-out';
            engine.drawImage(tempCanvas, 0, 0);
            engine.globalCompositeOperation = gco;
    
            releaseCell(tempCell);
        }
    };
  • ¶

    none

    P.none = λnull;
  • ¶

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

  • ¶

    doStroke

    P.doStroke = function (engine) {
    
        engine.setTransform(1, 0, 0, 1, 0, 0);
        engine.stroke(this.pathObject);
    };
  • ¶

    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) {
    
        let output = this.output,
            canvas = (this.currentHost) ? this.currentHost.element : false;
    
        if (output && canvas) {
    
            let tempCell = requestCell(),
                tempEngine = tempCell.engine,
                tempCanvas = tempCell.element;
    
            let w = canvas.width,
                h = canvas.height;
    
            tempCanvas.width = w;
            tempCanvas.height = h;
    
            tempEngine.putImageData(output, 0, 0);
            engine.setTransform(1, 0, 0, 1, 0, 0);
            engine.drawImage(tempCanvas, 0, 0);
    
            releaseCell(tempCell);
        }
    };
  • ¶

    Collision functionality

  • ¶

    checkHit

    • Overwrites mixin/position.js function
    P.checkHit = function (items = [], mycell) {
    
        if (this.noUserInteraction) return false;
    
        if (!this.pathObject) return false;
    
        let tests = (!Array.isArray(items)) ?  [items] : items,
            poolCellFlag = false;
    
        if (!mycell) {
    
            mycell = requestCell();
            poolCellFlag = true;
        }
    
        let engine = mycell.engine,
            tx, ty;
    
        if (tests.some(test => {
    
            if (Array.isArray(test)) {
    
                tx = test[0];
                ty = test[1];
            }
            else if (xta(test, test.x, test.y)) {
    
                tx = test.x;
                ty = test.y;
            }
            else return false;
    
            if (!tx.toFixed || !ty.toFixed || isNaN(tx) || isNaN(ty)) return false;
    
            return engine.isPointInPath(this.pathObject, tx, ty, this.winding);
    
        }, this)) {
    
            let r = {
                x: tx,
                y: ty,
                artefact: this
            };
    
            if (poolCellFlag) releaseCell(mycell);
    
            return r;
        }
        
        if (poolCellFlag) releaseCell(mycell);
        
        return false;
    };
  • ¶

    Factory

    let myMesh = scrawl.makeMesh({
  • ¶
    name: 'display-mesh',
  • ¶
    net: 'test-net',
    source: 'my-flower',
  • ¶
    lineWidth: 2,
    lineJoin: 'round',
    strokeStyle: 'orange',
  • ¶
    method: 'fillThenDraw',
  • ¶
    onEnter: function () { this.set({ lineWidth: 6 }) },
    onLeave: function () { this.set({ lineWidth: 2 }) },

    });

    const makeMesh = function (items) {
        return new Mesh(items);
    };
    
    constructors.Mesh = Mesh;
  • ¶

    Exports

    export {
        makeMesh,
    };