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

    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

    import { releaseArray, requestArray } from './array-pool.js';
  • §

    Shared constants

    import { _abs, _acos, _atan2, _cos, _isFinite, _max, _min, _piDouble, _piHalf, _pow, _radian, _sin, _sqrt, _tan, BEZIER, CLOSE, LINEAR, MOVE, QUADRATIC, UNKNOWN, ZERO_STR } from './shared-vars.js';
  • §

    Local constants

    const GET_BEZIER = 'getBezierXY',
        GET_QUADRATIC = 'getQuadraticXY';
  • §

    We only use one pathCalcObject, but treat it like a pool of such objects for resetting to defaults

    const pathCalcObjectPool = [];
  • §

    Exported helper functions

    export const requestPathCalcObject = function () {
    
        if (!pathCalcObjectPool.length) pathCalcObjectPool.push({
            localPath: null,
            length: 0,
            maxX: 0,
            maxY: 0,
            minX: 0,
            minY: 0,
            unitLengths: [],
            unitPartials: [],
            units: [],
            unitPositions: [],
            unitProgression: [],
            xRange: [],
            yRange: [],
        });
    
        return pathCalcObjectPool.shift();
    };
    
    export const releasePathCalcObject = function (a) {
    
        a.localPath = null;
        a.length = 0;
        a.units.length = 0;
        a.maxX = 0;
        a.maxY = 0;
        a.minX = 0;
        a.minY = 0;
        a.unitLengths.length = 0;
        a.unitPartials.length = 0;
        a.unitPositions.length = 0;
        a.unitProgression.length = 0;
        a.xRange.length = 0;
        a.yRange.length = 0;
    
        pathCalcObjectPool.push(a);
    };
  • §

    Export function

    export const calculatePath = (d, scale, start, useAsPath, precision, result) => {
    
        if (precision == null || isNaN(precision)) precision = 10;
  • §

    Setup local variables

        const points = requestArray(),
            myData = requestArray(),
            xPoints = requestArray(),
            yPoints = requestArray(),
            units = result.units,
            unitLengths = result.unitLengths,
            unitPartials = result.unitPartials,
            progression = result.unitProgression,
            positions = result.unitPositions,
            mySet = d.match(/([MmZzLlHhVvCcSsQqTtAa][^MmZzLlHhVvCcSsQqTtAa]*)/g) || [],
            localMatch = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
    
        let command = ZERO_STR,
            localPath = ZERO_STR,
            myLen = 0,
            i, iz, j, jz,
            checkMatch,
            curX = 0,
            curY = 0,
            oldX = 0,
            oldY = 0,
            reflectX = 0,
            reflectY = 0,
            subpathStartX = 0,
            subpathStartY = 0;
  • §

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

        const 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.length = 0;
            checkMatch = mySet[i].match(localMatch);
            if (checkMatch) points.push(...checkMatch);
    
            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) {
    
                            const isFirstPair = (j === 0),
                                saved = command;
    
                            if (!isFirstPair) command = 'L';
    
                            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));
    
                            if (isFirstPair) {
    
                                subpathStartX = curX;
                                subpathStartY = curY;
                            }
    
                            command = saved;
                        }
                        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] *= scale;
                            points[j + 1] *= scale;
                            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) {
    
                            const saved = command;
                            if (saved === 'm' && j > 0) command = 'l';
    
                            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));
    
                            if (saved === 'm' && j === 0) {
    
                                subpathStartX = curX;
                                subpathStartY = curY;
                            }
    
                            command = saved;
                        }
                        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++) {
    
            const curData = myData[i],
                myPts = curData.p;
    
            if (myPts) localPath += `${curData.c}${curData.p.join()}`;
            else localPath += `${curData.c}`;
        }
    
        result.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

            const v = vector;
            let curData, prevData, c, p, x, y, cx, cy;
  • §

    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++) {
    
                curData = myData[i];
                prevData = (i > 0) ? myData[i - 1] : false;
    
                ({c, p, x, y, cx, cy} = 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' :
                            subpathStartX = cx;
                            subpathStartY = cy;
    
                            units[i] = [MOVE, x, y];
                            break;
    
                        case 'l' :
                            units[i] = [LINEAR, x, y, p[0] + x, p[1] + y];
                            break;
    
                        case 't' :
                            if (prevData && _isFinite(prevData.rx) && _isFinite(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 && _isFinite(prevData.rx) && _isFinite(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' :
                            {
                                const sx = x,
                                    sy = y,
                                    ex = x + p[5],
                                    ey = y + p[6];
    
                                const curves = arcToCubicBeziers(
                                    sx,
                                    sy,
                                    _abs(p[0]),
                                    _abs(p[1]),
                                    p[2],
                                    p[3] ? 1 : 0,
                                    p[4] ? 1 : 0,
                                    ex,
                                    ey,
                                );
    
                                if (!curves.length) units[i] = [LINEAR, sx, sy, ex, ey];
                                else {
    
                                    const insertAt = i,
                                        splice = [];
    
                                    let px = sx,
                                        py = sy;
    
                                    for (const [c1x, c1y, c2x, c2y, x2, y2] of curves) {
    
                                        splice.push([BEZIER, px, py, c1x, c1y, c2x, c2y, x2, y2]);
    
                                        px = x2;
                                        py = y2;
                                    }
                                    units.splice(insertAt, 1, ...splice);
                                }
                            }
                            break;
    
                        case 'z' :
                            if (!_isFinite(x)) x = 0;
                            if (!_isFinite(y)) y = 0;
                            units[i] = [CLOSE, x, y, subpathStartX, subpathStartY];
                            break;
    
                        default :
                            if (!_isFinite(x)) x = 0;
                            if (!_isFinite(y)) y = 0;
                            units[i] = [UNKNOWN, x, y];
                    }
                }
                else units[i] = [`no-points-${c}`, x, y];
            }
    
            for (i = 0, iz = units.length; i < iz; i++) {
    
                const [spec, ...data] = units[i];
    
                let localResults;
    
                switch (spec) {
    
                    case LINEAR :
                    case QUADRATIC :
                    case BEZIER :
                    case CLOSE :
                        localResults = getShapeUnitMetaData(spec, precision, data);
                        unitLengths[i] = localResults.length;
                        xPoints.push(...localResults.xPoints);
                        yPoints.push(...localResults.yPoints);
                        progression.push(localResults.progression);
                        positions.push(localResults.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++) {
    
                if (myLen === 0) unitPartials[i] = 0;
                else {
    
                    mySum += unitLengths[i] / myLen;
                    unitPartials[i] = mySum;
                }
            }
        }
        result.length = myLen;
  • §

    calculate bounding box dimensions

        if (!xPoints.length) {
    
            result.minX = result.maxX = subpathStartX;
            result.minY = result.maxY = subpathStartY;
        }
        else {
    
            result.maxX = _max(...xPoints);
            result.maxY = _max(...yPoints);
            result.minX = _min(...xPoints);
            result.minY = _min(...yPoints);
        }
    
        result.xRange.push(...xPoints);
        result.yRange.push(...yPoints);
    
        releaseArray(points, myData, xPoints, yPoints);
    
        return result;
    };
  • §

    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 = _atan2(v.y, v.x);
        arg += (angle * _radian);
    
        const mag = _sqrt((v.x * v.x) + (v.y * v.y));
    
        v.x = mag * _cos(arg);
        v.y = mag * _sin(arg);
    };
  • §

    Helper functions

  • §

    getShapeUnitMetaData

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

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

    • because these calculations will be simple
        if (species === LINEAR || species === CLOSE) {
    
            const [sx, sy, ex, ey] = args;
    
            w = ex - sx;
            h = ey - sy;
    
            len = _sqrt((w * w) + (h * h));
    
            xPts = xPts.concat([sx, ex]);
            yPts = yPts.concat([sy, ey]);
        }
        else if (species === BEZIER || (species === QUADRATIC)) {
    
            const func = (species === BEZIER) ? GET_BEZIER : GET_QUADRATIC;
    
            let flag = false,
                step = 0.25,
                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 += _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 = {
    
        [GET_BEZIER]: function (t, sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey) {
    
            const T = 1 - t;
    
            return {
                x: (_pow(T, 3) * sx) + (3 * t * _pow(T, 2) * cp1x) + (3 * t * t * T * cp2x) + (t * t * t * ex),
                y: (_pow(T, 3) * sy) + (3 * t * _pow(T, 2) * cp1y) + (3 * t * t * T * cp2y) + (t * t * t * ey)
            };
        },
    
        [GET_QUADRATIC]: function (t, sx, sy, cp1x, cp1y, ex, ey) {
    
            const 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
            };
        },
    };
  • §

    Convert an SVG arc (endpoint param) into an array of cubic Beziers. Returns array of [cp1x, cp1y, cp2x, cp2y, x, y] segments.

    const arcToCubicBeziers = function (x1, y1, rx, ry, phi, fa, fs, x2, y2) {
  • §

    Based on SVG 1.1 spec F.6.5

        const rad = phi * _radian,
            cos = _cos(rad),
            sin = _sin(rad);
  • §

    Step 1: compute (x1’, y1’)

        const dx2 = (x1 - x2) / 2,
            dy2 = (y1 - y2) / 2,
            x1p = cos * dx2 + sin * dy2,
            y1p = -sin * dx2 + cos * dy2;
    
        let rxs = Math.abs(rx),
            rys = Math.abs(ry);
    
        if (rxs === 0 || rys === 0) return [];
  • §

    Correct radii

        const lambda = (x1p * x1p) / (rxs * rxs) + (y1p * y1p) / (rys * rys);
    
        if (lambda > 1) {
    
            const s = Math.sqrt(lambda);
            rxs *= s;
            rys *= s;
        }
  • §

    Step 2: compute center (cx’, cy’)

        const sign = (fa === fs) ? -1 : 1,
            rxs2 = rxs * rxs,
            rys2 = rys * rys,
            num = rxs2 * rys2 - rxs2 * y1p * y1p - rys2 * x1p * x1p,
            den = rxs2 * y1p * y1p + rys2 * x1p * x1p;
    
        if (den === 0) return [];
    
        const coef = sign * _sqrt(_max(0, num / den)),
            cxp = coef * (rxs * y1p / rys),
            cyp = coef * (-rys * x1p / rxs);
  • §

    Step 3: center (cx, cy)

        const cx = cos * cxp - sin * cyp + (x1 + x2) / 2,
            cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
  • §

    Angles

        const angle = function (u, v) {
    
            const dot = u.x * v.x + u.y * v.y,
                mag = _sqrt((u.x * u.x + u.y * u.y) * (v.x * v.x + v.y * v.y));
    
            if (mag === 0) return 0;
    
            let ang = _acos(_min(1, _max(-1, dot / mag)));
    
            if (u.x * v.y - u.y * v.x < 0) ang = -ang;
    
            return ang;
        };
    
        const ux = (x1p - cxp) / rxs,
            uy = (y1p - cyp) / rys,
            vx = (-x1p - cxp) / rxs,
            vy = (-y1p - cyp) / rys;
    
        const theta1 = angle({x:1, y:0}, {x:ux, y:uy});
    
        let dtheta = angle({x:ux, y:uy}, {x:vx, y:vy});
    
        if (!fs && dtheta > 0) dtheta -= _piDouble;
        if (fs && dtheta < 0) dtheta += _piDouble;
  • §

    Split into segments of <= 90°

        const segs = Math.ceil(Math.abs(dtheta) / _piHalf),
            delta = dtheta / segs,
            t = (4 / 3) * _tan(delta / 4);
    
        const beziers = [];
    
        let i, th1, th2,
            cosTh1, sinTh1, cosTh2, sinTh2,
            _x1, _y1, _x2, _y2, _dx1, _dy1, _dx2, _dy2,
            cp1x, cp1y, cp2x, cp2y;
    
    
        for (i = 0; i < segs; i++) {
    
            th1 = theta1 + i * delta;
            th2 = th1 + delta;
    
            cosTh1 = _cos(th1);
            sinTh1 = _sin(th1);
            cosTh2 = _cos(th2);
            sinTh2 = _sin(th2);
    
            _x1 = cx + rxs * (cos * cosTh1 - sin * sinTh1);
            _y1 = cy + rys * (sin * cosTh1 + cos * sinTh1);
            _x2 = cx + rxs * (cos * cosTh2 - sin * sinTh2);
            _y2 = cy + rys * (sin * cosTh2 + cos * sinTh2);
    
            _dx1 = -rxs * (cos * sinTh1 + sin * cosTh1);
            _dy1 = -rys * (sin * sinTh1 - cos * cosTh1);
            _dx2 = -rxs * (cos * sinTh2 + sin * cosTh2);
            _dy2 = -rys * (sin * sinTh2 - cos * cosTh2);
    
            cp1x = _x1 + t * _dx1;
            cp1y = _y1 + t * _dy1;
            cp2x = _x2 - t * _dx2;
            cp2y = _y2 - t * _dy2;
    
            beziers.push([cp1x, cp1y, cp2x, cp2y, _x2, _y2]);
        }
        return beziers;
    };