Tessarune
Harvest Festival · hay_cart.js

Hay cart

64×48 · 318 lines of JavaScript

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.

Hay cart — Rendered output6×
Rendered by this program, variant “rolling” · 64×48 · 8 frames
Harvest FestivalGet 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.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

The last lines. submit declares the canvas and lists the variants the renderer exports.

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 } },  ],});
Show the whole program (318 lines)
hay_cart.js · 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.