Windmill
A 64×64 post windmill features a fieldstone roundhouse, a timber-framed body, a red shingled barrel cap, a sack hoist, and four cloth lattice sails. In the 16-frame idle loop, the sails make a quarter turn while the sack gently sways, and the 8-frame fast loop spins them twice as fast with blur arcs at the tips. The 16-frame still variant stops the sails in an X while a sparrow lands on the cap, pecks, and departs.
6×
Get 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.
// Windmill — 64x64 floor prop (large), top-down 3/4, key light top-left.// A small post windmill for the harvest fair: a round fieldstone roundhouse with a wooden ladder up the front, a// timber-framed weatherboarded body (corner posts, braces, an arched door and a little window), a barrel cap of red// shingles, a sack hoist on the right with a rope and a grain sack, and four lattice sails covered in cloth.// Variants:// idle 16f loop: the sails turn slowly, a quarter turn per loop (4 identical sails, so the loop closes);// the hoisted sack sways a little// fast 8f loop: the sails spin twice as fast with blur arcs at the tips, the sack swings hard on its rope// still 16f loop: sails stopped in an X with one sail's cloth rolled up on its stock (bare lattice), the other// cloths ripple; a sparrow flies in, lands on the cap, hops and pecks, then flies offThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Windmill, { 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: "fast", args: ["fast"], animate: { fps: 10, duration: N_FAST / 10, loop: true } }, { name: "still", args: ["still"] }, ],});Show the whole program (292 lines)
// Windmill — 64x64 floor prop (large), top-down 3/4, key light top-left.// A small post windmill for the harvest fair: a round fieldstone roundhouse with a wooden ladder up the front, a// timber-framed weatherboarded body (corner posts, braces, an arched door and a little window), a barrel cap of red// shingles, a sack hoist on the right with a rope and a grain sack, and four lattice sails covered in cloth.// Variants:// idle 16f loop: the sails turn slowly, a quarter turn per loop (4 identical sails, so the loop closes);// the hoisted sack sways a little// fast 8f loop: the sails spin twice as fast with blur arcs at the tips, the sack swings hard on its rope// still 16f loop: sails stopped in an X with one sail's cloth rolled up on its stock (bare lattice), the other// cloths ripple; a sparrow flies in, lands on the cap, hops and pecks, then flies offconst W = 64, H = 64, GROUND = 60, CX = 32;const HUB = { x: 32, y: 22 };const R = 21, R0 = 6, SW = 6; // sail reach, where the lattice starts, lattice widthconst L = [-0.74, -0.67]; // direction toward the key light (screen space, y down)const STONE = ["n0", "n1", "n2", "n3", "n4"];const WOOD = ["w0", "w1", "w2", "w3", "w4"];const CAP = ["n0", "r0", "r1", "r2", "r3"];const N_IDLE = 16, N_FAST = 8;// ---- fieldstone roundhouse ------------------------------------------------------------------------------------const BASE = { rx: 13, top: 47, bot: 57, ry: 3 };function base(g) { const { rx, top, bot, ry } = BASE; g.part("base", () => g.field({ x: [CX - rx, CX + rx - 1], y: [top - ry, bot + ry] }, (x, y) => { const nx = (x + 0.5 - CX) / rx; if (Math.abs(nx) > 1) return null; const c = Math.sqrt(1 - nx * nx), yc = y + 0.5; if (yc < top - ry * c || yc > bot + ry * c) return null; if (yc < top + ry * c) { // capstone ring on top, lit from the top-left const a = -0.8 * nx - 0.4 * ((yc - top) / ry); return a > 0.35 ? "n4" : a > -0.25 ? "n3" : "n2"; } // courses curve with the wall; stones staggered row by row const yy = yc - (top + ry * c); const row = Math.floor(yy / 3), within = yy - row * 3; const arc = Math.asin(nx) * rx + (row % 2) * 2.5; const col = Math.floor(arc / 5), fc = arc / 5 - col; const light = -0.85 * nx + 0.25 * c - 0.04 * yy; let k = light > 0.45 ? 4 : light > 0.0 ? 3 : light > -0.45 ? 2 : 1; if (within < 1 || fc < 0.16) return k >= 3 ? "n1" : "n0"; // mortar const h = hash(col, row); if (h > 0.78) k = Math.max(1, k - 1); else if (h < 0.12) k = Math.min(4, k + 1); if (within > 2 && k > 1) k--; // each stone's lower lip in shade if (yy > bot - top + ry * c - 1.6) k = Math.max(0, k - 2); // damp footing return STONE[k]; }));}function ladder(g) { g.part("ladder", () => { for (const [x0, x1, lit] of [[30, 29, true], [34, 35, false]]) { g.line([x0, 47], [x1, 59], lit ? "w3" : "w1"); } for (let y = 49; y <= 58; y += 3) { const xl = 30 - Math.round((y - 47) / 12), xr = 34 + Math.round((y - 47) / 12); g.line([xl + 1, y], [xr - 1, y], "w3"); g.pixel(xl + 1, y, "w4"); } });}// ---- timber body ----------------------------------------------------------------------------------------------const BODY = { x0: 21, x1: 43, y0: 25, y1: 46 };function body(g) { const { x0, x1, y0, y1 } = BODY; g.part("sideWall", () => { // the right side in shade, receding up-right under the cap g.fillPoly([[x1 + 1, y0], [x1 + 4, y0 - 4], [x1 + 4, y1 - 5], [x1 + 1, y1]], "w1"); for (const x of [x1 + 2, x1 + 4]) g.line([x, y0 - 1], [x, y1 - 2 - (x - x1)], "w0"); g.line([x1 + 1, y0 + 1], [x1 + 1, y1], "w2"); g.rect({ x: [x1 + 2, x1 + 3], y: [30, 32] }, "b0", { fill: "b0" }); // small side window g.pixel(x1 + 2, 30, "b1"); }); g.part("frontWall", () => g.field({ x: [x0, x1], y: [y0, y1] }, (x, y) => { // vertical weatherboards, a darker frame of posts/rails/braces, and the cap's shadow along the top if (y <= y0 + 1) return y === y0 ? "w0" : "w1"; if (x <= x0 + 1) return x === x0 ? "w3" : "w2"; // corner post, lit edge if (x >= x1 - 1) return x === x1 ? "w1" : "w2"; if (y === 35 || y === 36) return y === 35 ? "w3" : "w1"; // mid rail if (y >= y1 - 1) return y === y1 ? "w0" : "w1"; // sole plate const bL = y < 35 ? (x - x0) - (y - y0) * 0.75 : 99, bR = y < 35 ? (x1 - x) - (y - y0) * 0.75 : 99; if (y < 35 && (Math.abs(bL - 2) < 0.8 || Math.abs(bR - 2) < 0.8)) return Math.abs(bL - 2) < 0.8 ? "w2" : "w1"; const plank = (x - x0 - 2) % 3; if (plank === 2) return "w2"; // board seams const lit = 0.55 - (x - x0) / (x1 - x0) * 0.6 - (y - y0) / (y1 - y0) * 0.3; return lit > 0.3 ? "w4" : lit > -0.05 ? "w3" : "w2"; })); g.part("window", () => { g.rect({ x: [24, 27], y: [28, 31] }, "w0", { fill: "w0" }); g.rect({ x: [25, 26], y: [29, 30] }, "b0", { fill: "b0" }); g.pixel(25, 29, "b2"); g.line([24, 32], [27, 32], "w4"); // sill catching the light }); g.part("door", () => { g.fillPoly([[29, 46], [29, 40], [30, 38], [34, 38], [35, 40], [35, 46]], "w1"); g.line([30, 39], [30, 46], "w2"); g.line([32, 38], [32, 46], "w0"); g.line([34, 40], [34, 46], "w0"); g.line([29, 40], [30, 38], "w0"); g.line([30, 38], [34, 38], "w0"); g.line([34, 38], [35, 40], "w0"); g.line([30, 41], [33, 41], "n1"); g.line([30, 44], [33, 44], "n1"); // iron straps g.pixel(30, 41, "n3"); g.pixel(34, 43, "g2"); // hinge glint, brass latch });}// ---- barrel cap of shingles ----------------------------------------------------------------------------------const ARCH = { cx: 32, cy: 25, rx: 14, ry: 10 }, BACK = [3, -6];function capSample(x, y) { // first arch (front -> back) whose upper boundary has reached the pixel; s = 0 is the front gable for (let i = 0; i <= 24; i++) { const s = i / 24, cx = ARCH.cx + BACK[0] * s, cy = ARCH.cy + BACK[1] * s; const nx = (x + 0.5 - cx) / ARCH.rx, ny = (y + 0.5 - cy) / ARCH.ry; if (ny <= 0.05 && nx * nx + ny * ny <= 1) return { s, nx, ny, r: Math.sqrt(nx * nx + ny * ny) }; } return null;}function cap(g) { g.part("cap", () => g.field({ x: [17, 50], y: [8, 26] }, (x, y) => { const c = capSample(x, y); if (!c) return null; if (c.s === 0 && c.r < 0.9) return null; // gable face drawn separately if (c.s === 0) return Math.atan2(-c.ny, c.nx) > 1.2 ? "r2" : "r1"; // the front eave of the shingles const ang = Math.atan2(-c.ny, c.nx); // 0 right .. PI left const lit = 0.9 * Math.cos(ang - 2.2) - 0.05; let k = lit > 0.55 ? 3 : lit > 0.0 ? 2 : lit > -0.45 ? 1 : 0; // shingle courses run along the cap; joints staggered course by course const arcLen = ang * ARCH.rx, course = Math.floor(arcLen / 3); if (arcLen - course * 3 < 0.9) k = Math.max(0, k - 1); else if (((Math.floor(c.s * 3) + course) % 2 === 0) && c.s > 0.08 && Math.abs(c.s * 3 - Math.round(c.s * 3)) < 0.12) k = Math.max(0, k - 1); if (k === 3 && arcLen > 30 && arcLen < 33) return "r3"; // sheen on the lit shoulder return CAP[k]; })); g.part("gable", () => g.field({ x: [18, 46], y: [14, 25] }, (x, y) => { const c = capSample(x, y); if (!c || c.s !== 0 || c.r >= 0.9) return null; if (c.r > 0.78) return "w0"; // shadow under the eave if (y % 2 === 1) return "w1"; // horizontal boards return x < 30 ? "w3" : "w2"; }));}// ---- sack hoist ---------------------------------------------------------------------------------------------function hoist(g, swing) { const P = [53, 22], len = 14, a = (swing * Math.PI) / 180; const end = [Math.round(P[0] + Math.sin(a) * len), Math.round(P[1] + Math.cos(a) * len)]; g.outlined("ink", () => { g.part("hoistBeam", () => { g.line([47, 22], [54, 20], "w2"); g.line([47, 21], [53, 19], "w3"); g.rect({ x: [52, 54], y: [21, 22] }, "n1", { fill: "n1" }); g.pixel(52, 21, "n3"); // pulley block }); g.part("rope", () => g.line([P[0], P[1] + 1], [end[0], end[1] - 1], "w4")); g.part("sack", () => { const [sx, sy] = end; g.field({ x: [sx - 3, sx + 3], y: [sy, sy + 7] }, (x, y) => { const i = litIndex(x, y, sx + 0.5, sy + 4.5, 3.4, 3.8, 4, 0.05); if (i < 0) return y <= sy + 1 && Math.abs(x - sx) <= 1 ? "w2" : null; // the gathered neck return ["w1", "w2", "w3", "w4"][i]; }); g.pixel(sx, sy + 1, "w0"); g.pixel(sx - 1, sy + 2, "w1"); // tie g.pixel(sx + 1, sy + 5, "w2"); g.pixel(sx - 1, sy + 4, "w4"); // a fold and a stencil mark }); });}// ---- sails ------------------------------------------------------------------------------------------------------// one field over the whole canvas; every pixel is inverse-rotated into each sail's own frame (u along, v across),// so the sails turn as solid geometry, never as moving single pixels.function sailAt(x, y, ang, rolledK, ripple) { const dx = x + 0.5 - HUB.x, dy = y + 0.5 - HUB.y; const r = Math.hypot(dx, dy); if (r > R + 0.6 || r < 2) return null; for (let k = 0; k < 4; k++) { const th = ang + (k * Math.PI) / 2, c = Math.cos(th), s = Math.sin(th); const u = dx * c + dy * s, v = -dx * s + dy * c; if (u < 0) continue; const nl = -s * L[0] + c * L[1]; // how much +v faces the light if (v >= -1.7 && v < 0 && u <= R + 0.5) { // the stock (whip) const side = v < -0.85 ? -nl : nl; return side > 0.2 ? "w3" : side > -0.4 ? "w2" : "w1"; } if (v >= 0 && v < SW && u >= R0 && u <= R + 0.5) { const rail = u > R - 0.6 || u < R0 + 0.9 || v > SW - 1; const bar = Math.abs(u - R0 - 3 * Math.round((u - R0) / 3)) < 0.5; if (k === rolledK) { if (v < 2.4) return Math.abs(u - 10) < 0.6 || Math.abs(u - 17) < 0.6 ? "r1" : (v < 1.2 ? (nl < 0 ? "n5" : "n4") : (nl < 0 ? "n4" : "n3")); if (rail || bar) return nl > 0 ? "w2" : "w3"; return "ink"; // the open lattice: dark gaps between bars } if (rail) return (v > SW - 1 ? nl : -nl) > 0 ? "w2" : "w1"; // cloth bellied by the wind: one edge turns toward the light, the other away const vc = (v - SW / 2) / (SW / 2); let lit = 0.5 + 0.55 * vc * nl + ripple(u); if (bar) lit -= 0.5; return lit > 0.72 ? "n5" : lit > 0.32 ? "n4" : "n3"; } } return null;}function sails(g, ang, rolledK, ripple) { g.outlined("ink", () => { g.part("sails", () => g.field({ x: [HUB.x - R - 1, HUB.x + R + 1], y: [HUB.y - R - 1, HUB.y + R + 1] }, (x, y) => sailAt(x, y, ang, rolledK, ripple))); g.part("hub", () => g.field({ x: [HUB.x - 3, HUB.x + 3], y: [HUB.y - 3, HUB.y + 3] }, (x, y) => { const i = litIndex(x, y, HUB.x + 0.5, HUB.y + 0.5, 3, 3, 4, 0.05); return i < 0 ? null : ["n0", "n1", "n2", "n4"][i]; })); }); g.part("hubBolt", () => { g.pixel(HUB.x, HUB.y, "n1"); g.pixel(HUB.x - 1, HUB.y - 1, "n5"); });}function blur(g, ang) { // fast: a dithered ghost of each sail trailing behind it g.detached("blur", () => g.field({ x: [HUB.x - R, HUB.x + R], y: [HUB.y - R, HUB.y + R] }, (x, y) => { const dx = x + 0.5 - HUB.x, dy = y + 0.5 - HUB.y, r = Math.hypot(dx, dy); if (r > R || r < R0 + 2) return null; let a = Math.atan2(dy, dx) - ang; a = ((a % (Math.PI / 2)) + Math.PI / 2) % (Math.PI / 2); // angle behind the nearest sail stock const d = a - SW / r; // past the cloth's far edge if (d < 0) return null; if (r > R - 1.3) return d < 0.16 ? "n4" : d < 0.36 && chk(x, y) ? "n3" : null; // tip streak if (r > R - 8 && d < 0.1) return chk(x, y) ? "n3" : null; // ghost of the cloth return null; }));}// ---- sparrow (still) -----------------------------------------------------------------------------------------const capTop = (x) => Math.ceil(ARCH.cy + BACK[1] - ARCH.ry * Math.sqrt(Math.max(0, 1 - ((x + 0.5 - ARCH.cx - BACK[0]) / ARCH.rx) ** 2)));function bird(g, f) { // [x, y(feet), pose, facing] pose: fly-up, fly-down, stand, hop, peck const ky = (x) => capTop(x) - 1; const K = [ null, [60, 2, "up", -1], [55, 2, "down", -1], [48, 4, "up", -1], [39, ky(39) - 2, "down", -1], [36, ky(36), "stand", -1], [36, ky(36), "stand", -1], [35, ky(35) - 2, "hop", -1], [34, ky(34), "stand", -1], [34, ky(34), "peck", -1], [34, ky(34), "stand", -1], [33, ky(33) - 2, "hop", -1], [32, ky(32), "stand", 1], [32, ky(32), "peck", 1], [29, ky(29) - 3, "up", -1], [22, 2, "down", -1], ][f]; if (!K) return; const [bx, by, p, dir] = K; const px = (dx, dy, k) => g.pixel(bx + dx * dir, by + dy, k); g.outlined("ink", () => g.part("bird", () => { if (p === "up" || p === "down") { px(-2, -1, "g2"); px(-1, -1, "n0"); px(0, -1, "n1"); px(1, -1, "n1"); px(2, -1, "n0"); px(3, -1, "n0"); px(4, -2, "n0"); px(0, 0, "n5"); px(1, 0, "n4"); if (p === "up") { px(0, -2, "n3"); px(1, -2, "n2"); px(1, -3, "n2"); px(2, -3, "n1"); } else { px(0, 1, "n2"); px(1, 1, "n1"); px(1, 2, "n1"); } return; } const dy = p === "peck" ? 1 : 0; px(-2, -3 + dy, "g2"); px(-1, -3 + dy, "n0"); px(0, -3 + dy, "n1"); px(-1, -4 + dy, "n1"); px(-1, -2, "n1"); px(0, -2, "n5"); px(1, -2, "n2"); px(2, -2, "n3"); px(3, -2, "n0"); px(4, -3, "n0"); px(0, -1, "n4"); px(1, -1, "n5"); px(2, -1, "n1"); if (p !== "hop") { px(0, 0, "g0"); px(2, 0, "g0"); } }));}// ---- assembly --------------------------------------------------------------------------------------------------function scene(g, ang, swing, rolledK, ripple) { contactShadow(g, CX, GROUND, 17, 2); g.outlined("ink", () => { base(g); body(g); cap(g); }); g.outlined("ink", () => ladder(g)); hoist(g, swing);}const Windmill = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; const still = () => 0; if (mode === "still") { const f = Math.round(t * N_IDLE) % N_IDLE; const ang = Math.PI / 4 + 0.08; scene(g, ang, 0, 0, (u) => 0.13 * Math.sin(TAU * 2 * t - u * 0.7)); sails(g, ang, 0, (u) => 0.13 * Math.sin(TAU * 2 * t - u * 0.7)); bird(g, f); return; } const fast = mode === "fast"; const ang = -(Math.PI / 2) * t + 0.3; // a quarter turn per loop, anticlockwise scene(g, ang, fast ? 20 * Math.sin(TAU * t) : 4 * Math.sin(TAU * t), -1, still); if (fast) blur(g, ang); sails(g, ang, -1, still);});submit(Windmill, { 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: "fast", args: ["fast"], animate: { fps: 10, duration: N_FAST / 10, loop: true } }, { name: "still", args: ["still"] }, ],});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.