UNPKG

p5.plotsvg

Version:

A Plotter-Oriented SVG Exporter for p5.js

1,356 lines (1,221 loc) 189 kB
// 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, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;'); } /** * @private * Escapes text for use inside double-quoted XML attribute values. */ function escapeXmlAttribute(value) { return escapeXmlText(value) .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); } /** * @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