Orrery
A 48×64 floor prop: an orrery with a glowing sun on a brass column over a turned dark-wood pedestal. Three planets (iron, a ringed violet one and a blue one) ride tilted brass rings at 3, 2 and 1 turns per loop, each held by a brass arm from its own collar on the column and lit by the sun they circle. The near halves of the rings, arms and planets pass in front of the sun and column, the far halves behind. The loop is 24 frames; “idle” has a fire sun and “idle-soul” swaps it to soulfire.
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.
// Orrery — 48x64 floor prop, top-down 3/4, key light top-left (the planets are lit by the sun they circle).// A glowing sun on a brass column over a turned dark-wood pedestal; three planets ride tilted brass rings at// 3, 2 and 1 turns per loop (iron, ringed violet, blue), each held by a brass arm from its own hub collar on the// column. Near halves of rings/arms/planets pass in front of the sun and column, far halves behind (g.depth).// Loop: 24 frames @ 10 fps. Variants: idle (fire sun), idle-soul (fl ramp swapped to soulfire).The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Orrery, { size: [48, 64], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 2.4, loop: true }, anchor: { x: 24, y: GROUND }, variants: [ { name: "idle" }, { name: "idle-soul", repalette: swap("fl", "soul") }, ],});Show the whole program (230 lines)
// Orrery — 48x64 floor prop, top-down 3/4, key light top-left (the planets are lit by the sun they circle).// A glowing sun on a brass column over a turned dark-wood pedestal; three planets ride tilted brass rings at// 3, 2 and 1 turns per loop (iron, ringed violet, blue), each held by a brass arm from its own hub collar on the// column. Near halves of rings/arms/planets pass in front of the sun and column, far halves behind (g.depth).// Loop: 24 frames @ 10 fps. Variants: idle (fire sun), idle-soul (fl ramp swapped to soulfire).const SX = 24, SY = 20, GROUND = 61; // sun centre (continuous coords), ground rowconst SUN_R = 7;const HUB = { top: 32, bot: 36 }; // brass hub drum on the column; the arms leave from itconst RINGS = [ { name: "iron", r: 10.5, k: 0.46, roll: 13, speed: 3, ph: 0.15, hub: 34, rad: 2.5 }, { name: "violet", r: 14.5, k: 0.46, roll: -8, speed: 2, ph: 0.62, hub: 34, rad: 2.5 }, { name: "blue", r: 18.5, k: 0.46, roll: 4, speed: 1, ph: 0.9, hub: 34, rad: 3.5 },];// a point on a ring (angle a); front = sin(a): +1 nearest the camera (bottom of the ellipse on screen)function orbitPt(R, a) { const c = Math.cos(a), s = Math.sin(a), q = (R.roll * Math.PI) / 180; const ex = R.r * c, ey = R.r * R.k * s; return { x: SX + ex * Math.cos(q) - ey * Math.sin(q), y: SY + ex * Math.sin(q) + ey * Math.cos(q), front: s };}// ring as a clean 1px pixel loop (sampled densely, deduplicated, L-corners removed), each pixel tagged near/farfunction ringPixels(R) { const pts = []; for (let i = 0; i < 480; i++) { const a = (i / 480) * TAU, p = orbitPt(R, a); const x = Math.floor(p.x), y = Math.floor(p.y), last = pts[pts.length - 1]; if (!last || last.x !== x || last.y !== y) pts.push({ x, y, front: p.front, u: Math.cos(a) }); } while (pts.length > 1 && pts[0].x === pts[pts.length - 1].x && pts[0].y === pts[pts.length - 1].y) pts.pop(); const out = []; for (let i = 0; i < pts.length; i++) { const p = out.length ? out[out.length - 1] : pts[pts.length - 1], c = pts[i], q = pts[(i + 1) % pts.length]; const corner = (p.x === c.x || p.y === c.y) && (q.x === c.x || q.y === c.y) && p.x !== q.x && p.y !== q.y; if (!corner) out.push(c); } return out;}const RING_PX = RINGS.map(ringPixels);// ---- pedestal ------------------------------------------------------------------------------------------// dark wood: the body lives in w0..w2, w3 only on the lit edgeconst stemR = (y) => (y <= 44 ? 4 : y <= 47 ? 2.5 : y <= 51 ? 4.5 - Math.abs(y - 49.5) * 0.6 : 3);function pedestal(g) { g.part("stem", () => { g.field({ x: [SX - 6, SX + 5], y: [42, 53] }, (x, y) => { const R = stemR(y), u = (x + 0.5 - SX) / R; if (Math.abs(u) > 1) return null; if (y === 42) return u < -0.2 ? "g3" : u < 0.5 ? "g2" : "g1"; // brass ring capping the stem if (y === 43) return u < -0.2 ? "g1" : "g0"; return u < -0.65 ? "w3" : u < -0.1 ? "w2" : u < 0.55 ? "w1" : "w0"; }); g.line([SX - 3, 45], [SX + 2, 45], "w0"); // shadow under the cap's lip }); // base: a short cylinder, top ellipse centred at yt, seen from above const yt = 55, rx = 11.5, ry = 3.2, h = 3; g.part("base", () => { g.field({ x: [SX - 13, SX + 12], y: [51, GROUND] }, (x, y) => { const u = (x + 0.5 - SX) / rx; if (Math.abs(u) > 1) return null; const e = ry * Math.sqrt(1 - u * u), yc = y + 0.5; if (yc >= yt - e && yc <= yt + e) { // top face: lit toward the top-left const v = (yc - yt) / ry; const a = -0.6 * u - 0.5 * v; if (yc < yt - e + 1 && u < 0.2) return "w3"; // lit far rim return a > 0.3 ? "w2" : a > -0.35 ? "w1" : "w0"; } if (yc > yt && yc <= yt + h + e) { // side if (yc > yt + h + e - 1) return "w0"; if (yc <= yt + e + 1) return u < -0.3 ? "g2" : u < 0.4 ? "g1" : "g0"; // brass trim band return u < -0.55 ? "w1" : "w0"; } return null; }); // brass inlay ring set into the top face (near arc bright, far arc dull), then countable grain for (let i = 0; i < 48; i++) { const a = (i / 48) * TAU, x = Math.floor(SX + Math.cos(a) * 7), y = Math.floor(yt - 0.5 + Math.sin(a) * 1.7); g.pixel(x, y, Math.sin(a) > 0 && Math.cos(a) < 0 ? "g1" : "g0"); } g.line([SX - 7, 54], [SX - 4, 54], "w2"); g.line([SX + 3, 56], [SX + 6, 56], "w0"); });}function column(g) { g.part("column", () => { g.rect({ x: [SX - 1, SX], y: [SY + SUN_R - 1, 42] }, "g1", { fill: "g1" }); g.line([SX - 1, SY + SUN_R - 1], [SX - 1, 42], "g2"); }); g.part("hub", () => { // clockwork drum: lit top face, toothed side, one arm per tier g.rect({ x: [SX - 3, SX + 2], y: [HUB.top + 1, HUB.bot] }, "g1", { fill: "g1" }); g.line([SX - 3, HUB.top + 1], [SX - 3, HUB.bot], "g2"); g.line([SX + 2, HUB.top + 1], [SX + 2, HUB.bot], "g0"); g.line([SX - 2, HUB.top], [SX + 1, HUB.top], "g3"); g.pixel(SX + 1, HUB.top, "g2"); for (let x = SX - 3; x <= SX + 2; x += 2) g.pixel(x, HUB.bot, "g0"); // gear teeth g.line([SX - 2, HUB.top + 2], [SX + 1, HUB.top + 2], "g0"); // seam between the tiers });}// ---- sun -----------------------------------------------------------------------------------------------function sun(g, t) { g.part("sun", () => { g.field({ x: [SX - SUN_R, SX + SUN_R - 1], y: [SY - SUN_R, SY + SUN_R - 1] }, (x, y) => { const nx = (x + 0.5 - SX) / SUN_R, ny = (y + 0.5 - SY) / SUN_R, r = Math.hypot(nx, ny); if (r > 1) return null; if (r > 0.86) return "fl0"; // cooler limb let v = 1 - Math.hypot(nx + 0.25, ny + 0.25) * 0.9; // hot core, offset toward the key light v += 0.05 * Math.sin(TAU * 2 * t); // the core breathes let i = v > 0.6 ? 3 : v > 0.28 ? 2 : 1; // two sunspots ride the surface as the sun turns (one turn per loop, half a turn apart) for (const [lat, ph] of [[-0.3, 0], [0.28, 0.5]]) { const lon = ((t + ph) % 1) * 2 - 1; // -1..1 across the face, then round the back const sx = Math.sin((lon * Math.PI) / 2) * Math.sqrt(1 - lat * lat) * 0.8; if (i === 2 && Math.abs(nx - sx) < 0.12 && Math.abs(ny - lat) < 0.1 && r < 0.8) i = 1; // never on the core } return "fl" + i; }); }); g.detached("corona", () => { for (let k = 0; k < 8; k++) { const a = (k / 8) * TAU + TAU / 16; const len = Math.sin(TAU * (2 * t) + k * 1.9) > 0.1 ? 2 : 1; for (let i = 0; i < len; i++) { const d = SUN_R + 0.7 + i; g.pixel(Math.floor(SX + Math.cos(a) * d), Math.floor(SY + Math.sin(a) * d), i === 0 ? "fl1" : "fl0"); } } });}// ---- rings, arms, planets ------------------------------------------------------------------------------function rings(g) { RINGS.forEach((R, i) => { g.part("ring" + (i + 1), () => { for (const p of RING_PX[i]) { const col = p.front > 0 ? (p.u < -0.35 ? "g3" : "g2") : (p.u < 0 ? "g1" : "g0"); g.depth(p.front > 0 ? -1 : 1, () => g.pixel(p.x, p.y, col)); } }); });}// planet sprite as a pixel map: sun-lit sphere + optional flat ring, ink outline around the unionfunction planetMap(cx, cy, rad, ramp, extra) { const m = new Map(), key = (x, y) => x + "," + y; const lx = SX - cx, ly = SY - cy, L = Math.hypot(lx, ly) || 1; const ux = (0.75 * lx) / L - 0.2, uy = (0.75 * ly) / L - 0.2, uz = 0.62; // sun light + a little key light const x0 = Math.floor(cx - rad - 6), x1 = Math.ceil(cx + rad + 6); const y0 = Math.floor(cy - rad - 3), y1 = Math.ceil(cy + rad + 3); const body = (x, y) => { const nx = (x + 0.5 - cx) / rad, ny = (y + 0.5 - cy) / rad; return nx * nx + ny * ny <= 1; }; if (extra && extra.back) extra.back(m, key); for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { if (!body(x, y)) continue; const nx = (x + 0.5 - cx) / rad, ny = (y + 0.5 - cy) / rad, nz = Math.sqrt(Math.max(0, 1 - nx * nx - ny * ny)); const d = nx * ux + ny * uy + nz * uz; m.set(key(x, y), ramp[clamp(Math.floor(((d + 0.1) / 1.1) * 4), 0, 3)]); } if (extra && extra.front) extra.front(m, key, body); const ring = []; for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { if (m.has(key(x, y))) continue; if (m.has(key(x - 1, y)) || m.has(key(x + 1, y)) || m.has(key(x, y - 1)) || m.has(key(x, y + 1))) ring.push(key(x, y)); } for (const k of ring) m.set(k, "ink"); return { m, box: { x: [x0, x1], y: [y0, y1] } };}const clamp = (v, a, b) => Math.max(a, Math.min(b, v));function planetExtras(name, cx, cy, t) { if (name === "iron") return { front: (m, key) => { const x = Math.floor(cx), y = Math.floor(cy); m.set(key(x - 1, y), "n1"); m.set(key(x + 1, y + 1), "n2"); }, }; if (name === "blue") return { // a small green continent that drifts round as the planet spins (2 turns per loop) front: (m, key, body) => { const x = Math.floor(cx), y = Math.floor(cy), s = Math.round(Math.sin(TAU * 2 * t) * 1.5); for (const [dx, dy, c] of [[-1, -1, "e2"], [0, -1, "e1"], [-1, 0, "e1"], [1, 1, "e1"]]) { const px = x + dx + s; if (body(px, y + dy) && Math.abs(px + 0.5 - cx) < 3) m.set(key(px, y + dy), c); } }, }; // violet: a flat ring (11x3), far arc behind the ball, near arc in front const pts = []; for (let i = 0; i < 64; i++) { const a = (i / 64) * TAU; pts.push([Math.floor(cx + Math.cos(a) * 5.4), Math.floor(cy + Math.sin(a) * 1.4 - Math.cos(a) * 0.9), Math.sin(a)]); } return { back: (m, key) => { for (const [x, y, s] of pts) if (s <= 0) m.set(key(x, y), x < cx ? "n4" : "n3"); }, front: (m, key) => { for (const [x, y, s] of pts) if (s > 0) m.set(key(x, y), x < cx ? "n5" : "n4"); }, };}function orbiters(g, t) { RINGS.forEach((R) => { const p = orbitPt(R, TAU * (R.ph + R.speed * t)); const cx = Math.floor(p.x) + 0.5, cy = Math.floor(p.y) + 0.5; const near = p.front > 0; // arm: from the hub drum on the column out to the planet (all arms share the drum's pivot, so they fan out // from one point instead of running side by side) g.depth(near ? -1 : 1, () => g.part("arm-" + R.name, () => { g.line([SX, R.hub], [Math.floor(cx), Math.floor(cy)], near ? "g1" : "g0"); })); const { m, box } = planetMap(cx, cy, R.rad, R.name === "iron" ? ["n1", "n2", "n3", "n4"] : R.name === "blue" ? ["b0", "b1", "b2", "b3"] : ["v0", "v1", "v2", "v3"], planetExtras(R.name, cx, cy, t)); g.depth(near ? -2 : 1, () => g.part(R.name, () => g.field(box, (x, y) => m.get(x + "," + y) ?? null))); });}const Orrery = sprite(pal, (g, t) => { contactShadow(g, SX, GROUND, 14, 2); g.outlined("ink", () => { pedestal(g); column(g); }); sun(g, t); rings(g); orbiters(g, t);});submit(Orrery, { size: [48, 64], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 2.4, loop: true }, anchor: { x: 24, y: GROUND }, variants: [ { name: "idle" }, { name: "idle-soul", repalette: swap("fl", "soul") }, ],});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.