p5.plotsvg
Version:
A Plotter-Oriented SVG Exporter for p5.js
1,399 lines (1,249 loc) • 118 kB
JavaScript
// p5.plotSvg: a Plotter-Oriented SVG Exporter for p5.js
// https://github.com/golanlevin/p5.plotSvg
// Initiated by Golan Levin (@golanlevin)
// v.0.1.7, November 22, 2025
// Known to work with p5.js versions 1.4.2–1.11.10
(function(global) {
// Create a namespace for the library
const p5plotSvg = {};
// Attach constants to the p5plotSvg namespace
p5plotSvg.VERSION = "0.1.7";
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 _bSvgExportPolylinesAsPaths = 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;
let _svgMergeNamedGroups = true;
let _svgGroupByStrokeColor = false;
// Internal variables, not to be accessed directly
let _p5Instance;
let _recordingSessionId = 0;
let _p5PixelDensity = 1;
let _svgGroupLevel = 0;
let _commands = [];
let _vertexStack = []; // Temp stack for polyline/polygon vertices
let _injectedHeaderAttributes = []; // Attributes to inject into the SVG header
let _injectedDefs = [];
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.
* Behavior is as follows:
* beginRecordSvg(this); // saves to output.svg (default)
* beginRecordSvg(this, "file.svg"); // saves to file.svg
* beginRecordSvg(this, null); // DOES NOT save any file!
* @param {object} p5Instance - A reference to the current p5.js sketch (e.g. `this`).
* @param {string} [fn] - Optional filename for the output SVG file.
*/
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 (fn === null) {
// if fn is null, explicit opt-out: do NOT save a file
_svgFilename = null;
} else 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
fn = fn.replace(/[^a-zA-Z0-9-_\.]/g, '');
// At this point fn might be ".svg" if everything else was stripped.
// Compute "basename" (characters before ".svg") and ensure it's real.
let base = fn.toLowerCase().endsWith(".svg") ? fn.slice(0, -4) : fn;
// Optionally strip dots from the base to avoid names like ".svg"
base = base.replace(/\./g, '');
// If basename is empty, fall back to default
if (base.length === 0) {
_svgFilename = "output.svg";
} else {
_svgFilename = fn;
}
} else {
// Default behavior: undefined or invalid fn → output.svg
_svgFilename = "output.svg";
}
// Initialize SVG settings and override functions
_bRecordingSvg = true;
_bRecordingSvgBegun = true;
_bTransformsExist = false;
_commands = [];
// This is critically important, do not move;
// Needed for addon libraries like e.g. p5PowerStroke to access _commands:
p5plotSvg._commands = _commands;
_vertexStack = [];
_injectedHeaderAttributes = [];
_injectedDefs = [];
_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.
* Returns the text of the SVG file as a string.
*/
p5plotSvg.endRecordSvg = function() {
let svgStr = exportSVG();
restoreP5Functions();
_bRecordingSvg = false;
_bRecordingSvgBegun = false;
_recordingSessionId++;
p5plotSvg._recordingSessionId = _recordingSessionId;
return svgStr;
}
// Old names: wrappers for backward compatibility
p5plotSvg.beginRecordSVG = function() {
console.warn("beginRecordSVG() is deprecated. The new name is beginRecordSvg().");
return p5plotSvg.beginRecordSvg.apply(p5plotSvg, arguments);
};
p5plotSvg.pauseRecordSVG = function() {
console.warn("pauseRecordSVG() is deprecated. The new name is pauseRecordSvg().");
return p5plotSvg.pauseRecordSvg.apply(p5plotSvg, arguments);
};
p5plotSvg.endRecordSVG = function() {
console.warn("endRecordSVG() is deprecated. The new name is endRecordSvg().");
return p5plotSvg.endRecordSvg.apply(p5plotSvg, arguments);
};
/**
* @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) {
let argumentsCopy = [...arguments]; // safe snapshot
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, argumentsCopy);
};
}
/**
* @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) {
let argumentsCopy = [...arguments]; // safe snapshot
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, argumentsCopy);
};
}
/**
* @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) {
let argumentsCopy = [...arguments]; // safe snapshot
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, argumentsCopy);
};
}
/**
* @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) {
let argumentsCopy = [...arguments]; // safe snapshot
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, argumentsCopy);
};
}
/**
* @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) {
let argumentsCopy = [...arguments]; // safe snapshot
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, argumentsCopy);
};
}
/**
* @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 = "";
let svgW = _bCustomSizeSet ? _svgWidth : _p5Instance.width;
let svgH = _bCustomSizeSet ? _svgHeight : _p5Instance.height;
let widthInches = svgW / _svgDPI;
let heightInches = svgH / _svgDPI;
// The <svg> tag
svgContent += `<svg `;
svgContent += ` version="1.1" `;
svgContent += ` xmlns="http://www.w3.org/2000/svg" `;
for (let attr of _injectedHeaderAttributes) {
svgContent += ` ${attr.name}="${attr.value}" `;
}
svgContent += ` width="${widthInches}in" height="${heightInches}in" `;
svgContent += ` viewBox="0 0 ${svgW} ${svgH}" `;
if (_svgBackgroundColor) {
svgContent += ` style="background-color: ${_svgBackgroundColor}" `;
}
svgContent += `>\n`; // close the <svg> tag
// The <defs> tag
if (_injectedDefs.length > 0) {
svgContent += ` <defs>\n`;
for (let def of _injectedDefs) {
svgContent += ` <${def.type} `;
for (let attr of def.attributes) {
svgContent += `${attr.name}="${attr.value}" `;
}
svgContent += ` />\n`;
}
svgContent += ` </defs>\n`;
}
// The <style> tag
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>`;
if (_svgMergeNamedGroups) {
svgContent = getSvgStrMergedGroups(svgContent);
}
if (_svgGroupByStrokeColor) {
svgContent = getSvgStrGroupByStrokeColor(svgContent);
}
let headerContent = ``;
if (_svgFilename){ headerContent += `<!-- ${_svgFilename} -->\n`; }
headerContent += `<!-- Generated using p5.plotSvg v.${p5plotSvg.VERSION}: -->\n`;
headerContent += `<!-- A Plotter-Oriented SVG Exporter for p5.js -->\n`;
headerContent += `<!-- ${new Date().toString()} -->\n`;
headerContent += `<!-- DPI: ${_svgDPI} -->\n`;
svgContent = headerContent + svgContent;
if (_svgFilename !== null) {
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);
} else {
// _svgFilename is explicitly null; do not save any file.
// Probably you're using the returned SVG string in some other way.
}
_vertexStack = [];
_injectedHeaderAttributes = [];
_injectedDefs = [];
// Delete commands array completely. Needed for e.g. p5PowerStroke.
if (Array.isArray(_commands)) _commands.length = 0;
_commands = null;
p5plotSvg._commands = null;
return svgContent;
}
/**
* @private
* Merges named groups in an SVG string by combining sibling groups with the same ID.
* @param {string} svgString
* @returns A SVG string with merged named groups.
*/
function getSvgStrMergedGroups(svgString){
const doc = new DOMParser().parseFromString(svgString, "image/svg+xml");
function processElement(element) {
const children = Array.from(element.children);
children.forEach((child) => processElement(child));
const groupsById = new Map();
const nodesToRemove = [];
children.forEach((child) => {
if (child.tagName === "g" && child.hasAttribute("id")) {
const id = child.getAttribute("id");
if (id !== "") {
if (!groupsById.has(id)) {
groupsById.set(id, []);
}
groupsById.get(id).push(child);
}
}
});
groupsById.forEach((groups, id) => {
if (groups.length > 1) {
const firstGroup = groups[0];
const firstGroupDepth = getGroupDepth(firstGroup);
for (let i = 1; i < groups.length; i++) {
const groupToMerge = groups[i];
const indent = '\n' + getIndentStr(firstGroupDepth + 1);
while (groupToMerge.firstChild) {
const child = groupToMerge.firstChild;
if (child.nodeType === Node.ELEMENT_NODE) {
firstGroup.appendChild(doc.createTextNode(indent));
firstGroup.appendChild(child);
} else {
groupToMerge.removeChild(child);
}
}
nodesToRemove.push(groupToMerge);
}
// Add a closing newline
const closingIndent = '\n' + getIndentStr(firstGroupDepth);
firstGroup.appendChild(doc.createTextNode(closingIndent));
}
});
nodesToRemove.forEach((node) => {
const next = node.nextSibling;
// remove any empty text nodes left after the element to remove
if (next && next.nodeType === Node.TEXT_NODE && /^\s*$/.test(next.nodeValue)) {
element.removeChild(next);
}
element.removeChild(node);
});
}
processElement(doc.documentElement);
return new XMLSerializer().serializeToString(doc);
}
/**
* @private
* Group sibling elements by stroke color in an SVG string.
* @param {string} svgString
* @returns A SVG string with sibling elements grouped by stroke color.
*/
function getSvgStrGroupByStrokeColor(svgString) {
const doc = new DOMParser().parseFromString(svgString, "image/svg+xml");
function processElement(element) {
const children = Array.from(element.children);
const colorGroups = new Map();
const nodesToRemove = [];
children.forEach((child) => processElement(child));
children.forEach((child) => {
const strokeColor = getStrokeColor(child);
if (strokeColor && child.tagName !== 'g') {
if (!colorGroups.has(strokeColor)) {
colorGroups.set(strokeColor, []);
}
colorGroups.get(strokeColor).push(child);
nodesToRemove.push(child);
}
});
nodesToRemove.forEach((node) => {
const next = node.nextSibling;
// remove any empty text nodes left after the element to remove
if (next && next.nodeType === Node.TEXT_NODE && /^\s*$/.test(next.nodeValue)) {
element.removeChild(next);
}
element.removeChild(node);
});
colorGroups.forEach((elements, col) => {
if (elements.length > 0) {
const group = doc.createElementNS("http://www.w3.org/2000/svg", "g");
group.setAttribute('id', `stroke-color-group-${col.replace(/[^a-zA-Z0-9]/g, '-')}`);
element.appendChild(group);
elements.forEach(elem => {
group.appendChild(elem);
});
}
});
}
processElement(doc.documentElement);
return new XMLSerializer().serializeToString(doc);
}
function getStrokeColor(element) {
if (element.hasAttribute('stroke')) {
const stroke = element.getAttribute('stroke');
if (stroke && stroke !== 'none') {
return stroke;
}
}
if (element.hasAttribute('style')) {
const style = element.getAttribute('style');