Tessarune
Harvest Festival · hay_cart.js

干し草の荷車

64×48 · JavaScript 318 行

干し草を高く積み、穀物袋やリンゴとカブの籠を載せた木製の荷車を描いた見下ろし 3/4 視点の 64×48 の小物です。16 コマのループである idle(待機)では干し草の先が揺れ、青いチョウが車の周りを飛びます。8 コマの rolling(走行)では車輪が回って荷が小刻みに揺れ、16 コマの festival(祭り)では杭のリボンや葉の飾りが風になびきます。

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

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

hay_cart.js · 1–10
// Hay cart — 64x48 floor prop, top-down 3/4, key light top-left.// A two-wheeled wooden farm cart heaped high with hay: a plank bed held by four stakes with iron straps, a burlap// grain sack and a smaller one leaning on the hay, a wicker basket of red apples and purple-topped turnips at the// front, the shafts resting on the ground, a prop stick under the tail, and spoked wheels with iron rims// (the far wheel peeks out behind the near one).// Variants://   idle      16f loop  hay wisps flutter on the top of the heap; a small blue butterfly flits around the cart//   rolling    8f loop  the wheels turn (spokes advance a third of a turn, 6-fold so the loop closes); the load jiggles//   festival  16f loop  decorated: red and blue ribbons on the stakes and a garland of autumn leaves along the side,//                       all fluttering

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

hay_cart.js · 308–318
submit(Cart, {  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: "rolling", args: ["rolling"], animate: { fps: 10, duration: N_ROLL / 10, loop: true } },    { name: "festival", args: ["festival"], animate: { fps: 10, duration: N_FEST / 10, loop: true } },  ],});
プログラム全体を表示 (318 行)
hay_cart.js · 318 行
// Hay cart — 64x48 floor prop, top-down 3/4, key light top-left.// A two-wheeled wooden farm cart heaped high with hay: a plank bed held by four stakes with iron straps, a burlap// grain sack and a smaller one leaning on the hay, a wicker basket of red apples and purple-topped turnips at the// front, the shafts resting on the ground, a prop stick under the tail, and spoked wheels with iron rims// (the far wheel peeks out behind the near one).// Variants://   idle      16f loop  hay wisps flutter on the top of the heap; a small blue butterfly flits around the cart//   rolling    8f loop  the wheels turn (spokes advance a third of a turn, 6-fold so the loop closes); the load jiggles//   festival  16f loop  decorated: red and blue ribbons on the stakes and a garland of autumn leaves along the side,//                       all flutteringconst W = 64, H = 48, CX = 32, GROUND = 45;const N_IDLE = 16, N_ROLL = 8, N_FEST = 16;const HAY = ["g0", "g1", "g2", "g3"];// ---- layered raster (same helper as the coop): a layer may carry its own ink ring ---------------------------function Layer(name) {  const px = new Map();  return {    name, px,    set(x, y, k, part) { if (!k || x < 0 || y < 0 || x >= W || y >= H) return; px.set(y * W + x, [k, part || name]); },    has(x, y) { return px.has(y * W + x); },  };}function emitParts(g, entries) {  const parts = new Map();  for (const [i, [k, p]] of entries) {    if (!parts.has(p)) parts.set(p, { m: new Map(), x0: W, x1: -1, y0: H, y1: -1 });    const P = parts.get(p), x = i % W, y = (i - x) / W;    P.m.set(i, k); P.x0 = Math.min(P.x0, x); P.x1 = Math.max(P.x1, x); P.y0 = Math.min(P.y0, y); P.y1 = Math.max(P.y1, y);  }  for (const [p, P] of parts) g.part(p, () => g.field({ x: [P.x0, P.x1], y: [P.y0, P.y1] }, (x, y) => P.m.get(y * W + x) ?? null));}function emitLayer(g, L, ring) {  emitParts(g, L.px);  if (!ring) return;  const edge = new Map();  for (const i of L.px.keys()) {    const x = i % W, y = (i - x) / W;    for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {      const nx = x + dx, ny = y + dy;      if (nx < 0 || ny < 0 || nx >= W || ny >= H || L.has(nx, ny)) continue;      edge.set(ny * W + nx, ["ink", L.name + "Edge"]);    }  }  emitParts(g, edge);}function fillEll(L, cx, cy, rx, ry, fn) {  for (let y = Math.floor(cy - ry - 1); y <= Math.ceil(cy + ry + 1); y++)    for (let x = Math.floor(cx - rx - 1); x <= Math.ceil(cx + rx + 1); x++) {      const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;      if (nx * nx + ny * ny <= 1) L.set(x, y, fn(x, y, nx, ny));    }}function line(L, x0, y0, x1, y1, k, part) {  const n = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0), 1);  for (let i = 0; i <= n; i++) L.set(Math.round(x0 + ((x1 - x0) * i) / n), Math.round(y0 + ((y1 - y0) * i) / n), k, part);}// ---- wheels -------------------------------------------------------------------------------------------------// ang: spoke rotation (radians). far: the wheel behind the bed, one step darker everywhere.function wheel(L, cx, cy, ang, far, part) {  const R = 9.6;  const D = far ? { n4: "n2", n3: "n1", n2: "n1", n1: "n0", w4: "w2", w3: "w1", w2: "w1", w1: "w0" } : {};  const K = (k) => D[k] || k;  for (let y = Math.floor(cy - R); y <= Math.ceil(cy + R); y++) for (let x = Math.floor(cx - R); x <= Math.ceil(cx + R); x++) {    const dx = x + 0.5 - cx, dy = y + 0.5 - cy, r = Math.hypot(dx, dy);    if (r > R) continue;    const lit = -(dx * 0.7 + dy * 0.7) / Math.max(r, 0.01);          // +1 on the top-left    if (r > 8.1) L.set(x, y, K(lit > 0.75 ? "n4" : lit > 0.1 ? "n3" : lit > -0.5 ? "n2" : "n1"), part);   // iron tyre    else if (r > 6.6) L.set(x, y, K(lit > 0.35 ? "w3" : lit > -0.45 ? "w2" : "w1"), part);             // felloe    else if (r < 2.6) L.set(x, y, K(r < 1.3 ? (lit > 0 ? "n4" : "n3") : lit > 0.2 ? "w3" : "w1"), part); // hub + cap    else L.set(x, y, far ? "n0" : (lit < -0.3 ? "n0" : "w0"), part);   // the shadowed gaps between the spokes  }  // tyre/felloe joint line  for (let k = 0; k < 6; k++) {    const a = ang + (k * Math.PI) / 3;    // spokes: a lit line with a shadow pixel row beside it    const ca = Math.cos(a), sa = Math.sin(a);    const x0 = Math.floor(cx + ca * 2.2), y0 = Math.floor(cy + sa * 2.2), x1 = Math.floor(cx + ca * 6.9), y1 = Math.floor(cy + sa * 6.9);    const [ox, oy] = Math.abs(ca) > Math.abs(sa) ? [0, 1] : [1, 0];   // shadow side: below / right of the lit line    line(L, x0 + ox, y0 + oy, x1 + ox, y1 + oy, K("w1"), part);    line(L, x0, y0, x1, y1, K("w3"), part);    // rivets on the felloe, between spokes (they rotate with the wheel)    const b = a + Math.PI / 6;    L.set(Math.floor(cx + Math.cos(b) * 7.3), Math.floor(cy + Math.sin(b) * 7.3), K("n4"), part);  }  // the lynch pin on the hub turns too  L.set(Math.floor(cx + Math.cos(ang) * 1.6), Math.floor(cy + Math.sin(ang) * 1.6), "ink", part);}// ---- cart body ------------------------------------------------------------------------------------------------const BED = { x0: 9, x1: 53, top: 25, bot: 33 };const STAKES = [11, 24, 38, 51];function shaft(L, x0, y0, x1, y1, far, part) {  // a round pole: lit top row, shaded underside  line(L, x0, y0, x1, y1, far ? "w2" : "w3", part);  line(L, x0, y0 + 1, x1, y1 + 1, far ? "w0" : "w1", part);  if (!far) L.set(x1, y1 + 1, "w2", part);}function bed(L) {  // underside beam (axle tree) and the prop stick under the tail  for (let x = BED.x0 + 2; x <= BED.x1 - 2; x++) L.set(x, BED.bot + 1, "w0", "bed");  for (let y = BED.bot + 1; y <= 44; y++) { L.set(49 + (y > 40 ? 1 : 0), y, "w2", "prop"); L.set(50 + (y > 40 ? 1 : 0), y, "w1", "prop"); }  L.set(49, 44, "w1", "prop"); L.set(52, 44, "w0", "prop");  // side planks  for (let y = BED.top; y <= BED.bot; y++) for (let x = BED.x0; x <= BED.x1; x++) {    const u = (x - BED.x0) / (BED.x1 - BED.x0);    let k = u < 0.55 ? "w3" : "w2";    const row = y - BED.top;    if (row === 0) k = u < 0.6 ? "w4" : "w3";               // lit top rim    else if (row === 3 || row === 6) k = "w0";              // plank seams    else if (row === 4 || row === 7) k = u < 0.55 ? "w2" : "w1";   // shade under each seam    else if (row === 8) k = "w1";    // grain: a short streak per plank    const pl = row < 3 ? 0 : row < 6 ? 1 : 2;    const gx = BED.x0 + 6 + ((pl * 13) % 17) + (pl === 1 ? 14 : 0);    if ((row === 2 || row === 5) && x >= gx && x < gx + 4) k = u < 0.55 ? "w2" : "w1";    if (x === BED.x0) k = row === 0 ? "w4" : "w2";    if (x === BED.x1) k = "w1";    L.set(x, y, k, "bed");  }}function stakes(L) {  for (const sx of STAKES) {    for (let y = 20; y <= BED.bot; y++) {      L.set(sx, y, y === 20 ? "w4" : "w3", "stakes"); L.set(sx + 1, y, y === 20 ? "w2" : "w1", "stakes");    }    // iron straps    for (const sy of [27, 31]) { L.set(sx - 1, sy, "n3", "straps"); L.set(sx, sy, "n4", "straps"); L.set(sx + 1, sy, "n2", "straps"); L.set(sx + 2, sy, "n1", "straps"); }  }}// hay heap: one tone expression decides the mound and its lightconst hayTop = (x) => {  const u = (x + 0.5 - 32) / 21.5;  if (Math.abs(u) > 1) return 99;  return Math.round(25.5 - 18 * Math.pow(1 - u * u, 0.55) + Math.sin(x * 0.9) * 0.8 + Math.sin(x * 0.37 + 1) * 0.6);};function hay(L, jy) {  for (let x = 10; x <= 53; x++) {    const top = hayTop(x);    for (let y = top; y <= 25; y++) {      const u = (x + 0.5 - 32) / 21.5, v = (y - top) / Math.max(1, 26 - top);      let a = -0.75 * u - 0.9 * v + 0.55 + (y - top < 2 ? 0.35 : 0)        + 0.32 * Math.sin(x * 0.55 + 0.8) * Math.cos(y * 0.62 - 0.4);   // lumpy clumps      L.set(x, y + jy, HAY[a > 0.7 ? 3 : a > 0.0 ? 2 : a > -0.55 ? 1 : 0], "hay");    }  }  // strands: short 3px streaks at fixed places, lighter on the lit side, darker in the shade  for (let i = 0; i < 80; i++) {    const x = 12 + Math.floor(hash(i, 1) * 40), top = hayTop(x);    if (top > 23) continue;    const y = top + 1 + Math.floor(hash(i, 2) * (24 - top)), d = hash(i, 3) < 0.5 ? 1 : -1;    const u = (x + 0.5 - 32) / 21.5, v = (y - top) / Math.max(1, 26 - top);    const a = -0.75 * u - 0.9 * v + 0.55;    const k = a > 0.35 ? (i % 3 ? "g3" : "g1") : a > -0.3 ? (i % 2 ? "g1" : "g3") : "g0";    for (let j = 0; j < 3; j++) if (y + j * 0 <= 25) L.set(x + j * d, y + (j === 2 ? 1 : 0) + jy, k, "hayStrands");  }  // tufts drooping over the side rim  for (const [x, len] of [[15, 2], [19, 1], [29, 2], [33, 1], [44, 2], [47, 1]]) for (let k = 1; k <= len; k++) {    L.set(x, 25 + k + jy, k === len ? "g1" : "g2", "hay"); L.set(x + 1, 25 + k - 1 + jy, "g1", "hay");  }}function sacks(L, jy) {  // big burlap sack standing against the hay (right), a smaller one tipped behind it  fillEll(L, 49.5, 19 + jy, 3.4, 4.6, (x, y, nx, ny) => {    const a = -0.6 * nx - 0.7 * ny;    return a > 0.5 ? "n4" : a > -0.1 ? "n3" : "n2";  });  fillEll(L, 43.5, 19 + jy, 4.6, 5.8, (x, y, nx, ny) => {    const a = -0.65 * nx - 0.6 * ny;    let k = a > 0.55 ? "w4" : a > 0 ? "w3" : a > -0.5 ? "w2" : "w1";    if ((x + y) % 4 === 0 && a < 0.55 && a > -0.5) k = a > 0 ? "w2" : "w1";      // weave marks    return k;  });  // gathered neck, cord and a stencilled band  for (const [x, y, k] of [[42, 12, "w3"], [43, 12, "w2"], [44, 12, "w2"], [42, 13, "w1"], [43, 13, "r1"], [44, 13, "r0"], [45, 13, "w1"],    [41, 11, "w3"], [45, 11, "w2"], [43, 11, "w4"]]) L.set(x, y + jy, k, "sackNeck");  for (let x = 40; x <= 47; x++) L.set(x, 21 + jy, x < 43 ? "w2" : "w1", "sacks");  L.set(43, 18 + jy, "w1", "sacks"); L.set(44, 18 + jy, "w1", "sacks"); L.set(43, 19 + jy, "w1", "sacks");}function basket(L, jy) {  // contents: apples and turnips heaped above the rim  const apple = (ax, ay) => {    for (const [dx, dy, k] of [[1, 0, "r2"], [2, 0, "r2"], [0, 1, "r2"], [1, 1, "r3"], [2, 1, "r2"], [3, 1, "r1"], [0, 2, "r1"], [1, 2, "r2"], [2, 2, "r1"], [3, 2, "r0"], [1, 3, "r0"], [2, 3, "r0"]])      L.set(ax + dx, ay + dy + jy, k, "apples");    L.set(ax + 2, ay - 1 + jy, "w1", "apples");  };  const turnip = (tx, ty) => {    for (const [dx, dy, k] of [[1, 0, "v2"], [2, 0, "v1"], [0, 1, "v2"], [1, 1, "v3"], [2, 1, "v1"], [3, 1, "v1"], [0, 2, "n5"], [1, 2, "n5"], [2, 2, "n4"], [3, 2, "n3"], [1, 3, "n4"], [2, 3, "n3"], [2, 4, "n3"]])      L.set(tx + dx, ty + dy + jy, k, "turnips");    L.set(tx + 1, ty - 1 + jy, "e2", "turnips"); L.set(tx + 2, ty - 2 + jy, "e3", "turnips"); L.set(tx + 3, ty - 1 + jy, "e1", "turnips");  };  turnip(18, 14); apple(13, 15); turnip(13, 12); apple(20, 15); apple(16, 16);  // wicker basket: rolled rim, woven body  for (let x = 12; x <= 24; x++) {    L.set(x, 19 + jy, x < 19 ? "w4" : "w3", "basket");    L.set(x, 20 + jy, x % 2 ? "w3" : "w2", "basket");    for (let y = 21; y <= 24; y++) {      if ((x === 12 || x === 24) && y === 24) continue;      const on = ((x >> 1) + y) % 2 === 0;      L.set(x, y + jy, y === 24 ? "w1" : on ? (x < 19 ? "w3" : "w2") : "w1", "basket");    }  }  // handle arching over the produce  for (const [x, y] of [[13, 18], [13, 17], [14, 16], [14, 15]]) L.set(x, y + jy, "w2", "basket");}// ---- animation pieces -------------------------------------------------------------------------------------------function wisps(g, t, jy) {  g.detached("wisps", () => {    [[21, 0.0], [30, 0.35], [36, 0.6], [41, 0.15]].forEach(([x, ph]) => {      const top = hayTop(x) + jy;      const sw = Math.sin(TAU * (t + ph)), sw2 = Math.sin(TAU * (t * 2 + ph));      g.pixel(x, top - 1, "g2");      g.pixel(x + Math.round(sw * 0.7), top - 2, "g3");      g.pixel(x + Math.round(sw * 1.4 + sw2 * 0.3), top - 3, "g3");    });  });}function butterfly(g, t) {  const f = Math.round(t * N_IDLE) % N_IDLE;  const x = Math.round(30 + 22 * Math.sin(TAU * t)), y = Math.round(12 + 5 * Math.sin(TAU * 2 * t + 0.6) + 3 * Math.cos(TAU * t));  const open = f % 2 === 0;  g.detached("butterfly", () => {    g.pixel(x, y, "ink"); g.pixel(x, y + 1, "ink");    if (open) {      g.pixel(x - 1, y - 1, "b3"); g.pixel(x - 2, y - 1, "b2"); g.pixel(x - 1, y, "b2"); g.pixel(x - 2, y, "b2"); g.pixel(x - 1, y + 1, "b1");      g.pixel(x + 1, y - 1, "b3"); g.pixel(x + 2, y - 1, "b2"); g.pixel(x + 1, y, "b2"); g.pixel(x + 2, y, "b2"); g.pixel(x + 1, y + 1, "b1");    } else {      g.pixel(x - 1, y - 1, "b3"); g.pixel(x - 1, y - 2, "b2"); g.pixel(x + 1, y - 1, "b2"); g.pixel(x + 1, y - 2, "b1");    }  });}function ribbons(L, t) {  STAKES.forEach((sx, i) => {    const R = i % 2 ? ["r1", "r2", "r3"] : ["b1", "b2", "b3"];    // bow knot on the stake top    L.set(sx - 1, 19, R[1], "ribbons"); L.set(sx + 2, 19, R[0], "ribbons"); L.set(sx, 19, R[2], "ribbons"); L.set(sx + 1, 19, R[1], "ribbons");    L.set(sx - 2, 18, R[1], "ribbons"); L.set(sx + 3, 18, R[0], "ribbons");    // two tails streaming right in the breeze, rippling    for (let k = 1; k <= 4; k++) {      const w = Math.sin(TAU * (t * 2 - k * 0.18 + i * 0.25));      const x = sx + 1 + k, y = 19 + Math.round(k * 0.35 + w * 0.9);      L.set(x, y, k < 3 ? R[1] : R[0], "ribbons");      L.set(x, y + 1, R[0], "ribbons");    }  });}function garland(g, t) {  const LEAF = [["r2", "r1"], ["g2", "g1"], ["r3", "r2"], ["w3", "w2"], ["r1", "r0"]];  g.part("garland", () => {    for (let s = 0; s < 3; s++) {      const x0 = STAKES[s] + 1, x1 = STAKES[s + 1];      for (let x = x0; x <= x1; x++) {        const u = (x - x0) / (x1 - x0);        const y = Math.round(25 + 4.2 * 4 * u * (1 - u));        g.pixel(x, y, "e1");        const i = x - STAKES[0];        if (i % 3 === 1 && u > 0.05 && u < 0.95) {          const [lt, dk] = LEAF[(i / 3 | 0) % 5];          const w = Math.sin(TAU * (t + i * 0.11));          // each leaf hangs from the cord and twists: swung left, straight down, or swung right          const sx = w > 0.35 ? 1 : w < -0.35 ? -1 : 0;          g.pixel(x, y + 1, lt); g.pixel(x - 1 + sx, y + 1, lt); g.pixel(x + 1 + sx, y + 1, dk);          g.pixel(x + sx, y + 2, lt); g.pixel(x + sx + (sx >= 0 ? 1 : -1), y + 2, dk); g.pixel(x + sx, y + 3, dk);        }      }    }  });}const Cart = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  const rolling = mode === "rolling";  const f = Math.round(t * (rolling ? N_ROLL : 16)) % (rolling ? N_ROLL : 16);  const ang = rolling ? -(TAU / 3) * t : 0.25;             // a third of a turn per loop (6 spokes -> seamless)  const jy = rolling ? [0, -1, 0, 0, 0, -1, 0, 0][f] : 0;  contactShadow(g, 31, GROUND, 22, 2);  const base = Layer("cart");  shaft(base, 13, 27, 3, 41, true, "shaftFar");  wheel(base, 36, 32, ang, true, "wheelFar");  bed(base);  hay(base, jy);  sacks(base, jy);  basket(base, jy);  stakes(base);  if (mode === "festival") ribbons(base, t);  g.outlined("ink", () => emitLayer(g, base, false));  if (mode === "festival") garland(g, t);  const wl = Layer("wheel"); wheel(wl, 30, 35, ang, false, "wheelNear"); emitLayer(g, wl, true);  const sl = Layer("shaft"); shaft(sl, 10, 31, 1, 43, false, "shaftNear"); emitLayer(g, sl, true);  wisps(g, t, jy);  if (mode === "idle") butterfly(g, t);});submit(Cart, {  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: "rolling", args: ["rolling"], animate: { fps: 10, duration: N_ROLL / 10, loop: true } },    { name: "festival", args: ["festival"], animate: { fps: 10, duration: N_FEST / 10, loop: true } },  ],});

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

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

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