Tessarune
Tavern & Market · market_stall.js

Market stall

64×64 · 325 lines of JavaScript

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.

Market stall — Rendered output6×
Rendered by this program, variant “idle” · 64×64 · 12 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.

market_stall.js · 1–9
// 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 ramp

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

market_stall.js · 316–325
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.js · 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.