UNPKG

p5.plotsvg

Version:

A Plotter-Oriented SVG Exporter for p5.js

1,371 lines (1,234 loc) 102 kB
/* // // _____ _ _ _____ // | ____| | | | | / ____| // _ __| |__ _ __ | | ___ | |_| (_____ ____ _ // | '_ \___ \ | '_ \| |/ _ \| __|\___ \ \ / / _` | // | |_) |__) || |_) | | (_) | |_ ____) \ V / (_| | // | .__/____(_) .__/|_|\___/ \__|_____/ \_/ \__, | // | | | | __/ | // |_| |_| |___/ // // */ // p5.plotSvg: a Plotter-Oriented SVG Exporter for p5.js // by Golan Levin (@golanlevin) // v.0.1.4, April 6, 2025 // For p5.js versions 1.4.2–1.11.3 (function(global) { // Create a namespace for the library const p5plotSvg = {}; // Attach constants to the p5plotSvg namespace p5plotSvg.SVG_INDENT_NONE = 0; p5plotSvg.SVG_INDENT_SPACES = 1; p5plotSvg.SVG_INDENT_TABS = 2; // Internal properties set using setter functions let _bFlattenTransforms = false; // false is default let _bTransformsExist = false; let _svgFilename = "output.svg"; let _svgCurveTightness = 0.0; let _svgCoordPrecision = 4; let _svgTransformPrecision = 6; let _svgIndentType = p5plotSvg.SVG_INDENT_SPACES; let _svgIndentAmount = 2; let _svgPointRadius = 0.25; // Default radius for point representation let _svgDPI = 96; // Default DPI value. Set from DPCM if needed. let _svgWidth = 816; // Default width for SVG output (8.5" at 96 DPI) let _svgHeight = 1056; // Default height for SVG output (11" at 96 DPI) let _svgDefaultStrokeColor = 'black'; let _svgCurrentStrokeColor = _svgDefaultStrokeColor; let _svgBackgroundColor = null; let _svgDefaultStrokeWeight = 1; // Internal variables, not to be accessed directly let _p5Instance; let _p5PixelDensity = 1; let _commands = []; let _svgGroupLevel = 0; let _vertexStack = []; // Temp stack for polyline/polygon vertices let _shapeMode = "simple"; // Track mode: "simple" or "complex" let _shapeKind = "poly"; let _bRecordingSvg = false; let _bRecordingSvgBegun = false; let _bCustomSizeSet = false; let _pointsSetCount = 0; let _linesSetCount = 0; let _trianglesSetCount = 0; let _triangleFanSetCount = 0; let _triangleStripSetCount = 0; let _quadsSetCount = 0; let _quadStripSetCount = 0; let _originalArcFunc; let _originalBezierFunc; let _originalCircleFunc; let _originalCurveFunc; let _originalEllipseFunc; let _originalLineFunc; let _originalPointFunc; let _originalQuadFunc; let _originalRectFunc; let _originalSquareFunc; let _originalTriangleFunc; let _originalBezierDetailFunc; let _originalCurveTightnessFunc; let _originalBeginShapeFunc; let _originalVertexFunc; let _originalBezierVertexFunc; let _originalQuadraticVertexFunc; let _originalCurveVertexFunc; let _originalEndShapeFunc; let _originalDescribeFunc; let _originalPushFunc; let _originalPopFunc; let _originalScaleFunc; let _originalTranslateFunc; let _originalRotateFunc; let _originalShearXFunc; let _originalShearYFunc; let _originalTextFunc; let _originalStrokeFunc; let _originalColorModeFunc; /** * Begins recording SVG output for a p5.js sketch. * Initializes recording state, validates and sets the output filename, * and overrides p5.js drawing functions to capture drawing commands for SVG export. * @param {object} p5Instance - A reference to the current p5.js sketch (e.g. `this`). * @param {string} [fn] - Optional filename for the output SVG file. * Defaults to "output.svg" if not provided or invalid. */ p5plotSvg.beginRecordSVG = function(p5Instance, fn) { // Validate the p5 instance if (!p5Instance) { throw new Error("Invalid p5 instance provided to beginRecordSVG()."); } // Store a reference to the p5 instance for use in other functions _p5Instance = p5Instance; _p5PixelDensity = p5Instance.pixelDensity(); // Check if filename is provided and valid if (typeof fn === 'string' && fn.length > 0) { // Ensure ".svg" is present before stripping invalid characters if (!fn.endsWith(".svg")) { fn += ".svg"; } // Strip out illegal characters // (allow only letters, numbers, dashes, dots, and underscores) // Ensure the filename has at least one character after stripping fn = fn.replace(/[^a-zA-Z0-9-_\.]/g, ''); _svgFilename = (fn.length > 0) ? fn : "output.svg"; } else { // Use default filename if `fn` is invalid or not provided _svgFilename = "output.svg"; } // Initialize SVG settings and override functions _bRecordingSvg = true; _bRecordingSvgBegun = true; _bTransformsExist = false; _commands = []; _svgGroupLevel = 0; _pointsSetCount = 0; _linesSetCount = 0; _trianglesSetCount = 0; _triangleFanSetCount = 0; _triangleStripSetCount = 0; _quadsSetCount = 0; _quadStripSetCount = 0; _svgCurrentStrokeColor = _svgDefaultStrokeColor; overrideP5Functions(); } /** * Pauses or unpauses recording of SVG output for a p5.js sketch, * depending on whether the bPause argument is true or false. */ p5plotSvg.pauseRecordSVG = function(bPause) { if (!_bRecordingSvgBegun){ console.warn("You must beginRecordSVG() before you can pauseRecordSVG()."); return; } else { if (bPause === true){ _bRecordingSvg = false; } else if (bPause === false){ _bRecordingSvg = true; } } } /** * Ends recording of SVG output for a p5.js sketch. * Calls the export function to generate the SVG output * and restores the original p5.js functions. */ p5plotSvg.endRecordSVG = function() { exportSVG(); restoreP5Functions(); _bRecordingSvg = false; _bRecordingSvgBegun = false; } /** * @private * Overrides p5.js drawing functions to capture commands for SVG export. * Includes support for shapes, vertices, transformations, and text functions. */ function overrideP5Functions() { overrideArcFunction(); overrideBezierFunction(); overrideCircleFunction(); overrideCurveFunction(); overrideEllipseFunction(); overrideLineFunction(); overridePointFunction(); overrideQuadFunction(); overrideRectFunction(); overrideSquareFunction(); overrideTriangleFunction(); overrideBezierDetailFunction(); overrideCurveTightnessFunction(); overrideBeginShapeFunction(); overrideVertexFunction(); overrideBezierVertexFunction(); overrideQuadraticVertexFunction(); overrideCurveVertexFunction(); overrideEndShapeFunction(); overrideDescribeFunction(); overridePushFunction(); overridePopFunction(); overrideScaleFunction(); overrideTranslateFunction(); overrideRotateFunction(); overrideShearXFunction(); overrideShearYFunction(); overrideTextFunction(); overrideStrokeFunction(); overrideColorModeFunction(); } /** * @private * Restores the original p5.js drawing functions that were overridden for SVG export. * Reverts all overrides, returning p5.js functions to their standard behavior. */ function restoreP5Functions(){ _p5Instance.arc = _originalArcFunc; _p5Instance.bezier = _originalBezierFunc; _p5Instance.circle = _originalCircleFunc; _p5Instance.curve = _originalCurveFunc; _p5Instance.ellipse = _originalEllipseFunc; _p5Instance.line = _originalLineFunc; _p5Instance.point = _originalPointFunc; _p5Instance.quad = _originalQuadFunc; _p5Instance.rect = _originalRectFunc; _p5Instance.square = _originalSquareFunc; _p5Instance.triangle = _originalTriangleFunc; _p5Instance.bezierDetail = _originalBezierDetailFunc; _p5Instance.curveTightness = _originalCurveTightnessFunc; _p5Instance.beginShape = _originalBeginShapeFunc; _p5Instance.vertex = _originalVertexFunc; _p5Instance.bezierVertex = _originalBezierVertexFunc; _p5Instance.quadraticVertex = _originalQuadraticVertexFunc; _p5Instance.curveVertex = _originalCurveVertexFunc; _p5Instance.endShape = _originalEndShapeFunc; _p5Instance.describe = _originalDescribeFunc; _p5Instance.push = _originalPushFunc; _p5Instance.pop = _originalPopFunc; _p5Instance.scale = _originalScaleFunc; _p5Instance.translate = _originalTranslateFunc; _p5Instance.rotate = _originalRotateFunc; _p5Instance.shearX = _originalShearXFunc; _p5Instance.shearY = _originalShearYFunc; _p5Instance.text = _originalTextFunc; _p5Instance.stroke = _originalStrokeFunc; _p5Instance.colorMode = _originalColorModeFunc; } /** * @private * Overrides the p5.js arc function to capture SVG arc commands for export. * Supports different arc modes. Warns about optional detail parameter in WEBGL context. * Stores arc parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/arc/} */ function overrideArcFunction() { _originalArcFunc = _p5Instance.arc; _p5Instance.arc = function(x, y, w, h, start, stop, mode = OPEN, detail = 0) { if (_bRecordingSvg) { if (detail !== undefined && p5.instance._renderer.drawingContext instanceof WebGLRenderingContext) { console.warn("arc() detail is currently unsupported in SVG output."); } let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'arc', x, y, w, h, start, stop, mode, transformMatrix }); } _originalArcFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js bezier function to capture SVG bezier curve commands for export. * Stores bezier curve control points in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/bezier/} */ function overrideBezierFunction(){ _originalBezierFunc = _p5Instance.bezier; _p5Instance.bezier = function(x1, y1, x2, y2, x3, y3, x4, y4) { if (_bRecordingSvg) { let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'bezier', x1, y1, x2, y2, x3, y3, x4, y4, transformMatrix }); } _originalBezierFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js circle function to capture SVG circle commands for export. * Handles different ellipse modes (center, corner, radius, corners) * to convert circle parameters appropriately. * Stores circle or ellipse parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/circle/} */ function overrideCircleFunction(){ _originalCircleFunc = _p5Instance.circle; _p5Instance.circle = function(x, y, d) { if (_bRecordingSvg) { let transformMatrix = captureCurrentTransformMatrix(); if (_p5Instance._renderer._ellipseMode === 'center'){ _commands.push({ type: 'circle', x, y, d, transformMatrix }); } else if (_p5Instance._renderer._ellipseMode === 'corner'){ x += d/2; y += d/2; _commands.push({ type: 'circle', x, y, d, transformMatrix }); } else if (_p5Instance._renderer._ellipseMode === 'radius'){ d *= 2; _commands.push({ type: 'circle', x, y, d, transformMatrix }); } else if (_p5Instance._renderer._ellipseMode === 'corners'){ let w = d - x; let h = d - y; x += w/2; y += h/2; _commands.push({ type: 'ellipse', x, y, w, h, transformMatrix }); } } _originalCircleFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js curve function to capture SVG curve commands for export. * Adjusts control points based on the current curve tightness setting before storing * curve parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/#/p5/curve} */ function overrideCurveFunction() { _originalCurveFunc = _p5Instance.curve; _p5Instance.curve = function(x1, y1, x2, y2, x3, y3, x4, y4) { if (_bRecordingSvg) { // Adjust control points based on the current tightness setting const [adjX1, adjY1, adjX2, adjY2, adjX3, adjY3, adjX4, adjY4] = adjustControlPointsForTightness(x1, y1, x2, y2, x3, y3, x4, y4, _svgCurveTightness); x1 = adjX1; y1 = adjY1; x2 = adjX2; y2 = adjY2; x3 = adjX3; y3 = adjY3; x4 = adjX4; y4 = adjY4; let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'curve', x1, y1, x2, y2, x3, y3, x4, y4, transformMatrix }); } _originalCurveFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js ellipse function to capture SVG ellipse commands for export. * Handles different ellipse modes (center, corner, radius, corners) and warns * when detail is used in WEBGL context as it is unsupported for SVG output. * Stores ellipse parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/ellipse/} */ function overrideEllipseFunction(){ _originalEllipseFunc = _p5Instance.ellipse; _p5Instance.ellipse = function(x, y, w, h, detail = 0) { if (_bRecordingSvg) { if (detail !== undefined && _p5Instance._renderer.drawingContext instanceof WebGLRenderingContext) { console.warn("ellipse() detail is currently unsupported in SVG output."); } // We can't use _p5Instance.ellipseMode() for reasons :( if (_p5Instance._renderer._ellipseMode === 'center'){ ; } else if (_p5Instance._renderer._ellipseMode === 'corner'){ x += w/2; y += h/2; } else if (_p5Instance._renderer._ellipseMode === 'radius'){ w *= 2; h *= 2; } else if (_p5Instance._renderer._ellipseMode === 'corners'){ let px = Math.min(x, w); let qx = Math.max(x, w); let py = Math.min(y, h); let qy = Math.max(y, h); x = px; y = py; w = qx - px; h = qy - py; x += w/2; y += h/2; } let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'ellipse', x, y, w, h, transformMatrix }); } _originalEllipseFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js line function to capture SVG line commands for export. * Stores line parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/line/} */ function overrideLineFunction() { _originalLineFunc = _p5Instance.line; _p5Instance.line = function(x1, y1, x2, y2) { if (_bRecordingSvg) { let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'line', x1, y1, x2, y2, transformMatrix }); } _originalLineFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js point function to capture SVG point commands for export. * Stores point parameters as small circles in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/point/} */ function overridePointFunction() { _originalPointFunc = _p5Instance.point; _p5Instance.point = function(x, y) { if (_bRecordingSvg) { let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'point', x, y, radius: _svgPointRadius, transformMatrix }); } _originalPointFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js quad function to capture SVG quad commands for export. * Stores quad parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/quad/} */ function overrideQuadFunction(){ _originalQuadFunc = _p5Instance.quad; _p5Instance.quad = function(x1, y1, x2, y2, x3, y3, x4, y4) { if (_bRecordingSvg) { let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'quad', x1, y1, x2, y2, x3, y3, x4, y4, transformMatrix }); } _originalQuadFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js rect function to capture SVG rect commands for export. * Handles different rect modes (corner, center, radius, corners) and supports * rectangles with optional uniform or individual corner radii. * Stores rect parameters in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/rect/} */ function overrideRectFunction() { _originalRectFunc = _p5Instance.rect; _p5Instance.rect = function(x, y, w, h, tl, tr, br, bl) { if (_bRecordingSvg) { if (arguments.length === 3) { h = w; } // Handle different rect modes if (_p5Instance._renderer._rectMode === 'corner') { // No adjustment needed for 'corner' } else if (_p5Instance._renderer._rectMode === 'center') { x = x - w / 2; y = y - h / 2; } else if (_p5Instance._renderer._rectMode === 'radius') { x = x - w; y = y - h; w = 2 * w; h = 2 * h; } else if (_p5Instance._renderer._rectMode === 'corners') { let px = Math.min(x, w); let qx = Math.max(x, w); let py = Math.min(y, h); let qy = Math.max(y, h); x = px; y = py; w = qx - px; h = qy - py; } let transformMatrix = captureCurrentTransformMatrix(); // Check for corner radii if (arguments.length === 5) { // Single corner radius _commands.push({ type: 'rect', x, y, w, h, tl, transformMatrix }); } else if (arguments.length === 8) { // Individual corner radii _commands.push({ type: 'rect', x, y, w, h, tl,tr,br,bl, transformMatrix }); } else { // Standard rectangle _commands.push({ type: 'rect', x, y, w, h, transformMatrix }); } } _originalRectFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js square function to capture SVG square commands for export. * Handles different rect modes (corner, center, radius, corners) and supports * squares with optional uniform or individual corner radii. * Converts square parameters to equivalent rectangle parameters and stores them * in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/square/} */ function overrideSquareFunction(){ _originalSquareFunc = _p5Instance.square; _p5Instance.square = function(x, y, s, tl,tr,br,bl) { if (_bRecordingSvg) { let w = s; let h = s; if (_p5Instance._renderer._rectMode === 'corner'){ ; } else if (_p5Instance._renderer._rectMode === 'center'){ x = x - w/2; y = y - h/2; } else if (_p5Instance._renderer._rectMode === 'radius'){ x = x - w; y = y - h; w = 2*w; h = 2*h; } else if (_p5Instance._renderer._rectMode === 'corners'){ let px = Math.min(x, s); let qx = Math.max(x, s); let py = Math.min(y, s); let qy = Math.max(y, s); x = px; y = py; w = qx - px; h = qy - py; } let transformMatrix = captureCurrentTransformMatrix(); if (arguments.length === 3) { // standard square _commands.push({ type: 'rect', x, y, w, h, transformMatrix }); } else if (arguments.length === 4) { // rounded square _commands.push({ type: 'rect', x, y, w, h, tl, transformMatrix }); } else if (arguments.length === 7) { _commands.push({ type: 'rect', x, y, w, h, tl,tr,br,bl, transformMatrix }); } } _originalSquareFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js triangle function to capture SVG triangle commands for export. * Stores triangle vertex coordinates in the `_commands` array when recording SVG output. * @see {@link https://p5js.org/reference/p5/triangle/} */ function overrideTriangleFunction(){ _originalTriangleFunc = _p5Instance.triangle; _p5Instance.triangle = function(x1, y1, x2, y2, x3, y3) { if (_bRecordingSvg) { let transformMatrix = captureCurrentTransformMatrix(); _commands.push({ type: 'triangle', x1, y1, x2, y2, x3, y3, transformMatrix }); } _originalTriangleFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js bezierDetail function to provide a warning when used in WEBGL context. * Warns users that bezierDetail is currently unsupported in SVG output. * https://p5js.org/reference/p5/bezierDetail/ */ function overrideBezierDetailFunction() { _originalBezierDetailFunc = _p5Instance.bezierDetail; _p5Instance.bezierDetail = function(detailLevel) { // Check if the renderer is WEBGL if (p5.instance._renderer.drawingContext instanceof WebGLRenderingContext) { console.warn("bezierDetail() is currently unsupported in SVG output."); } _originalBezierDetailFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js curveTightness function to capture curve tightness settings for SVG export. * Updates the `_svgCurveTightness` variable to reflect the specified tightness value. * @see {@link https://p5js.org/reference/p5/curveTightness/} */ function overrideCurveTightnessFunction() { _originalCurveTightnessFunc = _p5Instance.curveTightness; _p5Instance.curveTightness = function(tightness) { if (_bRecordingSvg) { _svgCurveTightness = tightness; } _originalCurveTightnessFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js beginShape function to initiate shape recording for SVG export. * Initializes the vertex stack and sets the shape kind based on the provided kind parameter. * @see {@link https://p5js.org/reference/p5/beginShape/} */ function overrideBeginShapeFunction() { _originalBeginShapeFunc = _p5Instance.beginShape; _p5Instance.beginShape = function(kind) { if (_bRecordingSvg) { _vertexStack = []; // Start with an empty vertex stack _shapeMode = "simple"; // Assume simple mode initially if ((kind !== null) && (kind === 0)) { _shapeKind = 'points'; } else if (kind === null){ _shapeKind = 'poly'; // default to "poly" for polyline/polygon } else { _shapeKind = kind; } } _originalBeginShapeFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js vertex function to capture vertex coordinates for SVG export. * Pushes simple vertex data to the `_vertexStack` when recording is active. * @see {@link https://p5js.org/reference/p5/vertex/} */ function overrideVertexFunction() { _originalVertexFunc = _p5Instance.vertex; _p5Instance.vertex = function(x, y) { if (_bRecordingSvg) { _vertexStack.push({ type: 'vertex', x, y }); } _originalVertexFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js bezierVertex function to capture Bézier control points for SVG export. * Marks the current shape as complex and stores Bézier vertex data in the `_vertexStack`. * @see {@link https://p5js.org/reference/p5/bezierVertex/} */ function overrideBezierVertexFunction() { // Override `bezierVertex()` and mark shape as complex _originalBezierVertexFunc = _p5Instance.bezierVertex; _p5Instance.bezierVertex = function(x2, y2, x3, y3, x4, y4) { if (_bRecordingSvg) { _shapeMode = 'complex'; // Switch to complex mode _vertexStack.push({ type: 'bezier', x2, y2, x3, y3, x4, y4 }); } _originalBezierVertexFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js quadraticVertex function to capture quadratic Bézier control points for SVG export. * Marks the current shape as complex and stores quadratic vertex data in the `_vertexStack`. * @see {@link https://p5js.org/reference/p5/quadraticVertex/} */ function overrideQuadraticVertexFunction() { // Override `quadraticVertex()` and mark shape as complex _originalQuadraticVertexFunc = _p5Instance.quadraticVertex; _p5Instance.quadraticVertex = function(cx, cy, x, y) { if (_bRecordingSvg) { _shapeMode = 'complex'; // Switch to complex mode _vertexStack.push({ type: 'quadratic', cx, cy, x, y }); } _originalQuadraticVertexFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js curveVertex function to capture Catmull-Rom curve control points for SVG export. * Marks the current shape as complex and handles specific kludge logic for initial vertices. * @see {@link https://p5js.org/reference/p5/curveVertex/} */ function overrideCurveVertexFunction() { // Override `curveVertex()` and mark shape as complex _originalCurveVertexFunc = _p5Instance.curveVertex; _p5Instance.curveVertex = function(x, y) { if (_bRecordingSvg) { _shapeMode = 'complex'; // Switch to complex mode let bDoKludge = true; // TODO: Revisit if (bDoKludge){ if (_vertexStack.length === 1){ if(_vertexStack[0].type === 'curve'){ let x0 = _vertexStack[0].x; let y0 = _vertexStack[0].y; let dist01 = Math.hypot(x-x0, y-y0); if (dist01 > 0){ _vertexStack.shift(); _vertexStack.push({ type: 'curve', x, y }); } } } } _vertexStack.push({ type: 'curve', x, y }); } _originalCurveVertexFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js `endShape` function to capture SVG shape data for export. * This function modifies the behavior of `endShape()` to record vertex data * and transformation matrices when creating SVG output from p5.js shapes. * It handles various shape kinds such as points, lines, triangles, quads, etc., * and pushes the recorded data to an internal command stack for later SVG rendering. * @see {@link https://p5js.org/reference/p5/endShape/} */ function overrideEndShapeFunction() { _originalEndShapeFunc = _p5Instance.endShape; _p5Instance.endShape = function(mode) { if (_bRecordingSvg && _vertexStack.length > 0) { let transformMatrix = captureCurrentTransformMatrix(); // Dispatch based on `_shapeKind` switch (_shapeKind) { case 'points': _commands.push({ type: 'points', vertices: [..._vertexStack], transformMatrix }); break; case _p5Instance.LINES: _commands.push({ type: 'lines', vertices: [..._vertexStack], transformMatrix }); break; case _p5Instance.TRIANGLES: _commands.push({ type: 'triangles', vertices: [..._vertexStack], transformMatrix }); break; case _p5Instance.TRIANGLE_FAN: _commands.push({ type: 'triangle_fan', vertices: [..._vertexStack], transformMatrix }); break; case _p5Instance.TRIANGLE_STRIP: _commands.push({ type: 'triangle_strip', vertices: [..._vertexStack], transformMatrix }); break; case _p5Instance.QUADS: _commands.push({ type: 'quads', vertices: [..._vertexStack], transformMatrix }); break; case _p5Instance.QUAD_STRIP: _commands.push({ type: 'quad_strip', vertices: [..._vertexStack], transformMatrix }); break; case 'poly': default: // Handle the default polyline/polygon behavior let isClosed = (mode === _p5Instance.CLOSE); if (_shapeMode === "simple") { _commands.push({ type: 'polyline', vertices: [..._vertexStack], closed: isClosed, transformMatrix }); } else { _commands.push({ type: 'path', segments: [..._vertexStack], closed: isClosed, transformMatrix }); } break; } _vertexStack = []; // Clear stack after pushing _shapeMode = 'simple'; // Reset _shapeMode _shapeKind = 'poly'; // Reset _shapeKind } _originalEndShapeFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js describe function to produce SVG description elements. * Captures the provided description text for embedding in the SVG as a <desc> element. * @see {@link https://p5js.org/reference/p5/describe/} */ function overrideDescribeFunction() { _originalDescribeFunc = _p5Instance.describe; _p5Instance.describe = function(description) { if (_bRecordingSvg) { if (description && description.trim().length > 0){ // Push a command to the stack for generating an SVG `desc` element _commands.push({ type: 'description', text: description }); } } _originalDescribeFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js push function to capture transformations for SVG output. * Captures transformation state for recording SVG output by storing a 'push' command. * @see {@link https://p5js.org/reference/p5/push/} */ function overridePushFunction(){ _originalPushFunc = _p5Instance.push; _bTransformsExist = true; _p5Instance.push = function() { if (_bRecordingSvg) { _commands.push({ type: 'push' }); } _originalPushFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js pop function to capture transformations for SVG output. * Captures transformation state for recording SVG output by storing a 'pop' command. * @see {@link https://p5js.org/reference/p5/pop/} */ function overridePopFunction(){ _originalPopFunc = _p5Instance.pop; _bTransformsExist = true; _p5Instance.pop = function() { if (_bRecordingSvg) { _commands.push({ type: 'pop' }); } _originalPopFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js scale function to capture scaling transformations for SVG output. * Captures scaling parameters for recording SVG output by storing a 'scale' command. * @see {@link https://p5js.org/reference/p5/scale/} */ function overrideScaleFunction(){ _originalScaleFunc = _p5Instance.scale; _bTransformsExist = true; _p5Instance.scale = function(sx, sy) { if (_bRecordingSvg) { _commands.push({ type: 'scale', sx, sy: sy || sx }); } _originalScaleFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js translate function to capture translation transformations for SVG output. * Captures translation parameters for recording SVG output by storing a 'translate' command. * @see {@link https://p5js.org/reference/p5/translate/} */ function overrideTranslateFunction(){ _originalTranslateFunc = _p5Instance.translate; _bTransformsExist = true; _p5Instance.translate = function(tx, ty) { if (_bRecordingSvg) { _commands.push({ type: 'translate', tx, ty }); } _originalTranslateFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js rotate function to capture rotation transformations for SVG output. * Captures rotation angle for recording SVG output by storing a 'rotate' command. * https://p5js.org/reference/p5/rotate/ */ function overrideRotateFunction(){ _originalRotateFunc = _p5Instance.rotate; _bTransformsExist = true; _p5Instance.rotate = function(angle) { if (_bRecordingSvg) { _commands.push({ type: 'rotate', angle }); } _originalRotateFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js shearX function to capture X-axis skew for SVG output. * Captures shearing angle for recording SVG output by storing a 'shearx' command. * @see {@link https://p5js.org/reference/p5/shearX/} */ function overrideShearXFunction(){ _originalShearXFunc = _p5Instance.shearX; _bTransformsExist = true; _p5Instance.shearX = function(angle) { if (_bRecordingSvg) { _commands.push({ type: 'shearx', angle }); } _originalShearXFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js shearY function to capture Y-axis skew for SVG output. * Captures shearing angle for recording SVG output by storing a 'sheary' command. * @see {@link https://p5js.org/reference/p5/shearY/} */ function overrideShearYFunction(){ _originalShearYFunc = _p5Instance.shearY; _bTransformsExist = true; _p5Instance.shearY = function(angle) { if (_bRecordingSvg) { _commands.push({ type: 'sheary', angle }); } _originalShearYFunc.apply(this, arguments); }; } /** * @private * Overrides the p5.js text function to capture SVG text commands for export. * Captures text content, position, font properties, alignment, and style for * later rendering in SVG format. Currently, it does not handle optional maxWidth * and maxHeight parameters and will issue a warning if these are provided. * @see {@link https://p5js.org/reference/p5/text/} */ function overrideTextFunction() { _originalTextFunc = _p5Instance.text; _p5Instance.text = function(content, x, y, maxWidth, maxHeight) { if (_bRecordingSvg) { // Warn if maxWidth or maxHeight are provided if (typeof maxWidth !== 'undefined' || typeof maxHeight !== 'undefined') { console.warn('The SVG export function currently does not support maxWidth or maxHeight for text rendering.'); } // Capture font, size, alignment, and style using _p5Instance const font = _p5Instance.textFont().font ? _p5Instance.textFont().font.names.fullName : _p5Instance.textFont(); const fontSize = _p5Instance.textSize(); const alignX = _p5Instance.textAlign().horizontal; const alignY = _p5Instance.textAlign().vertical; const style = _p5Instance.textStyle(); const leading = _p5Instance.textLeading(); const ascent = _p5Instance.textAscent(); const descent = _p5Instance.textDescent(); let transformMatrix = captureCurrentTransformMatrix(); // Push text command with properties _commands.push({ type: 'text', content, x, y, font, fontSize, alignX, alignY, style, leading, ascent, descent, transformMatrix }); } _originalTextFunc.apply(this, arguments); }; } /** * @private * Exports the recorded p5.js drawing commands as an SVG file. * Generates an SVG string from the recorded drawing commands, * including any applied transforms, styles, and shape data. * Creates an SVG file and triggers a download for the generated * SVG. Resets the internal recording state upon completion. */ function exportSVG() { let svgContent = ""; svgContent += `<!-- ${_svgFilename} -->\n`; svgContent += `<!-- Generated using p5.plotSvg: -->\n`; svgContent += `<!-- A Plotter-Oriented SVG Exporter for p5.js -->\n`; svgContent += `<!-- ${new Date().toString()} -->\n`; svgContent += `<!-- DPI: ${_svgDPI} -->\n`; let svgW = _bCustomSizeSet ? _svgWidth : _p5Instance.width; let svgH = _bCustomSizeSet ? _svgHeight : _p5Instance.height; let widthInches = svgW / _svgDPI; let heightInches = svgH / _svgDPI; svgContent += `<svg xmlns="http://www.w3.org/2000/svg" `; svgContent += `width="${widthInches}in" height="${heightInches}in" `; svgContent += `viewBox="0 0 ${svgW} ${svgH}"`; if (_svgBackgroundColor) { svgContent += ` style="background-color: ${_svgBackgroundColor}"`; } svgContent += `>\n`; svgContent += ` <style> circle, ellipse, line, path, polygon, polyline, rect, quad, text { fill: none; stroke: ${_svgDefaultStrokeColor}; stroke-width: ${_svgDefaultStrokeWeight}; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; } </style>\n`; // The SVG file wraps everything in a group with a non-scaling stroke effect: svgContent += `<g vector-effect="non-scaling-stroke">\n`; _svgGroupLevel++; let transformGroupStack = []; for (let cmd of _commands) { if (cmd.type === 'push' || cmd.type === 'pop' || cmd.type === 'scale' || cmd.type === 'translate' || cmd.type === 'rotate' || cmd.type === 'shearx' || cmd.type === 'sheary') { if (!_bFlattenTransforms && _bTransformsExist) { if (cmd.type === 'push') { // Open a new group svgContent += getIndentStr(); svgContent += `<g>\n`; transformGroupStack.push(1); _svgGroupLevel++; } else if (cmd.type === 'pop') { // Close the most recent group if (transformGroupStack.length > 0) { while (transformGroupStack[transformGroupStack.length - 1] > 0){ transformGroupStack[transformGroupStack.length - 1]--; _svgGroupLevel = Math.max(0, _svgGroupLevel - 1); svgContent += getIndentStr(); svgContent += `</g>\n`; } transformGroupStack.pop(); } } else { // Handle transformations by creating a group with a transform attribute let transformStr = ''; if (cmd.type === 'scale') { transformStr = getSvgStrScale(cmd); } else if (cmd.type === 'translate') { transformStr = getSvgStrTranslate(cmd); } else if (cmd.type === 'rotate') { transformStr = getSvgStrRotate(cmd); } else if (cmd.type === 'shearx') { transformStr = getSvgStrShearX(cmd); } else if (cmd.type === 'sheary') { transformStr = getSvgStrShearY(cmd); } svgContent += getIndentStr(); svgContent += `<g transform="${transformStr}">\n`; if (transformGroupStack.length > 0){ transformGroupStack[transformGroupStack.length - 1]++; } else { transformGroupStack.push(1); } _svgGroupLevel++; } } } else if (cmd.type === 'stroke') { handleSvgStrokeCommand(cmd); } else { svgContent += getIndentStr(); } if (cmd.type === 'description') { svgContent += getSvgStrDescription(cmd); } else if (cmd.type === 'beginGroup') { svgContent += getSvgStrBeginGroup(cmd); } else if (cmd.type === 'endGroup') { svgContent += getSvgStrEndGroup(cmd); } else if (cmd.type === 'arc') { svgContent += getSvgStrArc(cmd); } else if (cmd.type === 'bezier') { svgContent += getSvgStrBezier(cmd); } else if (cmd.type === 'circle') { svgContent += getSvgStrCircle(cmd); } else if (cmd.type === 'curve') { svgContent += getSvgStrCurve(cmd); } else if (cmd.type === 'ellipse') { svgContent += getSvgStrEllipse(cmd); } else if (cmd.type === 'line') { svgContent += getSvgStrLine(cmd); } else if (cmd.type === 'point') { svgContent += getSvgStrPoint(cmd); } else if (cmd.type === 'quad') { svgContent += getSvgStrQuad(cmd); } else if (cmd.type === 'rect') { svgContent += getSvgStrRect(cmd); } else if (cmd.type === 'triangle') { svgContent += getSvgStrTriangle(cmd); } else if (cmd.type === 'text'){ svgContent += getSvgStrText(cmd); } else if (cmd.type === 'polyline'){ svgContent += getSvgStrPoly(cmd); } else if (cmd.type === 'path'){ svgContent += getSvgStrPoly(cmd); } else if (cmd.type === 'points') { svgContent += getSvgStrPoints(cmd); } else if (cmd.type === 'lines') { svgContent += getSvgStrLines(cmd); } else if (cmd.type === 'triangles') { svgContent += getSvgStrTriangles(cmd); } else if (cmd.type === 'triangle_fan') { svgContent += getSvgStrTriangleFan(cmd); } else if (cmd.type === 'triangle_strip') { svgContent += getSvgStrTriangleStrip(cmd); } else if (cmd.type === 'quads') { svgContent += getSvgStrQuads(cmd); } else if (cmd.type === 'quad_strip') { svgContent += getSvgStrQuadStrip(cmd); } } // Close any remaining groups if (!_bFlattenTransforms) { while (transformGroupStack.length > 0) { while (transformGroupStack[transformGroupStack.length - 1] > 0){ transformGroupStack[transformGroupStack.length - 1]--; _svgGroupLevel = Math.max(0, _svgGroupLevel - 1); svgContent += getIndentStr(); svgContent += `</g>\n`; } transformGroupStack.pop(); } } svgContent += `</g>\n`; // Close the `non-scaling-stroke` group svgContent += `</svg>`; const blob = new Blob([svgContent], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = _svgFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); svgContent = ""; _commands = []; _vertexStack = []; } /** * @private * Generates an SVG scale transform string based on the given command object. * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform} * @param {Object} cmd - The command object containing scale values. * @param {number} cmd.sx - The scale factor along the x-axis. * @param {number} cmd.sy - The scale factor along the y-axis. * @returns {string} The SVG scale transform string. */ function getSvgStrScale(cmd){ let sxStr = formatNumber(cmd.sx, _svgTransformPrecision); let syStr = formatNumber(cmd.sy, _svgTransformPrecision); let str = `scale(${sxStr}, ${syStr})`; return str; } /** * @private * Generates an SVG translate transform string based on the given command object. * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform} * @param {Object} cmd - The command object containing translation values. * @param {number} cmd.tx - The translation distance along the x-axis. * @param {number} cmd.ty - The translation distance along the y-axis. * @returns {string} The SVG translate transform string. */ function getSvgStrTranslate(cmd){ let txStr = formatNumber(cmd.tx, _svgTransformPrecision); let tyStr = formatNumber(cmd.ty, _svgTransformPrecision); let str = `translate(${txStr}, ${tyStr})`; return str; } /** * @private * Generates an SVG rotate transform string based on the given command object. * Converts angles to degrees if necessary based on the current p5 angle mode. * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform} * @param {Object} cmd - The command object containing rotation values. * @param {number} cmd.angle - The rotation angle. * @returns {string} The SVG rotate transform string. */ function getSvgStrRotate(cmd){ let angle = cmd.angle; if (_p5Instance.angleMode() === _p5Instance.RADIANS) { angle = (cmd.angle * 180) / Math.PI; // Convert radians to degrees } let angStr = formatNumber(angle, _svgTransformPrecision); let str = `rotate(${angStr})`; return str; } /** * @private * Generates an SVG skewX transform string based on the given command object. * Converts angles to degrees if necessary based on the current p5 angle mode. * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform} * @param {Object} cmd - The command object containing the shear angle. * @param {number} cmd.angle - The shear angle along the x-axis. * @returns {string} The SVG skewX transform string in degrees. */ function getSvgStrShearX(cmd) { let angle = cmd.angle; if (_p5Instance.angleMode() === _p5Instance.RADIANS) { angle = (cmd.angle * 180) / Math.PI; // Convert radians to degrees } let angStr = formatNumber(angle, _svgTransformPrecision); let str = `skewX(${angStr})`; return str; } /** * @private * Generates an SVG skewY transform string based on the given command object. * Converts angles to degrees if necessary based on the current p5 angle mode. * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform} * @param {Object} cmd - The command object containing the shear angle. * @param {number} cmd.angle - The shear angle along the y-axis. * @returns {string} The SVG skewY transform string in degrees. */ function getSvgStrShearY(cmd) { let angle = cmd.angle; if (_p5Instance.angleMode() === _p5Instance.RADIANS) { angle = (cmd.angle * 180) / Math.PI; // Convert radians to degrees } let angStr = formatNumber(angle, _svgTransformPrecision); let str = `skewY(${angStr})`; return str; } /** * @private * Generates an SVG <desc> element string based on the given command object. * The <desc> element provides a textual description of the SVG content, * typically used for accessibility or metadata purposes. * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc} * @param {Object} cmd - The command object containing description text. * @param {string} cmd.text - The description text to be included within the <desc> element. * @returns {string} The SVG <desc> element string with the provided description text. */ function getSvgStrDescription(cmd){ let str = `<desc>${cmd.text}</desc>\n`; return str; } /** * @private * Generates an SVG string to start a new user-defined group element. * If a group name is provided, adds it as an ID attribute for the group. * @param {Object} cmd - The command object containing group properties. * @param {string} [cmd.gname] - Optional group name used as the ID for the SVG group. * @returns {string} The SVG string to open a new group. */ function getSvgStrBeginGroup(cmd){ // Start a new group, adding an ID if `gname` is provided let str = cmd.gname ? `<g id="${cmd.gname}">\n` : `<g>\n`; _svgGroupLevel++; return str; } /** * @private * Generates an SVG string to end the current group element. * Decreases the SVG group level counter to track nested groups. * @param {Object} cmd - The command object (not used but included for consistency). * @returns {string} The SVG string to close the group. */ function getSvgStrEndGroup(cmd){ // Close the current group _svgGroupLevel = Math.max(0, _svgGroupLevel-1); let str = `</g>\n`; return str; } /** * @private * Generates an SVG <path> element string representing an elliptical arc, * based on the given command object. Supports optional modes for chord and pie-slice shapes. * @param {Object} cmd - The command object containing arc parameters and optional mode. * @returns {string} The SVG <path> element string with formatted arc data. */ function getSvgStrArc(cmd) { // Generate the base arc path using p5ArcToSvgPath() let svgArcData = p5ArcToSvgPath(cmd.x, cmd.y, cmd.w, cmd.h, cmd.start, cmd.stop); let transformStr = generateTransformString(cmd); let styleStr = getSvgStrStroke(); let str = `<path d="` + svgArcData; // Extract the end point of the arc from svgArcData let endPoint = svgArcData.split(" ").slice(-2).join(" "); if (cmd.mode === _p5Instance.CHORD) { // Add a line segment to connect the arc endpoints str += ` L ${endPoint}`; str += ` Z`; // Close the path } else if (cmd.mode === _p5Instance.PIE) { // Add lines from the center to the end point to form a "pie" slice let ctrX = formatNumber(cmd.x); let ctrY = formatNumber(cmd.y); str += ` L ${endPoint} L ${ctrX} ${ctrY} Z`; // Connect end to center } str += `"${styleStr}${transformStr}/>\n`; return str; } /** * @private * Generates an SVG <path> element string representing a cubic Bézier curve, * based on the given command object. * @param {Object} cmd - The command object containing Bézier curve control points. * @returns {string} The SVG <path> element string with formatted control points. */ function getSvgStrBezier(cmd){ let x1Str = formatNumber(cmd.x1); let y1Str = formatNumber(cmd.y1); let x2Str = formatNumber(cmd.x2); let y2Str = formatNumber(cmd.y2); let x3Str = formatNumber(cmd.x3); let y3Str = formatNumber(cmd.y3); let x4Str = formatNumber(cmd.x4); let y4Str = formatNumber(cmd.y4); let styleStr = getSvgStrStroke(); let transformStr = generateTransformString(cmd); let str = `<path d="M ${x1Str},${y1Str} C ${x2Str},${y2St