Tessarune
Harvest Festival · wheat_sheaf.js

Wheat sheaf

32×48 · 287 lines of JavaScript

A 32×48 floor prop of a ripe wheat sheaf tied with a red ribbon and twine, with a crescent sickle leaning on its right side. In the 16-frame idle loop, a breeze runs across the golden ears as ribbon tails stir and grains fall. The gust variant loops over 12 frames as strong wind bends the crown and stalks tumble, while festival is a 16-frame loop dressed with a straw star, flowers, and bows.

Wheat sheaf — Rendered output6×
Rendered by this program, variant “gust” · 32×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.

wheat_sheaf.js · 1–11
// Wheat sheaf — 32x48 floor prop, top-down 3/4, key light top-left.// A tall hand-bound sheaf of ripe wheat standing on stubble: straw stalks pinched at the waist by a red ribbon// and twine, flaring out at the foot, a crown of heavy golden ears with whiskery awns, and a sickle (wooden grip,// iron crescent blade) leaning against its right side.// Variants://   idle      16f loop: a breeze runs across the ears (each ear nods a little after its neighbour), the crown leans//                       and returns, the ribbon tails stir, a couple of grains drift off and fall//   gust      12f loop: a strong wind bends the crown hard to the right with a fast flutter; three loose stalks//                       tumble across the canvas (they leave on the right and come back in on the left), chaff streams//   festival  16f loop: corn-dolly dressing for the fair: a woven straw star crowns the sheaf, a big red bow with//                       long streamers (and a blue one) flutters, poppies and cornflowers are tucked into the tie

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

wheat_sheaf.js · 277–287
submit(WheatSheaf, {  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: "gust", args: ["gust"], animate: { fps: 10, duration: N_GUST / 10, loop: true } },    { name: "festival", args: ["festival"] },  ],});
Show the whole program (287 lines)
wheat_sheaf.js · 287 lines
// Wheat sheaf — 32x48 floor prop, top-down 3/4, key light top-left.// A tall hand-bound sheaf of ripe wheat standing on stubble: straw stalks pinched at the waist by a red ribbon// and twine, flaring out at the foot, a crown of heavy golden ears with whiskery awns, and a sickle (wooden grip,// iron crescent blade) leaning against its right side.// Variants://   idle      16f loop: a breeze runs across the ears (each ear nods a little after its neighbour), the crown leans//                       and returns, the ribbon tails stir, a couple of grains drift off and fall//   gust      12f loop: a strong wind bends the crown hard to the right with a fast flutter; three loose stalks//                       tumble across the canvas (they leave on the right and come back in on the left), chaff streams//   festival  16f loop: corn-dolly dressing for the fair: a woven straw star crowns the sheaf, a big red bow with//                       long streamers (and a blue one) flutters, poppies and cornflowers are tucked into the tieconst W = 32, H = 48, GROUND = 45, CX = 16;const WAIST = 31, TOP = 19;const N_IDLE = 16, N_GUST = 12;const L = [-0.74, -0.67];const STRAW = ["w1", "w2", "g1", "w4", "g2"];const EAR_F = ["g0", "g1", "g2", "g3"], EAR_B = ["w1", "g0", "g1", "g2"];// ---- the bundle of stalks -------------------------------------------------------------------------------------const halfW = (y) => y >= WAIST ? 3.4 + 4.3 * Math.pow((y - WAIST) / (GROUND - 1 - WAIST), 1.4)  : 3.4 + 3.2 * Math.pow((WAIST - y) / (WAIST - TOP), 1.15);const centre = (y, bend) => CX + (y < WAIST ? bend * Math.pow((WAIST - y) / (WAIST - TOP), 2) : 0);function stalks(g, bend) {  g.part("stalks", () => g.field({ x: [5, 27], y: [TOP - 1, GROUND] }, (x, y) => {    const hw = halfW(y), u = (x + 0.5 - centre(y, bend)) / hw;    const sid = Math.floor(((u + 1) / 2) * 9);    // ragged cut ends at the foot: alternate stalks reach one pixel lower    if (y >= GROUND - 1 && (sid % 2 === 1 || y === GROUND)) return null;    if (Math.abs(u) > 1) return null;    const f = ((u + 1) / 2) * 9 - sid;    const lit = -0.85 * u + 0.35 * Math.sqrt(1 - u * u) - (y > WAIST ? 0 : 0.1);    let k = lit > 0.6 ? 3 : lit > 0.15 ? 2 : lit > -0.35 ? 1 : 0;    if (f < 0.22 && k > 0) k--;                                         // the gap between two stalks    if (y === GROUND - 2 && k < 3) k++;                                 // light catching the cut ends    if (y < WAIST && y > WAIST - 3 && k > 0) k--;                       // pinched just above the tie    if (k === 3 && f > 0.55 && f < 0.8 && (y % 5 === 1)) return STRAW[4]; // a node glinting on a lit stalk    return STRAW[k];  }));}function stubble(g, gust, front) {  const lean = Math.round(gust * 1.2);  g.part(front ? "stubbleFront" : "stubbleBack", () => {    const list = front ? [[4, 2], [7, 3], [10, 1], [21, 2], [24, 3], [27, 2]] : [[3, 2], [6, 1], [9, 3], [12, 2], [20, 3], [23, 1], [26, 2], [28, 1]];    for (const [x, h] of list) {      const yb = front ? GROUND + 1 : GROUND - 1;      g.line([x, yb], [x + (h > 1 ? lean : 0), yb - h], h > 2 ? "w3" : "w2");      g.pixel(x + (h > 1 ? lean : 0), yb - h, "g1");    }  });}// ---- ears ----------------------------------------------------------------------------------------------------const EARS = [];for (let i = 0; i < 11; i++) {  const s = (i / 10) * 2 - 1;  EARS.push({ s, front: i % 2 === 0, len: 6.2 + (1 - Math.abs(s)) * 1.3, lift: (1 - s * s) * 3 });}function earGeom(e, bend, swayAt) {  const by = TOP + 1 - e.lift + (e.front ? 0 : -1.5);  const bx = centre(by, bend) + e.s * 5.4;  const th = e.s * 0.78 + swayAt(e.s);                                 // radians from vertical, + = to the right  const droop = e.s * 0.35 + swayAt(e.s) * 0.5;                        // the heavy head nods further  const d1 = [Math.sin(th), -Math.cos(th)], d2 = [Math.sin(th + droop), -Math.cos(th + droop)];  const p0 = [bx, by], p1 = [bx + d1[0] * e.len * 0.5, by + d1[1] * e.len * 0.5];  const p2 = [p1[0] + d2[0] * e.len * 0.55, p1[1] + d2[1] * e.len * 0.55];  return { p0, p1, p2, dir: d2 };}function ear(g, e, bend, swayAt) {  const { p0, p1, p2 } = earGeom(e, bend, swayAt);  const dx = p2[0] - p0[0], dy = p2[1] - p0[1], len = Math.hypot(dx, dy) || 1;  const nl = (-dy / len) * L[0] + (dx / len) * L[1];                   // does +v face the light?  const ramp = e.front ? EAR_F : EAR_B;  // a short bare neck of stalk, then the plump head of grain  g.line([Math.round(p0[0]), Math.round(p0[1])], [Math.round(p1[0]), Math.round(p1[1])], e.front ? "w4" : "g1");  g.sweep([[p0[0] + (p1[0] - p0[0]) * 0.45, p0[1] + (p1[1] - p0[1]) * 0.45], p1, p2], { w0: 1.25, w1: 0.6, profile: 0.7 }, (u, v) => {    let l = v * nl * 1.3 + 0.3 - u * 0.25;    if (Math.abs(v) > 0.55 && v * nl < 0) l -= 0.5;                     // the shadow flank parts it from its neighbour    if (Math.floor(u * 6) % 2 === 1 && Math.abs(v) < 0.5) l -= 0.35;    // rows of kernels down the middle    return ramp[l > 0.75 ? 3 : l > 0.15 ? 2 : l > -0.45 ? 1 : 0];  });}function awns(g, bend, swayAt, front) {  g.detached(front ? "awnsFront" : "awnsBack", () => {    for (const e of EARS) {      if (e.front !== front) continue;      const { p2, dir } = earGeom(e, bend, swayAt);      const a = [Math.round(p2[0] + dir[0] * 1.2), Math.round(p2[1] + dir[1] * 1.2)];      const b = [Math.round(p2[0] + dir[0] * 3.4 + dir[1] * 0.8), Math.round(p2[1] + dir[1] * 3.4 - dir[0] * 0.8)];      g.line(a, b, front ? "w4" : "g1");    }  });}function crown(g, bend, swayAt) {  g.outlined("ink", () => g.part("earsBack", () => { for (const e of EARS) if (!e.front) ear(g, e, bend, swayAt); }));  awns(g, bend, swayAt, false);}function crownFront(g, bend, swayAt) {  g.outlined("ink", () => g.part("earsFront", () => { for (const e of EARS) if (e.front) ear(g, e, bend, swayAt); }));  awns(g, bend, swayAt, true);}// ---- tie, ribbon tails -----------------------------------------------------------------------------------------function tie(g, flutter) {  g.part("tie", () => {    for (let x = 12; x <= 20; x++) {      const k = x < 14 ? "r2" : x < 18 ? "r1" : "r0";      g.pixel(x, WAIST, k); g.pixel(x, WAIST + 1, x < 16 ? "r1" : "r0");    }    g.pixel(12, WAIST, "r3");    for (let x = 13; x <= 19; x += 2) g.pixel(x, WAIST - 1, "w3");        // twine wrap above the ribbon    g.pixel(13, WAIST - 1, "w4");  });  g.part("bow", () => {    // loops either side of a knot at the front-left    g.fillPoly([[13, WAIST], [10, WAIST - 2], [10, WAIST + 1]], "r2");    g.fillPoly([[13, WAIST], [16, WAIST - 2], [16, WAIST + 1]], "r1");    g.pixel(10, WAIST - 2, "r3"); g.pixel(16, WAIST + 1, "r0");    g.rect({ x: [12, 13], y: [WAIST, WAIST + 1] }, "r0", { fill: "r0" }); g.pixel(12, WAIST, "r2");  });  g.part("tails", () => {    // two short ribbon ends hanging from the knot, two pixels wide    for (const [x0, ph, dir, n] of [[11, 0, -1, 5], [13, 0.3, 1, 4]]) {      for (let i = 1; i <= n; i++) {        const x = Math.round(x0 + dir * i * 0.3 + flutter(i / n, ph) * (i / n));        const y = WAIST + 1 + i;        g.pixel(x, y, i === n ? "r1" : "r2"); g.pixel(x + 1, y, i === n ? "r0" : "r1");      }    }  });}// ---- sickle ----------------------------------------------------------------------------------------------------function sickle(g, festive) {  g.part("sickleGrip", () => {    g.line([26, 44], [24, 37], "w3"); g.line([27, 44], [25, 37], "w1");    g.line([26, 43], [25, 40], "w4");    g.pixel(27, 44, "w0");    if (festive) { g.pixel(25, 40, "b2"); g.pixel(26, 40, "b1"); g.pixel(24, 39, "b2"); }      // a blue ribbon wound on the grip  });  g.part("ferrule", () => { g.rect({ x: [23, 24], y: [35, 36] }, "n2", { fill: "n2" }); g.pixel(23, 35, "n4"); });  g.part("blade", () => g.sweep([[23.5, 34.5], [27, 31.5], [28.5, 27], [26, 23]], { w0: 1.3, w1: 0.45, profile: 0.8 }, (u, v) => {    if (v < -0.45) return "n5";                                           // honed inner edge catches the light    return v > 0.4 ? "n1" : u < 0.5 ? "n3" : "n2";  }));}// ---- drifting grains / loose stalks / chaff -----------------------------------------------------------------function grains(g, t, n, speed) {  g.detached("grains", () => {    for (let i = 0; i < n; i++) {      const p = (t * speed + i / n) % 1;      const x = Math.round(18 + i * 3 + p * 9 + Math.sin(TAU * (p + i * 0.3)) * 1.5), y = Math.round(12 + i * 2 + p * 26);      if (p > 0.85 || x > W - 1) continue;      g.pixel(x, y, p < 0.5 ? "g2" : "g1");    }  });}function looseStalks(g, t) {  // three whole stalks tumbling across on the wind (each one wraps from the right edge back to the left)  g.outlined("ink", () => g.part("looseStalks", () => {    for (let i = 0; i < 3; i++) {      const p = (t + i / 3) % 1;      const x = -9 + p * (W + 18), y = 7 + i * 10 + Math.sin(TAU * (p * 1.5 + i * 0.4)) * 3;      const a = TAU * (p + i * 0.3);                                    // end over end, once per crossing      const c = Math.cos(a), sn = Math.sin(a) * 0.7;      const s0 = [Math.round(x - c * 4), Math.round(y - sn * 4)], s1 = [Math.round(x + c * 1), Math.round(y + sn * 1)];      const e1 = [Math.round(x + c * 4), Math.round(y + sn * 4)];      g.line(s0, s1, "w3");      g.line(s1, e1, "g2");      g.line([s1[0], s1[1] - 1], [e1[0], e1[1] - 1], "g1");             // the plump head, two pixels thick      g.pixel(Math.round(x + c * 2.5), Math.round(y + sn * 2.5) - (Math.abs(c) > 0.5 ? 1 : 0), "g1");      g.pixel(e1[0], e1[1], "g3");    }  }));  g.detached("chaff", () => {    for (let i = 0; i < 4; i++) {                                         // short chaff streaks      const p = (t * 2 + i / 4) % 1;      const x = Math.round(-2 + p * (W + 4)), y = 5 + ((i * 11) % 34);      g.pixel(x, y, "w4"); g.pixel(x - 1, y, "g1");    }  });}// ---- festival dressing -----------------------------------------------------------------------------------------function star(g, t, bend) {  const c = [Math.round(centre(TOP, bend)), 6];  const spin = 0.12 * Math.sin(TAU * t);  g.outlined("ink", () => {    g.part("starStick", () => g.line([c[0], c[1] + 4], [Math.round(centre(TOP + 1, bend)), TOP - 3], "w3"));    g.part("star", () => {      for (let k = 0; k < 8; k++) {        const a = spin + (k * TAU) / 8 - Math.PI / 2, long = k % 2 === 0;        const r = long ? 5.6 : 3.8;        const tip = [Math.round(c[0] + Math.cos(a) * r), Math.round(c[1] + Math.sin(a) * r)];        const lit = L[0] * Math.cos(a) + L[1] * Math.sin(a);        g.line(c, tip, lit > 0.3 ? "g3" : lit > -0.3 ? "g2" : "g1");      }      // woven ring tying the straws      g.strokeEllipse({ x: [c[0] - 2, c[0] + 2], y: [c[1] - 2, c[1] + 2] }, "g1");      g.pixel(c[0] - 1, c[1] - 2, "g3"); g.pixel(c[0] + 2, c[1] + 1, "g0");      g.pixel(c[0], c[1], "r1");    });  });}function flowers(g) {  g.part("flowers", () => {    // poppies and cornflowers tucked under the ribbon    for (const [x, y] of [[18, WAIST - 2], [20, WAIST - 1]]) { g.pixel(x, y, "r2"); g.pixel(x + 1, y, "r1"); g.pixel(x, y - 1, "r3"); g.pixel(x + 1, y + 1, "ink"); }    for (const [x, y] of [[9, WAIST - 1], [15, WAIST - 3]]) { g.pixel(x, y, "b2"); g.pixel(x - 1, y, "b1"); g.pixel(x, y - 1, "b3"); }    g.pixel(17, WAIST - 3, "e2"); g.pixel(10, WAIST - 2, "e1");  });}function streamers(g, t) {  // a red and a blue streamer from the bow, two pixels wide, rippling as the breeze runs down them  g.outlined("ink", () => g.part("streamers", () => {    for (const [lit, dark, ph, x0, dir] of [["r2", "r1", 0, 10, -1], ["b2", "b1", 0.4, 14, 1]]) {      for (let i = 1; i <= 11; i++) {        const q = i / 11;        const wave = Math.sin(TAU * (t - q * 0.9 + ph));        const x = Math.round(x0 + dir * q * 2.5 + wave * 1.6 * q), y = WAIST + 1 + i;        const face = Math.cos(TAU * (t - q * 0.9 + ph)) > -0.2;          // the ribbon twists: lit face or back        g.pixel(x, y, face ? lit : dark); g.pixel(x + 1, y, face ? dark : (lit === "r2" ? "r0" : "b0"));      }    }  }));}// ---- assembly --------------------------------------------------------------------------------------------------function sheaf(g, t, cfg) {  contactShadow(g, CX, GROUND, 11, 2);  g.outlined("ink", () => stubble(g, cfg.gust, false));  crown(g, cfg.bend, cfg.sway);  g.outlined("ink", () => stalks(g, cfg.bend));  crownFront(g, cfg.bend, cfg.sway);  g.outlined("ink", () => { tie(g, cfg.flutter); });  if (cfg.festival) flowers(g);  g.outlined("ink", () => sickle(g, cfg.festival));  g.outlined("ink", () => stubble(g, cfg.gust, true));}const WheatSheaf = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  if (mode === "gust") {    const wob = Math.sin(TAU * 3 * t);                                  // fast flutter on a hard lean    const bend = 2 + Math.round(wob * 0.6);    sheaf(g, t, {      bend, gust: 1,      sway: (s) => 0.5 + 0.12 * Math.sin(TAU * 3 * t - s * 2) - s * 0.35 - Math.max(0, s) * 0.15,      flutter: (q, ph) => 2.2 + Math.sin(TAU * (3 * t - q + ph)) * 1.1,    });    looseStalks(g, t);    grains(g, t, 3, 2);    return;  }  const breeze = Math.sin(TAU * t);  const bend = Math.round(breeze * 1.1);  const cfg = {    bend, gust: breeze * 0.5,    sway: (s) => 0.13 * Math.sin(TAU * t - s * 1.3),    flutter: (q, ph) => Math.sin(TAU * (t - q * 0.8 + ph)) * 0.9,    festival: mode === "festival",  };  sheaf(g, t, cfg);  if (cfg.festival) { star(g, t, bend); streamers(g, t); }  else grains(g, t, 2, 1);});submit(WheatSheaf, {  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: "gust", args: ["gust"], animate: { fps: 10, duration: N_GUST / 10, loop: true } },    { name: "festival", args: ["festival"] },  ],});

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.