Sprite programs
Sprites are programs, not pixel grids. gen/sprites.lua runs at game
boot; each sprite.def draws onto an indexed canvas and registers the
result by name for spr(...) and map layers.
sprite.def("slime", 16, 16, function(c)
c:sym_x()
c:oval(2, 6, 12, 9, P.moss, true)
c:px(5, 9, P.ink)
c:outline(P.ink)
end)
sprite.def("walk", 24, 24, { frames = 4 }, function(c, f)
-- called once per frame, f = 1..4
end)
Canvases go up to full screen (768×432, or 432×768 in a portrait game) — a stage backdrop is one sprite, one blit. Earlier-defined sprites can be stamped into later ones.
Ops
Shapes and pixels:
px(x,y,c) get(x,y) line(...) rect(...) oval(...) circ(...)
tri(...) poly({x1,y1,x2,y2,...}, c, fill) flood(x,y,c)
fill(c) clear() replace(from,to)
Strokes (the figure-drawing kit):
capsule(x0,y0,x1,y1, r, c) thick round-capped line
taper(x0,y0,x1,y1, r0, r1, c) width lerps end to end
qcurve(x0,y0, cx,cy, x1,y1, r0,r1,c) tapered quadratic curve
Painterly (deterministic, seeded):
brush(cx,cy, r, c, jitter, seed) noise-edged dab
feather(a, b, strength, seed, n?) dither-swap along every a|b edge
featherr(x,y,w,h, a,b, s, seed, n?) feather confined to a rect
glaze(x,y,w,h, from, to, dens, seed) scatter-replace wash
vgrad(x,y,w,h, {c1,c2,...}, blend, seed) banded gradient, dithered seams
ridge(x0,y0,x1,y1, amp, seed, c, "down"|"up"|"left"|"right")
fractal skyline / strata edge, filled
mottle(x,y,w,h, target, {ramp}, scale, seed) fBm stone/texture recolor
Form and light:
shade(base, {ramp}, "down"|"up"|"left"|"right") banded replace of a color
bevel(target, hi, lo) 1px lit top edge + core shadow, per mass
form(target, hi, lo, kTop, kBot, seed) banded core shading, dithered
rimtop(c, skip?, keep?) rim light along every top edge
outline(c, diag?) contour into transparent pixels
outline_open(c, lit) outline that opens on lit crests
Texture and transforms:
dither(c, level) noise(c, p, seed) checker(...) pattern(...)
sym_x(on?) sym_y(on?) flip_x() flip_y()
stamp(name, x, y, flipx?, flipy?, frame?) composite an earlier sprite
Transforms
A matrix stack, like every big engine ships — push/pop scope it,
translate/rotate/scale compose onto it, and every later shape call
draws through it:
sprite.def("tree", 48, 64, function(c)
c:capsule(24, 64, 24, 30, 4, P.bark) -- trunk, untransformed
for i = -2, 2 do
c:push()
c:translate(24, 34)
c:rotate(i * 0.5) -- five branches, one drawing
c:capsule(0, 0, 0, -18, 2, P.bark)
c:circ(0, -20, 7, P.moss, true)
c:pop()
end
end)
Draw a limb once and rotate it into every pose; mirror a wing with
scale(-1, 1); spin one motif into a mandala. Rotated rects and ovals
become filled polygons (the console's deterministic trig, so it renders
bit-identical everywhere). stamp follows the transform's position but
does not rotate its pixels — rotate parts, not sheets. Unbalanced pop
is an error, and the stack must be empty when the program ends.
Motion math (ik2, ease, ramp, mix — see the reference) is in
scope here too: pose a limb with ik2 and draw it through the stack.
Grain, ramps, and the flat kit
The painterly helpers are one tradition, not the house style. grain
changes the cloth they're all cut from:
c:grain("off") -- partial coverage goes binary: hard bands, hard
-- edges — the flat-graphic fabric
c:grain("bayer") -- ordered dither: crisp retro patterning
c:grain("noise") -- the classic scatter (default)
Declare value ramps in the .pal (ramp turf = turf0 turf1 turf2) and
lighting stops being an ideology baked into a helper:
c:rect(0, 0, 24, 24, P.turf1, true)
c:shift(0, 12, 24, 12, -1) -- the lower half is one step
-- darker. No bevel, no dither.
c:silhouette(col) flattens everything opaque to one color — shadows,
crowds, poster figures. Together: whole styles (flat sports graphic,
1-bit, hard-band poster) that share zero fabric with the painterly look.
Variants and remaps
Palette swaps turn one drawing into a cast, a wardrobe, or a time of day:
sprite.variant("guard_red", "guard", { navy = "wine", sky = "blush" })
sprite.remap("night", { paper = "navy", amber = "grape", sun = "moss" })
variant clones an already-defined sprite with colors substituted (every
frame; palette names on both sides) — a real sprite, declared after its
source, free at draw time. remap registers a named swap applied at draw
time instead: spr("guard", x, y, { remap = "night" }), or over a whole
scene with map.draw({...}, px, py, { remap = "night" }) — day/night,
flashbacks, a poison status, four team colors from one soldier.
The look loop
Render, look, edit, repeat:
byo sprite render slime --game mygame --zoom 8 -o slime.png
Over the venue API the same loop is screenshot — the returned image URL
embeds in chat as markdown.