Tessarune
Harvest Festival · feast_table.js

祝宴のテーブル

64×48 · JavaScript 424 行

秋の葉の飾りとテーブルランナーで彩られ、水差しや果物、パイ、酒杯、ろうそくが並ぶ 64×48 の収穫祭の長机です。16 コマの idle(待機)ではパイから湯気が立ち上って炎が揺らめき、12 コマの toast では杯を持ち上げて乾杯を交わします。16 コマの cleared では宴の後の空になった皿や倒れた杯、燃え尽きかけたろうそくが描かれます。

祝宴のテーブル — 描画結果6×
このプログラムで描画(変種「toast」) · 64×48 · 12 コマ
Harvest Festival動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ

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

feast_table.js · 1–13
// Feast table — 64x48 floor prop, top-down 3/4, key light top-left.// A long rustic harvest-feast trestle table: lengthwise planks, two trestle legs with block feet and a pegged// stretcher, a red cloth runner with gold borders whose ends drape over the table ends, and a garland of autumn// leaves swagged along the front edge. The spread (back row): an earthenware cider jug, a wicker cornucopia spilling// grapes, an apple and a small pumpkin, a cheese wheel with a cut wedge, a taper candle in a brass holder. Front row:// a scored bread loaf, two pewter goblets of red wine, grapes, an apple and a striped gourd on the runner, and a// lattice apple pie in a tin dish. The candle pools warm light on the planks around it.// Variants://   idle     16f loop  steam curls up from the pie, the candle flame flickers and its light pool breathes//   toast    12f loop  the goblets lift off the table (shadows stay), meet over the fruit and clink, wine splashes,//                      the jug's cider sloshes, they set back down; a bee loops over the fruit all the while//   cleared  16f loop  after the feast: an empty pie dish, a bread heel, a rind of cheese, crumbs, one goblet tipped//                      over in a wine puddle, the cornucopia emptied, the candle burned to a stub and guttering

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

feast_table.js · 416–424
submit(FeastTable, {  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: "toast", args: ["toast"], animate: { fps: 10, duration: N_TOAST / 10, loop: true } },    { name: "cleared", args: ["cleared"], animate: { fps: 10, duration: N_CLEAR / 10, loop: true } },  ],});
プログラム全体を表示 (424 行)
feast_table.js · 424 行
// Feast table — 64x48 floor prop, top-down 3/4, key light top-left.// A long rustic harvest-feast trestle table: lengthwise planks, two trestle legs with block feet and a pegged// stretcher, a red cloth runner with gold borders whose ends drape over the table ends, and a garland of autumn// leaves swagged along the front edge. The spread (back row): an earthenware cider jug, a wicker cornucopia spilling// grapes, an apple and a small pumpkin, a cheese wheel with a cut wedge, a taper candle in a brass holder. Front row:// a scored bread loaf, two pewter goblets of red wine, grapes, an apple and a striped gourd on the runner, and a// lattice apple pie in a tin dish. The candle pools warm light on the planks around it.// Variants://   idle     16f loop  steam curls up from the pie, the candle flame flickers and its light pool breathes//   toast    12f loop  the goblets lift off the table (shadows stay), meet over the fruit and clink, wine splashes,//                      the jug's cider sloshes, they set back down; a bee loops over the fruit all the while//   cleared  16f loop  after the feast: an empty pie dish, a bread heel, a rind of cheese, crumbs, one goblet tipped//                      over in a wine puddle, the cornucopia emptied, the candle burned to a stub and gutteringconst W = 64, H = 48, CX = 32, GROUND = 45;const N_IDLE = 16, N_TOAST = 12, N_CLEAR = 16;const TOP = { x0: 3, x1: 60, y0: 20, y1: 30 };                    // table top faceconst CANDLE = { x: 57, base: 23 };const WOOD = ["w0", "w1", "w2", "w3", "w4"];// one-character codes for the little stamped objects (fruit, crumbs)const CH = {  a: "r3", b: "r2", c: "r1", d: "r0", p: "g3", q: "g2", r: "g1", s: "g0", u: "v3", v: "v2", w: "v1", x: "v0",  "0": "e0", "1": "e1", "2": "e2", "3": "e3", W: "w4", V: "w3", U: "w2", T: "w1", S: "w0",  N: "n5", M: "n4", L: "n3", J: "n2", I: "n1", H: "n0", K: "ink",};// ---- layers: objects standing ON the table get their own ink ring (g.outlined would melt them into the planks) --function layer() {  const m = new Map();  const put = (x, y, k) => { if (x >= 0 && y >= 0 && x < W && y < H) m.set(y * W + x, k); };  const stamp = (x, y, rows) => rows.forEach((row, j) => [...row].forEach((ch, i) => { if (ch !== ".") put(x + i, y + j, CH[ch]); }));  return { m, put, stamp };}function emit(g, name, m, ringKey) {  const ring = new Map();  let x0 = W, x1 = -1, y0 = H, y1 = -1;  const grow = (x, y) => { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); };  for (const i of m.keys()) {    const x = i % W, y = (i - x) / W;    grow(x, y);    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 || m.has(ny * W + nx)) continue;      ring.set(ny * W + nx, ringKey); grow(nx, ny);    }  }  if (x1 < 0) return;  g.part(name, () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => m.get(y * W + x) ?? ring.get(y * W + x) ?? null));}// ---- the table ------------------------------------------------------------------------------------------------function trestles(g) {  g.part("stretcher", () => {    g.rect({ x: [12, 51], y: [36, 38] }, "w1", { fill: "w1" });    g.line([12, 36], [51, 36], "w2");    g.line([12, 38], [51, 38], "w0");    for (const x of [13, 50]) { g.line([x, 35], [x, 39], "w3"); g.pixel(x, 40, "w1"); }      // through-wedges  });  for (const [x, name] of [[8, "legL"], [52, "legR"]]) {    g.part(name, () => {      g.field({ x: [x, x + 3], y: [33, 41] }, (px, py) => {        const u = px - x;        if (py <= 34) return u < 2 ? "w1" : "w0";                  // in the table's shadow        return ["w3", "w2", "w2", "w1"][u];      });      // block foot: lit top, front face with end grain      g.rect({ x: [x - 2, x + 5], y: [41, 44] }, "w2", { fill: "w2" });      g.line([x - 2, 41], [x + 5, 41], "w3");      g.line([x - 2, 44], [x + 5, 44], "w1");      g.pixel(x - 2, 42, "w3"); g.pixel(x + 5, 43, "w1");      g.line([x, 43], [x + 2, 43], "w1"); g.pixel(x + 1, 42, "w3");    });  }}function poolAt(x, y, R) {  const d = Math.hypot(x + 0.5 - CANDLE.x, (y + 0.5 - CANDLE.base - 1) * 2);  return d < R * 0.5 ? 2 : d < R - 1.2 ? 1 : d < R && chk(x, y) ? 1 : 0;}function tableTop(g, R) {  const { x0, x1, y0, y1 } = TOP;  g.part("tabletop", () => g.field({ x: [x0, x1], y: [y0, y1 + 3] }, (x, y) => {    if (y > y1) return y === y1 + 1 ? (x < 40 ? "w3" : "w2") : y === y1 + 2 ? "w1" : "w0";   // front edge, underside    let k = x > 50 ? 2 : 3;    if (y === y0 && x < 44) k = 4;                                   // back rim catches the key light    if (y === 22 || y === 28) k -= 1;                                // plank seams    if (x === x0) k = 2;    if ((y === 21 && (x === 12 || x === 13)) || (y === 29 && x >= 30 && x <= 32) || (y === 25 && x === 46)) k -= 1;   // grain    return WOOD[Math.max(0, Math.min(4, k + poolAt(x, y, R)))];  }));}function runner(g) {  g.part("runner", () => {    g.field({ x: [TOP.x0, TOP.x1], y: [23, 27] }, (x, y) => (y === 23 || y === 27 ? "g1" : y === 24 ? (x < 40 ? "r2" : "r1") : y === 26 ? "r0" : "r1"));    for (let x = TOP.x0 + 2; x < TOP.x1; x += 4) g.pixel(x, 25, "g1");      // woven dots down the middle    // ends draped over the table ends, with fringe    for (const s of [-1, 1]) {      const ex = s < 0 ? TOP.x0 - 1 : TOP.x1 + 1;      g.fillPoly([[ex, 23], [ex, 33], [ex + s, 34], [ex + s, 24]], s < 0 ? "r1" : "r0");      g.line([ex, 24], [ex, 33], s < 0 ? "r2" : "r1");      g.pixel(ex, 35, "g1"); g.pixel(ex + s, 36, "g1"); g.pixel(ex, 36, "g0");    }  });}function garland(g) {  const keys = [["r2", "r1"], ["g2", "g1"], ["r3", "r2"], ["w3", "w2"], ["r1", "r0"]];  g.part("garland", () => {    const anchors = [4, 22, 41, 59];    const yAt = (x) => { const k = x < 22 ? 0 : x < 41 ? 1 : 2, a = anchors[k], b = anchors[k + 1]; return 31 + Math.round(3 * Math.sin(Math.PI * (x - a) / (b - a))); };    for (let x = anchors[0]; x <= anchors[3]; x++) g.pixel(x, yAt(x), "w0");          // the twine    for (let x = anchors[0] + 1, i = 0; x < anchors[3]; x += 3, i++) {                  // leaf clusters      const y = yAt(x), k = keys[i % 5];      g.pixel(x, y - 1, k[0]); g.pixel(x + 1, y - 1, k[0]); g.pixel(x, y, k[0]); g.pixel(x + 1, y, k[1]); g.pixel(x + 1, y + 1, k[1]);    }    for (const x of anchors) { g.pixel(x, 30, "r2"); g.pixel(x - 1, 31, "r2"); g.pixel(x + 1, 31, "r1"); g.pixel(x, 32, "r0"); g.pixel(x, 31, "r1"); }   // ribbon bows  });}// ---- back row -------------------------------------------------------------------------------------------------function jug(g, slosh) {  const L = layer(), put = L.put, cx = 9;  const hw = (y) => (y <= 10 ? 2 : y <= 12 ? 1 : y <= 13 ? 2 : y <= 14 ? 3 : y <= 21 ? 4 : y <= 22 ? 3 : 2);  for (let y = 10; y <= 23; y++) {    const w = hw(y);    for (let x = cx - w; x <= cx + w; x++) {      const u = (x - (cx - w) + 0.5) / (2 * w + 1);      let k = y <= 15 ? (u < 0.22 ? "n5" : u < 0.5 ? "n4" : u < 0.8 ? "n3" : "n2")      // cream salt glaze above        : (u < 0.22 ? "w4" : u < 0.5 ? "w3" : u < 0.8 ? "w2" : "w1");                    // bare stoneware below      if (y === 16) k = u < 0.22 ? "r3" : u < 0.55 ? "r2" : u < 0.85 ? "r1" : "r0";    // glaze band      if (y === 23) k = u < 0.4 ? "w1" : "w0";      put(x, y, k);    }  }  // mouth: dark rim with the cider inside; a pouring lip on the left  put(8, 10, "w0"); put(9, 10, slosh === 1 ? "g2" : "g1"); put(10, 10, slosh === 2 ? "g2" : "w0");  put(6, 10, "w4");  // handle on the shaded side  put(12, 12, "w2"); put(13, 13, "w1"); put(13, 14, "w1"); put(13, 15, "w1"); put(12, 16, "w1");  emit(g, "jug", L.m, "ink");  if (slosh === 2) g.detached("ciderDrop", () => { g.pixel(9, 8, "g2"); g.pixel(10, 7, "g3"); });}// the horn: a cone swept along a quadratic curve (tail curling up at the back-left, mouth opening to the right),// built in a layer so it keeps its own ink ring against the jug behind itconst HORN = { p0: [18, 10], p1: [13, 23], p2: [29, 19] };const hornAt = (u) => {  const { p0, p1, p2 } = HORN, k = 1 - u;  return [k * k * p0[0] + 2 * k * u * p1[0] + u * u * p2[0], k * k * p0[1] + 2 * k * u * p1[1] + u * u * p2[1]];};function horn(g, full) {  const LD = [-0.74, -0.67];  const S = [];  for (let i = 0; i <= 80; i++) { const u = i / 80; S.push([u, ...hornAt(u)]); }  const H = layer();  for (let y = 7; y <= 25; y++) for (let x = 11; x <= 32; x++) {    let best = null, bd = 1e9;    for (const s of S) { const d = (x + 0.5 - s[1]) ** 2 + (y + 0.5 - s[2]) ** 2; if (d < bd) { bd = d; best = s; } }    const u = best[0], r = 0.7 + 3.5 * Math.pow(u, 1.3);    if (Math.sqrt(bd) > r) continue;    const [ax, ay] = hornAt(Math.max(0, u - 0.01)), [bx, by] = hornAt(Math.min(1, u + 0.01));    const tl = Math.hypot(bx - ax, by - ay) || 1, nx = -(by - ay) / tl, ny = (bx - ax) / tl;    const v = ((x + 0.5 - best[1]) * nx + (y + 0.5 - best[2]) * ny) / r;   // -1..1 across the horn    let l = v * (nx * LD[0] + ny * LD[1]) * 1.3 + 0.25 - 0.25 * v * v;    const ring = (u * 10) % 1;    if (ring < 0.2) l -= 0.6;                                                  // woven rings    else if ((Math.floor(u * 10) + Math.floor((v + 1) * 2)) % 2) l -= 0.3;    // basket weave    H.put(x, y, WOOD[l > 0.75 ? 4 : l > 0.2 ? 3 : l > -0.35 ? 2 : l > -0.8 ? 1 : 0]);  }  // the mouth, seen from the side: a lit rim around the dark inside  for (let y = 15; y <= 23; y++) for (let x = 27; x <= 32; x++) {    const ex = (x + 0.5 - 29.8) / 2.4, ey = (y + 0.5 - 19.3) / 4.4;    const r = ex * ex + ey * ey;    if (r > 1) continue;    H.put(x, y, r > 0.45 ? (ey < -0.2 || ex < -0.5 ? "w4" : "w2") : ey < 0 ? "ink" : "w0");  }  emit(g, "cornucopia", H.m, "ink");  const F = layer();  if (full) {    F.stamp(30, 14, ["..1..", ".uvvw", "uvwvw", ".wvwx", "..wx."]);        // grapes hanging over the rim    F.stamp(33, 18, [".T..", "Nbbc", "bbcd", ".cd."]);                       // apple    F.stamp(36, 19, ["..1..", ".abc.", "abbcd", ".ccd."]);                   // small pumpkin  } else {    F.stamp(31, 20, ["T.", "vw", ".x"]);                                     // two grapes left on a stem  }  emit(g, "hornFruit", F.m, "ink");}function cheese(g, whole) {  const L = layer(), put = L.put;  if (whole) {    const cx = 47, cy = 18.5, rx = 4.6, ry = 1.9;    for (let y = 16; y <= 23; y++) for (let x = 42; x <= 52; x++) {      const nx = (x + 0.5 - cx) / rx;      if (Math.abs(nx) > 1) continue;      const s = Math.sqrt(1 - nx * nx), top = cy - ry * s, bot = cy + ry * s;      if (y + 0.5 < top || y + 0.5 > bot + 3) continue;      // the wedge cut out of the front-right: the cut faces show paler paste      if (nx > 0.05 && y + 0.5 > cy - 0.2) { if (nx < 0.35 || y + 0.5 > bot + 2.2) put(x, y, nx < 0.35 ? "g3" : "g2"); continue; }      if (y + 0.5 <= bot) { const ny = (y + 0.5 - cy) / ry; put(x, y, -0.6 * nx - 0.5 * ny > 0.45 ? "g3" : nx > 0.5 ? "g1" : "g2"); }      else put(x, y, y + 0.5 > bot + 2 ? (nx < -0.3 ? "w2" : "w1") : nx < -0.4 ? "g1" : "g0");    }    put(48, 20, "g1"); put(48, 22, "g1");                                    // holes in the cut face    emit(g, "cheese", L.m, "ink");    const Wd = layer();                                                      // the cut wedge, lying in front    Wd.stamp(50, 21, ["pq..", "pqqr", "UTTT"]);    emit(g, "wedge", Wd.m, "ink");  } else {    L.stamp(45, 21, ["..pq", "pqqr", "UUTT"]);                               // a rind of cheese left    emit(g, "cheese", L.m, "ink");  }}function flick(t, s) {  const dh = Math.round(0.75 * Math.sin(TAU * (2 * t + s)) + 0.45 * Math.sin(TAU * (3 * t + 2.3 * s)));  const lean = 0.7 * Math.sin(TAU * (t + s)) + 0.45 * Math.sin(TAU * (3 * t + 1.7 * s));  return { dh, lean };}function candleFlame(g, fx, fy, h, lean) {  for (let i = 0; i < h; i++) {    const k = (i + 0.5) / h, y = fy - i;    const ax = fx + Math.round(lean * k * k);    if (k > 0.12 && k < 0.62 && h > 4) {      const edge = k > 0.18 && k < 0.5 ? "fl1" : "fl0";      g.pixel(ax - 1, y, edge); g.pixel(ax + 1, y, edge);    }    g.pixel(ax, y, k < 0.12 ? "fl1" : k < 0.5 ? "fl3" : k < 0.68 ? "fl2" : k < 0.86 ? "fl1" : "fl0");  }}function candle(g, t, stub) {  const { x, base } = CANDLE;  const top = stub ? base - 3 : base - 10;  const L = layer(), put = L.put;  // brass holder: a dish with a ring handle on the shaded side  for (let xx = x - 3; xx <= x + 2; xx++) { put(xx, base, xx < x ? "g2" : "g1"); put(xx, base + 1, xx < x - 1 ? "g1" : "g0"); }  put(x - 3, base, "g3");  // taper: two columns, lit left  for (let y = top; y < base; y++) { put(x - 1, y, "n5"); put(x, y, y > base - 3 ? "w3" : "w4"); }  put(x, top, "n4");  if (stub) { put(x - 2, base - 1, "n5"); put(x + 1, base - 1, "w4"); put(x - 2, base - 2, "n5"); put(x + 1, base, "w4"); }   // melted wax  else { put(x - 2, top + 3, "n5"); put(x - 2, top + 4, "w4"); }  emit(g, "candle", L.m, "ink");  const f = flick(t, 0.2);  let h = (stub ? 4 : 6) + f.dh, lean = f.lean;  if (stub) {                                                                // guttering: it dips and leans hard    const fr = Math.round(t * N_CLEAR) % N_CLEAR;    if (fr === 5 || fr === 6 || fr === 12) h -= 2;    if (fr === 6 || fr === 13) lean += 1.2;  }  g.part("flame", () => {    g.pixel(x - 1, top - 1, "ink");    candleFlame(g, x - 1, top - 2, Math.max(2, h), lean);  });}// ---- front row ------------------------------------------------------------------------------------------------function bread(g, whole) {  const L = layer(), put = L.put;  const cx = whole ? 10.5 : 9.5, cy = 26.3, rx = whole ? 5.2 : 2.8, ry = 2.6;  for (let y = 22; y <= 29; y++) for (let x = 3; x <= 17; x++) {    const i = litIndex(x, y, cx, cy, rx, ry, 4, 0.05);    if (i < 0) continue;    put(x, y, ["w1", "w2", "w3", "w4"][i]);  }  if (whole) {    for (const sx of [7, 10, 13]) { put(sx, 25, "w1"); put(sx + 1, 24, "w1"); put(sx - 1, 26, "w2"); }   // scores    put(8, 24, "n5"); put(6, 25, "n5");                                      // flour  } else {    for (let y = 25; y <= 28; y++) { put(12, y, y === 25 ? "n5" : "w4"); put(11, y, "n5"); }   // the cut face    put(11, 27, "w4");  }  emit(g, "bread", L.m, "ink");}// pewter goblet: x0 = left column of the bowl, yb = foot rowfunction goblet(g, name, x0, yb, slosh, empty) {  const L = layer();  const wine = empty ? ["I", "H", "H"] : slosh === 1 ? ["c", "b", "d"] : slosh === 2 ? ["b", "c", "c"] : ["b", "c", "d"];  L.stamp(x0, yb - 7, [    "MNMLJ",    "M" + wine.join("") + "J",    "NMLJI",    "MMLJI",    ".MLI.",    "..L..",    "..J..",    ".MLJ.",  ]);  emit(g, name, L.m, "ink");}function tippedGoblet(g) {  const L = layer();  L.stamp(16, 25, [    "M.....",    "MLLMNM",    "LJLMLJ",    "J.IJJI",  ]);  emit(g, "gobletTipped", L.m, "ink");  g.part("winePuddle", () => { g.line([23, 29], [25, 29], "r0"); g.pixel(24, 28, "r1"); g.pixel(26, 29, "r0"); });}function spill(g, full) {  const F = layer();  if (full) {    F.stamp(24, 23, ["..1..", ".uvvw", "uvwvw", ".wvwx", "..wx."]);          // grapes    F.stamp(29, 25, [".T..", "Nbbc", "bbcd", ".cd."]);                       // apple    F.stamp(33, 25, ["1......", ".pq2qr.", "qq2qq2r", ".rs2rs."]);           // striped gourd  } else {    F.stamp(33, 25, ["1......", ".pq2qr.", "qq2qq2r", ".rs2rs."]);           // the gourd was only decoration    F.stamp(29, 26, [".T", "Nq", "q.", "Nq"]);                               // an apple core  }  emit(g, "spill", F.m, "ink");}function pie(g, full) {  const L = layer(), put = L.put;  const cx = 51.5, cy = 25.6, rx = 5.7, ry = 2.6;  for (let y = 22; y <= 30; y++) for (let x = 44; x <= 58; x++) {    const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;    const r = nx * nx + ny * ny;    if (r <= 1) {      if (r > 0.58) {                                                         // crimped crust rim / tin rim        const lit = -0.6 * nx - 0.6 * ny > 0.2;        put(x, y, full ? (lit ? "w4" : (x & 1) ? "w3" : "w2") : lit ? "n4" : "n3");      } else if (full) {                                                       // lattice over apple filling        const lat = (x + y) % 3 === 0 || (x - y + 60) % 3 === 0;        put(x, y, lat ? (nx < 0.2 ? "w4" : "w3") : nx < -0.1 && ny < 0.2 ? "g1" : "g0");      } else put(x, y, ny < -0.2 ? "n1" : "n2");                               // the empty tin      continue;    }    // the dish's side below the rim    const s = Math.sqrt(Math.max(0, 1 - nx * nx));    if (Math.abs(nx) < 0.97 && y + 0.5 > cy + ry * s && y + 0.5 <= cy + ry * s + 1.6) put(x, y, nx < -0.3 ? "n3" : "n2");  }  if (!full) { for (const [x, y] of [[49, 25], [52, 26], [54, 25]]) put(x, y, "w3"); put(50, 26, "w4"); put(51, 26, "w3"); }   // crumbs, a bit of crust  emit(g, "pie", L.m, "ink");}function steam(g, t) {  g.detached("steam", () => {    for (let i = 0; i < 3; i++) {      const p = (t + i / 3) % 1;      const bx = [48, 51, 53][i], y0 = 22 - Math.round(p * 12);      const x = bx + Math.round(Math.sin(TAU * (p * 1.2 + i * 0.37)) * 2);      // a puff that thins out as it rises: 2x2, then a curl of two, then one faint pixel      if (p < 0.35) { g.rect({ x: [x, x + 1], y: [y0 - 1, y0] }, "n5", { fill: "n5" }); g.pixel(x + 1, y0, "n4"); }      else if (p < 0.7) { g.pixel(x, y0, "n4"); g.pixel(x + 1, y0, "n5"); }      else if (p < 0.9) g.pixel(x, y0, "n3");    }  });}function crumbs(g) {  g.part("crumbs", () => {    for (const [x, y, k] of [[17, 24, "w4"], [19, 25, "w3"], [21, 27, "w3"], [27, 24, "w4"], [38, 26, "w3"], [40, 24, "w4"], [43, 28, "w3"], [14, 29, "w3"], [5, 23, "w4"], [36, 29, "w4"], [45, 21, "w3"]]) g.pixel(x, y, k);    g.pixel(30, 23, "r2"); g.pixel(41, 27, "v1");                             // a stray leaf, a fallen grape  });}// ---- toast: goblets lift, meet over the fruit, clink -------------------------------------------------------const CUP_L = { x0: 17, yb: 29 }, CUP_R = { x0: 41, yb: 29 };const LIFT = [[0, 0], [1, -2], [3, -5], [6, -8], [9, -10], [8, -11], [7, -10], [5, -8], [3, -5], [1, -2], [0, 0], [0, 0]];function cupShadow(g, c, dx, dy) {  if (dy === 0) return;  g.part("cupShadow", () => { for (let x = c.x0 + 1; x <= c.x0 + 3; x++) { g.pixel(x, c.yb, "w1"); if (dy > -6) g.pixel(x, c.yb - 1, chk(x, c.yb) ? "w1" : "w2"); } });}function clink(g, f) {  g.detached("clink", () => {    const cx = 31, cy = 10;    if (f === 4) for (const [x, y, k] of [[cx, cy - 1, "g3"], [cx, cy - 2, "n5"], [cx - 3, cy, "g2"], [cx + 3, cy, "g2"], [cx - 2, cy - 2, "g3"], [cx + 2, cy - 2, "g3"]]) g.pixel(x, y, k);    const drops = { 4: [[0, -1], [-1, -2], [1, -3]], 5: [[-2, -4], [2, -5], [0, -3]], 6: [[-3, -2], [3, -3]], 7: [[-4, 1], [4, 0]] }[f];    if (drops) drops.forEach(([dx, dy], i) => { g.pixel(cx + dx, cy + dy, i % 2 ? "r1" : "r2"); if (f <= 5) g.pixel(cx + dx, cy + dy + 1, "r0"); });  });}function bee(g, t) {  const x = Math.round(30 + 8 * Math.sin(TAU * t)), y = Math.round(9 + 2.5 * Math.sin(2 * TAU * t));  const dir = Math.cos(TAU * t) >= 0 ? 1 : -1;                              // faces where it is flying  const up = Math.round(t * N_TOAST * 2) % 2 === 0;  g.detached("bee", () => {    // striped body (tail, band, band, head), dark underside, flickering wings    g.pixel(x - 2 * dir, y, "ink"); g.pixel(x - dir, y, "g2"); g.pixel(x, y, "ink"); g.pixel(x + dir, y, "g3"); g.pixel(x + 2 * dir, y, "ink");    g.pixel(x - dir, y + 1, "g1"); g.pixel(x, y + 1, "ink"); g.pixel(x + dir, y + 1, "g1");    if (up) { g.pixel(x, y - 1, "n5"); g.pixel(x - dir, y - 2, "b3"); g.pixel(x, y - 2, "n5"); }    else { g.pixel(x - dir, y - 1, "n5"); g.pixel(x - 2 * dir, y - 1, "b3"); }  });}const FeastTable = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  const cleared = mode === "cleared", toast = mode === "toast";  contactShadow(g, CX, GROUND, 27, 2);  g.outlined("ink", () => trestles(g));  const f = flick(t, 0.2);  g.outlined("ink", () => tableTop(g, (cleared ? 5 : 7) + 0.9 * f.dh));  runner(g);  g.outlined("ink", () => garland(g));  if (cleared) crumbs(g);  // back row  let ft = 0, slosh = 0;  if (toast) { ft = Math.round(t * N_TOAST) % N_TOAST; slosh = ft >= 4 && ft <= 7 ? (ft % 2 ? 1 : 2) : 0; }  jug(g, slosh);  horn(g, !cleared);  cheese(g, !cleared);  candle(g, t, cleared);  // front row  bread(g, !cleared);  spill(g, !cleared);  pie(g, !cleared);  if (cleared) { tippedGoblet(g); goblet(g, "gobletR", CUP_R.x0, CUP_R.yb, 0, true); return; }  if (!toast) {    goblet(g, "gobletL", CUP_L.x0, CUP_L.yb, 0, false);    goblet(g, "gobletR", CUP_R.x0, CUP_R.yb, 0, false);    steam(g, t);    return;  }  const [dx, dy] = LIFT[ft];  cupShadow(g, CUP_L, dx, dy); cupShadow(g, CUP_R, -dx, dy);  steam(g, t);  const cs = ft >= 3 && ft <= 8 ? 1 + (ft % 2) : 0;  goblet(g, "gobletL", CUP_L.x0 + dx, CUP_L.yb + dy, cs, false);  goblet(g, "gobletR", CUP_R.x0 - dx, CUP_R.yb + dy, cs ? 3 - cs : 0, false);  clink(g, ft);  bee(g, t);});submit(FeastTable, {  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: "toast", args: ["toast"], animate: { fps: 10, duration: N_TOAST / 10, loop: true } },    { name: "cleared", args: ["cleared"], animate: { fps: 10, duration: N_CLEAR / 10, loop: true } },  ],});

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

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

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