ag-charts-community
Version:
Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue
655 lines • 25.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const intersection_1 = require("./intersection");
class Path2D {
constructor() {
// The methods of this class will likely be called many times per animation frame,
// and any allocation can trigger a GC cycle during animation, so we attempt
// to minimize the number of allocations.
this.commands = [];
this.params = [];
this._closedPath = false;
}
draw(ctx) {
const commands = this.commands;
const params = this.params;
const n = commands.length;
let j = 0;
ctx.beginPath();
for (let i = 0; i < n; i++) {
switch (commands[i]) {
case 'M':
ctx.moveTo(params[j++], params[j++]);
break;
case 'L':
ctx.lineTo(params[j++], params[j++]);
break;
case 'C':
ctx.bezierCurveTo(params[j++], params[j++], params[j++], params[j++], params[j++], params[j++]);
break;
case 'Z':
ctx.closePath();
break;
}
}
}
moveTo(x, y) {
if (this.xy) {
this.xy[0] = x;
this.xy[1] = y;
}
else {
this.xy = [x, y];
}
this.commands.push('M');
this.params.push(x, y);
}
lineTo(x, y) {
if (this.xy) {
this.commands.push('L');
this.params.push(x, y);
this.xy[0] = x;
this.xy[1] = y;
}
else {
this.moveTo(x, y);
}
}
rect(x, y, width, height) {
this.moveTo(x, y);
this.lineTo(x + width, y);
this.lineTo(x + width, y + height);
this.lineTo(x, y + height);
this.closePath();
}
/**
* Adds an arc segment to the path definition.
* https://www.w3.org/TR/SVG11/paths.html#PathDataEllipticalArcCommands
* @param rx The major-axis radius.
* @param ry The minor-axis radius.
* @param rotation The x-axis rotation, expressed in radians.
* @param fA The large arc flag. `1` to use angle > π.
* @param fS The sweep flag. `1` for the arc that goes to `x`/`y` clockwise.
* @param x2 The x coordinate to arc to.
* @param y2 The y coordinate to arc to.
*/
arcTo(rx, ry, rotation, fA, fS, x2, y2) {
// Convert from endpoint to center parametrization:
// https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
const xy = this.xy;
if (!xy) {
return;
}
if (rx < 0) {
rx = -rx;
}
if (ry < 0) {
ry = -ry;
}
const x1 = xy[0];
const y1 = xy[1];
const hdx = (x1 - x2) / 2;
const hdy = (y1 - y2) / 2;
const sinPhi = Math.sin(rotation);
const cosPhi = Math.cos(rotation);
const xp = cosPhi * hdx + sinPhi * hdy;
const yp = -sinPhi * hdx + cosPhi * hdy;
const ratX = xp / rx;
const ratY = yp / ry;
let lambda = ratX * ratX + ratY * ratY;
let cx = (x1 + x2) / 2;
let cy = (y1 + y2) / 2;
let cpx = 0;
let cpy = 0;
if (lambda >= 1) {
lambda = Math.sqrt(lambda);
rx *= lambda;
ry *= lambda;
// me gives lambda == cpx == cpy == 0;
}
else {
lambda = Math.sqrt(1 / lambda - 1);
if (fA === fS) {
lambda = -lambda;
}
cpx = lambda * rx * ratY;
cpy = -lambda * ry * ratX;
cx += cosPhi * cpx - sinPhi * cpy;
cy += sinPhi * cpx + cosPhi * cpy;
}
const theta1 = Math.atan2((yp - cpy) / ry, (xp - cpx) / rx);
const deltaTheta = Math.atan2((-yp - cpy) / ry, (-xp - cpx) / rx) - theta1;
// if (fS) {
// if (deltaTheta <= 0) {
// deltaTheta += Math.PI * 2;
// }
// }
// else {
// if (deltaTheta >= 0) {
// deltaTheta -= Math.PI * 2;
// }
// }
this.cubicArc(cx, cy, rx, ry, rotation, theta1, theta1 + deltaTheta, 1 - fS);
}
/**
* Approximates an elliptical arc with up to four cubic Bézier curves.
* @param commands The string array to write SVG command letters to.
* @param params The number array to write SVG command parameters (cubic control points) to.
* @param cx The x-axis coordinate for the ellipse's center.
* @param cy The y-axis coordinate for the ellipse's center.
* @param rx The ellipse's major-axis radius.
* @param ry The ellipse's minor-axis radius.
* @param phi The rotation for this ellipse, expressed in radians.
* @param theta1 The starting angle, measured clockwise from the positive x-axis and expressed in radians.
* @param theta2 The ending angle, measured clockwise from the positive x-axis and expressed in radians.
* @param anticlockwise The arc control points are always placed clockwise from `theta1` to `theta2`,
* even when `theta1 > theta2`, unless this flag is set to `1`.
*/
static cubicArc(commands, params, cx, cy, rx, ry, phi, theta1, theta2, anticlockwise) {
if (anticlockwise) {
const temp = theta1;
theta1 = theta2;
theta2 = temp;
}
const start = params.length;
// See https://pomax.github.io/bezierinfo/#circles_cubic
// Arc of unit circle (start angle = 0, end angle <= π/2) in cubic Bézier coordinates:
// S = [1, 0]
// C1 = [1, f]
// C2 = [cos(θ) + f * sin(θ), sin(θ) - f * cos(θ)]
// E = [cos(θ), sin(θ)]
// f = 4/3 * tan(θ/4)
const f90 = 0.5522847498307935; // f for θ = π/2 is 4/3 * (Math.sqrt(2) - 1)
const sinTheta1 = Math.sin(theta1);
const cosTheta1 = Math.cos(theta1);
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
const rightAngle = Math.PI / 2;
// Since we know how to draw an arc of a unit circle with a cubic Bézier,
// to draw an elliptical arc with arbitrary rotation and radii we:
// 1) rotate the Bézier coordinates that represent a circular arc by θ
// 2) scale the circular arc separately along the x/y axes, making it elliptical
// 3) rotate elliptical arc by φ
// |cos(φ) -sin(φ)| |sx 0| |cos(θ) -sin(θ)| -> |xx xy|
// |sin(φ) cos(φ)| | 0 sy| |sin(θ) cos(θ)| -> |yx yy|
let xx = cosPhi * cosTheta1 * rx - sinPhi * sinTheta1 * ry;
let yx = sinPhi * cosTheta1 * rx + cosPhi * sinTheta1 * ry;
let xy = -cosPhi * sinTheta1 * rx - sinPhi * cosTheta1 * ry;
let yy = -sinPhi * sinTheta1 * rx + cosPhi * cosTheta1 * ry;
// TODO: what if delta between θ1 and θ2 is greater than 2π?
// Always draw clockwise from θ1 to θ2.
theta2 -= theta1;
if (theta2 < 0) {
theta2 += Math.PI * 2;
}
// Multiplying each point [x, y] by:
// |xx xy cx| |x|
// |yx yy cy| |y|
// | 0 0 1| |1|
// TODO: This move command may be redundant, if we are already at this point.
// The coordinates of the point calculated here may differ ever so slightly
// because of precision error.
commands.push('M');
params.push(xx + cx, yx + cy);
while (theta2 >= rightAngle) {
theta2 -= rightAngle;
commands.push('C');
// Temp workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=993330
// Revert this commit when fixed ^^.
const lastX = xy + cx;
params.push(xx + xy * f90 + cx, yx + yy * f90 + cy, xx * f90 + xy + cx, yx * f90 + yy + cy, Math.abs(lastX) < 1e-8 ? 0 : lastX, yy + cy);
// Prepend π/2 rotation matrix.
// |xx xy| | 0 1| -> | xy -xx|
// |yx yy| |-1 0| -> | yy -yx|
// [xx, yx, xy, yy] = [xy, yy, -xx, -yx];
// Compared to swapping with a temp variable, destructuring is:
// - 10% faster in Chrome 70
// - 99% slower in Firefox 63
// Temp variable solution is 45% faster in FF than Chrome.
// https://jsperf.com/multi-swap
// https://bugzilla.mozilla.org/show_bug.cgi?id=1165569
let temp = xx;
xx = xy;
xy = -temp;
temp = yx;
yx = yy;
yy = -temp;
}
if (theta2) {
const f = 4 / 3 * Math.tan(theta2 / 4);
const sinPhi2 = Math.sin(theta2);
const cosPhi2 = Math.cos(theta2);
const C2x = cosPhi2 + f * sinPhi2;
const C2y = sinPhi2 - f * cosPhi2;
commands.push('C');
// Temp workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=993330
// Revert this commit when fixed ^^.
const lastX = xx * cosPhi2 + xy * sinPhi2 + cx;
params.push(xx + xy * f + cx, yx + yy * f + cy, xx * C2x + xy * C2y + cx, yx * C2x + yy * C2y + cy, Math.abs(lastX) < 1e-8 ? 0 : lastX, yx * cosPhi2 + yy * sinPhi2 + cy);
}
if (anticlockwise) {
for (let i = start, j = params.length - 2; i < j; i += 2, j -= 2) {
let temp = params[i];
params[i] = params[j];
params[j] = temp;
temp = params[i + 1];
params[i + 1] = params[j + 1];
params[j + 1] = temp;
}
}
}
cubicArc(cx, cy, rx, ry, phi, theta1, theta2, anticlockwise) {
const commands = this.commands;
const params = this.params;
const start = commands.length;
Path2D.cubicArc(commands, params, cx, cy, rx, ry, phi, theta1, theta2, anticlockwise);
const x = params[params.length - 2];
const y = params[params.length - 1];
if (this.xy) {
commands[start] = 'L';
this.xy[0] = x;
this.xy[1] = y;
}
else {
this.xy = [x, y];
}
}
/**
* Returns the `[x, y]` coordinates of the curve at `t`.
* @param points `(n + 1) * 2` control point coordinates for a Bézier curve of n-th order.
* @param t
*/
deCasteljau(points, t) {
const n = points.length;
if (n < 2 || n % 2 === 1) {
throw new Error('Fewer than two points or not an even count.');
}
else if (n === 2 || t === 0) {
return points.slice(0, 2);
}
else if (t === 1) {
return points.slice(-2);
}
else {
const newPoints = [];
const last = n - 2;
for (let i = 0; i < last; i += 2) {
newPoints.push((1 - t) * points[i] + t * points[i + 2], // x
(1 - t) * points[i + 1] + t * points[i + 3] // y
);
}
return this.deCasteljau(newPoints, t);
}
}
/**
* Approximates the given curve using `n` line segments.
* @param points `(n + 1) * 2` control point coordinates for a Bézier curve of n-th order.
* @param n
*/
approximateCurve(points, n) {
const xy = this.deCasteljau(points, 0);
this.moveTo(xy[0], xy[1]);
const step = 1 / n;
for (let t = step; t <= 1; t += step) {
const xy = this.deCasteljau(points, t);
this.lineTo(xy[0], xy[1]);
}
}
/**
* Adds a quadratic curve segment to the path definition.
* Note: the given quadratic segment is converted and stored as a cubic one.
* @param cx x-component of the curve's control point
* @param cy y-component of the curve's control point
* @param x x-component of the end point
* @param y y-component of the end point
*/
quadraticCurveTo(cx, cy, x, y) {
if (!this.xy) {
this.moveTo(cx, cy);
}
// See https://pomax.github.io/bezierinfo/#reordering
this.cubicCurveTo((this.xy[0] + 2 * cx) / 3, (this.xy[1] + 2 * cy) / 3, // 1/3 start + 2/3 control
(2 * cx + x) / 3, (2 * cy + y) / 3, // 2/3 control + 1/3 end
x, y);
}
cubicCurveTo(cx1, cy1, cx2, cy2, x, y) {
if (!this.xy) {
this.moveTo(cx1, cy1);
}
this.commands.push('C');
this.params.push(cx1, cy1, cx2, cy2, x, y);
this.xy[0] = x;
this.xy[1] = y;
}
get closedPath() {
return this._closedPath;
}
closePath() {
if (this.xy) {
this.xy = undefined;
this.commands.push('Z');
this._closedPath = true;
}
}
clear() {
this.commands.length = 0;
this.params.length = 0;
this.xy = undefined;
this._closedPath = false;
}
isPointInPath(x, y) {
const commands = this.commands;
const params = this.params;
const cn = commands.length;
// Hit testing using ray casting method, where the ray's origin is some point
// outside the path. In this case, an offscreen point that is remote enough, so that
// even if the path itself is large and is partially offscreen, the ray's origin
// will likely be outside the path anyway. To test if the given point is inside the
// path or not, we cast a ray from the origin to the given point and check the number
// of intersections of this segment with the path. If the number of intersections is
// even, then the ray both entered and exited the path an equal number of times,
// therefore the point is outside the path, and inside the path, if the number of
// intersections is odd. Since the path is compound, we check if the ray segment
// intersects with each of the path's segments, which can be either a line segment
// (one or no intersection points) or a Bézier curve segment (up to 3 intersection
// points).
const ox = -10000;
const oy = -10000;
// the starting point of the current path
let sx = NaN;
let sy = NaN;
// the previous point of the current path
let px = 0;
let py = 0;
let intersectionCount = 0;
for (let ci = 0, pi = 0; ci < cn; ci++) {
switch (commands[ci]) {
case 'M':
if (!isNaN(sx)) {
if (intersection_1.segmentIntersection(sx, sy, px, py, ox, oy, x, y)) {
intersectionCount++;
}
}
sx = px = params[pi++];
sy = py = params[pi++];
break;
case 'L':
if (intersection_1.segmentIntersection(px, py, px = params[pi++], py = params[pi++], ox, oy, x, y)) {
intersectionCount++;
}
break;
case 'C':
intersectionCount += intersection_1.cubicSegmentIntersections(px, py, params[pi++], params[pi++], params[pi++], params[pi++], px = params[pi++], py = params[pi++], ox, oy, x, y).length;
break;
case 'Z':
if (!isNaN(sx)) {
if (intersection_1.segmentIntersection(sx, sy, px, py, ox, oy, x, y)) {
intersectionCount++;
}
}
break;
}
}
return intersectionCount % 2 === 1;
}
static fromString(value) {
const path = new Path2D();
path.setFromString(value);
return path;
}
/**
* Split the SVG path at command letters,
* then extract the command letter and parameters from each substring.
* @param value
*/
static parseSvgPath(value) {
return value.trim().split(Path2D.splitCommandsRe).map(part => {
const strParams = part.match(Path2D.matchParamsRe);
return {
command: part.substr(0, 1),
params: strParams ? strParams.map(parseFloat) : []
};
});
}
static prettifySvgPath(value) {
return Path2D.parseSvgPath(value).map(d => d.command + d.params.join(',')).join('\n');
}
/**
* See https://www.w3.org/TR/SVG11/paths.html
* @param value
*/
setFromString(value) {
this.clear();
const parts = Path2D.parseSvgPath(value);
// Current point.
let x;
let y;
// Last control point. Used to calculate the reflection point
// for `S`, `s`, `T`, `t` commands.
let cpx;
let cpy;
let lastCommand;
function checkQuadraticCP() {
if (!lastCommand.match(Path2D.quadraticCommandRe)) {
cpx = x;
cpy = y;
}
}
function checkCubicCP() {
if (!lastCommand.match(Path2D.cubicCommandRe)) {
cpx = x;
cpy = y;
}
}
// But that will make compiler complain about x/y, cpx/cpy
// being used without being set first.
parts.forEach(part => {
const p = part.params;
const n = p.length;
let i = 0;
switch (part.command) {
case 'M':
this.moveTo(x = p[i++], y = p[i++]);
while (i < n) {
this.lineTo(x = p[i++], y = p[i++]);
}
break;
case 'm':
this.moveTo(x += p[i++], y += p[i++]);
while (i < n) {
this.lineTo(x += p[i++], y += p[i++]);
}
break;
case 'L':
while (i < n) {
this.lineTo(x = p[i++], y = p[i++]);
}
break;
case 'l':
while (i < n) {
this.lineTo(x += p[i++], y += p[i++]);
}
break;
case 'C':
while (i < n) {
this.cubicCurveTo(p[i++], p[i++], cpx = p[i++], cpy = p[i++], x = p[i++], y = p[i++]);
}
break;
case 'c':
while (i < n) {
this.cubicCurveTo(x + p[i++], y + p[i++], cpx = x + p[i++], cpy = y + p[i++], x += p[i++], y += p[i++]);
}
break;
case 'S':
checkCubicCP();
while (i < n) {
this.cubicCurveTo(x + x - cpx, y + y - cpy, cpx = p[i++], cpy = p[i++], x = p[i++], y = p[i++]);
}
break;
case 's':
checkCubicCP();
while (i < n) {
this.cubicCurveTo(x + x - cpx, y + y - cpy, cpx = x + p[i++], cpy = y + p[i++], x += p[i++], y += p[i++]);
}
break;
case 'Q':
while (i < n) {
this.quadraticCurveTo(cpx = p[i++], cpy = p[i++], x = p[i++], y = p[i++]);
}
break;
case 'q':
while (i < n) {
this.quadraticCurveTo(cpx = x + p[i++], cpy = y + p[i++], x += p[i++], y += p[i++]);
}
break;
case 'T':
checkQuadraticCP();
while (i < n) {
this.quadraticCurveTo(cpx = x + x - cpx, cpy = y + y - cpy, x = p[i++], y = p[i++]);
}
break;
case 't':
checkQuadraticCP();
while (i < n) {
this.quadraticCurveTo(cpx = x + x - cpx, cpy = y + y - cpy, x += p[i++], y += p[i++]);
}
break;
case 'A':
while (i < n) {
this.arcTo(p[i++], p[i++], p[i++] * Math.PI / 180, p[i++], p[i++], x = p[i++], y = p[i++]);
}
break;
case 'a':
while (i < n) {
this.arcTo(p[i++], p[i++], p[i++] * Math.PI / 180, p[i++], p[i++], x += p[i++], y += p[i++]);
}
break;
case 'Z':
case 'z':
this.closePath();
break;
case 'H':
while (i < n) {
this.lineTo(x = p[i++], y);
}
break;
case 'h':
while (i < n) {
this.lineTo(x += p[i++], y);
}
break;
case 'V':
while (i < n) {
this.lineTo(x, y = p[i++]);
}
break;
case 'v':
while (i < n) {
this.lineTo(x, y += p[i++]);
}
break;
}
lastCommand = part.command;
});
}
toString() {
const c = this.commands;
const p = this.params;
const cn = c.length;
const out = [];
for (let ci = 0, pi = 0; ci < cn; ci++) {
switch (c[ci]) {
case 'M':
out.push('M' + p[pi++] + ',' + p[pi++]);
break;
case 'L':
out.push('L' + p[pi++] + ',' + p[pi++]);
break;
case 'C':
out.push('C' + p[pi++] + ',' + p[pi++] + ' ' +
p[pi++] + ',' + p[pi++] + ' ' +
p[pi++] + ',' + p[pi++]);
break;
case 'Z':
out.push('Z');
break;
}
}
return out.join('');
}
toPrettyString() {
return Path2D.prettifySvgPath(this.toString());
}
toSvg() {
return `${Path2D.xmlDeclaration}
<svg width="100%" height="100%" viewBox="0 0 50 50" version="1.1" xmlns="${Path2D.xmlns}">
<path d="${this.toString()}" style="fill:none;stroke:#000;stroke-width:0.5;"/>
</svg>`;
}
toDebugSvg() {
const d = Path2D.prettifySvgPath(this.toString());
return `${Path2D.xmlDeclaration}
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="${Path2D.xmlns}">
<path d="${d}" style="fill:none;stroke:#000;stroke-width:0.5;"/>
</svg>`;
}
/**
* Returns an array of sub-paths of this Path,
* where each sub-path is represented exclusively by cubic segments.
*/
toCubicPaths() {
// Each sub-path is an array of `(n * 3 + 1) * 2` numbers,
// where `n` is the number of segments.
const paths = [];
const params = this.params;
// current path
let path;
// the starting point of the current path
let sx;
let sy;
// the previous point of the current path
let px;
let py;
let i = 0; // current parameter
this.commands.forEach(command => {
switch (command) {
case 'M':
path = [
sx = px = params[i++],
sy = py = params[i++]
];
paths.push(path);
break;
case 'L':
const x = params[i++];
const y = params[i++];
// Place control points along the line `a + (b - a) * t`
// at t = 1/3 and 2/3:
path.push((px + px + x) / 3, (py + py + y) / 3, (px + x + x) / 3, (py + y + y) / 3, px = x, py = y);
break;
case 'C':
path.push(params[i++], params[i++], params[i++], params[i++], px = params[i++], py = params[i++]);
break;
case 'Z':
path.push((px + px + sx) / 3, (py + py + sy) / 3, (px + sx + sx) / 3, (py + sy + sy) / 3, px = sx, py = sy);
break;
}
});
return paths;
}
static cubicPathToString(path) {
const n = path.length;
if (!(n % 2 === 0 && (n / 2 - 1) / 2 >= 1)) {
throw new Error('Invalid path.');
}
return 'M' + path.slice(0, 2).join(',') + 'C' + path.slice(2).join(',');
}
}
exports.Path2D = Path2D;
Path2D.splitCommandsRe = /(?=[AaCcHhLlMmQqSsTtVvZz])/g;
Path2D.matchParamsRe = /-?[0-9]*\.?\d+/g;
Path2D.quadraticCommandRe = /[QqTt]/;
Path2D.cubicCommandRe = /[CcSs]/;
Path2D.xmlDeclaration = '<?xml version="1.0" encoding="UTF-8"?>';
Path2D.xmlns = 'http://www.w3.org/2000/svg';
//# sourceMappingURL=path2D.js.map