fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
1,045 lines (1,003 loc) • 29.4 kB
text/typescript
import { cache } from '../../cache';
import { config } from '../../config';
import { halfPI, PiBy180 } from '../../constants';
import type { TMat2D, TRadian, TRectBounds } from '../../typedefs';
import { cos } from '../misc/cos';
import { multiplyTransformMatrices, transformPoint } from '../misc/matrix';
import { sin } from '../misc/sin';
import { toFixed } from '../misc/toFixed';
import type {
TCurveInfo,
TComplexPathData,
TParsedAbsoluteCubicCurveCommand,
TPathSegmentInfo,
TPointAngle,
TSimpleParsedCommand,
TSimplePathData,
TPathSegmentCommandInfo,
TComplexParsedCommand,
TPathSegmentInfoCommon,
TEndPathInfo,
TParsedArcCommand,
TComplexParsedCommandType,
} from './typedefs';
import type { XY } from '../../Point';
import { Point } from '../../Point';
import { reArcCommandPoints, rePathCommand } from './regex';
import { reNum } from '../../parser/constants';
/**
* Commands that may be repeated
*/
const repeatedCommands: Record<string, 'l' | 'L'> = {
m: 'l',
M: 'L',
};
/**
* Convert an arc of a rotated ellipse to a Bezier Curve
* @param {TRadian} theta1 start of the arc
* @param {TRadian} theta2 end of the arc
* @param cosTh cosine of the angle of rotation
* @param sinTh sine of the angle of rotation
* @param rx x-axis radius (before rotation)
* @param ry y-axis radius (before rotation)
* @param cx1 center x of the ellipse
* @param cy1 center y of the ellipse
* @param mT
* @param fromX starting point of arc x
* @param fromY starting point of arc y
*/
const segmentToBezier = (
theta1: TRadian,
theta2: TRadian,
cosTh: number,
sinTh: number,
rx: number,
ry: number,
cx1: number,
cy1: number,
mT: number,
fromX: number,
fromY: number,
): TParsedAbsoluteCubicCurveCommand => {
const costh1 = cos(theta1),
sinth1 = sin(theta1),
costh2 = cos(theta2),
sinth2 = sin(theta2),
toX = cosTh * rx * costh2 - sinTh * ry * sinth2 + cx1,
toY = sinTh * rx * costh2 + cosTh * ry * sinth2 + cy1,
cp1X = fromX + mT * (-cosTh * rx * sinth1 - sinTh * ry * costh1),
cp1Y = fromY + mT * (-sinTh * rx * sinth1 + cosTh * ry * costh1),
cp2X = toX + mT * (cosTh * rx * sinth2 + sinTh * ry * costh2),
cp2Y = toY + mT * (sinTh * rx * sinth2 - cosTh * ry * costh2);
return ['C', cp1X, cp1Y, cp2X, cp2Y, toX, toY];
};
/**
* Adapted from {@link http://dxr.mozilla.org/mozilla-central/source/dom/svg/SVGPathDataParser.cpp}
* by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here
* http://mozilla.org/MPL/2.0/
* @param toX
* @param toY
* @param rx
* @param ry
* @param {number} large 0 or 1 flag
* @param {number} sweep 0 or 1 flag
* @param rotateX
*/
const arcToSegments = (
toX: number,
toY: number,
rx: number,
ry: number,
large: number,
sweep: number,
rotateX: TRadian,
): TParsedAbsoluteCubicCurveCommand[] => {
if (rx === 0 || ry === 0) {
return [];
}
let fromX = 0,
fromY = 0,
root = 0;
const PI = Math.PI,
theta = rotateX * PiBy180,
sinTheta = sin(theta),
cosTh = cos(theta),
px = 0.5 * (-cosTh * toX - sinTheta * toY),
py = 0.5 * (-cosTh * toY + sinTheta * toX),
rx2 = rx ** 2,
ry2 = ry ** 2,
py2 = py ** 2,
px2 = px ** 2,
pl = rx2 * ry2 - rx2 * py2 - ry2 * px2;
let _rx = Math.abs(rx);
let _ry = Math.abs(ry);
if (pl < 0) {
const s = Math.sqrt(1 - pl / (rx2 * ry2));
_rx *= s;
_ry *= s;
} else {
root =
(large === sweep ? -1.0 : 1.0) * Math.sqrt(pl / (rx2 * py2 + ry2 * px2));
}
const cx = (root * _rx * py) / _ry,
cy = (-root * _ry * px) / _rx,
cx1 = cosTh * cx - sinTheta * cy + toX * 0.5,
cy1 = sinTheta * cx + cosTh * cy + toY * 0.5;
let mTheta = calcVectorAngle(1, 0, (px - cx) / _rx, (py - cy) / _ry);
let dtheta = calcVectorAngle(
(px - cx) / _rx,
(py - cy) / _ry,
(-px - cx) / _rx,
(-py - cy) / _ry,
);
if (sweep === 0 && dtheta > 0) {
dtheta -= 2 * PI;
} else if (sweep === 1 && dtheta < 0) {
dtheta += 2 * PI;
}
// Convert into cubic bezier segments <= 90deg
const segments = Math.ceil(Math.abs((dtheta / PI) * 2)),
result = [],
mDelta = dtheta / segments,
mT =
((8 / 3) * Math.sin(mDelta / 4) * Math.sin(mDelta / 4)) /
Math.sin(mDelta / 2);
let th3 = mTheta + mDelta;
for (let i = 0; i < segments; i++) {
result[i] = segmentToBezier(
mTheta,
th3,
cosTh,
sinTheta,
_rx,
_ry,
cx1,
cy1,
mT,
fromX,
fromY,
);
fromX = result[i][5];
fromY = result[i][6];
mTheta = th3;
th3 += mDelta;
}
return result;
};
/**
* @private
* Calculate the angle between two vectors
* @param ux u endpoint x
* @param uy u endpoint y
* @param vx v endpoint x
* @param vy v endpoint y
*/
const calcVectorAngle = (
ux: number,
uy: number,
vx: number,
vy: number,
): TRadian => {
const ta = Math.atan2(uy, ux),
tb = Math.atan2(vy, vx);
if (tb >= ta) {
return tb - ta;
} else {
return 2 * Math.PI - (ta - tb);
}
};
// functions for the Cubic beizer
// taken from: https://github.com/konvajs/konva/blob/7.0.5/src/shapes/Path.ts#L350
const CB1 = (t: number) => t ** 3;
const CB2 = (t: number) => 3 * t ** 2 * (1 - t);
const CB3 = (t: number) => 3 * t * (1 - t) ** 2;
const CB4 = (t: number) => (1 - t) ** 3;
/**
* Calculate bounding box of a cubic Bezier curve
* Taken from http://jsbin.com/ivomiq/56/edit (no credits available)
* TODO: can we normalize this with the starting points set at 0 and then translated the bbox?
* @param {number} begx starting point
* @param {number} begy
* @param {number} cp1x first control point
* @param {number} cp1y
* @param {number} cp2x second control point
* @param {number} cp2y
* @param {number} endx end of bezier
* @param {number} endy
* @return {TRectBounds} the rectangular bounds
*/
export function getBoundsOfCurve(
begx: number,
begy: number,
cp1x: number,
cp1y: number,
cp2x: number,
cp2y: number,
endx: number,
endy: number,
): TRectBounds {
let argsString: string;
if (config.cachesBoundsOfCurve) {
// eslint-disable-next-line
argsString = [...arguments].join();
if (cache.boundsOfCurveCache[argsString]) {
return cache.boundsOfCurveCache[argsString];
}
}
const sqrt = Math.sqrt,
abs = Math.abs,
tvalues = [],
bounds: [[x: number, y: number], [x: number, y: number]] = [
[0, 0],
[0, 0],
];
let b = 6 * begx - 12 * cp1x + 6 * cp2x;
let a = -3 * begx + 9 * cp1x - 9 * cp2x + 3 * endx;
let c = 3 * cp1x - 3 * begx;
for (let i = 0; i < 2; ++i) {
if (i > 0) {
b = 6 * begy - 12 * cp1y + 6 * cp2y;
a = -3 * begy + 9 * cp1y - 9 * cp2y + 3 * endy;
c = 3 * cp1y - 3 * begy;
}
if (abs(a) < 1e-12) {
if (abs(b) < 1e-12) {
continue;
}
const t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
const b2ac = b * b - 4 * c * a;
if (b2ac < 0) {
continue;
}
const sqrtb2ac = sqrt(b2ac);
const t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
const t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
let j = tvalues.length;
const jlen = j;
const iterator = getPointOnCubicBezierIterator(
begx,
begy,
cp1x,
cp1y,
cp2x,
cp2y,
endx,
endy,
);
while (j--) {
const { x, y } = iterator(tvalues[j]);
bounds[0][j] = x;
bounds[1][j] = y;
}
bounds[0][jlen] = begx;
bounds[1][jlen] = begy;
bounds[0][jlen + 1] = endx;
bounds[1][jlen + 1] = endy;
const result: TRectBounds = [
new Point(Math.min(...bounds[0]), Math.min(...bounds[1])),
new Point(Math.max(...bounds[0]), Math.max(...bounds[1])),
];
if (config.cachesBoundsOfCurve) {
cache.boundsOfCurveCache[argsString!] = result;
}
return result;
}
/**
* Converts arc to a bunch of cubic Bezier curves
* @param {number} fx starting point x
* @param {number} fy starting point y
* @param {TParsedArcCommand} coords Arc command
*/
export const fromArcToBeziers = (
fx: number,
fy: number,
[_, rx, ry, rot, large, sweep, tx, ty]: TParsedArcCommand,
): TParsedAbsoluteCubicCurveCommand[] => {
const segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot);
for (let i = 0, len = segsNorm.length; i < len; i++) {
segsNorm[i][1] += fx;
segsNorm[i][2] += fy;
segsNorm[i][3] += fx;
segsNorm[i][4] += fy;
segsNorm[i][5] += fx;
segsNorm[i][6] += fy;
}
return segsNorm;
};
/**
* This function takes a parsed SVG path and makes it simpler for fabricJS logic.
* Simplification consist of:
* - All commands converted to absolute (lowercase to uppercase)
* - S converted to C
* - T converted to Q
* - A converted to C
* @param {TComplexPathData} path the array of commands of a parsed SVG path for `Path`
* @return {TSimplePathData} the simplified array of commands of a parsed SVG path for `Path`
* TODO: figure out how to remove the type assertions in a nice way
*/
export const makePathSimpler = (path: TComplexPathData): TSimplePathData => {
// x and y represent the last point of the path, AKA the previous command point.
// we add them to each relative command to make it an absolute comment.
// we also swap the v V h H with L, because are easier to transform.
let x = 0,
y = 0;
// x1 and y1 represent the last point of the subpath. the subpath is started with
// m or M command. When a z or Z command is drawn, x and y need to be resetted to
// the last x1 and y1.
let x1 = 0,
y1 = 0;
// previous will host the letter of the previous command, to handle S and T.
// controlX and controlY will host the previous reflected control point
const destinationPath: TSimplePathData = [];
let previous,
// placeholders
controlX = 0,
controlY = 0;
for (const parsedCommand of path) {
const current: TComplexParsedCommand = [...parsedCommand];
let converted: TSimpleParsedCommand | undefined;
switch (
current[0] // first letter
) {
case 'l': // lineto, relative
current[1] += x;
current[2] += y;
// falls through
case 'L':
x = current[1];
y = current[2];
converted = ['L', x, y];
break;
case 'h': // horizontal lineto, relative
current[1] += x;
// falls through
case 'H':
x = current[1];
converted = ['L', x, y];
break;
case 'v': // vertical lineto, relative
current[1] += y;
// falls through
case 'V':
y = current[1];
converted = ['L', x, y];
break;
case 'm': // moveTo, relative
current[1] += x;
current[2] += y;
// falls through
case 'M':
x = current[1];
y = current[2];
x1 = current[1];
y1 = current[2];
converted = ['M', x, y];
break;
case 'c': // bezierCurveTo, relative
current[1] += x;
current[2] += y;
current[3] += x;
current[4] += y;
current[5] += x;
current[6] += y;
// falls through
case 'C':
controlX = current[3];
controlY = current[4];
x = current[5];
y = current[6];
converted = ['C', current[1], current[2], controlX, controlY, x, y];
break;
case 's': // shorthand cubic bezierCurveTo, relative
current[1] += x;
current[2] += y;
current[3] += x;
current[4] += y;
// falls through
case 'S':
// would be sScC but since we are swapping sSc for C, we check just that.
if (previous === 'C') {
// calculate reflection of previous control points
controlX = 2 * x - controlX;
controlY = 2 * y - controlY;
} else {
// If there is no previous command or if the previous command was not a C, c, S, or s,
// the control point is coincident with the current point
controlX = x;
controlY = y;
}
x = current[3];
y = current[4];
converted = ['C', controlX, controlY, current[1], current[2], x, y];
// converted[3] and converted[4] are NOW the second control point.
// we keep it for the next reflection.
controlX = converted[3];
controlY = converted[4];
break;
case 'q': // quadraticCurveTo, relative
current[1] += x;
current[2] += y;
current[3] += x;
current[4] += y;
// falls through
case 'Q':
controlX = current[1];
controlY = current[2];
x = current[3];
y = current[4];
converted = ['Q', controlX, controlY, x, y];
break;
case 't': // shorthand quadraticCurveTo, relative
current[1] += x;
current[2] += y;
// falls through
case 'T':
if (previous === 'Q') {
// calculate reflection of previous control point
controlX = 2 * x - controlX;
controlY = 2 * y - controlY;
} else {
// If there is no previous command or if the previous command was not a Q, q, T or t,
// assume the control point is coincident with the current point
controlX = x;
controlY = y;
}
x = current[1];
y = current[2];
converted = ['Q', controlX, controlY, x, y];
break;
case 'a':
current[6] += x;
current[7] += y;
// falls through
case 'A':
fromArcToBeziers(x, y, current).forEach((b) => destinationPath.push(b));
x = current[6];
y = current[7];
break;
case 'z':
case 'Z':
x = x1;
y = y1;
converted = ['Z'];
break;
default:
}
if (converted) {
destinationPath.push(converted);
previous = converted[0];
} else {
previous = '';
}
}
return destinationPath;
};
// todo verify if we can just use the point class here
/**
* Calc length from point x1,y1 to x2,y2
* @param {number} x1 starting point x
* @param {number} y1 starting point y
* @param {number} x2 starting point x
* @param {number} y2 starting point y
* @return {number} length of segment
*/
const calcLineLength = (
x1: number,
y1: number,
x2: number,
y2: number,
): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
/**
* Get an iterator that takes a percentage and returns a point
* @param {number} begx
* @param {number} begy
* @param {number} cp1x
* @param {number} cp1y
* @param {number} cp2x
* @param {number} cp2y
* @param {number} endx
* @param {number} endy
*/
const getPointOnCubicBezierIterator =
(
begx: number,
begy: number,
cp1x: number,
cp1y: number,
cp2x: number,
cp2y: number,
endx: number,
endy: number,
) =>
(pct: number) => {
const c1 = CB1(pct),
c2 = CB2(pct),
c3 = CB3(pct),
c4 = CB4(pct);
return new Point(
endx * c1 + cp2x * c2 + cp1x * c3 + begx * c4,
endy * c1 + cp2y * c2 + cp1y * c3 + begy * c4,
);
};
const QB1 = (t: number) => t ** 2;
const QB2 = (t: number) => 2 * t * (1 - t);
const QB3 = (t: number) => (1 - t) ** 2;
const getTangentCubicIterator =
(
p1x: number,
p1y: number,
p2x: number,
p2y: number,
p3x: number,
p3y: number,
p4x: number,
p4y: number,
) =>
(pct: number) => {
const qb1 = QB1(pct),
qb2 = QB2(pct),
qb3 = QB3(pct),
tangentX =
3 * (qb3 * (p2x - p1x) + qb2 * (p3x - p2x) + qb1 * (p4x - p3x)),
tangentY =
3 * (qb3 * (p2y - p1y) + qb2 * (p3y - p2y) + qb1 * (p4y - p3y));
return Math.atan2(tangentY, tangentX);
};
const getPointOnQuadraticBezierIterator =
(
p1x: number,
p1y: number,
p2x: number,
p2y: number,
p3x: number,
p3y: number,
) =>
(pct: number) => {
const c1 = QB1(pct),
c2 = QB2(pct),
c3 = QB3(pct);
return new Point(
p3x * c1 + p2x * c2 + p1x * c3,
p3y * c1 + p2y * c2 + p1y * c3,
);
};
const getTangentQuadraticIterator =
(
p1x: number,
p1y: number,
p2x: number,
p2y: number,
p3x: number,
p3y: number,
) =>
(pct: number) => {
const invT = 1 - pct,
tangentX = 2 * (invT * (p2x - p1x) + pct * (p3x - p2x)),
tangentY = 2 * (invT * (p2y - p1y) + pct * (p3y - p2y));
return Math.atan2(tangentY, tangentX);
};
// this will run over a path segment (a cubic or quadratic segment) and approximate it
// with 100 segments. This will good enough to calculate the length of the curve
const pathIterator = (
iterator: (pct: number) => Point,
x1: number,
y1: number,
) => {
let tempP = new Point(x1, y1),
tmpLen = 0;
for (let perc = 1; perc <= 100; perc += 1) {
const p = iterator(perc / 100);
tmpLen += calcLineLength(tempP.x, tempP.y, p.x, p.y);
tempP = p;
}
return tmpLen;
};
/**
* Given a pathInfo, and a distance in pixels, find the percentage from 0 to 1
* that correspond to that pixels run over the path.
* The percentage will be then used to find the correct point on the canvas for the path.
* @param {Array} segInfo fabricJS collection of information on a parsed path
* @param {number} distance from starting point, in pixels.
* @return {TPointAngle} info object with x and y ( the point on canvas ) and angle, the tangent on that point;
*/
const findPercentageForDistance = (
segInfo: TCurveInfo<'Q' | 'C'>,
distance: number,
): TPointAngle => {
let perc = 0,
tmpLen = 0,
tempP: XY = { x: segInfo.x, y: segInfo.y },
p: XY = { ...tempP },
nextLen: number,
nextStep = 0.01,
lastPerc = 0;
// nextStep > 0.0001 covers 0.00015625 that 1/64th of 1/100
// the path
const iterator = segInfo.iterator,
angleFinder = segInfo.angleFinder;
while (tmpLen < distance && nextStep > 0.0001) {
p = iterator(perc);
lastPerc = perc;
nextLen = calcLineLength(tempP.x, tempP.y, p.x, p.y);
// compare tmpLen each cycle with distance, decide next perc to test.
if (nextLen + tmpLen > distance) {
// we discard this step and we make smaller steps.
perc -= nextStep;
nextStep /= 2;
} else {
tempP = p;
perc += nextStep;
tmpLen += nextLen;
}
}
return { ...p, angle: angleFinder(lastPerc) };
};
/**
* Run over a parsed and simplified path and extract some information (length of each command and starting point)
* @param {TSimplePathData} path parsed path commands
* @return {TPathSegmentInfo[]} path commands information
*/
export const getPathSegmentsInfo = (
path: TSimplePathData,
): TPathSegmentInfo[] => {
let totalLength = 0,
//x2 and y2 are the coords of segment start
//x1 and y1 are the coords of the current point
x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0,
iterator,
tempInfo: TPathSegmentInfo;
const info: TPathSegmentInfo[] = [];
for (const current of path) {
const basicInfo: TPathSegmentInfoCommon<keyof TPathSegmentCommandInfo> = {
x: x1,
y: y1,
command: current[0],
length: 0,
};
switch (
current[0] //first letter
) {
case 'M':
tempInfo = <TPathSegmentInfoCommon<'M'>>basicInfo;
tempInfo.x = x2 = x1 = current[1];
tempInfo.y = y2 = y1 = current[2];
break;
case 'L':
tempInfo = <TPathSegmentInfoCommon<'L'>>basicInfo;
tempInfo.length = calcLineLength(x1, y1, current[1], current[2]);
x1 = current[1];
y1 = current[2];
break;
case 'C':
iterator = getPointOnCubicBezierIterator(
x1,
y1,
current[1],
current[2],
current[3],
current[4],
current[5],
current[6],
);
tempInfo = <TCurveInfo<'C'>>basicInfo;
tempInfo.iterator = iterator;
tempInfo.angleFinder = getTangentCubicIterator(
x1,
y1,
current[1],
current[2],
current[3],
current[4],
current[5],
current[6],
);
tempInfo.length = pathIterator(iterator, x1, y1);
x1 = current[5];
y1 = current[6];
break;
case 'Q':
iterator = getPointOnQuadraticBezierIterator(
x1,
y1,
current[1],
current[2],
current[3],
current[4],
);
tempInfo = <TCurveInfo<'Q'>>basicInfo;
tempInfo.iterator = iterator;
tempInfo.angleFinder = getTangentQuadraticIterator(
x1,
y1,
current[1],
current[2],
current[3],
current[4],
);
tempInfo.length = pathIterator(iterator, x1, y1);
x1 = current[3];
y1 = current[4];
break;
case 'Z':
// we add those in order to ease calculations later
tempInfo = <TEndPathInfo>basicInfo;
tempInfo.destX = x2;
tempInfo.destY = y2;
tempInfo.length = calcLineLength(x1, y1, x2, y2);
x1 = x2;
y1 = y2;
break;
}
totalLength += tempInfo.length;
info.push(tempInfo);
}
info.push({ length: totalLength, x: x1, y: y1 });
return info;
};
/**
* Get the point on the path that is distance along the path
* @param path
* @param distance
* @param infos
*/
export const getPointOnPath = (
path: TSimplePathData,
distance: number,
infos: TPathSegmentInfo[] = getPathSegmentsInfo(path),
): TPointAngle | undefined => {
let i = 0;
while (distance - infos[i].length > 0 && i < infos.length - 2) {
distance -= infos[i].length;
i++;
}
const segInfo = infos[i],
segPercent = distance / segInfo.length,
segment = path[i];
switch (segInfo.command) {
case 'M':
return { x: segInfo.x, y: segInfo.y, angle: 0 };
case 'Z':
return {
...new Point(segInfo.x, segInfo.y).lerp(
new Point(segInfo.destX, segInfo.destY),
segPercent,
),
angle: Math.atan2(segInfo.destY - segInfo.y, segInfo.destX - segInfo.x),
};
case 'L':
return {
...new Point(segInfo.x, segInfo.y).lerp(
new Point(segment[1]!, segment[2]!),
segPercent,
),
angle: Math.atan2(segment[2]! - segInfo.y, segment[1]! - segInfo.x),
};
case 'C':
return findPercentageForDistance(segInfo, distance);
case 'Q':
return findPercentageForDistance(segInfo, distance);
default:
// throw Error('Invalid command');
}
};
const rePathCmdAll = new RegExp(rePathCommand, 'gi');
const regExpArcCommandPoints = new RegExp(reArcCommandPoints, 'g');
const reMyNum = new RegExp(reNum, 'gi');
const commandLengths = {
m: 2,
l: 2,
h: 1,
v: 1,
c: 6,
s: 4,
q: 4,
t: 2,
a: 7,
} as const;
/**
*
* @param {string} pathString
* @return {TComplexPathData} An array of SVG path commands
* @example <caption>Usage</caption>
* parsePath('M 3 4 Q 3 5 2 1 4 0 Q 9 12 2 1 4 0') === [
* ['M', 3, 4],
* ['Q', 3, 5, 2, 1, 4, 0],
* ['Q', 9, 12, 2, 1, 4, 0],
* ];
*/
export const parsePath = (pathString: string): TComplexPathData => {
const chain: TComplexPathData = [];
const all = pathString.match(rePathCmdAll) ?? [];
for (const matchStr of all) {
// take match string and save the first letter as the command
const commandLetter = matchStr[0] as TComplexParsedCommandType;
// in case of Z we have very little to do
if (commandLetter === 'z' || commandLetter === 'Z') {
chain.push([commandLetter]);
continue;
}
const commandLength =
commandLengths[
commandLetter.toLowerCase() as keyof typeof commandLengths
];
let paramArr = [];
if (commandLetter === 'a' || commandLetter === 'A') {
// the arc command ha some peculariaties that requires a special regex other than numbers
// it is possible to avoid using a space between the sweep and large arc flags, making them either
// 00, 01, 10 or 11, making them identical to a plain number for the regex reMyNum
// reset the regexp
regExpArcCommandPoints.lastIndex = 0;
for (let out = null; (out = regExpArcCommandPoints.exec(matchStr)); ) {
paramArr.push(...out.slice(1));
}
} else {
paramArr = matchStr.match(reMyNum) || [];
}
// inspect the length of paramArr, if is longer than commandLength
// we are dealing with repeated commands
for (let i = 0; i < paramArr.length; i += commandLength) {
const newCommand = new Array(commandLength) as TComplexParsedCommand;
const transformedCommand = repeatedCommands[commandLetter];
newCommand[0] =
i > 0 && transformedCommand ? transformedCommand : commandLetter;
for (let j = 0; j < commandLength; j++) {
newCommand[j + 1] = parseFloat(paramArr[i + j]);
}
chain.push(newCommand);
}
}
return chain;
};
/**
*
* Converts points to a smooth SVG path
* @param {XY[]} points Array of points
* @param {number} [correction] Apply a correction to the path (usually we use `width / 1000`). If value is undefined 0 is used as the correction value.
* @return {(string|number)[][]} An array of SVG path commands
*/
export const getSmoothPathFromPoints = (
points: Point[],
correction = 0,
): TSimplePathData => {
let p1 = new Point(points[0]),
p2 = new Point(points[1]),
multSignX = 1,
multSignY = 0;
const path: TSimplePathData = [],
len = points.length,
manyPoints = len > 2;
if (manyPoints) {
multSignX = points[2].x < p2.x ? -1 : points[2].x === p2.x ? 0 : 1;
multSignY = points[2].y < p2.y ? -1 : points[2].y === p2.y ? 0 : 1;
}
path.push([
'M',
p1.x - multSignX * correction,
p1.y - multSignY * correction,
]);
let i;
for (i = 1; i < len; i++) {
if (!p1.eq(p2)) {
const midPoint = p1.midPointFrom(p2);
// p1 is our bezier control point
// midpoint is our endpoint
// start point is p(i-1) value.
path.push(['Q', p1.x, p1.y, midPoint.x, midPoint.y]);
}
p1 = points[i];
if (i + 1 < points.length) {
p2 = points[i + 1];
}
}
if (manyPoints) {
multSignX = p1.x > points[i - 2].x ? 1 : p1.x === points[i - 2].x ? 0 : -1;
multSignY = p1.y > points[i - 2].y ? 1 : p1.y === points[i - 2].y ? 0 : -1;
}
path.push([
'L',
p1.x + multSignX * correction,
p1.y + multSignY * correction,
]);
return path;
};
/**
* Transform a path by transforming each segment.
* it has to be a simplified path or it won't work.
* WARNING: this depends from pathOffset for correct operation
* @param {TSimplePathData} path fabricJS parsed and simplified path commands
* @param {TMat2D} transform matrix that represent the transformation
* @param {Point} [pathOffset] `Path.pathOffset`
* @returns {TSimplePathData} the transformed path
*/
export const transformPath = (
path: TSimplePathData,
transform: TMat2D,
pathOffset: Point,
): TSimplePathData => {
if (pathOffset) {
transform = multiplyTransformMatrices(transform, [
1,
0,
0,
1,
-pathOffset.x,
-pathOffset.y,
]);
}
return path.map((pathSegment) => {
const newSegment: TSimpleParsedCommand = [...pathSegment];
for (let i = 1; i < pathSegment.length - 1; i += 2) {
// TODO: is there a way to get around casting to any?
const { x, y } = transformPoint(
{
x: pathSegment[i] as number,
y: pathSegment[i + 1] as number,
},
transform,
);
newSegment[i] = x;
newSegment[i + 1] = y;
}
return newSegment;
});
};
/**
* Returns an array of path commands to create a regular polygon
* @param {number} numVertexes
* @param {number} radius
* @returns {TSimplePathData} An array of SVG path commands
*/
export const getRegularPolygonPath = (
numVertexes: number,
radius: number,
): TSimplePathData => {
const interiorAngle = (Math.PI * 2) / numVertexes;
// rotationAdjustment rotates the path by 1/2 the interior angle so that the polygon always has a flat side on the bottom
// This isn't strictly necessary, but it's how we tend to think of and expect polygons to be drawn
let rotationAdjustment = -halfPI;
if (numVertexes % 2 === 0) {
rotationAdjustment += interiorAngle / 2;
}
const d = new Array(numVertexes + 1);
for (let i = 0; i < numVertexes; i++) {
const rad = i * interiorAngle + rotationAdjustment;
const { x, y } = new Point(cos(rad), sin(rad)).scalarMultiply(radius);
d[i] = [i === 0 ? 'M' : 'L', x, y];
}
d[numVertexes] = ['Z'];
return d;
};
/**
* Join path commands to go back to svg format
* @param {TSimplePathData} pathData fabricJS parsed path commands
* @param {number} fractionDigits number of fraction digits to "leave"
* @return {String} joined path 'M 0 0 L 20 30'
*/
export const joinPath = (pathData: TSimplePathData, fractionDigits?: number) =>
pathData
.map((segment) => {
return segment
.map((arg, i) => {
if (i === 0) return arg;
return fractionDigits === undefined
? arg
: toFixed(arg, fractionDigits);
})
.join(' ');
})
.join(' ');