UNPKG

plotboilerplate

Version:

A simple javascript plotting boilerplate for 2d stuff.

486 lines 22 kB
"use strict"; /** * 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 **/ Object.defineProperty(exports, "__esModule", { value: true }); exports.parseSVGPathData = void 0; var CubicBezierCurve_1 = require("../../../CubicBezierCurve"); var Line_1 = require("../../../Line"); var VEllipseSector_1 = require("../../../VEllipseSector"); var Vertex_1 = require("../../../Vertex"); var splitSVGPathData_1 = require("./splitSVGPathData"); var 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. */ var parseSVGPathData = function (dataString) { var dataElements = (0, splitSVGPathData_1.splitSVGPathData)(dataString); if (!dataElements) { return null; } // Array<PathSegments> var result = []; var i = 0; var firstPoint = { x: NaN, y: NaN }; var lastPoint = { x: NaN, y: NaN }; var lastControlPoint = { x: NaN, y: NaN }; var lastQuadraticControlPoint = { x: NaN, y: NaN }; while (i < dataElements.length) { // Could this also be SVGPathShorthandQuadraticCurveToCommand? var data = dataElements[i]; var 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 === "T") { throw "T command only allowed after q command."; } if (cmd === "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 exports.parseSVGPathData = parseSVGPathData; // Just update the current position var _handleMove = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, _result) { 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 var _handleLineTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { if (data.length < 3) { throw "Unsufficient params for LINETO"; } for (var i = 1; i + 1 < data.length; i += 2) { var line = new Line_1.Line(new Vertex_1.Vertex(lastPoint), new Vertex_1.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()); } }; var _handleHorizontalLineTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { // console.log("Handle HORIZONTALLINETO", data); if (data.length < 2) { throw "Unsufficient params for HORIZONTALLINETO"; } for (var i = 1; i < data.length; i++) { var line = new Line_1.Line(new Vertex_1.Vertex(lastPoint), new Vertex_1.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; } } }; var _handleVerticalLineTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { if (data.length < 2) { throw "Unsufficient params for VERTICALLINETO"; } for (var i = 1; i < data.length; i++) { var line = new Line_1.Line(new Vertex_1.Vertex(lastPoint), new Vertex_1.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 var _handleCubicBezierTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { // console.log("Handle CUBICBEZIERTO", data); if (data.length < 7) { throw "Unsufficient params for CUBICBEZIERTO"; } for (var i = 1; i + 5 < data.length; i += 6) { var curve = new CubicBezierCurve_1.CubicBezierCurve(new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.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 var _handleQuadraticCurveTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { // 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") { var curve = new CubicBezierCurve_1.CubicBezierCurve(new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.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! var subData = data.slice(5); // 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)+ var _handleShorthandQuadraticCurveTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, lastQuadraticControlPoint, result) { 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; } var curve = new CubicBezierCurve_1.CubicBezierCurve(new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.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 var _handleShorthandCubicCurveTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { // console.log("Handle SHORTHANDCUBICBEZIERTO", data); if (data.length < 5) { throw "Unsufficient params for SHORTHANDCUBICBEZIERTO"; } var curve = new CubicBezierCurve_1.CubicBezierCurve(new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.Vertex(lastPoint), new Vertex_1.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 var _handleArcTo = function (data, isRelative, firstPoint, lastPoint, lastControlPoint, result) { if (data.length < 8) { throw "Unsufficient params for ARCTO"; } for (var i = 1; i + 6 < data.length; i += 7) { var 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 var ellipseSector = VEllipseSector_1.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); var 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); var 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 }; var _handleClosePath = function (_data, _isRelative, firstPoint, lastPoint, lastControlPoint, result) { // console.log("Handle CLOSEPATH", "lastPoint", lastPoint, "firstPoint", firstPoint); var line = new Line_1.Line(new Vertex_1.Vertex(lastPoint), new Vertex_1.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; }; //# sourceMappingURL=parseSVGPathData.js.map