Tavern & Market · market_stall.js
市場の屋台
64×64 の床置きの小物で、見下ろし 3/4 視点です。9 本縞の布の日よけの下に木の屋台があり、板の台にはリンゴの木箱、キャベツの木箱、パンを入れた籠。日よけの陰にニンニクと薬草が下がり、手前の地面には穀物袋とかぼちゃがあります。染めた縞は glow の名前で描いているので、色ごとに階調を一つ差し替えるだけです。idle(待機、12 コマのループ)は赤い日よけで、波形の裾を風が伝い、薬草が揺れます。idle-blue と idle-green は同じループの青と緑です。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。