Tessarune
Harvest Festival · hay_bales.js

干し草の束

48×48 · JavaScript 283 行

紐で縛られた 3 つの藁束を積み上げ、右側に木製フォークを立てかけた見下ろし 3/4 視点の 48×48 の小物です。足元には散らばった藁が置かれています。16 コマのループである idle(待機)では藁の端が揺れ、12 コマの mouse(野ネズミ)では下の束の隙間からネズミが顔をのぞかせます。16 コマの harvest(収穫)では赤リンゴの籠が載り、落ち葉が舞い散ります。

干し草の束 — 描画結果6×
このプログラムで描画(変種「mouse」) · 48×48 · 12 コマ
Harvest Festival動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ

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

hay_bales.js · 1–9
// Hay bales — 48x48 floor prop, top-down 3/4, key light top-left.// Three square straw bales (two side by side on the ground, one stacked on top and set back), each tied with two// bands of dark twine that pinch the straw, strand marks along their length, a lit top face and a banded front.// Loose straw lies at the base and a wooden pitchfork (iron tines up) leans against the right of the stack.// Variants://   idle     16f loop: straw wisps on the bales and the loose straw at the base flutter; a stray strand blows past//   mouse    12f loop: a field mouse peeks out of a gap low between the bottom bales, twitches its nose, ducks back//   harvest  16f loop: the same stack with a wicker basket of red apples on top and a little pile of fallen leaves;//                      autumn leaves flutter down and around

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

hay_bales.js · 273–283
submit(HayBales, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "mouse", args: ["mouse"], animate: { fps: 10, duration: N_MOUSE / 10, loop: true } },    { name: "harvest", args: ["harvest"], animate: { fps: 10, duration: N_HARV / 10, loop: true } },  ],});
プログラム全体を表示 (283 行)
hay_bales.js · 283 行
// Hay bales — 48x48 floor prop, top-down 3/4, key light top-left.// Three square straw bales (two side by side on the ground, one stacked on top and set back), each tied with two// bands of dark twine that pinch the straw, strand marks along their length, a lit top face and a banded front.// Loose straw lies at the base and a wooden pitchfork (iron tines up) leans against the right of the stack.// Variants://   idle     16f loop: straw wisps on the bales and the loose straw at the base flutter; a stray strand blows past//   mouse    12f loop: a field mouse peeks out of a gap low between the bottom bales, twitches its nose, ducks back//   harvest  16f loop: the same stack with a wicker basket of red apples on top and a little pile of fallen leaves;//                      autumn leaves flutter down and aroundconst W = 48, H = 48, GROUND = 45, CX = 24;const N_IDLE = 16, N_MOUSE = 12, N_HARV = 16;// bales: [x0, x1, yTop] — top face rows yTop..yTop+5, front face rows yTop+6..yTop+15const TOP_ROWS = 6, FRONT_ROWS = 10;const BALE_L = [3, 20, 29], BALE_R = [22, 38, 29], BALE_T = [11, 28, 16];const HOLE = { x0: 19, x1: 23, y0: 39, y1: 44 };// ---- a bale ------------------------------------------------------------------------------------------------------const FRONT_BAND = [1.6, 1.45, 1.4, 1.3, 1.05, 1.0, 0.9, 0.55, 0.45, 0.0];   // per row: lit edge, body, shaded basefunction bale(g, name, [x0, x1, yT]) {  const yF = yT + TOP_ROWS, yB = yF + FRONT_ROWS - 1;  const ties = [x0 + 4, x1 - 4];  g.part(name, () => {    g.field({ x: [x0, x1], y: [yT, yB] }, (x, y) => {      if ((y === yT || y === yB) && (x === x0 || x === x1)) return null;          // rounded corners      const u = (x - x0) / (x1 - x0);      if (y < yF) {                                                               // top face, in the light        const r = y - yT;        const strand = (x * 2 + y * 5) % 9 < 2 && r % 2 === 1 ? 0.35 : (x + y * 4) % 11 === 0 ? -0.4 : 0;        const v = 0.75 - u * 0.75 + (r === TOP_ROWS - 1 ? 0.35 : 0) - (r === 0 ? 0.45 : 0) + strand;        return v > 0.8 ? "g3" : v > 0.0 ? "g2" : "g1";      }      const r = y - yF;      // horizontal strand dashes on alternate rows, plus a few dark gaps between flakes      const strand = r % 2 === 1 && (x + y * 5) % 9 < 2 ? 0.45 : (x * 3 + y * 7) % 13 === 0 ? -0.5 : 0;      const v = FRONT_BAND[r] - u * 0.45 + (x === x0 ? 0.4 : 0) + strand;      return v > 1.75 ? "g2" : v > 0.85 ? "g1" : v > 0.25 ? "g0" : "w1";    });    // twine: over the top face and down the front; the straw bulges beside it (lit left, pinched dark right)    for (const tx of ties) {      for (let y = yT + 1; y < yB; y++) {        const front = y >= yF;        g.pixel(tx, y, front ? (y < yF + 4 ? "w1" : "w0") : "w2");        if (y > yT + 1 && y < yB - 1) {          g.pixel(tx - 1, y, front ? (y < yF + 5 ? "g2" : "g1") : "g3");          g.pixel(tx + 1, y, front ? (y < yF + 3 ? "g0" : "w1") : "g1");        }      }      g.pixel(tx, yF, "w3");                                                       // the band turns over the edge      g.pixel(tx, yT, "w1");    }  });}// wisps: loose strands standing up off the bales, rooted on the surface; tips swing with the windconst WISPS = [  // [rootX, rootY, dx, dy, key, phase]  [13, 18, -1, -3, "g2", 0], [15, 17, 0, -3, "g3", 0.1], [14, 18, -3, -2, "g1", 0.2],  [23, 17, 1, -3, "g2", 0.15], [27, 18, 2, -3, "g3", 0], [26, 17, 1, -2, "g1", 0.3],  [5, 30, -1, -3, "g2", 0.25], [7, 30, 0, -2, "g3", 0.05], [4, 31, -3, -1, "g1", 0.3],  [33, 30, 1, -3, "g2", 0.2], [36, 30, 2, -2, "g3", 0.1], [38, 32, 3, -1, "g1", 0],  [3, 39, -2, 0, "g2", 0.15], [38, 38, 3, -1, "g2", 0.35], [29, 21, 2, -1, "g3", 0.05],];function wisps(g, t, amp) {  g.part("wisps", () => {    for (const [x, y, dx, dy, k, ph] of WISPS) {      const sw = Math.round(Math.sin(TAU * (t - ph)) * amp * (Math.abs(dy) >= 3 ? 1.2 : 0.7));      const tx = x + dx + sw, ty = y + dy + (Math.abs(sw) > 1 ? 1 : 0);      g.line([x, y], [tx, ty], k);    }  });}function looseStraw(g, wind) {  const d = Math.round(wind * 0.8);  g.part("looseStraw", () => {    for (const [a, b, k] of [[[2, 45], [7, 44], "g1"], [[6, 46], [10, 46], "g2"], [[14, 46], [18, 45], "g1"],      [[25, 46], [29, 46], "g0"], [[30, 45], [34, 46], "g2"], [[0, 43], [2, 45], "g0"], [[40, 46], [43, 45], "g1"]]) {      g.line(a, [b[0] + d, b[1]], k);    }    g.pixel(7 + d, 44, "g3"); g.pixel(34 + d, 46, "g3");  });}function strayStrand(g, t) {  // one strand tumbling across on the breeze, once per loop (off-canvas for the rest)  if (t > 0.7) return;  const q = t / 0.7;  const x = Math.round(-2 + q * 52), y = Math.round(26 - q * 10 + Math.sin(TAU * q * 2) * 3);  const up = Math.floor(q * 8) % 2;  g.detached("strayStrand", () => {    g.line([x, y], [x + 2, y + (up ? -1 : 1)], "g2");    g.pixel(x + 2, y + (up ? -1 : 1), "g3");  });}// ---- pitchfork ----------------------------------------------------------------------------------------------------function pitchfork(g) {  g.part("forkHandle", () => {    g.line([44, 44], [32, 10], "w2");    g.line([43, 43], [39, 32], "w3");                                             // lit edge along the shaft    g.line([38, 27], [34, 16], "w3");    g.pixel(44, 44, "w1");  });  g.part("forkHead", () => {    g.line([31, 9], [32, 10], "n2");                                              // ferrule    // crossbar, then three tines leaning with the shaft (3px apart so the gaps survive the outline)    g.line([27, 9], [35, 7], "n2");    g.pixel(27, 9, "n3"); g.pixel(28, 9, "n4");    for (const [bx, by] of [[27, 9], [31, 8], [35, 7]]) {      g.line([bx, by], [bx - 2, by - 6], bx === 27 ? "n4" : "n3");      g.pixel(bx - 2, by - 6, "n4");    }  });}// ---- mouse gap ---------------------------------------------------------------------------------------------------function hole(g) {  const { x0, x1, y0, y1 } = HOLE;  g.part("gap", () => {    g.fillPoly([[x0, y1], [x0, y0 + 2], [x0 + 2, y0], [x1 - 1, y0], [x1, y0 + 2], [x1, y1]], "n0");    g.line([x0 + 1, y0 + 1], [x1 - 1, y0 + 1], "ink");    g.pixel(x0, y0 + 2, "w0"); g.pixel(x1, y0 + 2, "w0");    for (const [x, k] of [[x0 + 1, "g1"], [x0 + 3, "g0"], [x1 - 1, "g1"]]) g.pixel(x, y0, k);   // straw fringe  });}// e: 0 hidden .. 1 fully out; nose: 0/1 twitch; ear: 0/1 flick; blinkconst MOUSE_ROWS = [  // [dy, dx0, keys] — head facing left, ty = top of the head  [0, 2, ["w4", "w4", "w3", "w3", "w2"]],  [1, 1, ["w4", "w4", "w4", "w3", "w3", "w2", "w1"]],  [2, 0, ["w4", "w4", "w3", "w3", "w3", "w2", "w2", "w1"]],  [3, 1, ["n5", "n5", "n4", "w2", "w2", "w1", "w1"]],      // pale chin and chest];function mouse(g, e, nose, ear, blink) {  if (e <= 0) return;  const nx = 21 - Math.round(e * 6), ty = 40;                                     // snout tip x, head top y  const px = (x, y, k) => { if (x <= HOLE.x1 - 1) g.pixel(x, y, k); };          // the rest is still inside the gap  g.outlined("ink", () => g.part("mouse", () => {    for (const [dy, dx0, ks] of MOUSE_ROWS) ks.forEach((k, i) => px(nx + dx0 + i, ty + dy, k));    px(nx + 4 + ear, ty - 1, "w3"); px(nx + 5 + ear, ty - 1, "w2");               // round ear, flicks back    px(nx + 4 + ear, ty - 2, "w3"); px(nx + 5 + ear, ty - 2, "w2");  }));  g.part("mouseFace", () => {    px(nx + 4 + ear, ty - 1, "v3");                                               // pink inner ear    px(nx - 1, ty + 2 - nose, "v3");                                              // pink nose, twitching    px(nx + 3, ty + 1, blink ? "w2" : "ink");                                     // bead eye    if (!blink) px(nx + 3, ty, "n5");  });  if (e >= 1) g.detached("whiskers", () => {    g.line([nx - 2, ty + 1 - nose], [nx - 3, ty - nose], "n4");    g.pixel(nx - 2, ty + 3, "n3");  });}// ---- harvest basket + leaves ---------------------------------------------------------------------------------------function basket(g) {  g.part("basketHandle", () => {    g.curve([[13, 13], [13, 5], [19, 1], [25, 5], [25, 13]], "w2");    g.curve([[13, 11], [14, 5], [19, 2]], "w3");  });  g.part("apples", () => {    // back row first, then the front row; each apple rimmed in dark red so the heap reads as separate fruit    for (const [ax, ay] of [[17, 8], [21, 8], [14, 11], [19, 10], [24, 11]]) {      g.outlined("r0", () => g.field({ x: [ax - 2, ax + 2], y: [ay - 2, ay + 2] }, (x, y) => {        const i = litIndex(x, y, ax + 0.5, ay + 0.5, 2.4, 2.3, 4, 0.1);        return i < 0 ? null : ["r1", "r1", "r2", "r2"][i];      }));      g.pixel(ax - 1, ay - 1, "r3");      g.pixel(ax + 1, ay + 1, "r0");    }    g.pixel(16, 7, "g3"); g.pixel(13, 10, "g3"); g.pixel(18, 9, "g3");             // glints of the key light    g.pixel(19, 6, "w1"); g.pixel(20, 5, "e2"); g.pixel(21, 5, "e1");              // stem + leaf on the top apple  });  g.part("basket", () => {    g.field({ x: [13, 26], y: [13, 20] }, (x, y) => {      const inset = Math.floor((y - 13) / 4);      if (x < 13 + inset || x > 26 - inset) return null;      if (y <= 14) return x < 17 ? "w4" : x < 23 ? "w3" : "w2";                    // rim      const u = (x - 13) / 13;      // wicker: horizontal withies (alternate rows lit) woven round darker upright stakes      const v = 0.6 - u * 1.1 - (y - 13) / 12 + (y % 2 ? 0.25 : -0.1) + (x % 3 === 0 ? -0.35 : 0);      return v > 0.45 ? "w3" : v > -0.25 ? "w2" : v > -0.7 ? "w1" : "w0";    });    g.line([14, 15], [25, 15], "w1");                                              // shadow under the rim  });  g.part("basketShadow", () => { g.line([26, 20], [27, 20], "g1"); g.pixel(27, 19, "g1"); });}const LEAF_KEYS = ["r2", "r3", "r1", "g1", "r2", "r3"];const LEAF_POSES = [  [[0, 0, 0], [1, 0, 0], [1, 1, 1], [2, 1, 0]],     // flat, broadside  [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 2, 1]],     // tipped  [[0, 0, 1], [1, 0, 0], [2, -1, 0]],               // edge-on];function leaves(g, t) {  g.detached("leaves", () => {    for (let i = 0; i < 6; i++) {      const p = (t + i / 6) % 1;      const x0 = [6, 40, 17, 31, 44, 25][i];      const x = Math.round(x0 + Math.sin(TAU * (p * 1.5 + i * 0.37)) * 4 - p * 3);      const y = Math.round(-3 + p * 50);      if (y > 45) continue;      const pose = LEAF_POSES[Math.floor(p * 12 + i) % 3];                         // tumbling      const k = LEAF_KEYS[i], d = i === 2 ? "r0" : "w1";      for (const [dx, dy, dark] of pose) g.pixel(x + dx, y + dy, dark ? d : k);    }  });}// a small pile of fallen leaves: overlapping clumps, each on its own rampconst CLUMPS = [  [8, 44, 3.2, 1.9, ["w1", "w2", "w3", "w4"]],  [4, 45, 2.8, 1.7, ["r0", "r1", "r2", "r3"]],  [11, 46, 2.6, 1.5, ["g0", "g1", "g2", "g3"]],];function leafPile(g) {  g.outlined("ink", () => g.part("leafPile", () => {    for (const [cx, cy, rx, ry, ramp] of CLUMPS) {      g.field({ x: [Math.floor(cx - rx), Math.ceil(cx + rx)], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => {        const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.1);        if (i < 0) return null;        return ramp[(x + y) % 4 === 0 ? Math.max(0, i - 1) : i];                  // leaf edges inside the clump      });    }  }));  g.part("strayLeaves", () => {    g.pixel(38, 46, "g1"); g.pixel(39, 46, "r2");    g.pixel(31, 33, "r1"); g.pixel(32, 33, "r2");                                  // one resting on the right bale  });}// ---- scene --------------------------------------------------------------------------------------------------------function stack(g, wind) {  contactShadow(g, CX, GROUND, 22, 2);  g.outlined("ink", () => bale(g, "baleL", BALE_L));  g.outlined("ink", () => bale(g, "baleR", BALE_R));  g.part("seam", () => {    // where the lower bales meet: the right bale's end catches light, the left one's falls dark    for (let y = 36; y <= 43; y++) { g.pixel(22, y, y < 39 ? "g2" : "g1"); g.pixel(20, y, "w1"); }  });  g.part("stackShadow", () => {    // the top bale casts a shadow down-right onto the lower top faces    g.line([12, 33], [29, 33], "g1"); g.line([12, 34], [29, 34], "g2");    for (let y = 29; y <= 33; y++) { g.pixel(30, y, "g1"); g.pixel(29, y, "g0"); }  });  g.outlined("ink", () => bale(g, "baleT", BALE_T));  g.part("contactLine", () => g.line([12, 33], [28, 33], "w1"));               // dark crease under the top bale  looseStraw(g, wind);}function scene(g, t, mode) {  const wind = Math.sin(TAU * t);  stack(g, wind * 1.2);  wisps(g, t, 1.3);  if (mode === "mouse") {    hole(g);    const f = Math.round(t * N_MOUSE) % N_MOUSE;    const E = [0, 0, 0.34, 0.67, 1, 1, 1, 1, 1, 1, 0.67, 0.34][f];    mouse(g, E, f === 5 || f === 7 ? 1 : 0, f === 6 ? 1 : 0, f === 8);  }  if (mode === "harvest") { leafPile(g); g.outlined("ink", () => basket(g)); }  g.outlined("ink", () => pitchfork(g));  if (mode === "idle") strayStrand(g, t);  if (mode === "harvest") leaves(g, t);}const HayBales = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  scene(g, t, mode);});submit(HayBales, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "mouse", args: ["mouse"], animate: { fps: 10, duration: N_MOUSE / 10, loop: true } },    { name: "harvest", args: ["harvest"], animate: { fps: 10, duration: N_HARV / 10, loop: true } },  ],});

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

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

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