Teleport rune
A 48×48 floor prop: a low round dais of flagstones with a ring of curb stones, a carved channel inlaid with light, a recessed band of twelve runes, an inner channel and a compass-star sigil cut through the four inner flagstones. “dormant” (12 frames) keeps the runes barely lit while a slow shimmer walks round the outer channel; “activate” (10 frames, once) sends a spark round the channel lighting every rune, whirls the runes up, flares the sigil and bursts a column of light, ending on the first frame of “active”; “active” (12 frames) turns the rune band as motes rise. All three also come in violet.
6×
Get the animated sprites: Dungeon Mechanisms on itch.ioVol.2 · 12 traps, doors and breakables
The header. The comment at the top of the file is the brief: size, view, light direction and every variant.
// Teleport rune — 48x48 floor prop, top-down 3/4, key light top-left.// A low round dais of flagstones: a ring of curb stones, a carved channel inlaid with light, a recessed band of// twelve runes, an inner channel, and a compass-star sigil cut through the four inner flagstones.// Variants (all light in glow0..3; violet = swap("glow","v")):// dormant 12f loop runes barely lit; a slow shimmer walks once round the outer channel// activate 10f once a spark races round the channel lighting every rune, the runes whirl up, the sigil flares// and a column of light bursts up; ends on active frame 0// active 12f loop bright channels, the rune band turns (4 runes per loop), motes rise through a light columnThe last lines. submit declares the canvas and lists the variants the renderer exports.
submit(Rune, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["dormant"], variants: [ { name: "dormant" }, { name: "activate", args: ["activate"], animate: { fps: 10, duration: 1.0, loop: false } }, { name: "active", args: ["active"] }, { name: "dormant-violet", repalette: swap("glow", "v") }, { name: "activate-violet", args: ["activate"], animate: { fps: 10, duration: 1.0, loop: false }, repalette: swap("glow", "v") }, { name: "active-violet", args: ["active"], repalette: swap("glow", "v") }, ],});Show the whole program (254 lines)
// Teleport rune — 48x48 floor prop, top-down 3/4, key light top-left.// A low round dais of flagstones: a ring of curb stones, a carved channel inlaid with light, a recessed band of// twelve runes, an inner channel, and a compass-star sigil cut through the four inner flagstones.// Variants (all light in glow0..3; violet = swap("glow","v")):// dormant 12f loop runes barely lit; a slow shimmer walks once round the outer channel// activate 10f once a spark races round the channel lighting every rune, the runes whirl up, the sigil flares// and a column of light bursts up; ends on active frame 0// active 12f loop bright channels, the rune band turns (4 runes per loop), motes rise through a light columnconst W = 48, H = 48, CX = 24, CY = 30, RX = 22, RY = 13, GROUND = 45;const S_G1 = 0.87, S_G2 = 0.46; // outer / inner channel (ellipse scale)const S_MID = (S_G1 + S_G2) / 2; // rune band centrelineconst DEG = Math.PI / 180;// ---- static per-pixel map -----------------------------------------------------------------------------const NONE = 0, FACE = 1, CURB = 2, G1 = 3, BAND = 4, G2 = 5, INNER = 6;const rOf = (x, y) => Math.hypot((x - CX) / RX, (y - CY) / RY);const inS = (x, y, s) => rOf(x, y) <= s - 0.01; // the margin trims the 1px nubs at the polesconst ringPx = (x, y, s) => inS(x, y, s) && [[1, 0], [-1, 0], [0, 1], [0, -1]].some(([a, b]) => !inS(x + a, y + b, s));const code = new Uint8Array(W * H), ang = new Float32Array(W * H);for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const i = y * W + x; ang[i] = ((Math.atan2((y - CY) / RY, (x - CX) / RX) / DEG) + 360) % 360; // 0 = right, 90 = front if (!inS(x, y, 1)) { code[i] = y > CY && (inS(x, y - 1, 1) || inS(x, y - 2, 1)) ? FACE : NONE; continue; } code[i] = ringPx(x, y, S_G1) ? G1 : inS(x, y, S_G1) === false ? CURB : ringPx(x, y, S_G2) ? G2 : inS(x, y, S_G2) ? INNER : BAND;}const at = (x, y) => (x < 0 || y < 0 || x >= W || y >= H ? NONE : code[y * W + x]);// band pixels touching a channel: they catch spill light when active; the channel's upper-left lip shades the bandconst nearG = (x, y) => [[1, 0], [-1, 0], [0, 1], [0, -1]].some(([a, b]) => { const c = at(x + a, y + b); return c === G1 || c === G2; });// angular distance (deg, 0..180)const adist = (a, b) => { const d = Math.abs(((a - b) % 360 + 360) % 360); return Math.min(d, 360 - d); };// swept(a, from, len): is angle a inside the arc that starts at `from` and runs `len` degrees clockwise?const swept = (a, from, len) => ((a - from) % 360 + 360) % 360 <= len;// ---- runes: twelve slots, four glyphs repeating, so turning by four slots (120 deg) closes the loop ------const GLYPHS = [ ["xx.", "x.x", "x.."], // thorn ["x.x", ".x.", ".x."], // elk [".x.", "x.x", ".x."], // ing ["x.x", "xxx", "x.x"], // hagal];const runeAt = (k, off) => { const a = (k * 30 + off) * DEG; return { x: Math.round(CX + Math.cos(a) * RX * S_MID), y: Math.round(CY + Math.sin(a) * RY * S_MID), a: ((k * 30 + off) % 360 + 360) % 360 };};// ---- stone -------------------------------------------------------------------------------------------const stoneHash = (a) => hash(Math.floor(((a + 15) % 360) / 30), 5); // one value per curb stonefunction dais(g, L) { g.part("dais", () => g.field({ x: [1, 46], y: [15, 46] }, (x, y) => { const c = at(x, y), i = y * W + x, a = ang[i]; const nx = (x - CX) / RX; if (c === NONE) return null; if (c === FACE) { // the dais' side: lit on the left, falling away on the right const top = !inS(x, y - 1, 1) ? false : true; if (nx < -0.62) return top ? "n2" : "n1"; if (nx > 0.55) return "n0"; return top ? "n1" : "n0"; } if (c === CURB) { const edge = !inS(x - 1, y, 1) || !inS(x, y - 1, 1) || !inS(x + 1, y, 1) || !inS(x, y + 1, 1); if (edge && a > 150 && a < 290) return "n4"; // outer lip catches the light (upper left) if (edge && a > 10 && a < 110) return "n2"; // lower right lip in shade return stoneHash(a) < 0.3 ? "n2" : "n3"; } if (c === G1 || c === G2) return L.groove(x, y, a, c); if (c === BAND) { if (L.spill && nearG(x, y)) return "glow0"; // carved band: the upper-left wall of the channel shades the band just inside it const shade = at(x, y - 1) === G1 || (at(x - 1, y) === G1 && a > 180); return shade ? "n0" : "n1"; } // INNER: four flagstones cut by a glowing cross; a diamond sigil at the centre const dx = x - CX, dy = y - CY, r = rOf(x, y); const ring = ringPx(x, y, 0.24) && !(dx === 0 && dy === 0); const spoke = r > 0.26 && Math.abs(Math.abs(dx) / RX - Math.abs(dy) / RY) < 0.024; // the flagstone seams if (dx === 0 && dy === 0) return L.sigil(x, y, true); if (ring || spoke) return L.sigil(x, y, false); if (L.pool) return r < 0.24 ? L.pool[1] : L.pool[0]; // flat flagstones; the lower-right lip of each cut seam faces the light const cut = (u, v) => ringPx(u, v, 0.24) || (rOf(u, v) > 0.26 && Math.abs(Math.abs(u - CX) / RX - Math.abs(v - CY) / RY) < 0.024) || at(u, v) === G2; return cut(x - 1, y) || cut(x, y - 1) ? "n3" : "n2"; }));}function curbMarks(g) { g.part("curbSeams", () => { for (let j = 0; j < 12; j++) { const a = (j * 30 + 15) * DEG, c = Math.cos(a), s = Math.sin(a); const p0 = [Math.round(CX + c * RX * 0.92), Math.round(CY + s * RY * 0.92)]; const p1 = [Math.round(CX + c * RX * 0.99), Math.round(CY + s * RY * 0.99)]; g.line(p0, p1, "n1"); if (s > 0.2) { const fx = Math.round(CX + c * RX); const fy = Math.round(CY + s * RY) + 1; g.line([fx, fy], [fx, fy + 1], "ink"); } } g.pixel(8, 23, "n4"); g.pixel(37, 20, "n2"); g.pixel(41, 38, "n1"); g.pixel(12, 40, "n4"); // chips });}// ---- light layers --------------------------------------------------------------------------------------function runes(g, off, lit) { g.part("runes", () => { for (let k = 0; k < 12; k++) { const r = runeAt(k, off), gl = GLYPHS[k % 4], key = lit(r.a, k); if (!key) continue; for (let j = 0; j < 3; j++) for (let i = 0; i < 3; i++) if (gl[j][i] === "x") g.pixel(r.x - 1 + i, r.y - 1 + j, key); } });}const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];function column(g, t, hgt, bright) { // an opaque shaft of light standing on the inner disc: hot core, softer body, dim edges; bright bands (seen as // near arcs of a ring) climb it, a few sparks streak up, and the top dissolves in a dither. Periodic in t. const RC = bright ? 9 : 7; const TONE = ["glow1", "glow2", "glow3", "n5"]; g.detached("column", () => g.field({ x: [CX - 10, CX + 10], y: [0, CY + 6] }, (x, y) => { const h0 = CY - y; // height above the disc centre const wob = 1 + 0.04 * Math.sin(TAU * (h0 / 9 - 2 * t)); // the shaft breathes as it rises const dx = Math.abs(x - CX) / (RC * wob); if (dx > 1) return null; const arc = Math.sqrt(Math.max(0, 1 - dx * dx)); const yb = CY + Math.round(4.8 * arc); // front edge of the base const h = yb - y; // height above the floor if (h < 0 || h > hgt) return null; const top = hgt - h; // distance below the column's top const fade = top / (hgt * 0.45); // ordered-dither dissolve over the top 45% if (fade < 1 && BAYER[(y & 3) * 4 + (x & 3)] / 16 >= fade * fade * 0.9 + 0.05) return null; let k = dx < 0.3 ? 2 : dx < 0.72 ? 1 : 0; if (bright) k = dx < 0.8 ? 2 : 1; if (h === 0) k = Math.max(k, 2); // hot seam where the beam meets the floor for (let j = 0; j < 3; j++) { // bright bands climbing, curved like the near arc of a ring const hr = ((t + j / 3) % 1) * (hgt - 2); const d = y - (CY - Math.round(hr) + Math.round(2 * arc)); if (d === 0 || d === 1) { k += 1; break; } } if (!bright && dx > 0.3 && dx < 0.72 && hash(x, 11) > 0.5) { // sparks streaking up the body const s = ((y + Math.round(t * 24) + Math.floor(hash(x, 13) * 12)) % 12 + 12) % 12; if (s < 2) k = Math.max(k, 2); } return TONE[Math.min(3, k)]; }));}function motes(g, t, maxAge) { g.detached("motes", () => { for (let i = 0; i < 9; i++) { const p = (t * (i % 3 === 0 ? 2 : 1) + hash(i, 21)) % 1; if (p > maxAge) continue; const a = (hash(i, 23) * 360) * DEG, rr = 0.55 + 0.35 * hash(i, 29); const x = Math.round(CX + Math.cos(a) * RX * rr + Math.sin(TAU * (p + hash(i, 31))) * 1.2); const y = Math.round(CY + Math.sin(a) * RY * rr - p * 22); if (y < 0) continue; const key = p < 0.35 ? "glow3" : p < 0.7 ? "glow2" : "glow1"; g.pixel(x, y, key); if (p < 0.15) { g.pixel(x, y + 1, "glow2"); } } });}// ---- states --------------------------------------------------------------------------------------------function dormantLight(t) { const head = (90 + 360 * t) % 360; // the shimmer walks once round per loop return { groove: (x, y, a, c) => { if (c === G2) return "glow0"; const d = ((head - a) % 360 + 360) % 360; // how far behind the shimmer's head return d < 6 ? "glow3" : d < 22 ? "glow2" : d < 45 ? "glow1" : "glow0"; }, sigil: (x, y, centre) => (centre ? "glow1" : "glow0"), rune: (a) => (adist(a, head - 12) < 16 ? "glow2" : "glow1"), spill: false, pool: null, };}function activeLight(t) { const p1 = (90 + 120 * t) % 360, p2 = (p1 + 180) % 360; // two pulses ride round with the runes return { groove: (x, y, a, c) => { if (c === G2) return adist(a, (270 - 120 * t + 360) % 360) < 20 ? "glow3" : "glow2"; return adist(a, p1) < 14 || adist(a, p2) < 14 ? "glow3" : "glow2"; }, sigil: () => "glow3", rune: () => "glow3", spill: true, pool: ["glow1", "glow2"], };}const ACT = 10;const SPIN = [0, 5, 15, 30, 50, 70, 88, 102, 112, 120]; // rune whirl during activation (ends = 4 slots)function activateLight(f) { const d = dormantLight(0); const len = Math.min(360, 72 * (f + 1)); // the racing spark's swept arc from the front const head = (90 + len) % 360; const lit = (a) => len >= 360 || swept(a, 90, len); return { groove: (x, y, a, c) => { if (c === G2) return f >= 5 ? "glow2" : f === 4 ? "glow1" : "glow0"; if (len < 360 && adist(a, head) < 9) return "glow3"; if (f === 4 && len >= 360) return "glow3"; // the ring closes with a flash return lit(a) ? "glow2" : d.groove(x, y, a, c); }, sigil: (x, y, centre) => (f >= 5 ? "glow3" : f === 4 ? "glow2" : d.sigil(x, y, centre)), rune: (a) => (len < 360 && adist(a, head) < 20 ? "glow3" : lit(a) ? (f >= 4 ? "glow3" : "glow2") : "glow1"), spill: f >= 5, pool: f >= 6 ? ["glow1", "glow2"] : f === 5 ? ["glow0", "glow1"] : null, };}function spark(g, f) { // the spark at the head of the racing light: a small star if (f > 3) return; const a = (90 + 72 * (f + 1)) * DEG; const x = Math.round(CX + Math.cos(a) * RX * S_G1), y = Math.round(CY + Math.sin(a) * RY * S_G1); g.detached("spark", () => { for (const [ox, oy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) g.pixel(x + ox, y + oy, "glow3"); g.pixel(x, y - 2, "glow2"); g.pixel(x, y, "n5"); });}const Rune = sprite(pal, (g, t, state) => { let L, off = 0, ct = t, colH = 0, bright = false, age = 1; if (state === "dormant") { L = dormantLight(t); age = -1; } else if (state === "active") { L = activeLight(t); off = 120 * t; colH = 34; } else { // activate: frame-indexed const f = Math.round(t * ACT); ct = ((f - (ACT - 1)) / 12 + 1) % 1; // same 10 fps clock as the active loop off = SPIN[f] % 360; if (f >= ACT - 1) L = activeLight(0); else L = activateLight(f); colH = [0, 0, 0, 0, 0, 22, 40, 36, 34, 34][f]; bright = f === 5 || f === 6; age = f >= 7 ? (f - 6) * 0.34 : -1; if (f < ACT - 1) spark(g, f); } contactShadow(g, CX, GROUND, 23, 2); g.outlined("ink", () => dais(g, L)); curbMarks(g); runes(g, off, (a) => L.rune(a)); if (colH > 0) column(g, ct, colH, bright); if (age > 0) motes(g, ct, age);});submit(Rune, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: CX, y: GROUND }, args: ["dormant"], variants: [ { name: "dormant" }, { name: "activate", args: ["activate"], animate: { fps: 10, duration: 1.0, loop: false } }, { name: "active", args: ["active"] }, { name: "dormant-violet", repalette: swap("glow", "v") }, { name: "activate-violet", args: ["activate"], animate: { fps: 10, duration: 1.0, loop: false }, repalette: swap("glow", "v") }, { name: "active-violet", args: ["active"], repalette: swap("glow", "v") }, ],});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.