plotboilerplate
Version:
A simple javascript plotting boilerplate for 2d stuff.
632 lines (597 loc) • 21.6 kB
text/typescript
/**
* Parse SVG path data strings and convert to PlotBoilerplate path objects.
*
* As this parser function does not detect all possible path data strings it should cover the most daily use-cases.
*
* For more insight see
* https://www.w3.org/TR/SVG/paths.html
*
* @author Ikaros Kappler
* @date 2022-11-06
* @modified 2022-12-21 (winter solstice) Porting this to Typescript.
* @modified 2023-01-17 Handling multiple parameter sets now.
* @version 0.0.2-alpha
**/
import { CubicBezierCurve } from "../../../CubicBezierCurve";
import { XYCoords } from "../../../interfaces/core";
import { Line } from "../../../Line";
import { VEllipseSector } from "../../../VEllipseSector";
import { Vertex } from "../../../Vertex";
import { splitSVGPathData } from "./splitSVGPathData";
import {
SVGPathArcToCommand,
SVGPathCloseCommand,
SVGPathCommand,
SVGPathCubicCurveToCommand,
SVGPathHorizontalLineToCommand,
SVGPathLineToCommand,
SVGPathMoveToCommand,
SVGPathQuadraticCurveToCommand,
SVGPathShorthandCubicCurveToCommand,
SVGPathShorthandQuadraticCurveToCommand,
SVGPathVerticalLineToCommand
} from "./types";
const DEG_TO_RAD = Math.PI / 180;
/**
* Transform the given path data (translate and scale. rotating is not intended here).
*
* @date 2022-11-06
*
* @name parseSVGPathData
* @static
* @param {string} data - The data to parse.
* @return An array of straight line segments (Line) or curve segments (CubicBezierCurve) representing the path.
*/
export const parseSVGPathData = (dataString: string): Array<Line | CubicBezierCurve> | null => {
const dataElements: Array<SVGPathCommand> | null = splitSVGPathData(dataString);
if (!dataElements) {
return null;
}
// Array<PathSegments>
const result: Array<Line | CubicBezierCurve> = [];
var i = 0;
const firstPoint: XYCoords = { x: NaN, y: NaN };
const lastPoint: XYCoords = { x: NaN, y: NaN };
const lastControlPoint: XYCoords = { x: NaN, y: NaN };
const lastQuadraticControlPoint: XYCoords = { x: NaN, y: NaN };
while (i < dataElements.length) {
// Could this also be SVGPathShorthandQuadraticCurveToCommand?
const data: SVGPathCommand = dataElements[i];
const cmd = data[0];
// console.log("cmd", cmd, "data", data);
switch (data[0]) {
case "M":
// MoveTo: M|m x y
_handleMove(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "m":
// MoveTo: M|m x y
_handleMove(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "L":
// LineTo L|l x y
_handleLineTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "l":
// LineTo L|l x y
_handleLineTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "H":
// HorizontalLineTo: H|h x
_handleHorizontalLineTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "h":
// HorizontalLineTo: H|h x
_handleHorizontalLineTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "V":
// VerticalLineTo: V|v y
_handleVerticalLineTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "v":
// VerticalLineTo: V|v y
_handleVerticalLineTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "C":
// CurveTo: C|c x1 y1 x2 y2 x y
_handleCubicBezierTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "c":
// CurveTo: C|c x1 y1 x2 y2 x y
_handleCubicBezierTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "S":
_handleShorthandCubicCurveTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "s":
_handleShorthandCubicCurveTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "Q":
// QuadraticCurveTo: Q|q x1 y1 x y
_handleQuadraticCurveTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "q":
// QuadraticCurveTo: Q|q x1 y1 x y
_handleQuadraticCurveTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "A":
// EllipticalArcTo: A|a rx ry x-axis-rotation large-arc-flag sweep-flag x y
_handleArcTo(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "a":
// EllipticalArcTo: A|a rx ry x-axis-rotation large-arc-flag sweep-flag x y
_handleArcTo(data, true, firstPoint, lastPoint, lastControlPoint, result);
break;
case "z":
_handleClosePath(data, false, firstPoint, lastPoint, lastControlPoint, result);
break;
case "Z":
// ClosePath: Z|z (no arguments)
_handleClosePath(data, true, firstPoint, lastPoint, lastControlPoint, result);
// i++;
break;
default:
if ((cmd as string) === "T") {
throw "T command only allowed after q command.";
}
if ((cmd as string) === "t") {
throw "t command only allowed after q command.";
}
throw "Unknown SVG path command found: " + cmd + ".";
}
// Safepoint: continue reading token by token until something is recognized again
i++;
} // END while
return result;
}; // END parseSVGPathData
// Just update the current position
const _handleMove = (
data: SVGPathMoveToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
_result: Array<Line | CubicBezierCurve>
) => {
if (data.length < 3) {
throw "Unsufficient params for MOVE";
}
// result.push( new)
if (isRelative && !isNaN(lastPoint.x) && !isNaN(lastPoint.y)) {
lastPoint.x += Number(data[1]);
lastPoint.y += Number(data[2]);
lastControlPoint.x = lastPoint.x;
lastControlPoint.y = lastPoint.y;
} else {
lastPoint.x = Number(data[1]);
lastPoint.y = Number(data[2]);
lastControlPoint.x = lastPoint.x;
lastControlPoint.y = lastPoint.y;
}
// console.log("AFTER MOVE", lastPoint);
if (isNaN(firstPoint.y)) {
firstPoint.x = lastPoint.x;
firstPoint.y = lastPoint.y;
}
};
// Draw a line segment from the current position
const _handleLineTo = (
data: SVGPathLineToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
if (data.length < 3) {
throw "Unsufficient params for LINETO";
}
for (var i = 1; i + 1 < data.length; i += 2) {
const line = new Line(new Vertex(lastPoint), new Vertex(lastPoint));
if (isRelative) {
line.b.x += Number(data[i]);
line.b.y += Number(data[i + 1]);
} else {
line.b.x = Number(data[i]);
line.b.y = Number(data[i + 1]);
}
result.push(line);
lastPoint.x = line.b.x;
lastPoint.y = line.b.y;
lastControlPoint.x = line.b.x;
lastControlPoint.y = line.b.y;
if (isNaN(firstPoint.y)) {
firstPoint.x = line.a.x;
firstPoint.y = line.a.y;
}
console.log("LINETO LINE", i, line.toString());
}
};
const _handleHorizontalLineTo = (
data: SVGPathHorizontalLineToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
// console.log("Handle HORIZONTALLINETO", data);
if (data.length < 2) {
throw "Unsufficient params for HORIZONTALLINETO";
}
for (var i = 1; i < data.length; i++) {
const line = new Line(new Vertex(lastPoint), new Vertex(lastPoint));
if (isRelative) {
line.b.x += Number(data[i]);
// line.b.y += Number(data[2]);
} else {
line.b.x = Number(data[i]);
// line.b.y = Number(data[2]);
}
result.push(line);
lastPoint.x = line.b.x;
// lastPoint.y = line.b.y;
lastControlPoint.x = line.b.x;
if (isNaN(firstPoint.y)) {
firstPoint.x = line.a.x;
firstPoint.y = line.a.y;
}
}
};
const _handleVerticalLineTo = (
data: SVGPathVerticalLineToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
if (data.length < 2) {
throw "Unsufficient params for VERTICALLINETO";
}
for (var i = 1; i < data.length; i++) {
const line = new Line(new Vertex(lastPoint), new Vertex(lastPoint));
if (isRelative) {
line.b.y += Number(data[i]);
} else {
line.b.y = Number(data[i]);
}
result.push(line);
// lastPoint.x = line.b.x;
lastPoint.y = line.b.y;
lastControlPoint.y = line.b.y;
if (isNaN(firstPoint.y)) {
firstPoint.x = line.a.x;
firstPoint.y = line.a.y;
}
}
};
// CurveTo: C|c x1 y1 x2 y2 x y
const _handleCubicBezierTo = (
data: SVGPathCubicCurveToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
// console.log("Handle CUBICBEZIERTO", data);
if (data.length < 7) {
throw "Unsufficient params for CUBICBEZIERTO";
}
for (var i = 1; i + 5 < data.length; i += 6) {
const curve = new CubicBezierCurve(
new Vertex(lastPoint),
new Vertex(lastPoint),
new Vertex(lastPoint),
new Vertex(lastPoint)
);
if (isRelative) {
curve.startControlPoint.x += Number(data[i]);
curve.startControlPoint.y += Number(data[i + 1]);
curve.endControlPoint.x += Number(data[i + 2]);
curve.endControlPoint.y += Number(data[i + 3]);
curve.endPoint.x += Number(data[i + 4]);
curve.endPoint.y += Number(data[i + 5]);
} else {
curve.startControlPoint.x = Number(data[i]);
curve.startControlPoint.y = Number(data[i + 1]);
curve.endControlPoint.x = Number(data[i + 2]);
curve.endControlPoint.y = Number(data[i + 3]);
curve.endPoint.x = Number(data[i + 4]);
curve.endPoint.y = Number(data[i + 5]);
}
result.push(curve);
lastPoint.x = curve.endPoint.x;
lastPoint.y = curve.endPoint.y;
lastControlPoint.x = curve.endControlPoint.x;
lastControlPoint.y = curve.endControlPoint.y;
if (isNaN(firstPoint.x)) {
firstPoint.x = curve.startPoint.x;
firstPoint.y = curve.startPoint.y;
}
}
};
// QuadraticCurveTo: Q|q x1 y1 x y
const _handleQuadraticCurveTo = (
data: SVGPathQuadraticCurveToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
// console.log("Handle QUADRATICBEZIERTO", data);
if (data.length < 5) {
throw "Unsufficient params for QUADRATICBEZIERTO";
}
var i = 1;
var localLastQuadraticControlPoint = { x: 0, y: 0 };
// This loops runs at least once
while (i + 3 < data.length && data[i] !== "t" && data[i] !== "T") {
const curve = new CubicBezierCurve(
new Vertex(lastPoint),
new Vertex(lastPoint),
new Vertex(lastPoint),
new Vertex(lastPoint)
);
if (isRelative) {
curve.startControlPoint.x += curve.startPoint.x + (Number(data[i]) - curve.startControlPoint.x); // * 0.666;
curve.startControlPoint.y += curve.startPoint.y + (Number(data[i + 1]) - curve.startControlPoint.y); // * 0.666;
curve.endPoint.x += Number(data[i + 2]);
curve.endPoint.y += Number(data[i + 3]);
curve.endControlPoint.x = curve.startControlPoint.x;
curve.endControlPoint.y = curve.startControlPoint.y;
} else {
curve.startControlPoint.x = curve.startPoint.x + (Number(data[i]) - curve.startControlPoint.x); // * 0.666;
curve.startControlPoint.y = curve.startPoint.y + (Number(data[i + 1]) - curve.startControlPoint.y); // * 0.666;
curve.endPoint.x = Number(data[i + 2]);
curve.endPoint.y = Number(data[i + 3]);
curve.endControlPoint.x = curve.startControlPoint.x;
curve.endControlPoint.y = curve.startControlPoint.y;
}
// var lastQuadraticControlPoint = { x: Number(data[1]), y: Number(data[2]) };
localLastQuadraticControlPoint = { x: curve.endControlPoint.x, y: curve.endControlPoint.y };
// Convert quadratic curve to cubic curve
curve.startControlPoint.x = curve.startPoint.x + (curve.startControlPoint.x - curve.startPoint.x) * 0.666;
curve.startControlPoint.y = curve.startPoint.y + (curve.startControlPoint.y - curve.startPoint.y) * 0.666;
curve.endControlPoint.x = curve.endPoint.x + (curve.endControlPoint.x - curve.endPoint.x) * 0.666;
curve.endControlPoint.y = curve.endPoint.y + (curve.endControlPoint.y - curve.endPoint.y) * 0.666;
result.push(curve);
lastPoint.x = curve.endPoint.x;
lastPoint.y = curve.endPoint.y;
lastControlPoint.x = curve.endControlPoint.x;
lastControlPoint.y = curve.endControlPoint.y;
if (isNaN(firstPoint.x)) {
firstPoint.x = curve.startPoint.x;
firstPoint.y = curve.startPoint.y;
}
i += 4;
} // END while
// 'T' or 't' command may follow
// if (data.length >= 8) {
if (i < data.length && (data[i] == "t" || data[i] === "T")) {
// TODO: think about this type cast!
const subData: SVGPathShorthandQuadraticCurveToCommand = data.slice(5) as SVGPathShorthandQuadraticCurveToCommand;
// var lastQuadraticControlPoint = { x: Number(data[1]), y: Number(data[2]) };
if (subData[0] === "T") {
// _handleShorthandQuadraticCurveTo(subData, false, firstPoint, lastPoint, lastControlPoint, lastQuadraticControlPoint, result);
_handleShorthandQuadraticCurveTo(
subData,
false,
firstPoint,
lastPoint,
lastControlPoint,
localLastQuadraticControlPoint,
result
);
} else if (subData[0] === "t") {
// _handleShorthandQuadraticCurveTo(subData, true, firstPoint, lastPoint, lastControlPoint, lastQuadraticControlPoint, result);
_handleShorthandQuadraticCurveTo(
subData,
true,
firstPoint,
lastPoint,
lastControlPoint,
localLastQuadraticControlPoint,
result
);
}
}
};
// This is a helper function and works only in combination with Quadratic Bézier Curves
// T|t (x y)+
const _handleShorthandQuadraticCurveTo = (
data: SVGPathShorthandQuadraticCurveToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
lastQuadraticControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
if (data.length < 3) {
throw "Unsufficient params for SHORTHANDQUADRATICCURVETO";
}
var i = 1;
while (i + 1 < data.length) {
// Respect multiple 'T|t' commands here
if (data[i] === "T" || data[i] === "t") {
i++;
continue;
}
const curve = new CubicBezierCurve(
new Vertex(lastPoint),
new Vertex(lastPoint),
new Vertex(lastPoint),
new Vertex(lastPoint)
);
if (isRelative) {
curve.endPoint.x += Number(data[i]);
curve.endPoint.y += Number(data[i + 1]);
curve.startControlPoint.x += curve.startPoint.x - (lastQuadraticControlPoint.x - lastPoint.x);
curve.startControlPoint.y += curve.startPoint.y - (lastQuadraticControlPoint.y - lastPoint.y);
} else {
curve.endPoint.x = Number(data[i]);
curve.endPoint.y = Number(data[i + 1]);
curve.startControlPoint.x = curve.startPoint.x - (lastQuadraticControlPoint.x - lastPoint.x);
curve.startControlPoint.y = curve.startPoint.y - (lastQuadraticControlPoint.y - lastPoint.y);
}
// First handle as symmetrical cubic curve
curve.endControlPoint.x = curve.startControlPoint.x;
curve.endControlPoint.y = curve.startControlPoint.y;
lastQuadraticControlPoint.y = curve.endControlPoint.y;
lastQuadraticControlPoint.x = curve.endControlPoint.x;
// Convert quadratic curve to cubic curve
var scaleFactor = 0.666; // i === 1 ? 0.666 : 1.0;
curve.startControlPoint.x = curve.startPoint.x + (curve.startControlPoint.x - curve.startPoint.x) * scaleFactor;
curve.startControlPoint.y = curve.startPoint.y + (curve.startControlPoint.y - curve.startPoint.y) * scaleFactor;
curve.endControlPoint.x = curve.endPoint.x + (curve.endControlPoint.x - curve.endPoint.x) * scaleFactor;
curve.endControlPoint.y = curve.endPoint.y + (curve.endControlPoint.y - curve.endPoint.y) * scaleFactor;
// console.log("ADDING T CURVE", curve);
result.push(curve);
lastPoint.x = curve.endPoint.x;
lastPoint.y = curve.endPoint.y;
lastControlPoint.x = curve.endControlPoint.x;
lastControlPoint.y = curve.endControlPoint.y;
if (isNaN(firstPoint.x)) {
firstPoint.x = curve.startPoint.x;
firstPoint.y = curve.startPoint.y;
}
i += 2;
}
};
// The S|s x2 y2 x y
const _handleShorthandCubicCurveTo = (
data: SVGPathShorthandCubicCurveToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
// console.log("Handle SHORTHANDCUBICBEZIERTO", data);
if (data.length < 5) {
throw "Unsufficient params for SHORTHANDCUBICBEZIERTO";
}
const curve = new CubicBezierCurve(new Vertex(lastPoint), new Vertex(lastPoint), new Vertex(lastPoint), new Vertex(lastPoint));
if (isRelative) {
curve.startControlPoint.x = lastPoint.x - (lastControlPoint.x - lastPoint.x);
curve.startControlPoint.y = lastPoint.y - (lastControlPoint.y - lastPoint.y);
curve.endControlPoint.x += Number(data[1]);
curve.endControlPoint.y += Number(data[2]);
curve.endPoint.x += Number(data[3]);
curve.endPoint.y += Number(data[4]);
} else {
curve.startControlPoint.x = lastPoint.x - (lastControlPoint.x - lastPoint.x);
curve.startControlPoint.y = lastPoint.y - (lastControlPoint.y - lastPoint.y);
curve.endControlPoint.x = Number(data[1]);
curve.endControlPoint.y = Number(data[2]);
curve.endPoint.x = Number(data[3]);
curve.endPoint.y = Number(data[4]);
}
result.push(curve);
lastPoint.x = curve.endPoint.x;
lastPoint.y = curve.endPoint.y;
lastControlPoint.x = curve.endControlPoint.x;
lastControlPoint.y = curve.endControlPoint.y;
if (isNaN(firstPoint.x)) {
firstPoint.x = curve.startPoint.x;
firstPoint.y = curve.startPoint.y;
}
};
// EllipticalArcTo: A|a rx ry x-axis-rotation large-arc-flag sweep-flag x y
const _handleArcTo = (
data: SVGPathArcToCommand,
isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
if (data.length < 8) {
throw "Unsufficient params for ARCTO";
}
for (var i = 1; i + 6 < data.length; i += 7) {
const arcEndPoint = { x: Number(data[i + 5]), y: Number(data[i + 6]) };
if (isRelative) {
arcEndPoint.x += lastPoint.x;
arcEndPoint.y += lastPoint.y;
}
// console.log(
// "ARC",
// "data.length",
// data.length,
// "i",
// i,
// "lastPoint.x",
// lastPoint.x, // x1
// "lastPoint.y",
// lastPoint.y, // y1
// "data[i]",
// Number(data[i]), // rx
// "data[i+1]",
// Number(data[i + 1]), // ry
// "data[i+2]",
// Number(data[i + 2]), // phi: number,
// "data[i+3]",
// Boolean(data[i + 3]), // fa: boolean,
// "data[i+4]",
// Boolean(data[i + 4]), // fs: boolean,
// "data[i+5]",
// arcEndPoint.x, // Number(data[6]), // x2: number,
// "data[i+6]",
// arcEndPoint.y // Number(data[7]) // y2: number
// );
// A 5 4 0 1 1 -10 -5
// TODO: respect relative/absolute here
const ellipseSector = VEllipseSector.ellipseSectorUtils.endpointToCenterParameters(
lastPoint.x,
lastPoint.y,
Number(data[i]),
Number(data[i + 1]),
Number(data[i + 2]) * DEG_TO_RAD,
Boolean(data[i + 3]),
Boolean(data[i + 4]),
arcEndPoint.x,
arcEndPoint.y // Number(data[7]) // y2: number
);
// console.log("ellipseSector", ellipseSector);
const curves = ellipseSector.toCubicBezier(4); // 4 segments already seems to be a good approximation
for (var j = 0; j < curves.length; j++) {
result.push(curves[j]); // Destruct!
}
// result.push(ellipseSector.ellipse);
if (curves.length > 0) {
// console.log("curves", curves);
const lastCurve = curves[curves.length - 1];
// lastPoint.x = lastCurve.endPoint.x;
// lastPoint.y = lastCurve.endPoint.y;
lastControlPoint.x = lastCurve.endControlPoint.x;
lastControlPoint.y = lastCurve.endControlPoint.y;
if (isNaN(firstPoint.x)) {
firstPoint.x = curves[0].startPoint.x;
firstPoint.y = curves[0].startPoint.y;
}
}
lastPoint.x = arcEndPoint.x;
lastPoint.y = arcEndPoint.y;
} // END for
// TODO: track first/last/control point
};
const _handleClosePath = (
_data: SVGPathCloseCommand,
_isRelative: boolean,
firstPoint: XYCoords,
lastPoint: XYCoords,
lastControlPoint: XYCoords,
result: Array<Line | CubicBezierCurve>
) => {
// console.log("Handle CLOSEPATH", "lastPoint", lastPoint, "firstPoint", firstPoint);
const line = new Line(new Vertex(lastPoint), new Vertex(firstPoint));
result.push(line);
lastPoint.x = line.b.x;
lastPoint.y = line.b.y;
lastControlPoint.x = line.b.x;
lastControlPoint.y = line.b.y;
};