Tessarune
Witch's Hut · scarecrow.js

かかし

32×64 · JavaScript 311 行

32×64 の縦長の床置きの小物で、木の柱と横木に組んだ藁人形です。首元を縛った麻袋の頭、赤い帯と真鍮の留め金が付いた先の垂れたとんがり帽子、革と紫の当て布をした色あせた緑の上着、縄のベルトを身に着け、袖口や襟、裾から藁がはみ出しています。柱は草の生えた小さな土盛りに立っています。idle(16 コマ)は風に揺れ、帽子の先が遅れてついてきます。haunted(16 コマ)は彫った目と口が橙色に燃え、火の粉が昇ります。turn(8 コマの一回再生)はゆっくりこちらへ顔を向け、haunted の最初のコマで終わります。

かかし — 描画結果6×
このプログラムで描画(変種「turn」) · 32×64 · 8 コマ
Witch's Hut動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ

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

scarecrow.js · 1–10
// Scarecrow — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A straw man on a wooden post and crossbar: a burlap sack head tied off at the neck with straw bursting out under// the cord, a floppy pointed witch hat (bent tip, red band, brass buckle), a faded green coat with a leather patch// and a violet sleeve patch, a rope belt, straw at the cuffs, the collar, the coat's open front and under the hem.// The post stands in a little mound of earth with grass tufts. No crow.// Variants://   idle     16f loop: sways in the wind — the head rocks, the hat tip lags behind, coat tatters, straw and rope flap//   haunted  16f loop: carved eyes and mouth burn orange (fl family), head held at a tilt, faint glow, embers rise//   turn     8f once:  the head slowly turns from looking aside to facing the viewer, eyes catch and flare;//                      starts on idle frame 0, ends on haunted frame 0

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

scarecrow.js · 301–311
submit(Scarecrow, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "haunted", args: ["haunted"] },    { name: "turn", args: ["turn"], animate: { fps: 10, duration: N_TURN / 10, loop: false } },  ],});
プログラム全体を表示 (311 行)
scarecrow.js · 311 行
// Scarecrow — 32x64 floor prop (tall), top-down 3/4, key light top-left.// A straw man on a wooden post and crossbar: a burlap sack head tied off at the neck with straw bursting out under// the cord, a floppy pointed witch hat (bent tip, red band, brass buckle), a faded green coat with a leather patch// and a violet sleeve patch, a rope belt, straw at the cuffs, the collar, the coat's open front and under the hem.// The post stands in a little mound of earth with grass tufts. No crow.// Variants://   idle     16f loop: sways in the wind — the head rocks, the hat tip lags behind, coat tatters, straw and rope flap//   haunted  16f loop: carved eyes and mouth burn orange (fl family), head held at a tilt, faint glow, embers rise//   turn     8f once:  the head slowly turns from looking aside to facing the viewer, eyes catch and flare;//                      starts on idle frame 0, ends on haunted frame 0const W = 32, H = 64, GROUND = 61, CX = 16;const N_LOOP = 16, N_TURN = 8;const NECK = [16, 24];const COAT = ["e0", "e1", "e2", "e3"], SACK = ["w1", "w2", "w3", "w4"], HAT = ["n0", "v0", "v1", "v2"];const WOOD = ["w0", "w1", "w2", "w3"];// ---- post, crossbar, mound --------------------------------------------------------------------------------------function frame(g) {  g.part("post", () => {    for (let y = 20; y <= 59; y++) { g.pixel(15, y, "w3"); g.pixel(16, y, "w2"); g.pixel(17, y, "w1"); }    for (const y of [48, 49, 53]) g.pixel(16, y, "w1");                      // grain    g.pixel(15, 55, "w2"); g.pixel(17, 51, "w0");  });  g.part("crossbar", () => {    for (let x = 1; x <= 30; x++) { g.pixel(x, 27, x < 16 ? "w3" : "w2"); g.pixel(x, 28, "w1"); }    g.pixel(1, 27, "w4"); g.pixel(1, 28, "w2"); g.pixel(30, 28, "w0");       // cut ends    g.pixel(2, 27, "w4");  });}function mound(g, wind) {  g.part("mound", () => {    g.field({ x: [9, 23], y: [57, 61] }, (x, y) => {      const nx = (x + 0.5 - 16) / 7, ny = (y + 0.5 - 60.5) / 3.5;      if (nx * nx + ny * ny > 1 || y > 61) return null;      const a = -0.8 * nx - 0.6 * ny;      return a > 0.55 ? "w2" : a > -0.1 ? "w1" : "w0";    });    g.pixel(12, 59, "n3"); g.pixel(13, 59, "n2");                             // a pebble    g.pixel(20, 60, "w2");  });  const sw = Math.round(wind * 0.8);  g.part("grass", () => {    for (const [x, h, k] of [[10, 3, "e2"], [11, 4, "e1"], [21, 4, "e2"], [22, 2, "e1"], [19, 2, "e1"]]) {      g.line([x, 60], [x + sw, 60 - h], k);      g.pixel(x + sw, 60 - h, k === "e2" ? "e3" : "e2");    }  });}// ---- coat --------------------------------------------------------------------------------------------------------const torsoL = (y) => 9.5 - (y - 26) * 0.09, torsoR = (y) => 22.5 + (y - 26) * 0.09;function coatAt(x, y, sh) {  // returns "sleeve" | "torso" | "gap" | null  const tip = y >= 44 ? x - sh : x;                                          // hem tatters swing in the wind  if (y >= 26 && y <= 31 && x >= 4 && x <= 28) {    const bottom = x < 7 || x > 25 ? 30 : 31;    if (y <= bottom && (x < 10 || x > 22)) return "sleeve";  }  if (y === 25 && x >= 11 && x <= 21) return "torso";  if (y < 26) return null;  if (x + 0.5 < torsoL(y) || x + 0.5 > torsoR(y) + 1) return null;  const v = ((tip % 4) + 4) % 4, tat = v < 2 ? v : 4 - v;                    // 0..2  if (y > 43 + tat + (tip % 7 === 3 ? 1 : 0)) return null;  if (y >= 37 && Math.abs(x + 0.5 - 16.5) < (y - 36) * 0.38) return "gap";     // the coat falls open  return "torso";}function coat(g, sh, flap) {  g.part("coat", () => g.field({ x: [3, 29], y: [25, 47] }, (x, y) => {    const r = coatAt(x, y, sh);    if (!r || r === "gap") return null;    if (r === "sleeve") {      const left = x < 16;      let k = y <= 26 ? 3 : y <= 28 ? 2 : y <= 29 ? 1 : 0;      if (!left) k = Math.max(0, k - 1);      if (k === 3 && (x > 8 || !left)) k = 2;      if ((x === 4 || x === 28) && y >= 28) k = 0;                            // inside of the cuff      if ((x === 5 || x === 27) && flap > 0.3) k = Math.max(0, k - 1);        // cuffs flutter      return COAT[k];    }    const nx = (x + 0.5 - 16) / 7.5;    const fold = 0.32 * Math.sin(x * 1.35 + 0.6) * (y > 33 ? 1 : 0.4);    let v = -0.95 * nx + fold - (y - 26) / 45;    if (y <= 33 && (x <= 10 || x >= 22)) v -= 0.5;                             // under the arms    if (y === 25 || (y === 26 && x > 10 && x < 15)) v += 0.6;                  // shoulders catch the light    const k = v > 0.55 ? 3 : v > -0.05 ? 2 : v > -0.6 ? 1 : 0;    return COAT[Math.min(k, y <= 27 ? 3 : 2)];  }));  g.part("coatDetail", () => {    for (let y = 26; y <= 36; y++) g.pixel(16, y, y < 30 ? "e1" : "e0");      // the front edge    g.pixel(15, 26, "e0"); g.pixel(17, 26, "e0");    for (const y of [29, 32, 35]) { g.pixel(18, y, "g1"); }                   // buttons    g.pixel(18, 29, "g2");    // leather patch, stitched    g.rect({ x: [11, 13], y: [38, 40] }, "w2", { fill: "w2" });    g.line([11, 38], [12, 38], "w3"); g.line([13, 39], [13, 40], "w1"); g.pixel(12, 40, "w1");    g.pixel(10, 38, "n4"); g.pixel(14, 41, "n4"); g.pixel(14, 38, "n4"); g.pixel(10, 41, "n3");    // violet patch on the right sleeve    g.rect({ x: [23, 25], y: [27, 29] }, "v1", { fill: "v1" });    g.line([23, 27], [24, 27], "v2"); g.pixel(25, 29, "v0");    g.pixel(22, 27, "n4"); g.pixel(26, 30, "n3"); g.pixel(24, 30, "n3");    // a tear on the left sleeve    g.pixel(7, 29, "e0"); g.pixel(8, 30, "e0");  });}function belt(g, wind) {  g.part("belt", () => {    for (let x = 10; x <= 23; x++) g.pixel(x, 36, x % 2 ? "w3" : "w2");    g.pixel(10, 36, "w4");    g.pixel(19, 36, "w4"); g.pixel(20, 36, "w3"); g.pixel(19, 37, "w2");      // knot    const d = Math.round(wind * 0.9);    g.line([19, 38], [19 + d, 40], "w2");    g.line([20, 37], [21 + d, 39], "w3");  });}// ---- straw ------------------------------------------------------------------------------------------------------// a fan of strands from a root point; tips: [[dx,dy],...]; each strand a line, alternating straw tonesfunction tuft(g, name, root, tips, sway) {  g.part(name, () => {    tips.forEach(([dx, dy], i) => {      const k = i % 3 === 0 ? "g1" : i % 3 === 1 ? "g2" : "g0";      const tx = root[0] + dx + Math.round(sway * (0.6 + 0.2 * (i % 3))), ty = root[1] + dy;      g.line(root, [tx, ty], k);      if (k === "g2") g.pixel(tx, ty, "g3");    });  });}function straw(g, sway) {  // cuffs: splay outward and down below the bar ends  tuft(g, "strawCuffL", [5, 29], [[-2, 0], [-3, 2], [-2, 4], [-4, 3], [-1, 4], [-3, 1]], sway);  tuft(g, "strawCuffR", [27, 29], [[2, 0], [2, 2], [2, 4], [3, 3], [1, 4], [3, 1]], sway);  // hanging under the hem around the post  tuft(g, "strawHem", [16, 43], [[-4, 4], [-2, 5], [-3, 3], [2, 5], [4, 4], [3, 3], [0, 5], [-5, 3], [5, 3]], sway);}function strawGap(g) {                                                       // straw stuffing seen through the open front  g.part("strawFront", () => g.field({ x: [12, 21], y: [37, 46] }, (x, y) => {    if (coatAt(x, y, 0) !== "gap") return null;    return x === 16 ? "g2" : x === 17 ? "g1" : (x + y) % 3 === 0 ? "g0" : "g1";  }));}// ---- head + hat (one joint, so the hat rides the head) ----------------------------------------------------------// face: yaw 1 = looking off to the viewer's left, 0 = facing the viewer. glow 0 = dead carved holes .. 1 = burningfunction head(g, angle, yaw, glow, hatSway, flick) {  g.joint("head", { at: NECK, angle }, () => {    g.outlined("ink", () => {      g.part("neckStraw", () => {        for (const [dx, k] of [[-3, "g1"], [-2, "g2"], [-1, "g1"], [0, "g2"], [1, "g0"], [2, "g1"], [3, "g0"]]) {          g.line([Math.sign(dx) * 0.5, 0], [dx * 1.3, 2 + (Math.abs(dx) === 2 ? 1 : 0)], k);        }      });      g.part("sack", () => g.field({ x: [-8, 8], y: [-14, 0] }, (x, y) => {        const inBall = litIndex(x, y, 0, -7, 6.6, 6.2, 4, 0.1);        const inNeck = y >= -3 && y <= -1 && Math.abs(x + 0.5) <= 3 - (y + 3) * 0.6;        if (inBall < 0 && !inNeck) return null;        let k = inBall < 0 ? 1 : inBall;        const seamX = Math.round(4.5 - 2.5 * yaw);                            // stitched back seam turns with the head        if (x === seamX && y > -12 && y < -2) return (y % 2) ? "w1" : SACK[Math.max(0, k - 1)];        if (k === 3 && (y === -9 || y === -6) && x < -2) k = 2;                // a couple of weave lines on the lit cheek        return SACK[k];      }));      g.part("tie", () => {        for (let x = -2; x <= 2; x++) g.pixel(x, -1, x < 0 ? "w3" : x === 0 ? "w2" : "w1");        g.pixel(-2, 0, "w2"); g.pixel(1, 0, "w1");      });      hat(g, hatSway);    });    face(g, yaw, glow, flick);  });}function hat(g, sway) {  g.part("hatCone", () => {    const tip = [6.5 + sway * 1.6, -21.5 + Math.abs(sway) * 0.6];    g.sweep([[0, -15], [0.4, -19], [2.0, -22.5], tip], { w0: 4.6, w1: 0.6, profile: 0.9 }, (u, v) => {      const s = -v * (u < 0.62 ? -1 : 1);      if (u > 0.6 && u < 0.68) return "n0";                                  // crease where the tip folds over      return HAT[s < -0.4 ? 2 : s < 0.25 ? 1 : 0];    });  });  g.part("hatBand", () => {    for (let x = -4; x <= 3; x++) { g.pixel(x, -17, x < -1 ? "r2" : x < 2 ? "r1" : "r0"); g.pixel(x, -16, x < 0 ? "r1" : "r0"); }    g.rect({ x: [-2, -1], y: [-17, -16] }, "g1", { fill: "g1" }); g.pixel(-2, -17, "g3");  });  g.part("hatBrim", () => g.field({ x: [-11, 11], y: [-18, -10] }, (x, y) => {    const ax = Math.abs(x + 0.5);    const droop = (ax > 6 ? 1 : 0) + (ax > 8.5 ? 1 : 0);    const nx = (x + 0.5) / 10, ny = (y + 0.5 - (-14.5 + droop)) / 2.3;    if (nx * nx + ny * ny > 1) return null;    if (ny > 0.15) return x < -4 ? "v0" : "n0";                               // the brim's front edge, in shade    if (y === -16 && x < -2) return "v2";    return x < -1 ? "v1" : x < 5 ? "v0" : "n0";  }));}function eyeAt(cx, cy, w) {                                                    // carved triangle pointing up  const px = [[0, -2], [-1, -1], [0, -1], [1, -1], [-1, 0], [0, 0], [1, 0]];  return px.filter(([dx]) => w >= 3 || dx <= 0).map(([dx, dy]) => [Math.round(cx) + dx, cy + dy]);}function face(g, yaw, glow, flick) {  const lx = -2.5 - 1.5 * yaw, rx = 2.5 - 3.3 * yaw, mx = -1.6 * yaw;  const hot = glow > 0.85 ? (flick > 0.5 ? "fl3" : "fl2") : glow > 0.6 ? "fl2" : glow > 0.3 ? "fl1" : glow > 0.05 ? "fl0" : "w0";  const rim = glow > 0.85 ? "fl2" : glow > 0.6 ? "fl1" : glow > 0.3 ? "fl0" : "w0";  g.part("eyes", () => {    for (const [cx, w] of [[lx, 3], [rx, yaw > 0.5 ? 2 : 3]]) {      const px = eyeAt(cx, -7, w);      const core = Math.round(cx);      px.forEach(([x, y]) => g.pixel(x, y, glow <= 0.05 ? (y === -7 ? "w0" : "ink") : (x === core && y > -9) ? hot : rim));      if (glow > 0.6) px.filter(([, y]) => y === -7).forEach(([x]) => g.pixel(x, -6, "w4"));   // glow spills on the burlap    }  });  g.part("mouth", () => {    const m = Math.round(mx), n = yaw > 0.5 ? 2 : 3;    for (let x = m - n; x <= m + n; x++) g.pixel(x, -4, glow > 0.3 ? (Math.abs(x - m) <= 1 ? hot : rim) : "w0");    for (const x of [m - 2, m, m + 2]) if (x >= m - n && x <= m + n) { g.pixel(x, -5, "n4"); g.pixel(x, -3, "n3"); }  });}// ---- haunting effects --------------------------------------------------------------------------------------------const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];const dith = (x, y, dens) => BAYER[(y & 3) * 4 + (x & 3)] / 16 < dens;function haloAt(g, t, strength) {  const C = { x: 16, y: 18 }, R = (10.5 + 0.7 * Math.sin(TAU * 2 * t)) * strength;  if (R < 2) return;  g.detached("halo", () => g.field({ x: [2, 30], y: [4, 32] }, (x, y) => {    const r = Math.hypot(x + 0.5 - C.x, (y + 0.5 - C.y) * 0.9);    if (r > R) return null;    return dith(x, y, 0.3 * (1 - Math.max(0, r - 6.5) / (R - 6.5))) ? "fl0" : null;  }));}function embers(g, t, amount) {  g.detached("embers", () => {    for (let i = 0; i < 5; i++) {      const p = (t + i / 5) % 1;      if (p > amount) continue;      const x = Math.round(16 + (i % 2 ? 9 : -9) + (i - 2) * 0.8 + Math.sin(TAU * (p + i * 0.31)) * 1.5 - p * 2);      const y = Math.round(14 - p * 14);      if (y < 0) continue;      if (p < 0.3) { g.pixel(x, y, "fl3"); g.pixel(x, y + 1, "fl1"); }      else if (p < 0.65) g.pixel(x, y, "fl2");      else if (chk(x, y)) g.pixel(x, y, "fl0");    }  });}// ---- frames ------------------------------------------------------------------------------------------------------function body(g, wt) {  const wind = Math.sin(TAU * wt), lag = Math.sin(TAU * (wt - 0.1));  const sh = Math.round(wind * 1.1), flap = 0.5 + 0.5 * Math.sin(TAU * 2 * wt);  contactShadow(g, CX, GROUND, 8, 1);  g.outlined("ink", () => frame(g));  g.outlined("ink", () => { straw(g, lag * 1.2); });  g.outlined("ink", () => { coat(g, sh, flap); });  strawGap(g);  belt(g, wind);  g.outlined("ink", () => tuft(g, "strawCollar", [16, 25], [[-5, -1], [-4, -2], [5, -1], [4, -2], [-3, -1], [3, -1]], lag * 0.6));  g.outlined("ink", () => mound(g, wind));  return { wind, lag };}function idleFrame(g, t) {  const { lag } = body(g, t);  head(g, 3 * Math.sin(TAU * t), 1, 0, lag, 0);}const H_TILT = -9;function hauntedFrame(g, t) {  haloAt(g, t, 1);  const { lag } = body(g, t);  const flick = 0.5 + 0.5 * Math.sin(TAU * 3 * t + 0.4);  head(g, H_TILT + 3 * Math.sin(TAU * t), 0, 1, lag, flick);  embers(g, t, 1);}function turnFrame(g, f) {  if (f >= N_TURN - 1) return hauntedFrame(g, 0);  const k = f / (N_TURN - 1), e = k * k * (3 - 2 * k);                         // eased turn  const wt = f / (N_TURN - 1);  if (f >= 5) haloAt(g, 0, f === 5 ? 1.25 : 1.1);                              // a flare as the eyes catch  const { lag } = body(g, wt);  const glow = [0, 0, 0.1, 0.35, 0.65, 1, 1][f];  head(g, 3 * Math.sin(TAU * wt) * (1 - e) + H_TILT * e, 1 - e, glow, lag, f === 5 ? 1 : 0.6);  if (f >= 5) embers(g, 0, (f - 4) / 3);}const Scarecrow = sprite(pal, (g, t, mode) => {  if (mode === "turn") return turnFrame(g, Math.min(N_TURN - 1, Math.round(t * N_TURN)));  t = ((t % 1) + 1) % 1;  if (mode === "haunted") return hauntedFrame(g, t);  idleFrame(g, t);});submit(Scarecrow, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_LOOP / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "haunted", args: ["haunted"] },    { name: "turn", args: ["turn"], animate: { fps: 10, duration: N_TURN / 10, loop: false } },  ],});

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

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

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