byo.games

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.