p5.plotsvg
Version:
A Plotter-Oriented SVG Exporter for p5.js
1,356 lines (1,221 loc) • 189 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.3.0, June 22, 2026
// Known to work with p5.js versions 1.4.2–1.11.13
// Basic 2D geometry export is tested with p5.js version 2.3.0 as well.
(function(global) {
// Create a namespace for the library
const p5plotSvg = {};
// Attach constants to the p5plotSvg namespace
p5plotSvg.VERSION = "0.3.0";
p5plotSvg.SVG_INDENT_NONE = 0;
p5plotSvg.SVG_INDENT_SPACES = 1;
p5plotSvg.SVG_INDENT_TABS = 2;
p5plotSvg.SVG_UNITS_IN = 0;
p5plotSvg.SVG_UNITS_CM = 1;
const SVG_COMMAND = Object.freeze({
ARC: 'arc',
BEZIER: 'bezier',
CIRCLE: 'circle',
CURVE: 'curve',
SPLINE: 'spline',
ELLIPSE: 'ellipse',
LINE: 'line',
POINT: 'point',
QUAD: 'quad',
RECT: 'rect',
TRIANGLE: 'triangle',
POLYLINE: 'polyline',
PATH: 'path',
POINTS: 'points',
LINES: 'lines',
TRIANGLES: 'triangles',
TRIANGLE_FAN: 'triangle_fan',
TRIANGLE_STRIP: 'triangle_strip',
QUADS: 'quads',
QUAD_STRIP: 'quad_strip',
DESCRIPTION: 'description',
BEGIN_GROUP: 'beginGroup',
END_GROUP: 'endGroup',
PUSH: 'push',
POP: 'pop',
SCALE: 'scale',
TRANSLATE: 'translate',
ROTATE: 'rotate',
SHEAR_X: 'shearx',
SHEAR_Y: 'sheary',
TEXT: 'text',
STROKE: 'stroke'
});
const SVG_SEGMENT = Object.freeze({
VERTEX: 'vertex',
BEZIER: 'bezier',
BEZIER_POINT: 'bezierPoint',
QUADRATIC: 'quadratic',
CURVE: 'curve',
SPLINE: 'spline',
CONTOUR_START: 'contourStart',
CONTOUR_END: 'contourEnd'
});
Object.defineProperty(p5plotSvg, 'SVG_COMMAND', {
value: SVG_COMMAND,
enumerable: true
});
Object.defineProperty(p5plotSvg, 'SVG_SEGMENT', {
value: SVG_SEGMENT,
enumerable: true
});
// Internal configuration set using public API functions.
const config = {
flattenTransforms: false,
exportPolylinesAsPaths: false,
filename: "output.svg",
coordPrecision: 4,
transformPrecision: 6,
indentType: p5plotSvg.SVG_INDENT_SPACES,
indentAmount: 2,
pointRadius: 0.25,
dpi: 96,
unitMode: p5plotSvg.SVG_UNITS_IN,
width: 816,
height: 1056,
customSizeSet: false,
defaultStrokeColor: 'black',
backgroundColor: null,
defaultStrokeWeight: 1,
mergeNamedGroups: true,
groupByStrokeColor: false,
inkscapeCompatibility: true,
exportMalformedNumbersWithSanitizations: true,
clampLargeCoordinates: true,
coordinateClampMagnitude: 1000000
};
const cssColorValidationCache = new Map();
// Internal recording state. Override bookkeeping remains separate for now.
const session = {
commands: [],
vertexStack: [], // Temp stack for polyline/polygon vertices
groupStack: [], // Stack for tracking open groups (for unclosed group detection)
injectedHeaderAttributes: [], // Attributes to inject into the SVG header
injectedDefs: [],
pointsSetCount: 0,
linesSetCount: 0,
trianglesSetCount: 0,
triangleFanSetCount: 0,
triangleStripSetCount: 0,
quadsSetCount: 0,
quadStripSetCount: 0,
transformsExist: false,
recording: false,
recordingBegun: false,
recordingSessionId: 0,
shapeMode: "simple",
shapeKind: "poly",
p5Instance: undefined,
p5PixelDensity: 1,
p5MajorVersion: 1,
usingGlobalMode: false,
curveTightness: 0.0,
bezierOrder: 3, // p5.js v2 bezierVertex() point-stream order
splineEndsMode: null, // p5.js v2 splineProperty('ends') value
curveVertexCompatActive: false,
curveVertexCompatPreviousEndsMode: undefined,
layers: {
inkscapeLayerMap: new Map(), // Maps group names to layer numbers
inkscapeUsedLabels: new Map(), // Tracks used label values for collision detection
inkscapeNextLayerNumber: 1 // Counter for auto-incrementing
},
overrides: {
restoreStack: [],
originals: {
arc: undefined,
bezier: undefined,
circle: undefined,
curve: undefined,
spline: undefined,
ellipse: undefined,
line: undefined,
point: undefined,
quad: undefined,
rect: undefined,
square: undefined,
triangle: undefined,
bezierDetail: undefined,
curveTightness: undefined,
bezierOrder: undefined,
splineProperty: undefined,
splineProperties: undefined,
beginShape: undefined,
vertex: undefined,
bezierVertex: undefined,
quadraticVertex: undefined,
curveVertex: undefined,
splineVertex: undefined,
beginContour: undefined,
endContour: undefined,
endShape: undefined,
describe: undefined,
push: undefined,
pop: undefined,
scale: undefined,
translate: undefined,
rotate: undefined,
shearX: undefined,
shearY: undefined,
text: undefined,
stroke: undefined,
colorMode: undefined,
clip: undefined
}
}
};
// Warn-once state is intentionally module-level, not session-local.
const warnings = {
p5v2Shown: false,
curveV2Shown: false,
curveTightnessV2Shown: false,
clipShown: false,
incompleteBezierPointSegmentShown: false,
mixedSplineSegmentShown: false,
unsupportedPathSegmentShown: false,
nonFiniteNumberShown: false,
malformedCommandSkippedShown: false,
largeCoordinateClampedShown: false
};
/**
* @private
* Resets per-recording Inkscape layer bookkeeping.
* Layer labels and numeric prefixes are session state; they must not leak
* between independent SVG exports.
*/
function resetSessionLayers() {
session.layers.inkscapeLayerMap = new Map();
session.layers.inkscapeUsedLabels = new Map();
session.layers.inkscapeNextLayerNumber = 1;
}
/**
* @private
* Overrides a function on a target object, handling p5.js v2's non-writable properties.
* In p5.js v2, global functions are defined with writable:false, so simple assignment fails.
* This helper uses Object.defineProperty when needed to ensure the override takes effect.
* @param {object} target - The object to override the function on (e.g., window or session.p5Instance)
* @param {string} funcName - The name of the function to override
* @param {function} newFunc - The new function to use
*/
function overrideFunction(target, funcName, newFunc) {
if (!target) return;
const globalTarget = getGlobalTarget();
if (target === globalTarget && !session.usingGlobalMode) return;
let restoreInfo = session.overrides.restoreStack.find(item =>
item.target === target && item.funcName === funcName
);
if (!restoreInfo) {
const hadOwnProperty = Object.prototype.hasOwnProperty.call(target, funcName);
restoreInfo = {
target,
funcName,
hadOwnProperty,
descriptor: hadOwnProperty ? Object.getOwnPropertyDescriptor(target, funcName) : undefined,
replacementFunc: newFunc
};
session.overrides.restoreStack.push(restoreInfo);
} else {
restoreInfo.replacementFunc = newFunc;
}
const currentDescriptor = Object.getOwnPropertyDescriptor(target, funcName);
Object.defineProperty(target, funcName, {
value: newFunc,
writable: true,
configurable: true,
enumerable: currentDescriptor ? currentDescriptor.enumerable : true
});
}
/**
* @private
* Restores all p5 functions overridden during the current recording session.
* Exact own-property descriptors are restored; temporary own properties are
* deleted so inherited p5 instance methods remain inherited.
*/
function restoreOverriddenFunctions() {
for (let i = session.overrides.restoreStack.length - 1; i >= 0; i--) {
const item = session.overrides.restoreStack[i];
if (!item.target) continue;
// Warn if another library/user replaced our temporary override while recording.
if (item.replacementFunc && item.target[item.funcName] !== item.replacementFunc) {
console.warn(`p5.plotSvg: ${item.funcName} changed during SVG recording; another library may have overwritten the temporary override.`);
}
if (item.hadOwnProperty) {
Object.defineProperty(item.target, item.funcName, item.descriptor);
} else {
delete item.target[item.funcName];
}
}
session.overrides.restoreStack = [];
}
/**
* @private
* Returns the browser global object used by p5 global mode.
*/
function getGlobalTarget() {
if (typeof window !== 'undefined') return window;
if (typeof globalThis !== 'undefined') return globalThis;
if (typeof global !== 'undefined') return global;
return undefined;
}
/**
* @private
* Escapes text for use inside XML element content.
*/
function escapeXmlText(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
/**
* @private
* Escapes text for use inside double-quoted XML attribute values.
*/
function escapeXmlAttribute(value) {
return escapeXmlText(value)
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* @private
* XML tag and attribute names cannot be escaped into validity; reject unsafe
* names instead of emitting malformed SVG.
*/
function normalizeXmlName(name, context) {
const trimmedName = String(name).trim();
if (/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(trimmedName)) {
return trimmedName;
}
console.warn(`p5.plotSvg: Ignoring invalid ${context} "${trimmedName}".`);
return null;
}
/**
* @private
* Serializes an array of { name, value } pairs into escaped SVG attributes.
*/
function attrsToString(attrs, context, options = {}) {
if (!Array.isArray(attrs)) return '';
let str = '';
for (let attr of attrs) {
if (!attr || typeof attr.name === 'undefined') continue;
if (options.skip && options.skip(attr)) continue;
const attrName = normalizeXmlName(attr.name, context);
if (!attrName) continue;
if (options.onAttr) {
options.onAttr(attr, attrName);
}
str += `${options.trailingSpace ? '' : ' '}${attrName}="${escapeXmlAttribute(attr.value)}"${options.trailingSpace ? ' ' : ''}`;
}
return str;
}
/**
* @private
* Returns the p5.js version without requiring a global p5 symbol.
*/
function getP5Version(p5Instance = session.p5Instance) {
if (p5Instance?.constructor?.VERSION) {
return p5Instance.constructor.VERSION;
}
if (typeof p5 !== 'undefined' && p5.VERSION) {
return p5.VERSION;
}
return '1.0.0';
}
/**
* @private
* Retrieves an original p5 function from the active instance, falling back to
* global mode if needed. Instance mode does not always define window aliases.
*/
function getOriginalFunction(funcName) {
const globalTarget = getGlobalTarget();
if (session.usingGlobalMode && globalTarget && typeof globalTarget[funcName] === 'function') {
return globalTarget[funcName];
}
if (session.p5Instance && typeof session.p5Instance[funcName] === 'function') {
return session.p5Instance[funcName];
}
if (globalTarget && typeof globalTarget[funcName] === 'function') {
return globalTarget[funcName];
}
return undefined;
}
/**
* @private
* Calls p5's original implementation without recording p5 internals. This is
* important in p5.js v2, where some public APIs delegate through shape APIs
* that p5.plotSvg also overrides.
*/
function callOriginalFunction(originalFunc, thisArg, args) {
if (typeof originalFunc !== 'function') return undefined;
const wasRecording = session.recording;
session.recording = false;
try {
return originalFunc.apply(thisArg, args);
} finally {
session.recording = wasRecording;
}
}
/**
* @private
* Checks for WEBGL rendering without assuming WebGLRenderingContext exists.
*/
function isWebGLDrawingContext() {
const WebGLCtor = (typeof WebGLRenderingContext !== 'undefined') ? WebGLRenderingContext : null;
return !!(WebGLCtor && session.p5Instance?._renderer?.drawingContext instanceof WebGLCtor);
}
/**
* @private
* Whether a value looks like a p5 sketch instance.
*/
function isP5InstanceLike(value) {
return !!(value && typeof value === 'object' && typeof value.pixelDensity === 'function');
}
/**
* @private
* Finds the active global-mode p5 sketch for argument-flexible APIs.
*/
function getGlobalP5Instance() {
const globalTarget = getGlobalTarget();
if (globalTarget && isP5InstanceLike(globalTarget)) {
return globalTarget;
}
if (typeof p5 !== 'undefined' && isP5InstanceLike(p5.instance)) {
return globalTarget || p5.instance;
}
if (globalTarget?.p5 && isP5InstanceLike(globalTarget.p5.instance)) {
return globalTarget || globalTarget.p5.instance;
}
return undefined;
}
/**
* @private
* Whether a p5 instance is the active global-mode sketch instance.
*/
function isGlobalP5Instance(value) {
const globalTarget = getGlobalTarget();
if (!isP5InstanceLike(value) || !globalTarget) return false;
if (typeof p5 !== 'undefined' && p5.instance === value) return true;
if (globalTarget.p5 && globalTarget.p5.instance === value) return true;
return false;
}
/**
* @private
* Normalizes old and add-on-style beginRecordSvg() argument forms.
*/
function normalizeBeginRecordSvgArgs(p5InstanceOrFilename, maybeFilename) {
if (isP5InstanceLike(p5InstanceOrFilename) || p5InstanceOrFilename === getGlobalTarget()) {
return {
p5Instance: p5InstanceOrFilename,
filename: maybeFilename
};
}
return {
p5Instance: getGlobalP5Instance(),
filename: p5InstanceOrFilename
};
}
/**
* 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!
* beginRecordSvg("file.svg"); // global-mode add-on style
* beginRecordSvg(null); // global-mode add-on style, does not save a file
* @param {object|string|null} p5Instance - A p5 sketch instance, or a filename in global-mode add-on style.
* @param {string|null} [fn] - Optional filename for explicit-instance usage.
*/
p5plotSvg.beginRecordSvg = function(p5InstanceOrFilename, maybeFilename) {
const normalizedArgs = normalizeBeginRecordSvgArgs(p5InstanceOrFilename, maybeFilename);
const p5Instance = normalizedArgs.p5Instance;
let fn = normalizedArgs.filename;
// Validate the p5 instance
if (!p5Instance) {
throw new Error("Invalid p5 instance provided to beginRecordSvg().");
}
// Detect p5.js version for compatibility handling
const p5Version = getP5Version(p5Instance);
session.p5MajorVersion = parseInt(p5Version.split('.')[0]);
// Store a reference to the p5 instance for use in other functions
// In p5.js v2 global mode, 'this' in draw() is window, not the p5 instance
// Use p5.instance instead when available and p5Instance appears to be window
const globalTarget = getGlobalTarget();
session.usingGlobalMode = (p5Instance === globalTarget);
if (session.p5MajorVersion >= 2 && session.usingGlobalMode && typeof p5 !== 'undefined' && typeof p5.instance !== 'undefined') {
session.p5Instance = p5.instance;
} else {
session.p5Instance = p5Instance;
}
session.p5PixelDensity = session.p5Instance.pixelDensity();
session.bezierOrder = getCurrentBezierOrder();
session.curveTightness = getCurrentSplineTightness();
session.splineEndsMode = getCurrentSplineEndsMode();
session.curveVertexCompatActive = false;
session.curveVertexCompatPreviousEndsMode = undefined;
// Warn if using v2 (only once per session)
if (!warnings.p5v2Shown && session.p5MajorVersion >= 2) {
console.warn(
`p5.plotSvg: Detected p5.js version ${p5Version}. ` +
`p5.js v2 compatibility is experimental. Basic 2D geometry export is supported, ` +
`including v2 spline() and splineVertex(), but some p5.js v2 API changes may ` +
`still produce differences from p5.js v1 output.`
);
warnings.p5v2Shown = true;
}
// Check if filename is provided and valid
if (fn === null) {
// if fn is null, explicit opt-out: do NOT save a file
config.filename = null;
} else if (typeof fn === 'string' && fn.length > 0) {
// Ensure ".svg" extension is present
if (!fn.endsWith(".svg")) {
fn += ".svg";
}
// Strip out illegal filename characters (keep alphanumeric, hyphen, underscore, dot)
fn = fn.replace(/[^a-zA-Z0-9-_\.]/g, '');
// Get the base name (without .svg extension)
let base = fn.slice(0, -4);
// Check if base has any real content (not just dots)
let hasContent = base.replace(/\./g, '').length > 0;
// If base is empty or only dots, fall back to default
if (!hasContent) {
config.filename = "output.svg";
} else {
config.filename = fn;
}
} else {
// Default behavior: undefined or invalid fn → output.svg
config.filename = "output.svg";
}
// Initialize SVG settings and override functions
session.recording = true;
session.recordingBegun = true;
session.transformsExist = false;
session.commands = [];
// Experimental extension hook: expose the live command array while
// recording so advanced external add-ons can inspect or inject commands.
p5plotSvg._commands = session.commands;
session.vertexStack = [];
session.groupStack = [];
session.injectedHeaderAttributes = [];
session.injectedDefs = [];
session.pointsSetCount = 0;
session.linesSetCount = 0;
session.trianglesSetCount = 0;
session.triangleFanSetCount = 0;
session.triangleStripSetCount = 0;
session.quadsSetCount = 0;
session.quadStripSetCount = 0;
resetSessionLayers();
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 (!session.recordingBegun){
console.warn("You must beginRecordSvg() before you can pauseRecordSvg().");
return;
} else {
if (bPause === true){
session.recording = false;
} else if (bPause === false){
session.recording = 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();
session.recording = false;
session.recordingBegun = false;
session.recordingSessionId++;
p5plotSvg._recordingSessionId = session.recordingSessionId;
return svgStr;
};
/**
* @public
* @deprecated Use p5plotSvg.beginRecordSvg() instead.
* Backward-compatible wrapper for the original capitalized API name.
*/
p5plotSvg.beginRecordSVG = function() {
console.warn("beginRecordSVG() is deprecated. The new name is beginRecordSvg().");
return p5plotSvg.beginRecordSvg.apply(p5plotSvg, arguments);
};
/**
* @public
* @deprecated Use p5plotSvg.pauseRecordSvg() instead.
* Backward-compatible wrapper for the original capitalized API name.
*/
p5plotSvg.pauseRecordSVG = function() {
console.warn("pauseRecordSVG() is deprecated. The new name is pauseRecordSvg().");
return p5plotSvg.pauseRecordSvg.apply(p5plotSvg, arguments);
};
/**
* @public
* @deprecated Use p5plotSvg.endRecordSvg() instead.
* Backward-compatible wrapper for the original capitalized API name.
*/
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();
overrideSplineFunction();
overrideEllipseFunction();
overrideLineFunction();
overridePointFunction();
overrideQuadFunction();
overrideRectFunction();
overrideSquareFunction();
overrideTriangleFunction();
overrideBezierDetailFunction();
overrideCurveTightnessFunction();
overrideBezierOrderFunction();
overrideSplinePropertyFunction();
overrideSplinePropertiesFunction();
overrideBeginShapeFunction();
overrideVertexFunction();
overrideBezierVertexFunction();
overrideQuadraticVertexFunction();
overrideCurveVertexFunction();
overrideSplineVertexFunction();
overrideBeginContourFunction();
overrideEndContourFunction();
overrideEndShapeFunction();
overrideDescribeFunction();
overridePushFunction();
overridePopFunction();
overrideScaleFunction();
overrideTranslateFunction();
overrideRotateFunction();
overrideShearXFunction();
overrideShearYFunction();
overrideTextFunction();
overrideStrokeFunction();
overrideColorModeFunction();
overrideClipFunction();
}
/**
* @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(){
restoreOverriddenFunctions();
}
/**
* @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 `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/arc/}
*/
function overrideArcFunction() {
// Save the original window.arc for proper restoration in v2
session.overrides.originals.arc = getOriginalFunction('arc');
const newArcFunc = function(x, y, w, h, start, stop, mode, detail = 0) {
if (session.recording) {
if (detail !== undefined && isWebGLDrawingContext()) {
console.warn("arc() detail is currently unsupported in SVG output.");
}
// Get ellipseMode: v2 stores in states.ellipseMode, v1 in _ellipseMode
const ellipseMode = session.p5Instance._renderer?.states?.ellipseMode ||
session.p5Instance._renderer?._ellipseMode ||
'center';
// Adjust x, y, w, h based on ellipseMode (arc follows ellipse mode)
if (ellipseMode === 'center') ; else if (ellipseMode === 'corner') {
x += w / 2;
y += h / 2;
} else if (ellipseMode === 'radius') {
w *= 2;
h *= 2;
} else if (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 + (qx - px) / 2;
y = py + (qy - py) / 2;
w = qx - px;
h = qy - py;
}
let transformMatrix = captureCurrentTransformMatrix();
const svgMode = (typeof mode === 'undefined') ? getP5Constant('OPEN') : mode;
session.commands.push({ type: SVG_COMMAND.ARC, x, y, w, h, start, stop, mode: svgMode, transformMatrix });
}
callOriginalFunction(session.overrides.originals.arc, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'arc', newArcFunc);
overrideFunction(getGlobalTarget(), 'arc', newArcFunc);
}
/**
* @private
* Overrides the p5.js bezier function to capture SVG bezier curve commands for export.
* Stores bezier curve control points in the `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/bezier/}
*/
function overrideBezierFunction(){
// Save the original window.bezier for proper restoration in v2
session.overrides.originals.bezier = getOriginalFunction('bezier');
const newBezierFunc = function(x1, y1, x2, y2, x3, y3, x4, y4) {
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
session.commands.push({ type: SVG_COMMAND.BEZIER, x1, y1, x2, y2, x3, y3, x4, y4, transformMatrix });
}
callOriginalFunction(session.overrides.originals.bezier, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'bezier', newBezierFunc);
overrideFunction(getGlobalTarget(), 'bezier', newBezierFunc);
}
/**
* @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 `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/circle/}
*/
function overrideCircleFunction(){
// Save the original window.circle for proper restoration in v2
session.overrides.originals.circle = getOriginalFunction('circle');
const newCircleFunc = function(x, y, d) {
let argumentsCopy = [...arguments]; // safe snapshot
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
// Get ellipseMode: v2 stores in states.ellipseMode, v1 in _ellipseMode
const ellipseMode = session.p5Instance._renderer?.states?.ellipseMode ||
session.p5Instance._renderer?._ellipseMode ||
'center';
if (ellipseMode === 'center'){
session.commands.push({ type: SVG_COMMAND.CIRCLE, x, y, d, transformMatrix });
} else if (ellipseMode === 'corner'){
x += d/2;
y += d/2;
session.commands.push({ type: SVG_COMMAND.CIRCLE, x, y, d, transformMatrix });
} else if (ellipseMode === 'radius'){
d *= 2;
session.commands.push({ type: SVG_COMMAND.CIRCLE, x, y, d, transformMatrix });
} else if (ellipseMode === 'corners'){
let w = d - x;
let h = d - y;
x += w/2;
y += h/2;
session.commands.push({ type: SVG_COMMAND.ELLIPSE, x, y, w, h, transformMatrix });
}
}
callOriginalFunction(session.overrides.originals.circle, session.p5Instance, argumentsCopy);
};
overrideFunction(session.p5Instance, 'circle', newCircleFunc);
overrideFunction(getGlobalTarget(), 'circle', newCircleFunc);
}
/**
* @private
* Overrides the p5.js curve function to capture SVG curve commands for export.
* Stores curve parameters and current tightness in the `session.commands` array.
* @see {@link https://p5js.org/reference/#/p5/curve}
*/
function overrideCurveFunction() {
session.overrides.originals.curve = getOriginalFunction('curve');
if (session.p5MajorVersion >= 2) {
const curveCompatFunc = function(x1, y1, x2, y2, x3, y3, x4, y4) {
let argumentsCopy = [...arguments];
if (!warnings.curveV2Shown) {
console.warn("p5.plotSvg: curve() is no longer part of p5.js v2; using a p5.plotSvg compatibility shim during SVG recording.");
warnings.curveV2Shown = true;
}
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
let tightness = getCurrentSplineTightness();
session.commands.push({ type: SVG_COMMAND.CURVE, x1, y1, x2, y2, x3, y3, x4, y4, tightness, transformMatrix });
}
const drawCurve = () => callOriginalFunction(session.overrides.originals.spline, session.p5Instance, argumentsCopy);
if (typeof session.overrides.originals.spline === 'function') {
withTemporarySplineEndsMode(getP5Constant('EXCLUDE'), drawCurve);
}
};
overrideFunction(session.p5Instance, 'curve', curveCompatFunc);
overrideFunction(getGlobalTarget(), 'curve', curveCompatFunc);
return;
}
// p5.js v1 - normal override
const newCurveFunc = function(x1, y1, x2, y2, x3, y3, x4, y4) {
let argumentsCopy = [...arguments]; // safe snapshot
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
let tightness = session.curveTightness; // Capture current tightness (set via curveTightness override)
session.commands.push({ type: SVG_COMMAND.CURVE, x1, y1, x2, y2, x3, y3, x4, y4, tightness, transformMatrix });
}
callOriginalFunction(session.overrides.originals.curve, session.p5Instance, argumentsCopy);
};
overrideFunction(session.p5Instance, 'curve', newCurveFunc);
overrideFunction(getGlobalTarget(), 'curve', newCurveFunc);
}
/**
* @private
* Overrides p5.js v2's spline function. p5.js v1 uses curve() instead.
*/
function overrideSplineFunction() {
session.overrides.originals.spline = getOriginalFunction('spline');
if (typeof session.overrides.originals.spline !== 'function') return;
const newSplineFunc = function(x1, y1, x2, y2, x3, y3, x4, y4) {
let argumentsCopy = [...arguments];
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
let tightness = getCurrentSplineTightness();
let endsMode = getCurrentSplineEndsMode();
session.commands.push({
type: SVG_COMMAND.SPLINE,
points: [
{ x: x1, y: y1 },
{ x: x2, y: y2 },
{ x: x3, y: y3 },
{ x: x4, y: y4 }
],
closed: false,
tightness,
endsMode,
transformMatrix
});
}
callOriginalFunction(session.overrides.originals.spline, session.p5Instance, argumentsCopy);
};
overrideFunction(session.p5Instance, 'spline', newSplineFunc);
overrideFunction(getGlobalTarget(), 'spline', newSplineFunc);
}
/**
* @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 `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/ellipse/}
*/
function overrideEllipseFunction(){
// Save the original window.ellipse for proper restoration in v2
session.overrides.originals.ellipse = getOriginalFunction('ellipse');
const newEllipseFunc = function(x, y, w, h, detail = 0) {
let argumentsCopy = [...arguments]; // safe snapshot
if (session.recording) {
if (detail !== undefined && isWebGLDrawingContext()) {
console.warn("ellipse() detail is currently unsupported in SVG output.");
}
// Get ellipseMode: v2 stores in states.ellipseMode, v1 in _ellipseMode
const ellipseMode = session.p5Instance._renderer?.states?.ellipseMode ||
session.p5Instance._renderer?._ellipseMode ||
'center';
if (ellipseMode === 'center'); else if (ellipseMode === 'corner'){
x += w/2;
y += h/2;
} else if (ellipseMode === 'radius'){
w *= 2;
h *= 2;
} else if (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();
session.commands.push({ type: SVG_COMMAND.ELLIPSE, x, y, w, h, transformMatrix });
}
callOriginalFunction(session.overrides.originals.ellipse, session.p5Instance, argumentsCopy);
};
overrideFunction(session.p5Instance, 'ellipse', newEllipseFunc);
overrideFunction(getGlobalTarget(), 'ellipse', newEllipseFunc);
}
/**
* @private
* Overrides the p5.js line function to capture SVG line commands for export.
* Stores line parameters in the `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/line/}
*/
function overrideLineFunction() {
// Save the original window.line for proper restoration in v2
session.overrides.originals.line = getOriginalFunction('line');
const newLineFunc = function(x1, y1, x2, y2) {
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
session.commands.push({ type: SVG_COMMAND.LINE, x1, y1, x2, y2, transformMatrix });
}
callOriginalFunction(session.overrides.originals.line, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'line', newLineFunc);
overrideFunction(getGlobalTarget(), 'line', newLineFunc);
}
/**
* @private
* Overrides the p5.js point function to capture SVG point commands for export.
* Stores point parameters as small circles in the `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/point/}
*/
function overridePointFunction() {
// Save the original window.point for proper restoration in v2
session.overrides.originals.point = getOriginalFunction('point');
const newPointFunc = function(x, y) {
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
session.commands.push({ type: SVG_COMMAND.POINT, x, y, radius: config.pointRadius, transformMatrix });
}
callOriginalFunction(session.overrides.originals.point, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'point', newPointFunc);
overrideFunction(getGlobalTarget(), 'point', newPointFunc);
}
/**
* @private
* Overrides the p5.js quad function to capture SVG quad commands for export.
* Stores quad parameters in the `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/quad/}
*/
function overrideQuadFunction(){
// Save the original window.quad for proper restoration in v2
session.overrides.originals.quad = getOriginalFunction('quad');
const newQuadFunc = function(x1, y1, x2, y2, x3, y3, x4, y4) {
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
session.commands.push({ type: SVG_COMMAND.QUAD, x1, y1, x2, y2, x3, y3, x4, y4, transformMatrix });
}
callOriginalFunction(session.overrides.originals.quad, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'quad', newQuadFunc);
overrideFunction(getGlobalTarget(), 'quad', newQuadFunc);
}
/**
* @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 `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/rect/}
*/
function overrideRectFunction() {
// Save the original window.rect for proper restoration in v2
session.overrides.originals.rect = getOriginalFunction('rect');
const newRectFunc = function(x, y, w, h, tl, tr, br, bl) {
let argumentsCopy = [...arguments]; // safe snapshot
if (session.recording) {
if (arguments.length === 3) { h = w; }
// Get rectMode: v2 stores in states.rectMode, v1 in _rectMode
const rectMode = session.p5Instance._renderer?.states?.rectMode ||
session.p5Instance._renderer?._rectMode ||
'corner';
// Handle different rect modes
if (rectMode === 'corner') ; else if (rectMode === 'center') {
x = x - w / 2;
y = y - h / 2;
} else if (rectMode === 'radius') {
x = x - w;
y = y - h;
w = 2 * w;
h = 2 * h;
} else if (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
session.commands.push({ type: SVG_COMMAND.RECT, x, y, w, h, tl, transformMatrix });
} else if (arguments.length === 8) { // Individual corner radii
session.commands.push({ type: SVG_COMMAND.RECT, x, y, w, h, tl,tr,br,bl, transformMatrix });
} else { // Standard rectangle
session.commands.push({ type: SVG_COMMAND.RECT, x, y, w, h, transformMatrix });
}
}
callOriginalFunction(session.overrides.originals.rect, session.p5Instance, argumentsCopy);
};
overrideFunction(session.p5Instance, 'rect', newRectFunc);
overrideFunction(getGlobalTarget(), 'rect', newRectFunc);
}
/**
* @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 `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/square/}
*/
function overrideSquareFunction(){
// Save the original window.square for proper restoration in v2
session.overrides.originals.square = getOriginalFunction('square');
const newSquareFunc = function(x, y, s, tl, tr, br, bl) {
let argumentsCopy = [...arguments]; // safe snapshot
if (session.recording) {
let w = s;
let h = s;
// Get rectMode: v2 stores in states.rectMode, v1 in _rectMode
const rectMode = session.p5Instance._renderer?.states?.rectMode ||
session.p5Instance._renderer?._rectMode ||
'corner';
if (rectMode === 'corner'); else if (rectMode === 'center'){
x = x - w/2;
y = y - h/2;
} else if (rectMode === 'radius'){
x = x - w;
y = y - h;
w = 2*w;
h = 2*h;
} else if (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
session.commands.push({ type: SVG_COMMAND.RECT, x, y, w, h, transformMatrix });
} else if (arguments.length === 4) { // rounded square
session.commands.push({ type: SVG_COMMAND.RECT, x, y, w, h, tl, transformMatrix });
} else if (arguments.length === 7) {
session.commands.push({ type: SVG_COMMAND.RECT, x, y, w, h, tl, tr, br, bl, transformMatrix });
}
}
callOriginalFunction(session.overrides.originals.square, session.p5Instance, argumentsCopy);
};
overrideFunction(session.p5Instance, 'square', newSquareFunc);
overrideFunction(getGlobalTarget(), 'square', newSquareFunc);
}
/**
* @private
* Overrides the p5.js triangle function to capture SVG triangle commands for export.
* Stores triangle vertex coordinates in the `session.commands` array when recording SVG output.
* @see {@link https://p5js.org/reference/p5/triangle/}
*/
function overrideTriangleFunction(){
// Save the original window.triangle for proper restoration in v2
session.overrides.originals.triangle = getOriginalFunction('triangle');
const newTriangleFunc = function(x1, y1, x2, y2, x3, y3) {
if (session.recording) {
let transformMatrix = captureCurrentTransformMatrix();
session.commands.push({ type: SVG_COMMAND.TRIANGLE, x1, y1, x2, y2, x3, y3, transformMatrix });
}
callOriginalFunction(session.overrides.originals.triangle, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'triangle', newTriangleFunc);
overrideFunction(getGlobalTarget(), 'triangle', newTriangleFunc);
}
/**
* @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() {
session.overrides.originals.bezierDetail = getOriginalFunction('bezierDetail');
if (typeof session.overrides.originals.bezierDetail !== 'function') return;
const newBezierDetailFunc = function(detailLevel) { // Check if the renderer is WEBGL
if (isWebGLDrawingContext()) {
console.warn("bezierDetail() is currently unsupported in SVG output.");
}
callOriginalFunction(session.overrides.originals.bezierDetail, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'bezierDetail', newBezierDetailFunc);
overrideFunction(getGlobalTarget(), 'bezierDetail', newBezierDetailFunc);
}
/**
* @private
* Overrides the p5.js curveTightness function to capture curve tightness settings for SVG export.
* Updates the recorded curve tightness to reflect the specified tightness value.
* @see {@link https://p5js.org/reference/p5/curveTightness/}
*/
function overrideCurveTightnessFunction() {
session.overrides.originals.curveTightness = getOriginalFunction('curveTightness');
if (session.p5MajorVersion >= 2) {
const curveTightnessCompatFunc = function(tightness) {
session.curveTightness = tightness;
setOriginalSplineProperty('tightness', tightness);
if (!warnings.curveTightnessV2Shown) {
console.warn("p5.plotSvg: curveTightness() is no longer part of p5.js v2; mapping it to splineProperty('tightness', value) during SVG recording.");
warnings.curveTightnessV2Shown = true;
}
return session.p5Instance;
};
overrideFunction(session.p5Instance, 'curveTightness', curveTightnessCompatFunc);
overrideFunction(getGlobalTarget(), 'curveTightness', curveTightnessCompatFunc);
return;
}
// p5.js v1 - normal override
const newCurveTightnessFunc = function(tightness) {
if (session.recording) {
session.curveTightness = tightness;
}
callOriginalFunction(session.overrides.originals.curveTightness, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'curveTightness', newCurveTightnessFunc);
overrideFunction(getGlobalTarget(), 'curveTightness', newCurveTightnessFunc);
}
/**
* @private
* Tracks p5.js v2's current Bezier order for bezierVertex() point streams.
*/
function overrideBezierOrderFunction() {
session.overrides.originals.bezierOrder = getOriginalFunction('bezierOrder');
if (typeof session.overrides.originals.bezierOrder !== 'function') return;
const newBezierOrderFunc = function(order) {
if (typeof order !== 'undefined') {
session.bezierOrder = order;
}
return callOriginalFunction(session.overrides.originals.bezierOrder, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'bezierOrder', newBezierOrderFunc);
overrideFunction(getGlobalTarget(), 'bezierOrder', newBezierOrderFunc);
}
/**
* @private
* Tracks p5.js v2 spline settings that affect exported spline geometry.
*/
function overrideSplinePropertyFunction() {
session.overrides.originals.splineProperty = getOriginalFunction('splineProperty');
if (typeof session.overrides.originals.splineProperty !== 'function') return;
const newSplinePropertyFunc = function(property, value) {
const ret = callOriginalFunction(session.overrides.originals.splineProperty, session.p5Instance, arguments);
if (typeof value !== 'undefined') {
if (property === 'tightness') {
session.curveTightness = value;
} else if (property === 'ends') {
session.splineEndsMode = value;
}
}
return ret;
};
overrideFunction(session.p5Instance, 'splineProperty', newSplinePropertyFunc);
overrideFunction(getGlobalTarget(), 'splineProperty', newSplinePropertyFunc);
}
/**
* @private
* Tracks p5.js v2 spline settings assigned in bulk.
*/
function overrideSplinePropertiesFunction() {
session.overrides.originals.splineProperties = getOriginalFunction('splineProperties');
if (typeof session.overrides.originals.splineProperties !== 'function') return;
const newSplinePropertiesFunc = function(values) {
const ret = callOriginalFunction(session.overrides.originals.splineProperties, session.p5Instance, arguments);
if (values && typeof values === 'object') {
if (Object.prototype.hasOwnProperty.call(values, 'tightness')) {
session.curveTightness = values.tightness;
}
if (Object.prototype.hasOwnProperty.call(values, 'ends')) {
session.splineEndsMode = values.ends;
}
}
return ret;
};
overrideFunction(session.p5Instance, 'splineProperties', newSplinePropertiesFunc);
overrideFunction(getGlobalTarget(), 'splineProperties', newSplinePropertiesFunc);
}
/**
* @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() {
session.overrides.originals.beginShape = getOriginalFunction('beginShape');
const newBeginShapeFunc = function(kind) {
if (session.recording) {
session.vertexStack = []; // Start with an empty vertex stack
session.shapeMode = "simple"; // Assume simple mode initially
if ((kind !== null) && (kind === 0 || (typeof session.p5Instance.POINTS !== 'undefined' && kind === session.p5Instance.POINTS))) {
session.shapeKind = SVG_COMMAND.POINTS;
} else if (kind === null || typeof kind === 'undefined'){
session.shapeKind = 'poly'; // default to "poly" for polyline/polygon
} else {
session.shapeKind = kind;
}
}
callOriginalFunction(session.overrides.originals.beginShape, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'beginShape', newBeginShapeFunc);
overrideFunction(getGlobalTarget(), 'beginShape', newBeginShapeFunc);
}
/**
* @private
* Overrides the p5.js vertex function to capture vertex coordinates for SVG export.
* Pushes simple vertex data to the `session.vertexStack` when recording is active.
* @see {@link https://p5js.org/reference/p5/vertex/}
*/
function overrideVertexFunction() {
session.overrides.originals.vertex = getOriginalFunction('vertex');
const newVertexFunc = function(x, y) {
if (session.recording) {
session.vertexStack.push({ type: SVG_SEGMENT.VERTEX, x, y });
}
callOriginalFunction(session.overrides.originals.vertex, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'vertex', newVertexFunc);
overrideFunction(getGlobalTarget(), 'vertex', newVertexFunc);
}
/**
* @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 `session.vertexStack`.
* @see {@link https://p5js.org/reference/p5/bezierVertex/}
*/
function overrideBezierVertexFunction() {
// Override `bezierVertex()` and mark shape as complex
session.overrides.originals.bezierVertex = getOriginalFunction('bezierVertex');
const newBezierVertexFunc = function(x2, y2, x3, y3, x4, y4) {
if (session.recording) {
session.shapeMode = 'complex'; // Switch to complex mode
if (session.p5MajorVersion >= 2 && arguments.length <= 5) {
session.vertexStack.push({
type: SVG_SEGMENT.BEZIER_POINT,
x: x2,
y: y2,
order: getCurrentBezierOrder()
});
} else {
session.vertexStack.push({ type: SVG_SEGMENT.BEZIER, x2, y2, x3, y3, x4, y4 });
}
}
callOriginalFunction(session.overrides.originals.bezierVertex, session.p5Instance, arguments);
};
overrideFunction(session.p5Instance, 'bezierVertex', newBezierVertexFunc);
overrideFunction(getGlobalTarget(), 'bezierVertex', newBezierVertexFunc);
}
/**
* @private
* Overrides the p5.js quadraticVertex function to capture quadratic Bézier control points for SVG export.
* Marks the