@slithy/prim-lib
v0.5.1
Published
Core engine for primitive-based image reconstruction.
Maintainers
Readme
@slithy/prim-lib
Core engine for primitive-based image reconstruction. Consumed by @slithy/prim-interface.
What it does
Reconstructs an image by iteratively placing geometric shapes. Each step evaluates candidate shapes, mutates them, picks the one that most reduces the difference from the target, and draws it onto the working canvas.
Exports
Classes
Canvas— wrapsHTMLCanvasElementorOffscreenCanvas(environment-detected); handles image loading, pixel reads, drawing steps, and SVG outputOptimizer— runs the step loop; callsonStepafter each shape is placed. Accepts an optionalschedulefunction (defaults torequestAnimationFrame; passfn => setTimeout(fn, 0)for worker contexts)State— holds the current canvas and its distance from the targetStep— a single candidate shape placement; computes best color and difference change
Shapes
Fixed shapes (class constructors, pass directly in shapeTypes):
Triangle,Rectangle,Ellipse,Circle,Square,Hexagon,Glyph,Debug
Configurable shapes (factory functions that return a constructor):
makeNGon(opts)— N-sided polygon; supports regular and irregular modesmakeRect(opts)— axis-aligned or rotatable rectangle with independent width/height control
shapeTypes: [Triangle, makeNGon({ sides: 6, regular: true }), makeRect({ aspectRatio: 1.78 })]Factory options
NGonOptions
interface NGonOptions {
sides: number // number of vertices (≥ 3, required)
regular?: boolean // true = equidistant vertices (default: true)
rotatable?: boolean // regular only: random rotation each instance (default: true)
startAngle?: number // regular, non-rotatable only: fixed orientation in radians
// default: -π/2 (top vertex pointing up)
noise?: number // regular only: 0–1 vertex jitter (default: 0)
convex?: boolean // irregular only: apply convex hull (default: false)
sizeRange?: [number, number] // vertex radius range in compute-space px (default: [1, 20])
// regular: radius from center; irregular: scatter radius from first point
mutationScale?: number // max mutation step size in px (default: 20)
}RectOptions
interface RectOptions {
widthRange?: [number, number] // half-width range in compute-space px (default: [5, 40])
heightRange?: [number, number] // half-height range in compute-space px (default: [5, 40])
// ignored when aspectRatio is set
aspectRatio?: number // width ÷ height; locks proportions, derives hh from hw
// e.g. 1.78 for 16:9, 1.33 for 4:3, 1.0 for square
rotatable?: boolean // random rotation per instance (default: false)
mutationScale?: number // max mutation step size in px (default: 20)
}Types
Cfg — runtime config (all fields required):
interface Cfg {
width: number // image width for computation (set by Canvas.original)
height: number // image height for computation (set by Canvas.original)
steps: number // total shapes to place
shapes: number // candidate shapes evaluated per step
mutations: number // hill-climb attempts per candidate
alpha: number // starting shape opacity
mutateAlpha: boolean // whether opacity is mutated per candidate
computeSize: number // max image dimension for pixel distance calculations
viewSize: number // max image dimension for display canvas
allowUpscale?: boolean // allow output larger than source image (default: false)
scale?: number // viewSize / computeSize ratio (set by Canvas.original)
shapeTypes: Array<new (w: number, h: number) => ShapeInterface>
shapeWeights?: number[] // per-shape selection weights (same length as shapeTypes)
fill: 'auto' | string
}PreCfg—Cfgwith optionalwidth/height; used before image dimensions are knownShapeInterface— structural interface for shapes; includestoData(alpha, color): StepDataCtx2D—CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; used for canvas operations that work in both main-thread and worker contextsBbox,Point,ImageDataLike,ShapeImageData
RGB — [number, number, number] tuple
StepData — discriminated union of serialized shape data, keyed by t:
type StepData =
| { t: 't'; a: number; c: RGB; pts: [number, number][] } // Triangle
| { t: 'r'; a: number; c: RGB; pts: [number, number][] } // Rectangle
| { t: 'p'; a: number; c: RGB; pts: [number, number][] } // makeNGon (regular and irregular)
| { t: 'e'; a: number; c: RGB; cx: number; cy: number; rx: number; ry: number } // Ellipse
| { t: 'c'; a: number; c: RGB; cx: number; cy: number; r: number } // Circle
| { t: 's'; a: number; c: RGB; cx: number; cy: number; r: number } // Square
| { t: 'h'; a: number; c: RGB; cx: number; cy: number; r: number; angle: number } // Hexagon
| { t: 'sm'; a: number; c: RGB; cx: number; cy: number; fs: number; text: string } // Glyph
| { t: 'rc'; a: number; c: RGB; cx: number; cy: number; hw: number; hh: number; angle: number } // makeRectSerializedOutput — compact, storage-ready representation of a completed run:
interface SerializedOutput {
v: 1 // schema version
w: number // compute width
h: number // compute height
scale: number // viewSize / computeSize ratio
fill: RGB // background fill color
steps: StepData[]
}ReplayResult — returned by replayOutput:
interface ReplayResult {
raster: HTMLCanvasElement | OffscreenCanvas
svg: SVGSVGElement
svgString: string
}Functions
replayOutput(data: SerializedOutput): ReplayResult — reconstructs canvas and SVG natively from serialized output, without re-running the optimizer. Useful for restoring saved results from localStorage or a database.
renderStepToCtx(data: StepData, ctx: Ctx2D): void — renders a single StepData onto a 2D canvas context. Used internally by replayOutput and by runWorker to incrementally apply worker-posted steps to the display canvas.
stepDataToSVGElement(data: StepData): SVGElement — creates a DOM SVGElement from a StepData. Used internally by replayOutput and by runWorker to build the live SVG incrementally on the main thread.
Utilities
getFill(data: ImageDataLike): string— computes a fill color (average of corner pixels) from image dataparseColor(str: string): RGB— parses"rgb(r, g, b)"to an[r, g, b]tuplestepPerf— accumulator for profiling rasterize vs pixel math time per step
Differences from primitive.js
prim-lib is derived from primitive.js, a JavaScript port of the Go primitive library by Michael Fogleman. The core algorithm — iterative shape placement via hill-climbing — is unchanged. What's different:
- TypeScript, strict mode — fully typed throughout;
strict: trueon both packages - ESM only — no CommonJS; no
importScripts() - Canvas reuse — the original creates a new
<canvas>element for every shape rasterization call (~58,000 per run at default settings).prim-libreuses a single module-level canvas, growing it only when a larger bbox is seen. This delivered a ~27× speedup (37 s → ~0.8 s rasterize time per run) willReadFrequently: true— canvas contexts used forgetImageDataare created with this flag, keeping pixel data in CPU memory and suppressing browser warnings- Worker-compatible —
Canvasdetects its environment: in a browser context it createsHTMLCanvasElement; in a worker it createsOffscreenCanvas(same 2D API).Canvas.fromBitmap()loads an image from anImageBitmap(worker-compatible) rather thannew Image().Optimizeraccepts an injectableschedulefunction so callers can substitutesetTimeoutforrequestAnimationFramein worker contexts. The original includedimportScripts()-based worker stubs (not ESM-compatible) which were removed; worker support is now done properly viaprim-interface'srunWorker() - Architecture split — the original is a single-layer library. Here,
prim-libis the pure algorithm (no knowledge of how images arrive or where results go), andprim-interfaceis the adapter that wires it to the browser PreCfg/Cfgdistinction —Canvas.original()acceptsPreCfg(optionalwidth/height) and resolves to a fully-populatedCfg, making the config lifecycle explicit in the types- Serialization —
StepData/SerializedOutputallow completed runs to be stored compactly and replayed viareplayOutput()without re-running the optimizer - Additional shapes —
Circle,Square,Hexagon, andGlyphare not in the original;Circleis a uniform-radius variant ofEllipse;makeNGonandmakeRectare configurable factory shapes also not in the original - Shape weighting —
shapeWeightsallows biased shape selection with a guaranteed distribution: the optimizer pre-allocates exact per-shape step counts (largest-remainder rounding) and shuffles them, so the final mix always matches the requested weights
Architecture notes
Shape.rasterize()reuses a single module-level canvas (_rasterCanvas) grown to the max shape size seen; in a worker, this is anOffscreenCanvas(environment-detected by theCanvasconstructor). Avoids per-call canvas creation which was the dominant cost (27x speedup)Cfg.shapeTypesholds shape constructors; shapes are chosen randomly each step unlessshapeWeightsis set- When
shapeWeightsis provided (same length asshapeTypes),Optimizerbuilds a step plan at construction time: each shape type is allocated an exact number of slots proportional to its weight (using largest-remainder rounding), then the plan is Fisher-Yates shuffled. This guarantees the final shape distribution matches the weights, rather than just biasing random selection PreCfgexists to bridge the gap between call time (dimensions unknown) and runtime (dimensions set byCanvas.original()orCanvas.fromBitmap())Canvas.svgRoot()is a static helper shared byCanvas.empty()andreplayOutput()to create the SVG root with clip path and background fill; it uses DOM APIs and is main-thread only- Factory shapes (
makeNGon,makeRect) capture their options in a closure and return an inner class. The class carries a static_shapeSpecproperty ({ f: string, o: opts }) used byrunWorkerto serialize and reconstruct the factory call inside the worker
