Tessarune
Tavern & Market · tavern_table.js

酒場のテーブル

48×48 · JavaScript 293 行

48×48 の床置きの小物で、見下ろし 3/4 視点です。3 本の脚が広がるろくろ挽きの一本脚に載った丸い板のテーブルで、両脇に三本脚の腰掛けが入っています。上には真鍮の皿に立てた獣脂のろうそく、丸パンとチーズの皿、真鍮のたがをはめた黒っぽい木のジョッキ 2 つ。ろうそくの暖かい光が板の上にたまります。idle(待機、12 コマのループ)はろうそくが揺れて光のたまりも息づき、泡が立っては落ち着きます。clink(8 コマの一回再生)はジョッキが影を残して持ち上がり、寄って打ち合わされ、泡が飛んで、また置かれます。

酒場のテーブル — 描画結果6×
このプログラムで描画(変種「clink」) · 48×48 · 8 コマ
Tavern & Market動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。