Hay cart
A 64×48 floor prop of a two-wheeled wooden farm cart heaped with hay, carrying grain sacks and a basket of apples and turnips. In the 16-frame idle loop, hay wisps stir on the heap while a small blue butterfly flits around the cart. The rolling variant is an 8-frame loop where the wheels turn and the load jiggles, while festival is a 16-frame loop with fluttering ribbons and a leaf garland.
6×
Get the animated sprites: Harvest Festival on itch.ioVol.6 · 12 harvest-fair props · 37 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// 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 flutteringThe last lines. submit declares the canvas and lists the variants the renderer exports.
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 } }, ],});Show the whole program (318 lines)
// 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 } }, ],});These excerpts omit the boilerplate prepended by the asset pack build script: a single-line runtime header, the shared palette, and common helper utilities (contactShadow, litIndex, flame, hash, chk, TAU). The displayed line numbers match the original source files. Each program was authored by an AI coding agent and adjusted after visual inspection of the rendered frames.
Have your agent write programs like this.
Tessarune gives it the drawing API reference, 25 production manuals and a renderer to check its work against.