Tessarune
Tavern & Market · market_stall.js

市場の屋台

64×64 · JavaScript 325 行

64×64 の床置きの小物で、見下ろし 3/4 視点です。9 本縞の布の日よけの下に木の屋台があり、板の台にはリンゴの木箱、キャベツの木箱、パンを入れた籠。日よけの陰にニンニクと薬草が下がり、手前の地面には穀物袋とかぼちゃがあります。染めた縞は glow の名前で描いているので、色ごとに階調を一つ差し替えるだけです。idle(待機、12 コマのループ)は赤い日よけで、波形の裾を風が伝い、薬草が揺れます。idle-blue と idle-green は同じループの青と緑です。

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

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

market_stall.js · 1–9
// Market stall — 64x64 floor prop, top-down 3/4, key light top-left.// A timber stall under a striped cloth awning (9 stripes: dyed cloth on the glow ramp, undyed cream on n), a plank// counter carrying a crate of apples, a crate of cabbages and a wicker basket of bread; garlic and herbs hang in the// shade under the awning; a grain sack and a pumpkin sit on the ground in front.// The dyed stripes are drawn with the glow0..3 aliases, so the colour variants are one ramp swap each.// Variants://   idle        loop 12f — red awning (glow -> r); a gust travels along the scalloped hem, the hanging herbs sway//   idle-blue   loop 12f — same, awning on the b ramp//   idle-green  loop 12f — same, awning on the e ramp

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

market_stall.js · 316–325
submit(MarketStall, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", repalette: swap("glow", "r") },    { name: "idle-blue" },    { name: "idle-green", repalette: swap("glow", "e") },  ],});
プログラム全体を表示 (325 行)
market_stall.js · 325 行
// Market stall — 64x64 floor prop, top-down 3/4, key light top-left.// A timber stall under a striped cloth awning (9 stripes: dyed cloth on the glow ramp, undyed cream on n), a plank// counter carrying a crate of apples, a crate of cabbages and a wicker basket of bread; garlic and herbs hang in the// shade under the awning; a grain sack and a pumpkin sit on the ground in front.// The dyed stripes are drawn with the glow0..3 aliases, so the colour variants are one ramp swap each.// Variants://   idle        loop 12f — red awning (glow -> r); a gust travels along the scalloped hem, the hanging herbs sway//   idle-blue   loop 12f — same, awning on the b ramp//   idle-green  loop 12f — same, awning on the e rampconst W = 64, H = 64, CX = 32, GROUND = 61;     // ground line; the timber feet stand on row 60const N_IDLE = 12;const AW = ["glow0", "glow1", "glow2", "glow3"], CR = ["n3", "n4", "n5", "n5"];const NS = 9;                                   // stripesconst TOP0 = 5, TOP1 = 19;                      // canopy top face rows (back .. front)const VAL0 = 20, VAL1 = 23;                     // valance rowsconst HEM_X0 = 5, SW = 6;                       // hem starts at x=5, stripes 6px wide at the front// ---- a tiny raster: paint keys per pixel, tagged with a part name, then emit one field per part ----------------function Canvas() {  const key = new Array(W * H).fill(null), part = new Array(W * H).fill(null), order = [];  return {    key, part, order,    set(x, y, k, p) {      if (x < 0 || y < 0 || x >= W || y >= H || !k) return;      key[y * W + x] = k; part[y * W + x] = p;      if (!order.includes(p)) order.push(p);    },    has(x, y) { return x >= 0 && y >= 0 && x < W && y < H && key[y * W + x] !== null; },  };}function emit(g, cv) {  for (const p of cv.order) {    let x0 = W, x1 = -1, y0 = H, y1 = -1;    for (let i = 0; i < W * H; i++) if (cv.part[i] === p) { const x = i % W, y = (i - x) / W; x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); }    if (x1 < 0) continue;    g.part(p, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => (cv.part[y * W + x] === p ? cv.key[y * W + x] : null)));  }}const stripeKey = (s, lv) => (s % 2 === 0 ? AW : CR)[Math.max(0, Math.min(3, lv))];// ---- awning ------------------------------------------------------------------------------------------------------const canL = (y) => Math.round(9 - ((y - TOP0) / (TOP1 - TOP0)) * 4);   // 9 at the back .. 5 at the frontconst canR = (y) => 63 - canL(y);function awning(cv, t) {  // ridge pole along the back edge, round knobs at its ends  for (let x = canL(TOP0) - 1; x <= canR(TOP0) + 1; x++) cv.set(x, TOP0 - 1, x < 24 ? "w3" : x < 48 ? "w2" : "w1", "awning");  for (const [x, lit] of [[canL(TOP0) - 2, true], [canR(TOP0) + 2, false]]) {    cv.set(x, TOP0 - 2, lit ? "w4" : "w3", "awning"); cv.set(x, TOP0 - 1, lit ? "w3" : "w2", "awning");    cv.set(x + (lit ? 1 : -1), TOP0 - 2, "w2", "awning");  }  // sloped top face: stripes fan out towards the front; lit on the left, a soft sag in the middle rows  for (let y = TOP0; y <= TOP1; y++) {    const L = canL(y), R = canR(y);    for (let x = L; x <= R; x++) {      const u = (x + 0.5 - L) / (R + 1 - L), s = Math.min(NS - 1, Math.floor(u * NS));      const fu = u * NS - s;                                   // position inside the stripe      // each stripe is a cloth panel bowing between two seams: lit left half, shaded right half;      // panels on the far right have turned from the light, so only their left edge stays lit      let lv = fu < 0.5 ? 2 : 1;      if (u > 0.72 && fu > 0.25) lv = 1;      if (y === TOP0) lv = 1;                                  // in the pole's shadow      if (y === TOP0 + 1 && u < 0.35 && fu < 0.5) lv = 3;      // sheen where the cloth leaves the pole      if (y === TOP1 && lv === 1) lv = 2;                      // the roll over the front beam catches light      cv.set(x, y, stripeKey(s, lv), "awning");    }  }  // valance: the cloth folds over the front beam and hangs straight  for (let y = VAL0; y <= VAL1; y++) for (let x = HEM_X0; x < HEM_X0 + NS * SW; x++) {    const s = Math.floor((x - HEM_X0) / SW);    let lv = y === VAL0 ? 2 : 1;    if (y === VAL0 && x > 48) lv = 1;    cv.set(x, y, stripeKey(s, lv), "valance");  }  // scalloped hem: one tongue per stripe; a gust travels left -> right, each tongue lifts (shortens) then drops  for (let s = 0; s < NS; s++) {    const w = Math.sin(TAU * (t - s / NS));    const depth = Math.round(3.4 + 1.6 * w);                   // 2..5    const lift = w < -0.55;                                    // lifted tongue shows its shaded underside    const x0 = HEM_X0 + s * SW, cx = x0 + SW / 2;    for (let x = x0; x < x0 + SW; x++) {      const nx = (x + 0.5 - cx) / (SW / 2);      const hang = Math.round(depth * Math.sqrt(Math.max(0, 1 - nx * nx * 0.6)));      for (let k = 1; k <= hang; k++) {        let lv = 1;        if (k === hang) lv = 0;                                // rolled rim in shadow        else if (x === x0 && k < hang - 1) lv = 2;             // lit left edge of each tongue        if (lift && k === hang - 1) lv = 0;        cv.set(x, VAL1 + k, stripeKey(s, lv), "hem");      }    }  }}// ---- timber ----------------------------------------------------------------------------------------------------function backWall(cv) {  for (let y = VAL1 + 1; y <= 39; y++) for (let x = 10; x <= 53; x++) {    let k = y <= 27 ? "n0" : "w0";                             // deep shade right under the awning    if (y > 27 && (x - 10) % 7 === 0) k = "n0";                // plank seams    else if (y > 30 && (x - 10) % 7 === 1) k = "w1";           // each plank's lit edge    if (y === 39) k = "n0";    cv.set(x, y, k, "backwall");  }}function garlic(cv, t) {  // a braid of bulbs on a cord, left of the crates  for (let y = 24; y <= 26; y++) cv.set(11, y, "w1", "garlic");  for (const [bx, by] of [[10, 27], [11, 30], [10, 33]]) {    cv.set(bx, by, "n5", "garlic"); cv.set(bx + 1, by, "n4", "garlic");    cv.set(bx, by + 1, "n4", "garlic"); cv.set(bx + 1, by + 1, "n3", "garlic");    cv.set(bx + 2, by + 1, "n2", "garlic"); cv.set(bx + 1, by + 2, "n2", "garlic");  }}function herbs(cv, t) {  // a bunch of drying herbs hanging head-down on the right; its tip swings with the gust  const sway = Math.round(Math.sin(TAU * (t - 0.9)) * 1);  for (let y = 24; y <= 26; y++) cv.set(52, y, "w1", "herbs");  cv.set(51, 26, "w2", "herbs"); cv.set(52, 26, "w3", "herbs"); cv.set(53, 26, "w2", "herbs");   // twine  const rows = [[50, 54], [50, 54], [49, 54], [49, 53], [50, 53], [50, 52], [51, 52]];  rows.forEach(([a, b], i) => {    const y = 27 + i, off = i >= 4 ? sway : 0;    for (let x = a; x <= b; x++) {      const k = x === a ? "e2" : x === b ? "e0" : (x + i) % 3 === 0 ? "e2" : "e1";      cv.set(x + off, y, i === 0 && x === a + 1 ? "e3" : k, "herbs");    }  });}function counter(cv) {  // top face (planks along x), front edge lit; front face of vertical planks with a nailed rail  for (let x = 7; x <= 56; x++) {    cv.set(x, 40, "w2", "counter");    cv.set(x, 41, x < 45 ? "w3" : "w2", "counter");    cv.set(x, 42, (x * 7) % 11 === 3 ? "w2" : "w3", "counter");    cv.set(x, 43, x < 40 ? "w4" : "w3", "counter");    cv.set(x, 44, "w0", "counter");    for (let y = 45; y <= 58; y++) {      const c = (x - 10) % 7, plank = Math.floor((x - 10) / 7);      let k = c === 6 ? "w1" : "w2";      if (y === 45) k = c === 6 ? "w0" : "w1";                 // shadow under the counter lip      else if (c !== 6 && x >= 52 && y > 46) k = "w1";          // far end turns away      // countable grain: one short streak per plank, at a plank-dependent height      const gy = 48 + ((plank * 5) % 7), gx = 2 + (plank % 3);      if (c === gx && (y === gy || y === gy + 1)) k = "w1";      if (y === 46 && c === 1) k = "n3";                        // nail heads along the top      cv.set(x, y, k, "counter");    }    cv.set(x, 59, x < 40 ? "w2" : "w1", "counter");            // kick board    cv.set(x, 60, "w0", "counter");  }  // a chalk slate hung on the front: price scrawl and a little apple doodle  const sx0 = 26, sx1 = 37, sy0 = 48, sy1 = 55;  cv.set(31, 46, "w1", "slate"); cv.set(32, 46, "w1", "slate"); cv.set(30, 47, "w1", "slate"); cv.set(33, 47, "w1", "slate");   // cord  for (let y = sy0; y <= sy1; y++) for (let x = sx0; x <= sx1; x++) {    const edge = x === sx0 || x === sx1 || y === sy0 || y === sy1;    let k = edge ? (x === sx0 || y === sy0 ? "w3" : "w1") : "n0";    if (!edge && (x === sx0 + 1 || y === sy0 + 1)) k = "n1";    cv.set(x, y, k, "slate");  }  for (const [x, y, k] of [[28, 50, "r2"], [29, 50, "r2"], [28, 51, "r1"], [29, 51, "r1"], [29, 49, "e2"],    [31, 50, "n4"], [32, 50, "n4"], [34, 50, "n4"], [35, 50, "n3"], [31, 52, "n3"], [32, 52, "n4"], [33, 52, "n3"], [35, 52, "n4"], [28, 53, "n3"], [29, 53, "n3"]]) cv.set(x, y, k, "slate");}function posts(cv) {  for (const [x0, lit] of [[7, true], [54, false]]) {    for (let y = VAL1 + 1; y <= 60; y++) {      cv.set(x0, y, lit ? "w3" : "w2", "posts");      cv.set(x0 + 1, y, lit ? "w2" : "w2", "posts");      cv.set(x0 + 2, y, "w1", "posts");    }    cv.set(x0, 40, "w4", "posts");                              // bolt through the counter    cv.set(x0 + 1, 41, "n3", "posts");    cv.set(x0 - 1, 60, "w2", "posts"); cv.set(x0 + 3, 60, "w0", "posts");   // foot  }}// ---- produce ----------------------------------------------------------------------------------------------------function crateFront(cv, x0, x1, name) {  for (let x = x0; x <= x1; x++) {    const lx = x < x0 + (x1 - x0) * 0.65;    cv.set(x, 36, lx ? "w4" : "w3", name);                      // rim, lit    cv.set(x, 37, "w3", name);    cv.set(x, 38, "w2", name);    cv.set(x, 39, "w0", name);                                  // slat gap    cv.set(x, 40, lx ? "w3" : "w2", name);    cv.set(x, 41, "w2", name);    cv.set(x, 42, "w1", name);  }  for (let y = 36; y <= 42; y++) { cv.set(x0, y, y === 36 ? "w4" : "w3", name); cv.set(x1, y, "w1", name); }  cv.set(x0 + 1, 37, "n3", name); cv.set(x1 - 1, 37, "n2", name);   // nails  cv.set(x0 + 1, 40, "n3", name); cv.set(x1 - 1, 40, "n2", name);}function apple(cv, ax, ay, stem) {  const P = [    [null, "r2", "r2", null],    ["r2", "r3", "r2", "r1"],    ["r1", "r2", "r1", "r1"],    [null, "r1", "r0", null],  ];  P.forEach((row, j) => row.forEach((k, i) => cv.set(ax + i, ay + j, k, "apples")));  if (stem) cv.set(ax + 2, ay - 1, "w1", "apples");}function apples(cv) {  const x0 = 14;  for (const [dx, dy, st] of [[2, 29, 1], [6, 29, 0], [0, 31, 0], [4, 31, 1], [8, 31, 0], [2, 33, 0], [6, 33, 1]]) apple(cv, x0 + dx, dy, st);  crateFront(cv, 13, 24, "crateA");}function cabbage(cv, cx, cy) {  const rx = 3.2, ry = 2.7;  for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {    const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.08);    if (i < 0) continue;    const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;    let k = ["e0", "e1", "e2", "e3"][i];    if (Math.abs(nx - 0.15 + ny * 0.35) < 0.14 && ny > -0.5) k = i >= 2 ? "e3" : "e2";   // central vein    if (ny > 0.35 && Math.abs(nx) < 0.75 && Math.abs(nx - 0.15 + ny * 0.35) > 0.3 && i < 3) k = "e1";   // leaf fold    cv.set(x, y, k, "cabbages");  }}function cabbages(cv) {  cabbage(cv, 31.5, 30.5);  cabbage(cv, 28.5, 33.5);  cabbage(cv, 34.5, 33.5);  crateFront(cv, 26, 37, "crateB");}function loaf(cv, cx, cy, rx, ry) {  for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {    const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.05);    if (i < 0) continue;    cv.set(x, y, ["w1", "w2", "w3", "w4"][i], "bread");  }}function bread(cv) {  // a baguette leaning out of the basket, two round loaves in front of it  for (let k = 0; k <= 9; k++) {    const x = 42 + k, y = 38 - k;    cv.set(x, y, "w2", "bread"); cv.set(x, y - 1, "w3", "bread"); cv.set(x - 1, y - 1, "w4", "bread"); cv.set(x - 1, y, "w3", "bread");    if (k % 3 === 1) cv.set(x, y - 1, "g2", "bread");            // scoring glints on the crust  }  cv.set(51, 28, "w4", "bread"); cv.set(52, 28, "w3", "bread");  loaf(cv, 42.5, 34.5, 3.2, 2.6);  loaf(cv, 47.5, 35, 3.2, 2.5);  cv.set(41, 33, "g2", "bread"); cv.set(46, 34, "g2", "bread");  cv.set(43, 35, "w2", "bread"); cv.set(48, 35, "w2", "bread");  // scoring  // wicker basket: rolled rim, woven front  for (let x = 39; x <= 50; x++) {    cv.set(x, 36, x < 46 ? "w4" : "w3", "basket");    cv.set(x, 37, x % 2 ? "w3" : "w2", "basket");    for (let y = 38; y <= 42; y++) {      const on = ((x >> 1) + y) % 2 === 0;      cv.set(x, y, y === 42 ? "w1" : on ? (x < 45 ? "w3" : "w2") : "w1", "basket");    }  }  for (let y = 37; y <= 41; y++) { cv.set(39, y, "w3", "basket"); cv.set(50, y, "w1", "basket"); }}function sack(cv) {  // a slumped grain sack leaning on the left post, cord at the neck  const rows = { 49: [4, 6], 50: [3, 7], 51: [2, 8], 52: [1, 9], 53: [1, 9], 54: [1, 10], 55: [1, 10], 56: [1, 10], 57: [1, 10], 58: [1, 10], 59: [2, 10], 60: [2, 9] };  for (const [ys, [a, b]] of Object.entries(rows)) {    const y = +ys;    for (let x = a; x <= b; x++) {      const u = (x - a) / Math.max(1, b - a);      let k = u < 0.3 ? "w4" : u < 0.72 ? "w3" : "w2";      if (y >= 59) k = u < 0.5 ? "w2" : "w1";      if ((y === 54 && x === 4) || (y === 55 && x === 5) || (y === 56 && x === 5) || (y === 57 && x === 6)) k = "w2";   // fold      if (y === 52 && x >= 7) k = "w2";      if (y === 51 && x <= 4) k = "n5";                          // lit shoulder      cv.set(x, y, k, "sack");    }  }  cv.set(4, 48, "g1", "sack"); cv.set(5, 48, "g2", "sack"); cv.set(6, 48, "g1", "sack");   // grain peeking out  for (let x = 3; x <= 7; x++) cv.set(x, 50, x === 3 ? "w2" : "w1", "sack");                  // cord  cv.set(8, 51, "w1", "sack");}function pumpkin(cv) {  const cx = 58.5, cy = 57.5, rx = 4.2, ry = 3.2;  for (let y = 53; y <= 61; y++) for (let x = 53; x <= 63; x++) {    const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.1);    if (i < 0) continue;    let k = ["r1", "r2", "r3", "g3"][i];    if (i === 3 && !(x === 56 && y === 55)) k = "r3";    const nx = (x + 0.5 - cx) / rx;    if (Math.abs(Math.abs(nx) - 0.42) < 0.1 && y > 54) k = i >= 2 ? "r2" : "r1";   // ribs    cv.set(x, y, k, "pumpkin");  }  cv.set(58, 53, "e1", "pumpkin"); cv.set(59, 53, "e0", "pumpkin"); cv.set(58, 52, "e2", "pumpkin");}const MarketStall = sprite(pal, (g, t) => {  const cv = Canvas();  backWall(cv);  garlic(cv, t);  herbs(cv, t);  counter(cv);  apples(cv);  cabbages(cv);  bread(cv);  posts(cv);  awning(cv, t);  sack(cv);  pumpkin(cv);  contactShadow(g, CX, GROUND, 30, 2);  g.outlined("ink", () => emit(g, cv));});submit(MarketStall, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", repalette: swap("glow", "r") },    { name: "idle-blue" },    { name: "idle-green", repalette: swap("glow", "e") },  ],});

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

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

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