Tessarune
Tavern & Market · notice_board.js

Notice board

48×48 · 289 lines of JavaScript

A 48×48 floor prop in 3/4 view: two squared wooden posts carry a plank board under a small slate-tiled roof. Pinned to it are a WANTED poster with a face silhouette, a letter sealed with red wax, an old note with a loose corner, a small slanted note and a torn scrap; there is no readable text, every line is a dash pattern. “idle” (12 frames, loop) flutters the old note’s corner in gusts; “post” (8 frames, once) sails in a new note, a little treasure map, slaps it flat and pins it; “idle_full” (12 frames, loop) is idle with the map in place.

Notice board — Rendered output6×
Rendered by this program, variant “post” · 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.

notice_board.js · 1–8
// Notice board — 48x48 floor prop, 3/4 view, key light top-left.// Two squared wooden posts carry a plank board under a small slate-tiled roof. Pinned to it: a WANTED poster with a// face silhouette, a sealed letter (red wax), an old note whose loose corner lifts in the wind, a small slanted// note, and a torn scrap left by an older notice. No readable text: every "line" is a dash pattern.// Variants://   idle       12f loop — the old note's corner flutters in gusts//   post       8f one-shot — a new note (a little treasure map) sails in, is slapped flat and pinned; ends == idle_full f0//   idle_full  12f loop — idle with the new note pinned

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

notice_board.js · 279–289
submit(NoticeBoard, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "post", args: ["post"], animate: { fps: 10, duration: POST.length / 10, loop: false } },    { name: "idle_full", args: ["idle_full"] },  ],});
Show the whole program (289 lines)
notice_board.js · 289 lines
// Notice board — 48x48 floor prop, 3/4 view, key light top-left.// Two squared wooden posts carry a plank board under a small slate-tiled roof. Pinned to it: a WANTED poster with a// face silhouette, a sealed letter (red wax), an old note whose loose corner lifts in the wind, a small slanted// note, and a torn scrap left by an older notice. No readable text: every "line" is a dash pattern.// Variants://   idle       12f loop — the old note's corner flutters in gusts//   post       8f one-shot — a new note (a little treasure map) sails in, is slapped flat and pinned; ends == idle_full f0//   idle_full  12f loop — idle with the new note pinnedconst W = 48, H = 48, CX = 24, GROUND = 45;const POSTS = [5, 40];                       // left x of each 3px postconst FRAME = { x0: 8, x1: 39, y0: 13, y1: 35 };const PANEL = { x0: 10, x1: 37, y0: 15, y1: 33 };// ---- structure ------------------------------------------------------------------------------------function posts(g) {  g.part("posts", () => {    for (const x of POSTS) {      g.rect({ x: [x, x + 2], y: [10, GROUND - 1] }, "w2", { fill: "w2" });      g.line([x, 10], [x, GROUND - 1], "w3");      g.line([x + 2, 10], [x + 2, GROUND - 1], "w1");      g.line([x, GROUND - 1], [x + 2, GROUND - 1], "w0");              // the foot sinks into the ground      g.pixel(x + 1, 39, "w1"); g.pixel(x + 1, 40, "w1");              // a grain check    }    g.pixel(POSTS[0] + 1, 42, "w4"); g.pixel(POSTS[1] + 1, 37, "w3");  });  g.part("rail", () => {                                               // a cross rail braces the legs    g.line([POSTS[0] + 3, 41], [POSTS[1] - 1, 41], "w3");    g.line([POSTS[0] + 3, 42], [POSTS[1] - 1, 42], "w1");    g.line([POSTS[1] - 6, 41], [POSTS[1] - 1, 41], "w2");    g.pixel(POSTS[0] + 3, 42, "w2");  });}function roof(g) {  const edge = (y) => Math.round(5 - ((y - 3) * 3) / 7);             // left end of each row: flares toward the eave  g.part("roof", () => g.field({ x: [1, W - 2], y: [3, 10] }, (x, y) => {    const l = edge(y), r = W - 1 - l;    if (x < l || x > r) return null;    const u = (x - l) / (r - l);                                         // 0 left .. 1 right    if (y === 3) return u < 0.35 ? "n4" : u < 0.8 ? "n3" : "n2";         // ridge cap    const c = (y - 4) >> 1, top = ((y - 4) & 1) === 0;                   // courses of slates, 2 rows each    const joint = ((x + c * 2) % 5) === 0;    if (x === l) return "n3";    if (x === r) return "n1";    const nearJ = (((x + c * 2) % 5) + 5) % 5;                          // 0 = joint, 1..4 across one slate    if (!top) {                                                          // rounded slate bottoms: dark corners, lit belly      if (joint) return "n0";      if (nearJ === 1 || nearJ === 4) return "n1";      return u < 0.3 ? "n3" : u < 0.8 ? "n2" : "n1";    }    if (joint) return u < 0.3 ? "n2" : "n1";    return u < 0.3 ? "n3" : u < 0.8 ? "n2" : "n1";  }));  g.part("fascia", () => {    g.line([2, 11], [W - 3, 11], "w2");    g.line([2, 11], [12, 11], "w3");    g.line([W - 8, 11], [W - 3, 11], "w1");    g.pixel(2, 11, "w4");  });}function boardFrame(g) {  const { x0, x1, y0, y1 } = FRAME;  g.part("frame", () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => {    const inPanel = x >= PANEL.x0 && x <= PANEL.x1 && y >= PANEL.y0 && y <= PANEL.y1;    if (inPanel) {      // vertical planks, dark so the paper pops; the frame and roof shade the top edge      if (y === PANEL.y0) return "w0";      if (x === PANEL.x0) return "w0";      if ((x - PANEL.x0) % 7 === 0) return "w0";                        // plank seams      if (y === PANEL.y0 + 1 && chk(x, y)) return "w0";                 // roof shadow dithers out      return (x - PANEL.x0) % 7 === 1 ? "w2" : "w1";                    // lit left edge of each plank    }    if (y === y0) return x < x1 - 4 ? "w4" : "w3";                      // top edge face    if (x === x0 || y === y0 + 1 && x < x1) return "w3";    if (x === x1 || y === y1) return "w1";    if (x === x1 - 1 && y > y0) return "w1";    if (y === y1 - 1) return x < x0 + 3 ? "w2" : "w1";    return "w2";  }));  g.part("grain", () => {    for (const [x, y] of [[14, 14], [15, 14], [27, 34], [9, 22], [9, 23]]) g.pixel(x, y, "w1");    for (const [x, y] of [[13, 21], [20, 31], [33, 19]]) g.pixel(x, y, "w2");   // knots / old pin holes lit    for (const [x, y] of [[25, 17], [34, 30], [16, 32]]) g.pixel(x, y, "w0");  });}// ---- paper ----------------------------------------------------------------------------------------// one char per pixel. P aged paper, Q fresh paper, p/q their shaded edge, K dark ink, k faint ink,// F face-box ground, S silhouette, r/R/o wax (body, highlight, shade), e green ink, X red ink, . emptyconst PKEY = { P: "n4", Q: "n5", p: "n3", q: "n4", K: "n1", k: "n2", l: "n3", F: "n3", S: "n1", r: "r1", R: "r2", o: "r0", e: "e1", X: "r1" };const WANTED = { x: 11, y: 15, pin: [15, 15, "red"], rows: [  "PPPPPPPPp",  "PPPPPPPPp",  "PKKPKPKKp",  "PKKPKPKKp",  "PPPPPPPPp",  "PFFFFFFPp",  "PFFSSFFPp",  "PFSSSSFPp",  "PFFSSFFPp",  "PFSSSSFPp",  "PSSSSSSPp",  "PPPPPPPPp",  "PkkkPkkPp",  "PkkPkkkPp",  ".pppppppp",] };const LETTER = { x: 21, y: 16, pin: [22, 16, "brass"], rows: [  "QQQQQQQQq",  "QllllllQq",  "QQQQQQQQq",  "QlllllQQq",  "QQQQQQQQq",  "QQQrrrQQq",  "QQrRrroQq",  "QQQroQQQq",  "qqqqoqqqq",] };const SLANT = { x: 31, y: 16, pin: [33, 16, "red"], shear: [[0, 3, 1]], rows: [  "QQQQQq",  "QlllQq",  "QQQQQq",  "QllQQq",  "QQQQQq",  "QlllQq",  "qqqqqq",] };const OLD = { x: 21, y: 26, pin: [22, 26, "brass"], rows: [  "PPPPPPPp",  "PkkkkkPp",  "PPPPPPPp",  "PkkkPPPp",  "PPPPPPPp",  "PkkkkPPp",  "pppppppp",] };const MAP = { x: 30, y: 25, pin: [33, 25, "red"], rows: [  "PPPPPPp",  "PekPPPp",  "PPPkPPp",  "PPXPXPp",  "PPPXPPp",  "PPXPXPp",  "ppppppp",] };const SCRAP = { x: 11, y: 31, pin: [12, 31, "old"], rows: ["pP.", "p.."] };const noteW = (n) => n.rows[0].length, noteH = (n) => n.rows.length;function noteKey(n, u, v, lift) {  const r = n.rows[v], c = r && r[u];  if (!c || c === ".") return null;  if (lift) {    // corner flutter: the bottom-right triangle folds up toward the viewer, showing the paper's back    const w = noteW(n), h = noteH(n), d = (w - 1 - u) + (h - 1 - v);    if (d < lift) return "gone";    if (d < 2 * lift) return d === 2 * lift - 1 ? "edge" : "back";      // the flap; its far edge darker  }  return c;}function shiftOf(n, v) {  if (!n.shear) return 0;  for (const [a, b, s] of n.shear) if (v >= a && v <= b) return s;  return 0;}function pin(g, x, y, kind) {  if (kind === "red") { g.pixel(x, y, "r2"); g.pixel(x + 1, y + 1, "r0"); }  else if (kind === "brass") { g.pixel(x, y, "g2"); g.pixel(x + 1, y + 1, "g0"); }  else g.pixel(x, y, "w0");}function note(g, name, n, lift) {  const w = noteW(n), h = noteH(n);  g.part(name, () => {    // drop shadow on the planks (right and below), then the sheet    g.field({ x: [n.x, n.x + w + 1], y: [n.y, n.y + h] }, (x, y) => {      const u = x - n.x - shiftOf(n, y - n.y), v = y - n.y;      const k = noteKey(n, u, v, lift);      if (k === "gone") return "w0";      if (k === "back") return "n3";      if (k === "edge") return "n2";      if (k) return PKEY[k];      // shadow: one pixel right / below of the sheet      const ks = noteKey(n, u - 1, v - 1, lift);      return ks && ks !== "gone" ? "w0" : null;    });    if (n.pin) pin(g, n.pin[0], n.pin[1], n.pin[2]);  });}// ---- the new note in flight (post) ----------------------------------------------------------------// drawn by inverse-mapping each pixel into the sheet's frame, so it can arrive tiltedfunction flyingNote(g, dx, dy, deg, lifted) {  const n = MAP, w = noteW(n), h = noteH(n);  const cx = n.x + w / 2 + dx, cy = n.y + h / 2 + dy;  const a = (deg * Math.PI) / 180, ca = Math.cos(a), sa = Math.sin(a);  const sample = (x, y) => {    const px = x + 0.5 - cx, py = y + 0.5 - cy;    const u = Math.floor(px * ca + py * sa + w / 2), v = Math.floor(-px * sa + py * ca + h / 2);    if (u < 0 || v < 0 || u >= w || v >= h) return null;    return n.rows[v][u];  };  const onPanel = (x, y) => x >= PANEL.x0 && x <= PANEL.x1 && y >= PANEL.y0 && y <= PANEL.y1;  const R = 9;  const bx = [Math.floor(cx - R), Math.ceil(cx + R)], by = [Math.floor(cy - R), Math.ceil(cy + R)];  if (lifted) g.part("newShadow", () => g.field({ x: bx.map((v) => v + lifted), y: by.map((v) => v + lifted) }, (x, y) =>    onPanel(x, y) && sample(x - lifted, y - lifted) ? "w0" : null));  g.part("newNote", () => g.field({ x: bx, y: by }, (x, y) => {    const c = sample(x, y);    if (c) return PKEY[c];    // a 1px ink rim where the sheet flies over open air, so it reads against any floor    if (!onPanel(x, y) && (sample(x - 1, y) || sample(x + 1, y) || sample(x, y - 1) || sample(x, y + 1))) return "ink";    return null;  }));}function trail(g, dx, dy, len) {  // speed lines streaming back toward where the sheet came from (up and to the right)  const cx = MAP.x + noteW(MAP) / 2 + dx, cy = MAP.y + noteH(MAP) / 2 + dy;  g.detached("trail", () => {    for (const [ox, oy, l] of [[-1, -2, len], [2, 1, len - 1]]) {      for (let i = 0; i < l; i++) {        const r = 5 + i;        const x = Math.round(cx + ox + 0.8 * r), y = Math.round(cy + oy - 0.6 * r);        if (x >= 0 && x < W && y >= 0) g.pixel(x, y, i === 0 ? "n5" : i < 3 ? "n4" : "n3");      }    }  });}function flash(g, x, y, size) {  // impact: four short ticks burst out around the pin  g.detached("flash", () => {    const r = size > 1 ? 2 : 3;    for (const [sx, sy] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) {      g.pixel(x + sx * r, y + sy * r, "n5");      if (size > 1) g.pixel(x + sx * (r + 1), y + sy * (r + 1), "g3");    }    if (size > 1) g.pixel(x, y - 3, "n5");  });}// ---- motion ---------------------------------------------------------------------------------------const N = 12;const FLUTTER = [0, 0, 0, 1, 2, 3, 2, 3, 2, 1, 0, 0];                   // corner lift per idle frame// post: [dx, dy, degrees, lifted shadow offset, corner bounce, pin shown, flash size]const POST = [  [9, -9, 34, 2, 0, false, 0],  [8, -5, 20, 2, 0, false, 0],  [3, -2, 8, 2, 0, false, 0],  [1, 0, 2, 1, 0, false, 0],  [0, 0, 0, 0, 0, true, 2],  [0, 0, 0, 0, 2, true, 1],  [0, 0, 0, 0, 1, true, 0],  [0, 0, 0, 0, 0, true, 0],];const NoticeBoard = sprite(pal, (g, t, state) => {  contactShadow(g, CX, GROUND, 20, 2);  g.outlined("ink", () => { posts(g); boardFrame(g); roof(g); });  let lift = 0;  if (state !== "post") lift = FLUTTER[Math.round(t * N) % N];  note(g, "scrap", SCRAP, 0);  note(g, "wanted", WANTED, 0);  note(g, "letter", LETTER, 0);  note(g, "slanted", SLANT, 0);  note(g, "oldNote", OLD, lift);  if (state === "idle_full") note(g, "mapNote", MAP, 0);  if (state === "post") {    const f = Math.min(POST.length - 1, Math.round(t * POST.length));    const [dx, dy, deg, lifted, bounce, pinned, fl] = POST[f];    if (deg === 0 && dx === 0 && dy === 0) {      note(g, "mapNote", Object.assign({}, MAP, { pin: pinned ? MAP.pin : null }), bounce);    } else {      flyingNote(g, dx, dy, deg, lifted);      if (f < 3) trail(g, dx, dy, 5 - f);    }    if (fl) flash(g, MAP.pin[0], MAP.pin[1], fl);  }});submit(NoticeBoard, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle" },    { name: "post", args: ["post"], animate: { fps: 10, duration: POST.length / 10, loop: false } },    { name: "idle_full", args: ["idle_full"] },  ],});

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.