Anvil
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.
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.
// 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 0The last lines. submit declares the canvas and lists the variants the renderer exports.
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)
// 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.