• 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/gradient-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
  • gradient-engine.js

  • §

    Imports

    import { constructors } from '../core/library.js';
    
    import { doCreate, easeEngines, isa_fn, λfirstArg } from './utilities.js';
    
    import { releaseCell, requestCell } from '../untracked-factory/cell-fragment.js';
    import { checkForWorkstoreItem, getWorkstoreItem, setWorkstoreItem } from './workstore.js';
    import { seededRandomNumberGenerator } from './random-seed.js';
    import { bluenoise, orderedNoise } from './filter-engine-bluenoise-data.js';
    import { makeNoiseAsset } from '../asset-management/noise-asset.js';
  • §

    Shared constants

    import { _abs, _atan2, _ceil, _cos, _floor, _isFinite, _max, _min, _radian, _sin, _sqrt, ADD_EASE, ADD_MAP_CONTOUR, ADD_MAP_DISPLACE, ADD_MAP_EASE, ADD_MAP_FLOW, ADD_MAP_ROTATE, ADD_MAP_THRESHOLD, ADD_MAP_WARP, ADD_NOISE, ADD_RIPPLE, ADD_WAVE, AFTER_SPREAD, BEFORE_SPREAD, BLUENOISE, BOTTOM, CENTER, DEFAULT_SEED, LEFT, ON_COORDINATES, ORDERED, PATH_ENTITY, PERMITTED_NOISE, RANDOM, REFLECT, REPEAT, RIGHT, T_GRADIENT, T_RADIAL_GRADIENT, T_CONIC_GRADIENT, TOP, TRANSPARENT } from './shared-vars.js';
  • §

    Local constants

    const T_GRADIENT_ENGINE = 'GradientEngine',
        X = 'x',
        Y = 'y',
        BOTH = 'both';
  • §

    Build out the local noise asset

    const noiseAsset = makeNoiseAsset({
        name: 'SC-gradient-engine-noise-asset',
    });
    
    const noiseDefs = noiseAsset.stateAttributeDefaults;
  • §

    GradientEngine constructor

    const GradientEngine = function () { return this };
  • §

    GradientEngine prototype

    const P = GradientEngine.prototype = doCreate();
    P.type = T_GRADIENT_ENGINE;
  • §

    The main entry into the engine

    P.action = function (packet) {
    
        const { entity, fixedGradientData: gradient, identifier, imageData, matrix } = packet;
    
        if (identifier) {
  • §

    Entity has a cached imageData object in the workstore?

            const itemInWorkstore = checkForWorkstoreItem(identifier);
            if (itemInWorkstore) return true;
    
            if (!gradient.identifier) return false;
  • §

    See if we have a stashed gradient

            const { width, height, data } = imageData;
    
            const gradientId = `${gradient.identifier}-${width}-${height}`;
    
            let gradientData = getWorkstoreItem(gradientId);
  • §

    Build the gradient ImageData object, if required

            if (!gradientData) {
    
                updateOperationsCache(gradient.operations);
    
                const coords = getGradientCoordinates(gradient, width, height);
    
                if (!coords.length) return false;
    
                if (gradient.type === T_GRADIENT) gradientData = applyLinearGradient(gradient, coords, width, height);
                if (gradient.type === T_RADIAL_GRADIENT) gradientData = applyRadialGradient(gradient, coords, width, height);
                if (gradient.type === T_CONIC_GRADIENT) gradientData = applyConicGradient(gradient, coords, width, height);
    
                if (gradientData) setWorkstoreItem(gradientId, gradientData);
                else return false;
            }
  • §

    Apply the gradient to the entity

            const workData = new ImageData(new Uint8ClampedArray(data.length), width, height);
    
            const result = {
                x: 0,
                y: 0,
                w: 0,
                h: 0,
                imageData: null,
            };
    
            if (gradient.lockedToEntity) applyEntityLockedGradient(result, imageData, workData, gradientData, gradientId, width, height, entity, matrix);
            else  applyCellLockedGradient(result, imageData, workData, gradientData, width, height);
  • §

    Store the entity’s gradient-applied image

            if (result.w && result.h && result.imageData) {
    
                setWorkstoreItem(identifier, result);
                return true;
            }
        }
  • §

    If we failed to generate an image for the gradient, return false

    • The fillStyle or strokeStyle of the affected entity will display as transparent, or black
        return false;
    };
  • §

    Gradient actions

    const getGradientCoordinates = function (gradient, width, height) {
    
        const cleanPosition = (val, dim)  => {
    
            let res = 0;
    
            if (val.toFixed) res = val;
            else if (val === LEFT || val === TOP) res = 0;
            else if (val === RIGHT || val === BOTTOM) res = dim;
            else if (val === CENTER) res = dim / 2;
            else if (!_isFinite(parseFloat(val))) res = 0;
            else res = (parseFloat(val) / 100) * dim;
    
            return res;
        };
    
        const cleanRadius = function (val, dim) {
    
            if (_isFinite(val)) return val;
    
            else {
    
                val = parseFloat(val);
    
                if (!_isFinite(val)) return 0;
    
                return ( val / 100) * dim;
            }
        };
    
        switch (gradient.type) {
    
            case T_GRADIENT : {
    
                let [sx, sy] = gradient.start;
                let [ex, ey] = gradient.end;
    
                sx = cleanPosition(sx, width);
                sy = cleanPosition(sy, height);
                ex = cleanPosition(ex, width);
                ey = cleanPosition(ey, height);
    
                return [sx, sy, ex, ey];
            }
    
            case T_RADIAL_GRADIENT : {
    
                let [sx, sy] = gradient.start;
                let [ex, ey] = gradient.end;
    
                let sr = gradient.startRadius,
                    er = gradient.endRadius;
    
                sx = cleanPosition(sx, width);
                sy = cleanPosition(sy, height);
                sr = cleanRadius(sr, width);
    
                ex = cleanPosition(ex, width);
                ey = cleanPosition(ey, height);
                er = cleanRadius(er, width);
    
                return [sx, sy, sr, ex, ey, er];
            }
    
            case T_CONIC_GRADIENT : {
    
                let [sx, sy] = gradient.start;
    
                sx = cleanPosition(sx, width);
                sy = cleanPosition(sy, height);
    
                const a = gradient.angle * _radian;
    
                return [a, sx, sy];
            }
    
            default :
                return [];
        }
    };
    
    const applyLinearGradient = function (gradient, coords, width, height) {
    
        if (!gradient.stopsData || !coords.length) return false;
    
        const [x0, y0, x1, y1] = coords,
            dx = x1 - x0,
            dy = y1 - y0,
            len2 = (dx * dx) + (dy * dy);
    
        if (!len2) return false;
    
        const { gData, pixels, pz, engines } = getGradientRenderData(gradient, width, height),
            { onCoordinatesEngine } = engines;
    
        const xStep = dx / len2,
            yStep = dy / len2,
            rowReset = yStep - (width * xStep);
    
        if (onCoordinatesEngine === λfirstArg) {
    
            let p = 0,
                rowEnd = width,
                t = ((-x0 * dx) + (-y0 * dy)) / len2,
                color;
    
            for (; p < pz; p++) {
    
                color = getGradientColor(gradient, t, engines, p, width, height);
    
                if (color) pixels[p] = color;
    
                t += xStep;
    
                if (p + 1 === rowEnd) {
    
                    t += rowReset;
                    rowEnd += width;
                }
            }
        }
        else {
    
            const coord = [0, 0];
    
            let p = 0,
                x = 0,
                y = 0,
                color, sampleX, sampleY, t;
    
            for (; p < pz; p++) {
    
                coord[0] = x;
                coord[1] = y;
    
                onCoordinatesEngine(coord, p, width, height);
    
                sampleX = coord[0];
                sampleY = coord[1];
    
                t = (((sampleX - x0) * dx) + ((sampleY - y0) * dy)) / len2;
    
                color = getGradientColor(gradient, t, engines, p, width, height);
                if (color) pixels[p] = color;
    
                x++;
                if (x >= width) {
    
                    x = 0;
                    y++;
                }
            }
        }
    
        return gData;
    };
    
    const applyRadialGradient = function (gradient, coords, width, height) {
    
        if (!gradient.stopsData || !coords.length) return false;
    
        const [x0, y0, r0, x1, y1, r1] = coords,
            cx = x1 - x0,
            cy = y1 - y0,
            cr = r1 - r0,
            qa = (cx * cx) + (cy * cy) - (cr * cr),
            nearZeroQa = _abs(qa) < 0.000001,
            twoQa = 2 * qa;
    
        const { gData, pixels, pz, engines } = getGradientRenderData(gradient, width, height),
            { onCoordinatesEngine } = engines;
    
        const coord = [0, 0];
    
        let p = 0,
            x = 0,
            y = 0,
            sampleX, sampleY,
            px, py,
            qb, qc, disc, root,
            t, t1, t2,
            rad1, rad2,
            color;
    
        for (; p < pz; p++) {
    
            if (onCoordinatesEngine === λfirstArg) {
    
                sampleX = x;
                sampleY = y;
            }
            else {
    
                coord[0] = x;
                coord[1] = y;
    
                onCoordinatesEngine(coord, p, width, height);
    
                sampleX = coord[0];
                sampleY = coord[1];
            }
    
            px = sampleX - x0;
            py = sampleY - y0;
    
            qb = -2 * ((px * cx) + (py * cy) + (r0 * cr));
            qc = (px * px) + (py * py) - (r0 * r0);
    
            t = null;
    
            if (nearZeroQa) {
    
                if (_abs(qb) >= 0.000001) {
    
                    t = -qc / qb;
    
                    if ((r0 + (t * cr)) < 0) t = null;
                }
            }
            else {
    
                disc = (qb * qb) - (4 * qa * qc);
    
                if (disc >= 0) {
    
                    root = _sqrt(disc);
    
                    t1 = (-qb - root) / twoQa;
                    t2 = (-qb + root) / twoQa;
    
                    rad1 = r0 + (t1 * cr);
                    rad2 = r0 + (t2 * cr);
    
                    if (rad1 >= 0 && rad2 >= 0) t = (t1 < t2) ? t1 : t2;
                    else if (rad1 >= 0) t = t1;
                    else if (rad2 >= 0) t = t2;
                }
            }
    
            if (t != null) {
    
                color = getGradientColor(gradient, t, engines, p, width, height);
                if (color) pixels[p] = color;
            }
    
            x++;
            if (x === width) {
    
                x = 0;
                y++;
            }
        }
        return gData;
    };
    
    const applyConicGradient = function (gradient, coords, width, height) {
    
        if (!gradient.stopsData || !coords.length) return false;
    
        const startAngle = coords[0],
            cx = coords[1],
            cy = coords[2];
    
        let angleRange = parseFloat(gradient.angleRange);
    
        if (!_isFinite(angleRange) || angleRange <= 0) angleRange = 360;
        else if (angleRange > 360) angleRange = 360;
    
        const range = angleRange * _radian,
            fullCircle = angleRange >= 360;
    
        let swirlDistance = parseFloat(gradient.swirlDistance);
    
        if (!_isFinite(swirlDistance) || swirlDistance <= 0) swirlDistance = 0;
    
        const swirlClockwise = gradient.swirlClockwise !== false,
            swirlDirection = swirlClockwise ? 1 : -1;
    
        const { gData, pixels, pz, engines } = getGradientRenderData(gradient, width, height),
            { onCoordinatesEngine } = engines;
    
        const tau = Math.PI * 2,
            spare = tau - range,
            halfSpare = spare / 2,
            swirlFactor = swirlDistance ? (swirlDirection * tau / swirlDistance) : 0,
            coord = [0, 0];
    
        let p = 0,
            x = 0,
            y = 0,
            sampleX, sampleY,
            dx, dy, angle, diff,
            t, color;
    
        for (; p < pz; p++) {
    
            if (onCoordinatesEngine === λfirstArg) {
    
                sampleX = x;
                sampleY = y;
            }
            else {
    
                coord[0] = x;
                coord[1] = y;
    
                onCoordinatesEngine(coord, p, width, height);
    
                sampleX = coord[0];
                sampleY = coord[1];
            }
    
            dx = sampleX - cx;
            dy = sampleY - cy;
    
            angle = _atan2(dy, dx);
    
            if (swirlFactor) angle += _sqrt((dx * dx) + (dy * dy)) * swirlFactor;
    
            diff = (angle - startAngle) % tau;
            if (diff < 0) diff += tau;
    
            t = diff / range;
    
            color = getGradientColor(gradient, t, engines, p, width, height, {
                fullCircle,
                diff,
                range,
                halfSpare,
            });
    
            if (color) pixels[p] = color;
    
            x++;
            if (x === width) {
    
                x = 0;
                y++;
            }
        }
        return gData;
    };
  • §

    Helper function

    const clampUnit = function (v) {
    
        if (v < 0) return 0;
        if (v > 1) return 1;
        return v;
    };
    
    const getGradientColor = function (gradient, t, engines, p, width, height, conicData = null) {
    
        const {
            beforeSpreadEngine,
            afterSpreadEngine,
            easingEngine,
        } = engines;
    
        const spread = gradient.spread,
            stopsData = gradient.stopsData;
    
        t = beforeSpreadEngine(t, p, width, height);
    
        if (spread === TRANSPARENT) {
    
            if (conicData && conicData.fullCircle) t -= _floor(t);
            else if (t < 0 || t > 1) return 0;
        }
        else if (spread === REPEAT) {
    
            t -= _floor(t);
        }
        else if (spread === REFLECT) {
    
            t %= 2;
            if (t < 0) t += 2;
            if (t > 1) t = 2 - t;
        }
        else {
    
            if (
                conicData &&
                !conicData.fullCircle &&
                t > 1
            ) {
    
                t = ((conicData.diff - conicData.range) <= conicData.halfSpare) ? 1 : 0;
            }
            else t = clampUnit(t);
        }
    
        t = clampUnit(t);
    
        t = afterSpreadEngine(t, p, width, height);
    
        if (easingEngine) t = easingEngine(t);
    
        t = clampUnit(t);
    
        return stopsData[getPaletteIndex(gradient, t)];
    };
    
    const getGradientRenderData = function (gradient, width, height) {
    
        const gData = new ImageData(width, height),
            d = gData.data,
            pixels = new Uint32Array(d.buffer, d.byteOffset, d.byteLength >>> 2),
            easing = gradient.easing;
    
        return {
            gData,
            pixels,
            pz: pixels.length,
            engines: {
                beforeSpreadEngine: getBeforeSpreadOperation(gData),
                afterSpreadEngine: getAfterSpreadOperation(gData),
                onCoordinatesEngine: getOnCoordinatesOperation(gData),
                easingEngine: isa_fn(easing) ? easing : easeEngines[easing],
            },
        };
    };
    
    
    const getPaletteIndex = function (gradient, t) {
    
        const start = gradient.paletteStart,
            end = gradient.paletteEnd,
            cycle = gradient.cyclePalette;
    
        let index, span;
    
        if (start === end) return start;
    
        if (start < end) {
    
            span = end - start;
            index = start + (t * span);
        }
        else if (cycle) {
    
            span = (1000 - start) + end;
            index = start + (t * span);
    
            if (index > 999) index -= 1000;
        }
        else {
    
            span = start - end;
            index = start - (t * span);
        }
    
        index = _floor(index);
    
        if (index < 0) index = 0;
        else if (index > 999) index = 999;
    
        return index;
    };
  • §

    Apply gradient data to entitys

    const applyEntityLockedGradient = function (result, imageData, workData, gradientData, gradientId, width, height, entity, matrix) {
    
        const pathBased = PATH_ENTITY.includes(entity.type),
    
            entityScale = entity.currentScale || 1,
            entityScalesLine = entity.scaleOutline,
    
            [entityWidth, entityHeight] = pathBased ? entity.currentDimensions : entity.get('dimensions'),
            [handleX, handleY] = entity.get('handle'),
    
            entityLineWidth = entity.get('lineWidth') || 0,
            entityScaledLine = entityScalesLine ? entityLineWidth * entityScale : entityLineWidth,
            lineOffset = entityScaledLine / 2,
    
            localHandleX = getCoordinateValue(handleX, entityWidth) * entityScale,
            localHandleY = getCoordinateValue(handleY, entityHeight) * entityScale,
    
            inverseMatrix = matrix.inverse(),
            { a: mxA, b: mxB, c: mxC, d: mxD, e: mxE, f: mxF } = inverseMatrix,
    
            iData = imageData.data,
            iPix = new Uint32Array(iData.buffer, iData.byteOffset, iData.byteLength >>> 2),
            iLen = iPix.length,
    
            wData = workData.data,
            wPix = new Uint32Array(wData.buffer, wData.byteOffset, wData.byteLength >>> 2);
    
        const coordScale = pathBased ? entityScale : 1;
    
        let localMinX = -lineOffset,
            localMinY = -lineOffset,
            localMaxX = (entityWidth * entityScale) + lineOffset,
            localMaxY = (entityHeight * entityScale) + lineOffset;
    
        if (pathBased) {
    
            let x = 0,
                y = 0,
                cursor = 0,
                iChannels, alpha,
                sx, sy;
    
            for (; cursor < iLen; cursor++) {
    
                iChannels = iPix[cursor];
                alpha = (iChannels >>> 24) & 0xFF;
    
                if (alpha) {
    
                    sx = (((mxA * x) + (mxC * y) + mxE) * coordScale) + localHandleX;
                    sy = (((mxB * x) + (mxD * y) + mxF) * coordScale) + localHandleY;
    
                    if (sx < localMinX) localMinX = sx;
                    if (sx > localMaxX) localMaxX = sx;
                    if (sy < localMinY) localMinY = sy;
                    if (sy > localMaxY) localMaxY = sy;
                }
    
                x++;
    
                if (x === width) {
    
                    x = 0;
                    y++;
                }
            }
    
            localMinX = _floor(localMinX) - 1;
            localMinY = _floor(localMinY) - 1;
            localMaxX = _ceil(localMaxX) + 1;
            localMaxY = _ceil(localMaxY) + 1;
        }
    
        const localWidth = pathBased ? localMaxX - localMinX + 1 : _ceil((entityWidth * entityScale) + entityScaledLine),
            localHeight = pathBased ? localMaxY - localMinY + 1 : _ceil((entityHeight * entityScale) + entityScaledLine),
    
            localOverscale = pathBased && entityScale < 1 ? _min(4, 1 / entityScale) : 1,
            rasterWidth = _ceil(localWidth * localOverscale),
            rasterHeight = _ceil(localHeight * localOverscale),
    
            localGradientId = `${gradientId}-for-${localWidth}-${localHeight}-at-${localMinX}-${localMinY}-os-${localOverscale}`;
    
        let localGradientData = getWorkstoreItem(localGradientId);
    
        if (!localGradientData) {
    
            const tmpSrc = requestCell(),
                tmpDest = requestCell();
    
            const { element: tmpSrcEl, engine: tmpSrcEng } = tmpSrc,
                { element: tmpDestEl, engine: tmpDestEng } = tmpDest;
    
            tmpSrcEl.width = width;
            tmpSrcEl.height = height;
            tmpSrcEng.putImageData(gradientData, 0, 0);
    
            tmpDestEl.width = rasterWidth;
            tmpDestEl.height = rasterHeight;
            tmpDestEng.drawImage(tmpSrcEl, 0, 0, width, height, 0, 0, rasterWidth, rasterHeight);
    
            localGradientData = tmpDestEng.getImageData(0, 0, rasterWidth, rasterHeight);
    
            releaseCell(tmpSrc, tmpDest);
    
            setWorkstoreItem(localGradientId, localGradientData);
        }
    
        const gData = localGradientData.data,
            gPix = new Uint32Array(gData.buffer, gData.byteOffset, gData.byteLength >>> 2);
    
        let minX = width,
            minY = height,
            maxX = 0,
            maxY = 0,
            x = 0,
            y = 0,
            cursor = 0,
            iChannels, alpha,
            localX, localY, localCursor,
            gChannels, gAlpha, outAlpha;
    
        for (; cursor < iLen; cursor++) {
    
            iChannels = iPix[cursor];
            alpha = (iChannels >>> 24) & 0xFF;
    
            if (alpha) {
    
                localX = _floor((((((mxA * x) + (mxC * y) + mxE) * coordScale) + localHandleX) - localMinX) * localOverscale);
                localY = _floor((((((mxB * x) + (mxD * y) + mxF) * coordScale) + localHandleY) - localMinY) * localOverscale);
    
                if (localX >= 0 && localX < rasterWidth && localY >= 0 && localY < rasterHeight) {
    
                    localCursor = (localY * rasterWidth) + localX;
    
                    gChannels = gPix[localCursor];
                    gAlpha = gChannels >>> 24;
                    outAlpha = ((alpha * gAlpha) / 255) | 0;
    
                    if (outAlpha) wPix[cursor] = ((outAlpha << 24) | (gChannels & 0x00ffffff)) >>> 0;
                }
    
                if (x < minX) minX = x;
                if (x > maxX) maxX = x;
                if (y < minY) minY = y;
                if (y > maxY) maxY = y;
            }
    
            x++;
    
            if (x === width) {
    
                x = 0;
                y++;
            }
        }
    
        if (maxX < minX || maxY < minY) return false;
    
        const resWidth = maxX - minX + 1,
            resHeight = maxY - minY + 1,
            trimmedImage = new ImageData(new Uint8ClampedArray(resWidth * resHeight * 4), resWidth, resHeight),
            tData = trimmedImage.data,
            tPix = new Uint32Array(tData.buffer, tData.byteOffset, tData.byteLength >>> 2);
    
        let rows = 0,
            index;
    
        for (; rows < resHeight; rows++) {
    
            index = ((minY + rows) * width) + minX;
            tPix.set(wPix.subarray(index, index + resWidth), rows * resWidth);
        }
    
        result.x = minX;
        result.y = minY;
        result.w = resWidth;
        result.h = resHeight;
        result.imageData = trimmedImage;
    
        return true;
    };
    
    const applyCellLockedGradient = function (result, imageData, workData, gradientData, width, height) {
    
        const iData = imageData.data,
            iPix = new Uint32Array(iData.buffer, iData.byteOffset, iData.byteLength >>> 2),
            iLen = iPix.length,
            gData = gradientData.data,
            gPix = new Uint32Array(gData.buffer, gData.byteOffset, gData.byteLength >>> 2),
            wData = workData.data,
            wPix = new Uint32Array(wData.buffer, wData.byteOffset, wData.byteLength >>> 2);
    
        let minX = width,
            minY = height,
            maxX = 0,
            maxY = 0,
            x = 0,
            y = 0,
            cursor = 0,
            iChannels, alpha, gChannels, gAlpha, outAlpha;
    
        for (; cursor < iLen; cursor++) {
    
            iChannels = iPix[cursor];
            alpha = (iChannels >>> 24) & 0xFF;
    
            if (alpha) {
    
                gChannels = gPix[cursor];
                gAlpha = gChannels >>> 24;
                outAlpha = ((alpha * gAlpha) / 255) | 0;
    
                if (outAlpha) wPix[cursor] = ((outAlpha << 24) | (gChannels & 0x00ffffff)) >>> 0;
    
                if (x < minX) minX = x;
                if (x > maxX) maxX = x;
                if (y < minY) minY = y;
                if (y > maxY) maxY = y;
            }
    
            x++;
    
            if (x === width) {
    
                x = 0;
                y++;
            }
        }
    
        if (maxX < minX || maxY < minY) return false;
    
        const resWidth = maxX - minX + 1,
            resHeight = maxY - minY + 1,
            trimmedImage = new ImageData(new Uint8ClampedArray(resWidth * resHeight * 4), resWidth, resHeight),
            tData = trimmedImage.data,
            tPix = new Uint32Array(tData.buffer, tData.byteOffset, tData.byteLength >>> 2);
    
        let rows = 0,
            index, slice;
    
        for (; rows < resHeight; rows++) {
    
            index = ((minY + rows) * width) + minX;
            slice = wPix.slice(index, index + resWidth);
            tPix.set(slice, rows * resWidth);
        }
    
        result.x = minX;
        result.y = minY;
        result.w = resWidth;
        result.h = resHeight;
        result.imageData = trimmedImage;
    
        return true;
    };
  • §

    Gradient operation functionality

    Operations are grouped by stage and compiled into per-stage operation chains

    const operationsCache = {
        [BEFORE_SPREAD]: [],
        [AFTER_SPREAD]: [],
        [ON_COORDINATES]: [],
    };
    
    const cleanOperationsCache = () => {
    
        operationsCache[BEFORE_SPREAD].length = 0;
        operationsCache[AFTER_SPREAD].length = 0;
        operationsCache[ON_COORDINATES].length = 0;
    }
    
    const updateOperationsCache = (operations = []) => {
    
        cleanOperationsCache();
    
        if (operations.length) {
    
            operationsCache[BEFORE_SPREAD].push(...operations.filter(op => op.stage === BEFORE_SPREAD));
            operationsCache[AFTER_SPREAD].push(...operations.filter(op => op.stage === AFTER_SPREAD));
            operationsCache[ON_COORDINATES].push(...operations.filter(op => op.stage === ON_COORDINATES));
        }
    };
    
    const buildOperationChain = function (engines) {
    
        if (!engines.length) return λfirstArg;
    
        if (engines.length === 1) return engines[0];
    
        return function (val, p, width, height) {
    
            for (let i = 0, iz = engines.length; i < iz; i++) {
    
                val = engines[i](val, p, width, height);
            }
            return val;
        };
    };
    
    const buildCoordinateOperationChain = function (engines) {
    
        if (!engines.length) return λfirstArg;
    
        if (engines.length === 1) return engines[0];
    
        return function (coord, p, width, height) {
    
            for (let i = 0, iz = engines.length; i < iz; i++) {
    
                engines[i](coord, p, width, height);
            }
            return coord;
        };
    };
    
    const compileOperation = function (op, workData, entity, lock) {
    
        switch (op.operation) {
    
            case ADD_EASE :
                return getEasingOperation(op);
    
            case ADD_MAP_CONTOUR :
                return getMapContourOperation(op, workData);
    
            case ADD_MAP_DISPLACE :
                return getMapDisplaceOperation(op, workData);
    
            case ADD_MAP_EASE :
                return getMapEaseOperation(op, workData);
    
            case ADD_MAP_FLOW :
                return getMapFlowOperation(op, workData);
    
            case ADD_MAP_ROTATE :
                return getMapRotateOperation(op, workData);
    
            case ADD_MAP_THRESHOLD :
                return getMapThresholdOperation(op, workData);
    
            case ADD_MAP_WARP :
                return getMapWarpOperation(op, workData);
    
            case ADD_NOISE :
                return getNoiseOperation(op, workData);
    
            case ADD_RIPPLE :
                return getRippleOperation(op, workData, entity, lock);
    
            case ADD_WAVE :
                return getWaveOperation(op);
    
            default :
                return λfirstArg;
        }
    };
  • §

    Determine which operation, if any, should be applied at each stage

    const getBeforeSpreadOperation = function (workData) {
    
        const ops = operationsCache[BEFORE_SPREAD],
            engines = [];
    
        ops.forEach(op => {
    
            const engine = compileOperation(op, workData);
    
            if (engine !== λfirstArg) engines.push(engine);
        });
    
        return buildOperationChain(engines);
    };
    
    const getAfterSpreadOperation = function (workData) {
    
        const ops = operationsCache[AFTER_SPREAD],
            engines = [];
    
        ops.forEach(op => {
    
            const engine = compileOperation(op, workData);
    
            if (engine !== λfirstArg) engines.push(engine);
        });
    
        return buildOperationChain(engines);
    };
    
    const getOnCoordinatesOperation = function (workData, entity, lock) {
    
        const ops = operationsCache[ON_COORDINATES],
            engines = [];
    
        ops.forEach(op => {
    
            const engine = compileOperation(op, workData, entity, lock);
    
            if (engine !== λfirstArg) engines.push(engine);
        });
    
        return buildCoordinateOperationChain(engines);
    };
  • §

    Noise operations

    const getNoiseOperation = function (op, workData) {
    
        const params = op.parameters || {},
            noise = params.noise;
    
        if (!PERMITTED_NOISE.includes(noise)) return λfirstArg;
    
        const strength = _isFinite(params.strength) ? params.strength : 0.05;
    
        const rnd = getRandomNumbers({
            seed: params.seed,
            length: workData.data.length >>> 2,
            imgWidth: workData.width,
            type: noise,
        });
    
        return function (val, p) {
    
            return val + ((rnd[p] - 0.5) * strength);
        };
    };
    
    const getRandomNumbers = function (items = {}) {
    
        const {
            seed = DEFAULT_SEED,
            length = 0,
            imgWidth = 0,
            type = RANDOM,
        } = items;
    
        const name = `random-${seed}-${length}-${imgWidth}-${type}`,
            itemInWorkstore = getWorkstoreItem(name);
    
        if (itemInWorkstore) return itemInWorkstore;
    
        if ((type === BLUENOISE || type === ORDERED) && imgWidth) {
    
            const base = (type === BLUENOISE) ? bluenoise : orderedNoise,
                dim = (_sqrt(base.length) | 0),
                imgH = _ceil(length / imgWidth),
                out = new Float32Array(length);
    
            let p = 0,
                y, y0, x;
    
            for (y = 0; y < imgH && p < length; y++) {
    
                y0 = (y % dim) * dim;
    
                for (x = 0; x < imgWidth && p < length; x++) {
    
                    out[p++] = base[y0 + (x % dim)];
                }
            }
            setWorkstoreItem(name, out);
    
            return out;
        }
        else {
    
            const engine = seededRandomNumberGenerator(seed),
                out = new Float32Array(length);
    
            for (let i = 0; i < length; i++) {
    
                out[i] = engine.random();
            }
            setWorkstoreItem(name, out);
    
            return out;
        }
    };
  • §

    Easing operations

    const getEasingOperation = function (op) {
    
        const params = op.parameters || {},
            easing = params.easing,
            engine = isa_fn(easing) ? easing : easeEngines[easing];
    
        if (isa_fn(engine)) return engine;
    
        return λfirstArg;
    };
  • §

    Coordinate operations

    • These operations have a known issue with entity-locked gradients, particularly when filipped or rotated. This is due to the discrepency between “global” cell coordinate space and “local” entity coordinate space. This also affects flipped cell-locked gradients.
    const getCoordinateValue = function (coord, dimension) {
    
        if (coord.substring) {
    
            const val = parseFloat(coord);
    
            if (_isFinite(val)) return (val / 100) * dimension;
    
            return 0.5 * dimension;
        }
    
        return _isFinite(coord) ? coord : 0.5 * dimension;
    };
    
    const getWaveOperation = function (op) {
    
        const params = op.parameters || {};
    
        const axis = (params.axis === Y || params.axis === BOTH) ? params.axis : X,
            amplitude = _isFinite(params.amplitude) ? params.amplitude : 10,
            frequency = _isFinite(params.frequency) ? params.frequency : 0.05,
            phase = _isFinite(params.phase) ? params.phase : 0;
    
        return function (coord) {
    
            const x = coord[0],
                y = coord[1];
    
            if (axis === X) coord[0] = x + (_sin((y * frequency) + phase) * amplitude);
    
            else if (axis === Y) coord[1] = y + (_sin((x * frequency) + phase) * amplitude);
    
            else {
    
                coord[0] = x + (_sin((y * frequency) + phase) * amplitude);
                coord[1] = y + (_sin((x * frequency) + phase) * amplitude);
            }
    
            return coord;
        };
    };
    
    const getRippleOperation = function (op, workData, entity, lock) {
    
        const params = op.parameters || {};
    
        let width = workData.width,
            height = workData.height;
    
        if (lock && entity) {
    
            const [w, h] = entity.get('dimensions');
    
            width = w;
            height = h;
        }
    
        const amplitude = _isFinite(params.amplitude) ? params.amplitude : 10,
            frequency = _isFinite(params.frequency) ? params.frequency : 0.05,
            phase = _isFinite(params.phase) ? params.phase : 0;
    
        const originX = getCoordinateValue(params.originX, width),
            originY = getCoordinateValue(params.originY, height);
    
        return function (coord) {
    
            const x = coord[0],
                y = coord[1],
    
                dx = x - originX,
                dy = y - originY,
    
                dist = _sqrt((dx * dx) + (dy * dy));
    
            if (dist) {
    
                const offset = _sin((dist * frequency) + phase) * amplitude,
                    ratio = offset / dist;
    
                coord[0] = x + (dx * ratio);
                coord[1] = y + (dy * ratio);
            }
    
            return coord;
        };
    };
    
    const getMapDisplaceOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            axis = (params.axis === Y || params.axis === BOTH) ? params.axis : X,
    
            strength = _isFinite(params.strength) ? params.strength : 20,
            offset = _isFinite(params.offset) ? params.offset : 0.5,
            linked = params.linked === true;
    
        if (!strength) return λfirstArg;
    
        if (axis === X) {
    
            const map = getNoiseMap(mapParams, width, height);
    
            return function (coord, p) {
    
                coord[0] += (map[p] - offset) * strength;
                return coord;
            };
        }
    
        if (axis === Y) {
    
            const map = getNoiseMap(mapParams, width, height);
    
            return function (coord, p) {
    
                coord[1] += (map[p] - offset) * strength;
                return coord;
            };
        }
    
        const xMap = getNoiseMap(linked ? mapParams : {
                ...mapParams,
                seed: `${mapParams.seed || DEFAULT_SEED}-x`,
            }, width, height),
    
            yMap = getNoiseMap(linked ? mapParams : {
                ...mapParams,
                seed: `${mapParams.seed || DEFAULT_SEED}-y`,
            }, width, height);
    
        return function (coord, p) {
    
            coord[0] += (xMap[p] - offset) * strength;
            coord[1] += (yMap[p] - offset) * strength;
    
            return coord;
        };
    };
    
    const getMapRotateOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            strength = _isFinite(params.strength) ? params.strength : 45,
            offset = _isFinite(params.offset) ? params.offset : 0.5,
    
            originX = getCoordinateValue(params.originX, width),
            originY = getCoordinateValue(params.originY, height);
    
        if (!strength) return λfirstArg;
    
        const map = getNoiseMap(mapParams, width, height),
            angleMultiplier = strength * _radian;
    
        return function (coord, p) {
    
            const angle = (map[p] - offset) * angleMultiplier,
                cos = _cos(angle),
                sin = _sin(angle),
    
                x = coord[0] - originX,
                y = coord[1] - originY;
    
            coord[0] = originX + ((x * cos) - (y * sin));
            coord[1] = originY + ((x * sin) + (y * cos));
    
            return coord;
        };
    };
    
    const getMapFlowOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            strength = _isFinite(params.strength) ? params.strength : 20,
            offset = _isFinite(params.offset) ? params.offset : 0.5,
            linked = params.linked === true,
            normalize = params.normalize !== false;
    
        if (!strength) return λfirstArg;
    
        const xMap = getNoiseMap(linked ? mapParams : {
                ...mapParams,
                seed: `${mapParams.seed || DEFAULT_SEED}-x`,
            }, width, height),
    
            yMap = getNoiseMap(linked ? mapParams : {
                ...mapParams,
                seed: `${mapParams.seed || DEFAULT_SEED}-y`,
            }, width, height);
    
        return function (coord, p) {
    
            let dx = xMap[p] - offset,
                dy = yMap[p] - offset;
    
            if (normalize) {
    
                const mag = _sqrt((dx * dx) + (dy * dy));
    
                if (mag) {
    
                    dx /= mag;
                    dy /= mag;
                }
            }
    
            coord[0] += dx * strength;
            coord[1] += dy * strength;
    
            return coord;
        };
    };
    
    const getMapThresholdOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            threshold = _isFinite(params.threshold) ? params.threshold : 0.5,
            low = _isFinite(params.low) ? params.low : 0,
            high = _isFinite(params.high) ? params.high : 1,
    
            bandModulate = params.bandModulate === true,
            bands = _isFinite(params.bands) ? _max(2, _floor(params.bands)) : 8,
            influence = _isFinite(params.influence) ? params.influence : 0.25;
    
        const map = getNoiseMap(mapParams, width, height);
    
        if (bandModulate) {
    
            return function (val, p) {
    
                return _floor(
                    (val + ((map[p] - threshold) * influence)) * bands
                ) / bands;
            };
        }
    
        return function (val, p) {
    
            return (map[p] >= threshold) ? high : low;
        };
    };
    
    const getMapContourOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            influence = _isFinite(params.influence) ? params.influence : 0.5,
            bands = _isFinite(params.bands) ? _max(2, _floor(params.bands)) : 8;
    
        const map = getNoiseMap(mapParams, width, height);
    
        return function (val, p) {
    
            const target = _floor(map[p] * bands) / bands;
    
            return val + ((target - val) * influence);
        };
    };
    
    const getMapEaseOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            strength = _isFinite(params.strength) ? params.strength : 0.25,
            offset = _isFinite(params.offset) ? params.offset : 0.5;
    
        if (!strength) return λfirstArg;
    
        const map = getNoiseMap(mapParams, width, height);
    
        return function (val, p) {
    
            return val + ((map[p] - offset) * strength);
        };
    };
    
    const getMapWarpOperation = function (op, workData) {
    
        const params = op.parameters || {},
            mapParams = params.map || {},
    
            width = workData.width,
            height = workData.height,
    
            strength = _isFinite(params.strength) ? params.strength : 12,
            offset = _isFinite(params.offset) ? params.offset : 0.5,
            iterations = _isFinite(params.iterations) ? _max(1, _floor(params.iterations)) : 3,
            linked = params.linked === true,
            normalize = params.normalize === true;
    
        if (!strength) return λfirstArg;
    
        const xMap = getNoiseMap(linked ? mapParams : {
                ...mapParams,
                seed: `${mapParams.seed || DEFAULT_SEED}-x`,
            }, width, height),
    
            yMap = getNoiseMap(linked ? mapParams : {
                ...mapParams,
                seed: `${mapParams.seed || DEFAULT_SEED}-y`,
            }, width, height),
    
            sampleMap = function (map, x, y) {
    
                x = _floor(x);
                y = _floor(y);
    
                if (x < 0) x = 0;
                else if (x >= width) x = width - 1;
    
                if (y < 0) y = 0;
                else if (y >= height) y = height - 1;
    
                return map[(y * width) + x];
            },
    
            step = strength / iterations;
    
        return function (coord) {
    
            let x = coord[0],
                y = coord[1],
                dx, dy, mag;
    
            for (let i = 0; i < iterations; i++) {
    
                dx = sampleMap(xMap, x, y) - offset;
                dy = sampleMap(yMap, x, y) - offset;
    
                if (normalize) {
    
                    mag = _sqrt((dx * dx) + (dy * dy));
    
                    if (mag) {
    
                        dx /= mag;
                        dy /= mag;
                    }
                }
    
                x += dx * step;
                y += dy * step;
            }
    
            coord[0] = x;
            coord[1] = y;
    
            return coord;
        };
    };
    
    const getNoiseMap = function (noiseParams, width, height) {
    
        const params = {
            ...noiseDefs,
            width,
            height,
            ...noiseParams,
        };
    
        const name = buildNoiseMapId(params),
            cached = getWorkstoreItem(name);
    
        if (cached) return cached;
    
        noiseAsset.set(noiseDefs);
        noiseAsset.set({
            width,
            height,
        });
        noiseAsset.set(noiseParams);
    
        noiseAsset.cleanOutput();
    
        const source = noiseAsset.noiseValues;
    
        if (!source || source.length !== width * height) return new Float32Array(width * height);
    
        const out = new Float32Array(source);
    
        setWorkstoreItem(name, out);
        return out;
    };
    
    const buildNoiseMapId = function (p) {
    
        return [
            'gradient-engine-noise-map',
            p.width,
            p.height,
            p.seed,
            p.noiseEngine,
            p.size,
            p.scale,
            p.octaves,
            p.octaveFunction,
            p.persistence,
            p.lacunarity,
            p.smoothing,
            p.sumFunction,
            p.sineFrequencyCoeff,
            p.sumAmplitude,
            p.worleyOutput,
            p.worleyDepth,
        ].join('-');
    };
  • §

    Factory

    constructors.GradientEngine = GradientEngine;
  • §

    Create a singleton filter engine, for export and use within this code base

    export const gradientEngine = new GradientEngine();