Skull and candles
A 32×32 floor cluster: a bone skull with a melted candle on its crown, and three candles around it, tall, medium and a stub. In the 12-frame loop each flame flickers on its own, and the tall one leans in a draft mid-loop. The flames come in fire, soul and witch by swapping one ramp, with no new colours.
6×
Get the animated sprites: Arcane Curiosities Vol.1 on itch.io12 animated props · 52 animations
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Skull & candles — 32x32 floor cluster, top-down 3/4, key light top-left.// A bone skull with a melted candle on its crown, plus three candles (tall / medium / stub) around it.// Loop: 12 frames @ 10 fps. Each flame flickers on its own seed; the tall one leans in a draft mid-loop.// Variants: flame colour fire / soul / witch (fl ramp swap — no new colours).The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(SkullCandles, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 16, y: 29 }, variants: [ { name: "idle-fire" }, { name: "idle-soul", repalette: swap("fl", "soul") }, { name: "idle-witch", repalette: swap("fl", "witch") }, ],});Show the whole program (171 lines)
// Skull & candles — 32x32 floor cluster, top-down 3/4, key light top-left.// A bone skull with a melted candle on its crown, plus three candles (tall / medium / stub) around it.// Loop: 12 frames @ 10 fps. Each flame flickers on its own seed; the tall one leans in a draft mid-loop.// Variants: flame colour fire / soul / witch (fl ramp swap — no new colours).const GROUND = 29;const SK = 16.5; // skull centre line (symmetric about column 16)const WAX = { 3: ["n5", "w4", "w3"], 4: ["n5", "n5", "w4", "w3"], 5: ["n5", "n5", "w4", "w4", "w3"] };const DIM = { n5: "w4", w4: "w3", w3: "w2" }; // wax is translucent: shallow low steps, one step dimmer near the floor// x0 = left column, w = width, top = top-face row, base = bottom row, h = flame height// drips: [column offset (-1 = bulge left, w = bulge right, else down the face), length]const CANDLES = { A: { x0: 7, w: 4, top: 9, base: 21, drips: [[-1, 4], [1, 7], [4, 2]], seed: 0.0, h: 6, gust: true }, B: { x0: 24, w: 4, top: 14, base: 22, drips: [[0, 3], [4, 4]], seed: 0.37, h: 5 }, C: { x0: 2, w: 3, top: 23, base: 28, drips: [[0, 2], [3, 2]], seed: 0.71, h: 5 }, S: { x0: 14, w: 4, top: 12, base: 16, drips: [[-1, 2], [2, 1], [4, 3]], seed: 0.52, h: 6 },};// ---- flames -----------------------------------------------------------------------------------// periodic flicker drivers (integer frequencies so t=1 closes the loop)function flick(t, s) { const dh = Math.round(0.75 * Math.sin(TAU * (2 * t + s)) + 0.45 * Math.sin(TAU * (3 * t + 2.3 * s))); const lean = 0.7 * Math.sin(TAU * (t + s)) + 0.45 * Math.sin(TAU * (3 * t + 1.7 * s)); return { dh, lean };}// a draft: smooth bump over t in [0.35, 0.8], zero elsewhere (so the loop still closes)function gust(t) { const u = (t - 0.35) / 0.45; return u <= 0 || u >= 1 ? 0 : Math.pow(Math.sin(Math.PI * u), 2);}// Small candle flame, built the pixel-art way: rows of odd width stacked on a staircase axis that// bends with lean. Edge pixels are the dark shell (fl0, warming to fl1 where the flame is fattest);// the axis runs body -> core -> bright -> body -> shell tip. fx = wick column, fy = lowest flame row.function candleFlame(g, fx, fy, h, lean) { for (let i = 0; i < h; i++) { const k = (i + 0.5) / h, y = fy - i; const ax = fx + Math.round(lean * k * k); const r = k > 0.12 && k < 0.62 ? 1 : 0; if (r) { const edge = k > 0.18 && k < 0.5 ? "fl1" : "fl0"; g.pixel(ax - 1, y, edge); g.pixel(ax + 1, y, edge); } g.pixel(ax, y, k < 0.12 ? "fl1" : k < 0.5 ? "fl3" : k < 0.68 ? "fl2" : k < 0.86 ? "fl1" : "fl0"); }}function flames(g, t) { for (const [name, c] of Object.entries(CANDLES)) { const f = flick(t, c.seed); const lean = f.lean + (c.gust ? 2.6 * gust(t) : 0); const fx = c.x0 + (c.w === 3 ? 1 : 2), h = c.h + f.dh; g.part("flame" + name, () => { g.pixel(fx, c.top - 1, "ink"); // wick candleFlame(g, fx, c.top - 2, h, lean); }); // as a tall flame shrinks back, its tip tears off and floats up for a frame or two const ph = TAU * (2 * t + c.seed); const ly = c.top - 2 - h - 1; if (c.h >= 6 && ly >= 0 && Math.sin(ph) > 0.2 && Math.cos(ph) < -0.2) { g.detached("lick" + name, () => g.pixel(fx + Math.round(lean), ly, "fl0")); } }}// ---- wax ----------------------------------------------------------------------------------------// wax dims a step once it is far below its flame; the lit column dims later than the shaded one,// so the falloff boundary curves round the cylinderfunction waxAt(c, key, y, i) { const u = i === undefined ? 0.5 : i / Math.max(1, c.w - 1); return y - c.top > 8 - 3 * u ? DIM[key] : key;}function candle(g, name, c) { g.part(name, () => { const cols = WAX[c.w]; for (let i = 0; i < c.w; i++) for (let y = c.top + 1; y <= c.base; y++) g.pixel(c.x0 + i, y, waxAt(c, cols[i], y, i)); // top face: lit rim, the far-right corner slumped where the wax overflowed g.line([c.x0, c.top], [c.x0 + c.w - 2, c.top], "n5"); g.pixel(c.x0 + c.w - 1, c.top + 1, "w4"); g.pixel(c.x0 + 1, c.top + 1, "fl3"); // flame light through the wax for (const [dx, len] of c.drips) { const x = c.x0 + dx, y0 = c.top + 1, y1 = c.top + len; for (let y = y0; y <= y1; y++) { if (dx < 0) g.pixel(x, y, waxAt(c, "n5", y, 0)); else if (dx >= c.w) g.pixel(x, y, y === y0 ? "w4" : waxAt(c, "w3", y, c.w - 1)); else g.pixel(x, y, waxAt(c, dx < c.w / 2 ? "n5" : "w4", y, dx)); } if (dx >= 0 && dx < c.w) g.pixel(x, y1 + 1, waxAt(c, "w3", y1 + 1, dx)); // the drip's bead casts a shadow } });}function puddle(g, name, cx, cy, rx, ry) { g.part(name, () => g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; if (nx * nx + ny * ny > 1) return null; if (ny > 0.35) return "w3"; return nx < -0.2 && ny < 0 ? "n5" : "w4"; }));}// ---- skull --------------------------------------------------------------------------------------function skull(g) { g.part("skull", () => { // cranium: a lit sphere (bone ramp n2..n5) g.field({ x: [9, 23], y: [15, 26] }, (x, y) => { const i = litIndex(x, y, SK, 21, 6.5, 5.6, 4, -0.05); return i < 0 ? null : ["n2", "n3", "n4", "n5"][i]; }); // face: cheekbones, maxilla and jaw narrowing downwards; front-facing, so a step darker than the dome g.field({ x: [10, 23], y: [23, 28] }, (x, y) => { const dx = x + 0.5 - SK; const hw = y <= 24 ? 6.2 : y === 25 ? 5.2 : y <= 27 ? 4.4 : 3.2; if (Math.abs(dx) > hw) return null; const side = dx / hw; return side > 0.5 || y >= 28 ? "n2" : side < -0.45 ? "n4" : "n3"; }); // eye sockets: deep ink hollows under the brow ridge for (const [a, b, s] of [[12, 14, -1], [18, 20, 1]]) { g.line([a, 20], [b, 20], s < 0 ? "n3" : "n2"); g.line([a, 21], [b, 21], "ink"); g.line([a, 22], [b, 22], "ink"); g.line([s < 0 ? a + 1 : a, 23], [s < 0 ? b : b - 1, 23], "ink"); } g.pixel(11, 24, "n5"); g.pixel(21, 24, "n3"); // cheekbones g.pixel(16, 24, "ink"); g.pixel(15, 25, "n1"); g.pixel(16, 25, "ink"); g.pixel(17, 25, "n1"); // nose for (let x = 13; x <= 19; x++) g.pixel(x, 26, (x & 1) ? "n5" : "n2"); // upper teeth g.line([13, 27], [19, 27], "ink"); // bite for (let x = 14; x <= 18; x++) g.pixel(x, 28, (x & 1) ? "n4" : "n1"); // lower teeth g.pixel(22, 19, "fl1"); g.pixel(22, 20, "fl1"); // rim light from candle B // melt pooled on the crown; its lower lip casts a shadow on the bone g.field({ x: [11, 22], y: [15, 19] }, (x, y) => { const nx = (x + 0.5 - 16) / 3.7, ny = (y + 0.5 - 16.6) / 1.3; if (nx * nx + ny * ny <= 1) return ny > 0.4 ? "w3" : nx < -0.25 ? "n5" : "w4"; const sy = (y + 0.5 - 16.6) / 1.9; return nx * nx + sy * sy <= 1 && sy > 0 ? "n2" : null; }); });}function crownDrips(g) { g.part("crown drips", () => { g.line([12, 18], [12, 19], "w4"); g.pixel(12, 20, "w3"); g.line([20, 18], [20, 20], "w4"); g.pixel(20, 21, "w3"); });}// ---- sprite -------------------------------------------------------------------------------------const SkullCandles = sprite(pal, (g, t) => { const { A, B, C, S } = CANDLES; contactShadow(g, 15, GROUND, 14, 2); g.outlined("ink", () => { puddle(g, "puddleA", 9, 21.5, 4, 1.5); candle(g, "candleA", A); }); g.outlined("ink", () => { puddle(g, "puddleB", 26, 22, 3.6, 1.5); candle(g, "candleB", B); }); g.outlined("ink", () => { puddle(g, "puddle", SK, 28.3, 7.4, 1.3); skull(g); }); g.outlined("ink", () => candle(g, "candleS", S)); crownDrips(g); g.outlined("ink", () => { puddle(g, "puddleC", 3.5, 28, 2.8, 1.2); candle(g, "candleC", C); }); flames(g, t);});submit(SkullCandles, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 16, y: 29 }, variants: [ { name: "idle-fire" }, { name: "idle-soul", repalette: swap("fl", "soul") }, { name: "idle-witch", repalette: swap("fl", "witch") }, ],});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.