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

    Shape path calculation

    The function, and its helper functions, translates SVG path data string - example: M0,0H100V100H-100V-100z - into data which can be used to construct the a Path2D object which can be consumed by the canvasRenderingContext2D engine’s fill, stroke and isPointInPath functions

    • TODO: this file is in the wrong place: it’s not a mixin; it exports a single function currently imported only by ./factory.shape.js; and it duplicates Vector code
    • Code has been separated out into its own file because it is a potential candidate for hiving off to a web worker
  • ¶

    Imports

    No imports required

  • ¶

    Export function

    export default function (d, scale, start, useAsPath, precision) {
  • ¶

    Setup local variables

        let points = [],
            myData = [],
            command = '',
            localPath = '',
            units = [],
            unitLengths = [],
            unitPartials = [],
            mySet = d.match(/([A-Za-z][0-9. ,\-]*)/g), 
            myLen = 0,
            i, iz, j, jz;
    
        let returnObject = {};
    
        let curX = 0, 
            curY = 0, 
            oldX = 0, 
            oldY = 0;
    
        let xPoints = [],
            yPoints = [],
            progression = [],
            positions = [];
    
        let reflectX = 0,
            reflectY = 0;
  • ¶

    Local function to populate the temporary myData array with data for every path partial

        let buildArrays = (thesePoints) => {
    
            myData.push({
                c: command.toLowerCase(),
                p: thesePoints || null,
                x: oldX,
                y: oldY,
                cx: curX,
                cy: curY,
                rx: reflectX,
                ry: reflectY
            });
    
            if (!useAsPath) {
    
                xPoints.push(curX);
                yPoints.push(curY);
            }
    
            oldX = curX;
            oldY = curY;
        };
  • ¶

    The purpose of this loop is to

    1. convert all point values from strings to floats
    2. scale every value
    3. relativize every value to the last stated cursor position
    4. populate the temporary myData array with data which can be used for all subsequent calculations
        for (i = 0, iz = mySet.length; i < iz; i++) {
    
            command = mySet[i][0];
            points = mySet[i].match(/(-?[0-9.]+\b)/g) || [];
    
            if (points.length) {
    
                for (j = 0, jz = points.length; j < jz; j++) {
    
                    points[j] = parseFloat(points[j]);
                }
    
                if (i === 0) {
    
                    if (command === 'M') {
    
                        oldX = (points[0] * scale) - start.x;
                        oldY = (points[1] * scale) - start.y;
                        command = 'm';
                    }
                } 
                else {
                    
                    oldX = curX;
                    oldY = curY;
                }
    
                switch (command) {
    
                    case 'H':
                        for (j = 0, jz = points.length; j < jz; j++) {
    
                            points[j] = (points[j] * scale) - oldX;
                            curX += points[j];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 1));
                        }
                        break;
    
                    case 'V':
                        for (j = 0, jz = points.length; j < jz; j++) {
    
                            points[j] = (points[j] * scale) - oldY;
                            curY += points[j];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 1));
                        }
                        break;
    
                    case 'M':
                        for (j = 0, jz = points.length; j < jz; j += 2) {
    
                            points[j] = (points[j] * scale) - oldX;
                            points[j + 1] = (points[j + 1] * scale) - oldY;
                            curX += points[j];
                            curY += points[j + 1];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 2));
                        }
                        break;
    
                    case 'L':
                    case 'T':
                        for (j = 0, jz = points.length; j < jz; j += 2) {
    
                            points[j] = (points[j] * scale) - oldX;
                            points[j + 1] = (points[j + 1] * scale) - oldY;
                            curX += points[j];
                            curY += points[j + 1];
    
                            if (command === 'T') {
    
                                reflectX = points[j] + oldX;
                                reflectY = points[j + 1] + oldY;
                            }
                            else {
    
                                reflectX = reflectY = 0;
                            }
                            buildArrays(points.slice(j, j + 2));
                        }
                        break;
    
                    case 'Q':
                    case 'S':
                        for (j = 0, jz = points.length; j < jz; j += 4) {
    
                            points[j] = (points[j] * scale) - oldX;
                            points[j + 1] = (points[j + 1] * scale) - oldY;
                            points[j + 2] = (points[j + 2] * scale) - oldX;
                            points[j + 3] = (points[j + 3] * scale) - oldY;
                            curX += points[j + 2];
                            curY += points[j + 3];
                            reflectX = points[j] + oldX;
                            reflectY = points[j + 1] + oldY;
                            buildArrays(points.slice(j, j + 4));
                        }
                        break;
    
                    case 'C':
                        for (j = 0, jz = points.length; j < jz; j += 6) {
    
                            points[j] = (points[j] * scale) - oldX;
                            points[j + 1] = (points[j + 1] * scale) - oldY;
                            points[j + 2] = (points[j + 2] * scale) - oldX;
                            points[j + 3] = (points[j + 3] * scale) - oldY;
                            points[j + 4] = (points[j + 4] * scale) - oldX;
                            points[j + 5] = (points[j + 5] * scale) - oldY;
                            curX += points[j + 4];
                            curY += points[j + 5];
                            reflectX = points[j + 2] + oldX;
                            reflectY = points[j + 3] + oldY;
                            buildArrays(points.slice(j, j + 6));
                        }
                        break;
    
                    case 'A':
                        for (j = 0, jz = points.length; j < jz; j += 7) {
    
                            points[j + 5] = (points[j + 5] * scale) - oldX;
                            points[j + 6] = (points[j + 6] * scale) - oldY;
                            curX += points[j + 5];
                            curY += points[j + 6];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 7));
                        }
                        break;
    
                    case 'h':
                        for (j = 0, jz = points.length; j < jz; j++) {
    
                            points[j] *= scale;
                            curX += points[j];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 1));
                        }
                        break;
    
                    case 'v':
                        for (j = 0, jz = points.length; j < jz; j++) {
    
                            points[j] *= scale;
                            curY += points[j];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 1));
                        }
                        break;
    
                    case 'm':
                    case 'l':
                    case 't':
                        for (j = 0, jz = points.length; j < jz; j += 2) {
    
                            points[j] *= scale;
                            points[j + 1] *= scale;
                            curX += points[j];
                            curY += points[j + 1];
    
                            if (command === 't') {
    
                                reflectX = points[j] + oldX;
                                reflectY = points[j + 1] + oldY;
                            }
                            else {
    
                                reflectX = reflectY = 0;
                            }
                            buildArrays(points.slice(j, j + 2));
                        }
                        break;
    
                    case 'q':
                    case 's':
                        for (j = 0, jz = points.length; j < jz; j += 4) {
    
                            points[j] *= scale;
                            points[j + 1] *= scale;
                            points[j + 2] *= scale;
                            points[j + 3] *= scale;
                            curX += points[j + 2];
                            curY += points[j + 3];
                            reflectX = points[j] + oldX;
                            reflectY = points[j + 1] + oldY;
                            buildArrays(points.slice(j, j + 4));
                        }
                        break;
    
                    case 'c':
                        for (j = 0, jz = points.length; j < jz; j += 6) {
    
                            points[j] *= scale;
                            points[j + 1] *= scale;
                            points[j + 2] *= scale;
                            points[j + 3] *= scale;
                            points[j + 4] *= scale;
                            points[j + 5] *= scale;
                            curX += points[j + 4];
                            curY += points[j + 5];
                            reflectX = points[j + 2] + oldX;
                            reflectY = points[j + 3] + oldY;
                            buildArrays(points.slice(j, j + 6));
                        }
                        break;
    
                    case 'a':
                        for (j = 0, jz = points.length; j < jz; j += 7) {
    
                            points[j] *= scale;
                            points[j + 1] *= scale;
                            points[j + 5] *= scale;
                            points[j + 6] *= scale;
                            curX += points[j + 5];
                            curY += points[j + 6];
                            reflectX = reflectY = 0;
                            buildArrays(points.slice(j, j + 7));
                        }
                        break;
                }
    
            }
            else {
    
                reflectX = reflectY = 0;
                buildArrays();
            }
        }
  • ¶

    This loop builds the local path string

        for (i = 0, iz = myData.length; i < iz; i++) {
    
            let curData = myData[i],
                points = curData.p;
    
            if (points) {
    
                for (j = 0, jz = points.length; j < jz; j++) {
    
                    points[j] = points[j].toFixed(1);
                }
    
                localPath += `${curData.c}${curData.p.join()}`;
    
                for (j = 0, jz = points.length; j < jz; j++) {
    
                    points[j] = parseFloat(points[j]);
                }
    
            }
            else localPath += `${curData.c}`;
        }
    
        returnObject.localPath = localPath;
  • ¶

    Calculates unit lengths and sum of lengths, alongside obtaining data to build a more accurate bounding box

        if (useAsPath) {
  • ¶

    Request a vector - used for reflection points

            let v = vector;
  • ¶

    This loop calculates this.units array data

    • because the lengths calculations requires absolute coordinates
    • and TtSs path units use reflective coordinates
            for (i = 0, iz = myData.length; i < iz; i++) {
    
                let curData = myData[i],
                    prevData = (i > 0) ? myData[i - 1] : false;
    
                let {c, p, x, y, cx, cy, rx, ry} = curData;
    
                if (p) {
    
                    switch (c) {
    
                        case 'h' :
                            units[i] = ['linear', x, y, p[0] + x, y];
                            break;
    
                        case 'v' :
                            units[i] = ['linear', x, y, x, p[0] + y];
                            break;
                            
                        case 'm' :
                            units[i] = ['move', x, y];
                            break;
                            
                        case 'l' :
                            units[i] = ['linear', x, y, p[0] + x, p[1] + y];
                            break;
                            
                        case 't' :
                            if (prevData && (prevData.rx || prevData.ry)) {
    
                                setVector(v, prevData.rx - cx, prevData.ry - cy);
                                rotateVector(v, 180);
    
                                units[i] = ['quadratic', x, y, v.x + cx, v.y + cy, p[0] + x, p[1] + y];
                            }
                            else units[i] = ['quadratic', x, y, x, y, p[0] + x, p[1] + y];
                            break;
                            
                        case 'q' :
                            units[i] = ['quadratic', x, y, p[0] + x, p[1] + y, p[2] + x, p[3] + y];
                            break;
                            
                        case 's' :
                            if (prevData && (prevData.rx || prevData.ry)) {
    
                                setVector(v, prevData.rx - cx, prevData.ry - cy);
                                rotateVector(v, 180);
    
                                units[i] = ['bezier', x, y, v.x + cx, v.y + cy, p[0] + x, p[1] + y, p[2] + x, p[3] + y];
                            }
                            else units[i] = ['bezier', x, y, x, y, p[0] + x, p[1] + y, p[2] + x, p[3] + y];
                            break;
                            
                        case 'c' :
                            units[i] = ['bezier', x, y, p[0] + x, p[1] + y, p[2] + x, p[3] + y, p[4] + x, p[5] + y];
                            break;
                            
                        case 'a' :
                            units[i] = ['linear', x, y, p[5] + x, p[6] + y];
                            break;
                            
                        case 'z' :
                            if (isNaN(x)) x = 0;
                            if (isNaN(y)) y = 0;
                            units[i] = ['close', x, y];
                            break;
    
                        default :
                            if (isNaN(x)) x = 0;
                            if (isNaN(y)) y = 0;
                            units[i] = ['unknown', x, y];
                    }
                }
                else units[i] = [`no-points-${c}`, x, y];
            }
    
            returnObject.units = units;
    
            for (i = 0, iz = units.length; i < iz; i++) {
    
                let [spec, ...data] = units[i],
                    results;
    
                switch (spec) {
    
                    case 'linear' :
                    case 'quadratic' :
                    case 'bezier' :
                        results = getShapeUnitMetaData(spec, precision, data);
                        unitLengths[i] = results.length;
                        xPoints = xPoints.concat(results.xPoints);
                        yPoints = yPoints.concat(results.yPoints);
                        progression.push(results.progression);
                        positions.push(results.positions);
                        break;
                        
                    default :
                        unitLengths[i] = 0;
                }
            }
    
            myLen = unitLengths.reduce((a, v) => a + v, 0);
    
            let mySum = 0;
    
            for (i = 0, iz = unitLengths.length; i < iz; i++) {
    
                mySum += unitLengths[i] / myLen;
                unitPartials[i] = parseFloat(mySum.toFixed(6));
            }
        }
    
        returnObject.unitLengths = unitLengths;
        returnObject.unitPartials = unitPartials;
        returnObject.length = parseFloat(myLen.toFixed(1));
        returnObject.unitPositions = positions;
        returnObject.unitProgression = progression;
  • ¶

    calculate bounding box dimensions

        let maxX = Math.max(...xPoints),
            maxY = Math.max(...yPoints),
            minX = Math.min(...xPoints),
            minY = Math.min(...yPoints);
    
        returnObject.maxX = maxX;
        returnObject.maxY = maxY;
        returnObject.minX = minX;
        returnObject.minY = minY;
        returnObject.xRange = xPoints;
        returnObject.yRange = yPoints;
    
        return returnObject;
    };
  • ¶

    Locally defined Vector

    • TODO: consider whether we could swap out this vector stuff for a pool coordinate?
    const vector = {
    
        x: 0,
        y: 0
    };
    
    const setVector = function (v, x, y) {
    
        v.x = x;
        v.y = y;
    };
    
    const rotateVector = function (v, angle) {
    
        let arg = Math.atan2(v.y, v.x);
        arg += (angle * 0.01745329251);
        
        let mag = Math.sqrt((v.x * v.x) + (v.y * v.y));
    
        v.x = mag * Math.cos(arg);
        v.y = mag * Math.sin(arg);
    };
  • ¶

    Helper functions

  • ¶

    getShapeUnitMetaData

    const getShapeUnitMetaData = function (species, precision, args) {
    
        let xPts = [],
            yPts = [],
            progression = [],
            positions = [],
            len = 0,
            w, h;
  • ¶

    We want to separate out linear species before going into the while loop

    • because these calculations will be simple
        if (species === 'linear') {
    
            let [sx, sy, ex, ey] = args;
    
            w = ex - sx,
            h = ey - sy;
    
            len = Math.sqrt((w * w) + (h * h));
    
            xPts = xPts.concat([sx, ex]);
            yPts = yPts.concat([sy, ey]);
        }
        else if (species === 'bezier' || (species === 'quadratic')) {
    
            let func = (species === 'bezier') ? 'getBezierXY' : 'getQuadraticXY',
                flag = false,
                step = 0.25,
                currentLength = 0,
                newLength = 0,
                oldX, oldY, x, y, t, res;
    
            while (!flag) {
    
                xPts.length = 0;
                yPts.length = 0;
                newLength = 0;
                progression.length = 0;
                positions.length = 0;
    
                res = getXY[func](0, ...args);
                oldX = res.x;
                oldY = res.y;
                xPts.push(oldX);
                yPts.push(oldY);
    
                for (t = step; t <= 1; t += step) {
    
                    res = getXY[func](t, ...args);
                    ({x, y} = res)
    
                    xPts.push(x);
                    yPts.push(y);
    
                    w = x - oldX,
                    h = y - oldY;
    
                    newLength += Math.sqrt((w * w) + (h * h));
                    oldX = x;
                    oldY = y;
    
                    progression.push(newLength);
                    positions.push(t);
                }
  • ¶

    Stop the while loop if we’re getting close to the true length of the curve

                if (newLength < len + precision) flag = true;
    
                len = newLength;
    
                step /= 2;
  • ¶

    Stop the while loop after checking a maximum of 129 points along the curve

                if (step < 0.004) flag = true;
            }
        }
    
        return {
            length: len,
            xPoints: xPts,
            yPoints: yPts,
            positions: positions,
            progression: progression,
        };
    };
  • ¶

    getXY

    const getXY = {
    
        getBezierXY: function (t, sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey) {
    
            let T = 1 - t;
    
            return {
                x: (Math.pow(T, 3) * sx) + (3 * t * Math.pow(T, 2) * cp1x) + (3 * t * t * T * cp2x) + (t * t * t * ex),
                y: (Math.pow(T, 3) * sy) + (3 * t * Math.pow(T, 2) * cp1y) + (3 * t * t * T * cp2y) + (t * t * t * ey)
            };
        },
    
        getQuadraticXY: function (t, sx, sy, cp1x, cp1y, ex, ey) {
    
            let T = 1 - t;
    
            return {
                x: T * T * sx + 2 * T * t * cp1x + t * t * ex,
                y: T * T * sy + 2 * T * t * cp1y + t * t * ey
            };
        },
    };