Skip to content

SVG export

SVGRenderer is a vector / static-export backend. It consumes the same readonly Layer[] that Renderer2D.render() takes and emits an SVG document as a string — never touching the GPU. Because it shares the layer model, a chart exports without a separate spec: build your layers once, draw them to the canvas and to SVG.

Preview of the SVG export demo
SVG export Serialize any scene to vector graphics with createSVGRenderer.

Use it for static export, print, server-side rendering (it runs in pure Node), and snapshot tests.

import { createSVGRenderer } from "insomni";
const svgRenderer = createSVGRenderer({ width: 800, height: 600 });
svgRenderer.setCamera({ x: 0, y: 0, zoom: 1 });
const svgString = svgRenderer.render(layers); // same Layer[] you pass to render()
// In a browser/jsdom you can also get a DOM node:
const node = svgRenderer.element(); // SVGSVGElement (throws in pure Node)

createSVGRenderer(options?) is a thin factory over new SVGRenderer(options?).

| Option | Default | Description | | -------- | -------- | -------------------------------------------------------------------------- | | width | 600 | SVG document width (also the viewBox width). | | height | 400 | SVG document height. | | camera | identity | Initial camera ({ x, y, zoom, rotation }) used for world-space layers. | | dpr | 1 | Device-pixel ratio used to scale ui layers into the viewBox. |

| Member | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | render(layers) | Render layers (composited in array order) to an SVG string, also caching it. | | svg() | The string from the most recent render() (empty before the first). | | element() | Parse the last SVG into an SVGSVGElement. Requires a DOM (DOMParser); throws in pure Node — use svg() there. | | setCamera(state) / getCamera() | Get/set the camera. | | fitCameraToBounds(bounds, options?) | Fit the camera to a Bounds2D. | | setBackground(color) | Background rect drawn when alpha > 0. | | resize(width, height) / setDpr(dpr) | Update document size / DPR. | | getViewMatrix() / worldToScreen(x, y) / screenToWorld(x, y) | Camera math helpers (mirror the GPU renderer). | | width / height / dpr | Readonly getters. | | compute() | No-op (SVG has no GPU compute phase; warns once). | | destroy() | Clear the cached SVG. |

The backend walks each layer’s UberPack (and GlyphPack) and emits semantic SVG elements:

| insomni kind | SVG element | | ------------------ | -------------------------------------------------------------------- | | rect | <rect> (with rx/ry for corner radius, rotate(...) transform) | | circle | <circle> | | ellipse | <ellipse> (with rotate(...) transform) | | segment | <line> | | curve (cubic) | <path> (cubic C) | | arc | <path> (A; full rings split into two semicircles) | | triangle / polygon | <polygon> | | text (glyphs) | <text> (reconstructed from the glyph atlas) |

Per-layer space (world / ui), the camera, per-shape group transforms (folded into the layer matrix), and per-layer clip rects (<clipPath>) are all honored.

  • Sprites / textures (TYPE_SPRITE) — skipped with a one-time warning.
  • BufferLayer / custom drawables — they expose no walkable UberPack, so a layer that lacks one is skipped.
  • compute() — no-op.

The UberPack does not retain the original high-level shape records — it stores the packed instance fields, so the SVG backend recovers from those:

  • Positions, endpoints, control points, triangle vertices, arc center/radius/angles are stored as full f32 → recovered losslessly. These are the values a vector consumer cares most about.
  • rect/circle/ellipse size, rotation, stroke width, corner radius, curve widths are stored as f16 → recovered to ~3 significant digits.
  • All colors are unorm8 → recovered to 8 bits per channel.

This f16/unorm8 loss is the same loss the live GPU render shows and is imperceptible at export resolution. Additional approximations specific to SVG:

  • Tapered curves — SVG has no per-endpoint stroke taper; tapered curves are emitted as a uniform-width path (the two widths are averaged), with a one-time warning.
  • Text — glyph strings are not retained, so runs are reconstructed by grouping consecutive glyphs that share a baseline/color into one <text>. The baseline is an export-time approximation (glyph top + quad height), so exact positions may drift a fraction of a glyph.
import { createSVGRenderer } from "insomni";
function exportSvg(layers, camera) {
const r = createSVGRenderer({ width: 1024, height: 768, camera });
return r.render(layers); // write this string to a .svg file
}