Tessarune
Tavern & Market · well.js

Village well

64×64 · 339 lines of JavaScript

A 64×64 floor prop in top-down 3/4: a round drum of coursed stone under a thick slab rim, two timber posts carrying a shingled gable roof and a windlass axle with a rope coil, and an iron crank on the right; water glints deep in the shaft. “idle” (16 frames, loop) has the bucket down the shaft, ripples spreading from the rope; “draw” (12 frames, once) turns the crank twice and the bucket climbs out of the water dripping and is set on the near rim; “idle_up” (16 frames, loop) rests the bucket there, its water glinting and a drip running down the drum.

Village well — Rendered output6×
Rendered by this program, variant “draw” · 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.

well.js · 1–8
// Village well — 64x64 floor prop, top-down 3/4, key light top-left.// A round drum of coursed stone under a thick slab rim; two timber posts carry a shingled gable roof and a// windlass axle with a rope coil; an iron crank on the right. Water glints deep in the shaft.// Variants://   idle     16f loop  the bucket is down the shaft: ripples spread from the rope, the water glint shimmers//   draw     12f once  the crank turns twice, the bucket climbs out of the water dripping, then is set on the//                      near rim; ends on idle_up frame 0//   idle_up  16f loop  the bucket rests on the rim: its water glints, a drip runs down the stone drum

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

well.js · 329–339
submit(Well, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "draw", args: ["draw"], animate: { fps: 10, duration: 1.2, loop: false } },    { name: "idle_up", args: ["idle_up"] },  ],});
Show the whole program (339 lines)
well.js · 339 lines
// Village well — 64x64 floor prop, top-down 3/4, key light top-left.// A round drum of coursed stone under a thick slab rim; two timber posts carry a shingled gable roof and a// windlass axle with a rope coil; an iron crank on the right. Water glints deep in the shaft.// Variants://   idle     16f loop  the bucket is down the shaft: ripples spread from the rope, the water glint shimmers//   draw     12f once  the crank turns twice, the bucket climbs out of the water dripping, then is set on the//                      near rim; ends on idle_up frame 0//   idle_up  16f loop  the bucket rests on the rim: its water glints, a drip runs down the stone drumconst W = 64, H = 64, CX = 32, GROUND = 61;const RCY = 40, RRX = 20, RRY = 8;          // rim slab (outer ellipse)const MRX = 15, MRY = 5;                    // shaft mouthconst WALL_H = 13;                          // drum height below the rim edgeconst WD = 4;                               // how far the water sits below the mouth (screen px)const WY = RCY + WD;                        // water centre rowconst POSTS = [14, 48];                     // left x of each 3px postconst AXLE = { y0: 24, y1: 27, x0: 17, x1: 47 };const PIV = [53, 26], CRANK_L = 5;         // crank pivot and arm lengthconst REST = 47;                            // bucket bottom when it sits on the near rimconst e2 = (x, y, cx, cy, rx, ry) => ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;// the 0.99 margin trims the lone 1px nubs a pixel ellipse grows at its polesconst inRim = (x, y) => e2(x, y, CX, RCY, RRX, RRY) <= 0.99;const inMouth = (x, y) => e2(x, y, CX, RCY, MRX, MRY) <= 0.99;const inWater = (x, y) => inMouth(x, y) && e2(x, y, CX, WY, MRX, MRY) <= 0.99;const LAST = [], FIRSTM = [];               // last rim row / first mouth row per columnfor (let x = 0; x < W; x++) {  LAST[x] = -1; FIRSTM[x] = -1;  for (let y = 0; y < H; y++) { if (inRim(x, y)) LAST[x] = y; if (FIRSTM[x] < 0 && inMouth(x, y)) FIRSTM[x] = y; }}const isWall = (x, y) => LAST[x] >= 0 && y > LAST[x] && y <= LAST[x] + WALL_H && y > RCY;const isRing = (x, y) => inRim(x, y) && !inMouth(x, y);// ---- stone -------------------------------------------------------------------------------------------const STONE = ["n0", "n1", "n2", "n3", "n4"];const clampI = (i) => Math.max(0, Math.min(4, i));function wallTone(x, y) {  const dy = y - LAST[x], nx = (x - CX) / RRX;  const lv = nx < -0.55 ? 3 : nx < 0.3 ? 2 : 1;           // cylinder lit from the left  if (dy <= 2) return STONE[clampI(lv + (dy === 1 ? 1 : 0))]; // the slab's front edge  if (dy === 3) return nx > 0.3 ? "ink" : "n0";           // shadow line under the overhanging slab  const c = Math.floor((dy - 4) / 4), k = (dy - 4) % 4;  const A = (xx) => Math.asin(Math.max(-0.99, Math.min(0.99, (xx - CX) / RRX))) / (Math.PI / 8) + c * 0.5;  const mortar = STONE[clampI(lv - 1)];  if (k === 3) return mortar;  if (Math.floor(A(x)) !== Math.floor(A(x + 1)) && x < CX + RRX) return mortar;  let s = lv;  if (hash(Math.floor(A(x)), c) < 0.24) s -= 1;             // a darker stone here and there  if (k === 0) s += 1;                                     // each stone's lit top edge  if (dy >= WALL_H) s -= 1;                                // ground contact  return STONE[clampI(s)];}function rimTone(x, y) {  const nx = (x - CX) / RRX, ny = (y - RCY) / RRY;  const a = ((Math.atan2(ny, nx) * 180 / Math.PI) + 360) % 360;       // 0 = right, 90 = front  const slab = (xx, yy) => Math.floor(((((Math.atan2((yy - RCY) / RRY, (xx - CX) / RRX) * 180 / Math.PI) + 360) % 360) + 20) / 40);  const sb = slab(x, y);  if (slab(x + 1, y) !== sb && isRing(x + 1, y)) return "n1";           // slab joints  const outer = !inRim(x - 1, y) || !inRim(x + 1, y) || !inRim(x, y - 1) || !inRim(x, y + 1);  const inner = inMouth(x - 1, y) || inMouth(x + 1, y) || inMouth(x, y - 1) || inMouth(x, y + 1);  if (outer && a > 140 && a < 300) return "n4";                         // lit outer arris (upper left)  if (inner && ny < 0.2) return "n2";                                   // bevel dropping into the shaft  if (inner && ny > 0.2 && nx < 0.2) return "n4";                       // near inner arris catches light  if (outer && a < 100) return "n2";  return hash(sb, 3) < 0.3 ? "n2" : "n3";}function shaftTone(x, y) {  // far inner wall above the water: coursed, the right side faces the light  const dw = y - FIRSTM[x], nx = (x - CX) / MRX;  let lv = nx > 0.35 ? 2 : nx < -0.3 ? 0 : 1;  if (dw >= 3) lv -= 1;  if (dw % 3 === 2) lv -= 1;  if (inWater(x, y + 1)) return "ink";                                  // dark water line  return ["n0", "n1", "n2"][Math.max(0, lv)];}function waterTone(x, y) {  return inWater(x, y - 2) ? "b1" : "b0";                               // far strip reflects the dark wall}function drum(g, near) {  // near = ring pixels on the front half + the drum wall (drawn nearer, so the bucket can sink behind them)  g.part(near ? "drumFront" : "rimBack", () => g.field({ x: [CX - RRX - 1, CX + RRX + 1], y: [RCY - RRY, RCY + RRY + WALL_H + 1] }, (x, y) => {    if (isRing(x, y)) return (y > RCY) === near ? rimTone(x, y) : null;    if (near && isWall(x, y)) return wallTone(x, y);    return null;  }));  if (near) g.part("moss", () => {    for (const [x, h] of [[15, 2], [16, 3], [17, 1], [21, 1], [38, 2], [39, 1], [46, 2], [47, 1]]) {      const b = LAST[x] + WALL_H;      for (let i = 0; i < h; i++) g.pixel(x, b - i, i === h - 1 ? "e2" : "e1");    }    g.pixel(19, LAST[19] + 7, "e1"); g.pixel(20, LAST[20] + 7, "e1");   // moss in a mortar joint    g.pixel(27, LAST[27] + 9, "n1"); g.pixel(28, LAST[28] + 10, "n1");  // a crack    g.pixel(43, LAST[43] + 5, "n3");                                      // a chip  });}function shaft(g) {  g.part("shaft", () => g.field({ x: [CX - MRX, CX + MRX], y: [RCY - MRY, RCY + MRY] }, (x, y) => {    if (!inMouth(x, y)) return null;    return inWater(x, y) ? waterTone(x, y) : shaftTone(x, y);  }));}// ---- water light -----------------------------------------------------------------------------------------function glint(g, t) {  g.part("glint", () => {    const q = Math.floor(t * 4) % 4;    const len = [4, 3, 4, 2][q], x0 = 22 + [0, 1, 0, 1][q];    g.line([x0, WY - 2], [x0 + len - 1, WY - 2], "b3");    if (q === 3) g.pixel(x0 + 3, WY - 2, "b2");    if (Math.sin(TAU * (t + 0.3)) > 0.4) g.pixel(38, WY - 1, "b2");    if (Math.sin(TAU * (t + 0.75)) > 0.5) { g.pixel(40, WY, "b3"); }  });}function rings(g, cx, cy, list) {  // list: [[radius, key]] — elliptical ripple rings on the water surface (only on water pixels)  if (!list.length) return;  g.part("ripples", () => g.field({ x: [CX - MRX, CX + MRX], y: [RCY - MRY, RCY + MRY] }, (x, y) => {    if (!inWater(x, y)) return null;    const q = Math.hypot(x - cx, (y - cy) * 3);    for (const [r, key] of list) if (Math.abs(q - r) < 0.9) return key;    return null;  }));}// ---- timber and iron -------------------------------------------------------------------------------------function posts(g) {  for (const px of POSTS) g.part(px < CX ? "postL" : "postR", () => {    g.rect({ x: [px, px], y: [20, RCY] }, "w3", { fill: "w3" });    g.rect({ x: [px + 1, px + 1], y: [20, RCY] }, "w2", { fill: "w2" });    g.rect({ x: [px + 2, px + 2], y: [20, RCY] }, "w1", { fill: "w1" });    g.pixel(px + 1, 31, "w1"); g.pixel(px + 1, 32, "w1"); g.pixel(px, 35, "w4");    // grain, a lit knot    g.pixel(px + 1, 37, "w1");    g.line([px, RCY], [px + 2, RCY], "w0");                                           // foot on the slab    g.line([px, 22], [px + 2, 22], "w1"); g.pixel(px + 2, 22, "w0");                  // the eave's shadow    g.line([px, 29], [px + 2, 29], "n2"); g.pixel(px, 29, "n3");                      // iron strap below the axle  });}function axle(g, th, wind) {  const { y0, y1, x0, x1 } = AXLE;  g.part("axle", () => {    g.line([x0, y0], [x1, y0], "w3");    g.line([x0, y0 + 1], [x1, y0 + 1], "w2");    g.line([x0, y0 + 2], [x1, y0 + 2], "w2");    g.line([x0, y1], [x1, y1], "w1");    // two grain marks turn with the axle (visible on the front half of the log)    for (const mx of [20, 44]) for (const off of [0, 180]) {      const c = Math.cos((th + off) * Math.PI / 180);      if (c < -0.2) continue;      g.pixel(mx, Math.round(25.5 + 1.6 * Math.sin((th + off) * Math.PI / 180)), "w1");    }    g.pixel(12, 25, "w3"); g.pixel(13, 25, "w2"); g.pixel(12, 26, "w2"); g.pixel(13, 26, "w1");   // axle end left    g.pixel(51, 25, "w2"); g.pixel(51, 26, "w1"); g.pixel(52, 25, "n3"); g.pixel(52, 26, "n1");    // stub right, iron boss  });  // rope coil: grows as the bucket is wound up; stripes step with the turning axle  const hw = 1 + Math.round(wind * 5), ph = Math.round(th / 90) & 1;  g.part("coil", () => {    for (let x = CX - hw; x <= CX + hw; x++) for (let y = y0 - 1; y <= y1 + 1; y++) {      if ((y === y0 - 1 || y === y1 + 1) && (x === CX - hw || x === CX + hw)) continue;      const stripe = ((x + y + ph) % 3) === 0;                             // helix of the wound rope      const key = y === y0 - 1 ? (stripe ? "w3" : "w4") : y >= y1 ? (stripe ? "w1" : "w2") : (stripe ? "w2" : "w3");      g.pixel(x, y, key);    }  });}function crank(g, th) {  const a = th * Math.PI / 180, [px, py] = PIV;  const tx = px, ty = Math.round(py - CRANK_L * 0.85 * Math.cos(a) + CRANK_L * 0.55 * Math.sin(a));  const front = Math.sin(a) > 0.3, back = Math.sin(a) < -0.3;  g.part("crank", () => {    g.line([px, py], [tx, ty], back ? "n1" : "n2");    g.pixel(px, py, "n3");    const gy = ty;    g.line([tx + 1, gy], [tx + 3, gy], front ? "w3" : back ? "w1" : "w2");    g.pixel(tx + 1, gy, front ? "w4" : back ? "w2" : "w3");  });}function roof(g) {  g.part("roof", () => g.field({ x: [RX0 - 1, RX1 + 1], y: [7, 21] }, (x, y) => {    if (y <= 10) {                                                                        // back slope, faces the sky      if (x < RX0 + 1 || x > RX1 - 1) return null;      if (y === 7) return x < 20 ? "w4" : "w3";      if (y === 10) return "w3";      if (y === 9 && ((x * 7 + 3) % 11) < 1) return "w3";                                 // a few shingle joints      return "w4";    }    if (y === 11) return x === RX0 - 1 ? "w3" : x === RX1 + 1 ? "w0" : x === RX0 ? "w2" : "w1";   // ridge beam, ends proud    if (x < RX0 || x > RX1) return null;    if (y === 21) return x === RX0 ? "w2" : "w1";                                         // fascia board    if (x === RX0) return "w3";                                                            // barge boards    if (x === RX1) return "w0";    // three shingle courses of 3 rows: [shadow under the course above, face, lit butt]; shingles of uneven    // width, a few short ones whose butt stays in shadow    const r = Math.floor((y - 12) / 3), k = (y - 12) % 3;    if (k === 0) return r === 0 ? "w0" : "w1";    const cuts = SHINGLE[r];    let si = 0;    while (si + 1 < cuts.length && cuts[si + 1] <= x) si++;    if (cuts[si] === x && k === 1) return "w1";    const light = hash(si, r + 7) < 0.4 ? 0 : 1;    if (k === 1) return light ? "w3" : "w2";    if (hash(si, r + 3) < 0.22) return "w2";                                               // short shingle    return light ? "w4" : "w3";  }));  g.part("roofMoss", () => {    g.pixel(11, 20, "e1"); g.pixel(12, 20, "e2"); g.pixel(13, 20, "e1"); g.pixel(12, 19, "e1");    g.pixel(44, 17, "e1"); g.pixel(45, 17, "e2");    g.pixel(52, 14, "e1");  });}const RX0 = 8, RX1 = 56;                     // front slope extent (eaves)const SHINGLE = [0, 1, 2].map((r) => {       // shingle cut positions per course, uneven widths, staggered  const out = [];  for (let x = RX0 + 1 + r * 2 - 4, i = 0; x <= RX1; i++) { out.push(x); x += 4 + Math.floor(hash(i, r + 11) * 4); }  return out;});// ---- bucket and rope -------------------------------------------------------------------------------------function bucket(g, bx, by) {  const top = by - 8;  g.part("bucket", () => {    // iron bail    for (const [dx, dy, k] of [[-4, -1, "n3"], [-3, -2, "n3"], [-2, -3, "n3"], [-1, -3, "n4"], [0, -3, "n3"], [1, -3, "n2"], [2, -3, "n2"], [3, -2, "n2"], [4, -1, "n1"]])      g.pixel(bx + dx, top + dy, k);    g.line([bx - 3, top], [bx + 3, top], "w2");                            // far rim (inside face)    g.pixel(bx - 4, top + 1, "w4"); g.pixel(bx + 4, top + 1, "w2");    g.line([bx - 3, top + 1], [bx + 3, top + 1], "b1");                     // water in the bucket    g.pixel(bx - 2, top + 1, "b3"); g.pixel(bx - 1, top + 1, "b2");    g.line([bx - 4, top + 2], [bx + 4, top + 2], "w3");                     // near lip    g.pixel(bx - 4, top + 2, "w4"); g.line([bx + 2, top + 2], [bx + 4, top + 2], "w2");    for (let j = 3; j <= 8; j++) {      const hw = j <= 5 ? 4 : 3, y = top + j, band = j === 4 || j === 7;      for (let dx = -hw; dx <= hw; dx++) {        const u = dx / hw;        let k;        if (band) k = u < -0.5 ? "n3" : u < 0.4 ? "n2" : "n1";        else {          k = u < -0.6 ? "w3" : u < 0.3 ? "w2" : u < 0.8 ? "w1" : "w0";          if ((dx === -1 || dx === 2) && k === "w2") k = "w1";             // stave seams          if (j === 8) k = k === "w3" ? "w2" : k === "w2" ? "w1" : "w0";        }        g.pixel(bx + dx, y, k);      }    }  });}function rope(g, yTop, yEnd) {  g.part("rope", () => {    for (let y = yTop; y <= yEnd; y++) {      g.pixel(CX, y, (y & 1) ? "w3" : "w2");      if (inRim(CX + 1, y) && !inWater(CX + 1, y)) g.pixel(CX + 1, y, inMouth(CX + 1, y) ? "ink" : "n1");   // its shadow    }  });}// ---- states ------------------------------------------------------------------------------------------------const DRAW = 12;const BB = [null, null, 55, 51, 47, 44, 42, 41, 40, 43, 46, REST];   // bucket bottom per draw framefunction stateAt(state, t) {  t = ((t % 1) + 1) % 1;                                    // hosts probe t = 1 and t < 0 too  if (state === "idle") return { th: 0, bb: null, wind: 0, wt: t, rt: t, front: false, f: -1 };  if (state === "idle_up") return { th: 0, bb: REST, wind: 1, wt: t, rt: -1, front: true, f: -1, dt: t };  const f = Math.min(DRAW - 1, Math.round(t * DRAW));  if (f === 0) return stateAt("idle", 0);  if (f === DRAW - 1) return stateAt("idle_up", 0);  return { th: Math.min(8, f) * 90, bb: BB[f], wind: Math.min(1, f / 8), wt: 0, rt: -1, front: f >= 9, f };}function splash(g, S) {  const f = S.f;  if (f < 1) return;  const L = [];  if (f === 1) L.push([3, "b2"], [7, "b1"]);                 // the rope jerks taut  if (f === 2) L.push([5, "b3"], [9, "b2"]);                 // the bucket breaks the surface  if (f === 3) L.push([8, "b2"], [12, "b1"]);  if (f === 4) L.push([11, "b1"]);  if (f >= 5 && f <= 9) L.push([5 + (f % 2) * 3, "b2"]);    // drips landing  rings(g, CX, WY, L);  g.detached("drops", () => {    if (f === 2) for (const [x, y] of [[28, 41], [36, 40], [26, 39], [38, 42]]) g.pixel(x, y, "b3");    if (f === 3) for (const [x, y] of [[26, 37], [38, 36], [24, 40]]) g.pixel(x, y, "b2");    if (f >= 3 && f <= 10 && S.bb != null) {                 // drips falling from the bucket's bottom      const by = S.bb, ph = f % 3;      g.pixel(CX - 2, by + 1 + ph, "b2");      g.pixel(CX + 2, by + 2 + ((ph + 1) % 3) * 2, "b3");      if (f >= 5) g.pixel(CX, by + 3 + ((ph + 2) % 3), "b2");    }  });}function wallDrip(g, t) {  // idle_up: a drop runs from the bucket over the slab edge and down the drum, then splashes at the foot  const x = CX + 3, top = REST + 1, bot = LAST[x] + WALL_H;  const p = t / 0.8;  g.detached("drip", () => {    if (p < 1) {      const y = top + Math.floor(p * (bot - top + 1));      g.pixel(x, y, "b3");      if (y - 1 >= top) g.pixel(x, y - 1, "b2");      if (y - 2 >= top && p > 0.3) g.pixel(x, y - 2, "b1");    } else {      const k = (t - 0.8) / 0.2;      if (k < 0.5) { g.pixel(x - 1, bot + 1, "b2"); g.pixel(x + 1, bot + 1, "b2"); g.pixel(x, bot, "b1"); }      else { g.pixel(x - 2, bot + 1, "b1"); g.pixel(x + 2, bot + 1, "b1"); }    }  });}const Well = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  contactShadow(g, CX, GROUND, 22, 2);  // the bucket first, so its own ink ring lands on empty pixels; its depth decides what hides it  if (S.bb != null) g.depth(S.front ? -3 : -1, () => g.outlined("ink", () => bucket(g, CX, S.bb)));  g.outlined("ink", () => {    roof(g);    crank(g, S.th);    posts(g);    axle(g, S.th, S.wind);    drum(g, false);    shaft(g);    g.depth(-2, () => drum(g, true));    const ropeEnd = S.bb != null ? S.bb - 12 : WY - 1;    rope(g, AXLE.y1 + 2, ropeEnd);  });  glint(g, S.wt);  if (S.rt >= 0) rings(g, CX, WY, [0, 0.5].map((o) => { const p = (S.rt + o) % 1; return [2 + p * 11, p < 0.55 ? "b2" : "b1"]; }));  g.depth(-1, () => splash(g, S));  if (S.dt !== undefined) g.depth(-4, () => wallDrip(g, S.dt));});submit(Well, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "draw", args: ["draw"], animate: { fps: 10, duration: 1.2, loop: false } },    { name: "idle_up", args: ["idle_up"] },  ],});

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.