You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Enable the library to construct scenes and serialize SVGs in Node.js without a browser. The browser remains the primary front-end for authoring and interactive use, but generation and export should not require one.
// Browser — consumer passes their containerconstscene=newScene({root: document.getElementById('root'),width: 960,height: 540});// Node — no container neededconstscene=newScene({width: 960,height: 540});constsvg=scene.serialize();
This unlocks scriptable SVG export, batch generation for consumer repos, and CI-based asset pipelines. The Node pipeline output (SVG → PNG via @resvg/resvg-js) should match the browser's FIGMA export output, validated by regression tests (#23).
Currently, the library produces browser-independent SVG output — but the process of getting there requires the browser. The browser resolves CSS variables, computes styles, and provides SVG geometry APIs that the export pipeline depends on. This refactor makes the library own its rendering directly, so the SVG is self-contained from construction time — no browser needed to produce the final output.
Eliminates multiple export targets
The library currently has three export targets (BROWSER, FIGMA, ILLUSTRATOR) that exist to work around differences in how each consumer implements the SVG spec:
FIGMA/ILLUSTRATOR: vector-effect: non-scaling-stroke isn't supported, so stroke widths are computed via getScreenCTM() and baked in
FIGMA: MathJax SVG output needs specific scaling adjustments
BROWSER: Different MathJax rendering hacks
If the library owns its rendering fully and produces SVGs where all styles are explicit attributes, stroke widths are pre-computed, MathJax is flattened with explicit sizing, and markers are embedded as paths — then there is nothing left for Chrome, Figma, resvg, or Illustrator to disagree about. The export targets collapse into a single unambiguous output format.
Element creation (~31 files)
Replace direct document.createElementNS() / document.createElement() calls with a centralized factory
In browser: delegates to document; in Node: delegates to a lightweight DOM (e.g., linkedom)
Breaking change: Scene/Frame constructors no longer implicitly reach for document or #root
Most mechanical part of the refactor
Theming (22 files, 32 CSS variables)
Theme currently uses CSS custom properties resolved by the browser's CSS engine via getComputedStyle()
Light/dark switching works via CSSOM rule manipulation (insertRule/deleteRule)
Refactor: Theme object provides resolved color values directly; elements consume theme.get('blue') instead of var(--blue)
In the browser, CSS variables can still be maintained as a side effect for live rendering and DevTools inspection, but the library does not rely on them for export
Before:
// Theme injects CSS rules via CSSOM, elements reference CSS variablesthis.backgroundRectangle.style.fill='var(--background)';circle.setAttribute('fill','var(--blue)');axesColor: 'var(--font-color-subtle)',
Currently loads MathJax as a browser global and calls MathJax.tex2svg() against the browser DOM
Uses getBoundingClientRect() for measurement and alignment after rendering
flattenSVG() resolves <use> references from MathJax output
Node.js path:mathjax-full (available since MathJax 3.0) provides a liteAdaptor that replaces the browser DOM entirely — tex2svg works in Node with no browser, jsdom, or headless Chrome needed. The SVG output includes computed width, height, and vertical-align from MathJax's internal font metrics, so getBoundingClientRect() should not be needed
Dependency change: Replace the mathjax peer dependency with mathjax-full as a regular dependency. mathjax-full works in both browser and Node (it's the superset), and making it a regular dependency means consumers don't need to think about MathJax at all — they just call new Tex(...) and the library handles the rest. Conditional exports and tree-shaking keep the browser bundle lean.
Used across Grid, GridAlt, PlotGridBased, PlotOld, Control, and GridPoint to convert mouse/touch coordinates into SVG-space for drag interactions
The pattern is: createSVGPoint() → set .x/.y → .matrixTransform(svg.getScreenCTM().inverse())
Inherently browser-interactive — stays browser-only, but won't work with a lightweight DOM shim since getScreenCTM() requires layout
createSVGPoint() is a deprecated browser API that could be replaced with {x, y} + manual matrix math to decouple from the SVG DOM
DOMParser / parseSVG()
src/util/svg.ts uses new DOMParser() to parse SVG strings — needs a Node equivalent (linkedom provides this)
getEncompassingBoundingClientRectangle()
src/util/svg.ts iterates elements calling getBoundingClientRect() and constructs a new DOMRect() — both are browser APIs with no Node equivalent
Consumer repo impact
Checked wumbo-site-images and wumbo-inventing-complex-numbers. Consumer code is clean — it mostly works through library abstractions (ScenePlayer, Plot, CoordinateSystem, .frame.path(), .frame.tex(), etc.). The library is the bottleneck; once it supports Node, consumers need only mechanical updates:
var(--blue) → theme.get('blue') (~7 files in wumbo-site-images set .style.fill/.style.stroke with CSS variables)
document.createElementNS() → element factory (2 call sites)
forceMode/restoreMode dance → pass 'light' or 'dark' to serialize()
Approach
Incremental — each piece can be tackled independently, validated by regression tests from #23. Sub-issues will be created as work begins. Some parts (path geometry) may remain browser-dependent longer than others.
Goal
Enable the library to construct scenes and serialize SVGs in Node.js without a browser. The browser remains the primary front-end for authoring and interactive use, but generation and export should not require one.
This unlocks scriptable SVG export, batch generation for consumer repos, and CI-based asset pipelines. The Node pipeline output (SVG → PNG via
@resvg/resvg-js) should match the browser's FIGMA export output, validated by regression tests (#23).Prerequisite
What this involves
Currently, the library produces browser-independent SVG output — but the process of getting there requires the browser. The browser resolves CSS variables, computes styles, and provides SVG geometry APIs that the export pipeline depends on. This refactor makes the library own its rendering directly, so the SVG is self-contained from construction time — no browser needed to produce the final output.
Eliminates multiple export targets
The library currently has three export targets (BROWSER, FIGMA, ILLUSTRATOR) that exist to work around differences in how each consumer implements the SVG spec:
vector-effect: non-scaling-strokeisn't supported, so stroke widths are computed viagetScreenCTM()and baked inIf the library owns its rendering fully and produces SVGs where all styles are explicit attributes, stroke widths are pre-computed, MathJax is flattened with explicit sizing, and markers are embedded as paths — then there is nothing left for Chrome, Figma, resvg, or Illustrator to disagree about. The export targets collapse into a single unambiguous output format.
Element creation (~31 files)
document.createElementNS()/document.createElement()calls with a centralized factorydocument; in Node: delegates to a lightweight DOM (e.g.,linkedom)documentor#rootTheming (22 files, 32 CSS variables)
getComputedStyle()insertRule/deleteRule)theme.get('blue')instead ofvar(--blue)Before:
After:
TeX/MathJax
MathJax.tex2svg()against the browser DOMgetBoundingClientRect()for measurement and alignment after renderingflattenSVG()resolves<use>references from MathJax outputmathjax-full(available since MathJax 3.0) provides aliteAdaptorthat replaces the browser DOM entirely —tex2svgworks in Node with no browser, jsdom, or headless Chrome needed. The SVG output includes computedwidth,height, andvertical-alignfrom MathJax's internal font metrics, sogetBoundingClientRect()should not be neededmathjaxpeer dependency withmathjax-fullas a regular dependency.mathjax-fullworks in both browser and Node (it's the superset), and making it a regular dependency means consumers don't need to think about MathJax at all — they just callnew Tex(...)and the library handles the rest. Conditional exports and tree-shaking keep the browser bundle lean.Tricky parts
Arrows/Markers
embedMarkers()usesgetTotalLength()andgetPointAtLength()to position arrows along paths — browser SVG geometry APIs with no pure-JS equivalentupdateArrow()usesdocument.getElementById()to find markers in the global DOMvar(--font-color), passed explicitly by callersSVG geometry APIs
getTotalLength()/getPointAtLength()— path measurement for marker positioninggetScreenCTM()— transformation matrix for stroke width adjustment (currently needed per export target, eliminated if strokes are pre-computed)getBoundingClientRect()— element measurement (partially eliminated by MathJax's built-in metrics)getScreenCTM()/createSVGPoint()/matrixTransform()— Screen ↔ SVG coordinate conversionGrid,GridAlt,PlotGridBased,PlotOld,Control, andGridPointto convert mouse/touch coordinates into SVG-space for drag interactionscreateSVGPoint()→ set.x/.y→.matrixTransform(svg.getScreenCTM().inverse())getScreenCTM()requires layoutcreateSVGPoint()is a deprecated browser API that could be replaced with{x, y}+ manual matrix math to decouple from the SVG DOMDOMParser/parseSVG()src/util/svg.tsusesnew DOMParser()to parse SVG strings — needs a Node equivalent (linkedom provides this)getEncompassingBoundingClientRectangle()src/util/svg.tsiterates elements callinggetBoundingClientRect()and constructs anew DOMRect()— both are browser APIs with no Node equivalentConsumer repo impact
Checked
wumbo-site-imagesandwumbo-inventing-complex-numbers. Consumer code is clean — it mostly works through library abstractions (ScenePlayer,Plot,CoordinateSystem,.frame.path(),.frame.tex(), etc.). The library is the bottleneck; once it supports Node, consumers need only mechanical updates:var(--blue)→theme.get('blue')(~7 files in wumbo-site-images set.style.fill/.style.strokewith CSS variables)document.createElementNS()→ element factory (2 call sites)bundle(root, ExportTarget.FIGMA)→scene.serialize()(export targets collapse)forceMode/restoreModedance → pass'light'or'dark'toserialize()Approach
Incremental — each piece can be tackled independently, validated by regression tests from #23. Sub-issues will be created as work begins. Some parts (path geometry) may remain browser-dependent longer than others.