@flare-engine/shapes
v0.4.6
Published
Procedural 2D shape primitives for Skia — atomic primitives + SVG path escape hatch, composable for fast game prototyping
Maintainers
Readme
@flare-engine/shapes
Procedural 2D shape primitives for react-native-skia. 12 named atomic shapes + an SVG path escape hatch — designed for fast 2D game prototyping where you compose entities from primitives instead of importing sprite art.
Why
Mobile and indie game prototypes don't need sprite sheets to start. Compose ships, enemies, pickups, and UI shapes from primitives, ship a playable build, and replace shapes with art only after the mechanics lock — exactly the workflow Unity Shapes2D and Defold prototyping threads document. Shape-first prototyping keeps the iteration loop tight: a project can reach a playable state immediately and swap in sprite art only after mechanics lock.
Install
bun add @flare-engine/shapes @shopify/react-native-skia@shopify/react-native-skia is an optional, type-only peer — src/draw.ts imports it via import type only (for the injected Skia / canvas / paint handles), so it never enters the runtime bundle. Skip it entirely on web/CanvasKit; add it only where you actually render on native Skia.
Usage
import type { Skia as SkiaModule, SkCanvas, SkPaint } from '@shopify/react-native-skia';
import { drawShape, type ShapeSpec } from '@flare-engine/shapes';
// `Skia` from react-native-skia is a value; the handle type is `typeof Skia`
// (mirrors `type Skia = typeof SkiaModule` inside src/draw.ts).
type Skia = typeof SkiaModule;
function demo(skia: Skia, canvas: SkCanvas, paint: SkPaint): void {
// Compose any entity from primitives.
const shape: ShapeSpec = { kind: 'circle', r: 24, fill: 0xff8844cc };
drawShape(skia, canvas, paint, shape, /* x */ 100, /* y */ 200, /* rot */ 0);
// Need a custom shape? Use the SVG escape hatch:
const star: ShapeSpec = {
kind: 'path',
d: 'M 128 0 L 168 80 L 256 93 L 192 155 L 207 244 L 128 202 L 49 244 L 64 155 L 0 93 L 88 80 Z',
boundsR: 256,
fill: 0xffc9a24a,
};
drawShape(skia, canvas, paint, star, 0, 0, 0);
}Primitives
13 spec kinds covering every common 2D shape. All accept optional fill (ARGB int), stroke, strokeWidth, ox/oy (local offset), rot (radians), and glow: 'friendly' | 'hazard'.
| Kind | Fields | Use case |
|---|---|---|
| circle | r | Bullets, pickups, eyes |
| rect | w, h | Weapons, HUD bars, frames |
| triangle | size | Ship hulls, arrows, sigils |
| ngon | n, r | Hex/pentagon/octagon hulls |
| star | points, rOuter, rInner | Sigils, sparks, pickups |
| ring | r | Auras, target markers |
| polyline | points: number[] | Bolts, freehand strokes |
| gradient-rect | w, h, colorA, colorB, axis: 0\|1 | HP bars, sky strips |
| arc | r, startAngle, endAngle, sector? | Half-circle, 1/4-moon, 1/8-moon |
| annulus | rOuter, rInner, startAngle?, endAngle? | Donut, half-donut, partial rings |
| trapezoid | topW, botW, h | Ship cockpits, pedestals |
| path | d, boundsR | Escape hatch — any SVG path string |
| ship | hull, weapons[], engine | Composite (3-element rule) |
Composing entities
import type { ShapeSpec } from '@flare-engine/shapes';
// A ship: hull + 2 weapons + engine, painted back-to-front.
const playerShip: ShapeSpec = {
kind: 'ship',
hull: { kind: 'triangle', size: 28, fill: 0xffc9a24a },
weapons: [
{ kind: 'rect', w: 6, h: 12, fill: 0xffc9a24a, ox: -14, oy: 8 },
{ kind: 'rect', w: 6, h: 12, fill: 0xffc9a24a, ox: 14, oy: 8 },
],
engine: { kind: 'square', size: 20, fill: 0xff806020 },
};
// A pickup: ring + inner glow circle.
const amulet: readonly ShapeSpec[] = [
{ kind: 'ring', r: 9, stroke: 0xffc9a24a, strokeWidth: 2, glow: 'friendly' },
{ kind: 'circle', r: 3, fill: 0xffff2b2b },
];Bounds
import { shapeBoundsRadius, singleShapeRadius, type ShapeSpec } from '@flare-engine/shapes';
declare const playerShip: ShapeSpec;
// For atlas tile sizing — max half-extent across a composition. Includes each
// shape's ox/oy offset, a stroke-width margin where a stroke is actually
// painted, and the full extent of a glow ring (+5.5 friendly, +2.25 hazard),
// so a tile sized from this fully contains what `drawShape` paints.
const tileRadius = shapeBoundsRadius([playerShip]);
// For glow halo sizing — radius of one primitive.
const glowR = singleShapeRadius({ kind: 'circle', r: 14 });Adding a new primitive
Three ways, in order of friction:
Use the
pathescape hatch. SVGd="..."strings parse natively viaSkia.Path.MakeFromSVGString. Same syntax as Figma / Inkscape /<path>. Zero engine change.Author a composition. Wrap multiple primitives in a
ShapeSpec[]array. No new code at all.Add a named primitive. When (1)/(2) aren't ergonomic enough or you need a perf-optimized branch:
- Extend the
ShapeSpecunion insrc/types.ts - Add a
case 'kind':branch indrawShape(src/draw.ts) - Add a bounds branch in
singleShapeRadius(src/bounds.ts) - Add tests in
tests/draw.test.tsandtests/bounds.test.ts
Tsc catches a missing step 3:
singleShapeRadiusreturnsnumber, so an unhandled kind fails to compile. It does not catch a missing step 2 —drawShapereturnsvoidand its switch has nodefault, so a new kind without a draw branch compiles and silently draws nothing.- Extend the
Glow tags
Two visual halos for free, set glow: 'friendly' | 'hazard' on any primitive:
| Tag | Color | Use |
|---|---|---|
| friendly | safe-green #6FA66B at α=0.25, single 3 px ring at r+4 | Player ships, pickups, safe markers |
| hazard | hazard-orange #E84B2A at α=0.6, double 0.5 px ring at r+1 and r+2 | Enemy bullets, danger cues |
License
Apache-2.0
