Crystal ball
A 32×48 crystal ball on a brass claw stand, in top-down 3/4 with the key light at the top-left. In the 16-frame loop, mist swirls inside the glass in two arms that make half a turn, an eye opens and fades back into the mist, and the specular glint twinkles once. The glow comes in blue, violet and green 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.
// Crystal ball on a brass claw stand — 32x48, top-down 3/4, key light top-left.// Loop: 16 frames @ 10 fps. Mist swirls inside the glass (two arms, half a turn per loop), an eye opens and// fades back into the mist, the specular twinkles once.// Variants: glow colour blue / violet / green (ramp swap of the glow0..3 aliases — no new colours).The last lines. submit declares the canvas and lists the variants the renderer exports.
submit(CrystalBall, { size: [32, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: 16, y: 45 }, variants: [ { name: "idle-blue" }, { name: "idle-violet", repalette: swap("glow", "v") }, { name: "idle-green", repalette: swap("glow", "e") }, ],});Show the whole program (184 lines)
// Crystal ball on a brass claw stand — 32x48, top-down 3/4, key light top-left.// Loop: 16 frames @ 10 fps. Mist swirls inside the glass (two arms, half a turn per loop), an eye opens and// fades back into the mist, the specular twinkles once.// Variants: glow colour blue / violet / green (ramp swap of the glow0..3 aliases — no new colours).const CX = 16, GROUND = 45;const ORB = { cx: 16, cy: 19, r: 11 };const BASE = { cy: 37, rx: 10, ry: 3, h: 4 };function orbAt(x, y) { const nx = (x + 0.5 - ORB.cx) / ORB.r, ny = (y + 0.5 - ORB.cy) / ORB.r; return { nx, ny, r: Math.sqrt(nx * nx + ny * ny) };}// ---- glass -----------------------------------------------------------------------------------------------function glass(g, t) { const eyeOpen = 0.5 - 0.5 * Math.cos(TAU * t); g.field({ x: [ORB.cx - ORB.r, ORB.cx + ORB.r], y: [ORB.cy - ORB.r, ORB.cy + ORB.r] }, (x, y) => { const { nx, ny, r } = orbAt(x, y); if (r > 1) return null; const ux = nx / (r || 1), uy = ny / (r || 1); // specular: a crisp window highlight on the upper-left shoulder const sx = nx + 0.4, sy = ny + 0.42; if (sx * sx * 26 + sy * sy * 44 < 1) return "n5"; // dark rim — the far (upper) rim is thicker: the eye looks through the full depth of glass there if (r > 0.9) return "glow0"; if (r > 0.8 && uy < -0.35) return "glow0"; // reflection of the room: a thin bright arc just inside the upper-left rim, running on from the specular if (r > 0.72 && r <= 0.8 && ux < -0.2 && uy < -0.35) return "glow3"; // light passing through: a caustic crescent on the lower inner wall, opposite the key light const dir = ux * 0.42 + uy * 0.91; if (r > 0.64 && dir > 0.9) return r > 0.7 && r < 0.86 && dir > 0.97 ? "glow3" : "glow2"; // swirling mist: two arms, 2-fold symmetric, half a turn per loop (closes the loop) const inEye = Math.abs(nx) < 0.44 && Math.abs(ny) < 0.24 && eyeOpen > 0.3; if (r < 0.72 && r > 0.14 && !inEye) { const th = Math.atan2(ny, nx); const m = Math.cos(2 * th + 5.4 * r - TAU * t); if (m > 0.92 && r > 0.3 && r < 0.6) return "glow3"; if (m > 0.62) return "glow2"; } // body of the glass: a shade darker on the upper-left (reflects the dark room), lighter where light exits const d = 0.55 * nx + 0.83 * ny; if (d > 0.4) return "glow2"; if (d < -0.62) return "glow0"; return "glow1"; });}function eye(g, t) { // fades in with the mist parting, opens, and sinks back: periodic in t const e = 0.5 - 0.5 * Math.cos(TAU * t); if (e < 0.3) return; const cx = ORB.cx, cy = ORB.cy; g.part("eye", () => { if (e < 0.55) { // a slit of light g.line([cx - 3, cy], [cx + 2, cy], "glow2"); return; } const open = e > 0.86; const lid = open ? "glow3" : "glow2"; // almond lids g.line([cx - 4, cy], [cx - 3, cy], lid); g.line([cx + 2, cy], [cx + 3, cy], lid); g.line([cx - 2, cy - 1 - (open ? 1 : 0)], [cx + 1, cy - 1 - (open ? 1 : 0)], lid); g.line([cx - 2, cy + 1 + (open ? 1 : 0)], [cx + 1, cy + 1 + (open ? 1 : 0)], lid); if (open) { g.pixel(cx - 3, cy - 1, lid); g.pixel(cx + 2, cy - 1, lid); g.pixel(cx - 3, cy + 1, lid); g.pixel(cx + 2, cy + 1, lid); g.rect({ x: [cx - 2, cx + 1], y: [cy - 1, cy + 1] }, "glow2", { fill: "glow2" }); g.line([cx, cy - 1], [cx, cy + 1], "glow0"); // slit pupil g.line([cx - 1, cy - 1], [cx - 1, cy + 1], "glow0"); g.pixel(cx - 2, cy - 1, "glow3"); // catchlight } else { g.line([cx - 2, cy], [cx + 1, cy], "glow1"); g.pixel(cx - 1, cy, "glow0"); g.pixel(cx, cy, "glow0"); } });}// ---- brass claw stand ------------------------------------------------------------------------------------const brassV = (u, v) => (v < -0.45 ? "g2" : v > 0.45 ? "g0" : "g1");const TALONS = { L: [[13, 31], [9, 29], [6, 26], [5, 22], [7, 19]], R: [[19, 31], [23, 29], [26, 26], [27, 22], [25, 19]], F: [[16, 32], [16, 29], [15, 26]],};function talon(g, name, pts, w0, leftV) { // a jointed claw: a knuckle crease part-way up, a polished nail at the tip. // leftV = which sign of v (across the stroke) points screen-left, i.e. toward the key light. g.part(name, () => g.sweep(pts, { w0, w1: 0.3, profile: 1.3 }, (u, v) => { const s = v * leftV; if (u > 0.42 && u < 0.5) return s > 0.3 ? "g1" : "g0"; if (u > 0.8) return s > -0.2 ? "g3" : "g2"; return s > 0.35 ? "g2" : s < -0.45 ? "g0" : "g1"; }));}function talons(g) { talon(g, "talonL", TALONS.L, 1.5, 1); talon(g, "talonR", TALONS.R, 1.5, -1); talon(g, "talonF", TALONS.F, 1.6, -1);}function stem(g) { g.part("stem", () => { // domed foot on the wood, a slim neck, and the palm the talons grow from g.ellipse({ x: [12, 20], y: [34, 38] }, "g0", { fill: "g0" }); g.ellipse({ x: [12, 20], y: [33, 37] }, "g1", { fill: "g1" }); g.line([13, 35], [14, 34], "g2"); g.line([18, 36], [19, 35], "g0"); g.rect({ x: [15, 17], y: [31, 34] }, "g1", { fill: "g1" }); g.line([15, 31], [15, 34], "g2"); g.line([17, 31], [17, 34], "g0"); g.ellipse({ x: [12, 20], y: [29, 32] }, "g1", { fill: "g1" }); g.line([13, 30], [15, 29], "g3"); g.line([17, 31], [19, 31], "g0"); });}function caustic(g) { // light focused through the orb lands on the base, down-right of the stand (away from the key light) g.part("caustic", () => { g.line([21, 37], [23, 37], "glow2"); g.pixel(22, 37, "glow3"); g.pixel(22, 38, "glow1"); });}// ---- dark-wood base --------------------------------------------------------------------------------------function base(g) { const { cy, rx, ry, h } = BASE; g.part("base", () => { // side band (front of the round plinth), then the lit top face g.field({ x: [CX - rx, CX + rx], y: [cy, cy + h + ry] }, (x, y) => { const nx = (x + 0.5 - CX) / rx; if (Math.abs(nx) > 1) return null; const yb = cy + h + ry * Math.sqrt(1 - nx * nx); // bottom edge of the band if (y + 0.5 > yb) return null; const rel = y - (cy + ry * Math.sqrt(1 - nx * nx)); if (Math.abs(rel - 1.5) < 0.5) return nx < -0.3 ? "w1" : "w0"; // carved groove if (nx < -0.55) return "w2"; if (nx < 0.35) return "w1"; return "w0"; }); 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; const d = nx * nx + ny * ny; if (d > 1) return null; const a = -0.75 * nx - 0.4 * ny; if (d > 0.62) return a > 0.2 ? "w3" : a > -0.3 ? "w2" : "w1"; // bevelled rim return a > 0.35 ? "w2" : "w1"; }); });}function twinkle(g, t) { const p = (t + 0.3) % 1; if (p > 0.18) return; const x = ORB.cx - 5, y = ORB.cy - 6; g.detached("twinkle", () => { const k = p < 0.06 || p > 0.12 ? 1 : 2; for (let i = 1; i <= k; i++) { g.pixel(x - i - 1, y, "n5"); g.pixel(x + i + 1, y, "n5"); g.pixel(x, y - i - 1, "n5"); g.pixel(x, y + i + 1, "n5"); } });}const CrystalBall = sprite(pal, (g, t) => { contactShadow(g, CX, GROUND, 12, 2); g.outlined("ink", () => { base(g); caustic(g); stem(g); g.part("orb", () => glass(g, t)); talons(g); }); eye(g, t); twinkle(g, t);});submit(CrystalBall, { size: [32, 48], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.6, loop: true }, anchor: { x: 16, y: 45 }, variants: [ { name: "idle-blue" }, { name: "idle-violet", repalette: swap("glow", "v") }, { name: "idle-green", repalette: swap("glow", "e") }, ],});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.