heerich.js A tiny engine for 3D voxel scenes rendered to SVG
heerich.js is a minimalist JavaScript engine that constructs 3D voxel compositions and distills them into pristine SVG. By extruding volumes, carving negative space, and applying boolean operations, you wield a programmatic chisel—projecting complex spatial arrangements into a flat, resolution-independent vector canvas.
While SVG trades raw frame-rates for architectural elegance, its integration with the DOM is profound. The resulting geometry sits natively within the browser, inviting manipulation through CSS and uncompromised infinite scaling. The output is not an ephemeral pixel buffer, but semantic, stylable, and enduring markup.
The visual language draws deep inspiration from the geometric rigor of Erwin Heerich (1922–2004) — exploring stacked topologies, deliberate subtractions, and the quiet tension that exists between solid mass and absolute void.
This exhibition serves as both a technical manual and an interactive gallery. Engage with the examples below to comprehend the mechanics of the engine one primitive at a time.
Creating an engine
A Heerich instance holds a 3D grid of voxels and knows
how to project them into 2D SVG. You initialize one by providing grid
dimensions, a rendering tile size in pixels, and an optional baseline
style applied universally across the active sequence.
import { Heerich } from './src/heerich.js'
const heerich = new Heerich({
tile: [30, 30], // pixel size per tile
camera: { type: 'oblique', angle: 315, distance: 25 },
style: { fill: '#ddd', stroke: 'var(--stroke-c)', strokeWidth: 0.5 }
})
All options are optional — new Heerich() works with
sensible defaults. Call heerich.toSVG() to get an SVG
string you can drop into the DOM.
The SVG is automatically centered — the engine computes the exact
bounding box of all visible geometry and sets the
viewBox to fit it, with optional padding. No manual
positioning needed, even when shapes extend into negative coordinates.
// Auto-centered with padding
document.body.innerHTML = heerich.toSVG({ padding: 30 })
// Access raw bounds if you need them
const { x, y, w, h } = heerich.getViewBoxBounds()
Boxes
The most fundamental primitive is a box — a rectangular volume anchored by a corner position and dimensions. Every voxel inside it inherits the default style.
heerich.addBox({ position: [0, 0, 0], size: [6, 4, 3] })
Drag the sliders below — the engine clears and rebuilds the scene on
every frame. Because getFaces() utilizes a dirty-flag
cache, calculating rapid structural changes remains highly
efficient.
Coordinates & alignment
The system intentionally avoids a generic 'ground plane'. Instead,
position always anchors to the minimum corner of the
shape's bounds. To align objects of different dimensions on a shared
surface, you handle the offsets manually.
const big = [6, 6, 6], small = [2, 2, 2]
// Align max corners (tops flush)
const max = big.map((b, i) => b - small[i])
// Center (works perfectly when difference is even)
const center = big.map((b, i) => (b - small[i]) / 2)
This requires just one simple subtraction per axis, keeping the engine minimal and leaving you in complete control. For pixel-perfect centering on an integer grid, aim for even-numbered coordinate differences.
Spheres
addSphere() plots a rounded volume using an internal
distance check. Using fractional offsets like .5 for
both radius and center (e.g., center: 3.5, radius:
3.5) tends to produce the cleanest symmetrical forms on
integer grids.
heerich.addSphere({ center: [4.5, 4.5, 4.5], radius: 4.5 })
Conversely, removeSphere() carves a hollow void using
the exact same parameters. Layer a sphere subtraction into a solid
box to effortlessly create arched doorways or dome cutouts.
Lines
addLine() plots a continuous voxelized path between two
spatial coordinates. You can thicken the stroke via
radius, and alter its joint geometry via
shape — choosing either 'rounded' (a
sequence of overlapping spheres) or
'square' (overlapping cubes).
heerich.addLine({
from: [0, 0, 0], to: [8, 8, 8],
radius: 1.5, shape: 'rounded'
})
Custom shapes
addWhere() is the engine's architectural bedrock
— allowing you to define arbitrary geometry as an evaluation
function over (x, y, z)
coordinates. Under the hood, boxes, spheres, and lines are merely
convenient wrappers around this core method.
heerich.addWhere({
bounds: [[0, 0, 0], [s, s, s]],
test: (x, y, z) => {
const c = Math.ceil(s / 4)
const nearEdge = [x, y, z].filter(
v => v < c || v >= s - c
).length
return nearEdge < 3
}
})
The test method evaluates every coordinate within the
specified bounds — returning
true spawns a voxel. All standard properties (like
style, scale, or mode)
continue to apply.
Boolean operations
Every shape primitive accepts a mode property dictating
how it weaves into the existing voxel grid:
- union — add voxels (default)
- subtract — remove voxels
- intersect — keep only the overlap
- exclude — XOR, toggle each voxel
heerich.addBox({ position: [0, 0, 0], size: [6, 6, 6] })
// Carve a sphere out of the box
heerich.addSphere({
center: [3, 3, 3], radius: 3,
mode: 'subtract'
})
// Keep only where box and sphere overlap
heerich.addSphere({
center: [3, 3, 3], radius: 3,
mode: 'intersect'
})
removeBox() and removeSphere() still work
— they're shortcuts for mode: 'subtract'.
Rotation
Every primitive accepts a rotate configuration mapped
to precise 90° increments. Simply specify a rotation
axis and the count of quarter-turns. By
default, shapes pivot around their local center, but arbitrary
custom origins are seamlessly supported.
// Build one arm, rotate a copy to make an L
heerich.addBox({ position: [0, 0, 0], size: [2, 8, 2] })
heerich.addBox({
position: [0, 0, 0], size: [2, 8, 2],
rotate: { axis: 'z', turns: 1 }
})
// Rotate all existing voxels in place
heerich.rotate({ axis: 'y', turns: 2 })
This dramatically simplifies creating symmetrical structures: construct one facet, then repeatedly rotate clones around a central pivot. (For example, the Kreuzplastik in the gallery below derives entirely from a single arm sequence rotated into a cross.)
Smooth solids
By default, every 1×1×1 unit renders its own distinct
stroke. To fuse a shape into a single, visually seamless solid,
simply assign the
stroke the exact same color as the fill,
effectively rendering the internal grid geometry invisible.
heerich.addBox({
position: [0, 0, 0], size: [4, 4, 4],
style: { default: { fill: '#0e0e0e', stroke: '#0e0e0e' } }
})
Untouched structural voxels retain their outlines, naturally producing a crisp contrast between smooth, monolithic volumes and articulated grids.
Styling faces
The engine tracks styles on a strict per-face basis:
top, front, left,
right, bottom, and back,
alongside a default fallback. You can inject these
styles during shape creation, or explicitly overwrite existing
regions later using styleBox().
heerich.addBox({
position: [0, 0, 0], size: [4, 4, 4],
style: {
default: { fill: '#eee', stroke: '#333' },
top: { fill: '#ff6666' },
front: { fill: '#6666ff' },
right: { fill: '#66ff66' }
}
})
Crucially, styleBox() applies localized 'paint' to
existing
geometry without fabricating new voxels — ideal for shading specific
cross-sections after the overall volume is finalized.
SVG style properties
Internal style objects map perfectly to native SVG attributes. If
SVG natively understands a property, you can pass it directly. This
unleashes the full rendering spec — enabling opacity,
dotted edges via strokeDasharray, rounded
strokeLinecap margins, or any other
presentation attribute.
heerich.addBox({
position: [0, 0, 0], size: [5, 5, 5],
style: {
default: {
fill: 'var(--fill)',
stroke: 'var(--stroke-c)',
strokeWidth: 'var(--stroke-w)',
opacity: 0.6,
strokeDasharray: '4 2'
}
}
})
Because style values resolve as strings, you can seamlessly pass
native CSS
var() references. That strategy powers the interactive
controls across this documentation: the engine natively outputs
var(--fill), and the browser resolves the cascade
against document state in real time.
Functional styles
Instead of static objects, styles can be assigned as pure computing
functions tracking over (x, y, z) parameters.
This effortlessly powers cascading gradients, periodic patterns, or
geometry-dependent shading rules — bypassing the need to iterate
through individual voxels manually.
heerich.addBox({
position: [0, 0, 0], size: [8, 8, 8],
style: {
default: (x, y, z) => {
const L = 0.4 + (y / 8) * 0.5
const C = 0.05 + (z / 8) * 0.2
const H = (x / 8) * 360
return {
fill: `oklch(${L} ${C} ${H})`,
stroke: `oklch(${L - 0.12} ${C} ${H})`
}
}
}
})
These callbacks are invoked exactly once at voxel creation. Individual faces can possess their own isolated functions — letting you freely cross-pollinate static hues with calculated spectral shifts.
Voxel scaling
Every single voxel supports an internal scale — an
[x, y, z] envelope mapped from 0 to 1 that collapses
the volume along internal axes. Scaled geometry automatically
triggers non-opaque rendering, correctly preserving visibility for
occluded geometry layered behind it.
// Static — same scale for every voxel
heerich.addBox({
position: [0, 0, 0],
size: [1, 1, 1],
scale: [1, 0.5, 1],
scaleOrigin: [0.5, 1, 0.5]
})
// Functional — scale varies by position
heerich.addBox({
position: [0, 0, 0],
size: [4, 4, 4],
scale: (x, y, z) => [1, 1 - y * 0.2, 1],
scaleOrigin: [0.5, 1, 0.5]
})
The scaleOrigin dictates the spatial pivot for this
scaling inside the 1×1×1 unit block — for example,
[0.5, 1, 0.5]
anchors geometry strictly to the bottom-center, creating natural
staircase topologies. As with styles, both vectors seamlessly accept
functional callbacks for dynamic parameter shifting.
Functional scale
As implied, evaluating scale as an
(x, y, z) function empowers you to calculate
spatial decay based exclusively on local position. You can instantly
engineer complex tapers, rounded organic falloffs, or gracefully
staggered architectural elements without stepping outside a single
generative routine.
heerich.addBox({
position: [0, 0, 0], size: [s, s, s],
scale: (x, y, z) => {
const t = 1 - y / s
const f = 1 - t * taper
return [f, 1, f]
},
scaleOrigin: [0.5, 1, 0.5]
})
Return null from the function to leave a voxel at full
size.
Camera & projection
The rendering pipeline natively exposes two distinct geometric projections. Oblique implements parallel projection where depth angles and offset scales are manually enforced — perfect for hyper-clean orthographic aesthetics. Perspective leans on classical single-vanishing-point math anchored by an abstract camera position.
// Oblique (parallel)
heerich.setCamera({ type: 'oblique', angle: 315, distance: 25 })
// Perspective (1-point)
heerich.setCamera({ type: 'perspective', position: [5, 5], distance: 12 })
Look for the floating Settings panel (typically in the top right). Swapping to perspective, adjusting camera elevations, or shifting base hues applies sweeping, uninterrupted recalculations instantly across the entire documentation.
In perspective mode, the Angle slider moves the camera's X position, and very low distances can clip geometry behind the camera.
Content voxels
You can embed pure SVG arbitrarily into volumetric cells by
injecting a literal
content string. The engine intercepts these strings,
seamlessly calculates their projected 2D location alongside other
faces, enforces a strict Z-level depth sort, and encases everything
cleanly inside an integrated <g> element.
heerich.addBox({
position: [3, 0, 6], size: [1, 1, 1],
opaque: false,
content: `<text
font-family="Aboreto"
font-size="20"
text-anchor="middle"
dominant-baseline="central"
>A</text>`
})
Ensuring opaque: false explicitly directs neighboring
elements to render their intersection faces accurately. Otherwise,
neighbors would falsely assume a solid structure is blocking their
boundaries.
The content is wrapped in a <g> with
transform="translate(x, y) scale(s)" and CSS custom
properties you can use for sizing:
--x,--y— projected 2D position--z— original z coordinate-
--scale— perspective foreshortening (1 in oblique) --tile— tile size in pixels
Transparent voxels
Set opaque: false and the voxel itself remains visible,
but adjacent voxels treat it as empty space — forcing their inner
faces to be drawn. Combine this with a transparent fill to construct
wireframe cages, bounding volumes, or see-through sculptural guides
anchored cleanly over solid geometry.
// Wireframe cage over a solid core
heerich.addBox({ position: [1, 1, 1], size: [3, 3, 3] })
heerich.addBox({
position: [0, 0, 0], size: [5, 5, 5],
opaque: false,
style: { default: { fill: 'none' } }
})
Querying voxels
A robust set of internal methods query localized voxel boundaries. This is fundamental for evaluating complex procedural decorations, handling adjacency logic, or driving edge-aware shading passes.
// Color voxels by how exposed they are
heerich.forEach((voxel, pos) => {
const n = heerich.getNeighbors(pos)
const exposed = Object.values(n)
.filter(v => !v).length
// exposed = number of open faces (0–6)
})
The getNeighbors() query explicitly retrieves adjacent
solid geometry across the six cardinal planes. In the interactive
demo, individual cubes compute their overall exposure count —
completely encased shapes remain flat, while openly protruding forms
inherit high-contrast luminance.
heerich.hasVoxel([3, 4, 5]) // boolean
heerich.getVoxel([3, 4, 5]) // voxel data or null
heerich.forEach((voxel, pos) => { ... })
// Serialization
const json = heerich.toJSON()
const copy = Heerich.fromJSON(json)
Animation
The heerich.js architecture strictly separates state
from timing — animation is driven entirely externally, hooking
directly into your own requestAnimationFrame loop. By
interpolating a fractional value and redrawing the active sequence
per frame, standard voxel functions progressively toggle discrete
coordinates — yielding snappy, pixel-aligned deconstruction.
const holes = [
{ x: 1, y: 1, targetDepth: 5 },
{ x: 5, y: 2, targetDepth: 4 },
]
function frame(now) {
heerich.clear()
heerich.addBox(...)
holes.forEach((h, i) => {
const t = ease(elapsed / duration)
heerich.removeBox({
position: [h.x, h.y, 0],
size: [3, 3, Math.round(t * h.targetDepth)]
})
})
container.innerHTML = heerich.toSVG()
if (!done) requestAnimationFrame(frame)
}
Applying a stagger offset means each hole strictly starts animating instantly after the previous one, rendering a beautifully cascading reveal.
Putting it all together
A dynamic, procedurally generated composition combining boxes, boolean carving, and functional styles. Recessed voids plunge into a full-width block, revealing depth-shaded walls, while towers steadily rise from within. Fractional voxel scaling smooths the transitions between integer depths, allowing layers to slide smoothly into place rather than abruptly snapping. Click the scene to regenerate.
After Heerich
Exacting recreations of physical cardboard and brass sculptures designed by Erwin Heerich, synthesized entirely in code using this engine.
Kreuzplastik (Brass Cross)
Schachbrett (Checkerboard)
Kartonplastik (Stepped Block)