Skip to content
berkesPublic

About

A set of tools and libraries for generative art in SVG

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Vormen

Vormen is a bag of tools, modules and scripts for creating generative art in SVG using Javascript

Features

  • 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.

Quickstart

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

readme-example (This is generated with examples/cubic-disarray.ts)

Or output to stdout.

deno run drawing.ts render --outfile - > drawing.svg

Or override settings

deno run drawing.ts render --outfile drawing.svg --settings.rotationStrength=10

Examples

The 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-examples

Pick and Choose

The 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.

Examples

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.

Modules

Warning

Modules documentation is WIP. So incomplete and probably out of date

Vormen

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 serve will 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.svg will render the svg to the file. Additionally, any settings can be overridden with --settings.theSettingName=bar to 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.

Drawing

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.

Metadata

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.

Typography

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() returns

addTo(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.

Grid

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.

Math

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.

Primitives

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), and pointAtDistanceTowards() (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), and dist() (distance between vectors), as well as linear interpolation via Vector.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);

Path

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.

Settings

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 number

Settings.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.

Development

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

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.

Future

  • 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, npx and bun, 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

No-Goals

  • 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.

About

A set of tools and libraries for generative art in SVG

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages