Scarecrow
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”.
6×
Get 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 — 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 0The last lines. submit declares the canvas and lists the variants the renderer exports.
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 — 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.