Tavern & Market · tavern_table.js
酒場のテーブル
48×48 の床置きの小物で、見下ろし 3/4 視点です。3 本の脚が広がるろくろ挽きの一本脚に載った丸い板のテーブルで、両脇に三本脚の腰掛けが入っています。上には真鍮の皿に立てた獣脂のろうそく、丸パンとチーズの皿、真鍮のたがをはめた黒っぽい木のジョッキ 2 つ。ろうそくの暖かい光が板の上にたまります。idle(待機、12 コマのループ)はろうそくが揺れて光のたまりも息づき、泡が立っては落ち着きます。clink(8 コマの一回再生)はジョッキが影を残して持ち上がり、寄って打ち合わされ、泡が飛んで、また置かれます。
6×
動くスプライト素材: Tavern & Market(itch.io)Vol.3・酒場と市場の小物 12 点・32 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
tavern_table.js · 1–8
// Tavern table — 48x48 floor prop, top-down 3/4, key light top-left.// A round plank table on a turned pedestal with three splayed feet, a three-legged stool tucked in at each side.// On the top: a tallow candle in a brass dish (back), a plate with a bread roll and cheese (front), and two dark-stave tankards with brass bands and foaming heads (front left / front right).// The candle pools warm light on the planks around it.// Variants:// idle loop 12f — the candle flickers and its light pool breathes with it, the foam heads bubble and settle// clink one-shot 8f — the tankards lift off the table (their shadows stay behind), lean in and clink, a splash of// foam flies up, then they set back down; the last frame == idle frame 0最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
tavern_table.js · 286–293
submit(Table, { 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: "clink", args: ["clink"], animate: { fps: 10, duration: N_CLINK / 10, loop: false } }, ],});プログラム全体を表示 (293 行)
tavern_table.js · 293 行
// Tavern table — 48x48 floor prop, top-down 3/4, key light top-left.// A round plank table on a turned pedestal with three splayed feet, a three-legged stool tucked in at each side.// On the top: a tallow candle in a brass dish (back), a plate with a bread roll and cheese (front), and two dark-stave tankards with brass bands and foaming heads (front left / front right).// The candle pools warm light on the planks around it.// Variants:// idle loop 12f — the candle flickers and its light pool breathes with it, the foam heads bubble and settle// clink one-shot 8f — the tankards lift off the table (their shadows stay behind), lean in and clink, a splash of// foam flies up, then they set back down; the last frame == idle frame 0const W = 48, H = 48, CX = 24, GROUND = 45;const N_IDLE = 12, N_CLINK = 8;const TOP = { cx: 24, cy: 24, rx: 16, ry: 7.5 };const CANDLE = { x: 24, top: 12, base: 19 }; // wax column x-1..x+1, dish under baseconst PLATE = { cx: 24, cy: 28.5 };const MUG_L = { x0: 14, yb: 27, side: -1, seed: 0.0 };const MUG_R = { x0: 29, yb: 27, side: 1, seed: 0.45 };const WOOD = ["w0", "w1", "w2", "w3", "w4"];// ---- candle flame (the skull_candles idiom: odd-width rows on a staircase axis that bends with lean) --------function flick(t, s) { const dh = Math.round(0.75 * Math.sin(TAU * (2 * t + s)) + 0.45 * Math.sin(TAU * (3 * t + 2.3 * s))); const lean = 0.7 * Math.sin(TAU * (t + s)) + 0.45 * Math.sin(TAU * (3 * t + 1.7 * s)); return { dh, lean };}function candleFlame(g, fx, fy, h, lean) { for (let i = 0; i < h; i++) { const k = (i + 0.5) / h, y = fy - i; const ax = fx + Math.round(lean * k * k); if (k > 0.12 && k < 0.62) { const edge = k > 0.18 && k < 0.5 ? "fl1" : "fl0"; g.pixel(ax - 1, y, edge); g.pixel(ax + 1, y, edge); } g.pixel(ax, y, k < 0.12 ? "fl1" : k < 0.5 ? "fl3" : k < 0.68 ? "fl2" : k < 0.86 ? "fl1" : "fl0"); }}// ---- stools ------------------------------------------------------------------------------------------------function stool(g, name, cx) { const cy = 35, rx = 4.6, ry = 1.7; g.part(name, () => { // three legs: left and right splay out, the back one peeks out between them g.line([cx, 37], [cx, 42], "w0"); g.line([cx - 4, 36], [cx - 5, 44], "w2"); g.line([cx + 4, 36], [cx + 5, 44], "w1"); g.line([cx - 4, 40], [cx + 4, 40], "w1"); // stretcher rung g.pixel(cx - 3, 40, "w2"); // seat: a flat round slab — a lit top face and a two-row edge band g.field({ x: [cx - 5, cx + 5], y: [cy - 2, cy + 4] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) return null; const s = Math.sqrt(1 - nx * nx); if (y + 0.5 < cy - ry * s || y + 0.5 > cy + ry * s + 2) return null; if (y + 0.5 > cy + ry * s) return y + 0.5 > cy + ry * s + 1 ? (nx < -0.3 ? "w1" : "w0") : nx < -0.4 ? "w2" : "w1"; const ny = (y + 0.5 - cy) / ry; return -0.6 * nx - 0.5 * ny > 0.5 ? "w4" : nx > 0.45 ? "w2" : "w3"; }); });}// ---- table -------------------------------------------------------------------------------------------------function pedestal(g) { g.part("pedestal", () => { // three splayed feet g.fillPoly([[22, 39], [16, 42], [15, 44], [22, 42]], "w2"); g.line([22, 39], [16, 42], "w3"); g.fillPoly([[26, 39], [32, 42], [33, 44], [26, 42]], "w1"); g.line([26, 39], [32, 42], "w2"); g.rect({ x: [23, 25], y: [40, 45] }, "w2", { fill: "w2" }); g.line([23, 40], [23, 45], "w3"); g.line([25, 41], [25, 45], "w1"); // turned column with a bead at mid height, in the table's shadow at the top g.field({ x: [21, 27], y: [32, 40] }, (x, y) => { const hw = y === 35 || y === 36 ? 3 : 2; const dx = x - 24; if (dx < -hw || dx > hw) return null; const u = (dx + hw) / (2 * hw); if (y <= 33) return u < 0.45 ? "w1" : "w0"; if (y === 36) return u < 0.3 ? "w2" : "w1"; // underside of the bead return u < 0.2 ? "w4" : u < 0.45 ? "w3" : u < 0.8 ? "w2" : "w1"; }); });}function poolAt(x, y, R) { // candle light on the planks: a hot core, a solid ring, a checker-dithered rim const d = Math.hypot(x + 0.5 - CANDLE.x, (y + 0.5 - CANDLE.base - 1) * 2.1); return d < R * 0.45 ? 2 : d < R - 1.4 ? 1 : d < R && chk(x, y) ? 1 : 0;}function tableTop(g, R) { const { cx, cy, rx, ry } = TOP; g.part("tabletop", () => { g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 2] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) return null; const s = Math.sqrt(1 - nx * nx); if (y + 0.5 < cy - ry * s || y + 0.5 > cy + ry * s + 2) return null; const ny = (y + 0.5 - cy) / ry; if (nx * nx + ny * ny <= 1) { // the top: planks running front to back, the top-left rim catches the key light let k = nx > 0.5 ? 2 : 3; const rim = -0.62 * nx - 0.5 * ny > 0.66; if (rim) k = 4; const seam = (x + 48) % 5 === 1 && Math.abs(ny) < 0.93 && !rim; if (seam) return nx > 0.5 ? "w1" : poolAt(x, y, R) ? "w3" : "w2"; return WOOD[Math.min(4, k + poolAt(x, y, R))]; } return y + 0.5 > cy + ry * s + 1 ? (nx < -0.4 ? "w1" : "w0") : nx < -0.5 ? "w2" : "w1"; // near edge }); });}// ---- things on the table -----------------------------------------------------------------------------------// g.outlined only inks where the neighbour is empty, so an object standing ON the table would melt into the// planks. Table objects are drawn into a small layer first, then emitted with their own ink ring.function layer() { const m = new Map(); const put = (x, y, k) => { if (x >= 0 && y >= 0 && x < W && y < H) m.set(y * W + x, k); }; return { m, put };}function emit(g, name, m, ringKey, lowOnly) { const ring = new Map(); let x0 = W, x1 = -1, y0 = H, y1 = -1; const grow = (x, y) => { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }; for (const i of m.keys()) { const x = i % W, y = (i - x) / W; grow(x, y); const nb = lowOnly ? [[x + 1, y], [x, y + 1]] : [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]; for (const [nx, ny] of nb) { if (nx < 0 || ny < 0 || nx >= W || ny >= H || m.has(ny * W + nx)) continue; ring.set(ny * W + nx, ringKey); grow(nx, ny); } } if (x1 < 0) return; g.part(name, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => m.get(y * W + x) ?? ring.get(y * W + x) ?? null));}function candle(g, t) { const { x, top, base } = CANDLE; const L = layer(), put = L.put; // brass dish: a shallow ellipse, lit rim at the back-left for (let yy = base - 1; yy <= base + 1; yy++) for (let xx = x - 3; xx <= x + 3; xx++) { const nx = (xx + 0.5 - x - 0.5) / 3.5, ny = (yy + 0.5 - base - 0.5) / 1.5; if (nx * nx + ny * ny > 1) continue; put(xx, yy, yy === base - 1 ? (xx < x ? "g3" : "g2") : yy === base + 1 ? (xx < x ? "g1" : "g0") : xx < x - 1 ? "g2" : "g1"); } // tallow column: lit left, dimmer near the dish, one fat drip on the lit side const cols = ["n5", "w4", "w3"]; for (let i = 0; i < 3; i++) for (let y = top + 1; y <= base - 1; y++) put(x - 1 + i, y, y > base - 3 && i > 0 ? "w3" : cols[i]); put(x - 1, top, "n5"); put(x, top, "n5"); put(x + 1, top, "w4"); put(x, top + 1, "fl3"); // flame light through the tallow for (let y = top + 2; y <= top + 4; y++) put(x - 2, y, "n5"); put(x - 2, top + 5, "w4"); emit(g, "candle", L.m, "ink"); const f = flick(t, 0.2); g.part("flame", () => { g.pixel(x, top - 1, "ink"); candleFlame(g, x, top - 2, 6 + f.dh, f.lean); });}function plate(g) { const { cx, cy } = PLATE; const P = layer(); for (let y = Math.floor(cy - 2); y <= cy + 2; y++) for (let x = cx - 6; x <= cx + 6; x++) { const nx = (x + 0.5 - cx) / 5.6, ny = (y + 0.5 - cy) / 2.1; const r = nx * nx + ny * ny; if (r > 1) continue; P.put(x, y, r > 0.55 ? (ny > 0.3 ? "n3" : nx < 0 ? "n5" : "n4") : nx < 0.45 ? "n5" : "n4"); // rim / well, lit on the left } emit(g, "plate", P.m, "ink"); // a bread roll (left) and a wedge of cheese (right); they cast a grey shadow on the plate const F = layer(), put = F.put; for (const [x, y, k] of [ [21, 26, "w4"], [22, 26, "w3"], [20, 27, "w4"], [21, 27, "w3"], [22, 27, "w2"], [23, 27, "w3"], [20, 28, "w3"], [21, 28, "w2"], [22, 28, "w3"], [23, 28, "w2"], [26, 27, "g3"], [27, 27, "g2"], [25, 28, "g3"], [26, 28, "g2"], [27, 28, "g1"], ]) put(x, y, k); emit(g, "food", F.m, "n3", true);}// a dark-stave tankard with brass bands. x0 = left column of the body, yb = bottom row, side = handle side,// tilt leans the top toward +x (px at the rim), dome = extra foam height (0..1), bubble = which bubble showsfunction mug(g, name, m, dx, dy, tilt, dome, bubble) { const x0 = m.x0 + dx, yb = m.yb + dy, side = m.side; const sh = (y) => Math.round((tilt * (yb - y)) / 8); const BODY = ["w2", "w3", "w2", "w1", "w0"]; const BAND = ["g2", "g3", "g2", "g1", "g0"]; const L = layer(); const px = (c, y, k) => L.put(x0 + c + sh(y), y, k); // handle (outer side): a C loop; its hole is left empty so the ring fills it with ink const hc = side < 0 ? -1 : 5, oc = side < 0 ? -2 : 6; const lit = side < 0 ? ["w3", "w2"] : ["w1", "w0"]; px(hc, yb - 5, lit[0]); px(oc, yb - 4, lit[0]); px(oc, yb - 3, lit[1]); px(hc, yb - 2, lit[1]); // body for (let y = yb - 6; y <= yb; y++) for (let c = 0; c < 5; c++) { let k = y === yb - 5 || y === yb - 1 ? BAND[c] : BODY[c]; if (y === yb) k = c < 2 ? "w1" : "w0"; px(c, y, k); } // foam head: a shaded lip at the rim, a lit top, a dome; one spill + drip down the lit side for (let c = 0; c < 5; c++) px(c, yb - 7, c === 0 ? "n4" : c < 3 ? "n3" : "n2"); for (let c = 0; c < 5; c++) px(c, yb - 8, c < 2 ? "n5" : c < 4 ? "n4" : "n3"); for (let c = 1; c < 4; c++) px(c, yb - 9, c < 3 ? "n5" : "n4"); if (dome) { px(1, yb - 10, "n5"); px(2, yb - 10, "n4"); } px(-1, yb - 8, "n4"); px(-1, yb - 7, "n5"); px(1, yb - 6, "n5"); px(1, yb - 5, "n4"); // spill and drip if (bubble >= 0) { const [bc, br] = [[2, 9], [3, 8], [1, 8]][bubble]; px(bc, yb - br, "n3"); } emit(g, name, L.m, "ink");}function mugShadow(g, m, dx, dy) { // the tankard's shadow stays on the planks and shrinks as it lifts away g.part("mugShadow", () => { const x0 = m.x0 + dx, y = m.yb + 1; if (dy === 0) { g.line([x0, y], [x0 + 5, y], "w0"); return; } const w = dy > -5 ? 4 : 2; for (let x = x0 + 2 - (w >> 1); x <= x0 + 2 + (w >> 1); x++) { g.pixel(x, y - 1, "w1"); if (chk(x, y)) g.pixel(x, y, "w0"); } });}// ---- sprite ------------------------------------------------------------------------------------------------function foamState(t, m) { const p = (t + m.seed) % 1; return { dome: p < 0.5 ? 1 : 0, bubble: p < 0.17 ? 0 : p < 0.34 ? 1 : p > 0.67 && p < 0.84 ? 2 : -1 };}function scene(g, t, pose) { contactShadow(g, CX, GROUND, 11, 1); contactShadow(g, 8, GROUND, 5, 1); contactShadow(g, 40, GROUND, 5, 1); g.outlined("ink", () => stool(g, "stoolL", 8)); g.outlined("ink", () => stool(g, "stoolR", 40)); g.outlined("ink", () => pedestal(g)); const f = flick(t, 0.2); g.outlined("ink", () => tableTop(g, 7.5 + 0.9 * f.dh)); mugShadow(g, MUG_L, 0, pose.L[1]); mugShadow(g, MUG_R, 0, pose.R[1]); plate(g); candle(g, t); const fl = foamState(t, MUG_L), fr = foamState(t, MUG_R); const still = pose.dome !== undefined; mug(g, "mugL", MUG_L, pose.L[0], pose.L[1], pose.L[2], still ? pose.dome : fl.dome, still ? -1 : fl.bubble); mug(g, "mugR", MUG_R, pose.R[0], pose.R[1], pose.R[2], still ? pose.dome : fr.dome, still ? -1 : fr.bubble);}const REST = { L: [0, 0, 0], R: [0, 0, 0] };// clink keys: [dx, dy, tilt] per mugconst CLINK = [ null, { L: [1, -3, 0], R: [-1, -3, 0] }, { L: [3, -7, 1], R: [-3, -7, -1] }, { L: [5, -9, 2], R: [-5, -9, -2], dome: 0 }, // clink { L: [4, -10, 1], R: [-4, -10, -1], dome: 1 }, // rebound, foam in the air { L: [2, -6, 0], R: [-2, -6, 0], dome: 1 }, { L: [0, -2, 0], R: [0, -2, 0] }, null,];function splash(g, f) { const cx = 24, cy = 7; // foam: small clumps (a cream pixel with a grey underside) bursting up from the rims, then falling const CLUMPS = { 3: [[0, -1], [-2, -1], [2, -2], [-1, -3], [1, -4]], 4: [[0, -6], [-3, -5], [3, -4], [-5, -2], [5, -3], [-1, -3], [2, -1]], 5: [[-4, -4], [4, -3], [-7, 1], [7, 0], [0, -5]], 6: [[-8, 5], [8, 4], [-1, 0]], }[f]; if (!CLUMPS) return; g.detached("splash", () => { CLUMPS.forEach(([dx, dy], i) => { const x = cx + dx, y = cy + dy; g.pixel(x, y, "n5"); if (f <= 4 && i % 2 === 0) { g.pixel(x + 1, y, "n5"); g.pixel(x + 1, y + 1, "n3"); } g.pixel(x, y + 1, f >= 5 ? "n3" : "n4"); }); });}function clinkStar(g) { g.detached("clinkStar", () => { for (const [x, y, k] of [[24, 5, "g3"], [24, 4, "n5"], [17, 10, "g3"], [31, 10, "g3"], [16, 10, "g2"], [32, 10, "g2"], [20, 6, "g2"], [28, 6, "g2"]]) g.pixel(x, y, k); });}const Table = sprite(pal, (g, t, mode) => { if (mode === "idle") { scene(g, t, REST); return; } const f = Math.min(N_CLINK - 1, Math.round(t * N_CLINK)); const pose = CLINK[f]; if (!pose) { scene(g, 0, REST); return; } scene(g, 0, pose); if (f === 3) clinkStar(g); splash(g, f);});submit(Table, { 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: "clink", args: ["clink"], animate: { fps: 10, duration: N_CLINK / 10, loop: false } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。