Tessarune
Tavern & Market · hanging_sign.js

酒場の吊り看板

48×48 · JavaScript 180 行

48×48 の壁に付ける小物で、3/4 視点、接地影はありません。鋲を打った鉄の壁金具から、渦巻きの支えと先の丸まった鉄の腕が出ていて、額縁付きの木の看板が 2 本の鎖で下がっています。緑の板には泡立つ金色のジョッキが描かれています。長さの同じ 2 本の鎖が平行四辺形の仕組みになるので、看板は弧を描いて揺れても傾かず、傾くのは鎖だけです。idle(待機、12 コマのループ)は ±2px で静かに揺れ、swing(10 コマの一回再生)は突風にあおられてから落ち着きます。

酒場の吊り看板 — 描画結果6×
このプログラムで描画(変種「swing」) · 48×48 · 10 コマ
Tavern & Market動くスプライト素材: Tavern & Market(itch.io)Vol.3・酒場と市場の小物 12 点・32 アニメ

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

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).

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

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 } },  ],});
プログラム全体を表示 (180 行)
hanging_sign.js · 180 行
// 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 } },  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。