Harvest Festival · wheat_sheaf.js
麦の束
赤いリボンと紐で中央を束ねた小麦の束と、右側に立てかけた鎌を描いた見下ろし 3/4 視点の 32×48 の小物です。16 コマのループである idle(待機)では風が穂を揺らし、リボンの端がなびいて粒がこぼれ落ちます。12 コマの gust(突風)では強い風に穂先が大きくしなって藁が舞い、16 コマの festival(祭り)では藁の星飾りや花、リボンが結ばれて揺れます。
6×
動くスプライト素材: Harvest Festival(itch.io)Vol.6・収穫祭の小物 12 点・37 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
wheat_sheaf.js · 1–11
// Wheat sheaf — 32x48 floor prop, top-down 3/4, key light top-left.// A tall hand-bound sheaf of ripe wheat standing on stubble: straw stalks pinched at the waist by a red ribbon// and twine, flaring out at the foot, a crown of heavy golden ears with whiskery awns, and a sickle (wooden grip,// iron crescent blade) leaning against its right side.// Variants:// idle 16f loop: a breeze runs across the ears (each ear nods a little after its neighbour), the crown leans// and returns, the ribbon tails stir, a couple of grains drift off and fall// gust 12f loop: a strong wind bends the crown hard to the right with a fast flutter; three loose stalks// tumble across the canvas (they leave on the right and come back in on the left), chaff streams// festival 16f loop: corn-dolly dressing for the fair: a woven straw star crowns the sheaf, a big red bow with// long streamers (and a blue one) flutters, poppies and cornflowers are tucked into the tie最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
wheat_sheaf.js · 277–287
submit(WheatSheaf, { 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: "gust", args: ["gust"], animate: { fps: 10, duration: N_GUST / 10, loop: true } }, { name: "festival", args: ["festival"] }, ],});プログラム全体を表示 (287 行)
wheat_sheaf.js · 287 行
// Wheat sheaf — 32x48 floor prop, top-down 3/4, key light top-left.// A tall hand-bound sheaf of ripe wheat standing on stubble: straw stalks pinched at the waist by a red ribbon// and twine, flaring out at the foot, a crown of heavy golden ears with whiskery awns, and a sickle (wooden grip,// iron crescent blade) leaning against its right side.// Variants:// idle 16f loop: a breeze runs across the ears (each ear nods a little after its neighbour), the crown leans// and returns, the ribbon tails stir, a couple of grains drift off and fall// gust 12f loop: a strong wind bends the crown hard to the right with a fast flutter; three loose stalks// tumble across the canvas (they leave on the right and come back in on the left), chaff streams// festival 16f loop: corn-dolly dressing for the fair: a woven straw star crowns the sheaf, a big red bow with// long streamers (and a blue one) flutters, poppies and cornflowers are tucked into the tieconst W = 32, H = 48, GROUND = 45, CX = 16;const WAIST = 31, TOP = 19;const N_IDLE = 16, N_GUST = 12;const L = [-0.74, -0.67];const STRAW = ["w1", "w2", "g1", "w4", "g2"];const EAR_F = ["g0", "g1", "g2", "g3"], EAR_B = ["w1", "g0", "g1", "g2"];// ---- the bundle of stalks -------------------------------------------------------------------------------------const halfW = (y) => y >= WAIST ? 3.4 + 4.3 * Math.pow((y - WAIST) / (GROUND - 1 - WAIST), 1.4) : 3.4 + 3.2 * Math.pow((WAIST - y) / (WAIST - TOP), 1.15);const centre = (y, bend) => CX + (y < WAIST ? bend * Math.pow((WAIST - y) / (WAIST - TOP), 2) : 0);function stalks(g, bend) { g.part("stalks", () => g.field({ x: [5, 27], y: [TOP - 1, GROUND] }, (x, y) => { const hw = halfW(y), u = (x + 0.5 - centre(y, bend)) / hw; const sid = Math.floor(((u + 1) / 2) * 9); // ragged cut ends at the foot: alternate stalks reach one pixel lower if (y >= GROUND - 1 && (sid % 2 === 1 || y === GROUND)) return null; if (Math.abs(u) > 1) return null; const f = ((u + 1) / 2) * 9 - sid; const lit = -0.85 * u + 0.35 * Math.sqrt(1 - u * u) - (y > WAIST ? 0 : 0.1); let k = lit > 0.6 ? 3 : lit > 0.15 ? 2 : lit > -0.35 ? 1 : 0; if (f < 0.22 && k > 0) k--; // the gap between two stalks if (y === GROUND - 2 && k < 3) k++; // light catching the cut ends if (y < WAIST && y > WAIST - 3 && k > 0) k--; // pinched just above the tie if (k === 3 && f > 0.55 && f < 0.8 && (y % 5 === 1)) return STRAW[4]; // a node glinting on a lit stalk return STRAW[k]; }));}function stubble(g, gust, front) { const lean = Math.round(gust * 1.2); g.part(front ? "stubbleFront" : "stubbleBack", () => { const list = front ? [[4, 2], [7, 3], [10, 1], [21, 2], [24, 3], [27, 2]] : [[3, 2], [6, 1], [9, 3], [12, 2], [20, 3], [23, 1], [26, 2], [28, 1]]; for (const [x, h] of list) { const yb = front ? GROUND + 1 : GROUND - 1; g.line([x, yb], [x + (h > 1 ? lean : 0), yb - h], h > 2 ? "w3" : "w2"); g.pixel(x + (h > 1 ? lean : 0), yb - h, "g1"); } });}// ---- ears ----------------------------------------------------------------------------------------------------const EARS = [];for (let i = 0; i < 11; i++) { const s = (i / 10) * 2 - 1; EARS.push({ s, front: i % 2 === 0, len: 6.2 + (1 - Math.abs(s)) * 1.3, lift: (1 - s * s) * 3 });}function earGeom(e, bend, swayAt) { const by = TOP + 1 - e.lift + (e.front ? 0 : -1.5); const bx = centre(by, bend) + e.s * 5.4; const th = e.s * 0.78 + swayAt(e.s); // radians from vertical, + = to the right const droop = e.s * 0.35 + swayAt(e.s) * 0.5; // the heavy head nods further const d1 = [Math.sin(th), -Math.cos(th)], d2 = [Math.sin(th + droop), -Math.cos(th + droop)]; const p0 = [bx, by], p1 = [bx + d1[0] * e.len * 0.5, by + d1[1] * e.len * 0.5]; const p2 = [p1[0] + d2[0] * e.len * 0.55, p1[1] + d2[1] * e.len * 0.55]; return { p0, p1, p2, dir: d2 };}function ear(g, e, bend, swayAt) { const { p0, p1, p2 } = earGeom(e, bend, swayAt); const dx = p2[0] - p0[0], dy = p2[1] - p0[1], len = Math.hypot(dx, dy) || 1; const nl = (-dy / len) * L[0] + (dx / len) * L[1]; // does +v face the light? const ramp = e.front ? EAR_F : EAR_B; // a short bare neck of stalk, then the plump head of grain g.line([Math.round(p0[0]), Math.round(p0[1])], [Math.round(p1[0]), Math.round(p1[1])], e.front ? "w4" : "g1"); g.sweep([[p0[0] + (p1[0] - p0[0]) * 0.45, p0[1] + (p1[1] - p0[1]) * 0.45], p1, p2], { w0: 1.25, w1: 0.6, profile: 0.7 }, (u, v) => { let l = v * nl * 1.3 + 0.3 - u * 0.25; if (Math.abs(v) > 0.55 && v * nl < 0) l -= 0.5; // the shadow flank parts it from its neighbour if (Math.floor(u * 6) % 2 === 1 && Math.abs(v) < 0.5) l -= 0.35; // rows of kernels down the middle return ramp[l > 0.75 ? 3 : l > 0.15 ? 2 : l > -0.45 ? 1 : 0]; });}function awns(g, bend, swayAt, front) { g.detached(front ? "awnsFront" : "awnsBack", () => { for (const e of EARS) { if (e.front !== front) continue; const { p2, dir } = earGeom(e, bend, swayAt); const a = [Math.round(p2[0] + dir[0] * 1.2), Math.round(p2[1] + dir[1] * 1.2)]; const b = [Math.round(p2[0] + dir[0] * 3.4 + dir[1] * 0.8), Math.round(p2[1] + dir[1] * 3.4 - dir[0] * 0.8)]; g.line(a, b, front ? "w4" : "g1"); } });}function crown(g, bend, swayAt) { g.outlined("ink", () => g.part("earsBack", () => { for (const e of EARS) if (!e.front) ear(g, e, bend, swayAt); })); awns(g, bend, swayAt, false);}function crownFront(g, bend, swayAt) { g.outlined("ink", () => g.part("earsFront", () => { for (const e of EARS) if (e.front) ear(g, e, bend, swayAt); })); awns(g, bend, swayAt, true);}// ---- tie, ribbon tails -----------------------------------------------------------------------------------------function tie(g, flutter) { g.part("tie", () => { for (let x = 12; x <= 20; x++) { const k = x < 14 ? "r2" : x < 18 ? "r1" : "r0"; g.pixel(x, WAIST, k); g.pixel(x, WAIST + 1, x < 16 ? "r1" : "r0"); } g.pixel(12, WAIST, "r3"); for (let x = 13; x <= 19; x += 2) g.pixel(x, WAIST - 1, "w3"); // twine wrap above the ribbon g.pixel(13, WAIST - 1, "w4"); }); g.part("bow", () => { // loops either side of a knot at the front-left g.fillPoly([[13, WAIST], [10, WAIST - 2], [10, WAIST + 1]], "r2"); g.fillPoly([[13, WAIST], [16, WAIST - 2], [16, WAIST + 1]], "r1"); g.pixel(10, WAIST - 2, "r3"); g.pixel(16, WAIST + 1, "r0"); g.rect({ x: [12, 13], y: [WAIST, WAIST + 1] }, "r0", { fill: "r0" }); g.pixel(12, WAIST, "r2"); }); g.part("tails", () => { // two short ribbon ends hanging from the knot, two pixels wide for (const [x0, ph, dir, n] of [[11, 0, -1, 5], [13, 0.3, 1, 4]]) { for (let i = 1; i <= n; i++) { const x = Math.round(x0 + dir * i * 0.3 + flutter(i / n, ph) * (i / n)); const y = WAIST + 1 + i; g.pixel(x, y, i === n ? "r1" : "r2"); g.pixel(x + 1, y, i === n ? "r0" : "r1"); } } });}// ---- sickle ----------------------------------------------------------------------------------------------------function sickle(g, festive) { g.part("sickleGrip", () => { g.line([26, 44], [24, 37], "w3"); g.line([27, 44], [25, 37], "w1"); g.line([26, 43], [25, 40], "w4"); g.pixel(27, 44, "w0"); if (festive) { g.pixel(25, 40, "b2"); g.pixel(26, 40, "b1"); g.pixel(24, 39, "b2"); } // a blue ribbon wound on the grip }); g.part("ferrule", () => { g.rect({ x: [23, 24], y: [35, 36] }, "n2", { fill: "n2" }); g.pixel(23, 35, "n4"); }); g.part("blade", () => g.sweep([[23.5, 34.5], [27, 31.5], [28.5, 27], [26, 23]], { w0: 1.3, w1: 0.45, profile: 0.8 }, (u, v) => { if (v < -0.45) return "n5"; // honed inner edge catches the light return v > 0.4 ? "n1" : u < 0.5 ? "n3" : "n2"; }));}// ---- drifting grains / loose stalks / chaff -----------------------------------------------------------------function grains(g, t, n, speed) { g.detached("grains", () => { for (let i = 0; i < n; i++) { const p = (t * speed + i / n) % 1; const x = Math.round(18 + i * 3 + p * 9 + Math.sin(TAU * (p + i * 0.3)) * 1.5), y = Math.round(12 + i * 2 + p * 26); if (p > 0.85 || x > W - 1) continue; g.pixel(x, y, p < 0.5 ? "g2" : "g1"); } });}function looseStalks(g, t) { // three whole stalks tumbling across on the wind (each one wraps from the right edge back to the left) g.outlined("ink", () => g.part("looseStalks", () => { for (let i = 0; i < 3; i++) { const p = (t + i / 3) % 1; const x = -9 + p * (W + 18), y = 7 + i * 10 + Math.sin(TAU * (p * 1.5 + i * 0.4)) * 3; const a = TAU * (p + i * 0.3); // end over end, once per crossing const c = Math.cos(a), sn = Math.sin(a) * 0.7; const s0 = [Math.round(x - c * 4), Math.round(y - sn * 4)], s1 = [Math.round(x + c * 1), Math.round(y + sn * 1)]; const e1 = [Math.round(x + c * 4), Math.round(y + sn * 4)]; g.line(s0, s1, "w3"); g.line(s1, e1, "g2"); g.line([s1[0], s1[1] - 1], [e1[0], e1[1] - 1], "g1"); // the plump head, two pixels thick g.pixel(Math.round(x + c * 2.5), Math.round(y + sn * 2.5) - (Math.abs(c) > 0.5 ? 1 : 0), "g1"); g.pixel(e1[0], e1[1], "g3"); } })); g.detached("chaff", () => { for (let i = 0; i < 4; i++) { // short chaff streaks const p = (t * 2 + i / 4) % 1; const x = Math.round(-2 + p * (W + 4)), y = 5 + ((i * 11) % 34); g.pixel(x, y, "w4"); g.pixel(x - 1, y, "g1"); } });}// ---- festival dressing -----------------------------------------------------------------------------------------function star(g, t, bend) { const c = [Math.round(centre(TOP, bend)), 6]; const spin = 0.12 * Math.sin(TAU * t); g.outlined("ink", () => { g.part("starStick", () => g.line([c[0], c[1] + 4], [Math.round(centre(TOP + 1, bend)), TOP - 3], "w3")); g.part("star", () => { for (let k = 0; k < 8; k++) { const a = spin + (k * TAU) / 8 - Math.PI / 2, long = k % 2 === 0; const r = long ? 5.6 : 3.8; const tip = [Math.round(c[0] + Math.cos(a) * r), Math.round(c[1] + Math.sin(a) * r)]; const lit = L[0] * Math.cos(a) + L[1] * Math.sin(a); g.line(c, tip, lit > 0.3 ? "g3" : lit > -0.3 ? "g2" : "g1"); } // woven ring tying the straws g.strokeEllipse({ x: [c[0] - 2, c[0] + 2], y: [c[1] - 2, c[1] + 2] }, "g1"); g.pixel(c[0] - 1, c[1] - 2, "g3"); g.pixel(c[0] + 2, c[1] + 1, "g0"); g.pixel(c[0], c[1], "r1"); }); });}function flowers(g) { g.part("flowers", () => { // poppies and cornflowers tucked under the ribbon for (const [x, y] of [[18, WAIST - 2], [20, WAIST - 1]]) { g.pixel(x, y, "r2"); g.pixel(x + 1, y, "r1"); g.pixel(x, y - 1, "r3"); g.pixel(x + 1, y + 1, "ink"); } for (const [x, y] of [[9, WAIST - 1], [15, WAIST - 3]]) { g.pixel(x, y, "b2"); g.pixel(x - 1, y, "b1"); g.pixel(x, y - 1, "b3"); } g.pixel(17, WAIST - 3, "e2"); g.pixel(10, WAIST - 2, "e1"); });}function streamers(g, t) { // a red and a blue streamer from the bow, two pixels wide, rippling as the breeze runs down them g.outlined("ink", () => g.part("streamers", () => { for (const [lit, dark, ph, x0, dir] of [["r2", "r1", 0, 10, -1], ["b2", "b1", 0.4, 14, 1]]) { for (let i = 1; i <= 11; i++) { const q = i / 11; const wave = Math.sin(TAU * (t - q * 0.9 + ph)); const x = Math.round(x0 + dir * q * 2.5 + wave * 1.6 * q), y = WAIST + 1 + i; const face = Math.cos(TAU * (t - q * 0.9 + ph)) > -0.2; // the ribbon twists: lit face or back g.pixel(x, y, face ? lit : dark); g.pixel(x + 1, y, face ? dark : (lit === "r2" ? "r0" : "b0")); } } }));}// ---- assembly --------------------------------------------------------------------------------------------------function sheaf(g, t, cfg) { contactShadow(g, CX, GROUND, 11, 2); g.outlined("ink", () => stubble(g, cfg.gust, false)); crown(g, cfg.bend, cfg.sway); g.outlined("ink", () => stalks(g, cfg.bend)); crownFront(g, cfg.bend, cfg.sway); g.outlined("ink", () => { tie(g, cfg.flutter); }); if (cfg.festival) flowers(g); g.outlined("ink", () => sickle(g, cfg.festival)); g.outlined("ink", () => stubble(g, cfg.gust, true));}const WheatSheaf = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; if (mode === "gust") { const wob = Math.sin(TAU * 3 * t); // fast flutter on a hard lean const bend = 2 + Math.round(wob * 0.6); sheaf(g, t, { bend, gust: 1, sway: (s) => 0.5 + 0.12 * Math.sin(TAU * 3 * t - s * 2) - s * 0.35 - Math.max(0, s) * 0.15, flutter: (q, ph) => 2.2 + Math.sin(TAU * (3 * t - q + ph)) * 1.1, }); looseStalks(g, t); grains(g, t, 3, 2); return; } const breeze = Math.sin(TAU * t); const bend = Math.round(breeze * 1.1); const cfg = { bend, gust: breeze * 0.5, sway: (s) => 0.13 * Math.sin(TAU * t - s * 1.3), flutter: (q, ph) => Math.sin(TAU * (t - q * 0.8 + ph)) * 0.9, festival: mode === "festival", }; sheaf(g, t, cfg); if (cfg.festival) { star(g, t, bend); streamers(g, t); } else grains(g, t, 2, 1);});submit(WheatSheaf, { 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: "gust", args: ["gust"], animate: { fps: 10, duration: N_GUST / 10, loop: true } }, { name: "festival", args: ["festival"] }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。