Tessarune
Harvest Festival · apple_tub.js

Apple tub

48×48 · 270 lines of JavaScript

A 48×48 floor prop showing a wooden half-barrel bound by iron hoops and filled with water where red and green apples float. In the 16-frame idle loop, the apples bob and drift as rings spread and glints shimmer. The splash variant is an 8-frame one-shot where the centre apple ducks under with a crown of droplets, while empty loops across 16 frames with a single circling apple.

Apple tub — Rendered output6×
Rendered by this program, variant “splash” · 48×48 · 8 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.

apple_tub.js · 1–9
// Apple tub — 48x48 floor prop, top-down 3/4, key light top-left.// Bobbing for apples: a wide wooden half-barrel of tapered staves bound by two riveted iron hoops, its end-grain rim// jointed per stave, filled to just under the rim with water (dark under the far wall, lighter toward the near edge).// Red and green apples float in it, each with a stem, a lit shoulder and a bright meniscus ring at the waterline.// Variants://   idle    16f loop  the apples bob and drift slowly and turn their stems, rings spread from each, a glint shimmers//   splash  8f once   the centre apple is ducked under, a crown of droplets rises and falls, rings spread wide,//                     the apple bobs back up and the other apples rock; ends on idle frame 0//   empty   16f loop  one apple left circling slowly, rings spreading over the open water, the glint shimmers

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

apple_tub.js · 260–270
submit(AppleTub, {  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: "splash", args: ["splash"], animate: { fps: 10, duration: N_SPLASH / 10, loop: false } },    { name: "empty", args: ["empty"] },  ],});
Show the whole program (270 lines)
apple_tub.js · 270 lines
// Apple tub — 48x48 floor prop, top-down 3/4, key light top-left.// Bobbing for apples: a wide wooden half-barrel of tapered staves bound by two riveted iron hoops, its end-grain rim// jointed per stave, filled to just under the rim with water (dark under the far wall, lighter toward the near edge).// Red and green apples float in it, each with a stem, a lit shoulder and a bright meniscus ring at the waterline.// Variants://   idle    16f loop  the apples bob and drift slowly and turn their stems, rings spread from each, a glint shimmers//   splash  8f once   the centre apple is ducked under, a crown of droplets rises and falls, rings spread wide,//                     the apple bobs back up and the other apples rock; ends on idle frame 0//   empty   16f loop  one apple left circling slowly, rings spreading over the open water, the glint shimmersconst W = 48, H = 48, CX = 24, GROUND = 45;const RCY = 24, RRX = 20, RRY = 7;          // rim (outer ellipse)const MRX = 17, MRY = 5;                    // mouth (inside of the rim)const WY = RCY + 2;                         // water surface centre (2px below the mouth)const BCY = 36, BRX = 18, BRY = 6;          // bottom curve of the tapered wallconst N_LOOP = 16, N_SPLASH = 8;const e2 = (x, y, cx, cy, rx, ry) => ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2;const inRim = (x, y) => e2(x, y, CX, RCY, RRX, RRY) <= 0.99;const inMouth = (x, y) => e2(x, y, CX, RCY, MRX, MRY) <= 0.99;const inWater = (x, y) => inMouth(x, y) && e2(x, y, CX, WY, MRX, MRY) <= 0.99;const isRing = (x, y) => inRim(x, y) && !inMouth(x, y);const hwAt = (y) => RRX + (BRX - RRX) * Math.max(0, Math.min(1, (y - RCY) / (BCY - RCY)));const isWall = (x, y) => {  if (y <= RCY || inRim(x, y)) return false;  if (y <= BCY) return Math.abs(x + 0.5 - CX) <= hwAt(y);  return e2(x + 0.5, y, CX, BCY, BRX, BRY) <= 1;};const LAST = [];                            // last rim row per columnfor (let x = 0; x < W; x++) { LAST[x] = -1; for (let y = 0; y < H; y++) if (inRim(x, y)) LAST[x] = y; }// ---- tub ------------------------------------------------------------------------------------------------------const WOOD = ["w0", "w1", "w2", "w3", "w4"], IRON = ["n0", "n1", "n2", "n3", "n4"];const cl = (i, a = 0, b = 4) => Math.max(a, Math.min(b, i));const staveOf = (x, y) => Math.floor(Math.asin(Math.max(-0.99, Math.min(0.99, (x + 0.5 - CX) / hwAt(y)))) / (Math.PI / 10));const HOOPS = [[3, 4], [9, 10]];            // rows below the rim's front edgefunction wallTone(x, y) {  const dy = y - LAST[x], nx = (x + 0.5 - CX) / hwAt(y);  const lv = nx < -0.62 ? 3 : nx < 0.05 ? 2 : nx < 0.7 ? 1 : 0;     // cylinder lit from the left  const hoop = HOOPS.findIndex(([a, b]) => dy >= a && dy <= b);  if (hoop >= 0) {    const top = dy === HOOPS[hoop][0];    const riv = top && [-0.75, -0.3, 0.2, 0.65].some((u) => Math.round(CX + u * hwAt(y) - 0.5) === x);    if (riv) return lv >= 2 ? "n4" : "n3";    return IRON[cl(lv + (top ? 1 : 0) - (lv === 0 ? 0 : 0), 0, 4)];  }  if (HOOPS.some(([a, b]) => dy === b + 1)) return WOOD[cl(lv - 1)];   // the hoop's shadow on the staves  let s = lv;  if (dy === 1) s -= 1;                                              // under the rim's lip  if (staveOf(x, y) !== staveOf(x + 1, y) && x + 1 < CX + hwAt(y)) s -= 1;   // stave seam  if (!isWall(x, y + 1) || !isWall(x, y + 2)) s -= 1;                 // the floor-facing bottom edge  return WOOD[cl(s)];}function rimTone(x, y) {  const nx = (x + 0.5 - CX) / RRX, ny = (y + 0.5 - RCY) / RRY;  const ang = (xx, yy) => Math.floor(((Math.atan2((yy + 0.5 - RCY) / RRY, (xx + 0.5 - CX) / RRX) / TAU) * 20 + 20.5)) % 20;  if (ang(x + 1, y) !== ang(x, y) && isRing(x + 1, y) && ny < 0.3) return "w1";   // stave joints (far half)  const outer = !inRim(x - 1, y) || !inRim(x + 1, y) || !inRim(x, y - 1) || !inRim(x, y + 1);  const inner = inMouth(x - 1, y) || inMouth(x + 1, y) || inMouth(x, y - 1) || inMouth(x, y + 1);  const a = -0.75 * nx - 0.6 * ny;  if (outer && a > 0.2) return "w4";                                 // lit outer arris, upper left  if (inner && ny > 0.25 && nx < 0.35) return "w4";                  // near inner arris catches the light  if (inner && ny < 0) return "w2";                                  // far inner bevel drops into the tub  if (outer && ny > 0.3 && nx > 0.35) return "w1";  return a > -0.35 ? "w3" : "w2";}function insideTone(x, y) {  // far inner wall above the water: staves seen from inside; their right side faces the light  if (inWater(x, y + 1)) return "w0";                                // dark line where wall meets water  const nx = (x + 0.5 - CX) / MRX;  return nx > 0.25 ? "w2" : "w1";}function waterTone(x, y) {  if (!inWater(x, y - 1) || !inWater(x, y - 2)) return "b0";         // the far wall's reflection  const ny = (y + 0.5 - WY) / MRY;  if (ny > 0.55 && inWater(x, y + 1)) return "b1";  return "b1";}function tub(g) {  g.part("tub", () => g.field({ x: [CX - RRX - 1, CX + RRX + 1], y: [RCY - RRY - 1, H - 1] }, (x, y) => {    if (isRing(x, y)) return rimTone(x, y);    if (inMouth(x, y)) return inWater(x, y) ? waterTone(x, y) : insideTone(x, y);    if (isWall(x, y)) return wallTone(x, y);    return null;  }));  g.part("wear", () => {    g.pixel(9, 37, "w1"); g.pixel(10, 38, "w1");                      // a split in a stave    g.pixel(37, 33, "w0");                                           // a knot    g.pixel(30, 39, "w1");    g.pixel(6, 25, "e1"); g.pixel(7, 26, "e2");                      // a fleck of moss on the rim  });}// ---- water light -----------------------------------------------------------------------------------------------function glint(g, t) {  g.part("glint", () => {    const q = Math.floor(t * 8) % 4;    const len = [3, 4, 3, 2][q], x0 = 10 + [0, 0, 1, 1][q];    g.line([x0, 26], [x0 + len - 1, 26], "b3");    g.line([x0 + 1, 27], [x0 + 1 + [1, 2, 1, 0][q], 27], "b2");    if (Math.sin(TAU * (t + 0.2)) > 0.3) g.pixel(38, 25, "b2");    if (Math.sin(TAU * (t * 2 + 0.6)) > 0.5) g.pixel(36, 27, "b3");    g.line([26, 29], [28, 29], "b2");                                 // near-edge sheen  });}function rings(g, list) {  // list: [[cx, cy, r, key]] — elliptical ripple rings, only on open water  if (!list.length) return;  g.part("ripples", () => g.field({ x: [CX - MRX, CX + MRX], y: [RCY - MRY, RCY + MRY] }, (x, y) => {    if (!inWater(x, y) || !inWater(x, y - 1)) return null;    for (const [cx, cy, r, key] of list) {      const q = Math.hypot(x + 0.5 - cx, (y + 0.5 - cy) * 2.6);      if (Math.abs(q - r) < 0.75) return key === "faint" ? (chk(x, y) ? "b2" : null) : key;    }    return null;  }));}// ---- apples ----------------------------------------------------------------------------------------------------// 6 rows; a floating apple shows rows 0..4 above its waterline (row 5 is under water)const SHAPE = [[1, 4], [0, 5], [0, 5], [0, 5], [0, 5], [1, 4]];const SKIN = { red: ["r0", "r1", "r1", "r2"], green: ["e0", "e1", "e2", "e2"] };const SHINE = { red: "r3", green: "e3" };function apple(g, ax, wl, kind, sink, stem, name) {  // ax: left column, wl: waterline row (the last visible row), sink: rows pushed under (negative = lifted)  const top = wl - 4 + sink, m = new Map();  const P = (x, y, k) => m.set(y * W + x, k);  const ramp = SKIN[kind];  for (let r = 0; r < 6; r++) {    const y = top + r;    if (y > wl) break;    for (let c = SHAPE[r][0]; c <= SHAPE[r][1]; c++) {      let i = litIndex(c, r, 3, 3, 3.2, 3.2, 4, -0.02);      if (i < 0) i = 0;      P(ax + c, y, ramp[i]);    }  }  if (top <= wl) {    P(ax + 1, top + 1, "n5");                                         // specular on the lit shoulder    if (top + 2 <= wl) P(ax + 1, top + 2, SHINE[kind]);    if (top + 1 <= wl) P(ax + 2, top + 1, SHINE[kind]);    if (top >= 0) P(ax + 2 + stem, top, ramp[0]);                     // the dimple  }  const out = new Map(m);  if (top - 1 <= wl && top >= 1) out.set((top - 1) * W + ax + 2 + stem, "w1");   // stem  if (kind === "green" && top >= 1 && top <= wl) out.set((top - 1) * W + ax + 3 + stem, "e2");   // a leaf  // ink around it (not below the waterline), meniscus ring under it  const ink = new Set();  for (const i of out.keys()) {    const x = i % W, y = (i - x) / W;    for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y - 1], [x, y + 1]]) {      const k = ny * W + nx;      if (!out.has(k) && ny <= wl) ink.add(k);    }  }  const wr = SHAPE[Math.min(5, wl - top)] || [1, 4];  g.part(name, () => g.field({ x: [ax - 2, ax + 8], y: [wl - 7, wl + 1] }, (x, y) => {    const i = y * W + x;    if (out.has(i)) return out.get(i);    if (ink.has(i)) return "ink";    if (y === wl + 1 && top <= wl && inWater(x, y)) {      const c = x - ax;      if (c >= wr[0] && c <= wr[1]) return "b3";      if (c === wr[0] - 1 || c === wr[1] + 1) return "b2";    }    return null;  }));}// apples: [left x, waterline, kind, phase]const APPLES = [  [9, 24, "red", 0.0], [30, 23, "green", 0.62], [19, 24, "red", 0.37], [24, 28, "green", 0.81],];const DUCK = 2;                              // the centre apple is the one ducked in the splashfunction applePose(ph, t) {  return {    dx: Math.round(Math.sin(TAU * (t + ph)) * 1.2),    sink: Math.sin(TAU * (2 * t + ph * 1.7)) > 0.35 ? 1 : 0,    stem: Math.round(Math.sin(TAU * (t + ph + 0.25)) * 1.1),  };}function appleRing(ax, wl, ph, t) {  const p = (t * 2 + ph) % 1;  return [ax + 3, wl + 1, 3.5 + p * 4.5, p < 0.45 ? "b2" : "faint"];}// ---- splash ----------------------------------------------------------------------------------------------------// per splash frame: sink of the ducked apple, rock of the others, ring radii, droplet setconst DUCK_SINK = [0, 3, 6, 5, -1, 1, 0, 0];const ROCK = [0, 0, 0, 1, 1, 0, 0, 0];function droplets(g, f, cx, cy) {  // the crown: a central column and side drops, each a 2px streak (bright head, darker tail along its motion)  const up = f <= 4;  const D = {    2: [[0, -1], [-2, 0], [2, 0]],    3: [[0, -4], [-2, -3], [2, -3], [-4, -1], [4, -1], [-1, -6]],    4: [[0, -8], [-3, -6], [3, -6], [-5, -3], [5, -4], [1, -10], [-1, -5]],    5: [[0, -6], [-4, -5], [4, -5], [-7, -1], [7, -2], [1, -8]],    6: [[-6, 1], [7, 1], [0, -2], [-9, 2], [9, 2]],  }[f] || [];  g.detached("droplets", () => {    for (const [dx, dy] of D) {      g.pixel(cx + dx, cy + dy, "b3");      if (f !== 6) g.pixel(cx + dx, cy + dy + (up ? 1 : -1), "b2");    }    if (f === 3) g.rect({ x: [cx - 1, cx + 1], y: [cy - 1, cy + 1] }, "b2", { fill: "b2" });   // the water heaves up    if (f === 3) g.pixel(cx, cy - 2, "b3");  });}function stateAt(state, t) {  t = ((t % 1) + 1) % 1;  if (state !== "splash") return { mode: state, t, f: -1 };  const f = Math.min(N_SPLASH - 1, Math.round(t * N_SPLASH));  if (f === 0 || f === N_SPLASH - 1) return { mode: "idle", t: 0, f: -1 };  return { mode: "splash", t: 0, f };}const AppleTub = sprite(pal, (g, t, state) => {  const S = stateAt(state, t);  contactShadow(g, CX, GROUND, 20, 2);  g.outlined("ink", () => tub(g));  if (S.mode === "empty") {    const [ax0, wl, kind] = [21, 25, "red"];    const ax = ax0 + Math.round(Math.cos(TAU * S.t) * 4), wlx = wl + Math.round(Math.sin(TAU * S.t) * 1.4);    glint(g, S.t);    rings(g, [0, 0.5].map((o) => { const p = (S.t + o) % 1; return [ax + 3, wlx + 1, 3.5 + p * 9, p < 0.4 ? "b2" : "faint"]; }));    const sink = Math.sin(TAU * (2 * S.t)) > 0.35 ? 1 : 0;    apple(g, ax, wlx, kind, sink, Math.round(Math.sin(TAU * (S.t + 0.25)) * 1.1), "apple0");    return;  }  glint(g, S.t);  const L = [];  const f = S.f;  APPLES.forEach(([ax, wl, , ph], i) => {    const P = applePose(ph, S.t);    if (f < 0 || i !== DUCK) L.push(appleRing(ax + P.dx, wl, ph, S.t));  });  if (f > 0) {    const [ax, wl] = APPLES[DUCK], cx = ax + 3, cy = wl + 1;    const R = { 1: [[3, "b3"]], 2: [[2, "b3"], [5, "b2"]], 3: [[4, "b3"], [8, "b2"]], 4: [[6, "b2"], [11, "faint"]], 5: [[9, "b2"], [3, "b3"]], 6: [[12, "faint"], [6, "b2"]] }[f] || [];    for (const [r, k] of R) L.push([cx, cy, r, k]);  }  rings(g, L);  const order = APPLES.map((a, i) => i).sort((a, b) => APPLES[a][1] - APPLES[b][1]);  for (const i of order) {    const [ax, wl, kind, ph] = APPLES[i];    const P = applePose(ph, S.t);    let sink = P.sink;    if (f > 0) sink = i === DUCK ? DUCK_SINK[f] : Math.min(1, sink + ROCK[f]);    if (sink >= 5) continue;                                          // fully under    apple(g, ax + P.dx, wl, kind, sink, P.stem, "apple" + i);  }  if (f > 0) {    const [ax, wl] = APPLES[DUCK];    if (f === 2) g.detached("bubble", () => { g.pixel(ax + 3, wl + 1, "b3"); g.pixel(ax + 1, wl, "b2"); });    droplets(g, f, ax + 3, wl - 1);  }});submit(AppleTub, {  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: "splash", args: ["splash"], animate: { fps: 10, duration: N_SPLASH / 10, loop: false } },    { name: "empty", args: ["empty"] },  ],});

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.