Tessarune
Witch's Hut · jack_o_lantern.js

Jack-o'-lantern

32×32 · 224 lines of JavaScript

A 32×32 floor prop: a ribbed pumpkin with a cut lid, a curled vine and leaf, and a carved face with triangle eyes and a jagged grin with two teeth. The candle inside lights the carved holes from within, leaks through the lid seam and spills a dithered pool of light onto the floor in front. The inner light comes in “fire”, “witch” (violet) and “soul” (blue) by swapping one colour ramp, plus “unlit” with a thin wisp of smoke, all 12-frame loops; “ignite” (8 frames, once) shows a spark catching and ends on the first frame of “fire”.

Jack-o'-lantern — Rendered output6×
Rendered by this program, variant “ignite” · 32×32 · 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.

jack_o_lantern.js · 1–10
// Jack-o'-lantern — 32x32 floor prop, top-down 3/4, key light top-left.// A ribbed pumpkin with a cut lid (seam around the stem), a curled vine and leaf, and a carved face: triangle eyes,// a nose and a jagged grin with two teeth. The candle inside lights the carved holes from within; light leaks// through the lid seam and spills onto the floor in front (detached, dithered pool).// Variants (the inner light is the fl0..3 family, recoloured by ramp swap — no new colours)://   fire    warm candle, flicker (12f loop)//   witch   violet witchfire (12f loop)//   soul    blue soulfire (12f loop)//   unlit   dark carved face, a thin wisp of smoke from the seam (12f loop)//   ignite  once, 8f: a spark catches, the light flares and settles; the last frame equals fire frame 0

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

jack_o_lantern.js · 212–224
submit(JackOLantern, {  size: [W, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["fire"],  variants: [    { name: "fire", args: ["fire"] },    { name: "witch", args: ["fire"], repalette: swap("fl", "witch") },    { name: "soul", args: ["fire"], repalette: swap("fl", "soul") },    { name: "unlit", args: ["unlit"] },    { name: "ignite", args: ["ignite"], animate: { fps: 10, duration: N_IGN / 10, loop: false } },  ],});
Show the whole program (224 lines)
jack_o_lantern.js · 224 lines
// Jack-o'-lantern — 32x32 floor prop, top-down 3/4, key light top-left.// A ribbed pumpkin with a cut lid (seam around the stem), a curled vine and leaf, and a carved face: triangle eyes,// a nose and a jagged grin with two teeth. The candle inside lights the carved holes from within; light leaks// through the lid seam and spills onto the floor in front (detached, dithered pool).// Variants (the inner light is the fl0..3 family, recoloured by ramp swap — no new colours)://   fire    warm candle, flicker (12f loop)//   witch   violet witchfire (12f loop)//   soul    blue soulfire (12f loop)//   unlit   dark carved face, a thin wisp of smoke from the seam (12f loop)//   ignite  once, 8f: a spark catches, the light flares and settles; the last frame equals fire frame 0const W = 32, CX = 16, GROUND = 29;const P = { cx: 16, cy: 20, rx: 11, ry: 8.6 };const SKIN = ["r0", "r1", "r2", "r2"];const N_LOOP = 12, N_IGN = 8;// ---- pumpkin body -------------------------------------------------------------------------------------------function pumpkin(g) {  const { cx, cy, rx, ry } = P;  g.part("pumpkin", () => g.field({ x: [cx - rx, cx + rx], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => {    const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;    // squash the silhouette a little at the top (shoulders) so it reads as a pumpkin, not an egg    const sq = ny < 0 ? ny * 1.06 : ny;    const r2 = nx * nx + sq * sq;    if (r2 > 1) return null;    const nz = Math.sqrt(Math.max(0, 1 - r2));    const phi = Math.asin(Math.max(-1, Math.min(1, nx / Math.sqrt(Math.max(0.05, 1 - sq * sq)))));    const rib = Math.cos(phi * 5);                       // 1 at a lobe's crown, -1 in a groove    let d = -0.55 * nx - 0.45 * ny + 0.7 * nz + 0.22 * rib;        const k = Math.max(0, Math.min(0.999, (d + 0.05) / 1.2));    let key = SKIN[Math.floor(k * 4)];    if (rib < -0.6 && Math.abs(ny) < 0.95) key = key === "r3" ? "r2" : "r1";   // groove: continuous darker line    // lit stripe on the crown of the upper-left lobes (sharp key-light catch)    if (rib > 0.55 && (nx < -0.5 || ny < -0.55) && ny < 0.4 && d > 0.62) key = d > 0.85 && ny < -0.2 ? "g2" : "r3";    // dent around the stem on the top face    const tx = (x + 0.5 - cx) / 4.5, ty = (y + 0.5 - (cy - ry + 1.6)) / 1.6;    if (tx * tx + ty * ty < 1) key = tx < -0.2 ? "r1" : "r0";    return key;  }));}// lid seam: a jagged cut around the stem; lit variants leak light through itfunction seam(g, t, light) {  const pts = [];  for (let i = 0; i <= 18; i++) {    const a = Math.PI * (i / 18);                          // the near half of the cut (front arc)    const x = Math.round(CX + Math.cos(a) * 5.4);    const y = Math.round(P.cy - P.ry + 2.2 + Math.sin(a) * 1.6 + ((i & 1) ? 0.6 : 0));    pts.push([x, y]);  }  g.part("lidSeam", () => {    let prev = null;    for (const [x, y] of pts) {      if (prev && prev[0] === x && prev[1] === y) continue;      const leak = light > 0 && ((x * 3 + y) % 4 === Math.floor(t * N_LOOP * 0.5) % 4 || light > 1.2);      g.pixel(x, y, leak ? (light > 0.8 ? "fl3" : "fl2") : "r0");      prev = [x, y];    }  });}function stemVine(g) {  g.part("stem", () => {    // a short twisted stem leaning right, cut flat on top    g.fillPoly([[14, 13], [18, 13], [19, 9], [18, 7], [16, 7], [15, 9]], "w1");    g.line([15, 12], [16, 8], "w3");    g.line([16, 12], [17, 8], "w2");    g.pixel(18, 11, "w0"); g.pixel(18, 12, "w0");    g.rect({ x: [16, 18], y: [6, 6] }, "w2", { fill: "w2" });    g.pixel(16, 6, "w4"); g.pixel(17, 6, "w3");  });  g.part("vine", () => {    // tendril curling off the stem to the left, ending in a small spiral    g.line([14, 11], [12, 10], "e1");    g.pixel(11, 9, "e1"); g.pixel(10, 8, "e1"); g.pixel(9, 8, "e1"); g.pixel(8, 9, "e1"); g.pixel(9, 10, "e2");    g.pixel(10, 10, "e1");  });  g.part("leaf", () => {    // broad leaf lying on the pumpkin's right shoulder, lit edge top-left, vein down the middle    g.fillPoly([[19, 10], [23, 8], [26, 9], [25, 12], [21, 13]], "e1");    g.line([20, 10], [23, 8], "e3");    g.line([23, 9], [25, 9], "e2");    g.line([20, 11], [24, 10], "e0");    g.pixel(24, 12, "e0"); g.pixel(22, 12, "e0");  });}// ---- carved face --------------------------------------------------------------------------------------------// carved holes, hand-placed (row -> [x0,x1] spans); hole(x,y) returns "eye" | "nose" | "mouth" | nullconst HOLES = {  eye: { 15: [[11, 11], [21, 21]], 16: [[10, 12], [20, 22]], 17: [[9, 13], [19, 23]], 18: [[9, 13], [20, 23]] },  nose: { 19: [[16, 16]], 20: [[15, 17]] },  mouth: {    21: [[8, 8], [24, 24]],    22: [[8, 11], [21, 24]],    23: [[9, 12], [14, 18], [20, 23]],    24: [[10, 15], [17, 22]],    25: [[12, 15], [17, 20]],  },};function hole(x, y) {  for (const k in HOLES) {    const row = HOLES[k][y];    if (row && row.some(([a, b]) => x >= a && x <= b)) return k;  }  return null;}// light: 0 = unlit, ~1 = steady candle, >1 = flare. fl keys follow the variant's flame ramp.function face(g, t, light) {  const LC = { x: 16, y: 21 };  const fl = light > 0 ? 0.5 + 0.28 * Math.sin(TAU * 2 * t) + 0.22 * Math.sin(TAU * (3 * t + 0.35)) : 0;  g.part("face", () => g.field({ x: [7, 25], y: [14, 26] }, (x, y) => {    const h = hole(x, y);    if (!h) return null;    const left = hole(x - 1, y) === h, right = hole(x + 1, y) === h, below = hole(x, y + 1) === h;    if (light <= 0) {      // unlit: the cut wall shows pale flesh along the lower/right inside edge, the cavity is dark      if (!right || !below) return left || !below ? "g0" : "ink";      return "ink";    }    // lit: the cut wall on the hole's left side is in shade; the cavity glows, brightest near the candle    const d = Math.hypot(x + 0.5 - LC.x, (y + 0.5 - LC.y) * 1.3) / Math.max(0.25, light);    if (light < 0.9) {                        // kindling: a small core, the rest of the cavity still dark      const dd = Math.hypot(x + 0.5 - LC.x, (y + 0.5 - LC.y) * 1.3);      return dd < 2 + light * 3 ? "fl3" : dd < 3 + light * 6 ? "fl1" : "ink";    }    const edge = !right || !below || !hole(x, y - 1);    if (!left && light < 1.3) return d < 5 ? "fl1" : "fl0";    if (d < 6.5 + fl * 3) return edge && d > 3 ? "fl2" : "fl3";    return edge ? "fl1" : "fl2";  }));  // rim of each hole: a 1px darker cut edge on the skin around the hole's top (the carved bevel catches no light)  g.part("faceBevel", () => g.field({ x: [7, 25], y: [14, 26] }, (x, y) => {    if (hole(x, y)) return null;    return hole(x, y + 1) && !hole(x - 1, y) && !hole(x + 1, y) ? "r0" : null;  }));}// ---- light on the floor, smoke ------------------------------------------------------------------------------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 floorLight(g, t, light) {  if (light <= 0) return;  const br = Math.min(1.3, light) * (1 + 0.08 * Math.sin(TAU * 2 * t));  g.detached("floorLight", () => g.field({ x: [0, W - 1], y: [25, 31] }, (x, y) => {    const nx = (x + 0.5 - CX) / (12 * br), ny = (y + 0.5 - (GROUND + 1.2)) / (2.4 * br);    const r = Math.sqrt(nx * nx + ny * ny);    if (r > 1) return null;    // the carved face throws three bright lobes forward (eyes + grin): a light fan, broken by the pumpkin's walls    const fan = Math.abs(Math.sin((nx * 3.1) + 0.3));    const dens = (r < 0.45 ? 0.5 : r < 0.75 ? 0.3 : 0.12) * (0.5 + 0.5 * fan);    if (!dith(x, y, dens)) return null;    return r < 0.5 ? "fl2" : r < 0.8 ? "fl1" : "fl0";  }));}function glowSparks(g, t, light) {  if (light <= 0) return;  g.detached("heat", () => {    // two sparks escape from the seam and drift up    for (let k = 0; k < 2; k++) {      const p = (t * (k ? 1 : 2) + k * 0.5) % 1;      if (p > 0.6) continue;      const x = CX + (k ? 4 : -3) + Math.round(Math.sin(TAU * (p + k * 0.3)) * 1.5);      const y = 12 - Math.round(p * 11);      g.pixel(x, y, p < 0.3 ? "fl3" : "fl1");    }  });}function smoke(g, t) {  g.detached("smoke", () => {    for (let i = 0; i < 3; i++) {      const p = (t + i / 3) % 1;      const x = Math.round(CX + 3 + Math.sin(TAU * (p * 1.2 + i * 0.15)) * 2 + p * 3);      const y = Math.round(11 - p * 11);      if (y < 0) continue;      if (p < 0.4) { g.pixel(x, y, "n3"); g.pixel(x, y + 1, "n2"); }      else if (p < 0.75) g.pixel(x, y, "n2");      else if (chk(x, Math.floor(t * N_LOOP))) g.pixel(x, y, "n1");    }  });}function draw(g, t, light, mode) {  contactShadow(g, CX, GROUND, 11, 2);  floorLight(g, t, light);  g.outlined("ink", () => { pumpkin(g); stemVine(g); });  seam(g, t, light);  face(g, t, light);  if (mode === "unlit") smoke(g, t);  else glowSparks(g, t, light);}const JackOLantern = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  if (mode === "unlit") return draw(g, t, 0, mode);  if (mode === "ignite") {    const f = Math.min(N_IGN - 1, Math.round(t * N_IGN));    if (f === N_IGN - 1) return draw(g, 0, 1, "fire");     // == fire frame 0    const light = [0, 0, 0.35, 0.7, 1.45, 1.25, 1.1][f];    draw(g, f * 0.13, light, "ignite");    if (f <= 1) g.detached("spark", () => {                  // the match-spark inside the mouth      g.pixel(15, 23, "fl3"); if (f === 1) { g.pixel(16, 22, "fl2"); g.pixel(14, 22, "fl2"); }    });    return;  }  draw(g, t, 1, mode);});submit(JackOLantern, {  size: [W, 32], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["fire"],  variants: [    { name: "fire", args: ["fire"] },    { name: "witch", args: ["fire"], repalette: swap("fl", "witch") },    { name: "soul", args: ["fire"], repalette: swap("fl", "soul") },    { name: "unlit", args: ["unlit"] },    { name: "ignite", args: ["ignite"], animate: { fps: 10, duration: N_IGN / 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.