Tessarune
Dungeon Mechanisms · teleport_rune.js

Teleport rune

48×48 · 254 lines of JavaScript

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.

Teleport rune — Rendered output6×
Rendered by this program, variant “activate” · 48×48 · 10 frames
Dungeon MechanismsGet 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.js · 1–8
// 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 column

The last lines. submit declares the canvas and lists the variants the renderer exports.

teleport_rune.js · 241–254
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.js · 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.