Tessarune
Witch's Hut · pumpkin_patch.js

Pumpkin patch

48×32 · 249 lines of JavaScript

A 48×32 floor prop: a raised bed of tilled earth with a furrowed top and a darker front lip, three uncarved pumpkins of different sizes, broad palmate leaves and curling vines with tendrils. The objects are drawn nearest-first inside g.depth, so each keeps its own ink outline against the ones behind it. “idle” (12 frames) rustles the leaves, each on its own phase, and curls the tendrils; “grow” (10 frames, once) swells the middle pumpkin from a green bud to full size; “harvested” (12 frames) leaves cut stems and hollows with one small pumpkin.

Pumpkin patch — Rendered output6×
Rendered by this program, variant “grow” · 48×32 · 10 frames
Witch's HutGet the animated sprites: Witch's Hut on itch.ioVol.5 · 12 Halloween props · 42 animations

The header. The comment at the top of the file is the brief: size, view, light direction and every variant.

pumpkin_patch.js · 1–8
// Pumpkin patch — 48x32 floor prop, top-down 3/4, key light top-left.// A raised bed of tilled earth (furrowed top face, darker front lip) with three uncarved pumpkins of different sizes,// broad palmate leaves and curling vines with tendrils.// Variants://   idle       12f loop: the leaves rustle in a breeze (each on its own phase), the tendrils curl and uncurl//   grow       10f once: the middle pumpkin swells from a green bud to full size, the leaves unfurl; ends on idle f0//   harvested  12f loop: cut stem stumps and hollows where pumpkins were, one small pumpkin left, leaves rustle// Objects are drawn nearest-first inside g.depth so each one keeps its own ink outline against the ones behind it.

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

pumpkin_patch.js · 239–249
submit(PumpkinPatch, {  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: "grow", args: ["grow"], animate: { fps: 10, duration: N_GROW / 10, loop: false } },    { name: "harvested", args: ["harvested"] },  ],});
Show the whole program (249 lines)
pumpkin_patch.js · 249 lines
// Pumpkin patch — 48x32 floor prop, top-down 3/4, key light top-left.// A raised bed of tilled earth (furrowed top face, darker front lip) with three uncarved pumpkins of different sizes,// broad palmate leaves and curling vines with tendrils.// Variants://   idle       12f loop: the leaves rustle in a breeze (each on its own phase), the tendrils curl and uncurl//   grow       10f once: the middle pumpkin swells from a green bud to full size, the leaves unfurl; ends on idle f0//   harvested  12f loop: cut stem stumps and hollows where pumpkins were, one small pumpkin left, leaves rustle// Objects are drawn nearest-first inside g.depth so each one keeps its own ink outline against the ones behind it.const W = 48, H = 32, GROUND = 29, CX = 24;const N_IDLE = 12, N_GROW = 10;const BED = { cx: 24, cy: 20, rx: 21, ry: 6.5 };const PUMP = ["r0", "r1", "r2", "r3", "g2"];const BUD = ["e0", "e1", "e2", "e3", "e3"];// ---- the bed ------------------------------------------------------------------------------------------------function bed(g) {  const { cx, cy, rx, ry } = BED;  g.part("bed", () => {    // front lip of the raised bed (the earth wall facing the camera)    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 3] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx;      for (let d = 0; d <= 3; d++) {        const ny = (y + 0.5 - d - cy) / ry;        if (nx * nx + ny * ny <= 1) {          if (d === 0) return null;                          // top face drawn below          return nx < -0.55 ? "w1" : "w0";        }      }      return null;    });    // top face: furrows running across, ridges catch the light, left side warmer    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;      const r2 = nx * nx + ny * ny;      if (r2 > 1) return null;      // furrows run across the bed, gently wavy so they read as hand-hoed earth, not planks      const wav = Math.round(0.6 * Math.sin(x * 0.55 + Math.floor(y / 3) * 2.1));      const row = ((y + wav - Math.round(cy - ry) + 30) % 3);      const lit = -0.6 * nx - 0.4 * ny;      if (r2 > 0.8 && ny > 0.3) return chk(x, y) ? "w2" : "w1";   // crumbly near rim      if (row === 0) return lit > 0.3 ? "w3" : "w2";          // ridge top      if (row === 1) return "w1";      return lit > 0.35 && chk(x, y) ? "w1" : "w0";           // furrow (softened on the lit side)    });    // clods and stones: countable marks    for (const [x, y, k] of [[7, 19, "w3"], [11, 24, "n3"], [19, 25, "w3"], [30, 24, "n2"], [40, 20, "w2"], [43, 21, "w3"], [5, 21, "n3"], [36, 15, "w3"]]) g.pixel(x, y, k);  });}// ---- pumpkin --------------------------------------------------------------------------------------------------// k: 0..1 growth (1 = ripe orange; small k = a green bud that ripens through the ramp swap point)function pumpkin(g, name, cx, base, rx, ry, lobes, opts) {  const o = opts || {};  const ramp = o.green ? BUD : PUMP;  const cy = base - ry;  g.part(name, () => {    g.field({ x: [Math.floor(cx - rx - 1), Math.ceil(cx + rx + 1)], y: [Math.floor(cy - ry - 1), Math.ceil(base)] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;      // squat body with ribbed bumps along the silhouette      const s = nx / Math.sqrt(Math.max(0.08, 1 - Math.min(0.95, ny * ny)));      const lob = (Math.asin(Math.max(-1, Math.min(1, s))) / (Math.PI / 2) + 1) * lobes / 2;      const fr = lob - Math.floor(lob);      const bump = 1 + 0.07 * Math.sin(Math.PI * fr);      if (nx * nx + ny * ny > bump * bump) return null;      let i = litIndex(x, y, cx, cy, rx * bump, ry * bump, 5, 0.12);      if (i < 0) i = 0;      // each lobe is a little barrel: its left half catches more light      if (fr < 0.38 && i < 4) i += 0;      else if (fr > 0.62 && i > 0) i -= 1;      // groove between lobes      if ((fr < 0.14 || fr > 0.9) && Math.abs(ny) < 0.85) i = Math.max(0, i - 2);      // dimple at the crown      if (ny < -0.55 && Math.abs(nx) < 0.22) i = Math.max(0, i - 1);      // specular on the upper-left lobe      if (!o.green && rx > 4) { const sx = nx + 0.42, sy = ny + 0.42; if (sx * sx * 4 + sy * sy * 9 < 0.12) i = 4; }      return ramp[Math.max(0, Math.min(4, i))];    });  });  // stem  if (o.stem !== false) {    const sx = Math.round(cx), sy = Math.round(cy - ry);    g.part(name + "Stem", () => {      if (rx >= 5) {        g.pixel(sx, sy, "w1"); g.pixel(sx - 1, sy, "w2");        g.pixel(sx, sy - 1, "w2"); g.pixel(sx + 1, sy - 1, "w1");        g.pixel(sx + 1, sy - 2, "w3"); g.pixel(sx + 2, sy - 2, "w2");      } else {        g.pixel(sx, sy, "w2"); g.pixel(sx + 1, sy - 1, "w3");      }    });  }}// cut stem stump left in the bed after harvest, with a shallow hollow where the fruit satfunction hollow(g, name, cx, base, rx) {  // the shallow dent the fruit left in the earth: dark far wall, lit near lip  g.part(name, () => {    g.ellipse({ x: [cx - rx, cx + rx], y: [base - 3, base] }, "w0", { fill: "w0" });    g.line([cx - rx + 2, base - 3], [cx + rx - 2, base - 3], "ink");    g.line([cx - rx + 1, base], [cx + rx - 1, base], "w3");    g.pixel(cx - rx + 1, base - 1, "w2"); g.pixel(cx + rx - 1, base - 1, "w1");  });}function stump(g, name, cx, base) {  // the cut stem: a short woody stub with a pale fresh-cut face, a curl of vine still attached  g.part(name, () => {    g.pixel(cx, base, "w1"); g.pixel(cx + 1, base, "w0");    g.pixel(cx, base - 1, "w2"); g.pixel(cx + 1, base - 1, "w1");    g.pixel(cx, base - 2, "w4"); g.pixel(cx + 1, base - 2, "w3");  });}// ---- leaves ---------------------------------------------------------------------------------------------------// a palmate pumpkin leaf seen from above-front: 5 rounded lobes, three veins, lit top-leftfunction leaf(g, name, cx, cy, R, ang, scale, wilt) {  const r = R * scale;  if (r < 0.9) return;  const ry = 0.72;                                              // foreshortened by the 3/4 view  g.part(name, () => g.field({ x: [Math.floor(cx - r - 1), Math.ceil(cx + r + 1)], y: [Math.floor(cy - r - 1), Math.ceil(cy + r + 1)] }, (x, y) => {    const dx = x + 0.5 - cx, dy = (y + 0.5 - cy) / ry;    const rho = Math.hypot(dx, dy) / r;    const th = Math.atan2(dy, dx) - ang;    const edge = 0.86 + 0.14 * Math.cos(5 * th);    const notch = Math.cos(th) < -0.9 ? 0.55 : 1;                 // the stalk notch at the base    if (rho > edge * notch) return null;    // veins from the base    if (r >= 3 && rho < 0.85) for (const va of [0, 1.15, -1.15]) {      let d = th - va; d = Math.atan2(Math.sin(d), Math.cos(d));      if (Math.abs(d) * rho * r < 0.5 && rho > 0.15) return "e1";    }    const l = (-0.65 * dx - 0.55 * dy * ry) / r;    if (wilt && rho > 0.62) return l > 0 ? "g1" : "w2";        // late-season leaf: browned, curling edge    // the leaf is a shallow cup: its far rim is lit, its near half falls in shade    return l > 0.25 ? "e3" : l > -0.25 ? "e2" : rho > 0.55 ? "e1" : "e2";  }));}// ---- vines and tendrils -------------------------------------------------------------------------------------function vine(g, name, path) {  g.part(name, () => g.sweep(path, { w0: 0.7, w1: 0.45 }, (u, v) => (v < -0.3 ? "e2" : "e1")));}function tendril(g, x, y, dir, turns, curl, key) {  // a spiral that tightens toward its tip; curl (0..1) winds it up  for (let s = 0; s <= 1.001; s += 0.08) {    const rad = 2.6 * (1 - s * 0.7);    const a = dir * (s * TAU * turns * (0.65 + 0.35 * curl)) - Math.PI / 2;    g.pixel(Math.round(x + dir * s * 2 + Math.cos(a) * rad - Math.cos(-Math.PI / 2) * 2.6),            Math.round(y + Math.sin(a) * rad * 0.8 + 2), s > 0.7 ? "e3" : key);  }}// ---- scene ------------------------------------------------------------------------------------------------------// leaf table: [cx, cy, R, ang, phase, depthOrder]  (bigger y = nearer)const LEAVES = [  [6, 15, 4.6, -2.3, 0.0],  [17, 13, 4.2, -1.6, 0.3],  [33, 13, 4.6, -1.2, 0.55],  [42, 16, 4.0, -0.6, 0.8],  [20, 25, 3.8, 1.9, 0.15],  [31, 24, 4.4, 1.2, 0.65],  [43, 23, 3.4, 0.6, 0.4],];function scene(g, t, mode, grow) {  const gk = grow === undefined ? 1 : grow;                     // growth of the middle pumpkin (1 = ripe)  const unf = grow === undefined ? 1 : 0.35 + 0.65 * Math.min(1, grow * 1.25);  const harvested = mode === "harvested";  contactShadow(g, CX, GROUND, 21, 1);  // layer depths: nearer = more negative; objects are drawn front-to-back  const Z = (y) => -Math.round(y);  // front leaves first (they overlap the pumpkins)  LEAVES.forEach(([x, y, R, a, ph], i) => {    if (y < 20) return;    const sw = 0.22 * Math.sin(TAU * (t + ph));    const s = harvested ? (i === 4 ? 0.7 : 0.9) : (i === 4 ? unf : 1);    g.depth(Z(y + 6), () => g.outlined("ink", () => leaf(g, "leafF" + i, x, y + Math.round(sw * 0.6), R, a + sw, s, harvested && i !== 6)));  });  if (!harvested) {    // big pumpkin, front left    g.depth(Z(27), () => g.outlined("ink", () => pumpkin(g, "pumpkinBig", 12, 27, 7.5, 5.2, 5)));    // middle pumpkin, back centre — it is the one that grows    const k = gk;    const rx = 2 + 4.6 * k, ry = 1.6 + 3.6 * k;    g.depth(Z(22), () => g.outlined("ink", () => pumpkin(g, "pumpkinMid", 26, 21, rx, ry, k > 0.5 ? 5 : 3, { green: k < 0.45, stem: true })));  } else {    g.depth(Z(27), () => g.outlined("ink", () => stump(g, "stumpBig", 12, 24)));    g.depth(Z(22), () => g.outlined("ink", () => stump(g, "stumpMid", 26, 19)));    g.depth(Z(11), () => { hollow(g, "hollowBig", 12, 26, 6); hollow(g, "hollowMid", 26, 21, 4); });  }  // small pumpkin, right (the one left after the harvest)  g.depth(Z(25), () => g.outlined("ink", () => pumpkin(g, "pumpkinSmall", 37, 25, 4.6, 3.4, 3)));  // vines: thin runners lying on the earth (no outline; dark green reads against the brown)  g.depth(Z(14), () => {    vine(g, "vineL", [[8, 22], [4, 20], [4, 17], [7, 15]]);    vine(g, "vineM", [[17, 22], [20, 24], [24, 24], [28, 22], [31, 20]]);    vine(g, "vineR", [[31, 20], [35, 18], [39, 18], [43, 19]]);    vine(g, "vineB", [[22, 18], [20, 15], [16, 14]]);  });  // back leaves  LEAVES.forEach(([x, y, R, a, ph], i) => {    if (y >= 20) return;    const sw = 0.2 * Math.sin(TAU * (t + ph));    const s = harvested ? (i === 1 ? 0.75 : 0.95) : (i === 1 || i === 2 ? unf : 1);    g.depth(Z(y), () => g.outlined("ink", () => leaf(g, "leafB" + i, x, y + Math.round(sw * 0.6), R, a + sw, s, harvested && (i === 0 || i === 2))));  });  g.depth(Z(10), () => g.outlined("ink", () => bed(g)));  // tendrils: thin spirals drawn on top, no outline  g.depth(-60, () => g.detached("tendrils", () => {    const c1 = 0.5 + 0.5 * Math.sin(TAU * t), c2 = 0.5 + 0.5 * Math.sin(TAU * (t + 0.5));    tendril(g, 3, 12, 1, 1.1, c1, "e2");    tendril(g, 44, 12, -1, 1.0, c2, "e2");    if (!harvested) tendril(g, 22, 21, -1, 0.9, c2, "e2");  }));}const PumpkinPatch = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  if (mode === "grow") {    const f = Math.min(N_GROW - 1, Math.round(t * N_GROW));    const p = f / (N_GROW - 1);    const k = 1 - Math.pow(1 - p, 2.2);                        // fast swell, settles into full size    // a small overshoot near the end gives the swell some weight, back to exactly 1 on the last frame    const ko = f === N_GROW - 2 ? 1.06 : f === N_GROW - 1 ? 1 : k;    scene(g, 0, "idle", ko);    return;  }  const f = Math.round(t * N_IDLE) % N_IDLE;  scene(g, f / N_IDLE, mode);});submit(PumpkinPatch, {  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: "grow", args: ["grow"], animate: { fps: 10, duration: N_GROW / 10, loop: false } },    { name: "harvested", args: ["harvested"] },  ],});

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.