Harvest Festival · feast_table.js
祝宴のテーブル
秋の葉の飾りとテーブルランナーで彩られ、水差しや果物、パイ、酒杯、ろうそくが並ぶ 64×48 の収穫祭の長机です。16 コマの idle(待機)ではパイから湯気が立ち上って炎が揺らめき、12 コマの toast では杯を持ち上げて乾杯を交わします。16 コマの cleared では宴の後の空になった皿や倒れた杯、燃え尽きかけたろうそくが描かれます。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。