Tessarune
Tavern & Market · hanging_sign.js

Hanging tavern sign

48×48 · 180 lines of JavaScript

A 48×48 wall prop in 3/4 view, with no contact shadow: a riveted iron wall strap carries an iron arm with a scroll brace and a curled tip, and a framed wooden board hangs from it on two chains, its green panel painted with a foaming golden mug. The two equal parallel chains form a parallelogram linkage, so the board swings along an arc but stays upright and only the chains tilt. “idle” (12 frames, loop) sways it gently by ±2px; “swing” (10 frames, once) has a gust throw it out before it settles.

Hanging tavern sign — Rendered output6×
Rendered by this program, variant “swing” · 48×48 · 10 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.

hanging_sign.js · 1–6
// Hanging tavern sign — 48x48 WALL prop (no contact shadow), 3/4 view, key light top-left.// A riveted iron wall strap carries a square iron arm with a scroll brace and a curled tip. A framed wooden board// hangs from it on two chains; its painted green panel shows a foaming golden mug.// Two equal parallel chains make a parallelogram linkage: the board swings along an arc but stays upright, so the// painted emblem never shears or shimmers — only the chains tilt.// Variants: idle (12f loop, gentle sway ±2px), swing (10f one-shot: a gust throws it out, it settles; ends == idle f0).

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

hanging_sign.js · 171–180
submit(HangingSign, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: IDLE_N / 10, loop: true },  anchor: { x: 24, y: 40 },  args: ["idle"],  variants: [    { name: "idle" },    { name: "swing", args: ["swing"], animate: { fps: 10, duration: SWING.length / 10, loop: false } },  ],});
Show the whole program (180 lines)
hanging_sign.js · 180 lines
// Hanging tavern sign — 48x48 WALL prop (no contact shadow), 3/4 view, key light top-left.// A riveted iron wall strap carries a square iron arm with a scroll brace and a curled tip. A framed wooden board// hangs from it on two chains; its painted green panel shows a foaming golden mug.// Two equal parallel chains make a parallelogram linkage: the board swings along an arc but stays upright, so the// painted emblem never shears or shimmers — only the chains tilt.// Variants: idle (12f loop, gentle sway ±2px), swing (10f one-shot: a gust throws it out, it settles; ends == idle f0).const W = 48, H = 48;const ARM_Y = 5;                        // arm: top face row ARM_Y, front face ARM_Y+1const CH = [18, 37];                    // chain hang points on the arm (x)const CH_Y = ARM_Y + 2;                 // first chain pixel under the armconst SX = 15, SY = 18, SW = 26, SH = 22;   // board at rest: top-left, size (row 0 = the board's top edge face)const L = SY - CH_Y;                    // chain length (px)const WOOD = ["w0", "w1", "w2", "w3", "w4"];// ---- bracket ----------------------------------------------------------------------------------------function plate(g) {  g.part("strap", () => {    g.rect({ x: [3, 6], y: [2, 17] }, "n2", { fill: "n2" });    g.line([3, 2], [3, 17], "n3"); g.line([4, 2], [6, 2], "n4");          // lit left bevel and top    g.line([6, 3], [6, 17], "n1"); g.line([4, 17], [6, 17], "n0");        // shaded right bevel and foot    g.pixel(3, 2, "n4");    for (const y of [4, 15]) { g.pixel(4, y, "n4"); g.pixel(5, y + 1, "n0"); g.pixel(5, y, "n3"); }   // rivets    g.pixel(4, 10, "n1"); g.pixel(5, 12, "n3");                            // a pit, a scuff  });}function arm(g) {  g.part("arm", () => {    g.line([7, ARM_Y], [42, ARM_Y], "n3");    g.line([7, ARM_Y + 1], [42, ARM_Y + 1], "n1");    g.line([8, ARM_Y], [16, ARM_Y], "n4");                                // specular run near the wall    for (const x of CH) { g.pixel(x - 1, ARM_Y, "n2"); g.pixel(x + 1, ARM_Y, "n2"); }   // chain collars    // curled tip: the bar rolls down and back into a small scroll    g.pixel(43, ARM_Y, "n3"); g.pixel(44, ARM_Y + 1, "n2"); g.pixel(44, ARM_Y + 2, "n2");    g.pixel(43, ARM_Y + 3, "n1"); g.pixel(42, ARM_Y + 3, "n1"); g.pixel(41, ARM_Y + 2, "n2");  });  g.part("brace", () => {    // 2px diagonal strut from the strap up to the arm (lit upper edge), and a scroll ring in the corner above it    for (let x = 7; x <= 15; x++) { g.pixel(x, 22 - x, "n3"); if (x < 15) g.pixel(x + 1, 22 - x, "n1"); }    for (const [x, y, k] of [[9, 8, "n3"], [8, 9, "n3"], [10, 9, "n2"], [9, 10, "n1"], [10, 8, "n2"]]) g.pixel(x, y, k);  });}// ---- chains -----------------------------------------------------------------------------------------function chain(g, x0, dx, rise) {  const x1 = x0 + dx, y1 = SY - 1 - rise;  const n = y1 - CH_Y;  for (let i = 0; i <= n; i++) {    const y = CH_Y + i, x = Math.round(x0 + (dx * i) / n);    // alternating face-on (light) and edge-on (dark) links    g.pixel(x, y, i === n ? "n3" : i % 2 === 0 ? "n3" : "n1");    if (i % 2 === 0 && i > 0 && i < n) g.pixel(x - 1, y, "n2");  }  return [x1, y1];}// ---- board ------------------------------------------------------------------------------------------// local coords u 0..SW-1, v 0..SH-1 (v 0 = the top edge of the board seen from above)const PANEL = { u0: 2, u1: SW - 3, v0: 3, v1: SH - 3 };// the painted mug, one char per pixel: F foam, f foam shade, s foam crease, H/G/g/d beer gold light -> darkconst MUG = [  "..FF..FFF....",  ".FFFFFFFFFf..",  "FFFFFFFFFFff.",  "FFFFFFFFffff.",  "fFFsfFFFsff..",  ".HFGGGGGgg...",  ".HFGGHGGggGg.",  ".HgggggggggGg",  ".HGGGGGGgg.gd",  ".HGGGHGGgg.gd",  ".HGGHGGGgg.gd",  ".HGGGGGGggGgd",  ".HgggggggggdD",  ".HGGGGGGgg...",  "dgggggggggd..",];const MUG_U = 7, MUG_V = 4;const MUG_KEY = { F: "n5", f: "n4", s: "n3", H: "g3", G: "g2", g: "g1", d: "g0", D: "g0" };function mugAt(u, v) {  const r = MUG[v - MUG_V], c = r && r[u - MUG_U];  return c && c !== "." ? MUG_KEY[c] : null;}function mugKey(u, v) {  const k = mugAt(u, v);  if (k) return k;  // a painted dark contour around the emblem (4-neighbour)  if (mugAt(u - 1, v) || mugAt(u + 1, v) || mugAt(u, v - 1) || mugAt(u, v + 1)) return "e0";  return null;}function boardKey(u, v) {  const last = SH - 1, right = SW - 1;  if (v === last && (u === 0 || u === right)) return null;                // clipped bottom corners  if (v === 0) return u === right ? "w2" : u < 11 ? "w4" : "w3";         // top edge face, lit  const frame = u <= 1 || u >= right - 1 || v <= 2 || v >= last - 1;  if (frame) {    if (v === last || u === right) return "w1";    if (u === 0 || v === 1) return "w3";    if (v === last - 1 && u >= 2) return u > right - 3 ? "w1" : "w2";    if (u === right - 1) return v > 2 ? "w1" : "w2";    // a few countable grain marks on the frame    if ((v === 2 && (u === 7 || u === 8 || u === 17)) || (u === 1 && v === 12) || (v === last - 1 && u === 13)) return "w1";    return "w2";  }  // painted panel (local to the panel so the emblem is easy to place)  const pu = u - PANEL.u0, pv = v - PANEL.v0;  if (pu === 0 || pv === 0) return "e0";                                 // recessed: the frame shades its top/left edge  const m = mugKey(u, v);  if (m) return m;  // corner flourishes: small painted L-brackets with a gold dot  const cu = Math.min(pu - 1, PANEL.u1 - u - 1), cv = Math.min(pv - 1, PANEL.v1 - v - 1);  if ((cu === 0 && cv <= 2) || (cv === 0 && cu <= 2)) return cu === 0 && cv === 0 ? "g2" : "g1";  // flaked paint showing the wood beneath (countable chips)  if ((u === 19 && v === 5) || (u === 20 && v === 5) || (u === 4 && v === 14)) return "w1";  if (u === PANEL.u1) return "e0";  return "e1";}function board(g, dx, rise) {  const x0 = SX + dx, y0 = SY - rise;  g.part("board", () => g.field({ x: [x0, x0 + SW - 1], y: [y0, y0 + SH - 1] }, (x, y) => boardKey(x - x0, y - y0)));  g.part("eyes", () => {                                                  // eye bolts where the chains hook on    for (const cx of CH) { g.pixel(cx + dx, y0, "n3"); }  });}// ---- wind (swing only) ------------------------------------------------------------------------------function gust(g, f) {  // streaks race left -> right through the open air (above the board, beside it, below it)  const board = (x, y) => x >= 13 && x <= 44 && y >= 16 && y <= 41;  g.detached("wind", () => {    const streaks = [[12, 0, 9, 0], [27, 1, 8, 0], [44, 0, 10, 1], [33, 2, 7, 1]];    for (const [y, lag, len, bend] of streaks) {      const k = f - lag;      if (k < 0 || k > 5) continue;      const x = -8 + k * 12;      for (let i = 0; i < len; i++) {        const px = x + i, py = y - (bend && i > len - 3 ? 1 : 0);        if (px < 0 || px >= W || board(px, py) || (py <= 17 && px <= 7)) continue;        if (i < 3 && !chk(px, py)) continue;                             // the tail dithers out        g.pixel(px, py, i >= len - 3 ? "n5" : "n4");        if (i >= 2 && i < len - 4 && !board(px + 3, py + 2)) g.pixel(px + 3, py + 2, "n3");   // a fainter second line      }    }  });  g.detached("leaf", () => {    // one dry leaf tumbles past, flipping each frame    if (f < 1 || f > 7) return;    const x = (f - 1) * 8 - 1, y = 12 + [0, -2, -2, 0, 3, 5, 6][f - 1];    if (f % 2 === 0) { g.pixel(x, y, "w3"); g.pixel(x + 1, y, "g2"); g.pixel(x + 1, y - 1, "w3"); g.pixel(x + 2, y - 1, "w2"); }    else { g.pixel(x, y - 1, "w2"); g.pixel(x + 1, y, "g2"); g.pixel(x + 1, y - 1, "w3"); g.pixel(x, y, "w3"); }  });}// ---- motion -----------------------------------------------------------------------------------------const IDLE_N = 12;const SWING = [1, 3, 5, 5, 3, 0, -2, -2, -1, 0];                          // board offset per frame, pxconst riseOf = (dx) => Math.round(L - Math.sqrt(L * L - dx * dx));const HangingSign = sprite(pal, (g, t, state) => {  let dx, f = -1;  if (state === "swing") { f = Math.min(SWING.length - 1, Math.round(t * SWING.length)); dx = SWING[f]; }  else dx = Math.round(1.5 * Math.sin(TAU * t));  const rise = riseOf(dx);  g.outlined("ink", () => { plate(g); arm(g); });  g.outlined("ink", () => {    g.part("chains", () => { for (const cx of CH) chain(g, cx, dx, rise); });    board(g, dx, rise);  });  if (f >= 0) gust(g, f);});submit(HangingSign, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: IDLE_N / 10, loop: true },  anchor: { x: 24, y: 40 },  args: ["idle"],  variants: [    { name: "idle" },    { name: "swing", args: ["swing"], animate: { fps: 10, duration: SWING.length / 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.