Tessarune
Tavern & Market · grain_sack.js

Grain sack

32×32 · 251 lines of JavaScript

A 32×32 prop in top-down 3/4: a burlap sack slumped on the floor, its neck tied with twine and the cloth flaring in pleats above the tie, with folds, a weave stitch, two woven blue stripes and a frayed hole low on the right. Spilled wheat lies in a little heap at its foot. Each frame is built up layer by layer in one buffer, so overlapping layers get their own ink seam. “idle” (16 frames, loop) lets one grain slip out of the hole onto the heap; “mouse” (11 frames, once) has a mouse poke its head out from behind the sack, sniff twice, flick an ear and duck back.

Grain sack — Rendered output6×
Rendered by this program, variant “mouse” · 32×32 · 11 frames
Tavern & MarketGet the animated sprites: Tavern & Market on itch.ioVol.3 · 12 tavern and market props · 32 animations

The header. The comment at the top of the file is the brief: size, view, light direction and every variant.

grain_sack.js · 1–11
// Grain sack — 32x32, top-down 3/4, key light top-left.// A burlap sack slumped on the floor, its neck tied with twine, the gathered cloth flaring above the tie in pleats.// The body is shaded from a pseudo-normal (profile half-width per row -> across and up components), then gets// countable marks: folds pulled down from the tie (dark crease + lit lip), a staggered weave stitch every 3 rows,// two woven blue stripes, a frayed hole low on the right. Spilled wheat lies in a little heap at its foot.// Every frame is composited into one keyed buffer here, layer by layer, so a layer that overlaps an earlier one gets// its own ink seam (outlined() only inks empty pixels).// Variants://   idle   loop 16f      — the sack is still; once a loop a grain slips out of the hole and drops onto the heap//   mouse  one-shot 11f  — a mouse pokes its head out from behind the sack, sniffs twice, flicks an ear and ducks//                          back; ends on idle f0

The last lines. submit declares the canvas and lists the variants the renderer exports.

grain_sack.js · 244–251
submit(Sack, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "mouse", args: ["mouse"], animate: { fps: 10, duration: N_MOUSE / 10, loop: false } },  ],});
Show the whole program (251 lines)
grain_sack.js · 251 lines
// Grain sack — 32x32, top-down 3/4, key light top-left.// A burlap sack slumped on the floor, its neck tied with twine, the gathered cloth flaring above the tie in pleats.// The body is shaded from a pseudo-normal (profile half-width per row -> across and up components), then gets// countable marks: folds pulled down from the tie (dark crease + lit lip), a staggered weave stitch every 3 rows,// two woven blue stripes, a frayed hole low on the right. Spilled wheat lies in a little heap at its foot.// Every frame is composited into one keyed buffer here, layer by layer, so a layer that overlaps an earlier one gets// its own ink seam (outlined() only inks empty pixels).// Variants://   idle   loop 16f      — the sack is still; once a loop a grain slips out of the hole and drops onto the heap//   mouse  one-shot 11f  — a mouse pokes its head out from behind the sack, sniffs twice, flicks an ear and ducks//                          back; ends on idle f0const W = 32, H = 32, CX = 16, GROUND = 29;const N_IDLE = 16, N_MOUSE = 11;const BURLAP = ["w1", "w2", "w3", "w4"];// ---- sack shape --------------------------------------------------------------------------------------------------const NECK_Y = 9, BOT_Y = 28;const PROF = [[9, 3.6], [10, 5.6], [11, 7.0], [13, 8.0], [17, 8.3], [22, 8.7], [26, 9.2], [28, 9.1]];function hwAt(y) {  for (let k = 1; k < PROF.length; k++) {    const [y0, r0] = PROF[k - 1], [y1, r1] = PROF[k];    if (y <= y1) { const u = (y - y0) / (y1 - y0); return r0 + (r1 - r0) * u; }  }  return PROF[PROF.length - 1][1];}const cxAt = (y) => CX + (y < 16 ? 0.6 * (16 - y) / 7 : 0);      // the neck leans a little to the right// the base: an ellipse footprint, so the bottom edge is a curveconst bottomAt = (x) => { const u = (x + 0.5 - CX) / 9.6; return u * u >= 1 ? -1 : Math.floor(25.8 + 3.0 * Math.sqrt(1 - u * u)); };// folds pulled down from the tie: [x offset at the neck, x offset at the end, end row]const FOLDS = [[-2.4, -6.2, 16], [-0.6, -2.4, 19], [1.3, 2.4, 17], [2.8, 6.4, 15]];function sackPixels() {  const m = new Map();  const tone = new Map();  for (let y = NECK_Y; y <= BOT_Y; y++) {    const hw = hwAt(y + 0.5), cx = cxAt(y);    const slope = (hwAt(y + 1.5) - hwAt(y - 0.5)) / 2;             // >0 on the shoulders: faces up    for (let x = 0; x < W; x++) {      const nx = (x + 0.5 - cx) / hw;      if (Math.abs(nx) > 1) continue;      const bot = bottomAt(x);      if (bot < 0 || y > bot) continue;      const nz = Math.sqrt(Math.max(0, 1 - nx * nx));      const up = Math.max(-0.6, Math.min(0.8, slope * 0.55)) - (y > 24 ? (y - 24) * 0.12 : 0);      const d = -0.6 * nx + 0.55 * up + 0.62 * nz;      let lv = d > 0.78 ? 3 : d > 0.45 ? 2 : d > 0.05 ? 1 : 0;      if (y === bot && lv > 0) lv = Math.max(0, lv - 1);            // the base rolls under      tone.set(y * W + x, lv);    }  }  // folds: dark crease with a lit lip on its left  for (const [a, b, yEnd] of FOLDS) {    for (let y = NECK_Y + 1; y <= yEnd; y++) {      const s = (y - NECK_Y) / (yEnd - NECK_Y), e = Math.pow(s, 0.7);      const x = Math.round(cxAt(y) + a + (b - a) * e);      const i = y * W + x;      if (!tone.has(i)) continue;      if (s > 0.85 && (y & 1)) continue;                             // the crease thins out at its end      tone.set(i, Math.max(0, tone.get(i) - 1));      if (tone.has(i - 1) && s < 0.8) tone.set(i - 1, Math.min(3, tone.get(i - 1) + 1));    }  }  for (const [i, lv] of tone) {    const x = i % W, y = (i - x) / W;    let k = lv;    // weave stitch: a staggered dotted weft every third row on the lit and mid cloth (countable, never random)    const row = y - NECK_Y - 2;    if (row >= 0 && row % 3 === 0 && k >= 2) {      const off = (row / 3) % 2 ? 2 : 0;      if ((x + off) % 4 === 0) k -= 1;    }    m.set(i, BURLAP[k]);  }  // two woven stripes down the front (a miller's sack): blue on the lit cloth, deep blue where it turns away  // they start under the shoulder and bow outward with the bulge of the sack  for (let y = NECK_Y + 3; y <= BOT_Y; y++) {    const cx = cxAt(y);    for (const off of [-4.2, -2.2]) {      const x = Math.round(cx + off * (hwAt(y + 0.5) / 8.4) - 0.5), i = y * W + x;      if (!m.has(i) || y > bottomAt(x)) continue;      const lv = tone.get(i);      m.set(i, lv >= 2 ? "b1" : "b0");    }  }  // frayed hole low on the right: dark mouth, a pale torn lip above, a loose thread below  m.set(HOLE_Y * W + HOLE_X, "w0"); m.set(HOLE_Y * W + HOLE_X + 1, "w0");  m.set((HOLE_Y - 1) * W + HOLE_X, "w4"); m.set((HOLE_Y - 1) * W + HOLE_X + 1, "w3");  m.set((HOLE_Y + 1) * W + HOLE_X + 1, "w3");  return m;}const HOLE_X = 21, HOLE_Y = 20;// gathered cloth above the tie: pleats 2px wide, alternately lit / shaded, a jagged top edgefunction ruffle() {  const m = new Map();  const rows = [[2, 3.8], [3, 4.2], [4, 4.6], [5, 4.2], [6, 3.4]];  for (const [y, hw] of rows) {    const cx = cxAt(y) + 0.3;    for (let x = Math.floor(cx - hw); x <= Math.ceil(cx + hw - 1); x++) {      const nx = (x + 0.5 - cx) / hw;      if (Math.abs(nx) > 1.05) continue;      const p = Math.floor((x - 9) / 2), left = (x - 9) % 2 === 0;      const top = 1 + (p % 2);                                        // pleat tips at alternating heights      if (y < top + 1) continue;      let lv = left ? 3 : 2;      if (nx > 0.35) lv -= 1;                                          // the right side turns from the light      if (nx < -0.6 && !left) lv += 1;      if (y === top + 1 && left) lv = Math.min(3, lv + 1);      if (y >= 6) lv = Math.max(0, lv - 1);                            // pinched into the tie's shadow      m.set(y * W + x, BURLAP[Math.max(0, Math.min(3, lv))]);    }  }  return m;}// twine: two turns around the neck, a knot on the front right, two loose ends hanging over the clothfunction twine() {  const m = new Map();  const cx = Math.round(cxAt(7));  for (let x = cx - 3; x <= cx + 3; x++) {    m.set(7 * W + x, x < cx - 1 ? "n5" : x < cx + 2 ? "n4" : "n3");    m.set(8 * W + x, x < cx ? "n3" : "n2");  }  m.set(7 * W + cx + 1, "n5"); m.set(8 * W + cx + 1, "n4");          // knot  m.set(9 * W + cx + 1, "n4"); m.set(10 * W + cx + 1, "n3"); m.set(11 * W + cx + 1, "n2");  m.set(9 * W + cx + 2, "n3"); m.set(10 * W + cx + 3, "n2");  return m;}// spilled heap at the sack's foot (front right), grains as a regular stagger with a lit top-leftfunction heap() {  // golden wheat: a mound with a jagged crest of single grains (every other one missing), lit on the left, a regular  // sparse fleck of husks and shaded grains inside, the foot in shade  const m = new Map();  const rows = [[25, 23, 24], [26, 21, 26], [27, 20, 27], [28, 20, 27], [29, 21, 26]];  const topOf = (x) => rows.find((r) => x >= r[1] && x <= r[2])[0];  for (const [y, x0, x1] of rows) for (let x = x0; x <= x1; x++) {    const depth = y - topOf(x);    if (depth === 0 && y > 25 && (x & 1)) continue;    let k = depth === 0 ? (x < 24 ? "g3" : "g2") : "g2";    if (y >= 28 || (x === x1 && depth > 0)) k = "g1";    if (y === 29) k = x < 23 ? "g1" : "g0";    if (depth > 0 && y < 29 && (x * 2 + y * 3) % 5 === 0) k = y >= 28 ? "g0" : "w4";    else if (depth > 0 && y < 28 && (x * 2 + y * 3) % 5 === 2) k = "g1";    m.set(y * W + x, k);  }  return m;}const LOOSE = [[9, 30], [28, 30], [29, 27], [19, 30], [22, 24]];// mouse, facing left; (0,0) is the tip of the noseconst MOUSE = [  [3, -3, "n3"], [4, -3, "n2"],  [3, -2, "v3"], [4, -2, "n3"], [5, -2, "n2"],  [1, -1, "n3"], [2, -1, "n4"], [3, -1, "ink"], [4, -1, "n3"], [5, -1, "n2"], [6, -1, "n2"], [7, -1, "n2"],  [0, 0, "v3"], [1, 0, "n4"], [2, 0, "n4"], [3, 0, "n3"], [4, 0, "n3"], [5, 0, "n2"], [6, 0, "n2"], [7, 0, "n1"],  [1, 1, "n2"], [2, 1, "n3"], [3, 1, "n3"], [4, 1, "n2"], [5, 1, "n2"], [6, 1, "n1"], [7, 1, "n1"],  [2, 2, "n4"], [3, 2, "n3"], [5, 2, "n1"], [6, 2, "n1"], [7, 2, "n1"],];function mouseMap(nx, ny, earDown) {  const m = new Map();  for (const [dx, dy, k] of MOUSE) {    if (earDown && dy <= -2) continue;    const x = nx + dx, y = ny + dy;    if (x >= 0 && x < W && y >= 0 && y < H) m.set(y * W + x, k);  }  if (earDown) { m.set((ny - 2) * W + nx + 4, "n3"); m.set((ny - 2) * W + nx + 5, "n2"); }   // ear laid back  return m;}// ---- compositor -------------------------------------------------------------------------------------------------function compose(layers) {  const key = new Array(W * H).fill(null), part = new Array(W * H).fill(null);  for (const { name, map, seam } of layers) {    for (const [i, k] of map) { key[i] = k; part[i] = name; }    if (!seam) continue;    for (const i of map.keys()) {      const x = i % W, y = (i - x) / W;      for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {        if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;        const j = ny * W + nx;        if (!map.has(j) && key[j] !== null && part[j] !== name) { key[j] = "ink"; part[j] = name; }      }    }  }  return { key, part };}function emit(g, comp) {  const names = [];  for (const p of comp.part) if (p && !names.includes(p)) names.push(p);  g.outlined("ink", () => {    for (const n of names) g.part(n, () => g.field({ x: [0, W - 1], y: [0, H - 1] }, (x, y) => {      const i = y * W + x;      return comp.part[i] === n ? comp.key[i] : null;    }));  });}const CACHE = {};const SACK = () => CACHE.sack || (CACHE.sack = sackPixels());const RUF = () => CACHE.ruf || (CACHE.ruf = ruffle());const TWN = () => CACHE.twn || (CACHE.twn = twine());const HEAP = () => CACHE.heap || (CACHE.heap = heap());function scene(g, mouse) {  const layers = [];  if (mouse) layers.push({ name: "mouse", map: mouse, seam: false });  layers.push({ name: "ruffle", map: RUF(), seam: false });  layers.push({ name: "sack", map: SACK(), seam: true });  layers.push({ name: "twine", map: TWN(), seam: false });  layers.push({ name: "heap", map: HEAP(), seam: true });  emit(g, compose(layers));  g.detached("loose", () => { LOOSE.forEach(([x, y], k) => g.pixel(x, y, k % 2 ? "g1" : "g2")); });}const Sack = sprite(pal, (g, t, mode) => {  contactShadow(g, CX, GROUND - 1, 11, 2);  if (mode === "idle") {    const f = Math.round(t * N_IDLE) % N_IDLE;    scene(g, null);    // one grain per loop: peeks from the hole, drops, lands on the heap and rolls into it    const path = { 7: [HOLE_X + 1, HOLE_Y], 8: [HOLE_X + 1, HOLE_Y + 1], 9: [HOLE_X + 1, HOLE_Y + 3], 10: [HOLE_X + 1, HOLE_Y + 4], 11: [HOLE_X + 2, HOLE_Y + 5] };    if (path[f]) g.detached("grain", () => { const [x, y] = path[f]; g.pixel(x, y, f === 7 ? "g2" : "g3"); if (f >= 8 && f <= 9) g.pixel(x, y - 1, "g1"); });    return;  }  const f = Math.min(N_MOUSE - 1, Math.round(t * N_MOUSE));  const NOSE = [null, 5, 3, 1, 1, 1, 1, 1, 1, 4, null][f];  const BOB = [0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0][f];  const earDown = f === 7 || f === 9;  scene(g, NOSE === null ? null : mouseMap(NOSE, 25 + BOB, earDown));  if (NOSE !== null && f >= 3 && f <= 8) {    // whiskers: two pale hairs off the snout, fanned wider on the sniffs    const y = 25 + BOB, wide = BOB !== 0;    g.detached("whiskers", () => {      g.pixel(NOSE - 1, y - 1 - (wide ? 1 : 0), "n4");      g.pixel(NOSE - 1, y + 1 + (wide ? 1 : 0), "n4");      if (wide && NOSE >= 2) g.pixel(NOSE - 2, y - 2, "n3");    });  }});submit(Sack, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "mouse", args: ["mouse"], animate: { fps: 10, duration: N_MOUSE / 10, loop: false } },  ],});

These excerpts omit the boilerplate prepended by the asset pack build script: a single-line runtime header, the shared palette, and common helper utilities (contactShadow, litIndex, flame, hash, chk, TAU). The displayed line numbers match the original source files. Each program was authored by an AI coding agent and adjusted after visual inspection of the rendered frames.

Have your agent write programs like this.

Tessarune gives it the drawing API reference, 25 production manuals and a renderer to check its work against.