Tessarune
Harvest Festival · windmill.js

Windmill

64×64 · 292 lines of JavaScript

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.

Windmill — Rendered output6×
Rendered by this program, variant “idle” · 64×64 · 16 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.

windmill.js · 1–10
// 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 off

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

windmill.js · 282–292
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.js · 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.