npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

heerich

v0.6.6

Published

A tiny engine for 3D voxel scenes rendered to SVG

Downloads

1,285

Readme

heerich.js

A tiny engine for 3D voxel scenes rendered to SVG. Build shapes with CSG-like boolean operations, style individual faces, and output crisp vector graphics — no WebGL, no canvas, just <svg>.

Named after Erwin Heerich, the German sculptor known for geometric cardboard sculptures.

Install

npm install heerich
import { Heerich } from 'heerich'

Or use the UMD build via a <script> tag — the global Heerich will be available.

Quick Start

import { Heerich } from 'heerich'

const h = new Heerich({
  tile: 40,
  camera: { type: 'oblique', angle: 45, distance: 15 },
})

// A simple house
h.applyGeometry({ type: 'box', position: [0, 0, 0], size: [5, 4, 5], style: {
  default: { fill: '#e8d4b8', stroke: '#333' },
  top:     { fill: '#c94c3a' },
}})

// Carve out a door
h.removeGeometry({
  type: 'box',
  position: [2, 1, 0],
  size: [1, 3, 1]
})

document.body.innerHTML = h.toSVG()

Camera

Two projection modes are available:

// Oblique (default) — classic pixel-art look
const h = new Heerich({
  camera: { type: 'oblique', angle: 45, distance: 15 }
})

// Perspective — vanishing-point projection
const h = new Heerich({
  camera: { type: 'perspective', position: [5, 5], distance: 10 }
})

// Update camera at any time
h.setCamera({ angle: 30, distance: 20 })

Shapes

All shape methods accept a common set of options:

| Option | Type | Description | |-----------|------|-------------| | mode | 'union' | 'subtract' | 'intersect' | 'exclude' | Boolean operation (default: 'union') | | style | object or function | Per-face styles (see Styling) | | content | string | Raw SVG content to render instead of polygon faces | | opaque | boolean | Whether this voxel occludes neighbors (default: true) | | meta | object | Key/value pairs emitted as data-* attributes on SVG polygons | | rotate | object | Rotate coordinates before placement (see Rotation) | | scale | [x, y, z] or (x, y, z) => [sx, sy, sz] | Per-axis scale 0–1 (auto-sets opaque: false) | | scaleOrigin | [x, y, z] or (x, y, z) => [ox, oy, oz] | Scale anchor within the voxel cell (default: [0.5, 0, 0.5]) |

Convenience methods

  • addGeometry(opts) — shortcut for applyGeometry({ ...opts, mode: 'union' })
  • removeGeometry(opts) — shortcut for applyGeometry({ ...opts, mode: 'subtract' })

Uniform positioning

Box, sphere, and fill all accept both position (min-corner) and center (geometric center) — the engine converts between them automatically based on the shape's size:

// These are equivalent for a 5×5×5 box:
h.applyGeometry({ type: 'box', position: [0, 0, 0], size: 5 })
h.applyGeometry({ type: 'box', center: [2, 2, 2], size: 5 })

// These are equivalent for a sphere with radius 3:
h.applyGeometry({ type: 'sphere', center: [3, 3, 3], radius: 3 })
h.applyGeometry({ type: 'sphere', position: [0, 0, 0], radius: 3 })
h.applyGeometry({ type: 'sphere', center: [3, 3, 3], size: 7 })

Fill also accepts position/center + size as an alternative to bounds.

Box

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

// Style the carved walls (optional)
h.removeGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 1,
  style: { default: { fill: '#222' } }
})

Sphere

h.applyGeometry({
  type: 'sphere',
  center: [5, 5, 5],
  radius: 3
})
h.removeGeometry({
  type: 'sphere',
  center: [5, 5, 5],
  radius: 1.5
})

// Style the carved walls (optional)
h.removeGeometry({
  type: 'sphere',
  center: [5, 5, 5],
  radius: 1,
  style: { default: { fill: '#222' } }
})

Line

Lines are the only shape that uses different positioning — from/to instead of position/center + size:

h.applyGeometry({
  type: 'line',
  from: [0, 0, 0],
  to: [10, 5, 0]
})

// Thick rounded line
h.applyGeometry({
  type: 'line',
  from: [0, 0, 0],
  to: [10, 0, 0],
  radius: 2,
  shape: 'rounded'
})

// Thick square line
h.applyGeometry({
  type: 'line',
  from: [0, 0, 0],
  to: [0, 10, 0],
  radius: 1,
  shape: 'square'
})

h.removeGeometry({
  type: 'line',
  from: [3, 0, 0],
  to: [7, 0, 0]
})

Custom Shapes

applyGeometry with type: 'fill' is the general-purpose shape primitive — define any shape as a function of (x, y, z). Boxes, spheres, and lines are just convenience wrappers around this pattern.

// Hollow sphere
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
  }
})

// Torus
h.applyGeometry({
  type: 'fill',
  bounds: [[-8, -3, -8], [8, 3, 8]],
  test: (x, y, z) => {
    const R = 6, r = 2
    const q = Math.sqrt(x*x + z*z) - R
    return q*q + y*y <= r*r
  }
})

h.removeGeometry({
  type: 'fill',
  bounds: [[0, -6, -6], [6, 6, 6]],
  test: () => true
})

Combine with functional scale and style for fully procedural shapes — closest thing to a voxel shader.

Boolean Operations

All shape methods support a mode option for CSG-like operations:

// Union (default) — add voxels
h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 5
})

// Subtract — carve out voxels
h.applyGeometry({
  type: 'sphere',
  center: [2, 2, 2],
  radius: 2,
  mode: 'subtract'
})

// Intersect — keep only the overlap
h.applyGeometry({
  type: 'box',
  position: [1, 1, 1],
  size: 3,
  mode: 'intersect'
})

// Exclude — XOR: add where empty, remove where occupied
h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 5,
  mode: 'exclude'
})

Styling carved faces

When removing voxels, you can pass a style to color the newly exposed faces of neighboring voxels — the "walls" of the carved hole:

h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 10
})

// Carve a hole with dark walls
h.removeGeometry({
  type: 'box',
  position: [3, 3, 0],
  size: [4, 4, 5],
  style: { default: { fill: '#222', stroke: '#111' } }
})

This works on removeGeometry (with any type) and on applyGeometry with mode: 'subtract'. Without a style, subtract behaves as before — just deleting voxels.

Styling

Styles are set per face name: default, top, bottom, left, right, front, back. Each face style is an object with SVG presentation attributes (fill, stroke, strokeWidth, etc.).

h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 3,
  style: {
    default: { fill: '#6699cc', stroke: '#234' },
    top:     { fill: '#88bbee' },
    front:   { fill: '#557799' },
  }
})

Dynamic styles

Style values can be functions of (x, y, z):

h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 8,
  style: {
    default: (x, y, z) => ({
      fill: `hsl(${x * 40}, 60%, ${50 + z * 5}%)`,
      stroke: '#222',
    })
  }
})

Restyling

Restyle existing voxels without adding or removing them:

h.applyStyle({
  type: 'box',
  position: [0, 0, 0],
  size: 3,
  style: { top: { fill: 'red' } }
})
h.applyStyle({
  type: 'sphere',
  center: [5, 5, 5],
  radius: 2,
  style: { default: { fill: 'gold' } }
})
h.applyStyle({
  type: 'line',
  from: [0, 0, 0],
  to: [10, 0, 0],
  radius: 1,
  style: { default: { fill: 'blue' } }
})

Voxel Scaling

Shrink individual voxels along any axis. Scaled voxels automatically become non-opaque, revealing neighbors behind them.

// Static — same scale for every voxel
h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 1,
  scale: [1, 0.5, 1],
  scaleOrigin: [0.5, 1, 0.5]
})

// Functional — scale varies by position
h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: 4,
  scale: (x, y, z) => [1, 1 - y * 0.2, 1],
  scaleOrigin: [0.5, 1, 0.5]
})

The scaleOrigin sets where scaling anchors within the voxel cell (0–1 per axis). [0.5, 1, 0.5] pins to the bottom-center (floor), [0.5, 0, 0.5] pins to the top-center (ceiling). Both scale and scaleOrigin accept functions of (x, y, z) for per-voxel control. Return null from a scale function to leave that voxel at full size.

Rotation

Rotate coordinates by 90-degree increments before or after placement:

// Rotate a shape before placing it
h.applyGeometry({
  type: 'box',
  position: [0, 0, 0],
  size: [5, 1, 3],
  rotate: { axis: 'z', turns: 1 }
})

// Rotate all existing voxels in place
h.rotate({ axis: 'y', turns: 2 })

// With explicit center
h.rotate({ axis: 'x', turns: 1, center: [5, 5, 5] })

Rendering

toSVG(options?)

Render the scene to an SVG string:

const svg = h.toSVG()
const svg = h.toSVG({ padding: 40 })
const svg = h.toSVG({ viewBox: [0, 0, 800, 600] })

Options:

| Option | Type | Description | |--------|------|-------------| | padding | number | ViewBox padding in px (default: 20) | | faces | Face[] | Pre-computed faces (skips internal rendering) | | viewBox | [x,y,w,h] | Custom viewBox override | | offset | [x,y] | Translate all geometry | | prepend | string | Raw SVG inserted before faces | | append | string | Raw SVG inserted after faces | | faceAttributes | function | Per-face attribute callback |

Use prepend and append to inject SVG filters for effects like cel-shaded outlines:

const svg = h.toSVG({
  prepend: `<defs><filter id="cel">
    <feMorphology in="SourceAlpha" operator="dilate" radius="2" result="thick"/>
    <feFlood flood-color="#000"/>
    <feComposite in2="thick" operator="in" result="border"/>
    <feMerge><feMergeNode in="border"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter></defs><g filter="url(#cel)">`,
  append: `</g>`,
})

Every polygon gets data attributes for interactivity:

<... data-voxel="x,y,z"  data-x="x"  data-y="y"  data-z="z"  data-face="top" ../>

Voxels with a meta object get additional data-* attributes.

getFaces() / renderTest(opts)

Get the projected 2D face array directly (for custom renderers or Canvas output):

// From stored voxels
const faces = h.getFaces()

// Stateless — from a test function, no voxels stored
const faces = h.renderTest({
  bounds: [[-10, -10, -10], [10, 10, 10]],
  test: (x, y, z) => x*x + y*y + z*z <= 100,
  style: (x, y, z, faceName) => ({ fill: faceName === 'top' ? '#fff' : '#ccc' })
})

// Render pre-computed faces
const svg = h.toSVG({ faces })

Custom Renderers

getFaces() returns everything you need to build your own renderer. Each face has:

  • face.points — projected 2D coordinates (flat array via face.points.data: [x0, y0, x1, y1, ...])
  • face.style — resolved style object (fill, stroke, strokeWidth, etc.)
  • face.type — face direction ('top', 'front', 'right', etc.) or 'content'
  • face.voxel — source voxel with x, y, z, and optional meta
  • face.depth — depth value (array is already sorted back-to-front)
const faces = h.getFaces()

for (const face of faces) {
  if (face.type === 'content') continue
  const d = face.points.data
  // d = [x0, y0, x1, y1, x2, y2, x3, y3] — four corners of a quad
  ctx.beginPath()
  ctx.moveTo(d[0], d[1])
  ctx.lineTo(d[2], d[3])
  ctx.lineTo(d[4], d[5])
  ctx.lineTo(d[6], d[7])
  ctx.closePath()
  ctx.fillStyle = face.style.fill
  ctx.fill()
}

getBounds(padding?, faces?)

Compute the 2D bounding box of the rendered geometry:

const { x, y, w, h } = h.getBounds()
const padded = h.getBounds(30)

Content Voxels

Embed arbitrary SVG at a voxel position (depth-sorted with the rest of the scene):

h.applyGeometry({
  type: 'box',
  position: [3, 0, 3],
  size: 1,
  content: '<text font-size="12" text-anchor="middle">Hi</text>',
  opaque: false,
})

Content voxels receive CSS custom properties --x, --y, --z, --scale, --tile for positioning.

Querying

h.getVoxel([2, 3, 1])       // voxel data or null
h.hasVoxel([2, 3, 1])       // boolean
h.getNeighbors([2, 3, 1])   // { top, bottom, left, right, front, back }
for (const voxel of h) { /* voxel.x, voxel.y, voxel.z, voxel.styles, ... */ }

Serialization

const data = h.toJSON()
const json = JSON.stringify(data)

const h2 = Heerich.fromJSON(JSON.parse(json))

Note: functional styles (callbacks) cannot be serialized and will be omitted with a console warning.

Coordinate System

  • X — horizontal (left/right)
  • Y — vertical (up/down). Note: Y increases downward, originating from SVG/DOM screen space.
  • Z — depth (front/back).

Common 3D "Gotchas"

Because the engine outputs standard SVG graphics and relies on Oblique projections, its grid behaves slightly differently than classic WebGL or mathematical 3D setups:

  1. Y Pointing Down: Setting a voxel at y: -4 places it above the origin, and y: 4 places it below the origin in standard rendering.
  2. Oblique Z-Offset: At the default angle of 315° (pointing up and left visually), the Z-axis projects horizontally and vertically on screen.
  3. The "Front" Quadrant: Due to this isometric-style camera offset and Painter's Algorithm sorting, the closest visual corner pointing toward the camera is the [-X, -Y, -Z] (Negative) octant, not [+X, +Y, +Z] (Positive) as one might expect. Carving out the "front" of a block to expose the inside means subtracting negative values.

Valid voxel coordinate bounds range from -512 to 511 on each axis.

Acknowledgements

Shape calculations for lines and spheres are based on the excellent guides by Red Blob Games:

License

MIT © 2026 David Aerne