Witch's Hut · jack_o_lantern.js
ジャック・オー・ランタン
32×32 の床置きの小物で、筋の入ったかぼちゃに切り込みの蓋、巻いたつるに葉、三角の目と 2 本の歯がのぞくギザギザの口を彫ってあります。中のろうそくが彫った穴を内側から照らし、蓋のすき間から光が漏れ、手前の床にディザの光だまりを落とします。中の光は階調を差し替えるだけで fire(炎)、witch(魔女火)、soul(魂火)に変わり、煙が細く上がる unlit(消灯)とあわせて 12 コマのループです。ignite(点火)は 8 コマの一回再生で、fire の最初のコマで終わります。
6×
動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
jack_o_lantern.js · 1–10
// Jack-o'-lantern — 32x32 floor prop, top-down 3/4, key light top-left.// A ribbed pumpkin with a cut lid (seam around the stem), a curled vine and leaf, and a carved face: triangle eyes,// a nose and a jagged grin with two teeth. The candle inside lights the carved holes from within; light leaks// through the lid seam and spills onto the floor in front (detached, dithered pool).// Variants (the inner light is the fl0..3 family, recoloured by ramp swap — no new colours):// fire warm candle, flicker (12f loop)// witch violet witchfire (12f loop)// soul blue soulfire (12f loop)// unlit dark carved face, a thin wisp of smoke from the seam (12f loop)// ignite once, 8f: a spark catches, the light flares and settles; the last frame equals fire frame 0最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
jack_o_lantern.js · 212–224
submit(JackOLantern, { size: [W, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_LOOP / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["fire"], variants: [ { name: "fire", args: ["fire"] }, { name: "witch", args: ["fire"], repalette: swap("fl", "witch") }, { name: "soul", args: ["fire"], repalette: swap("fl", "soul") }, { name: "unlit", args: ["unlit"] }, { name: "ignite", args: ["ignite"], animate: { fps: 10, duration: N_IGN / 10, loop: false } }, ],});プログラム全体を表示 (224 行)
jack_o_lantern.js · 224 行
// Jack-o'-lantern — 32x32 floor prop, top-down 3/4, key light top-left.// A ribbed pumpkin with a cut lid (seam around the stem), a curled vine and leaf, and a carved face: triangle eyes,// a nose and a jagged grin with two teeth. The candle inside lights the carved holes from within; light leaks// through the lid seam and spills onto the floor in front (detached, dithered pool).// Variants (the inner light is the fl0..3 family, recoloured by ramp swap — no new colours):// fire warm candle, flicker (12f loop)// witch violet witchfire (12f loop)// soul blue soulfire (12f loop)// unlit dark carved face, a thin wisp of smoke from the seam (12f loop)// ignite once, 8f: a spark catches, the light flares and settles; the last frame equals fire frame 0const W = 32, CX = 16, GROUND = 29;const P = { cx: 16, cy: 20, rx: 11, ry: 8.6 };const SKIN = ["r0", "r1", "r2", "r2"];const N_LOOP = 12, N_IGN = 8;// ---- pumpkin body -------------------------------------------------------------------------------------------function pumpkin(g) { const { cx, cy, rx, ry } = P; g.part("pumpkin", () => g.field({ x: [cx - rx, cx + rx], y: [Math.floor(cy - ry), Math.ceil(cy + ry)] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; // squash the silhouette a little at the top (shoulders) so it reads as a pumpkin, not an egg const sq = ny < 0 ? ny * 1.06 : ny; const r2 = nx * nx + sq * sq; if (r2 > 1) return null; const nz = Math.sqrt(Math.max(0, 1 - r2)); const phi = Math.asin(Math.max(-1, Math.min(1, nx / Math.sqrt(Math.max(0.05, 1 - sq * sq))))); const rib = Math.cos(phi * 5); // 1 at a lobe's crown, -1 in a groove let d = -0.55 * nx - 0.45 * ny + 0.7 * nz + 0.22 * rib; const k = Math.max(0, Math.min(0.999, (d + 0.05) / 1.2)); let key = SKIN[Math.floor(k * 4)]; if (rib < -0.6 && Math.abs(ny) < 0.95) key = key === "r3" ? "r2" : "r1"; // groove: continuous darker line // lit stripe on the crown of the upper-left lobes (sharp key-light catch) if (rib > 0.55 && (nx < -0.5 || ny < -0.55) && ny < 0.4 && d > 0.62) key = d > 0.85 && ny < -0.2 ? "g2" : "r3"; // dent around the stem on the top face const tx = (x + 0.5 - cx) / 4.5, ty = (y + 0.5 - (cy - ry + 1.6)) / 1.6; if (tx * tx + ty * ty < 1) key = tx < -0.2 ? "r1" : "r0"; return key; }));}// lid seam: a jagged cut around the stem; lit variants leak light through itfunction seam(g, t, light) { const pts = []; for (let i = 0; i <= 18; i++) { const a = Math.PI * (i / 18); // the near half of the cut (front arc) const x = Math.round(CX + Math.cos(a) * 5.4); const y = Math.round(P.cy - P.ry + 2.2 + Math.sin(a) * 1.6 + ((i & 1) ? 0.6 : 0)); pts.push([x, y]); } g.part("lidSeam", () => { let prev = null; for (const [x, y] of pts) { if (prev && prev[0] === x && prev[1] === y) continue; const leak = light > 0 && ((x * 3 + y) % 4 === Math.floor(t * N_LOOP * 0.5) % 4 || light > 1.2); g.pixel(x, y, leak ? (light > 0.8 ? "fl3" : "fl2") : "r0"); prev = [x, y]; } });}function stemVine(g) { g.part("stem", () => { // a short twisted stem leaning right, cut flat on top g.fillPoly([[14, 13], [18, 13], [19, 9], [18, 7], [16, 7], [15, 9]], "w1"); g.line([15, 12], [16, 8], "w3"); g.line([16, 12], [17, 8], "w2"); g.pixel(18, 11, "w0"); g.pixel(18, 12, "w0"); g.rect({ x: [16, 18], y: [6, 6] }, "w2", { fill: "w2" }); g.pixel(16, 6, "w4"); g.pixel(17, 6, "w3"); }); g.part("vine", () => { // tendril curling off the stem to the left, ending in a small spiral g.line([14, 11], [12, 10], "e1"); g.pixel(11, 9, "e1"); g.pixel(10, 8, "e1"); g.pixel(9, 8, "e1"); g.pixel(8, 9, "e1"); g.pixel(9, 10, "e2"); g.pixel(10, 10, "e1"); }); g.part("leaf", () => { // broad leaf lying on the pumpkin's right shoulder, lit edge top-left, vein down the middle g.fillPoly([[19, 10], [23, 8], [26, 9], [25, 12], [21, 13]], "e1"); g.line([20, 10], [23, 8], "e3"); g.line([23, 9], [25, 9], "e2"); g.line([20, 11], [24, 10], "e0"); g.pixel(24, 12, "e0"); g.pixel(22, 12, "e0"); });}// ---- carved face --------------------------------------------------------------------------------------------// carved holes, hand-placed (row -> [x0,x1] spans); hole(x,y) returns "eye" | "nose" | "mouth" | nullconst HOLES = { eye: { 15: [[11, 11], [21, 21]], 16: [[10, 12], [20, 22]], 17: [[9, 13], [19, 23]], 18: [[9, 13], [20, 23]] }, nose: { 19: [[16, 16]], 20: [[15, 17]] }, mouth: { 21: [[8, 8], [24, 24]], 22: [[8, 11], [21, 24]], 23: [[9, 12], [14, 18], [20, 23]], 24: [[10, 15], [17, 22]], 25: [[12, 15], [17, 20]], },};function hole(x, y) { for (const k in HOLES) { const row = HOLES[k][y]; if (row && row.some(([a, b]) => x >= a && x <= b)) return k; } return null;}// light: 0 = unlit, ~1 = steady candle, >1 = flare. fl keys follow the variant's flame ramp.function face(g, t, light) { const LC = { x: 16, y: 21 }; const fl = light > 0 ? 0.5 + 0.28 * Math.sin(TAU * 2 * t) + 0.22 * Math.sin(TAU * (3 * t + 0.35)) : 0; g.part("face", () => g.field({ x: [7, 25], y: [14, 26] }, (x, y) => { const h = hole(x, y); if (!h) return null; const left = hole(x - 1, y) === h, right = hole(x + 1, y) === h, below = hole(x, y + 1) === h; if (light <= 0) { // unlit: the cut wall shows pale flesh along the lower/right inside edge, the cavity is dark if (!right || !below) return left || !below ? "g0" : "ink"; return "ink"; } // lit: the cut wall on the hole's left side is in shade; the cavity glows, brightest near the candle const d = Math.hypot(x + 0.5 - LC.x, (y + 0.5 - LC.y) * 1.3) / Math.max(0.25, light); if (light < 0.9) { // kindling: a small core, the rest of the cavity still dark const dd = Math.hypot(x + 0.5 - LC.x, (y + 0.5 - LC.y) * 1.3); return dd < 2 + light * 3 ? "fl3" : dd < 3 + light * 6 ? "fl1" : "ink"; } const edge = !right || !below || !hole(x, y - 1); if (!left && light < 1.3) return d < 5 ? "fl1" : "fl0"; if (d < 6.5 + fl * 3) return edge && d > 3 ? "fl2" : "fl3"; return edge ? "fl1" : "fl2"; })); // rim of each hole: a 1px darker cut edge on the skin around the hole's top (the carved bevel catches no light) g.part("faceBevel", () => g.field({ x: [7, 25], y: [14, 26] }, (x, y) => { if (hole(x, y)) return null; return hole(x, y + 1) && !hole(x - 1, y) && !hole(x + 1, y) ? "r0" : null; }));}// ---- light on the floor, smoke ------------------------------------------------------------------------------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 floorLight(g, t, light) { if (light <= 0) return; const br = Math.min(1.3, light) * (1 + 0.08 * Math.sin(TAU * 2 * t)); g.detached("floorLight", () => g.field({ x: [0, W - 1], y: [25, 31] }, (x, y) => { const nx = (x + 0.5 - CX) / (12 * br), ny = (y + 0.5 - (GROUND + 1.2)) / (2.4 * br); const r = Math.sqrt(nx * nx + ny * ny); if (r > 1) return null; // the carved face throws three bright lobes forward (eyes + grin): a light fan, broken by the pumpkin's walls const fan = Math.abs(Math.sin((nx * 3.1) + 0.3)); const dens = (r < 0.45 ? 0.5 : r < 0.75 ? 0.3 : 0.12) * (0.5 + 0.5 * fan); if (!dith(x, y, dens)) return null; return r < 0.5 ? "fl2" : r < 0.8 ? "fl1" : "fl0"; }));}function glowSparks(g, t, light) { if (light <= 0) return; g.detached("heat", () => { // two sparks escape from the seam and drift up for (let k = 0; k < 2; k++) { const p = (t * (k ? 1 : 2) + k * 0.5) % 1; if (p > 0.6) continue; const x = CX + (k ? 4 : -3) + Math.round(Math.sin(TAU * (p + k * 0.3)) * 1.5); const y = 12 - Math.round(p * 11); g.pixel(x, y, p < 0.3 ? "fl3" : "fl1"); } });}function smoke(g, t) { g.detached("smoke", () => { for (let i = 0; i < 3; i++) { const p = (t + i / 3) % 1; const x = Math.round(CX + 3 + Math.sin(TAU * (p * 1.2 + i * 0.15)) * 2 + p * 3); const y = Math.round(11 - p * 11); if (y < 0) continue; if (p < 0.4) { g.pixel(x, y, "n3"); g.pixel(x, y + 1, "n2"); } else if (p < 0.75) g.pixel(x, y, "n2"); else if (chk(x, Math.floor(t * N_LOOP))) g.pixel(x, y, "n1"); } });}function draw(g, t, light, mode) { contactShadow(g, CX, GROUND, 11, 2); floorLight(g, t, light); g.outlined("ink", () => { pumpkin(g); stemVine(g); }); seam(g, t, light); face(g, t, light); if (mode === "unlit") smoke(g, t); else glowSparks(g, t, light);}const JackOLantern = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; if (mode === "unlit") return draw(g, t, 0, mode); if (mode === "ignite") { const f = Math.min(N_IGN - 1, Math.round(t * N_IGN)); if (f === N_IGN - 1) return draw(g, 0, 1, "fire"); // == fire frame 0 const light = [0, 0, 0.35, 0.7, 1.45, 1.25, 1.1][f]; draw(g, f * 0.13, light, "ignite"); if (f <= 1) g.detached("spark", () => { // the match-spark inside the mouth g.pixel(15, 23, "fl3"); if (f === 1) { g.pixel(16, 22, "fl2"); g.pixel(14, 22, "fl2"); } }); return; } draw(g, t, 1, mode);});submit(JackOLantern, { size: [W, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_LOOP / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["fire"], variants: [ { name: "fire", args: ["fire"] }, { name: "witch", args: ["fire"], repalette: swap("fl", "witch") }, { name: "soul", args: ["fire"], repalette: swap("fl", "soul") }, { name: "unlit", args: ["unlit"] }, { name: "ignite", args: ["ignite"], animate: { fps: 10, duration: N_IGN / 10, loop: false } }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。