• 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
  • §

    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.
  • §

    Imports

    import { artefact, constructors, group } from '../core/library.js';
    
    import { addStrings, doCreate, mergeOver, pushUnique, 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 { releaseArray, requestArray } from '../helper/array-pool.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, _isArray, _isFinite, _keys, _max, _min, _parse, _piHalf, _sqrt, ARG_SPLITTER, DESTINATION_OUT, ENTITY, FILL, NAME, STATE_KEYS, T_CELL, T_GROUP, T_NET, T_PICTURE, UNDEF, ZERO_STR } from '../helper/shared-vars.js';
  • §

    Local constants

    const T_MESH = 'Mesh';
  • §

    Mesh constructor

    const Mesh = function (items = Ωempty) {
    
        this.makeName(items.name);
        this.register();
    
        this.modifyConstructorInputForAnchorButton(items);
    
        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.currentHost = null;
        this.dirtyHost = true;
        this.badNet = true;
        this.dirtyParticles = true;
        this.particlePositions = null;
        this.sourceImageData = null;
        this.sourceDimension = 0;
        this.rows = null;
        this.columns = null;
        this.struts = null;
        this.boundingBox = null;
        this.pathObject = null;
        this.dirtyOutput = true;
        this.dirtyTargetImage = true;
        this.output = null;
    
        this.set(items);
    
        this.fromPathData = [];
        this.toPathData = [];
    
        this.watchFromPath = null;
        this.watchIndex = -1;
        this.engineInstructions = [];
        this.engineDeltaLengths = [];
    
        return this;
    };
  • §

    Mesh prototype

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

    Mixins

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

    Mesh attributes

    const 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,
        calculateOrder: 0,
        stampOrder: 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.packetObjects = pushUnique(P.packetObjects, ['group', 'net', 'source']);
    P.packetFunctions = pushUnique(P.packetFunctions, ['onEnter', 'onLeave', 'onDown', 'onUp']);
    
    P.processPacketOut = function (key, value, incl) {
    
        let result = true;
    
        if(!incl.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;
  • §

    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 (typeof def !== UNDEF) {
    
                val = this[item];
                return (typeof val !== UNDEF) ? val : def;
            }
    
            def = state.defs[item];
    
            if (typeof def !== UNDEF) {
    
                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;
                            this.dirtyFilterIdentifier = true;
                        }
                    }
                    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);
                            this.dirtyFilterIdentifier = true;
                        }
                    }
                    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;
        this.dirtyFilterIdentifier = true;
    };
  • §

    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;
    };
  • §

    net

    S.net = function (item) {
    
        if (item) {
    
            item = (item.substring) ? artefact[item] : item;
    
            if (item && item.type === T_NET) {
    
                this.net = item;
                this.dirtyStart = true;
            }
            this.dirtyFilterIdentifier = 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;
            this.dirtyFilterIdentifier = true;
        }
    };
  • §

    isHorizontalCopy

    S.isHorizontalCopy = function (item) {
    
        this.isHorizontalCopy = (item) ? true : false;
        this.dirtyPathData = true;
        this.dirtyFilterIdentifier = 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;
  • §

    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 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;
    
        if (!this.particlePositions) this.particlePositions = [];
    
        const {net, particlePositions} = this;
    
        if (net && net.particleStore && net.particleStore.length > 3) {
    
            const {rows, columns, particleStore} = net;
    
            if (rows && columns) {
    
                this.badNet = false;
                this.rows = rows;
                this.columns = columns;
  • §

    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
                const checkPositions = [];
    
                particleStore.forEach(p => {
    
                    const pos = p.position;
                    const {x, y} = pos;
    
                    checkPositions.push([x, y]);
                });
                const checkPositionsString = checkPositions.join(ARG_SPLITTER),
                    particlePositionsString = particlePositions.join(ARG_SPLITTER);
    
                if (particlePositionsString !== checkPositionsString) {
    
                    this.particlePositions = checkPositions;
                    this.dirtyInput = true;
                }
    
                if (this.sourceIsVideoOrSprite) this.dirtyInput = true;
            }
        }
  • §

    prepareStampTabsHelper is defined in the mixin/hidden-dom-elements.js file - handles updates to anchor and button objects

        this.prepareStampTabsHelper();
    };
  • §

    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 = requestArray(),
                lengths = requestArray(),
                xPos = requestArray(),
                yPos = requestArray(),
                top = requestArray(),
                left = requestArray(),
                right = requestArray(),
                coords = requestArray(),
                bottom = requestArray();
    
            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] = requestArray();
                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 = _sqrt((dx * dx) + (dy * dy));
                    res += l;
                    len.push(l);
                }
                results.push(res);
            }
            this.sourceDimension = _max(...results);
  • §

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

            const host = this.currentHost || this.getHost();
            if (host) {
    
                const max = _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]);
                }
            }
    
            if (!this.struts) this.struts = [];
            this.struts.length = 0;
            this.struts.push(...coords);
    
            const xMin = _min(...xPos),
                yMin = _min(...yPos),
                xMax = _max(...xPos),
                yMax = _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);
    
            releaseArray(...lengths, lengths);
            releaseArray(results, xPos, yPos, top, left, right, bottom, coords);
        }
    };
  • §

    simpleStamp - Simple stamping is entirely synchronous

    • TODO: we may have to disable this functionality for the Mesh 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 && host.type === T_CELL) {
    
            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 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 === T_CELL) this.currentHost = host;
    
            if (changes) {
    
                this.set(changes);
                this.prepareStamp();
            }
            this.regularStamp();
        }
    
        else if (this.visibility) {
    
            if (this.dirtyInput) {
    
                this.sourceImageData = this.cleanInput();
    
                if (this.sourceImageData) this.output = this.cleanOutput();
                else this.dirtyInput = true;
            }
    
            if (this.output) this.regularStamp();
        }
    };
  • §

    Clean functions

  • §

    cleanInput - internal function called by stamp

    P.cleanInput = function () {
    
        this.dirtyInput = false;
    
        this.setSourceDimension();
    
        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,
        })
        const sourceImageData = engine.getImageData(0, 0, sourceDimension, sourceDimension);
    
        releaseCell(mycell);
        return sourceImageData;
    };
  • §

    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 {sourceImageData, columns, rows, struts, boundingBox} = this;
    
        const sourceDimension = _ceil(this.sourceDimension);
    
        if (sourceImageData && rows - 1 > 0) {
    
    /* eslint-disable-next-line */
            let [startX, startY, outputWidth, outputHeight] = boundingBox;
    
            outputWidth = ~~(startX + outputWidth);
            outputHeight = ~~(startY + outputHeight);
    
            const inputCell = requestCell(),
                inputEngine = inputCell.engine,
                inputCanvas = inputCell.element;
    
            inputCanvas.width = sourceDimension;
            inputCanvas.height = sourceDimension;
            inputEngine.resetTransform();
            inputEngine.putImageData(sourceImageData, 0, 0);
    
            const outputCell = requestCell(),
                outputEngine = outputCell.engine,
                outputCanvas = outputCell.element;
    
            outputCanvas.width = outputWidth;
            outputCanvas.height = outputHeight;
            outputEngine.globalAlpha = this.state.globalAlpha;
            outputEngine.resetTransform();
    
            const inputStrutHeight = parseFloat((sourceDimension / (rows - 1)).toFixed(4)),
                inputStrutWidth = parseFloat((sourceDimension / (columns - 1)).toFixed(4));
    
            let topStruts, baseStruts,
                maxLen, iStep, xtStep, ytStep, xbStep, ybStep, tx, ty, bx, by, sx, sy,
                xLen, yLen, stripLength, stripAngle,
                c, cz, r, rz, i;
    
            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++) {
    
    /* eslint-disable-next-line */
                    let [ltx, lty, rtx, rty, tLen] = topStruts[c];
    /* eslint-disable-next-line */
                    let [lbx, lby, rbx, rby, bLen] = baseStruts[c];
    
                    tLen *= sourceDimension;
                    bLen *= sourceDimension;
    
                    maxLen = _max(tLen, bLen, inputStrutWidth);
    
                    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 = _sqrt((xLen * xLen) + (yLen * yLen));
                        stripAngle = _atan2(yLen, xLen) + _piHalf;
    
                        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
                        const testHeight = (sy + inputStrutHeight > sourceDimension) ? sourceDimension - sy : inputStrutHeight;
    
                        outputEngine.drawImage(inputCanvas, ~~sx, ~~sy, 1, ~~testHeight, 0, 0, 1, ~~stripLength);
                    }
                }
            }
    
            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 (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);
            }
    
            const outputData = outputEngine.getImageData(0, 0, outputWidth, outputHeight);
    
            releaseCell(inputCell, outputCell);
    
            this.dirtyTargetImage = true;
    
            return outputData;
        }
        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);
        }
    };
  • §
    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) {
    
        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;
    
            const w = canvas.width,
                h = canvas.height;
    
            tempCanvas.width = w;
            tempCanvas.height = h;
    
            tempEngine.putImageData(output, 0, 0);
            engine.resetTransform();
            engine.globalCompositeOperation = DESTINATION_OUT;
            engine.drawImage(tempCanvas, 0, 0);
            engine.globalCompositeOperation = gco;
    
            releaseCell(tempCell);
        }
    };
  • §

    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 Mesh entity onto a canvas cell.

  • §

    doStroke

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

    Collision functionality

  • §

    checkHit

    • Overwrites mixin/position.js function
    P.checkHit = function (items = []) {
    
        if (this.noUserInteraction) return false;
    
        if (!this.pathObject) return false;
    
        const tests = (!_isArray(items)) ?  [items] : items;
    
        let tx, ty;
    
        const mycell = requestCell();
    
        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;
    
            return mycell.engine.isPointInPath(this.pathObject, tx, ty, this.winding);
    
        }, this)) {
    
            const r = {
                x: tx,
                y: ty,
                artefact: this
            };
    
            releaseCell(mycell);
            return r;
        }
    
        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 }) },
    

    });

    
    
    export const makeMesh = function (items) {
    
        if (!items) return false;
        return new Mesh(items);
    };
    
    constructors.Mesh = Mesh;