Tessarune
Witch's Hut · scarecrow.js

Scarecrow

32×64 · 311 lines of JavaScript

A 32×64 tall floor prop: a straw man on a wooden post and crossbar with a burlap sack head tied at the neck, a floppy pointed witch hat with a red band and brass buckle, a faded green coat with a leather patch and a violet sleeve patch, a rope belt and straw at the cuffs, collar and hem, standing in a little mound with grass tufts. “idle” (16 frames) sways in the wind, the hat tip lagging behind; “haunted” (16 frames) burns the carved eyes and mouth orange while embers rise; “turn” (8 frames, once) slowly turns the head to face the viewer and ends on the first frame of “haunted”.

Scarecrow — Rendered output6×
Rendered by this program, variant “turn” · 32×64 · 8 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.

scarecrow.js · 1–10
// Scarecrow — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A straw man on a wooden post and crossbar: a burlap sack head tied off at the neck with straw bursting out under// the cord, a floppy pointed witch hat (bent tip, red band, brass buckle), a faded green coat with a leather patch// and a violet sleeve patch, a rope belt, straw at the cuffs, the collar, the coat's open front and under the hem.// The post stands in a little mound of earth with grass tufts. No crow.// Variants://   idle     16f loop: sways in the wind — the head rocks, the hat tip lags behind, coat tatters, straw and rope flap//   haunted  16f loop: carved eyes and mouth burn orange (fl family), head held at a tilt, faint glow, embers rise//   turn     8f once:  the head slowly turns from looking aside to facing the viewer, eyes catch and flare;//                      starts on idle frame 0, ends on haunted frame 0

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

scarecrow.js · 301–311
submit(Scarecrow, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "haunted", args: ["haunted"] },    { name: "turn", args: ["turn"], animate: { fps: 10, duration: N_TURN / 10, loop: false } },  ],});
Show the whole program (311 lines)
scarecrow.js · 311 lines
// Scarecrow — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A straw man on a wooden post and crossbar: a burlap sack head tied off at the neck with straw bursting out under// the cord, a floppy pointed witch hat (bent tip, red band, brass buckle), a faded green coat with a leather patch// and a violet sleeve patch, a rope belt, straw at the cuffs, the collar, the coat's open front and under the hem.// The post stands in a little mound of earth with grass tufts. No crow.// Variants://   idle     16f loop: sways in the wind — the head rocks, the hat tip lags behind, coat tatters, straw and rope flap//   haunted  16f loop: carved eyes and mouth burn orange (fl family), head held at a tilt, faint glow, embers rise//   turn     8f once:  the head slowly turns from looking aside to facing the viewer, eyes catch and flare;//                      starts on idle frame 0, ends on haunted frame 0const W = 32, H = 64, GROUND = 61, CX = 16;const N_LOOP = 16, N_TURN = 8;const NECK = [16, 24];const COAT = ["e0", "e1", "e2", "e3"], SACK = ["w1", "w2", "w3", "w4"], HAT = ["n0", "v0", "v1", "v2"];const WOOD = ["w0", "w1", "w2", "w3"];// ---- post, crossbar, mound --------------------------------------------------------------------------------------function frame(g) {  g.part("post", () => {    for (let y = 20; y <= 59; y++) { g.pixel(15, y, "w3"); g.pixel(16, y, "w2"); g.pixel(17, y, "w1"); }    for (const y of [48, 49, 53]) g.pixel(16, y, "w1");                      // grain    g.pixel(15, 55, "w2"); g.pixel(17, 51, "w0");  });  g.part("crossbar", () => {    for (let x = 1; x <= 30; x++) { g.pixel(x, 27, x < 16 ? "w3" : "w2"); g.pixel(x, 28, "w1"); }    g.pixel(1, 27, "w4"); g.pixel(1, 28, "w2"); g.pixel(30, 28, "w0");       // cut ends    g.pixel(2, 27, "w4");  });}function mound(g, wind) {  g.part("mound", () => {    g.field({ x: [9, 23], y: [57, 61] }, (x, y) => {      const nx = (x + 0.5 - 16) / 7, ny = (y + 0.5 - 60.5) / 3.5;      if (nx * nx + ny * ny > 1 || y > 61) return null;      const a = -0.8 * nx - 0.6 * ny;      return a > 0.55 ? "w2" : a > -0.1 ? "w1" : "w0";    });    g.pixel(12, 59, "n3"); g.pixel(13, 59, "n2");                             // a pebble    g.pixel(20, 60, "w2");  });  const sw = Math.round(wind * 0.8);  g.part("grass", () => {    for (const [x, h, k] of [[10, 3, "e2"], [11, 4, "e1"], [21, 4, "e2"], [22, 2, "e1"], [19, 2, "e1"]]) {      g.line([x, 60], [x + sw, 60 - h], k);      g.pixel(x + sw, 60 - h, k === "e2" ? "e3" : "e2");    }  });}// ---- coat --------------------------------------------------------------------------------------------------------const torsoL = (y) => 9.5 - (y - 26) * 0.09, torsoR = (y) => 22.5 + (y - 26) * 0.09;function coatAt(x, y, sh) {  // returns "sleeve" | "torso" | "gap" | null  const tip = y >= 44 ? x - sh : x;                                          // hem tatters swing in the wind  if (y >= 26 && y <= 31 && x >= 4 && x <= 28) {    const bottom = x < 7 || x > 25 ? 30 : 31;    if (y <= bottom && (x < 10 || x > 22)) return "sleeve";  }  if (y === 25 && x >= 11 && x <= 21) return "torso";  if (y < 26) return null;  if (x + 0.5 < torsoL(y) || x + 0.5 > torsoR(y) + 1) return null;  const v = ((tip % 4) + 4) % 4, tat = v < 2 ? v : 4 - v;                    // 0..2  if (y > 43 + tat + (tip % 7 === 3 ? 1 : 0)) return null;  if (y >= 37 && Math.abs(x + 0.5 - 16.5) < (y - 36) * 0.38) return "gap";     // the coat falls open  return "torso";}function coat(g, sh, flap) {  g.part("coat", () => g.field({ x: [3, 29], y: [25, 47] }, (x, y) => {    const r = coatAt(x, y, sh);    if (!r || r === "gap") return null;    if (r === "sleeve") {      const left = x < 16;      let k = y <= 26 ? 3 : y <= 28 ? 2 : y <= 29 ? 1 : 0;      if (!left) k = Math.max(0, k - 1);      if (k === 3 && (x > 8 || !left)) k = 2;      if ((x === 4 || x === 28) && y >= 28) k = 0;                            // inside of the cuff      if ((x === 5 || x === 27) && flap > 0.3) k = Math.max(0, k - 1);        // cuffs flutter      return COAT[k];    }    const nx = (x + 0.5 - 16) / 7.5;    const fold = 0.32 * Math.sin(x * 1.35 + 0.6) * (y > 33 ? 1 : 0.4);    let v = -0.95 * nx + fold - (y - 26) / 45;    if (y <= 33 && (x <= 10 || x >= 22)) v -= 0.5;                             // under the arms    if (y === 25 || (y === 26 && x > 10 && x < 15)) v += 0.6;                  // shoulders catch the light    const k = v > 0.55 ? 3 : v > -0.05 ? 2 : v > -0.6 ? 1 : 0;    return COAT[Math.min(k, y <= 27 ? 3 : 2)];  }));  g.part("coatDetail", () => {    for (let y = 26; y <= 36; y++) g.pixel(16, y, y < 30 ? "e1" : "e0");      // the front edge    g.pixel(15, 26, "e0"); g.pixel(17, 26, "e0");    for (const y of [29, 32, 35]) { g.pixel(18, y, "g1"); }                   // buttons    g.pixel(18, 29, "g2");    // leather patch, stitched    g.rect({ x: [11, 13], y: [38, 40] }, "w2", { fill: "w2" });    g.line([11, 38], [12, 38], "w3"); g.line([13, 39], [13, 40], "w1"); g.pixel(12, 40, "w1");    g.pixel(10, 38, "n4"); g.pixel(14, 41, "n4"); g.pixel(14, 38, "n4"); g.pixel(10, 41, "n3");    // violet patch on the right sleeve    g.rect({ x: [23, 25], y: [27, 29] }, "v1", { fill: "v1" });    g.line([23, 27], [24, 27], "v2"); g.pixel(25, 29, "v0");    g.pixel(22, 27, "n4"); g.pixel(26, 30, "n3"); g.pixel(24, 30, "n3");    // a tear on the left sleeve    g.pixel(7, 29, "e0"); g.pixel(8, 30, "e0");  });}function belt(g, wind) {  g.part("belt", () => {    for (let x = 10; x <= 23; x++) g.pixel(x, 36, x % 2 ? "w3" : "w2");    g.pixel(10, 36, "w4");    g.pixel(19, 36, "w4"); g.pixel(20, 36, "w3"); g.pixel(19, 37, "w2");      // knot    const d = Math.round(wind * 0.9);    g.line([19, 38], [19 + d, 40], "w2");    g.line([20, 37], [21 + d, 39], "w3");  });}// ---- straw ------------------------------------------------------------------------------------------------------// a fan of strands from a root point; tips: [[dx,dy],...]; each strand a line, alternating straw tonesfunction tuft(g, name, root, tips, sway) {  g.part(name, () => {    tips.forEach(([dx, dy], i) => {      const k = i % 3 === 0 ? "g1" : i % 3 === 1 ? "g2" : "g0";      const tx = root[0] + dx + Math.round(sway * (0.6 + 0.2 * (i % 3))), ty = root[1] + dy;      g.line(root, [tx, ty], k);      if (k === "g2") g.pixel(tx, ty, "g3");    });  });}function straw(g, sway) {  // cuffs: splay outward and down below the bar ends  tuft(g, "strawCuffL", [5, 29], [[-2, 0], [-3, 2], [-2, 4], [-4, 3], [-1, 4], [-3, 1]], sway);  tuft(g, "strawCuffR", [27, 29], [[2, 0], [2, 2], [2, 4], [3, 3], [1, 4], [3, 1]], sway);  // hanging under the hem around the post  tuft(g, "strawHem", [16, 43], [[-4, 4], [-2, 5], [-3, 3], [2, 5], [4, 4], [3, 3], [0, 5], [-5, 3], [5, 3]], sway);}function strawGap(g) {                                                       // straw stuffing seen through the open front  g.part("strawFront", () => g.field({ x: [12, 21], y: [37, 46] }, (x, y) => {    if (coatAt(x, y, 0) !== "gap") return null;    return x === 16 ? "g2" : x === 17 ? "g1" : (x + y) % 3 === 0 ? "g0" : "g1";  }));}// ---- head + hat (one joint, so the hat rides the head) ----------------------------------------------------------// face: yaw 1 = looking off to the viewer's left, 0 = facing the viewer. glow 0 = dead carved holes .. 1 = burningfunction head(g, angle, yaw, glow, hatSway, flick) {  g.joint("head", { at: NECK, angle }, () => {    g.outlined("ink", () => {      g.part("neckStraw", () => {        for (const [dx, k] of [[-3, "g1"], [-2, "g2"], [-1, "g1"], [0, "g2"], [1, "g0"], [2, "g1"], [3, "g0"]]) {          g.line([Math.sign(dx) * 0.5, 0], [dx * 1.3, 2 + (Math.abs(dx) === 2 ? 1 : 0)], k);        }      });      g.part("sack", () => g.field({ x: [-8, 8], y: [-14, 0] }, (x, y) => {        const inBall = litIndex(x, y, 0, -7, 6.6, 6.2, 4, 0.1);        const inNeck = y >= -3 && y <= -1 && Math.abs(x + 0.5) <= 3 - (y + 3) * 0.6;        if (inBall < 0 && !inNeck) return null;        let k = inBall < 0 ? 1 : inBall;        const seamX = Math.round(4.5 - 2.5 * yaw);                            // stitched back seam turns with the head        if (x === seamX && y > -12 && y < -2) return (y % 2) ? "w1" : SACK[Math.max(0, k - 1)];        if (k === 3 && (y === -9 || y === -6) && x < -2) k = 2;                // a couple of weave lines on the lit cheek        return SACK[k];      }));      g.part("tie", () => {        for (let x = -2; x <= 2; x++) g.pixel(x, -1, x < 0 ? "w3" : x === 0 ? "w2" : "w1");        g.pixel(-2, 0, "w2"); g.pixel(1, 0, "w1");      });      hat(g, hatSway);    });    face(g, yaw, glow, flick);  });}function hat(g, sway) {  g.part("hatCone", () => {    const tip = [6.5 + sway * 1.6, -21.5 + Math.abs(sway) * 0.6];    g.sweep([[0, -15], [0.4, -19], [2.0, -22.5], tip], { w0: 4.6, w1: 0.6, profile: 0.9 }, (u, v) => {      const s = -v * (u < 0.62 ? -1 : 1);      if (u > 0.6 && u < 0.68) return "n0";                                  // crease where the tip folds over      return HAT[s < -0.4 ? 2 : s < 0.25 ? 1 : 0];    });  });  g.part("hatBand", () => {    for (let x = -4; x <= 3; x++) { g.pixel(x, -17, x < -1 ? "r2" : x < 2 ? "r1" : "r0"); g.pixel(x, -16, x < 0 ? "r1" : "r0"); }    g.rect({ x: [-2, -1], y: [-17, -16] }, "g1", { fill: "g1" }); g.pixel(-2, -17, "g3");  });  g.part("hatBrim", () => g.field({ x: [-11, 11], y: [-18, -10] }, (x, y) => {    const ax = Math.abs(x + 0.5);    const droop = (ax > 6 ? 1 : 0) + (ax > 8.5 ? 1 : 0);    const nx = (x + 0.5) / 10, ny = (y + 0.5 - (-14.5 + droop)) / 2.3;    if (nx * nx + ny * ny > 1) return null;    if (ny > 0.15) return x < -4 ? "v0" : "n0";                               // the brim's front edge, in shade    if (y === -16 && x < -2) return "v2";    return x < -1 ? "v1" : x < 5 ? "v0" : "n0";  }));}function eyeAt(cx, cy, w) {                                                    // carved triangle pointing up  const px = [[0, -2], [-1, -1], [0, -1], [1, -1], [-1, 0], [0, 0], [1, 0]];  return px.filter(([dx]) => w >= 3 || dx <= 0).map(([dx, dy]) => [Math.round(cx) + dx, cy + dy]);}function face(g, yaw, glow, flick) {  const lx = -2.5 - 1.5 * yaw, rx = 2.5 - 3.3 * yaw, mx = -1.6 * yaw;  const hot = glow > 0.85 ? (flick > 0.5 ? "fl3" : "fl2") : glow > 0.6 ? "fl2" : glow > 0.3 ? "fl1" : glow > 0.05 ? "fl0" : "w0";  const rim = glow > 0.85 ? "fl2" : glow > 0.6 ? "fl1" : glow > 0.3 ? "fl0" : "w0";  g.part("eyes", () => {    for (const [cx, w] of [[lx, 3], [rx, yaw > 0.5 ? 2 : 3]]) {      const px = eyeAt(cx, -7, w);      const core = Math.round(cx);      px.forEach(([x, y]) => g.pixel(x, y, glow <= 0.05 ? (y === -7 ? "w0" : "ink") : (x === core && y > -9) ? hot : rim));      if (glow > 0.6) px.filter(([, y]) => y === -7).forEach(([x]) => g.pixel(x, -6, "w4"));   // glow spills on the burlap    }  });  g.part("mouth", () => {    const m = Math.round(mx), n = yaw > 0.5 ? 2 : 3;    for (let x = m - n; x <= m + n; x++) g.pixel(x, -4, glow > 0.3 ? (Math.abs(x - m) <= 1 ? hot : rim) : "w0");    for (const x of [m - 2, m, m + 2]) if (x >= m - n && x <= m + n) { g.pixel(x, -5, "n4"); g.pixel(x, -3, "n3"); }  });}// ---- haunting effects --------------------------------------------------------------------------------------------const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;function haloAt(g, t, strength) {  const C = { x: 16, y: 18 }, R = (10.5 + 0.7 * Math.sin(TAU * 2 * t)) * strength;  if (R < 2) return;  g.detached("halo", () => g.field({ x: [2, 30], y: [4, 32] }, (x, y) => {    const r = Math.hypot(x + 0.5 - C.x, (y + 0.5 - C.y) * 0.9);    if (r > R) return null;    return dith(x, y, 0.3 * (1 - Math.max(0, r - 6.5) / (R - 6.5))) ? "fl0" : null;  }));}function embers(g, t, amount) {  g.detached("embers", () => {    for (let i = 0; i < 5; i++) {      const p = (t + i / 5) % 1;      if (p > amount) continue;      const x = Math.round(16 + (i % 2 ? 9 : -9) + (i - 2) * 0.8 + Math.sin(TAU * (p + i * 0.31)) * 1.5 - p * 2);      const y = Math.round(14 - p * 14);      if (y < 0) continue;      if (p < 0.3) { g.pixel(x, y, "fl3"); g.pixel(x, y + 1, "fl1"); }      else if (p < 0.65) g.pixel(x, y, "fl2");      else if (chk(x, y)) g.pixel(x, y, "fl0");    }  });}// ---- frames ------------------------------------------------------------------------------------------------------function body(g, wt) {  const wind = Math.sin(TAU * wt), lag = Math.sin(TAU * (wt - 0.1));  const sh = Math.round(wind * 1.1), flap = 0.5 + 0.5 * Math.sin(TAU * 2 * wt);  contactShadow(g, CX, GROUND, 8, 1);  g.outlined("ink", () => frame(g));  g.outlined("ink", () => { straw(g, lag * 1.2); });  g.outlined("ink", () => { coat(g, sh, flap); });  strawGap(g);  belt(g, wind);  g.outlined("ink", () => tuft(g, "strawCollar", [16, 25], [[-5, -1], [-4, -2], [5, -1], [4, -2], [-3, -1], [3, -1]], lag * 0.6));  g.outlined("ink", () => mound(g, wind));  return { wind, lag };}function idleFrame(g, t) {  const { lag } = body(g, t);  head(g, 3 * Math.sin(TAU * t), 1, 0, lag, 0);}const H_TILT = -9;function hauntedFrame(g, t) {  haloAt(g, t, 1);  const { lag } = body(g, t);  const flick = 0.5 + 0.5 * Math.sin(TAU * 3 * t + 0.4);  head(g, H_TILT + 3 * Math.sin(TAU * t), 0, 1, lag, flick);  embers(g, t, 1);}function turnFrame(g, f) {  if (f >= N_TURN - 1) return hauntedFrame(g, 0);  const k = f / (N_TURN - 1), e = k * k * (3 - 2 * k);                         // eased turn  const wt = f / (N_TURN - 1);  if (f >= 5) haloAt(g, 0, f === 5 ? 1.25 : 1.1);                              // a flare as the eyes catch  const { lag } = body(g, wt);  const glow = [0, 0, 0.1, 0.35, 0.65, 1, 1][f];  head(g, 3 * Math.sin(TAU * wt) * (1 - e) + H_TILT * e, 1 - e, glow, lag, f === 5 ? 1 : 0.6);  if (f >= 5) embers(g, 0, (f - 4) / 3);}const Scarecrow = sprite(pal, (g, t, mode) => {  if (mode === "turn") return turnFrame(g, Math.min(N_TURN - 1, Math.round(t * N_TURN)));  t = ((t % 1) + 1) % 1;  if (mode === "haunted") return hauntedFrame(g, t);  idleFrame(g, t);});submit(Scarecrow, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "haunted", args: ["haunted"] },    { name: "turn", args: ["turn"], animate: { fps: 10, duration: N_TURN / 10, loop: false } },  ],});

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.