Tessarune
Tavern & Market · grain_sack.js

穀物袋

32×32 · JavaScript 251 行

32×32 の小物で、見下ろし 3/4 視点です。床にくたりと置いた麻袋で、口を紐で縛り、その上の布がひだになって広がっています。しわ、織り目、織り込んだ 2 本の青い縞、右下のほつれた穴があり、足元にはこぼれた小麦が小さく積もっています。重なる層ごとに輪郭線が付くように、毎コマを一つのバッファに一層ずつ重ねています。idle(待機、16 コマのループ)は 1 ループに一度、穴から粒が一つこぼれて山に落ちます。mouse(11 コマの一回再生)は袋の後ろからネズミが顔を出し、2 回においを嗅ぎ、耳を動かして引っ込みます。

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

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

grain_sack.js · 1–11
// Grain sack — 32x32, top-down 3/4, key light top-left.// A burlap sack slumped on the floor, its neck tied with twine, the gathered cloth flaring above the tie in pleats.// The body is shaded from a pseudo-normal (profile half-width per row -> across and up components), then gets// countable marks: folds pulled down from the tie (dark crease + lit lip), a staggered weave stitch every 3 rows,// two woven blue stripes, a frayed hole low on the right. Spilled wheat lies in a little heap at its foot.// Every frame is composited into one keyed buffer here, layer by layer, so a layer that overlaps an earlier one gets// its own ink seam (outlined() only inks empty pixels).// Variants://   idle   loop 16f      — the sack is still; once a loop a grain slips out of the hole and drops onto the heap//   mouse  one-shot 11f  — a mouse pokes its head out from behind the sack, sniffs twice, flicks an ear and ducks//                          back; ends on idle f0

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

grain_sack.js · 244–251
submit(Sack, {  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: "mouse", args: ["mouse"], animate: { fps: 10, duration: N_MOUSE / 10, loop: false } },  ],});
プログラム全体を表示 (251 行)
grain_sack.js · 251 行
// Grain sack — 32x32, top-down 3/4, key light top-left.// A burlap sack slumped on the floor, its neck tied with twine, the gathered cloth flaring above the tie in pleats.// The body is shaded from a pseudo-normal (profile half-width per row -> across and up components), then gets// countable marks: folds pulled down from the tie (dark crease + lit lip), a staggered weave stitch every 3 rows,// two woven blue stripes, a frayed hole low on the right. Spilled wheat lies in a little heap at its foot.// Every frame is composited into one keyed buffer here, layer by layer, so a layer that overlaps an earlier one gets// its own ink seam (outlined() only inks empty pixels).// Variants://   idle   loop 16f      — the sack is still; once a loop a grain slips out of the hole and drops onto the heap//   mouse  one-shot 11f  — a mouse pokes its head out from behind the sack, sniffs twice, flicks an ear and ducks//                          back; ends on idle f0const W = 32, H = 32, CX = 16, GROUND = 29;const N_IDLE = 16, N_MOUSE = 11;const BURLAP = ["w1", "w2", "w3", "w4"];// ---- sack shape --------------------------------------------------------------------------------------------------const NECK_Y = 9, BOT_Y = 28;const PROF = [[9, 3.6], [10, 5.6], [11, 7.0], [13, 8.0], [17, 8.3], [22, 8.7], [26, 9.2], [28, 9.1]];function hwAt(y) {  for (let k = 1; k < PROF.length; k++) {    const [y0, r0] = PROF[k - 1], [y1, r1] = PROF[k];    if (y <= y1) { const u = (y - y0) / (y1 - y0); return r0 + (r1 - r0) * u; }  }  return PROF[PROF.length - 1][1];}const cxAt = (y) => CX + (y < 16 ? 0.6 * (16 - y) / 7 : 0);      // the neck leans a little to the right// the base: an ellipse footprint, so the bottom edge is a curveconst bottomAt = (x) => { const u = (x + 0.5 - CX) / 9.6; return u * u >= 1 ? -1 : Math.floor(25.8 + 3.0 * Math.sqrt(1 - u * u)); };// folds pulled down from the tie: [x offset at the neck, x offset at the end, end row]const FOLDS = [[-2.4, -6.2, 16], [-0.6, -2.4, 19], [1.3, 2.4, 17], [2.8, 6.4, 15]];function sackPixels() {  const m = new Map();  const tone = new Map();  for (let y = NECK_Y; y <= BOT_Y; y++) {    const hw = hwAt(y + 0.5), cx = cxAt(y);    const slope = (hwAt(y + 1.5) - hwAt(y - 0.5)) / 2;             // >0 on the shoulders: faces up    for (let x = 0; x < W; x++) {      const nx = (x + 0.5 - cx) / hw;      if (Math.abs(nx) > 1) continue;      const bot = bottomAt(x);      if (bot < 0 || y > bot) continue;      const nz = Math.sqrt(Math.max(0, 1 - nx * nx));      const up = Math.max(-0.6, Math.min(0.8, slope * 0.55)) - (y > 24 ? (y - 24) * 0.12 : 0);      const d = -0.6 * nx + 0.55 * up + 0.62 * nz;      let lv = d > 0.78 ? 3 : d > 0.45 ? 2 : d > 0.05 ? 1 : 0;      if (y === bot && lv > 0) lv = Math.max(0, lv - 1);            // the base rolls under      tone.set(y * W + x, lv);    }  }  // folds: dark crease with a lit lip on its left  for (const [a, b, yEnd] of FOLDS) {    for (let y = NECK_Y + 1; y <= yEnd; y++) {      const s = (y - NECK_Y) / (yEnd - NECK_Y), e = Math.pow(s, 0.7);      const x = Math.round(cxAt(y) + a + (b - a) * e);      const i = y * W + x;      if (!tone.has(i)) continue;      if (s > 0.85 && (y & 1)) continue;                             // the crease thins out at its end      tone.set(i, Math.max(0, tone.get(i) - 1));      if (tone.has(i - 1) && s < 0.8) tone.set(i - 1, Math.min(3, tone.get(i - 1) + 1));    }  }  for (const [i, lv] of tone) {    const x = i % W, y = (i - x) / W;    let k = lv;    // weave stitch: a staggered dotted weft every third row on the lit and mid cloth (countable, never random)    const row = y - NECK_Y - 2;    if (row >= 0 && row % 3 === 0 && k >= 2) {      const off = (row / 3) % 2 ? 2 : 0;      if ((x + off) % 4 === 0) k -= 1;    }    m.set(i, BURLAP[k]);  }  // two woven stripes down the front (a miller's sack): blue on the lit cloth, deep blue where it turns away  // they start under the shoulder and bow outward with the bulge of the sack  for (let y = NECK_Y + 3; y <= BOT_Y; y++) {    const cx = cxAt(y);    for (const off of [-4.2, -2.2]) {      const x = Math.round(cx + off * (hwAt(y + 0.5) / 8.4) - 0.5), i = y * W + x;      if (!m.has(i) || y > bottomAt(x)) continue;      const lv = tone.get(i);      m.set(i, lv >= 2 ? "b1" : "b0");    }  }  // frayed hole low on the right: dark mouth, a pale torn lip above, a loose thread below  m.set(HOLE_Y * W + HOLE_X, "w0"); m.set(HOLE_Y * W + HOLE_X + 1, "w0");  m.set((HOLE_Y - 1) * W + HOLE_X, "w4"); m.set((HOLE_Y - 1) * W + HOLE_X + 1, "w3");  m.set((HOLE_Y + 1) * W + HOLE_X + 1, "w3");  return m;}const HOLE_X = 21, HOLE_Y = 20;// gathered cloth above the tie: pleats 2px wide, alternately lit / shaded, a jagged top edgefunction ruffle() {  const m = new Map();  const rows = [[2, 3.8], [3, 4.2], [4, 4.6], [5, 4.2], [6, 3.4]];  for (const [y, hw] of rows) {    const cx = cxAt(y) + 0.3;    for (let x = Math.floor(cx - hw); x <= Math.ceil(cx + hw - 1); x++) {      const nx = (x + 0.5 - cx) / hw;      if (Math.abs(nx) > 1.05) continue;      const p = Math.floor((x - 9) / 2), left = (x - 9) % 2 === 0;      const top = 1 + (p % 2);                                        // pleat tips at alternating heights      if (y < top + 1) continue;      let lv = left ? 3 : 2;      if (nx > 0.35) lv -= 1;                                          // the right side turns from the light      if (nx < -0.6 && !left) lv += 1;      if (y === top + 1 && left) lv = Math.min(3, lv + 1);      if (y >= 6) lv = Math.max(0, lv - 1);                            // pinched into the tie's shadow      m.set(y * W + x, BURLAP[Math.max(0, Math.min(3, lv))]);    }  }  return m;}// twine: two turns around the neck, a knot on the front right, two loose ends hanging over the clothfunction twine() {  const m = new Map();  const cx = Math.round(cxAt(7));  for (let x = cx - 3; x <= cx + 3; x++) {    m.set(7 * W + x, x < cx - 1 ? "n5" : x < cx + 2 ? "n4" : "n3");    m.set(8 * W + x, x < cx ? "n3" : "n2");  }  m.set(7 * W + cx + 1, "n5"); m.set(8 * W + cx + 1, "n4");          // knot  m.set(9 * W + cx + 1, "n4"); m.set(10 * W + cx + 1, "n3"); m.set(11 * W + cx + 1, "n2");  m.set(9 * W + cx + 2, "n3"); m.set(10 * W + cx + 3, "n2");  return m;}// spilled heap at the sack's foot (front right), grains as a regular stagger with a lit top-leftfunction heap() {  // golden wheat: a mound with a jagged crest of single grains (every other one missing), lit on the left, a regular  // sparse fleck of husks and shaded grains inside, the foot in shade  const m = new Map();  const rows = [[25, 23, 24], [26, 21, 26], [27, 20, 27], [28, 20, 27], [29, 21, 26]];  const topOf = (x) => rows.find((r) => x >= r[1] && x <= r[2])[0];  for (const [y, x0, x1] of rows) for (let x = x0; x <= x1; x++) {    const depth = y - topOf(x);    if (depth === 0 && y > 25 && (x & 1)) continue;    let k = depth === 0 ? (x < 24 ? "g3" : "g2") : "g2";    if (y >= 28 || (x === x1 && depth > 0)) k = "g1";    if (y === 29) k = x < 23 ? "g1" : "g0";    if (depth > 0 && y < 29 && (x * 2 + y * 3) % 5 === 0) k = y >= 28 ? "g0" : "w4";    else if (depth > 0 && y < 28 && (x * 2 + y * 3) % 5 === 2) k = "g1";    m.set(y * W + x, k);  }  return m;}const LOOSE = [[9, 30], [28, 30], [29, 27], [19, 30], [22, 24]];// mouse, facing left; (0,0) is the tip of the noseconst MOUSE = [  [3, -3, "n3"], [4, -3, "n2"],  [3, -2, "v3"], [4, -2, "n3"], [5, -2, "n2"],  [1, -1, "n3"], [2, -1, "n4"], [3, -1, "ink"], [4, -1, "n3"], [5, -1, "n2"], [6, -1, "n2"], [7, -1, "n2"],  [0, 0, "v3"], [1, 0, "n4"], [2, 0, "n4"], [3, 0, "n3"], [4, 0, "n3"], [5, 0, "n2"], [6, 0, "n2"], [7, 0, "n1"],  [1, 1, "n2"], [2, 1, "n3"], [3, 1, "n3"], [4, 1, "n2"], [5, 1, "n2"], [6, 1, "n1"], [7, 1, "n1"],  [2, 2, "n4"], [3, 2, "n3"], [5, 2, "n1"], [6, 2, "n1"], [7, 2, "n1"],];function mouseMap(nx, ny, earDown) {  const m = new Map();  for (const [dx, dy, k] of MOUSE) {    if (earDown && dy <= -2) continue;    const x = nx + dx, y = ny + dy;    if (x >= 0 && x < W && y >= 0 && y < H) m.set(y * W + x, k);  }  if (earDown) { m.set((ny - 2) * W + nx + 4, "n3"); m.set((ny - 2) * W + nx + 5, "n2"); }   // ear laid back  return m;}// ---- compositor -------------------------------------------------------------------------------------------------function compose(layers) {  const key = new Array(W * H).fill(null), part = new Array(W * H).fill(null);  for (const { name, map, seam } of layers) {    for (const [i, k] of map) { key[i] = k; part[i] = name; }    if (!seam) continue;    for (const i of map.keys()) {      const x = i % W, y = (i - x) / W;      for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {        if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;        const j = ny * W + nx;        if (!map.has(j) && key[j] !== null && part[j] !== name) { key[j] = "ink"; part[j] = name; }      }    }  }  return { key, part };}function emit(g, comp) {  const names = [];  for (const p of comp.part) if (p && !names.includes(p)) names.push(p);  g.outlined("ink", () => {    for (const n of names) g.part(n, () => g.field({ x: [0, W - 1], y: [0, H - 1] }, (x, y) => {      const i = y * W + x;      return comp.part[i] === n ? comp.key[i] : null;    }));  });}const CACHE = {};const SACK = () => CACHE.sack || (CACHE.sack = sackPixels());const RUF = () => CACHE.ruf || (CACHE.ruf = ruffle());const TWN = () => CACHE.twn || (CACHE.twn = twine());const HEAP = () => CACHE.heap || (CACHE.heap = heap());function scene(g, mouse) {  const layers = [];  if (mouse) layers.push({ name: "mouse", map: mouse, seam: false });  layers.push({ name: "ruffle", map: RUF(), seam: false });  layers.push({ name: "sack", map: SACK(), seam: true });  layers.push({ name: "twine", map: TWN(), seam: false });  layers.push({ name: "heap", map: HEAP(), seam: true });  emit(g, compose(layers));  g.detached("loose", () => { LOOSE.forEach(([x, y], k) => g.pixel(x, y, k % 2 ? "g1" : "g2")); });}const Sack = sprite(pal, (g, t, mode) => {  contactShadow(g, CX, GROUND - 1, 11, 2);  if (mode === "idle") {    const f = Math.round(t * N_IDLE) % N_IDLE;    scene(g, null);    // one grain per loop: peeks from the hole, drops, lands on the heap and rolls into it    const path = { 7: [HOLE_X + 1, HOLE_Y], 8: [HOLE_X + 1, HOLE_Y + 1], 9: [HOLE_X + 1, HOLE_Y + 3], 10: [HOLE_X + 1, HOLE_Y + 4], 11: [HOLE_X + 2, HOLE_Y + 5] };    if (path[f]) g.detached("grain", () => { const [x, y] = path[f]; g.pixel(x, y, f === 7 ? "g2" : "g3"); if (f >= 8 && f <= 9) g.pixel(x, y - 1, "g1"); });    return;  }  const f = Math.min(N_MOUSE - 1, Math.round(t * N_MOUSE));  const NOSE = [null, 5, 3, 1, 1, 1, 1, 1, 1, 4, null][f];  const BOB = [0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0][f];  const earDown = f === 7 || f === 9;  scene(g, NOSE === null ? null : mouseMap(NOSE, 25 + BOB, earDown));  if (NOSE !== null && f >= 3 && f <= 8) {    // whiskers: two pale hairs off the snout, fanned wider on the sniffs    const y = 25 + BOB, wide = BOB !== 0;    g.detached("whiskers", () => {      g.pixel(NOSE - 1, y - 1 - (wide ? 1 : 0), "n4");      g.pixel(NOSE - 1, y + 1 + (wide ? 1 : 0), "n4");      if (wide && NOSE >= 2) g.pixel(NOSE - 2, y - 2, "n3");    });  }});submit(Sack, {  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: "mouse", args: ["mouse"], animate: { fps: 10, duration: N_MOUSE / 10, loop: false } },  ],});

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

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

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