Tessarune
Tavern & Market · anvil.js

金床

48×48 · JavaScript 263 行

48×48 の床置きの小物で、見下ろし 3/4 視点、左上からの光です。裾の広がった樫の切り株の上に、左に角、右のかかとに角穴のあるロンドン型の金床があり、やっとこが立てかけてあります。面の上の鉄の棒は左端が冷えた灰色、右の先が白熱していて、その暖かい光が鋼の面にたまります。idle(待機、12 コマのループ)は熱い芯が伸び縮みして陽炎が立ちます。strike(8 コマの一回再生)は上から大槌が落ちて当たる瞬間に光り、火花が飛び散って、槌が持ち上がって去ります。

金床 — 描画結果6×
このプログラムで描画(変種「strike」) · 48×48 · 8 コマ
Tavern & Market動くスプライト素材: Tavern & Market(itch.io)Vol.3・酒場と市場の小物 12 点・32 アニメ

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

anvil.js · 1–7
// Blacksmith's anvil on a tree stump — 48x48 floor prop, top-down 3/4, key light top-left.// A London-pattern anvil (horn left, heel with a hardy hole right) stands on a flared oak stump; a bar lies across// the face, cold grey at the left end and white-hot at the right tip. Tongs lean on the stump. Warm light from the bar pools on the steel face around it.// Variants://   idle    loop 12f     — the bar breathes (the hot core grows and shrinks), the glow pool follows, heat shimmer rises//   strike  one-shot 8f  — a sledge drops in from the top, hits (flash), a burst of sparks flies out and falls,//                          the sledge lifts away; the last frame == idle frame 0

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

anvil.js · 256–263
submit(Anvil, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "strike", args: ["strike"], animate: { fps: 10, duration: N_STRIKE / 10, loop: false } },  ],});
プログラム全体を表示 (263 行)
anvil.js · 263 行
// Blacksmith's anvil on a tree stump — 48x48 floor prop, top-down 3/4, key light top-left.// A London-pattern anvil (horn left, heel with a hardy hole right) stands on a flared oak stump; a bar lies across// the face, cold grey at the left end and white-hot at the right tip. Tongs lean on the stump. Warm light from the bar pools on the steel face around it.// Variants://   idle    loop 12f     — the bar breathes (the hot core grows and shrinks), the glow pool follows, heat shimmer rises//   strike  one-shot 8f  — a sledge drops in from the top, hits (flash), a burst of sparks flies out and falls,//                          the sledge lifts away; the last frame == idle frame 0const W = 48, H = 48, CX = 24, GROUND = 45;const N_IDLE = 12, N_STRIKE = 8;const ST = { cx: 24, rx: 12, top: 31, ry: 4, bot: 42, bry: 2.5 };   // stump: top ellipse, bottom ellipseconst FACE = { y0: 15, y1: 19, x0: 13, x1: 37 };                    // anvil working face (top)const BAR = { y: 17, x0: 14, x1: 31 };                              // bar: top row y, front row y + 1const HIT = [27, 17];                                               // where the sledge lands on the bar// ---- stump -------------------------------------------------------------------------------------------------function stump(g) {  const WOOD = ["w0", "w1", "w2", "w3", "w4"];  g.part("stump", () => g.field({ x: [8, 40], y: [ST.top - ST.ry, GROUND] }, (x, y) => {    const flare = y > 38 ? (y - 38) * 0.45 : 0;                   // the base flares into roots    const rx = ST.rx + flare, dx = x + 0.5 - ST.cx, nx = dx / rx;    if (Math.abs(nx) > 1) return null;    const s = Math.sqrt(1 - nx * nx);    if (y + 0.5 < ST.top - ST.ry * s) return null;    if (y + 0.5 > ST.bot + ST.bry * s + 0.6) return null;    // three root lobes break the bottom outline    if (y >= 42 && !(Math.abs(dx + 9) < 3.2 || Math.abs(dx - 2) < 2.6 || Math.abs(dx - 10.5) < 2.8) && Math.abs(nx) > 0.55) return null;    // bark: vertical ridges and grooves; sampled on the cylinder's angle so they crowd together at the sides    let k = nx < -0.55 ? 2 : nx < 0.3 ? 1 : 0;    const a = Math.asin(nx) * 3.0 + 0.4, plate = Math.floor(a), fr = a - plate;    if (fr < 0.25) k = Math.min(4, k + 1);                        // each bark plate's lit left edge    else if (fr > 0.8) k = Math.max(0, k - 1);                    // groove between plates    else if (y === 34 + ((plate * 5 + 20) % 6)) k = Math.max(0, k - 1);   // one cross-crack per plate    if (nx < -0.78 && fr < 0.25 && y > 32 && y < 41) k = 4;      // lit ridges on the left flank    if (y + 0.5 > ST.bot + ST.bry * s - 1.2) k = Math.max(0, k - 1); // foot of the stump in shadow    return WOOD[k];  }));  // end grain on top: rings, a lit cut edge top-left, a split running to the front  g.part("stumpTop", () => g.field({ x: [ST.cx - ST.rx, ST.cx + ST.rx], y: [ST.top - ST.ry, ST.top + ST.ry] }, (x, y) => {    const nx = (x + 0.5 - ST.cx) / ST.rx, ny = (y + 0.5 - ST.top) / ST.ry;    const r = Math.sqrt(nx * nx + ny * ny);    if (r > 1) return null;    if (r > 0.9) return -nx - 0.6 * ny > 0.3 ? "w2" : "w1";         // bark rim    if (r > 0.76) return -nx - ny > -0.2 ? "w4" : "w3";             // pale sapwood ring, brightest top-left    if (Math.abs(r - 0.5) < 0.07 || Math.abs(r - 0.25) < 0.08) return "w2";   // growth rings    if (ny > 0.2 && Math.abs(nx - 0.3 - ny * 0.15) < 0.06) return "w1";       // a drying split    return "w3";  }));}// ---- anvil -------------------------------------------------------------------------------------------------// row spans [x0, x1] per screen row for the body below the faceconst SPANS = {  20: [13, 37], 21: [13, 37], 22: [15, 35], 23: [17, 33],            // body front, underside slopes in at both ends  24: [19, 29], 25: [20, 28], 26: [20, 28], 27: [19, 29],            // waist  28: [17, 31], 29: [16, 32], 30: [15, 33], 31: [15, 33], 32: [14, 34], // foot (top row + front)};function hornEdges(x) {  const u = (FACE.x0 - x) / 10;                                     // 0 at the body, 1 at the tip  return [FACE.y0 + 2.3 * Math.pow(u, 1.4), 22 - 4.6 * Math.pow(u, 0.8)];}function anvilKey(x, y, heat) {  // horn: a cone tapering left, lit along its upper flank  if (x < FACE.x0 && x >= 3) {    const [yt, yb] = hornEdges(x + 0.5);    if (y + 0.5 < yt || y + 0.5 > yb + 0.9) return null;    const v = (y + 0.5 - yt) / (yb + 0.9 - yt);    if (x <= 4) return v < 0.5 ? "n3" : "n2";    return v < 0.22 ? "n4" : v < 0.45 ? "n3" : v < 0.78 ? "n2" : "n1";  }  if (y >= FACE.y0 && y <= FACE.y1) {    if (x < FACE.x0 || x > FACE.x1) return null;    if ((x === 34 || x === 35) && y === 16) return "ink";            // hardy hole    if ((x === 34 || x === 35) && y === 17) return x === 34 ? "ink" : "n1";    if (x === 31 && y === 16) return "n1";                           // pritchel hole    if (y === FACE.y0) return x < 31 ? "n4" : "n3";                  // lit back edge    if (x === FACE.x0) return "n4";    if (x === FACE.x1) return "n2";    // the bar's glow pools on the steel just in front of / behind it    const T = barTemp(x, heat);    if (y === BAR.y + 2 && x >= BAR.x0 && x <= BAR.x1 + 1) return T > 0.85 ? "r2" : T > 0.55 ? "r1" : T > 0.2 ? "n2" : "n1";    if (y === BAR.y - 1 && x >= BAR.x0 && x <= BAR.x1 && T > 0.8) return "r1";    if (y === 16 && x >= 15 && x <= 18) return "n4";                 // polished wear on the face    return "n3";  }  const sp = SPANS[y];  if (!sp || x < sp[0] || x > sp[1]) return null;  if (y >= 31 && x >= 21 - (y - 31) && x <= 27 + (y - 31)) return null;   // arch cut out of the foot  if (y === 20) {                                                    // front bevel catches light (and the glow)    const T = barTemp(x, heat);    return x > 33 ? "n2" : T > 0.88 && heat > 0.5 ? "r1" : "n3";  }  if (y <= 23) {    if (x >= sp[1] - (y === 23 ? 1 : 0) || y === 23) return x > 28 ? "n0" : "n1";    return x < 20 ? "n3" : x < 29 ? "n2" : "n1";  }  if (y === 24) return x < 22 ? "n1" : "n0";                         // waist: in the body's shadow  if (y <= 27) return x <= sp[0] ? "n2" : x < 26 ? "n1" : "n0";  if (y === 28) return x < 21 ? "n4" : x < 29 ? "n3" : "n2";         // foot top  if (y === 32) return x < 22 ? "n1" : "n0";  return x <= sp[0] + 1 ? "n3" : x < 22 ? "n2" : x < 30 ? "n1" : "n0";}function anvil(g, heat) {  g.part("anvil", () => g.field({ x: [3, 37], y: [14, 32] }, (x, y) => anvilKey(x, y, heat)));}// ---- the bar -----------------------------------------------------------------------------------------------// temperature 0..1 along the bar (hot at the right tip); heat (0..1) breathes itfunction barTemp(x, heat) {  const T = Math.max(0, Math.min(1, (x - 17) / 10));  return T * (0.84 + 0.26 * heat);}const TOP_RAMP = ["n2", "r0", "r1", "r2", "r3", "g3"];const FRONT_RAMP = ["n1", "r0", "r0", "r1", "r2", "r3"];function tempIdx(T) { return T < 0.12 ? 0 : T < 0.25 ? 1 : T < 0.42 ? 2 : T < 0.62 ? 3 : T < 0.85 ? 4 : 5; }function bar(g, heat, flash) {  g.part("bar", () => {    for (let x = BAR.x0; x <= BAR.x1; x++) {      const i = tempIdx(barTemp(x, heat));      let top = TOP_RAMP[i], front = FRONT_RAMP[i];      if (flash && i >= 3) { top = i >= 4 ? "n5" : "g3"; front = "r3"; }      if (x === BAR.x0) top = "n3";                                  // lit cut end of the cold stock      g.pixel(x, BAR.y, top);      g.pixel(x, BAR.y + 1, front);    }    g.pixel(BAR.x1 + 1, BAR.y + 1, flash ? "r3" : "r2");             // the tip's end face  });}// ---- tongs ----------------------------------------------------------------------------function tongs(g) {  g.part("tongs", () => {    // two reins splaying to the floor from the pivot, lit / shaded    g.line([36, 31], [37, 44], "n3");    g.line([37, 31], [42, 44], "n1");    g.pixel(37, 38, "n2"); g.pixel(40, 39, "n0");    g.rect({ x: [36, 37], y: [29, 30] }, "n2", { fill: "n2" });    g.pixel(36, 29, "n4");                                           // rivet    g.line([36, 28], [35, 25], "n3"); g.line([37, 28], [38, 25], "n1"); // jaws    g.pixel(36, 24, "n3"); g.pixel(37, 24, "n1");  });}// ---- effects -----------------------------------------------------------------------------------------------function shimmer(g, t) {  g.detached("shimmer", () => {    for (let i = 0; i < 4; i++) {      const p = (t + i / 4) % 1;      const x0 = [25, 28, 30, 26][i] + Math.round(Math.sin(TAU * (p * 1.5 + i * 0.37)) * 1.2);      const y0 = BAR.y - 3 - Math.round(p * 11);      if (p < 0.3) { g.pixel(x0, y0, "r2"); g.pixel(x0, y0 + 1, "r1"); }      else if (p < 0.6) { g.pixel(x0, y0, "r1"); if (chk(x0, y0)) g.pixel(x0 + 1, y0 + 1, "r0"); }      else if (p < 0.85) g.pixel(x0, y0, "r0");    }  });}function embers(g, t) {  g.detached("embers", () => {    for (let k = 0; k < 2; k++) {      const p = (t + k * 0.5) % 1;      if (p > 0.55) continue;      const x = 28 + k * 2 + Math.round(Math.sin(TAU * (p + k * 0.3)) * 2), y = BAR.y - 2 - Math.round(p * 14);      g.pixel(x, y, p < 0.25 ? "g3" : "g2");    }  });}// the smith's sledge, entering from the top of the frame; yb = bottom row of the striking facefunction sledge(g, yb) {  // a double-faced sledge held head-down; its haft leaves the head's right cheek and runs up-right out of frame  // (the smith stands behind and to the right)  const hx0 = 23, hx1 = 31, HH = 10;  g.detached("sledge", () => g.outlined("ink", () => {    const hy = yb - 5.5;                                           // eye height    if (hy > 2) g.sweep([[30.5, hy], [30.5 + 0.62 * (hy - 1.5), 1.5]], { w0: 1.5, w1: 1.5 },      (u, v) => (Math.abs(v) < 0.34 ? "w2" : (v < 0) ? "w3" : "w1"));    g.field({ x: [hx0, hx1], y: [yb - HH, yb] }, (x, y) => {      if (y < 0) return null;      const r = yb - y;                                            // 0 = striking face      const flare = r <= 1 || r >= HH - 1;      const x0 = flare ? hx0 : hx0 + 1, x1 = flare ? hx1 : hx1 - 1;      if (x < x0 || x > x1) return null;      if (r === HH) return x < 30 ? "n4" : "n3";                   // top face      if (r === HH - 1 || r === 1) return x < 25 ? "n4" : x < 29 ? "n3" : "n2";   // flared chamfers      if (r === 0) return x < 28 ? "n2" : "n1";      if (r === 2 || r === HH - 2) return x < 25 ? "n2" : "n1";    // neck under each flare      return x <= x0 ? "n4" : x < 27 ? "n3" : x < 29 ? "n2" : "n1"; // cheek, lit left    });    if (yb - 6 >= 0) { g.pixel(25, yb - 6, "n5"); g.pixel(25, yb - 5, "n4"); }   // specular on the cheek  }));}function smear(g, yb) {  g.detached("smear", () => {    for (let y = Math.max(0, yb - 17); y < yb - 11; y++) {      if (chk(22, y)) g.pixel(22, y, "n3");      if (chk(25, y)) g.pixel(25, y, "n4");    }  });}function flashStar(g) {  // the flash squirts out sideways from under the sledge's face, along the bar  const [x, y] = HIT;  g.detached("flash", () => {    for (const [dx, dy, k] of [[-5, -1, "n5"], [5, -1, "n5"], [-6, -1, "g3"], [6, -1, "g3"], [-7, -1, "g3"], [7, -1, "g3"],      [-9, -1, "g2"], [9, -1, "g2"], [-6, -3, "g3"], [6, -3, "g3"], [-7, -4, "g2"], [7, -4, "g2"], [-5, 1, "g3"], [5, 1, "g3"],      [-7, 2, "r3"], [7, 2, "r3"]]) g.pixel(x + dx, y + dy, k);  });}const SPARKS = [  [-3.2, -2.0], [-2.3, -3.1], [-1.0, -3.8], [0.5, -4.1], [1.8, -3.4], [3.1, -2.6], [3.9, -1.1],  [-3.9, -0.7], [-2.7, 0.3], [2.9, 0.2], [1.1, -2.2], [-1.6, -1.7], [4.4, -2.0], [-4.3, -1.8],];function sparks(g, k) {  g.detached("sparks", () => {    SPARKS.forEach(([vx, vy], i) => {      if (k === 3 && i % 3 === 0) return;                          // some burn out      const sp = 1.55 + (i % 3) * 0.25;      const x = Math.round(HIT[0] + vx * sp * k), y = Math.round(HIT[1] - 1 + vy * sp * k + 0.75 * k * k);      const tx = Math.round(HIT[0] + vx * sp * (k - 0.45)), ty = Math.round(HIT[1] - 1 + vy * sp * (k - 0.45) + 0.75 * (k - 0.45) * (k - 0.45));      const head = k === 1 ? "g3" : k === 2 ? (i % 2 ? "g3" : "g2") : i % 2 ? "r3" : "r2";      const tail = k === 1 ? "g2" : k === 2 ? "r3" : "r1";      const put = (px, py, key) => { if (px >= 0 && py >= 0 && px < W && py < H) g.pixel(px, py, key); };      if (tx !== x || ty !== y) put(tx, ty, tail);      put(x, y, head);    });  });}// ---- sprite ------------------------------------------------------------------------------------------------function drawAnvil(g, heat, flash) {  contactShadow(g, CX, GROUND, 16, 2);  g.outlined("ink", () => stump(g));  g.outlined("ink", () => anvil(g, heat));  bar(g, heat, flash);  g.outlined("ink", () => tongs(g));}const heatAt = (t) => 0.5 + 0.5 * Math.cos(TAU * t);const Anvil = sprite(pal, (g, t, mode) => {  if (mode === "idle") {    drawAnvil(g, heatAt(t), false);    shimmer(g, t);    embers(g, t);    return;  }  const f = Math.min(N_STRIKE - 1, Math.round(t * N_STRIKE));  if (f === 0 || f === N_STRIKE - 1) { drawAnvil(g, heatAt(0), false); shimmer(g, 0); embers(g, 0); return; }  // 1 raised · 2 falling · 3 impact · 4 rebound · 5 lifting · 6 gone  const YB = [null, 3, 11, 16, 13, 5, null][f];  drawAnvil(g, 1, f === 3);  if (f === 6) { shimmer(g, 0); }  if (f === 2) smear(g, YB);  if (f >= 4) sparks(g, f - 3);  if (YB !== null) sledge(g, YB);  if (f === 3) flashStar(g);});submit(Anvil, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  anchor: { x: CX, y: GROUND },  variants: [    { name: "idle", args: ["idle"], animate: { fps: 10, duration: N_IDLE / 10, loop: true } },    { name: "strike", args: ["strike"], animate: { fps: 10, duration: N_STRIKE / 10, loop: false } },  ],});

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

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

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