Grain sack
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.
6×
Get 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 — 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 f0The last lines. submit declares the canvas and lists the variants the renderer exports.
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 — 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.