Tavern & Market · hanging_sign.js
酒場の吊り看板
48×48 の壁に付ける小物で、3/4 視点、接地影はありません。鋲を打った鉄の壁金具から、渦巻きの支えと先の丸まった鉄の腕が出ていて、額縁付きの木の看板が 2 本の鎖で下がっています。緑の板には泡立つ金色のジョッキが描かれています。長さの同じ 2 本の鎖が平行四辺形の仕組みになるので、看板は弧を描いて揺れても傾かず、傾くのは鎖だけです。idle(待機、12 コマのループ)は ±2px で静かに揺れ、swing(10 コマの一回再生)は突風にあおられてから落ち着きます。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。