Vormen is a bag of tools, modules and scripts for creating generative art in SVG using Javascript
- Classes, functions and modules used in your code to easily create artwork in SVG.
- An optional runner module that turns your drawing into a program: render it to an SVG file, or serve it in a browser with a settings UI.
- Tools to run your code in a browser and interact with the SVG there
Warning
This is very early, alpha code. The examples below may not be implemented yet, or may not work anymore.
Note
Examples below use deno. The final library will be usable on any javascript or typescript platform. node, npm, yarn, bun etc.
Create a new project and add the dependency:
deno init my-drawing && deno add jsr:@berkes/vormen
Edit a file, e.g. drawing.ts. Your drawing is a program: it defines a draw
function that takes settings and returns a string, then hands that function to
the runner as its last line. Here's a full example:
import {
Drawing,
Grid,
Noise,
Settings,
StandardSize,
Vormen,
} from "@berkes/vormen";
const defaults = {
rotationStrength: 35,
};
const draw = (rawSettings) => {
const settings = Settings.parse(defaults, rawSettings);
// Create a drawing with a4 size, equal margins on all sides and a white background.
const drawing = new Drawing().withStandardSize(StandardSize.A4).withMargin(20)
.withBackground(
"white",
);
// From this drawing, we extract the area that we can draw on.
const canvas = drawing.build();
// Another utility is a grid. This allows us to put stuff in a grid.
// Here we creat a 8x9 grid, each cell is padded 4 units and cells are square.
const grid = new Grid().rows(9).columns(8).padding(4).squareCells();
// Another utility is a seedable noise generator. Simplex noise.
const noise = new Noise("seeeed");
// The grit in action: loop over all cells
grid.cells().forEach((cell) => {
// And finally we use the svg.js API to create some rectangles
let square = canvas
.rect(cell.innerWidth, cell.innerHeight) // Use the grid to find the sizes of the squares
.fill("none")
.stroke({ width: 1, color: "black" })
.move(cell.x, cell.y) // Use the grid to position the square
.rotate(
// Use the noise to randomly rotate this square
// Apply a setting that controls how much the cells will be rotated
noise.get(cell.x, cell.y) * (cell.row * settings.rotationStrength),
);
});
return drawing.svg();
};
Vormen(draw, defaults);In this example, we see a lot of things coming together.
We can run it to generate an SVG:
deno run drawing.ts render --outfile drawing.svg (This is generated with
examples/cubic-disarray.ts)
Or output to stdout.
deno run drawing.ts render --outfile - > drawing.svgOr override settings
deno run drawing.ts render --outfile drawing.svg --settings.rotationStrength=10The examples directory contains runnable example drawings. Each example has a page in doc/examples/ with a short explanation and the SVGs it generates.
All example SVGs are generated and committed, and can be regenerated with a single command:
just generate-examplesThe full example above is by no means how it must be set up. This is not a framework. It is a library that you can pick and choose from.
Just use the runner, by passing it a function that returns the SVG as string.
import { Vormen } from "@berkes/vormen";
const draw = (_settings) => {
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<rect x="10" y="10" width="80" height="80" fill="red" />
</svg>`;
};
Vormen(draw, {});Or just the page builder, by letting that create the SVG string and then
console.log() that, or write it to a file.
import { writeFileSync } from "node:fs";
import { Drawing, StandardSize } from "@berkes/vormen";
const drawing = new Drawing().withStandardSize(StandardSize.A4).withMargin(20);
const canvas = drawing.build();
for(i = 0, i < 8, i++) {
for(j = 0, j < 9, j++) {
const x = i * 10;
const y = j * 10;
canvas.rect(10, 10).x(x).y(y);
}
}
writeFileSync('mydrawing.svg', drawing.svg());All modules should be usable without requiring the others (this is work in progress, some requirements exist that will be removed).
See below for a full list of modules and their usage.
The examples/ directory holds runnable drawings.
You can run these with deno run examples/thefile.ts after you check out the
source code of this repo.
Warning
Modules documentation is WIP. So incomplete and probably out of date
Main execution module for running drawings. Contains the Vormen function, render, and serve.
We assume deno run drawing.ts where drawing.ts uses Vormen.
deno run drawing.ts servewill open a web app with the rendered drawing and a GUI with tools to play with it. Additionally, any settings will create a widget next to the drawing which we can adjust settings and have the drawing re-rendered with the changes.deno run drawing.ts render --outfile file.svgwill render the svg to the file. Additionally, any settings can be overridden with--settings.theSettingName=barto render with that setting instead of the default from the drawing.
Drawings receive their settings as a SettingsData record
(Record<string, string | number | boolean>), so the Vormen and Drawing modules
work without the Settings class. A drawing that wants typed settings parses the
record inside its draw function, e.g. with
Settings.parse(schema, rawSettings).
In future, we should be able to use node drawing.js, npx, node run, yarn
or any runtime that can execute a javascript (or typescript) file.
The basis of SVG creation. Contains the Drawing class for building SVG artwork with standard sizes, margins, and backgrounds. This is a simplified API that wraps svg.js to create drawings with standard size presets (A3, A4, A5), margin support, and background color support.
Definition elements (e.g. gradients or reusable shapes) are set with
withDefs(elements), where each element is an svg.js Element. They are placed
in the <defs> of the SVG when the drawing is built. Calling withDefs() again
replaces the previously set elements.
Metadata is attached with withMetadata(metadata), where the metadata is built
with MetadataBuilder (see the Metadata module below). On build(), the
metadata is serialized into the SVG as <title> and <desc> child elements of
the root and a <metadata> element with an RDF/XML block (Dublin Core /
Creative Commons, the same shape Inkscape writes and reads), plus the generator
settings as JSON in the vormen: namespace. Serializing the metadata does not
alter the drawing geometry.
A printable metadata caption is attached with withCaption(caption) (see the
Metadata module below). On build() it is rendered as plottable pen-stroke
paths in the margin at the caption's position (bottom-right by default), so a
pen plotter can draw it. If the margin on that side is too small, the viewBox is
expanded so the caption never covers the artwork.
All with*() methods return a new Drawing and leave the drawing they were
called on unchanged, so one base drawing can be branched into several
variations. The copies share the underlying SVG document.
Calling build() locks the drawing it is called on. Builder methods called on a
locked drawing log a warning to the console: they return a new, unlocked
Drawing, but cannot change what the earlier build() produced.
Embedding metadata in the SVG. Contains the Metadata and MetadataBuilder classes for machine-readable metadata, and the Caption and CaptionBuilder classes for printable metadata.
const metadata = new MetadataBuilder()
.title("Cubic Disarray")
.description("A grid of rotating squares, after Georg Nees")
.author("Bär Kessels")
.copyright("Copyright 2026 Bär Kessels")
.date(new Date())
.url("https://github.com/berkes/vormen")
.license("https://creativecommons.org/licenses/by/4.0/")
.keywords(["generative", "grid"])
.settings(settings.asRecord()) // store the seed and other settings as JSON
.build();
const caption = new CaptionBuilder()
.template("{title} — {author} — seed: {seed}") // default: "{title} — {author} — {seed}"
.position(CaptionPosition.BottomRight) // default: bottom-right
.font(SvgFont.named("EMSTech")) // default: EMSTech, a plottable SVG stroke font
.fontSize(10) // cap height in user units
.color("#000")
.build();
const drawing = new Drawing()
.withStandardSize(StandardSize.A4)
.withMargin(20)
.withMetadata(metadata)
.withCaption(caption);On build(), the metadata is written as <title>, <desc> and <metadata>
(the latter holding RDF/XML with dc:/cc: terms and a vormen:generator
element with the settings as JSON). Files written this way open in Inkscape with
title, author and license in Document Properties → Metadata, and
exiftool file.svg extracts title, creator, rights and date. The caption is
printed in the margin as pen-stroke paths (see the Typography module), so it
survives on a pen plotter; its template resolves {placeholders} against the
metadata fields and each generator setting (e.g. {seed}), and placeholders
without a value are dropped.
Plottable text: rendering strings as pen strokes a pen plotter can draw, using
SVG stroke fonts. Contains the Text and TextBuilder classes (a higher-level
element that acts like svg.js text(), but builds paths) and the SvgFont class
(SVG 1.1 fonts — the format Inkscape and Hershey Text use — with per-glyph
horiz-adv-x, real vertical metrics, and <hkern> kerning pairs).
const text = new TextBuilder()
.content("Hello plotter")
.font(SvgFont.named("EMSTech")) // or any bundled name; see bundledNames()
.size(20) // cap height in user units
.x(10) // left edge of the text
.y(100) // baseline of the text
.color("#000")
.strokeWidth(1)
.letterSpacing(0)
.build();
text.addTo(canvas); // canvas is the group Drawing.build() returnsaddTo(container) adds a <g> holding one stroked, unfilled <path> per glyph
(each with a translate/scale transform that maps the font's y-up units into
SVG's y-down): pen-up travel between strokes is an M move or a separate
element, so every drawn line is a stroke the pen can follow. There is no SVG
<text> in the output. text.width, text.height, text.ascent and
text.descent give the rendered metrics. Text is single-line only, with no
inline markup.
41 stroke fonts ship with the package as real .svg files in
src/typography/fonts/ (the EMS display faces and the Hershey classics as SVG
fonts; list them with SvgFont.bundledNames()). Because they are plain files in
the package directory, they are included in the published npm and JSR packages
too, and the library reads them at runtime (lazily, on first use) relative
to its own module — no font data is embedded in the code. Reading bundled fonts
needs read permission, e.g. deno run --allow-read ...; without it, load a font
yourself with SvgFont.fromData() (see below). Fonts cover whatever glyphs the
font file defines; accents are stripped and common punctuation (— – ‘ ’ “ ” …)
is mapped to its ASCII stand-in, unknown characters are skipped. Spacing uses
each glyph's horiz-adv-x, plus <hkern> pairs for fonts that define them,
plus an optional letterSpacing on top. Licenses: the Hershey-derived files
carry the Hershey use restriction and the EMS fonts are SIL OFL 1.1 — both
documented with sources in src/typography/fonts/README.md (and OFL.txt).
Bring your own SVG font and the typography module uses it like a bundled one (e.g. any font from gitlab.com/oskay/svg-fonts):
// Deno: read the file for me
const myFont = SvgFont.fromFile("ems", "./svg-fonts/fonts/EMS/EMSTech.svg");
// Any runtime: read the file yourself (node:fs, fetch, ...) and pass the text
const fromText = SvgFont.fromData("ems", svgText);
const text = new TextBuilder().content("Engraved").font(myFont).size(20)
.build();See the Font Specimen example for every bundled font rendered with the Text element.
The metadata caption above uses this module: CaptionBuilder.font() picks the
stroke font, and Drawing.build() renders the caption with a Text element.
A grid system for organizing cells in a drawing. Contains Grid and Cell classes.
Grid uses the builder pattern for configuration; its cells() method yields
Cell objects that provide position and dimension information. Every with*()
method returns a new Grid and leaves the grid it was called on unchanged. The
grid isn't visible, but allows us to position elements in a grid with easy tools
like being able to loop over all the cells, get X and Y coordinates of a cell,
add paddings between cells and so on. grid.intersections() returns all points
where the grid lines cross, including the outer edges, sorted in ascending order
by y and then by x.
Calculation utilities, randomness, and noise generation. Contains constrain, Random, Noise, and Stringifiable.
- constrain: Constrains a value between a minimum and maximum
- Random: Seeded pseudo-random number generator with methods like between() and betweenFloat()
- Noise: Seeded Simplex noise generator for reproducible noise generation. Use the get(x, y) method to sample noise values.
Random and Noise are seeded, so we have reproducible results.
Note
We will add many of the utilities from p5.js, but slightly stricter
implemented to allow for better type checking and better documentation.
Geometric primitives for vector mathematics. Contains Point and Vector. These are objects that aren't drawn, but typically used when simulating or calculating and then drawing the results.
- Point: Represents a point in 2D space with x and y coordinates. Provides
methods for vector calculations like
to()(creates a vector between points),add()(offsets a point by a vector),equals()(compares points), andpointAtDistanceTowards()(finds a point at a specific distance towards another point). - Vector: Represents a 2D vector with x and y components. Provides
operations like
length(Euclidean magnitude),normalized(unit vector),scale()(scaling),negate()(reversal), anddist()(distance between vectors), as well as linear interpolation viaVector.lerp().
The example calculation for finding a point at distance L from point b towards point a can be expressed as either:
// Using the convenience method
const result = b.pointAtDistanceTowards(a, L);
// Or using chained vector operations
const back = a.to(b).normalized.scale(L).negate();
const result = b.add(back);Building SVG path data strings from points, as defined in the w3c SVG Spec on PathData. Contains the PathBuilder and RoundedPathBuilder classes.
- PathBuilder: Builds an angular path (straight line segments) from a list
of points. Uses the builder pattern:
new PathBuilder(points), optionally.withClosed(true)to close the path, then.build()returns the path data string. - RoundedPathBuilder: Extends PathBuilder to draw every corner as a
quadratic bezier curve. Set the corner radius with
.withRounding(radius)and optionally.withRoundingConstrained(true)to shrink the radius so corners never overlap on close-together points.
Both builders are constructed from points, so a path built with one can be
re-drawn with the other by re-using the points, e.g.
new RoundedPathBuilder(linearPath.points).
All builder methods (withClosed(), push(), withRounding() and
withRoundingConstrained()) return a new builder and leave the builder they
were called on unchanged.
Configuration management for drawings. Contains the Settings class for managing drawing parameters.
Settings are declared as a schema: a plain object whose values define each
setting's canonical type (string, number or boolean) and its default. Only
strings, numbers and booleans are valid setting values; declare your schema with
the SettingsData type to get compile-time errors for anything else.
const schema: SettingsData = {
seed: "default-seed",
strokeWidth: 1,
enabled: true,
};
const settings = Settings.parse(schema, rawSettings);
new Random(settings.seed); // seed is a string
path.stroke({ width: settings.strokeWidth }); // strokeWidth is a numberSettings.parse() parses raw values (from the CLI, a web server, or elsewhere)
to each setting's canonical type at construction, and returns an instance that
also allows typed property access. asRecord() converts an instance back to a
plain SettingsData record. merge() returns a new Settings instance and
leaves the instance it was called on unchanged.
Settings is optional: DrawFn and Vormen do not depend on it. Drawings
receive their settings as a SettingsData record
(Record<string, string | number | boolean>) and can parse them with
Settings.parse(), their own implementation, or use them directly.
The project uses Deno for development, with just as the common entry point for
tasks. Install both with just install (or install just with your package
manager and Deno from deno.com). Run all checks with
just check-all, or just --list to see all available tasks.
Releases are built by CI. To publish one, run just version patch (or minor
or major) on a clean main, then just version-push to push the version
commit and its tag. Only pushing a tag that starts with v (for example
v1.2.3) triggers a release.
- Finalize all the Math tools, primitives and other utilities
- Support for custom Shapes that can be re-used and output (larger) svg structures
- Ergonomic abstractions over bezier paths and other complex organic shapes
- Ergonomic abstractions over layers
- Predefined common and often used filters, gradients, masks, and patterns
- Ability to use and run with
node,yarn,npxandbun, not just deno - Debug rendering. E.g. Grid already has some beginning.
- Utilities like "simplify" and "optimize" through external tools allowing for smaller svg files
- Ergonomic CSS abstractions
- Animation through CSS transforms
- Animation and interaction through javascript in the SVG
- Not meant to create svg in a browser or web app. Plenty of libraries like svg.js, d3.js, etc. already do that.
- Not meant to be a full graphics library. It's meant to be a tool to create svg files.
- No support for shaders, sound, video, etc. For that, you are free to import any other dependency in a drawing file.