Tessarune
Harvest Festival · hay_bales.js

Hay bales

48×48 · 283 lines of JavaScript

A 48×48 floor prop of three tied straw bales stacked together, with loose straw at the base and a pitchfork leaning against the right side. In the 16-frame idle loop, straw wisps flutter while a stray strand blows past. The 12-frame mouse variant shows a field mouse peeking from between the bales, while the 16-frame harvest variant adds a basket of red apples and fluttering leaves.

Hay bales — Rendered output6×
Rendered by this program, variant “mouse” · 48×48 · 12 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_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

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

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

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.