import { releaseArray, requestArray } from './array-pool.js';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
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 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
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
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;
};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);
};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
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;
};