Tessarune
Witch's Hut · pumpkin_patch.js

かぼちゃ畑

48×32 · JavaScript 249 行

48×32 の床置きの小物で、上面に畝を切り手前の縁が濃い、耕した土の盛り床に、大きさの違う彫っていないかぼちゃが 3 つ、手のひら形の大きな葉、巻きひげのあるつるが伸びています。g.depth の中で手前のものから描くので、後ろのものに重なっても一つずつ輪郭線が保たれます。idle(12 コマ)は葉がそれぞれの拍子でそよぎ、巻きひげが巻いたりほどけたりします。grow(10 コマの一回再生)は真ん中のかぼちゃが緑のつぼみから大きく育ちます。harvested(12 コマ)は切った茎とくぼみが残り、小さなかぼちゃが 1 つだけあります。

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

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

pumpkin_patch.js · 1–8
// Pumpkin patch — 48x32 floor prop, top-down 3/4, key light top-left.// A raised bed of tilled earth (furrowed top face, darker front lip) with three uncarved pumpkins of different sizes,// broad palmate leaves and curling vines with tendrils.// Variants://   idle       12f loop: the leaves rustle in a breeze (each on its own phase), the tendrils curl and uncurl//   grow       10f once: the middle pumpkin swells from a green bud to full size, the leaves unfurl; ends on idle f0//   harvested  12f loop: cut stem stumps and hollows where pumpkins were, one small pumpkin left, leaves rustle// Objects are drawn nearest-first inside g.depth so each one keeps its own ink outline against the ones behind it.

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

pumpkin_patch.js · 239–249
submit(PumpkinPatch, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "grow", args: ["grow"], animate: { fps: 10, duration: N_GROW / 10, loop: false } },    { name: "harvested", args: ["harvested"] },  ],});
プログラム全体を表示 (249 行)
pumpkin_patch.js · 249 行
// Pumpkin patch — 48x32 floor prop, top-down 3/4, key light top-left.// A raised bed of tilled earth (furrowed top face, darker front lip) with three uncarved pumpkins of different sizes,// broad palmate leaves and curling vines with tendrils.// Variants://   idle       12f loop: the leaves rustle in a breeze (each on its own phase), the tendrils curl and uncurl//   grow       10f once: the middle pumpkin swells from a green bud to full size, the leaves unfurl; ends on idle f0//   harvested  12f loop: cut stem stumps and hollows where pumpkins were, one small pumpkin left, leaves rustle// Objects are drawn nearest-first inside g.depth so each one keeps its own ink outline against the ones behind it.const W = 48, H = 32, GROUND = 29, CX = 24;const N_IDLE = 12, N_GROW = 10;const BED = { cx: 24, cy: 20, rx: 21, ry: 6.5 };const PUMP = ["r0", "r1", "r2", "r3", "g2"];const BUD = ["e0", "e1", "e2", "e3", "e3"];// ---- the bed ------------------------------------------------------------------------------------------------function bed(g) {  const { cx, cy, rx, ry } = BED;  g.part("bed", () => {    // front lip of the raised bed (the earth wall facing the camera)    g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 3] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx;      for (let d = 0; d <= 3; d++) {        const ny = (y + 0.5 - d - cy) / ry;        if (nx * nx + ny * ny <= 1) {          if (d === 0) return null;                          // top face drawn below          return nx < -0.55 ? "w1" : "w0";        }      }      return null;    });    // top face: furrows running across, ridges catch the light, left side warmer    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 r2 = nx * nx + ny * ny;      if (r2 > 1) return null;      // furrows run across the bed, gently wavy so they read as hand-hoed earth, not planks      const wav = Math.round(0.6 * Math.sin(x * 0.55 + Math.floor(y / 3) * 2.1));      const row = ((y + wav - Math.round(cy - ry) + 30) % 3);      const lit = -0.6 * nx - 0.4 * ny;      if (r2 > 0.8 && ny > 0.3) return chk(x, y) ? "w2" : "w1";   // crumbly near rim      if (row === 0) return lit > 0.3 ? "w3" : "w2";          // ridge top      if (row === 1) return "w1";      return lit > 0.35 && chk(x, y) ? "w1" : "w0";           // furrow (softened on the lit side)    });    // clods and stones: countable marks    for (const [x, y, k] of [[7, 19, "w3"], [11, 24, "n3"], [19, 25, "w3"], [30, 24, "n2"], [40, 20, "w2"], [43, 21, "w3"], [5, 21, "n3"], [36, 15, "w3"]]) g.pixel(x, y, k);  });}// ---- pumpkin --------------------------------------------------------------------------------------------------// k: 0..1 growth (1 = ripe orange; small k = a green bud that ripens through the ramp swap point)function pumpkin(g, name, cx, base, rx, ry, lobes, opts) {  const o = opts || {};  const ramp = o.green ? BUD : PUMP;  const cy = base - ry;  g.part(name, () => {    g.field({ x: [Math.floor(cx - rx - 1), Math.ceil(cx + rx + 1)], y: [Math.floor(cy - ry - 1), Math.ceil(base)] }, (x, y) => {      const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry;      // squat body with ribbed bumps along the silhouette      const s = nx / Math.sqrt(Math.max(0.08, 1 - Math.min(0.95, ny * ny)));      const lob = (Math.asin(Math.max(-1, Math.min(1, s))) / (Math.PI / 2) + 1) * lobes / 2;      const fr = lob - Math.floor(lob);      const bump = 1 + 0.07 * Math.sin(Math.PI * fr);      if (nx * nx + ny * ny > bump * bump) return null;      let i = litIndex(x, y, cx, cy, rx * bump, ry * bump, 5, 0.12);      if (i < 0) i = 0;      // each lobe is a little barrel: its left half catches more light      if (fr < 0.38 && i < 4) i += 0;      else if (fr > 0.62 && i > 0) i -= 1;      // groove between lobes      if ((fr < 0.14 || fr > 0.9) && Math.abs(ny) < 0.85) i = Math.max(0, i - 2);      // dimple at the crown      if (ny < -0.55 && Math.abs(nx) < 0.22) i = Math.max(0, i - 1);      // specular on the upper-left lobe      if (!o.green && rx > 4) { const sx = nx + 0.42, sy = ny + 0.42; if (sx * sx * 4 + sy * sy * 9 < 0.12) i = 4; }      return ramp[Math.max(0, Math.min(4, i))];    });  });  // stem  if (o.stem !== false) {    const sx = Math.round(cx), sy = Math.round(cy - ry);    g.part(name + "Stem", () => {      if (rx >= 5) {        g.pixel(sx, sy, "w1"); g.pixel(sx - 1, sy, "w2");        g.pixel(sx, sy - 1, "w2"); g.pixel(sx + 1, sy - 1, "w1");        g.pixel(sx + 1, sy - 2, "w3"); g.pixel(sx + 2, sy - 2, "w2");      } else {        g.pixel(sx, sy, "w2"); g.pixel(sx + 1, sy - 1, "w3");      }    });  }}// cut stem stump left in the bed after harvest, with a shallow hollow where the fruit satfunction hollow(g, name, cx, base, rx) {  // the shallow dent the fruit left in the earth: dark far wall, lit near lip  g.part(name, () => {    g.ellipse({ x: [cx - rx, cx + rx], y: [base - 3, base] }, "w0", { fill: "w0" });    g.line([cx - rx + 2, base - 3], [cx + rx - 2, base - 3], "ink");    g.line([cx - rx + 1, base], [cx + rx - 1, base], "w3");    g.pixel(cx - rx + 1, base - 1, "w2"); g.pixel(cx + rx - 1, base - 1, "w1");  });}function stump(g, name, cx, base) {  // the cut stem: a short woody stub with a pale fresh-cut face, a curl of vine still attached  g.part(name, () => {    g.pixel(cx, base, "w1"); g.pixel(cx + 1, base, "w0");    g.pixel(cx, base - 1, "w2"); g.pixel(cx + 1, base - 1, "w1");    g.pixel(cx, base - 2, "w4"); g.pixel(cx + 1, base - 2, "w3");  });}// ---- leaves ---------------------------------------------------------------------------------------------------// a palmate pumpkin leaf seen from above-front: 5 rounded lobes, three veins, lit top-leftfunction leaf(g, name, cx, cy, R, ang, scale, wilt) {  const r = R * scale;  if (r < 0.9) return;  const ry = 0.72;                                              // foreshortened by the 3/4 view  g.part(name, () => g.field({ x: [Math.floor(cx - r - 1), Math.ceil(cx + r + 1)], y: [Math.floor(cy - r - 1), Math.ceil(cy + r + 1)] }, (x, y) => {    const dx = x + 0.5 - cx, dy = (y + 0.5 - cy) / ry;    const rho = Math.hypot(dx, dy) / r;    const th = Math.atan2(dy, dx) - ang;    const edge = 0.86 + 0.14 * Math.cos(5 * th);    const notch = Math.cos(th) < -0.9 ? 0.55 : 1;                 // the stalk notch at the base    if (rho > edge * notch) return null;    // veins from the base    if (r >= 3 && rho < 0.85) for (const va of [0, 1.15, -1.15]) {      let d = th - va; d = Math.atan2(Math.sin(d), Math.cos(d));      if (Math.abs(d) * rho * r < 0.5 && rho > 0.15) return "e1";    }    const l = (-0.65 * dx - 0.55 * dy * ry) / r;    if (wilt && rho > 0.62) return l > 0 ? "g1" : "w2";        // late-season leaf: browned, curling edge    // the leaf is a shallow cup: its far rim is lit, its near half falls in shade    return l > 0.25 ? "e3" : l > -0.25 ? "e2" : rho > 0.55 ? "e1" : "e2";  }));}// ---- vines and tendrils -------------------------------------------------------------------------------------function vine(g, name, path) {  g.part(name, () => g.sweep(path, { w0: 0.7, w1: 0.45 }, (u, v) => (v < -0.3 ? "e2" : "e1")));}function tendril(g, x, y, dir, turns, curl, key) {  // a spiral that tightens toward its tip; curl (0..1) winds it up  for (let s = 0; s <= 1.001; s += 0.08) {    const rad = 2.6 * (1 - s * 0.7);    const a = dir * (s * TAU * turns * (0.65 + 0.35 * curl)) - Math.PI / 2;    g.pixel(Math.round(x + dir * s * 2 + Math.cos(a) * rad - Math.cos(-Math.PI / 2) * 2.6),            Math.round(y + Math.sin(a) * rad * 0.8 + 2), s > 0.7 ? "e3" : key);  }}// ---- scene ------------------------------------------------------------------------------------------------------// leaf table: [cx, cy, R, ang, phase, depthOrder]  (bigger y = nearer)const LEAVES = [  [6, 15, 4.6, -2.3, 0.0],  [17, 13, 4.2, -1.6, 0.3],  [33, 13, 4.6, -1.2, 0.55],  [42, 16, 4.0, -0.6, 0.8],  [20, 25, 3.8, 1.9, 0.15],  [31, 24, 4.4, 1.2, 0.65],  [43, 23, 3.4, 0.6, 0.4],];function scene(g, t, mode, grow) {  const gk = grow === undefined ? 1 : grow;                     // growth of the middle pumpkin (1 = ripe)  const unf = grow === undefined ? 1 : 0.35 + 0.65 * Math.min(1, grow * 1.25);  const harvested = mode === "harvested";  contactShadow(g, CX, GROUND, 21, 1);  // layer depths: nearer = more negative; objects are drawn front-to-back  const Z = (y) => -Math.round(y);  // front leaves first (they overlap the pumpkins)  LEAVES.forEach(([x, y, R, a, ph], i) => {    if (y < 20) return;    const sw = 0.22 * Math.sin(TAU * (t + ph));    const s = harvested ? (i === 4 ? 0.7 : 0.9) : (i === 4 ? unf : 1);    g.depth(Z(y + 6), () => g.outlined("ink", () => leaf(g, "leafF" + i, x, y + Math.round(sw * 0.6), R, a + sw, s, harvested && i !== 6)));  });  if (!harvested) {    // big pumpkin, front left    g.depth(Z(27), () => g.outlined("ink", () => pumpkin(g, "pumpkinBig", 12, 27, 7.5, 5.2, 5)));    // middle pumpkin, back centre — it is the one that grows    const k = gk;    const rx = 2 + 4.6 * k, ry = 1.6 + 3.6 * k;    g.depth(Z(22), () => g.outlined("ink", () => pumpkin(g, "pumpkinMid", 26, 21, rx, ry, k > 0.5 ? 5 : 3, { green: k < 0.45, stem: true })));  } else {    g.depth(Z(27), () => g.outlined("ink", () => stump(g, "stumpBig", 12, 24)));    g.depth(Z(22), () => g.outlined("ink", () => stump(g, "stumpMid", 26, 19)));    g.depth(Z(11), () => { hollow(g, "hollowBig", 12, 26, 6); hollow(g, "hollowMid", 26, 21, 4); });  }  // small pumpkin, right (the one left after the harvest)  g.depth(Z(25), () => g.outlined("ink", () => pumpkin(g, "pumpkinSmall", 37, 25, 4.6, 3.4, 3)));  // vines: thin runners lying on the earth (no outline; dark green reads against the brown)  g.depth(Z(14), () => {    vine(g, "vineL", [[8, 22], [4, 20], [4, 17], [7, 15]]);    vine(g, "vineM", [[17, 22], [20, 24], [24, 24], [28, 22], [31, 20]]);    vine(g, "vineR", [[31, 20], [35, 18], [39, 18], [43, 19]]);    vine(g, "vineB", [[22, 18], [20, 15], [16, 14]]);  });  // back leaves  LEAVES.forEach(([x, y, R, a, ph], i) => {    if (y >= 20) return;    const sw = 0.2 * Math.sin(TAU * (t + ph));    const s = harvested ? (i === 1 ? 0.75 : 0.95) : (i === 1 || i === 2 ? unf : 1);    g.depth(Z(y), () => g.outlined("ink", () => leaf(g, "leafB" + i, x, y + Math.round(sw * 0.6), R, a + sw, s, harvested && (i === 0 || i === 2))));  });  g.depth(Z(10), () => g.outlined("ink", () => bed(g)));  // tendrils: thin spirals drawn on top, no outline  g.depth(-60, () => g.detached("tendrils", () => {    const c1 = 0.5 + 0.5 * Math.sin(TAU * t), c2 = 0.5 + 0.5 * Math.sin(TAU * (t + 0.5));    tendril(g, 3, 12, 1, 1.1, c1, "e2");    tendril(g, 44, 12, -1, 1.0, c2, "e2");    if (!harvested) tendril(g, 22, 21, -1, 0.9, c2, "e2");  }));}const PumpkinPatch = sprite(pal, (g, t, mode) => {  t = ((t % 1) + 1) % 1;  if (mode === "grow") {    const f = Math.min(N_GROW - 1, Math.round(t * N_GROW));    const p = f / (N_GROW - 1);    const k = 1 - Math.pow(1 - p, 2.2);                        // fast swell, settles into full size    // a small overshoot near the end gives the swell some weight, back to exactly 1 on the last frame    const ko = f === N_GROW - 2 ? 1.06 : f === N_GROW - 1 ? 1 : k;    scene(g, 0, "idle", ko);    return;  }  const f = Math.round(t * N_IDLE) % N_IDLE;  scene(g, f / N_IDLE, mode);});submit(PumpkinPatch, {  size: [W, H], origin: "top-left", kind: "icon", scale: 1,  animate: { fps: 10, duration: N_IDLE / 10, loop: true },  anchor: { x: CX, y: GROUND },  args: ["idle"],  variants: [    { name: "idle", args: ["idle"] },    { name: "grow", args: ["grow"], animate: { fps: 10, duration: N_GROW / 10, loop: false } },    { name: "harvested", args: ["harvested"] },  ],});

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

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

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