Black cat
A 32×32 floor prop: a witch’s black cat sitting on a small embroidered cushion of red velvet with gold braid and corner tassels. The fur is near-black with a violet rim light on the top-left edges; the eyes are yellow-green with slit pupils, and a red collar carries a brass bell. Four variants: “idle” (16 frames, the tail swishes and the cat blinks once), “groom” (12 frames, it licks a raised paw with its eyes shut), “hiss” (8 frames, once: it stands up stiff-legged with its fur bristling) and “sleep” (20 frames, curled up nose-to-tail, breathing).
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.
// Black cat — 32x32 floor prop, top-down 3/4, key light top-left.// A witch's black cat sitting on a small embroidered cushion (red velvet, gold braid, corner tassels). The fur is// near-black (n0 / v0) with a violet rim light on the top-left edges, yellow-green eyes with slit pupils, and a// red collar with a brass bell.// Variants:// idle 16f loop: the tail swishes, an ear twitches, one blink// groom 12f loop: head tilted down, licks a raised paw (tongue flicks), eyes shut// hiss once, 8f: stands up on stiff legs, fur bristles, ears flatten, eyes flare, mouth opens on the fangs;// settles back; the last frame equals idle frame 0// sleep 20f loop: curled up nose-to-tail, breathing, a small 'z' drifts upThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(BlackCat, { size: [W, 32], 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: "groom", args: ["groom"], animate: { fps: 10, duration: N_GROOM / 10, loop: true } }, { name: "hiss", args: ["hiss"], animate: { fps: 10, duration: N_HISS / 10, loop: false } }, { name: "sleep", args: ["sleep"], animate: { fps: 8, duration: N_SLEEP / 8, loop: true } }, ],});Show the whole program (267 lines)
// Black cat — 32x32 floor prop, top-down 3/4, key light top-left.// A witch's black cat sitting on a small embroidered cushion (red velvet, gold braid, corner tassels). The fur is// near-black (n0 / v0) with a violet rim light on the top-left edges, yellow-green eyes with slit pupils, and a// red collar with a brass bell.// Variants:// idle 16f loop: the tail swishes, an ear twitches, one blink// groom 12f loop: head tilted down, licks a raised paw (tongue flicks), eyes shut// hiss once, 8f: stands up on stiff legs, fur bristles, ears flatten, eyes flare, mouth opens on the fangs;// settles back; the last frame equals idle frame 0// sleep 20f loop: curled up nose-to-tail, breathing, a small 'z' drifts upconst W = 32, CX = 16, GROUND = 30;const N_IDLE = 16, N_GROOM = 12, N_HISS = 8, N_SLEEP = 20;const FUR = ["n0", "n0", "n0", "v0"];// ---- cushion ------------------------------------------------------------------------------------------------const CU = { cx: 16, cy: 24, rx: 12, ry: 3.6 };function cushion(g) { const { cx, cy, rx, ry } = CU; g.part("cushion", () => { // the plump front side, then the lit top face g.field({ x: [cx - rx, cx + rx], y: [cy - 1, cy + ry + 3] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; if (Math.abs(nx) > 1) return null; const yb = cy + ry * Math.sqrt(1 - nx * nx) + 2.6 - 0.8 * nx * nx; const yt = cy; if (y + 0.5 < yt || y + 0.5 > yb) return null; return nx < -0.55 ? "r1" : nx < 0.6 ? "r0" : "r0"; }); g.field({ x: [cx - rx, cx + rx], y: [cy - ry - 1, cy + ry + 1] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; const r = nx * nx + ny * ny; if (r > 1) return null; if (r > 0.7 && ny > 0.3) return nx < -0.3 ? "g2" : "g1"; // gold braid along the front edge const a = -0.6 * nx - 0.5 * ny; return a > 0.45 ? "r2" : "r1"; }); // embroidered gold diamonds on the top face (mostly hidden under the cat; they show at the sides) for (const [x, y] of [[7, 24], [25, 24]]) { g.pixel(x, y - 1, "g1"); g.pixel(x - 1, y, "g0"); g.pixel(x + 1, y, "g0"); g.pixel(x, y + 1, "g0"); } // a seam along the side's bottom g.line([cx - rx + 3, cy + ry + 2], [cx + rx - 3, cy + ry + 2], "w0"); }); g.part("tassels", () => { for (const [x, s] of [[cx - rx - 1, -1], [cx + rx + 1, 1]]) { g.pixel(x, cy + 1, "g2"); g.pixel(x, cy + 2, s < 0 ? "g2" : "g1"); g.pixel(x + s, cy + 3, "g1"); g.pixel(x, cy + 3, s < 0 ? "g3" : "g1"); g.pixel(x, cy + 4, "g0"); } });}// ---- fur shading -------------------------------------------------------------------------------------------function furBlob(g, name, cx, cy, rx, ry, bias, inkEdge) { g.part(name, () => g.field({ x: [Math.floor(cx - rx), Math.ceil(cx + rx)], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => { const i = litIndex(x, y, cx, cy, rx, ry, 4, bias || 0); if (i < 0) return null; // separation from a body part behind it: ink on the right/bottom boundary if (inkEdge && (litIndex(x + 1, y, cx, cy, rx, ry, 4, 0) < 0 || litIndex(x, y + 1, cx, cy, rx, ry, 4, 0) < 0)) return "ink"; const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; const r = Math.sqrt(nx * nx + ny * ny); // violet rim light: the top-left edge if (r > 0.72 && -0.7 * nx - 0.7 * ny > 0.55) return "v1"; return FUR[i]; }));}// pose: { lift, puff, hx, hy (head offset), earL, earR (0 up .. 1 flat), eyes, mouth, tail:[...path], tailW, paw }function cat(g, p) { const L = p.lift, P = p.puff; const hx = 15 + p.hx, hy = 10 - L + p.hy; g.outlined("ink", () => { // tail first (behind the haunch on the right) g.part("tail", () => g.sweep(p.tail, { w0: p.tailW, w1: p.tailW * 0.75 }, (u, v) => (v < -0.2 && u < 0.85 ? "v0" : u > 0.86 ? "v0" : "n0"))); if (P > 0.6) g.part("tailFluff", () => { for (let i = 1; i < p.tail.length; i++) { const [x, y] = p.tail[i]; g.pixel(x + (i & 1 ? 2 : -2), y, "n0"); } }); furBlob(g, "haunches", 16, 21 - L * 0.6, 7 + P, 4.6 + P * 0.4, 0.05); furBlob(g, "chest", 15, 17 - L, 4.6 + P * 0.8, 4.6, 0.1); if (P > 0.6) g.part("hackles", () => { // bristling fur along the back/shoulders: a row of spikes the outline turns into a jagged silhouette for (let k = 0; k < 6; k++) { const a = Math.PI * (1.1 + k * 0.16); const x = Math.round(16 + Math.cos(a) * (8 + P)), y = Math.round(21 - L * 0.6 + Math.sin(a) * (5.4 + P * 0.4)); g.pixel(x, y, k < 3 ? "v0" : "n0"); } }); // front legs: stiff columns down to the cushion, the left one lit g.part("legs", () => { const top = 18 - L; for (const [x0, lit] of [[12, true], [17, false]]) { if (!lit && p.paw) continue; // raised for grooming (drawn separately) for (let y = top; y <= 25; y++) { g.pixel(x0, y, lit ? "v0" : "n0"); g.pixel(x0 + 1, y, lit ? "n0" : "n0"); } g.pixel(x0, 25, lit ? "n1" : "v0"); g.pixel(x0 + 1, 25, "n0"); // paws: lit toes g.pixel(x0 - 1, 25, "n0"); } g.line([14, 20 - L], [14, 25], "ink"); // gap between the legs }); g.part("head", () => { furBlob(g, "skull", hx, hy, 5.4, 4.3, 0.12); // ears: lean back as earX -> 1 const ear = (s, f) => { const base0 = [hx + s * 5, hy - 0.5 + f * 1.5], base1 = [hx + s * 1.5, hy - 3.2 + f]; const tip = [hx + s * (4 + f * 3), hy - 6.5 + f * 4.5]; g.fillPoly([base0, tip, base1].map(([a, b]) => [Math.round(a), Math.round(b)]), s < 0 ? "v0" : "n0"); const ix = Math.round(hx + s * (3.4 + f * 1.6)), iy = Math.round(hy - 3.6 + f * 2.4); g.pixel(ix, iy, "r0"); if (f < 0.5) g.pixel(ix, iy + 1, "r0"); if (s < 0) g.pixel(Math.round(tip[0]), Math.round(tip[1]), "v1"); }; ear(-1, p.earL); ear(1, p.earR); }); }); face(g, hx, hy, p); g.part("collar", () => { const cy = hy + 4; for (let x = hx - 3; x <= hx + 3; x++) g.pixel(x, cy + (Math.abs(x - hx) > 2 ? -1 : 0), x < hx ? "r2" : "r1"); if (!p.paw) { g.pixel(hx, cy + 1, "g2"); g.pixel(hx - 1, cy + 1, "g3"); g.pixel(hx, cy + 2, "g0"); } }); g.part("chestTuft", () => { g.pixel(hx, hy + 7 - (p.paw ? 1 : 0), "n1"); g.pixel(hx - 1, hy + 8, "v0"); }); g.outlined("ink", () => { if (p.paw) g.part("pawRaised", () => { // the right forepaw lifted to the mouth (drawn in front of the chest) const [px, py] = p.paw; g.rect({ x: [px, px + 1], y: [py, 21] }, "v0", { fill: "v0" }); g.line([px + 2, py], [px + 2, 21], "ink"); // the foreleg's shaded edge against the chest g.line([px - 1, py + 1], [px - 1, 21], "ink"); g.rect({ x: [px - 1, px + 1], y: [py - 1, py] }, "n0", { fill: "n0" }); g.pixel(px - 1, py - 1, "v1"); g.pixel(px, py - 1, "n1"); g.pixel(px + 1, py - 1, "v0"); g.line([px - 1, py + 1 - 0], [px - 1, py + 1], "ink"); g.pixel(px + 2, py - 1, "ink"); g.pixel(px + 2, py, "ink"); }); });}function face(g, hx, hy, p) { g.part("eyes", () => { for (const s of [-1, 1]) { const x0 = hx + (s < 0 ? -4 : 2), y = hy; if (p.eyes === "closed") { g.line([x0, y + 1], [x0 + 2, y + 1], "n2"); g.pixel(x0 + 1, y, "n0"); continue; } if (p.eyes === "half") { g.line([x0, y], [x0 + 2, y], "n0"); g.line([x0, y + 1], [x0 + 2, y + 1], "e2"); g.pixel(x0 + 1, y + 1, "ink"); continue; } if (p.eyes === "wide") { g.rect({ x: [x0, x0 + 2], y: [y - 1, y + 1] }, "e3", { fill: "e3" }); g.pixel(x0, y - 1, "g3"); g.pixel(x0 + 1, y - 1, "g3"); g.pixel(x0 + 1, y, "ink"); // a pinpoint pupil continue; } g.pixel(x0, y, s < 0 ? "g3" : "e3"); g.pixel(x0 + 1, y, "ink"); g.pixel(x0 + 2, y, "e3"); g.pixel(x0, y + 1, "e2"); g.pixel(x0 + 1, y + 1, "ink"); g.pixel(x0 + 2, y + 1, "e1"); } }); g.part("muzzle", () => { g.pixel(hx, hy + 2, "r0"); // nose if (p.mouth === "hiss") { g.rect({ x: [hx - 1, hx + 1], y: [hy + 3, hy + 4] }, "r1", { fill: "r1" }); g.pixel(hx - 1, hy + 3, "n5"); g.pixel(hx + 1, hy + 3, "n5"); g.pixel(hx, hy + 4, "r0"); } else if (p.mouth === "lick") { g.pixel(hx - 1, hy + 3, "r2"); g.pixel(hx - 1, hy + 4, "r3"); } else { g.pixel(hx - 1, hy + 3, "n1"); g.pixel(hx + 1, hy + 3, "n1"); } }); g.detached("whiskers", () => { g.line([hx - 7, hy + 2], [hx - 6, hy + 2], "n3"); g.pixel(hx - 7, hy + 3, "n2"); g.line([hx + 6, hy + 2], [hx + 7, hy + 2], "n2"); });}// ---- poses --------------------------------------------------------------------------------------------------function idlePose(t) { const sw = Math.sin(TAU * t), sw2 = Math.sin(TAU * (t + 0.12)); const f = Math.round(t * N_IDLE) % N_IDLE; return { lift: 0, puff: 0, hx: 0, hy: 0, earL: 0, earR: f === 5 || f === 6 ? 0.5 : 0, // the right ear flicks eyes: f === 10 || f === 12 ? "half" : f === 11 ? "closed" : "open", mouth: "", tailW: 1.3, tail: [[21, 24], [24, 25], [26.5, 23.5], [27.5 + sw * 0.6, 20.5], [27 + sw2 * 2, 17.5]], };}function groomPose(t) { const f = Math.round(t * N_GROOM) % N_GROOM; const bob = Math.round(0.5 + 0.5 * Math.sin(TAU * 2 * t)); // two licks per loop const p = idlePose(0); p.hx = -1; p.hy = 1 + bob; p.eyes = "closed"; p.mouth = bob ? "" : "lick"; p.paw = [16, 15 + (1 - bob)]; p.tail = [[21, 24], [24, 25], [26.5, 24], [28, 22.5 + Math.sin(TAU * t)]]; return p;}const HISS = [ // lift, puff, ears, eyes, mouth [1, 0.4, 0.4, "wide", ""], [2, 1.0, 1.0, "wide", "hiss"], [3, 1.5, 1.0, "wide", "hiss"], [3, 1.6, 1.0, "wide", "hiss"], [2, 1.0, 0.8, "wide", "hiss"], [1, 0.5, 0.4, "open", ""], [0, 0.2, 0.2, "open", ""],];function hissPose(t) { const f = Math.min(N_HISS - 1, Math.round(t * N_HISS)); if (f === N_HISS - 1) return idlePose(0); const [lift, puff, ears, eyes, mouth] = HISS[f]; const p = idlePose(0); Object.assign(p, { lift, puff, earL: ears, earR: ears, eyes, mouth }); // tail goes up like a bottle brush const up = Math.min(1, puff / 1.2); p.tail = [[21, 24], [24 - up, 24 - up * 3], [26 - up, 22 - up * 5], [27 - up * 0.5, 19 - up * 7], [27 + up * 0.5, 17.5 - up * 8]]; p.tailW = 1.3 + puff * 0.7; return p;}// ---- sleep: curled up nose-to-tail --------------------------------------------------------------------------function sleep(g, t) { const br = 0.5 - 0.5 * Math.cos(TAU * t); // 0 exhaled .. 1 inhaled const ry = 4.3 + br * 0.9, cy = 26 - ry; g.outlined("ink", () => { furBlob(g, "body", 17.5, cy, 8.5, ry, 0.05); g.part("tail", () => g.sweep([[25, 23.5], [23, 25.6], [17, 26.2], [11, 25.6]], { w0: 1.4, w1: 1.1 }, (u, v) => (v < 0 ? "v0" : "n0"))); }); g.outlined("ink", () => { furBlob(g, "head", 10, 22.2, 4.4, 3.5, 0.15, true); g.part("ears", () => { g.fillPoly([[7, 20], [7, 17], [9, 19]], "v0"); g.pixel(7, 17, "v1"); g.fillPoly([[11, 19], [13, 17], [13, 20]], "n0"); g.pixel(12, 19, "r0"); }); }); g.part("sleepFace", () => { g.line([7, 23], [8, 23], "n2"); g.line([11, 23], [12, 23], "n2"); // shut eyes g.pixel(10, 24, "r0"); g.pixel(9, 25, "n1"); g.pixel(10, 25, "v0"); g.pixel(12, 26, "n1"); g.pixel(13, 26, "v0"); // tucked forepaws }); g.part("collar", () => { g.pixel(13, 25, "r2"); g.pixel(14, 25, "r1"); g.pixel(14, 26, "g2"); }); g.part("spine", () => { // a faint ridge of rim light along the back, rising and falling with the breath const yy = Math.round(cy - ry + 1); g.line([14, yy], [19, yy], "v1"); g.pixel(20, yy, "v0"); }); g.detached("zzz", () => { const p = t; if (p > 0.85) return; const x = Math.round(6 + p * 4 + Math.sin(TAU * p) * 1), y = Math.round(14 - p * 12); const k = p < 0.55 ? "n4" : "n3"; for (const [dx, dy] of [[0, 0], [1, 0], [2, 0], [3, 0], [2, 1], [1, 2], [0, 3], [1, 3], [2, 3], [3, 3]]) g.pixel(x + dx, y + dy, k); });}const BlackCat = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; contactShadow(g, CX, GROUND, 13, 1); cushion(g); if (mode === "sleep") return sleep(g, t); cat(g, mode === "groom" ? groomPose(t) : mode === "hiss" ? hissPose(t) : idlePose(t));});submit(BlackCat, { size: [W, 32], 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: "groom", args: ["groom"], animate: { fps: 10, duration: N_GROOM / 10, loop: true } }, { name: "hiss", args: ["hiss"], animate: { fps: 10, duration: N_HISS / 10, loop: false } }, { name: "sleep", args: ["sleep"], animate: { fps: 8, duration: N_SLEEP / 8, loop: true } }, ],});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.