TargetProcess/tauCharts

View on GitHub
src/utils/path/svg/brush-line.ts

Summary

Maintainability
C
1 day
Test Coverage
import {
    getBezierPoint as bezierPt,
    splitCubicSegment as split
} from '../bezier';

import {Point as Point} from '../point';

/**
 * Returns line with variable width.
 * @param points Linear points.
 */
export function getBrushLine(points: Point[]) {
    if (points.length === 0) {
        return '';
    }
    if (points.length === 1) {
        return getCirclePath(points[0]);
    }
    var segments = [];
    for (var i = 1; i < points.length; i++) {
        segments.push(getStraightSegmentPath(points[i - 1], points[i]));
    }
    return segments.join(' ');
}

/**
 * Returns curve with variable width.
 * @param points Cubic spline points.
 */
export function getBrushCurve(points: Point[]) {
    if (points.length === 0) {
        return '';
    }
    if (points.length === 1) {
        return getCirclePath(points[0]);
    }
    var segments = [];
    for (var i = 3; i < points.length; i += 3) {
        segments.push(getCurveSegmentPath(points[i - 3], points[i - 2], points[i - 1], points[i]));
    }
    return segments.join(' ');
}

function getCirclePath(pt: Point) {
    var r = (pt.size / 2);
    return [
        `M${pt.x},${pt.y - r}`,
        `A${r},${r} 0 0 1`,
        `${pt.x},${pt.y + r}`,
        `A${r},${r} 0 0 1`,
        `${pt.x},${pt.y - r}`,
        'Z'
    ].join(' ');
}

function getStraightSegmentPath(a: Point, b: Point) {
    var tan = getCirclesTangents(a, b);
    if (!tan) {
        return getCirclePath((a.size > b.size ? a : b));
    }
    return [
        `M${tan.left[0].x},${tan.left[0].y}`,
        `L${tan.left[1].x},${tan.left[1].y}`,
        `A${b.size / 2},${b.size / 2} 0 ${Number(a.size < b.size)} 1`,
        `${tan.right[1].x},${tan.right[1].y}`,
        `L${tan.right[0].x},${tan.right[0].y}`,
        `A${a.size / 2},${a.size / 2} 0 ${Number(a.size > b.size)} 1`,
        `${tan.left[0].x},${tan.left[0].y}`,
        'Z'
    ].join(' ');
}

function getCurveSegmentPath(a: Point, ca: Point, cb: Point, b: Point) {
    var ctan = getCirclesCurveTangents(a, ca, cb, b);
    if (!ctan) {
        return getStraightSegmentPath(a, b);
    }
    var qa = rotation(angle(a, ctan.right[0]), angle(a, ctan.left[0]));
    var qb = rotation(angle(b, ctan.right[1]), angle(b, ctan.left[1]));
    return [
        `M${ctan.left[0].x},${ctan.left[0].y}`,
        `C${ctan.left[1].x},${ctan.left[1].y}`,
        `${ctan.left[2].x},${ctan.left[2].y}`,
        `${ctan.left[3].x},${ctan.left[3].y}`,
        `A${b.size / 2},${b.size / 2} 0 ${Number(qa > Math.PI)} 1`,
        `${ctan.right[3].x},${ctan.right[3].y}`,
        `C${ctan.right[2].x},${ctan.right[2].y}`,
        `${ctan.right[1].x},${ctan.right[1].y}`,
        `${ctan.right[0].x},${ctan.right[0].y}`,
        `A${a.size / 2},${a.size / 2} 0 ${Number(qb > Math.PI)} 1`,
        `${ctan.left[0].x},${ctan.left[0].y}`,
        'Z'
    ].join(' ');
}

function angle(a: Point, b: Point) {
    return Math.atan2(b.y - a.y, b.x - a.x);
}

function rotation(a: number, b: number) {
    if (b < a) {
        b += 2 * Math.PI;
    }
    return (b - a);
}

function dist(...p: Point[]) {
    var total = 0;
    for (var i = 1; i < p.length; i++) {
        total += Math.sqrt(
            (p[i].x - p[i - 1].x) * (p[i].x - p[i - 1].x) +
            (p[i].y - p[i - 1].y) * (p[i].y - p[i - 1].y)
        );
    }
    return total;
}

function polar(start: Point, d: number, a: number) {
    return {
        x: (start.x + d * Math.cos(a)),
        y: (start.y + d * Math.sin(a))
    };
}

function splitCurveSegment(t: number, ...p: Point[]);
function splitCurveSegment(t: number, p0: Point, c0: Point, c1: Point, p1: Point) {
    var seg = split(t, p0, c0, c1, p1);
    var tl = 1 / (1 +
        dist(seg[3], seg[4], seg[5], seg[6], seg[3]) /
        dist(seg[0], seg[1], seg[2], seg[3], seg[0])
    );
    (<Point>seg[3]).size = (p0.size * (1 - tl) + p1.size * tl);

    return seg;
}

function approximateQuadCurve(...p: Point[]);
function approximateQuadCurve(p0: Point, p1: Point, p2: Point) {
    var m = bezierPt(dist(p0, p1) / dist(p0, p1, p2), p0, p2);
    var c = bezierPt(2, m, p1);
    return [p0, c, p2];
}

function getCirclesTangents(a, b) {
    var d = dist(a, b);
    if (d === 0 ||
        (d + a.size / 2 <= b.size / 2) ||
        (d + b.size / 2 <= a.size / 2)
    ) {
        return null;
    }

    var ma = angle(a, b);
    var ta = Math.asin((a.size - b.size) / d / 2);
    var aleft = (ma - Math.PI / 2 + ta);
    var aright = (ma + Math.PI / 2 - ta);

    return {
        left: [
            polar(a, a.size / 2, aleft),
            polar(b, b.size / 2, aleft)
        ],
        right: [
            polar(a, a.size / 2, aright),
            polar(b, b.size / 2, aright)
        ]
    };
}

function getCirclesCurveTangents(a: Point, ca: Point, cb: Point, b: Point) {
    var d = dist(a, b);
    if (d === 0 ||
        (d + a.size / 2 <= b.size / 2) ||
        (d + b.size / 2 <= a.size / 2)
    ) {
        return null;
    }

    // Get approximate endings tangents
    // TODO: Use formulas instead of approximate equations.
    const kt = 1 / 12;
    var getTangentsVectors = (isEnd) => {
        var curve = (isEnd ? [b, cb, ca, a] : [a, ca, cb, b]);
        var seg1 = splitCurveSegment(2 * kt, ...curve);
        var seg2 = splitCurveSegment(0.5, ...seg1.slice(0, 4));

        var m = seg2[3];
        var n = seg2[6];
        var mtan = getCirclesTangents(curve[0], m);
        var ntan = getCirclesTangents(m, n);

        var lpoints = [
            mtan.left[0],
            bezierPt(0.5, mtan.left[1], ntan.left[0]),
            ntan.left[1]
        ];
        var rpoints = [
            mtan.right[0],
            bezierPt(0.5, mtan.right[1], ntan.right[0]),
            ntan.right[1]
        ];

        var lq = approximateQuadCurve(...lpoints)[1];
        var rq = approximateQuadCurve(...rpoints)[1];
        var lc = bezierPt(1 / 3 / kt, mtan.left[0], lq);
        var rc = bezierPt(1 / 3 / kt, mtan.right[0], rq);

        return {
            left: (isEnd ? [rc, rpoints[0]] : [lpoints[0], lc]),
            right: (isEnd ? [lc, lpoints[0]] : [rpoints[0], rc])
        };
    };

    var tstart = getTangentsVectors(false);
    var tend = getTangentsVectors(true);

    return {
        left: [...tstart.left, ...tend.left],
        right: [...tstart.right, ...tend.right]
    };
}