Harvest Festival · hay_cart.js
干し草の荷車
干し草を高く積み、穀物袋やリンゴとカブの籠を載せた木製の荷車を描いた見下ろし 3/4 視点の 64×48 の小物です。16 コマのループである idle(待機)では干し草の先が揺れ、青いチョウが車の周りを飛びます。8 コマの rolling(走行)では車輪が回って荷が小刻みに揺れ、16 コマの festival(祭り)では杭のリボンや葉の飾りが風になびきます。
6×
動くスプライト素材: 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 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。