Pixel art has rules: no orphan pixels, no undersized clusters, regular slopes, outlines that are never pure black. Following them by hand takes an eye and hours.
This engine follows them for you. You pick a silhouette, a corner treatment, a fill; it derives a correct shape.
It powers PixelDraw, and knows nothing about React, the DOM or the network — it runs in a browser and under Node alike.
It is not a 2D game engine. There is no render loop, no scene graph, no sprites, no input handling. It takes a description and returns a grid of colours; what you do with that grid is yours.
npm install @rasterie/engineimport { renderComposition, surfaceToSvg, blankTheme } from "@rasterie/engine";
const surface = renderComposition(composition, blankTheme());
const svg = surfaceToSvg(surface, 8);No canvas involved. surfaceToPng encodes a PNG by hand, so images can
be generated from a script.
A piece is not a drawing but a recipe:
type ShapeRecipe = {
silhouette: "rect" | "diamond" | "shield" | "hexagon" | "capsule" | "banner" | "cross";
corner: "square" | "cut" | "round" | "spike" | "notch";
cornerSize: number;
fill: "solid" | "hollow" | "double" | "half";
thickness: number;
};Seven silhouettes × five corners × four fills = 140 recipes, before proportions, rotation and tint.
| Rule | What it prevents |
|---|---|
| Clusters of at least 10 pixels | Isolated noise |
| No orphan pixels (8-connectivity) | Stray dots left by an outline |
| Canonical slopes 1:1, 1:2, 2:1, 1:3, 3:1 | Ragged staircases |
| Outlines never pure black | The sticker look |
| Bayer dithering in 2×2 blocks | Gradients turning to noise |
validate can audit a finished surface and report what still looks wrong.
Ramps are computed in OKLCH, a perceptually uniform space:
import { generateRamp } from "@rasterie/engine/color";
const ramp = generateRamp("#c85a28", {
stops: 5,
hueShiftDark: 24, // shadows drift cold
hueShiftLight: 18, // highlights drift warm
});That hue shift is what separates living shading from plain darkening.
Erasing carves the shape, in local coordinates — it does not mask a canvas:
type Piece = {
/** Erased pixels, relative to the piece's top-left corner. */
erased?: Record<string, true>;
/** Arrival rank: an erase only cuts through what precedes it. */
epoch: number;
};The hole follows the piece when it moves, and shows in its thumbnail.
Import everything from the root, or one domain at a time:
| Entry | What it holds |
|---|---|
@rasterie/engine |
Everything |
…/core |
Surfaces, bevels, nine-slice |
…/color |
OKLCH ramps, harmonies, materials |
…/shapes |
Shape grammar, sparkles |
…/compose |
Pieces into compositions, thumbnails |
…/minecraft |
Minecraft GUI screens, pixel-accurate |
…/paint |
Freehand edits, selection |
…/validate |
Quality audit |
…/export |
PNG, SVG, palettes, metadata |
…/style |
Themes |
The engine returns a Surface — a flat array of colours — and stops
there. Painting it is the caller's job, which keeps the package free of
any DOM dependency:
function paint(context: CanvasRenderingContext2D, surface: Surface, scale: number) {
for (let y = 0; y < surface.height; y += 1) {
for (let x = 0; x < surface.width; x += 1) {
const colour = getPixel(surface, x, y);
if (!colour) continue;
context.fillStyle = colour;
context.fillRect(x * scale, y * scale, scale, scale);
}
}
}167 tests, 85 % line coverage. The pixel art rules are checked across 840 renders: the grammar's 140 recipes in each of six themes.
pnpm test
pnpm exec vitest run --coverageMIT. Use it in anything, including closed and commercial work.
The application it comes from, PixelDraw, is AGPL — a different licence for a different purpose.