Brazier
A 48×64 wrought-iron brazier: a wide riveted bowl of glowing coals on three curved legs, with a big fire made of three overlapping flames and rising embers. In the 12-frame loop the flames lick upward and shed tongues, the cracks in the coal pulse and the embers rise. The flame comes in fire, soul, witch and bile by swapping one ramp, and “unlit” is a single cold still of ash with a few dying embers.
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.
// Wrought-iron brazier — 48x64 (tall), top-down 3/4, key light top-left.// A wide riveted bowl of glowing coals on three curved legs, with a big fire (3 overlapping flames) and rising embers.// Loop: 12 frames @ 10 fps: flames lick upward and shed tongues, coal cracks pulse, embers rise.// Variants: flame colour fire / soul / witch / bile (fl ramp swap), plus a cold `unlit` still (ash, a few dying embers).The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Brazier, { size: [48, 64], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 24, y: 61 }, args: ["lit"], variants: [ { name: "idle-fire" }, { name: "idle-soul", repalette: swap("fl", "soul") }, { name: "idle-witch", repalette: swap("fl", "witch") }, { name: "idle-bile", repalette: swap("fl", "bile") }, { name: "unlit", args: ["unlit"], animate: false }, ],});Show the whole program (190 lines)
// Wrought-iron brazier — 48x64 (tall), top-down 3/4, key light top-left.// A wide riveted bowl of glowing coals on three curved legs, with a big fire (3 overlapping flames) and rising embers.// Loop: 12 frames @ 10 fps: flames lick upward and shed tongues, coal cracks pulse, embers rise.// Variants: flame colour fire / soul / witch / bile (fl ramp swap), plus a cold `unlit` still (ash, a few dying embers).const CX = 24, GROUND = 61;const RIM = { cy: 33, rx: 18, ry: 5 };const MOUTH = { cy: 33, rx: 15, ry: 3.2 };const BOWL = { cy: 34, rx: 17, ry: 12 };const IRON = ["n0", "n1", "n2", "n3"];// ---- legs ---------------------------------------------------------------------------------------// three legs at 120 degrees: the front one drops from the boss under the bowl, the two back ones// splay outward from the bowl's flanks; each ends in a scroll where the bar curls up off the floorfunction backLeg(g, name, s) { const X = (x) => CX + s * x; // s = -1 left, +1 right const lit = s < 0; const span = (a, b) => [Math.min(X(a), X(b)), Math.max(X(a), X(b))]; g.part(name, () => { g.sweep([[X(11), 42], [X(15.5), 45], [X(17.5), 49.5], [X(19), 55]], { w0: 1.1, w1: 0.9 }, (u, v) => (v * s > 0.2 ? (lit ? "n3" : "n2") : v * s < -0.4 ? "n0" : "n1")); g.strokeEllipse({ x: span(19, 21), y: [52, 55] }, "n1"); g.pixel(X(20), 52, lit ? "n3" : "n2"); });}function frontLeg(g) { g.part("legF", () => { g.sweep([[24, 45], [25, 49], [24, 54], [23, 58]], { w0: 1.3, w1: 1.0 }, (u, v) => (v < -0.25 ? "n3" : v > 0.4 ? "n0" : "n1")); g.strokeEllipse({ x: [23, 26], y: [58, 61] }, "n1"); // scroll foot facing us g.pixel(23, 59, "n3"); g.pixel(24, 58, "n3"); g.line([25, 61], [26, 60], "n0"); });}// ---- bowl ---------------------------------------------------------------------------------------function bowl(g) { g.part("bowl", () => { g.field({ x: [CX - BOWL.rx, CX + BOWL.rx], y: [BOWL.cy, BOWL.cy + BOWL.ry] }, (x, y) => { const i = litIndex(x, y, CX, BOWL.cy, BOWL.rx, BOWL.ry, 4, 0.15); if (i < 0) return null; const nx = (x + 0.5 - CX) / BOWL.rx, ny = (y + 0.5 - BOWL.cy) / BOWL.ry; const sx = nx + 0.5, sy = ny - 0.5; if (sx * sx * 3 + sy * sy * 12 < 0.08) return "n4"; // soft specular on the upper-left shoulder return IRON[i]; }); // a boss under the bowl where the legs meet g.ellipse({ x: [CX - 3, CX + 3], y: [44, 46] }, "n1", { fill: "n1" }); g.line([CX - 2, 44], [CX, 44], "n2"); }); // riveted strap round the belly, following the curve of the bowl const yAt = (x) => { const u = (x + 0.5 - CX) / (BOWL.rx - 0.5); return Math.round(BOWL.cy + 6.5 - (1 - Math.sqrt(Math.max(0, 1 - u * u))) * 4); }; g.part("strap", () => { for (let x = CX - BOWL.rx + 1; x <= CX + BOWL.rx - 1; x++) { const u = (x - CX) / BOWL.rx, y = yAt(x); g.pixel(x, y, u < -0.3 ? "n3" : u < 0.4 ? "n2" : "n1"); g.pixel(x, y + 1, "n0"); } for (const u of [-0.8, -0.48, -0.16, 0.16, 0.48, 0.8]) { const x = Math.round(CX + u * (BOWL.rx - 1)), y = yAt(x); g.pixel(x, y, u < 0 ? "n5" : "n4"); } });}function rim(g, lit) { const { cy, rx, ry } = RIM; g.part("rim", () => { g.ellipse({ x: [CX - rx, CX + rx], y: [cy - ry + 1, cy + ry + 1] }, "n1", { fill: "n1" }); // rolled lip thickness 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; const a = -0.75 * nx - 0.35 * ny; if (lit && ny < -0.2 && ny > -0.75 && Math.abs(nx) > 0.72 && Math.abs(nx) < 0.86) return "fl0"; // coal glow on the inner lip return a > 0.5 ? "n4" : a > 0.1 ? "n3" : a > -0.4 ? "n2" : "n1"; }); });}// ---- coals --------------------------------------------------------------------------------------const BACK_LUMPS = [[12, 31], [17, 30], [23, 29], [29, 30], [35, 31], [14, 33], [20, 32], [27, 32], [33, 33]];const FRONT_LUMPS = [[11, 34], [16, 35], [21, 35], [27, 35], [32, 35], [37, 34]];function lumps(g, t, lit, list, k0) { for (let i = 0; i < list.length; i++) { const [lx, ly] = list[i], id = i + k0; g.ellipse({ x: [lx - 2, lx + 2], y: [ly - 1, ly + 1] }, "n0", { fill: "n0" }); g.line([lx - 1, ly - 1], [lx + 1, ly - 1], "n1"); // lit top if (lit) { g.pixel(lx + 2, ly, "fl0"); // hot underside edge const p = 0.5 + 0.5 * Math.sin(TAU * (t + hash(id, 3))); // crack pulses on its own phase if (hash(id, 7) > 0.3) g.line([lx - 1, ly], [lx, ly + (hash(id, 9) > 0.5 ? 1 : 0)], p > 0.72 ? "fl3" : p > 0.36 ? "fl2" : "fl1"); } else { g.pixel(lx - 1, ly - 1, "n2"); // dull charcoal sheen if (hash(id, 7) > 0.72) g.pixel(lx + 2, ly, "r0"); // a dying ember in the gap } }}function coalBed(g, lit) { const { cy, rx, ry } = MOUTH; g.part("coals", () => { g.field({ x: [CX - rx, CX + rx], y: [cy - ry - 4, cy + ry] }, (x, y) => { const nx = (x + 0.5 - CX) / rx, ny = (y + 0.5 - cy) / ry; const top = -1 - 1.3 * Math.max(0, 1 - nx * nx * 2.2); // the heap bulges above the rim if (!(nx * nx + ny * ny <= 1 || (ny < 0 && ny >= top && Math.abs(nx) < 0.68))) return null; if (!lit) { // grey ash settled between cold lumps const ax = nx / 0.5, ay = (y + 0.5 - (cy - 1.5)) / 2.8; return ax * ax + ay * ay <= 1 ? (ax < -0.2 && ay < -0.1 ? "n3" : "n2") : ny < 0 ? "n2" : "n1"; } return Math.abs(nx) < 0.3 ? "fl2" : Math.abs(nx) < 0.65 ? "fl1" : "fl0"; }); });}// ---- fire ---------------------------------------------------------------------------------------// each flame: a teardrop with nested inner teardrops (body, bright, core) that are shorter and narrower,// so the inner layers are themselves flame-shaped. Side flames lean outward; the centre one is in front.const FLAMES = [ { name: "flameL", x: 17, base: 32, h: 18, hw: 4.6, lean: -2.5, seed: 0.31 }, { name: "flameR", x: 31, base: 32, h: 20, hw: 4.6, lean: 2.5, seed: 0.64 }, { name: "flameC", x: 24, base: 33, h: 28, hw: 7, lean: 0, seed: 0.05 },];const LAYERS = [["fl0", 1, 1], ["fl1", 0.88, 0.72], ["fl2", 0.62, 0.48], ["fl3", 0.36, 0.26]]; // key, height, widthconst profile = (k) => (k < 0.22 ? 0.7 + 0.3 * Math.sin((k / 0.22) * Math.PI / 2) : Math.pow((1 - k) / 0.78, 0.9));function flameHeight(f, t) { return f.h * (1 + 0.08 * Math.sin(TAU * (2 * t + f.seed)) + 0.05 * Math.sin(TAU * (3 * t + 2 * f.seed)));}function flame(g, f, t) { const hh = flameHeight(f, t); g.part(f.name, () => g.field({ x: [2, 45], y: [f.base - 32, f.base] }, (x, y) => { const k = (f.base + 1 - (y + 0.5)) / hh; if (k < 0 || k > 1) return null; const ax = f.x + 0.5 + (f.lean + 1.4 * Math.sin(TAU * (t + f.seed))) * k * k + 1.0 * Math.sin(TAU * (1.2 * k - 2 * t + f.seed)) * k; const lick = 1 + 0.2 * Math.sin(TAU * (2.3 * k - 2 * t + 1.7 * f.seed)); // bulges travel upward const dx = Math.abs(x + 0.5 - ax); let key = null; for (const [c, hf, wf] of LAYERS) { const kj = k / hf; if (kj > 1) break; if (dx <= f.hw * wf * profile(kj) * lick) key = c; else break; } return key; }));}function fire(g, t) { for (const f of FLAMES) flame(g, f, t); // tongues tear off the tips and rise; embers drift up and cool g.detached("tongues", () => { for (const f of FLAMES) { const p = (2 * t + f.seed) % 1, tip = f.base - flameHeight(f, t); const x = f.x + Math.round(f.lean + 1.4 * Math.sin(TAU * (t + f.seed))), y = Math.round(tip - 1 - p * 7); if (y < 1) continue; if (p < 0.4) { g.rect({ x: [x, x + 1], y: [y, y + 1] }, "fl1", { fill: "fl1" }); g.pixel(x + 1, y, "fl0"); g.pixel(x, y + 1, "fl0"); } else if (p < 0.7) g.pixel(x, y, "fl0"); } }); g.detached("embers", () => { for (let i = 0; i < 6; i++) { const p = (t + i / 6) % 1; const x = CX + Math.round((i - 2.5) * 4 + Math.sin(TAU * (p * 1.5 + hash(i, 1))) * 2.5); const y = 26 - Math.round(p * 24); if (p < 0.92 && y >= 0) g.pixel(x, y, p < 0.3 ? "fl3" : p < 0.6 ? "fl2" : p < 0.8 ? "fl1" : "fl0"); } });}// ---- sprite -------------------------------------------------------------------------------------const Brazier = sprite(pal, (g, t, state) => { const lit = state !== "unlit"; contactShadow(g, CX, 58, 21, 3); g.outlined("ink", () => { backLeg(g, "legL", -1); backLeg(g, "legR", 1); }); g.outlined("ink", () => { bowl(g); rim(g, lit); }); g.outlined("ink", () => { coalBed(g, lit); g.part("coal lumps", () => lumps(g, t, lit, BACK_LUMPS, 0)); }); if (lit) fire(g, t); g.part("front coals", () => lumps(g, t, lit, FRONT_LUMPS, 20)); g.outlined("ink", () => frontLeg(g));});submit(Brazier, { size: [48, 64], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 24, y: 61 }, args: ["lit"], variants: [ { name: "idle-fire" }, { name: "idle-soul", repalette: swap("fl", "soul") }, { name: "idle-witch", repalette: swap("fl", "witch") }, { name: "idle-bile", repalette: swap("fl", "bile") }, { name: "unlit", args: ["unlit"], animate: 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.