Tessarune
Arcane Curiosities Vol.1 · potion_shelf.js

薬棚

64×64 · JavaScript 330 行

64×64 の壁掛けの小物で、正面から見て棚板の上面だけを奥行きをつけて描いています。2 枚の側板の間に 2 段の棚があり、棚板の下に真鍮の支えが付き、金色の小瓶や口の開いた緑のフラスコから、髑髏の栓の青い瓶、取っ手の付いた赤い壺まで 7 つの違うガラスの器が並びます。壁掛けなので接地影はなく、かわりに棚板の下の壁にディザの細い影が落ちます。16 コマのループで、光が器から器へ左から右へ渡り、フラスコが泡立って湯気を上げ、髑髏の瓶の中身が明るくふくらんで目の穴が光り、また暗くなります。

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

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

potion_shelf.js · 1–7
// Potion shelf — 64x64 wall prop, front view with the plank tops foreshortened (3/4 top-down). Key light top-left.// A two-tier shelf between two side boards, brass gussets under each plank, seven distinct glass vessels://   top:    gold vial · green round flask (open, bubbling) · violet heart bottle · red + blue test tubes in a rack//   bottom: green herb jar (cork, label) · blue skull-stoppered Erlenmeyer (soul-light) · red squat jug with handle// Wall prop: no contact shadow — a thin dithered shadow line falls on the wall under each plank instead.// Loop: 16 frames @ 10 fps. A glint sweeps left to right from vessel to vessel, the flask bubbles and steams,// the skull bottle's liquid swells bright (its eye sockets light up) and dims.

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

potion_shelf.js · 325–330
submit(Shelf, {  size: [64, 64], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: 32, y: 61 },  variants: [{ name: "idle" }],});
プログラム全体を表示 (330 行)
potion_shelf.js · 330 行
// Potion shelf — 64x64 wall prop, front view with the plank tops foreshortened (3/4 top-down). Key light top-left.// A two-tier shelf between two side boards, brass gussets under each plank, seven distinct glass vessels://   top:    gold vial · green round flask (open, bubbling) · violet heart bottle · red + blue test tubes in a rack//   bottom: green herb jar (cork, label) · blue skull-stoppered Erlenmeyer (soul-light) · red squat jug with handle// Wall prop: no contact shadow — a thin dithered shadow line falls on the wall under each plank instead.// Loop: 16 frames @ 10 fps. A glint sweeps left to right from vessel to vessel, the flask bubbles and steams,// the skull bottle's liquid swells bright (its eye sockets light up) and dims.const FR = 16;const frameOf = (t) => Math.round(t * FR) % FR;const clamp = (v, a, b) => Math.max(a, Math.min(b, v));const inBox = (x, y, x0, x1, y0, y1) => x >= x0 && x <= x1 && y >= y0 && y <= y1;function inEll(x, y, x0, x1, y0, y1) {  const nx = (2 * (x - x0) + 1) / (x1 - x0 + 1) - 1, ny = (2 * (y - y0) + 1) / (y1 - y0 + 1) - 1;  return nx * nx + ny * ny <= 1;}// ---- glass ---------------------------------------------------------------------------------------------// The glint sweeps the vessels in screen order (left to right across both tiers), 2 frames apart, 3 frames each.const SWEEP = ["vial", "jar", "flask", "skull", "heart", "jug", "tubes"];function glintStep(name, t) {  const k = frameOf(t) - 2 * SWEEP.indexOf(name);  return k >= 0 && k <= 2 ? k : -1;}const soul = (t) => 0.5 - 0.5 * Math.cos(TAU * t);                 // 0 -> 1 -> 0 over the loopconst soulLevel = (t) => (soul(t) > 0.72 ? 1 : soul(t) < 0.28 ? -1 : 0);// One glass vessel as ONE tone expression: dark glass rim, liquid under a bright liquid line, a specular// column on the lit (left) side, refracted light pooling low on the far side, and the travelling glint band.function vessel(g, V, t) {  const lv = V.level ? V.level(t) : 0;  const R = (i) => V.ramp + clamp(i + lv, 0, 3);  const k = glintStep(V.name, t);  const [x0, x1] = V.box.x, [y0, y1] = V.box.y;  const a = x0 + 0.4 * y0, b = x1 + 0.4 * y1;  const band = k < 0 ? null : a + (0.2 + 0.3 * k) * (b - a);  const I = V.inside;  g.field(V.box, (x, y) => {    if (!I(x, y)) return null;    const eL = !I(x - 1, y), eR = !I(x + 1, y), eT = !I(x, y - 1), eB = !I(x, y + 1);    const edge = eL || eR || eT || eB;    const solid = V.solid && V.solid(x, y);    const wet = y > V.liq && !solid;    if (!edge && band !== null && Math.abs(x + 0.4 * y - band) < 0.5) return "n5";    if (!edge && V.spec(x, y)) return wet ? "n5" : "n4";    if (wet) {      if (eR || eB) return R(0);      if (V.lite && V.lite(x, y)) return R(3);      if (V.hot && V.hot(x, y)) return R(2);      return R(1);    }    if (y === V.liq && !solid) return edge ? R(1) : R(3);   // the liquid line    return edge ? "n1" : "n2";                              // empty glass: dark rim, smoky interior  });}function lipRow(g, x0, x1, y) {           // thick glass lip: lit on the left, shaded on the right  for (let x = x0; x <= x1; x++) g.pixel(x, y, x === x0 + 1 ? "n4" : x === x1 ? "n1" : x === x1 - 1 ? "n2" : "n3");}function cork(g, x0, x1, y0, y1) {  g.rect({ x: [x0, x1], y: [y0, y1] }, "w2", { fill: "w2" });  g.line([x0, y0], [x1, y0], "w3");         // top face  g.pixel(x0, y0, "w4");  g.line([x0, y0 + 1], [x0, y1], "w3");  g.line([x1, y0 + 1], [x1, y1], "w1");}function sparkle(g, x, y, k) {  if (k === 1) {    g.pixel(x, y, "n5");    for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) g.pixel(x + dx, y + dy, "n4");  } else if (k === 2) g.pixel(x, y, "n5");}// refracted light: the ring of pixels one step inside the far rim of a round vesselconst rimNear = (I, x, y) => !I(x + 2, y) || !I(x, y + 2) || !I(x + 1, y + 1);// ---- the seven vessels ---------------------------------------------------------------------------------const VIAL = {  name: "vial", ramp: "g", box: { x: [7, 11], y: [8, 25] }, liq: 12, spark: [11, 9],  inside: (x, y) => inBox(x, y, 8, 10, 8, 9) || inBox(x, y, 7, 11, 10, 24) || inBox(x, y, 8, 10, 25, 25),  spec: (x, y) => x === 8 && y >= 10 && y <= 21,  hot: (x, y) => x === 10 && y >= 20,  dress: (g) => { lipRow(g, 7, 11, 7); cork(g, 8, 10, 4, 6); },};const FLASK = {  name: "flask", ramp: "e", box: { x: [15, 25], y: [10, 25] }, liq: 18, spark: [24, 15],  inside: (x, y) => inEll(x, y, 15, 25, 15, 25) || inBox(x, y, 19, 21, 10, 15),  spec: (x, y) => (x === 16 && y >= 19 && y <= 21) || (x === 17 && y === 17),  hot: (x, y) => x >= 20 && y >= 20 && rimNear(FLASK.inside, x, y),  dress: (g) => {    // flared open mouth seen slightly from above: far rim, dark opening, near rim    g.line([19, 7], [21, 7], "n3");    g.pixel(18, 8, "n3"); g.line([19, 8], [21, 8], "e0"); g.pixel(22, 8, "n1");    lipRow(g, 18, 22, 9);  },};const heartHalf = (y) => (y <= 18 ? 6 : 6 - (y - 18) * 0.6);const HEART = {  name: "heart", ramp: "v", box: { x: [29, 41], y: [11, 25] }, liq: 15, spark: [40, 14],  inside: (x, y) =>    inBox(x, y, 34, 36, 11, 15) || inEll(x, y, 29, 33, 15, 20) || inEll(x, y, 37, 41, 15, 20) ||    (y >= 16 && y <= 25 && Math.abs(x - 35) <= Math.max(1, heartHalf(y))),  spec: (x, y) => (x === 30 && y >= 18 && y <= 19) || (x === 31 && (y === 17 || y === 20)),  hot: (x, y) => x >= 36 && y >= 21,  lite: (x, y) => (x === 38 && y === 17) || (y === 16 && (x === 31 || x === 32)),  dress: (g) => {    lipRow(g, 33, 37, 10);    // round glass stopper, the heart's colour made pale    g.ellipse({ x: [33, 37], y: [6, 9] }, "v2", { fill: "v2" });    g.pixel(34, 6, "v3"); g.pixel(33, 7, "v3"); g.pixel(34, 7, "n5");    g.pixel(37, 8, "v1"); g.pixel(36, 9, "v1");  },};const TUBE_A = {  name: "tubes", ramp: "r", box: { x: [46, 49], y: [10, 21] }, liq: 13,  inside: (x, y) => inBox(x, y, 46, 49, 10, 21),  spec: (x, y) => x === 47 && y >= 11 && y <= 19,  hot: (x, y) => x === 48 && y >= 18,};const TUBE_B = {  name: "tubes", ramp: "b", box: { x: [51, 54], y: [12, 21] }, liq: 16,  inside: (x, y) => inBox(x, y, 51, 54, 12, 21),  spec: (x, y) => x === 52 && y >= 13 && y <= 19,  hot: (x, y) => x === 53 && y >= 18,};const TUBES = { name: "tubes", spark: [55, 14] };const JAR = {  name: "jar", ramp: "e", box: { x: [7, 17], y: [41, 53] }, liq: 45, spark: [16, 44],  inside: (x, y) => inBox(x, y, 9, 15, 41, 42) || inBox(x, y, 8, 16, 43, 53) || inBox(x, y, 7, 17, 44, 52),  spec: (x, y) => x === 8 && y >= 44 && y <= 51,  hot: (x, y) => x === 16 && y >= 49,  dress: (g) => {    lipRow(g, 8, 16, 40);    cork(g, 9, 15, 37, 39);    g.line([9, 42], [15, 42], "w1"); g.pixel(15, 42, "w0"); g.pixel(16, 43, "w1");   // twine and its tail    // herbs steeping in the brew    for (const [x, y] of [[10, 51], [11, 52], [14, 52], [15, 51], [12, 46]]) g.pixel(x, y, "e0");    // parchment label with two lines of ink    g.rect({ x: [10, 14], y: [47, 50] }, "n4", { fill: "n4" });    g.line([10, 47], [13, 47], "n5");    g.line([14, 48], [14, 50], "n3"); g.line([10, 50], [13, 50], "n3");    g.line([11, 48], [13, 48], "n1"); g.line([11, 49], [12, 49], "n1");  },};const coneHalf = (y) => (y >= 52 ? 6 : 2 + Math.floor((y - 44) / 2));const SKULL = {  name: "skull", ramp: "b", box: { x: [23, 35], y: [41, 53] }, liq: 47, spark: [33, 46], level: soulLevel,  inside: (x, y) => inBox(x, y, 28, 30, 41, 43) || (y >= 44 && y <= 53 && Math.abs(x - 29) <= coneHalf(y) - (y === 53 ? 1 : 0)),  spec: (x, y) => y >= 45 && y <= 51 && x === 29 - coneHalf(y) + 1,  hot: (x, y) => x >= 31 && y >= 51,  dress: (g, t) => {    lipRow(g, 27, 31, 40);    // bone stopper: domed cranium, two sockets, nose, jaw with teeth — lit from the top-left    g.line([28, 33], [30, 33], "n4");    g.line([27, 34], [31, 34], "n4");    g.rect({ x: [26, 32], y: [35, 37] }, "n4", { fill: "n4" });    g.rect({ x: [27, 31], y: [38, 39] }, "n4", { fill: "n4" });    g.line([28, 33], [29, 33], "n5"); g.line([27, 34], [28, 34], "n5"); g.pixel(26, 35, "n5");    g.pixel(31, 34, "n3"); g.line([32, 35], [32, 37], "n3"); g.line([31, 38], [31, 39], "n3");    const lv = soulLevel(t);    const eye = lv > 0 ? "b3" : lv === 0 ? "b1" : "ink";    for (const ex of [27, 30]) g.rect({ x: [ex, ex + 1], y: [36, 37] }, eye, { fill: eye });    if (lv > 0) { g.pixel(28, 37, "b2"); g.pixel(31, 37, "b2"); }    g.pixel(29, 38, "ink");    g.pixel(28, 39, "n2"); g.pixel(30, 39, "n2");  },};const JUG = {  name: "jug", ramp: "r", box: { x: [40, 52], y: [41, 53] }, liq: 45, spark: [50, 43],  inside: (x, y) => inBox(x, y, 44, 48, 41, 42) || (y <= 53 && inEll(x, y, 40, 52, 43, 55)),  spec: (x, y) => (x === 41 && y >= 47 && y <= 50) || (x === 42 && y === 46),  hot: (x, y) => x >= 46 && y >= 48 && rimNear(JUG.inside, x, y),  dress: (g) => {    lipRow(g, 43, 49, 40);    cork(g, 44, 48, 37, 39);    // glass loop handle on the right    g.line([52, 45], [55, 45], "n3"); g.pixel(53, 45, "n4");    g.line([56, 46], [56, 50], "n2");    g.line([53, 51], [55, 51], "n1");  },};function drawVessel(g, V, t) {  g.depth(-1, () => g.outlined("ink", () => g.part(V.name, () => { vessel(g, V, t); V.dress(g, t); })));}function tubes(g, t) {  g.depth(-1, () => g.outlined("ink", () => g.part("tubes", () => {    g.line([45, 20], [55, 20], "w1");                 // far half of the rack's top, behind the tubes    vessel(g, TUBE_A, t); vessel(g, TUBE_B, t);    cork(g, 46, 49, 7, 9); cork(g, 51, 54, 9, 11);    // rack: near half of the top, then the front face    g.line([45, 21], [55, 21], "w3"); g.line([45, 21], [49, 21], "w4");    g.rect({ x: [45, 55], y: [22, 25] }, "w2", { fill: "w2" });    g.line([45, 22], [45, 25], "w3");    g.line([45, 25], [55, 25], "w1");    g.pixel(50, 23, "g2"); g.pixel(51, 23, "g1");         // brass pin  })));}// ---- animated touches ---------------------------------------------------------------------------------function bubbles(g, t) {  g.depth(-2, () => g.part("bubbles", () => {    for (const [x, ph] of [[18, 0], [21, 0.37], [23, 0.7]]) {      const p = (t * 2 + ph) % 1;      const y = 24 - Math.floor(p * 7);                    // 24 -> 18, the liquid line      const bx = x + (y % 3 === 0 ? 1 : 0) - (x === 23 && y >= 23 ? 1 : 0);      if (y > 20) g.pixel(bx, y, "e3");                          // small bubble leaving the bottom      else if (y > 18) { g.rect({ x: [bx, bx + 1], y: [y, y + 1] }, "e2", { fill: "e2" }); g.pixel(bx, y, "e3"); }      else { g.pixel(bx, 17, "e3"); g.pixel(bx - 1, 16, "e2"); g.pixel(bx + 2, 16, "e2"); }   // pop    }  }));  g.detached("vapor", () => {    for (let i = 0; i < 2; i++) {      const p = (t * 2 + i / 2) % 1;      const x = 20 + Math.round(Math.sin(TAU * (p * 0.8 + i * 0.5)) * 1.5);      const y = 5 - Math.round(p * 5);      if (p < 0.4) { g.pixel(x, y, "e3"); g.pixel(x + 1, y, "e2"); }      else if (p < 0.75) g.pixel(x, y, "e2");      else if (chk(x, frameOf(t))) g.pixel(x, y, "e1");    }  });}function glints(g, t) {  g.depth(-2, () => g.detached("glint", () => {    for (const V of [VIAL, JAR, FLASK, SKULL, HEART, JUG, TUBES]) {      const k = glintStep(V.name, t);      if (k > 0) sparkle(g, V.spark[0], V.spark[1], k);    }  }));}function soulLight(g, t) {  const lv = soulLevel(t);  // soul sparks drift up through the liquid while it is lit  g.depth(-2, () => g.part("sparks", () => {    if (lv < 0) return;    for (const [x, ph] of [[27, 0], [30, 0.5]]) {      const p = (t * 2 + ph) % 1;      const y = 52 - Math.floor(p * 5);                       // 52 -> 48, just under the liquid line      g.pixel(x + (y % 2), y, lv > 0 ? "n5" : "b3");    }  }));  // light pooling on the plank top either side of the base  if (lv < 1) return;  g.depth(-2, () => g.part("spill", () => {    g.line([20, 53], [22, 53], "b1"); g.pixel(22, 54, "b2"); g.pixel(21, 54, "b1");    g.line([36, 53], [38, 53], "b1"); g.pixel(36, 54, "b2"); g.pixel(37, 54, "b1");  }));}// ---- the shelf ----------------------------------------------------------------------------------------function sideBoard(g, x0, name) {  g.part(name, () => {    g.rect({ x: [x0, x0 + 2], y: [7, 60] }, "w2", { fill: "w2" });    g.line([x0, 7], [x0, 60], "w3");    g.line([x0 + 2, 7], [x0 + 2, 60], "w1");    g.pixel(x0 + 1, 61, "w1");                            // tapered foot    for (const y of [15, 16, 40, 41, 42]) g.pixel(x0 + 1, y, "w1");   // grain    // brass finial    g.pixel(x0 + 1, 3, "g2");    g.pixel(x0, 4, "g3"); g.pixel(x0 + 1, 4, "g2"); g.pixel(x0 + 2, 4, "g1");    g.pixel(x0 + 1, 5, "g1");    g.line([x0, 6], [x0 + 2, 6], "g0");    // brass nail heads where the planks are housed    g.pixel(x0 + 1, 28, "g2"); g.pixel(x0 + 1, 56, "g2");  });}function plank(g, y0, name, casts) {  g.part(name, () => {    g.line([5, y0], [58, y0], "w2");                                     // back strip, shaded by the wall    g.rect({ x: [5, 58], y: [y0 + 1, y0 + 2] }, "w3", { fill: "w3" });  // top face    g.line([5, y0 + 2], [22, y0 + 2], "w4");                             // front edge catches the key light    g.rect({ x: [5, 58], y: [y0 + 3, y0 + 5] }, "w2", { fill: "w2" });  // front face    g.line([5, y0 + 5], [58, y0 + 5], "w1");    for (const [a, b, r] of [[9, 13, 4], [21, 24, 3], [34, 39, 4], [48, 51, 3]]) g.line([a, y0 + r], [b, y0 + r], "w1");    g.pixel(29, y0 + 4, "w0"); g.pixel(28, y0 + 4, "w1"); g.pixel(30, y0 + 4, "w1");   // knot    // each vessel casts a short shadow back-right across the plank top (vessels occlude the rest)    for (const xr of casts) { g.line([xr + 1, y0 + 1], [xr + 3, y0 + 1], "w2"); g.pixel(xr + 1, y0 + 2, "w2"); }  });}function gusset(g, xw, y, dir, name) {  // brass bracket under a plank, fixed to the side board: a right triangle with a lit top edge and a screw  g.part(name, () => {    for (let i = 0; i < 4; i++) g.line([xw, y + i], [xw + dir * (3 - i), y + i], "g1");    g.line([xw, y], [xw + dir * 3, y], "g2");    if (dir > 0) { g.line([xw, y], [xw, y + 3], "g2"); g.pixel(xw, y, "g3"); }    for (let i = 0; i < 4; i++) g.pixel(xw + dir * (3 - i), y + i, dir > 0 ? "g0" : "g1");    g.pixel(xw + dir, y + 1, "g0");  });}function wallShadow(g) {  // the planks cast a thin line of shadow on the wall (drawn behind everything: lands only on bare wall)  g.depth(1, () => g.part("wallShadow", () => {    for (const y of [31, 59]) for (let x = 6; x <= 59; x++) if (chk(x, y)) g.pixel(x, y, "ink");  }));}const Shelf = sprite(pal, (g, t) => {  for (const V of [VIAL, FLASK, HEART, JAR, SKULL, JUG]) drawVessel(g, V, t);  tubes(g, t);  bubbles(g, t);  glints(g, t);  g.outlined("ink", () => {    sideBoard(g, 1, "boardL");    sideBoard(g, 60, "boardR");    plank(g, 24, "plankTop", [12, 23, 37, 56]);    plank(g, 52, "plankBottom", [17, 35, 52]);    gusset(g, 5, 30, 1, "gussetTL"); gusset(g, 58, 30, -1, "gussetTR");    gusset(g, 5, 58, 1, "gussetBL"); gusset(g, 58, 58, -1, "gussetBR");  });  soulLight(g, t);  wallShadow(g);});submit(Shelf, {  size: [64, 64], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: 1.6, loop: true },  anchor: { x: 32, y: 61 },  variants: [{ name: "idle" }],});

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

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

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