# heerich renders voxel scenes to SVG without a canvas

> heerich is a small JavaScript engine that turns a 3D voxel scene into an SVG document, using boolean operations to build shapes, per-face styling to colour them, and one of four projection types to view them. It has no runtime dependencies and no WebGL, so the output is a vector file you can diff, version and print at any size.

**meodai/heerich** — Tiny engine for 3D voxel scenes rendered to SVG — boolean ops, oblique/perspective cameras, zero dependencies. Inspired by the geometric cardboard sculptures of Erwin Heerich.

- Repository: https://github.com/meodai/heerich
- Website: https://meodai.github.io/heerich/
- Stars: 628 · Forks: 16
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/meodai-heerich

## Vector output, and no rendering dependency at all

heerich is described in one sentence: a tiny engine for 3D voxel scenes rendered to SVG, where you build shapes with CSG-like boolean operations, style individual faces, and output crisp vector graphics with no WebGL and no canvas, just an svg element.

That last clause is the whole design. A WebGL renderer gives you pixels and a camera and takes over your pipeline. An SVG renderer gives you a document, and a document can be diffed in a pull request, committed to a repository, embedded in a static site or printed at poster size without a re-render. For the kind of work the library is aimed at, which the topic tags describe as generative art and isometric graphics, that difference decides the workflow.

The package name is heerich, and it is installed from npm:

```bash
npm install heerich
```

and imported as a named export:

```js
import { Heerich } from 'heerich'
```

There is also a UMD build for use through a script tag, in which case the global Heerich becomes available. The package metadata confirms both delivery routes, with a module entry for import and a CommonJS entry for require, plus a types entry pointing at the generated declaration file.

The name comes from Erwin Heerich, the German sculptor known for geometric cardboard sculptures, and that reference explains the aesthetic the defaults aim at: flat faces, visible edges, the look of stacked card rather than a rendered solid. The project is MIT licensed, and the homepage is a GitHub Pages site built from the site directory in the repository.

## Four cameras, one angle parameter that means different things

The camera is where most of the surprise lives, because the engine exposes four projection types while sharing a single angle parameter across them.

The default is oblique, a parallel projection where depth recedes at a configurable angle, described as the classic pixel-art or cabinet-projection look. Then there is perspective, a single-vanishing-point projection with an explicit camera position. Orthographic is a true 3D parallel projection with configurable pan, called angle, and tilt, called pitch, which the README says you use for dimetric, trimetric or any custom orthographic view. Isometric is an orthographic preset with the pitch locked to 35.264 degrees, giving the classic isometric diamond grid.

The angle parameter is where care is needed. For oblique it controls the direction the depth axis recedes and the default is 45 degrees. For orthographic it is the horizontal rotation around the scene, also defaulting to 45 degrees. For isometric it is again the horizontal rotation, and the README lists 45, 135, 225 and 315 degrees as the recommended values, because those are the orientations where edges align to the pixel grid. Any value works for isometric, but the alignment is what makes the output look deliberate.

For perspective, angle is mapped to the camera X position and has no default listed. The practical advice is to treat angle as the one control you keep while switching modes, and to set the rest per mode:

```js
new Heerich({ camera: { type: 'isometric', angle: 45 } })
```

One behaviour is worth memorising: the orthographic and isometric modes use parallel projection, so the distance setting has no effect in those modes. The camera can also be changed at any time with setCamera, for example with an angle of 30 and a distance of 20, which is how you animate a turntable without rebuilding the scene.

## Boolean modes do the modelling

Every shape method accepts a mode option, and the README describes the four values as boolean operations with union as the default. Union adds voxels, subtract carves them out, intersect keeps only the overlap, and exclude is the fourth.

Two convenience methods wrap the most common cases. addGeometry is a shortcut for applying geometry with mode set to union, and removeGeometry is the shortcut for subtract. The quick start uses exactly this pair, applying a box to make a house and then removing a smaller box to carve out a door:

```js
h.removeGeometry({
  type: 'box',
  position: [2, 1, 0],
  size: [1, 3, 1]
})
```

The subtraction is what makes the engine usable for architecture, since a door is not something you model as a positive shape, it is something you cut. The same holds for windows, arches and interior rooms, and the README shows the equivalent for spheres, where removing a smaller sphere from a larger one produces a hollow shell with a styled interior wall.

Positioning is forgiving in a way that saves argument order mistakes. Box, sphere and fill all accept either position, which is the minimum corner, or center, which is the geometric center, and the engine converts between them based on the shape's size. The README spells out the equivalences: for a 5 by 5 by 5 box, position at the origin is the same as center at 2, 2, 2, and for a sphere of radius 3, a center at 3, 3, 3 is the same as a position at the origin, and a sphere with center 3, 3, 3 and size 7 is the same shape again. Fill also accepts position or center with size as an alternative to explicit bounds.

The result is that the shape of the data in your code is not a source of silent mistakes, which matters more than it sounds when you are cutting holes in a model by hand.

## fill is the real primitive, and boxes and spheres are wrappers

The general-purpose shape is applyGeometry with type fill, and the README is blunt about the hierarchy: boxes, spheres and lines are just convenience wrappers around this pattern. A fill shape is defined as a function of the coordinates, given bounds and a test that decides whether a given point is inside the shape.

A hollow sphere is a fill with a test that keeps points whose squared distance from the origin falls between two radii:

```js
h.applyGeometry({
  type: 'fill',
  bounds: [[-6, -6, -6], [6, 6, 6]],
  test: (x, y, z) => {
    const d = x*x + y*y + z*z
    return d <= 25 && d >= 16
  }
})
```

A torus is the same idea with a test that measures the distance from a ring of radius R and compares it against a tube radius r. The bounds are what let the engine know which cells to evaluate, so the shape is defined as data and the engine does the rasterisation of it into voxels.

The README notes that fill can be combined with functional scale and style for fully procedural shapes, calling it the closest thing to a voxel shader. That is an accurate description of the role it plays: it is where you write the shape function, while the engine handles the boolean algebra against the existing scene, the styling and the projection.

Lines are the exception to the positioning convention. A line uses from and to instead of position or center with size, and it can be given a radius and a shape of rounded or square, which is how a thick line gets a round or a square cross section. The README notes that lines are the only shape with different positioning, and the engine treats them as a separate primitive rather than as a stretched box.

## Per-voxel options: style, meta, scale and gap

Beyond the geometry, each shape carries a set of options that control how the resulting faces are drawn, and these are what separate a plain wireframe of cubes from something that looks designed.

The style option takes an object or a function and is where per-face styling lives, which is the mechanism behind the quick start's house example, where a default fill and stroke are given and the top face is given its own fill. The content option takes a string of raw SVG to render instead of polygon faces, which is an escape hatch for anything the polygon renderer cannot express. The opaque flag decides whether a voxel occludes its neighbours and defaults to true, and setting it to false is how you get a see-through effect.

The meta option takes key and value pairs that are emitted as data attributes on the SVG polygons. That is the hook for making the output addressable from JavaScript or from a test, and it means the geometry carries information the picture does not show.

The rotate option rotates coordinates before placement. The scale option accepts either an array of three numbers or a function returning one, applies a per-axis scale between 0 and 1, and automatically sets opaque to false as a consequence. The scaleOrigin option chooses the anchor within the voxel cell, and it defaults to the centre of the horizontal cell and the bottom of the vertical one, so scaled objects stay on the ground.

Finally, gap sets the spacing between voxels, from 0 to 0.5, and it overrides the constructor default. Gap is a small setting with a large visual effect, because it turns a solid mass into a stack of distinguishable blocks, which is the cardboard construction look the library is named after.

## The package is ESM first, with a types build and a benchmark as the test suite

The package metadata describes a project with a small, deliberate toolchain.

The package is version 0.16.0, is declared as an ES module, and ships both an ES module entry and a UMD CommonJS entry, with a types field pointing at the generated declaration file. The published files list is short: the dist directory and the source file, so consumers get the built output and one readable source file.

There are no runtime dependencies at all, which matches the promise of zero dependencies in the project description. The devDependencies are prettier, typescript and vite, and there is a tsconfig.json in the repository, so the library is written in JavaScript but ships generated type declarations, with typescript used to emit them.

The build script runs the Vite build and then tsc, and there is a separate script for building the documentation site with its own Vite config from the site directory. The dev script serves that site on a host, and the preview script previews it.

The most interesting script is the test one, because it is not what you would expect. The test script runs the benchmark, and the benchmark runs node tests/benches.js. So the test suite is a performance harness, which makes sense for an engine whose output is generated voxel by voxel, and it means the project's own definition of correctness is partly about speed. A separate format and format:check pair runs prettier over the source, the site scripts and the site styles and markup.

The repository has a docs directory, a site directory, a tests directory and a CLAUDE.md file, the last being a hint that the project expects to be worked on with coding agents, and there is no CI configuration listed at the top level, which is worth noting for a package that publishes a public site and an npm release.

## Where the voxel model and the SVG target stop you

Three limits are worth naming.

The first is the fundamental one. A voxel engine on a grid cannot represent a smooth surface, and a perspective projection of a voxel grid still looks like a voxel grid. If you need a product render, a game view or a scene with dynamic lighting and a camera you can fly, this is the wrong category of tool. The library has no material, lighting or shadow model at all, because the output is flat vector art with per-face styles, which is a feature for its purpose and a wall for any other.

The second is the size of the grid. Every fill shape is evaluated per cell within its bounds, and the number of faces in the output grows with the surface area of the scene. The README's own benchmark harness exists because this is a real concern, and if you build a large scene you should measure before you ship it, especially since the output is a single SVG document with one polygon per visible face.

The third is that there is no scene graph, animation timeline or interaction layer. You can change the camera at runtime and re-render, which is enough for an interactive turntable, but anything that needs per-frame updating means re-running the geometry pipeline and replacing the document, not animating existing nodes.

On maintenance, the record is short. The repository is not archived and the last push was on 2026-06-22, and it publishes no GitHub releases, so the version in the package metadata is the only version signal. The project is at 0.16.0, which for a library with no dependencies and a single source file is a reasonable place to be, but with no release history to consult, treat the main branch as the thing you would pin.

## Conclusion

heerich fits a team that wants to generate isometric or oblique illustrations, generative art or diagram-like scenes as vector output in a browser or a build step, and it does not fit a project that needs photorealistic or interactive 3D, since the whole model is voxels and the output is a static SVG document. Before you adopt it, decide which camera mode your artwork needs, because the shared angle parameter means different things per mode and distance is ignored in the parallel projections, and check the fill primitive if your shapes are not boxes, spheres or lines. The package is MIT licensed at version 0.16.0, and the last push was on 2026-06-22.

## FAQ

### How do I install and use heerich?

Install it with npm install heerich and import the named export with import { Heerich } from 'heerich'. You can also use the UMD build through a script tag, where the global Heerich becomes available, and then render by assigning the result of the toSVG method to your document.

### What camera projections does heerich support?

Four: oblique, which is the default parallel projection where depth recedes at an angle, perspective, a single-vanishing-point projection with an explicit camera position, orthographic, a true parallel projection with pan and pitch, and isometric, an orthographic preset with pitch locked to 35.264 degrees.

### How do I carve a hole in a shape with heerich?

Apply the shape you want to remove with mode set to subtract, or use the removeGeometry shortcut. Subtracting a smaller box from a larger one produces a cavity, and the README shows that the carved walls can be styled separately with a style option.

### Can I create a shape that is not a box, sphere or line in heerich?

Yes. applyGeometry with type fill is the general purpose primitive, where you give bounds and a test function of the coordinates, and the README notes that boxes, spheres and lines are convenience wrappers around that pattern. It also notes that fill can be combined with functional scale and style for fully procedural shapes.

### Does heerich have any runtime dependencies?

The project description says zero dependencies, and the package metadata lists no runtime dependencies. The devDependencies are prettier, typescript and vite, and the published files are the dist directory and the source file.

## Sources

- [Issues](https://github.com/meodai/heerich/issues)
- [License: MIT](https://github.com/meodai/heerich/blob/main/LICENSE)
- [meodai/heerich on GitHub](https://github.com/meodai/heerich)
- [Project website](https://meodai.github.io/heerich/)
- [README](https://github.com/meodai/heerich/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/meodai-heerich
