Harvest Festival · beehive.js
蜂の巣箱
木製の台に載せた藁の蜂の巣(スケップ)と、溝付きの柄杓を立てた素焼きの蜜壺が並ぶ見下ろし 3/4 視点の 32×48 の小物です。16 コマのループである idle(待機)では 3 匹の蜂が羽ばたきながら出入口を旋回します。12 コマの swarm(群れ)では 12 匹ほどの蜂が巣の周囲を激しく飛び交い、16 コマの honey(ハチミツ)では柄杓から蜜のしずくが壺へと垂れ落ちます。
6×
動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
beehive.js · 1–9
// Beehive — 32x48 floor prop, top-down 3/4, key light top-left.// A straw skep (a bell of coiled straw bands that curve with the dome, a twisted knot on top, a dark arched entrance// at the foot) on a short wooden stand; beside it on the ground a clay honey pot with a twine tie, honey dribbling// over the lip and a grooved wooden dipper standing in it, head up.// Variants:// idle 16f loop: three bees (gold + ink, beating wings) circle the entrance on separate paths// swarm 12f loop: a dozen agitated bees whirl around the whole skep, some passing behind it// honey 16f loop: a drop of honey swells on the dipper head, stretches, falls into the pot and rings the surface;// the glint on the honey slides; one lazy bee hovers by the entrance最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
beehive.js · 230–240
submit(Beehive, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 16, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "swarm", args: ["swarm"], animate: { fps: 10, duration: N_SWARM / 10, loop: true } }, { name: "honey", args: ["honey"], animate: { fps: 10, duration: N_HONEY / 10, loop: true } }, ],});プログラム全体を表示 (240 行)
beehive.js · 240 行
// Beehive — 32x48 floor prop, top-down 3/4, key light top-left.// A straw skep (a bell of coiled straw bands that curve with the dome, a twisted knot on top, a dark arched entrance// at the foot) on a short wooden stand; beside it on the ground a clay honey pot with a twine tie, honey dribbling// over the lip and a grooved wooden dipper standing in it, head up.// Variants:// idle 16f loop: three bees (gold + ink, beating wings) circle the entrance on separate paths// swarm 12f loop: a dozen agitated bees whirl around the whole skep, some passing behind it// honey 16f loop: a drop of honey swells on the dipper head, stretches, falls into the pot and rings the surface;// the glint on the honey slides; one lazy bee hovers by the entranceconst W = 32, H = 48, GROUND = 45;const N_IDLE = 16, N_SWARM = 12, N_HONEY = 16;const SKEP = { cx: 11, base: 31, h: 19, rx: 8.6 };const ENTR = { x: 11, y: 29 };const POT = { cx: 25, top: 34, bottom: 44, rx: 5 };const DIP = { hx: 27, hy: 23 }; // dipper head centre// ---- stand --------------------------------------------------------------------------------------------------------function stand(g) { g.part("standLegs", () => { for (const [x0, lit] of [[3, true], [17, false]]) { for (let y = 35; y <= 44; y++) { g.pixel(x0, y, lit ? "w3" : "w2"); g.pixel(x0 + 1, y, lit ? "w2" : "w1"); } g.pixel(x0 + 1, 37, "w1"); // a knot in the grain } g.line([5, 39], [16, 39], "w1"); // rear stretcher, in shadow }); g.part("standTop", () => { for (let x = 1; x <= 21; x++) { const u = (x - 1) / 20; g.pixel(x, 31, u < 0.3 ? "w4" : "w3"); g.pixel(x, 32, u < 0.6 ? "w3" : "w2"); g.pixel(x, 33, u < 0.2 ? "w2" : "w1"); // front edge of the plank g.pixel(x, 34, "w0"); } for (const x of [7, 14]) g.pixel(x, 33, "w0"); // plank joins g.pixel(19, 32, "w1"); g.pixel(4, 32, "w4"); });}// ---- skep ---------------------------------------------------------------------------------------------------------const BAND = 3;function skepHalfWidth(y) { const { base, h, rx } = SKEP; const k = (base - y) / h; // 0 at the foot .. 1 at the crown if (k < 0 || k > 1) return -1; return rx * Math.pow(Math.max(0, 1 - Math.pow(k, 2.1)), 0.5) + (k < 0.12 ? 0.5 : 0);}function skep(g) { const { cx, base, h } = SKEP; g.part("skep", () => g.field({ x: [cx - 10, cx + 10], y: [base - h, base] }, (x, y) => { const hw = skepHalfWidth(y); if (hw < 0.6) return null; const nx = (x + 0.5 - cx) / hw; if (Math.abs(nx) > 1) return null; const k = (base - y) / h; // coils are rings seen from above: the band lines dip toward the front centre const yy = y - 1.6 * Math.sqrt(Math.max(0, 1 - nx * nx)); const r = (((Math.floor(base - yy) % BAND) + BAND) % BAND); // 0 groove .. 2 crown of the coil let v = -0.85 * nx + 0.5 * k - 0.1; v += r === 2 ? 0.45 : r === 1 ? 0.1 : -0.55; // stitches binding the coils: short dark ticks on the groove, staggered per band const band = Math.floor((base - yy) / BAND); if (r === 0 && (x + band * 2) % 5 === 0) return "w1"; if (Math.abs(nx) > 0.93) v -= 0.35; // the dome turns away at the edge return v > 0.75 ? "g3" : v > 0.15 ? "g2" : v > -0.45 ? "g1" : v > -0.95 ? "g0" : "w1"; })); g.part("skepKnot", () => { // twisted straw knot at the crown g.pixel(cx - 1, base - h - 1, "g2"); g.pixel(cx, base - h - 1, "g1"); g.pixel(cx - 1, base - h - 2, "g3"); g.pixel(cx, base - h - 2, "g2"); g.pixel(cx, base - h, "g0"); }); g.part("entrance", () => { const { x, y } = ENTR; g.fillPoly([[x - 2, y + 2], [x - 2, y], [x - 1, y - 1], [x + 1, y - 1], [x + 2, y], [x + 2, y + 2]], "ink"); g.line([x - 3, y + 2], [x - 3, y], "g0"); g.pixel(x - 2, y - 1, "g0"); // straw lip of the arch g.pixel(x - 1, y - 2, "g3"); g.pixel(x, y - 2, "g2"); g.pixel(x + 1, y - 2, "g1"); g.pixel(x, y + 1, "n0"); // a little depth inside });}// ---- honey pot + dipper -------------------------------------------------------------------------------------------function pot(g) { const { cx, top, bottom, rx } = POT; g.part("pot", () => { g.field({ x: [cx - rx - 1, cx + rx + 1], y: [top + 2, bottom] }, (x, y) => { // squat belly: widest two-thirds of the way down const k = (y - top - 2) / (bottom - top - 2); const hw = 3.6 + 2.0 * Math.sin(Math.PI * Math.min(1, k * 1.15)) - (k > 0.9 ? 0.8 : 0); const nx = (x + 0.5 - cx) / hw; if (Math.abs(nx) > 1) return null; const nz = Math.sqrt(1 - nx * nx); const v = -0.6 * nx + 0.5 * nz - k * 0.35; return v > 0.55 ? "w4" : v > 0.15 ? "w3" : v > -0.3 ? "w2" : v > -0.65 ? "w1" : "w0"; }); // rim: a lit ring, honey in the mouth g.ellipse({ x: [cx - 4, cx + 4], y: [top, top + 3] }, "w2", { fill: "w2" }); g.line([cx - 3, top], [cx + 1, top], "w4"); g.pixel(cx - 4, top + 1, "w3"); g.ellipse({ x: [cx - 3, cx + 3], y: [top + 1, top + 2] }, "g1", { fill: "g1" }); g.line([cx - 2, top + 1], [cx + 1, top + 1], "g2"); g.pixel(cx + 3, top + 3, "w1"); g.pixel(cx + 4, top + 2, "w1"); // twine tie under the rim for (let x = cx - 4; x <= cx + 4; x++) g.pixel(x, top + 4, x < cx - 1 ? "w3" : x < cx + 2 ? "w1" : "w0"); g.pixel(cx - 3, top + 5, "w3"); g.pixel(cx - 4, top + 6, "w1"); // dangling end // honey dribbling over the lip g.pixel(cx - 1, top + 3, "g2"); g.pixel(cx - 1, top + 4, "g2"); g.pixel(cx - 1, top + 5, "g1"); g.pixel(cx, top + 3, "g1"); g.pixel(cx - 1, top + 6, "g1"); g.pixel(cx - 2, top + 5, "g3"); });}function dipper(g) { const { hx, hy } = DIP; g.part("dipperHandle", () => { // the handle runs down-left from the head into the honey g.line([hx - 1, hy + 3], [hx - 4, POT.top + 1], "w3"); g.line([hx, hy + 3], [hx - 3, POT.top + 1], "w1"); }); g.part("dipperHead", () => { // grooved head, coated in honey: ridges lit on the left, grooves dark for (let dy = -2; dy <= 2; dy++) { for (let dx = -1; dx <= 1; dx++) { const ridge = (dy + 2) % 2 === 0; const k = ridge ? (dx < 0 ? "g3" : dx === 0 ? "g2" : "g1") : (dx < 0 ? "g1" : dx === 0 ? "g0" : "w1"); g.pixel(hx + dx, hy + dy, k); } } g.pixel(hx, hy - 3, "w2"); g.pixel(hx, hy + 3, "g1"); // knob on top, honey collar below });}// honey drip, f in 0..N_HONEY-1; returns nothing when the cycle is between dropsfunction drip(g, f, still) { const { hx, hy } = DIP, surf = POT.top + 1; const x = hx + 1, y0 = hy + 3; // drips off the head's lower right g.detached("drip", () => { if (still) { g.pixel(x, y0, "g1"); return; } // swell (0-5), stretch (6-8), fall (9-11), splash + rings (12-15) if (f <= 5) { g.pixel(x, y0, f < 3 ? "g1" : "g2"); if (f >= 3) { g.pixel(x, y0 + 1, "g2"); g.pixel(x + 1, y0 + 1, "g1"); } if (f >= 5) { g.pixel(x, y0 + 2, "g1"); g.pixel(x, y0 + 1, "g3"); } } else if (f <= 8) { const len = f - 4; // 2..4 g.line([x, y0], [x, y0 + len - 1], "g1"); g.rect({ x: [x, x + 1], y: [y0 + len, y0 + len + 1] }, "g2", { fill: "g2" }); g.pixel(x, y0 + len, "g3"); g.pixel(x + 1, y0 + len + 1, "g1"); } else if (f <= 11) { const y = y0 + 4 + (f - 9) * 3; if (y < surf - 1) { g.rect({ x: [x, x + 1], y: [y, y + 1] }, "g2", { fill: "g2" }); g.pixel(x, y, "g3"); g.pixel(x + 1, y + 1, "g1"); g.pixel(x, y - 1, "g1"); } if (f === 9) g.pixel(x, y0, "g1"); // the thread snaps back } else { const r = f - 11; // 1..4 rings widen if (r <= 3) { g.pixel(x - r, surf, "g3"); g.pixel(Math.min(POT.cx + 3, x + r - 1), surf, r < 3 ? "g3" : "g2"); } if (r === 1) g.pixel(x - 1, surf - 1, "g2"); // a small crown on impact } });}function honeyGlint(g, t) { // a glint that slides slowly across the honey in the mouth const x = POT.cx - 2 + Math.round(2.5 * (0.5 - 0.5 * Math.cos(TAU * t))); g.part("honeyGlint", () => { g.pixel(x, POT.top + 1, "g3"); g.pixel(x + 1, POT.top + 1, "g2"); });}// ---- bees ---------------------------------------------------------------------------------------------------------// a bee: gold body with an ink stripe trailing behind its heading, a pale wing that beats every framefunction bee(g, x, y, dir, wingUp, name) { x = Math.round(x); y = Math.round(y); g.detached(name, () => { g.pixel(x + dir, y, "g2"); // head end g.pixel(x, y, "ink"); // stripe g.pixel(x - dir, y, "g1"); // abdomen if (wingUp) { g.pixel(x, y - 1, "n5"); g.pixel(x - dir, y - 1, "n4"); } else g.pixel(x, y + 1, "n4"); });}function pathBee(g, fn, t, frame, name, behind) { const [x, y, back] = fn(t); const [x2] = fn(t + 0.004); if (!!back !== !!behind) return; bee(g, x, y, x2 >= x ? 1 : -1, (frame + name.length) % 2 === 0, name);}const IDLE_BEES = [ (t) => [ENTR.x + 7 * Math.cos(TAU * t), ENTR.y - 1 + 3 * Math.sin(TAU * t), false], // ring at the door (t) => [SKEP.cx + 8 * Math.sin(TAU * -t + 1), 7 + 3 * Math.sin(TAU * -2 * t + 2), false], // figure-eight over the crown (t) => { const a = TAU * 2 * t + 2.2; return [SKEP.cx + 11 * Math.cos(a), 21 + 5 * Math.sin(a), Math.sin(a) < -0.2]; },];function swarmBee(i) { const f1 = 1 + (i % 3), f2 = 2 + (i % 2), s = i % 2 ? 1 : -1; const rx = 8 + (i * 37 % 6), ry = 3 + (i * 13 % 4), cy = 5 + (i * 7 % 23), ph = i * 0.61; return (t) => { const a = TAU * f1 * s * t + ph; return [SKEP.cx + 1 + rx * Math.cos(a), cy + ry * Math.sin(TAU * f2 * t + ph * 1.7) * 0.6 + 1.5 * Math.sin(a), Math.sin(a) < -0.5 && cy > 15]; };}const SWARM = Array.from({ length: 12 }, (_, i) => swarmBee(i));// ---- scene --------------------------------------------------------------------------------------------------------const Beehive = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; const n = mode === "swarm" ? N_SWARM : mode === "honey" ? N_HONEY : N_IDLE; const frame = Math.round(t * n) % n; contactShadow(g, 11, GROUND, 10, 1); contactShadow(g, POT.cx, GROUND, 5, 1); const bees = mode === "swarm" ? SWARM : mode === "honey" ? [IDLE_BEES[0]] : IDLE_BEES; bees.forEach((fn, i) => pathBee(g, fn, t, frame, "beeB" + i, true)); // bees behind the skep g.outlined("ink", () => { stand(g); skep(g); }); g.outlined("ink", () => { pot(g); dipper(g); }); if (mode === "honey") { honeyGlint(g, t); drip(g, frame, false); } else { honeyGlint(g, 0); drip(g, 0, true); } // jitter for the swarm: a pixel of nervous wobble that repeats with the loop bees.forEach((fn, i) => { const jf = mode === "swarm" ? (tt) => { const p = fn(tt); const j = Math.round(hash(i, frame) * 2 - 1); return [p[0] + j, p[1] + (frame + i) % 2, p[2]]; } : fn; pathBee(g, jf, t, frame, "bee" + i, false); });});submit(Beehive, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: 16, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "swarm", args: ["swarm"], animate: { fps: 10, duration: N_SWARM / 10, loop: true } }, { name: "honey", args: ["honey"], animate: { fps: 10, duration: N_HONEY / 10, loop: true } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。