Tessarune
Arcane Curiosities Vol.1 · crystal_ball.js

水晶玉

32×48 · JavaScript 184 行

32×48 の水晶玉で、真鍮の爪の台に載っています。見下ろし 3/4 視点、左上からの光です。16 コマのループで、ガラスの中の霧が 2 本の腕のように渦を巻いて半回転し、目が一つ開いてまた霧に溶け、光の点が一度きらめきます。光の色は階調を一つ差し替えて青・紫・緑になり、新しい色は足していません。

水晶玉 — 描画結果6×
このプログラムで描画(変種「idle-violet」) · 32×48 · 16 コマ
Arcane Curiosities Vol.1動くスプライト素材: Arcane Curiosities Vol.1(itch.io)動く小物 12 点・52 アニメ

冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。

crystal_ball.js · 1–4
// 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).

最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。

crystal_ball.js · 175–184
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") },  ],});
プログラム全体を表示 (184 行)
crystal_ball.js · 184 行
// 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") },  ],});

掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。

エージェントに、こうしたプログラムを書かせる。

Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。