Market stall
A 64×64 floor prop in top-down 3/4: a timber stall under a striped cloth awning of 9 stripes, a plank counter with a crate of apples, a crate of cabbages and a wicker basket of bread, garlic and herbs hanging in the shade, and a grain sack and a pumpkin on the ground in front. The dyed stripes are drawn with the glow aliases, so each colour is one ramp swap. “idle” (12 frames, loop) has a red awning, a gust travelling along the scalloped hem and the hanging herbs swaying; “idle-blue” and “idle-green” are the same loop in blue and green.
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.
// Market stall — 64x64 floor prop, top-down 3/4, key light top-left.// A timber stall under a striped cloth awning (9 stripes: dyed cloth on the glow ramp, undyed cream on n), a plank// counter carrying a crate of apples, a crate of cabbages and a wicker basket of bread; garlic and herbs hang in the// shade under the awning; a grain sack and a pumpkin sit on the ground in front.// The dyed stripes are drawn with the glow0..3 aliases, so the colour variants are one ramp swap each.// Variants:// idle loop 12f — red awning (glow -> r); a gust travels along the scalloped hem, the hanging herbs sway// idle-blue loop 12f — same, awning on the b ramp// idle-green loop 12f — same, awning on the e rampThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(MarketStall, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", repalette: swap("glow", "r") }, { name: "idle-blue" }, { name: "idle-green", repalette: swap("glow", "e") }, ],});Show the whole program (325 lines)
// Market stall — 64x64 floor prop, top-down 3/4, key light top-left.// A timber stall under a striped cloth awning (9 stripes: dyed cloth on the glow ramp, undyed cream on n), a plank// counter carrying a crate of apples, a crate of cabbages and a wicker basket of bread; garlic and herbs hang in the// shade under the awning; a grain sack and a pumpkin sit on the ground in front.// The dyed stripes are drawn with the glow0..3 aliases, so the colour variants are one ramp swap each.// Variants:// idle loop 12f — red awning (glow -> r); a gust travels along the scalloped hem, the hanging herbs sway// idle-blue loop 12f — same, awning on the b ramp// idle-green loop 12f — same, awning on the e rampconst W = 64, H = 64, CX = 32, GROUND = 61; // ground line; the timber feet stand on row 60const N_IDLE = 12;const AW = ["glow0", "glow1", "glow2", "glow3"], CR = ["n3", "n4", "n5", "n5"];const NS = 9; // stripesconst TOP0 = 5, TOP1 = 19; // canopy top face rows (back .. front)const VAL0 = 20, VAL1 = 23; // valance rowsconst HEM_X0 = 5, SW = 6; // hem starts at x=5, stripes 6px wide at the front// ---- a tiny raster: paint keys per pixel, tagged with a part name, then emit one field per part ----------------function Canvas() { const key = new Array(W * H).fill(null), part = new Array(W * H).fill(null), order = []; return { key, part, order, set(x, y, k, p) { if (x < 0 || y < 0 || x >= W || y >= H || !k) return; key[y * W + x] = k; part[y * W + x] = p; if (!order.includes(p)) order.push(p); }, has(x, y) { return x >= 0 && y >= 0 && x < W && y < H && key[y * W + x] !== null; }, };}function emit(g, cv) { for (const p of cv.order) { let x0 = W, x1 = -1, y0 = H, y1 = -1; for (let i = 0; i < W * H; i++) if (cv.part[i] === p) { const x = i % W, y = (i - x) / W; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); } if (x1 < 0) continue; g.part(p, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => (cv.part[y * W + x] === p ? cv.key[y * W + x] : null))); }}const stripeKey = (s, lv) => (s % 2 === 0 ? AW : CR)[Math.max(0, Math.min(3, lv))];// ---- awning ------------------------------------------------------------------------------------------------------const canL = (y) => Math.round(9 - ((y - TOP0) / (TOP1 - TOP0)) * 4); // 9 at the back .. 5 at the frontconst canR = (y) => 63 - canL(y);function awning(cv, t) { // ridge pole along the back edge, round knobs at its ends for (let x = canL(TOP0) - 1; x <= canR(TOP0) + 1; x++) cv.set(x, TOP0 - 1, x < 24 ? "w3" : x < 48 ? "w2" : "w1", "awning"); for (const [x, lit] of [[canL(TOP0) - 2, true], [canR(TOP0) + 2, false]]) { cv.set(x, TOP0 - 2, lit ? "w4" : "w3", "awning"); cv.set(x, TOP0 - 1, lit ? "w3" : "w2", "awning"); cv.set(x + (lit ? 1 : -1), TOP0 - 2, "w2", "awning"); } // sloped top face: stripes fan out towards the front; lit on the left, a soft sag in the middle rows for (let y = TOP0; y <= TOP1; y++) { const L = canL(y), R = canR(y); for (let x = L; x <= R; x++) { const u = (x + 0.5 - L) / (R + 1 - L), s = Math.min(NS - 1, Math.floor(u * NS)); const fu = u * NS - s; // position inside the stripe // each stripe is a cloth panel bowing between two seams: lit left half, shaded right half; // panels on the far right have turned from the light, so only their left edge stays lit let lv = fu < 0.5 ? 2 : 1; if (u > 0.72 && fu > 0.25) lv = 1; if (y === TOP0) lv = 1; // in the pole's shadow if (y === TOP0 + 1 && u < 0.35 && fu < 0.5) lv = 3; // sheen where the cloth leaves the pole if (y === TOP1 && lv === 1) lv = 2; // the roll over the front beam catches light cv.set(x, y, stripeKey(s, lv), "awning"); } } // valance: the cloth folds over the front beam and hangs straight for (let y = VAL0; y <= VAL1; y++) for (let x = HEM_X0; x < HEM_X0 + NS * SW; x++) { const s = Math.floor((x - HEM_X0) / SW); let lv = y === VAL0 ? 2 : 1; if (y === VAL0 && x > 48) lv = 1; cv.set(x, y, stripeKey(s, lv), "valance"); } // scalloped hem: one tongue per stripe; a gust travels left -> right, each tongue lifts (shortens) then drops for (let s = 0; s < NS; s++) { const w = Math.sin(TAU * (t - s / NS)); const depth = Math.round(3.4 + 1.6 * w); // 2..5 const lift = w < -0.55; // lifted tongue shows its shaded underside const x0 = HEM_X0 + s * SW, cx = x0 + SW / 2; for (let x = x0; x < x0 + SW; x++) { const nx = (x + 0.5 - cx) / (SW / 2); const hang = Math.round(depth * Math.sqrt(Math.max(0, 1 - nx * nx * 0.6))); for (let k = 1; k <= hang; k++) { let lv = 1; if (k === hang) lv = 0; // rolled rim in shadow else if (x === x0 && k < hang - 1) lv = 2; // lit left edge of each tongue if (lift && k === hang - 1) lv = 0; cv.set(x, VAL1 + k, stripeKey(s, lv), "hem"); } } }}// ---- timber ----------------------------------------------------------------------------------------------------function backWall(cv) { for (let y = VAL1 + 1; y <= 39; y++) for (let x = 10; x <= 53; x++) { let k = y <= 27 ? "n0" : "w0"; // deep shade right under the awning if (y > 27 && (x - 10) % 7 === 0) k = "n0"; // plank seams else if (y > 30 && (x - 10) % 7 === 1) k = "w1"; // each plank's lit edge if (y === 39) k = "n0"; cv.set(x, y, k, "backwall"); }}function garlic(cv, t) { // a braid of bulbs on a cord, left of the crates for (let y = 24; y <= 26; y++) cv.set(11, y, "w1", "garlic"); for (const [bx, by] of [[10, 27], [11, 30], [10, 33]]) { cv.set(bx, by, "n5", "garlic"); cv.set(bx + 1, by, "n4", "garlic"); cv.set(bx, by + 1, "n4", "garlic"); cv.set(bx + 1, by + 1, "n3", "garlic"); cv.set(bx + 2, by + 1, "n2", "garlic"); cv.set(bx + 1, by + 2, "n2", "garlic"); }}function herbs(cv, t) { // a bunch of drying herbs hanging head-down on the right; its tip swings with the gust const sway = Math.round(Math.sin(TAU * (t - 0.9)) * 1); for (let y = 24; y <= 26; y++) cv.set(52, y, "w1", "herbs"); cv.set(51, 26, "w2", "herbs"); cv.set(52, 26, "w3", "herbs"); cv.set(53, 26, "w2", "herbs"); // twine const rows = [[50, 54], [50, 54], [49, 54], [49, 53], [50, 53], [50, 52], [51, 52]]; rows.forEach(([a, b], i) => { const y = 27 + i, off = i >= 4 ? sway : 0; for (let x = a; x <= b; x++) { const k = x === a ? "e2" : x === b ? "e0" : (x + i) % 3 === 0 ? "e2" : "e1"; cv.set(x + off, y, i === 0 && x === a + 1 ? "e3" : k, "herbs"); } });}function counter(cv) { // top face (planks along x), front edge lit; front face of vertical planks with a nailed rail for (let x = 7; x <= 56; x++) { cv.set(x, 40, "w2", "counter"); cv.set(x, 41, x < 45 ? "w3" : "w2", "counter"); cv.set(x, 42, (x * 7) % 11 === 3 ? "w2" : "w3", "counter"); cv.set(x, 43, x < 40 ? "w4" : "w3", "counter"); cv.set(x, 44, "w0", "counter"); for (let y = 45; y <= 58; y++) { const c = (x - 10) % 7, plank = Math.floor((x - 10) / 7); let k = c === 6 ? "w1" : "w2"; if (y === 45) k = c === 6 ? "w0" : "w1"; // shadow under the counter lip else if (c !== 6 && x >= 52 && y > 46) k = "w1"; // far end turns away // countable grain: one short streak per plank, at a plank-dependent height const gy = 48 + ((plank * 5) % 7), gx = 2 + (plank % 3); if (c === gx && (y === gy || y === gy + 1)) k = "w1"; if (y === 46 && c === 1) k = "n3"; // nail heads along the top cv.set(x, y, k, "counter"); } cv.set(x, 59, x < 40 ? "w2" : "w1", "counter"); // kick board cv.set(x, 60, "w0", "counter"); } // a chalk slate hung on the front: price scrawl and a little apple doodle const sx0 = 26, sx1 = 37, sy0 = 48, sy1 = 55; cv.set(31, 46, "w1", "slate"); cv.set(32, 46, "w1", "slate"); cv.set(30, 47, "w1", "slate"); cv.set(33, 47, "w1", "slate"); // cord for (let y = sy0; y <= sy1; y++) for (let x = sx0; x <= sx1; x++) { const edge = x === sx0 || x === sx1 || y === sy0 || y === sy1; let k = edge ? (x === sx0 || y === sy0 ? "w3" : "w1") : "n0"; if (!edge && (x === sx0 + 1 || y === sy0 + 1)) k = "n1"; cv.set(x, y, k, "slate"); } for (const [x, y, k] of [[28, 50, "r2"], [29, 50, "r2"], [28, 51, "r1"], [29, 51, "r1"], [29, 49, "e2"], [31, 50, "n4"], [32, 50, "n4"], [34, 50, "n4"], [35, 50, "n3"], [31, 52, "n3"], [32, 52, "n4"], [33, 52, "n3"], [35, 52, "n4"], [28, 53, "n3"], [29, 53, "n3"]]) cv.set(x, y, k, "slate");}function posts(cv) { for (const [x0, lit] of [[7, true], [54, false]]) { for (let y = VAL1 + 1; y <= 60; y++) { cv.set(x0, y, lit ? "w3" : "w2", "posts"); cv.set(x0 + 1, y, lit ? "w2" : "w2", "posts"); cv.set(x0 + 2, y, "w1", "posts"); } cv.set(x0, 40, "w4", "posts"); // bolt through the counter cv.set(x0 + 1, 41, "n3", "posts"); cv.set(x0 - 1, 60, "w2", "posts"); cv.set(x0 + 3, 60, "w0", "posts"); // foot }}// ---- produce ----------------------------------------------------------------------------------------------------function crateFront(cv, x0, x1, name) { for (let x = x0; x <= x1; x++) { const lx = x < x0 + (x1 - x0) * 0.65; cv.set(x, 36, lx ? "w4" : "w3", name); // rim, lit cv.set(x, 37, "w3", name); cv.set(x, 38, "w2", name); cv.set(x, 39, "w0", name); // slat gap cv.set(x, 40, lx ? "w3" : "w2", name); cv.set(x, 41, "w2", name); cv.set(x, 42, "w1", name); } for (let y = 36; y <= 42; y++) { cv.set(x0, y, y === 36 ? "w4" : "w3", name); cv.set(x1, y, "w1", name); } cv.set(x0 + 1, 37, "n3", name); cv.set(x1 - 1, 37, "n2", name); // nails cv.set(x0 + 1, 40, "n3", name); cv.set(x1 - 1, 40, "n2", name);}function apple(cv, ax, ay, stem) { const P = [ [null, "r2", "r2", null], ["r2", "r3", "r2", "r1"], ["r1", "r2", "r1", "r1"], [null, "r1", "r0", null], ]; P.forEach((row, j) => row.forEach((k, i) => cv.set(ax + i, ay + j, k, "apples"))); if (stem) cv.set(ax + 2, ay - 1, "w1", "apples");}function apples(cv) { const x0 = 14; for (const [dx, dy, st] of [[2, 29, 1], [6, 29, 0], [0, 31, 0], [4, 31, 1], [8, 31, 0], [2, 33, 0], [6, 33, 1]]) apple(cv, x0 + dx, dy, st); crateFront(cv, 13, 24, "crateA");}function cabbage(cv, cx, cy) { const rx = 3.2, ry = 2.7; for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) { const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.08); if (i < 0) continue; const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; let k = ["e0", "e1", "e2", "e3"][i]; if (Math.abs(nx - 0.15 + ny * 0.35) < 0.14 && ny > -0.5) k = i >= 2 ? "e3" : "e2"; // central vein if (ny > 0.35 && Math.abs(nx) < 0.75 && Math.abs(nx - 0.15 + ny * 0.35) > 0.3 && i < 3) k = "e1"; // leaf fold cv.set(x, y, k, "cabbages"); }}function cabbages(cv) { cabbage(cv, 31.5, 30.5); cabbage(cv, 28.5, 33.5); cabbage(cv, 34.5, 33.5); crateFront(cv, 26, 37, "crateB");}function loaf(cv, cx, cy, rx, ry) { for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) { const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.05); if (i < 0) continue; cv.set(x, y, ["w1", "w2", "w3", "w4"][i], "bread"); }}function bread(cv) { // a baguette leaning out of the basket, two round loaves in front of it for (let k = 0; k <= 9; k++) { const x = 42 + k, y = 38 - k; cv.set(x, y, "w2", "bread"); cv.set(x, y - 1, "w3", "bread"); cv.set(x - 1, y - 1, "w4", "bread"); cv.set(x - 1, y, "w3", "bread"); if (k % 3 === 1) cv.set(x, y - 1, "g2", "bread"); // scoring glints on the crust } cv.set(51, 28, "w4", "bread"); cv.set(52, 28, "w3", "bread"); loaf(cv, 42.5, 34.5, 3.2, 2.6); loaf(cv, 47.5, 35, 3.2, 2.5); cv.set(41, 33, "g2", "bread"); cv.set(46, 34, "g2", "bread"); cv.set(43, 35, "w2", "bread"); cv.set(48, 35, "w2", "bread"); // scoring // wicker basket: rolled rim, woven front for (let x = 39; x <= 50; x++) { cv.set(x, 36, x < 46 ? "w4" : "w3", "basket"); cv.set(x, 37, x % 2 ? "w3" : "w2", "basket"); for (let y = 38; y <= 42; y++) { const on = ((x >> 1) + y) % 2 === 0; cv.set(x, y, y === 42 ? "w1" : on ? (x < 45 ? "w3" : "w2") : "w1", "basket"); } } for (let y = 37; y <= 41; y++) { cv.set(39, y, "w3", "basket"); cv.set(50, y, "w1", "basket"); }}function sack(cv) { // a slumped grain sack leaning on the left post, cord at the neck const rows = { 49: [4, 6], 50: [3, 7], 51: [2, 8], 52: [1, 9], 53: [1, 9], 54: [1, 10], 55: [1, 10], 56: [1, 10], 57: [1, 10], 58: [1, 10], 59: [2, 10], 60: [2, 9] }; for (const [ys, [a, b]] of Object.entries(rows)) { const y = +ys; for (let x = a; x <= b; x++) { const u = (x - a) / Math.max(1, b - a); let k = u < 0.3 ? "w4" : u < 0.72 ? "w3" : "w2"; if (y >= 59) k = u < 0.5 ? "w2" : "w1"; if ((y === 54 && x === 4) || (y === 55 && x === 5) || (y === 56 && x === 5) || (y === 57 && x === 6)) k = "w2"; // fold if (y === 52 && x >= 7) k = "w2"; if (y === 51 && x <= 4) k = "n5"; // lit shoulder cv.set(x, y, k, "sack"); } } cv.set(4, 48, "g1", "sack"); cv.set(5, 48, "g2", "sack"); cv.set(6, 48, "g1", "sack"); // grain peeking out for (let x = 3; x <= 7; x++) cv.set(x, 50, x === 3 ? "w2" : "w1", "sack"); // cord cv.set(8, 51, "w1", "sack");}function pumpkin(cv) { const cx = 58.5, cy = 57.5, rx = 4.2, ry = 3.2; for (let y = 53; y <= 61; y++) for (let x = 53; x <= 63; x++) { const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.1); if (i < 0) continue; let k = ["r1", "r2", "r3", "g3"][i]; if (i === 3 && !(x === 56 && y === 55)) k = "r3"; const nx = (x + 0.5 - cx) / rx; if (Math.abs(Math.abs(nx) - 0.42) < 0.1 && y > 54) k = i >= 2 ? "r2" : "r1"; // ribs cv.set(x, y, k, "pumpkin"); } cv.set(58, 53, "e1", "pumpkin"); cv.set(59, 53, "e0", "pumpkin"); cv.set(58, 52, "e2", "pumpkin");}const MarketStall = sprite(pal, (g, t) => { const cv = Canvas(); backWall(cv); garlic(cv, t); herbs(cv, t); counter(cv); apples(cv); cabbages(cv); bread(cv); posts(cv); awning(cv, t); sack(cv); pumpkin(cv); contactShadow(g, CX, GROUND, 30, 2); g.outlined("ink", () => emit(g, cv));});submit(MarketStall, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, variants: [ { name: "idle", repalette: swap("glow", "r") }, { name: "idle-blue" }, { name: "idle-green", repalette: swap("glow", "e") }, ],});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.