Harvest Festival · hay_bales.js
干し草の束
紐で縛られた 3 つの藁束を積み上げ、右側に木製フォークを立てかけた見下ろし 3/4 視点の 48×48 の小物です。足元には散らばった藁が置かれています。16 コマのループである idle(待機)では藁の端が揺れ、12 コマの mouse(野ネズミ)では下の束の隙間からネズミが顔をのぞかせます。16 コマの harvest(収穫)では赤リンゴの籠が載り、落ち葉が舞い散ります。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。