Tessarune
Tavern & Market · anvil.js

Anvil

48×48 · 263 lines of JavaScript

A 48×48 floor prop in top-down 3/4 with the key light at the top-left: a London-pattern anvil, horn on the left and a hardy hole in the heel, on a flared oak stump with tongs leaning against it. A bar lies across the face, cold grey at the left end and white-hot at the right tip, and its warm light pools on the steel. “idle” (12 frames, loop) makes the hot core grow and shrink while heat shimmer rises; “strike” (8 frames, once) drops a sledge from the top, flashes on the hit, throws a burst of sparks and lifts the sledge away.

Anvil — Rendered output6×
Rendered by this program, variant “strike” · 48×48 · 8 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.

anvil.js · 1–7
// Blacksmith's anvil on a tree stump — 48x48 floor prop, top-down 3/4, key light top-left.// A London-pattern anvil (horn left, heel with a hardy hole right) stands on a flared oak stump; a bar lies across// the face, cold grey at the left end and white-hot at the right tip. Tongs lean on the stump. Warm light from the bar pools on the steel face around it.// Variants://   idle    loop 12f     — the bar breathes (the hot core grows and shrinks), the glow pool follows, heat shimmer rises//   strike  one-shot 8f  — a sledge drops in from the top, hits (flash), a burst of sparks flies out and falls,//                          the sledge lifts away; the last frame == idle frame 0

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

anvil.js · 256–263
submit(Anvil, {  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: "strike", args: ["strike"], animate: { fps: 10, duration: N_STRIKE / 10, loop: false } },  ],});
Show the whole program (263 lines)
anvil.js · 263 lines
// Blacksmith's anvil on a tree stump — 48x48 floor prop, top-down 3/4, key light top-left.// A London-pattern anvil (horn left, heel with a hardy hole right) stands on a flared oak stump; a bar lies across// the face, cold grey at the left end and white-hot at the right tip. Tongs lean on the stump. Warm light from the bar pools on the steel face around it.// Variants://   idle    loop 12f     — the bar breathes (the hot core grows and shrinks), the glow pool follows, heat shimmer rises//   strike  one-shot 8f  — a sledge drops in from the top, hits (flash), a burst of sparks flies out and falls,//                          the sledge lifts away; the last frame == idle frame 0const W = 48, H = 48, CX = 24, GROUND = 45;const N_IDLE = 12, N_STRIKE = 8;const ST = { cx: 24, rx: 12, top: 31, ry: 4, bot: 42, bry: 2.5 };   // stump: top ellipse, bottom ellipseconst FACE = { y0: 15, y1: 19, x0: 13, x1: 37 };                    // anvil working face (top)const BAR = { y: 17, x0: 14, x1: 31 };                              // bar: top row y, front row y + 1const HIT = [27, 17];                                               // where the sledge lands on the bar// ---- stump -------------------------------------------------------------------------------------------------function stump(g) {  const WOOD = ["w0", "w1", "w2", "w3", "w4"];  g.part("stump", () => g.field({ x: [8, 40], y: [ST.top - ST.ry, GROUND] }, (x, y) => {    const flare = y > 38 ? (y - 38) * 0.45 : 0;                   // the base flares into roots    const rx = ST.rx + flare, dx = x + 0.5 - ST.cx, nx = dx / rx;    if (Math.abs(nx) > 1) return null;    const s = Math.sqrt(1 - nx * nx);    if (y + 0.5 < ST.top - ST.ry * s) return null;    if (y + 0.5 > ST.bot + ST.bry * s + 0.6) return null;    // three root lobes break the bottom outline    if (y >= 42 && !(Math.abs(dx + 9) < 3.2 || Math.abs(dx - 2) < 2.6 || Math.abs(dx - 10.5) < 2.8) && Math.abs(nx) > 0.55) return null;    // bark: vertical ridges and grooves; sampled on the cylinder's angle so they crowd together at the sides    let k = nx < -0.55 ? 2 : nx < 0.3 ? 1 : 0;    const a = Math.asin(nx) * 3.0 + 0.4, plate = Math.floor(a), fr = a - plate;    if (fr < 0.25) k = Math.min(4, k + 1);                        // each bark plate's lit left edge    else if (fr > 0.8) k = Math.max(0, k - 1);                    // groove between plates    else if (y === 34 + ((plate * 5 + 20) % 6)) k = Math.max(0, k - 1);   // one cross-crack per plate    if (nx < -0.78 && fr < 0.25 && y > 32 && y < 41) k = 4;      // lit ridges on the left flank    if (y + 0.5 > ST.bot + ST.bry * s - 1.2) k = Math.max(0, k - 1); // foot of the stump in shadow    return WOOD[k];  }));  // end grain on top: rings, a lit cut edge top-left, a split running to the front  g.part("stumpTop", () => g.field({ x: [ST.cx - ST.rx, ST.cx + ST.rx], y: [ST.top - ST.ry, ST.top + ST.ry] }, (x, y) => {    const nx = (x + 0.5 - ST.cx) / ST.rx, ny = (y + 0.5 - ST.top) / ST.ry;    const r = Math.sqrt(nx * nx + ny * ny);    if (r > 1) return null;    if (r > 0.9) return -nx - 0.6 * ny > 0.3 ? "w2" : "w1";         // bark rim    if (r > 0.76) return -nx - ny > -0.2 ? "w4" : "w3";             // pale sapwood ring, brightest top-left    if (Math.abs(r - 0.5) < 0.07 || Math.abs(r - 0.25) < 0.08) return "w2";   // growth rings    if (ny > 0.2 && Math.abs(nx - 0.3 - ny * 0.15) < 0.06) return "w1";       // a drying split    return "w3";  }));}// ---- anvil -------------------------------------------------------------------------------------------------// row spans [x0, x1] per screen row for the body below the faceconst SPANS = {  20: [13, 37], 21: [13, 37], 22: [15, 35], 23: [17, 33],            // body front, underside slopes in at both ends  24: [19, 29], 25: [20, 28], 26: [20, 28], 27: [19, 29],            // waist  28: [17, 31], 29: [16, 32], 30: [15, 33], 31: [15, 33], 32: [14, 34], // foot (top row + front)};function hornEdges(x) {  const u = (FACE.x0 - x) / 10;                                     // 0 at the body, 1 at the tip  return [FACE.y0 + 2.3 * Math.pow(u, 1.4), 22 - 4.6 * Math.pow(u, 0.8)];}function anvilKey(x, y, heat) {  // horn: a cone tapering left, lit along its upper flank  if (x < FACE.x0 && x >= 3) {    const [yt, yb] = hornEdges(x + 0.5);    if (y + 0.5 < yt || y + 0.5 > yb + 0.9) return null;    const v = (y + 0.5 - yt) / (yb + 0.9 - yt);    if (x <= 4) return v < 0.5 ? "n3" : "n2";    return v < 0.22 ? "n4" : v < 0.45 ? "n3" : v < 0.78 ? "n2" : "n1";  }  if (y >= FACE.y0 && y <= FACE.y1) {    if (x < FACE.x0 || x > FACE.x1) return null;    if ((x === 34 || x === 35) && y === 16) return "ink";            // hardy hole    if ((x === 34 || x === 35) && y === 17) return x === 34 ? "ink" : "n1";    if (x === 31 && y === 16) return "n1";                           // pritchel hole    if (y === FACE.y0) return x < 31 ? "n4" : "n3";                  // lit back edge    if (x === FACE.x0) return "n4";    if (x === FACE.x1) return "n2";    // the bar's glow pools on the steel just in front of / behind it    const T = barTemp(x, heat);    if (y === BAR.y + 2 && x >= BAR.x0 && x <= BAR.x1 + 1) return T > 0.85 ? "r2" : T > 0.55 ? "r1" : T > 0.2 ? "n2" : "n1";    if (y === BAR.y - 1 && x >= BAR.x0 && x <= BAR.x1 && T > 0.8) return "r1";    if (y === 16 && x >= 15 && x <= 18) return "n4";                 // polished wear on the face    return "n3";  }  const sp = SPANS[y];  if (!sp || x < sp[0] || x > sp[1]) return null;  if (y >= 31 && x >= 21 - (y - 31) && x <= 27 + (y - 31)) return null;   // arch cut out of the foot  if (y === 20) {                                                    // front bevel catches light (and the glow)    const T = barTemp(x, heat);    return x > 33 ? "n2" : T > 0.88 && heat > 0.5 ? "r1" : "n3";  }  if (y <= 23) {    if (x >= sp[1] - (y === 23 ? 1 : 0) || y === 23) return x > 28 ? "n0" : "n1";    return x < 20 ? "n3" : x < 29 ? "n2" : "n1";  }  if (y === 24) return x < 22 ? "n1" : "n0";                         // waist: in the body's shadow  if (y <= 27) return x <= sp[0] ? "n2" : x < 26 ? "n1" : "n0";  if (y === 28) return x < 21 ? "n4" : x < 29 ? "n3" : "n2";         // foot top  if (y === 32) return x < 22 ? "n1" : "n0";  return x <= sp[0] + 1 ? "n3" : x < 22 ? "n2" : x < 30 ? "n1" : "n0";}function anvil(g, heat) {  g.part("anvil", () => g.field({ x: [3, 37], y: [14, 32] }, (x, y) => anvilKey(x, y, heat)));}// ---- the bar -----------------------------------------------------------------------------------------------// temperature 0..1 along the bar (hot at the right tip); heat (0..1) breathes itfunction barTemp(x, heat) {  const T = Math.max(0, Math.min(1, (x - 17) / 10));  return T * (0.84 + 0.26 * heat);}const TOP_RAMP = ["n2", "r0", "r1", "r2", "r3", "g3"];const FRONT_RAMP = ["n1", "r0", "r0", "r1", "r2", "r3"];function tempIdx(T) { return T < 0.12 ? 0 : T < 0.25 ? 1 : T < 0.42 ? 2 : T < 0.62 ? 3 : T < 0.85 ? 4 : 5; }function bar(g, heat, flash) {  g.part("bar", () => {    for (let x = BAR.x0; x <= BAR.x1; x++) {      const i = tempIdx(barTemp(x, heat));      let top = TOP_RAMP[i], front = FRONT_RAMP[i];      if (flash && i >= 3) { top = i >= 4 ? "n5" : "g3"; front = "r3"; }      if (x === BAR.x0) top = "n3";                                  // lit cut end of the cold stock      g.pixel(x, BAR.y, top);      g.pixel(x, BAR.y + 1, front);    }    g.pixel(BAR.x1 + 1, BAR.y + 1, flash ? "r3" : "r2");             // the tip's end face  });}// ---- tongs ----------------------------------------------------------------------------function tongs(g) {  g.part("tongs", () => {    // two reins splaying to the floor from the pivot, lit / shaded    g.line([36, 31], [37, 44], "n3");    g.line([37, 31], [42, 44], "n1");    g.pixel(37, 38, "n2"); g.pixel(40, 39, "n0");    g.rect({ x: [36, 37], y: [29, 30] }, "n2", { fill: "n2" });    g.pixel(36, 29, "n4");                                           // rivet    g.line([36, 28], [35, 25], "n3"); g.line([37, 28], [38, 25], "n1"); // jaws    g.pixel(36, 24, "n3"); g.pixel(37, 24, "n1");  });}// ---- effects -----------------------------------------------------------------------------------------------function shimmer(g, t) {  g.detached("shimmer", () => {    for (let i = 0; i < 4; i++) {      const p = (t + i / 4) % 1;      const x0 = [25, 28, 30, 26][i] + Math.round(Math.sin(TAU * (p * 1.5 + i * 0.37)) * 1.2);      const y0 = BAR.y - 3 - Math.round(p * 11);      if (p < 0.3) { g.pixel(x0, y0, "r2"); g.pixel(x0, y0 + 1, "r1"); }      else if (p < 0.6) { g.pixel(x0, y0, "r1"); if (chk(x0, y0)) g.pixel(x0 + 1, y0 + 1, "r0"); }      else if (p < 0.85) g.pixel(x0, y0, "r0");    }  });}function embers(g, t) {  g.detached("embers", () => {    for (let k = 0; k < 2; k++) {      const p = (t + k * 0.5) % 1;      if (p > 0.55) continue;      const x = 28 + k * 2 + Math.round(Math.sin(TAU * (p + k * 0.3)) * 2), y = BAR.y - 2 - Math.round(p * 14);      g.pixel(x, y, p < 0.25 ? "g3" : "g2");    }  });}// the smith's sledge, entering from the top of the frame; yb = bottom row of the striking facefunction sledge(g, yb) {  // a double-faced sledge held head-down; its haft leaves the head's right cheek and runs up-right out of frame  // (the smith stands behind and to the right)  const hx0 = 23, hx1 = 31, HH = 10;  g.detached("sledge", () => g.outlined("ink", () => {    const hy = yb - 5.5;                                           // eye height    if (hy > 2) g.sweep([[30.5, hy], [30.5 + 0.62 * (hy - 1.5), 1.5]], { w0: 1.5, w1: 1.5 },      (u, v) => (Math.abs(v) < 0.34 ? "w2" : (v < 0) ? "w3" : "w1"));    g.field({ x: [hx0, hx1], y: [yb - HH, yb] }, (x, y) => {      if (y < 0) return null;      const r = yb - y;                                            // 0 = striking face      const flare = r <= 1 || r >= HH - 1;      const x0 = flare ? hx0 : hx0 + 1, x1 = flare ? hx1 : hx1 - 1;      if (x < x0 || x > x1) return null;      if (r === HH) return x < 30 ? "n4" : "n3";                   // top face      if (r === HH - 1 || r === 1) return x < 25 ? "n4" : x < 29 ? "n3" : "n2";   // flared chamfers      if (r === 0) return x < 28 ? "n2" : "n1";      if (r === 2 || r === HH - 2) return x < 25 ? "n2" : "n1";    // neck under each flare      return x <= x0 ? "n4" : x < 27 ? "n3" : x < 29 ? "n2" : "n1"; // cheek, lit left    });    if (yb - 6 >= 0) { g.pixel(25, yb - 6, "n5"); g.pixel(25, yb - 5, "n4"); }   // specular on the cheek  }));}function smear(g, yb) {  g.detached("smear", () => {    for (let y = Math.max(0, yb - 17); y < yb - 11; y++) {      if (chk(22, y)) g.pixel(22, y, "n3");      if (chk(25, y)) g.pixel(25, y, "n4");    }  });}function flashStar(g) {  // the flash squirts out sideways from under the sledge's face, along the bar  const [x, y] = HIT;  g.detached("flash", () => {    for (const [dx, dy, k] of [[-5, -1, "n5"], [5, -1, "n5"], [-6, -1, "g3"], [6, -1, "g3"], [-7, -1, "g3"], [7, -1, "g3"],      [-9, -1, "g2"], [9, -1, "g2"], [-6, -3, "g3"], [6, -3, "g3"], [-7, -4, "g2"], [7, -4, "g2"], [-5, 1, "g3"], [5, 1, "g3"],      [-7, 2, "r3"], [7, 2, "r3"]]) g.pixel(x + dx, y + dy, k);  });}const SPARKS = [  [-3.2, -2.0], [-2.3, -3.1], [-1.0, -3.8], [0.5, -4.1], [1.8, -3.4], [3.1, -2.6], [3.9, -1.1],  [-3.9, -0.7], [-2.7, 0.3], [2.9, 0.2], [1.1, -2.2], [-1.6, -1.7], [4.4, -2.0], [-4.3, -1.8],];function sparks(g, k) {  g.detached("sparks", () => {    SPARKS.forEach(([vx, vy], i) => {      if (k === 3 && i % 3 === 0) return;                          // some burn out      const sp = 1.55 + (i % 3) * 0.25;      const x = Math.round(HIT[0] + vx * sp * k), y = Math.round(HIT[1] - 1 + vy * sp * k + 0.75 * k * k);      const tx = Math.round(HIT[0] + vx * sp * (k - 0.45)), ty = Math.round(HIT[1] - 1 + vy * sp * (k - 0.45) + 0.75 * (k - 0.45) * (k - 0.45));      const head = k === 1 ? "g3" : k === 2 ? (i % 2 ? "g3" : "g2") : i % 2 ? "r3" : "r2";      const tail = k === 1 ? "g2" : k === 2 ? "r3" : "r1";      const put = (px, py, key) => { if (px >= 0 && py >= 0 && px < W && py < H) g.pixel(px, py, key); };      if (tx !== x || ty !== y) put(tx, ty, tail);      put(x, y, head);    });  });}// ---- sprite ------------------------------------------------------------------------------------------------function drawAnvil(g, heat, flash) {  contactShadow(g, CX, GROUND, 16, 2);  g.outlined("ink", () => stump(g));  g.outlined("ink", () => anvil(g, heat));  bar(g, heat, flash);  g.outlined("ink", () => tongs(g));}const heatAt = (t) => 0.5 + 0.5 * Math.cos(TAU * t);const Anvil = sprite(pal, (g, t, mode) => {  if (mode === "idle") {    drawAnvil(g, heatAt(t), false);    shimmer(g, t);    embers(g, t);    return;  }  const f = Math.min(N_STRIKE - 1, Math.round(t * N_STRIKE));  if (f === 0 || f === N_STRIKE - 1) { drawAnvil(g, heatAt(0), false); shimmer(g, 0); embers(g, 0); return; }  // 1 raised · 2 falling · 3 impact · 4 rebound · 5 lifting · 6 gone  const YB = [null, 3, 11, 16, 13, 5, null][f];  drawAnvil(g, 1, f === 3);  if (f === 6) { shimmer(g, 0); }  if (f === 2) smear(g, YB);  if (f >= 4) sparks(g, f - 3);  if (YB !== null) sledge(g, YB);  if (f === 3) flashStar(g);});submit(Anvil, {  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: "strike", args: ["strike"], animate: { fps: 10, duration: N_STRIKE / 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.