Arcane Curiosities Vol.1 · skull_candles.js
髑髏と蝋燭
32×32 の床置きの小物で、頭のてっぺんに溶けたろうそくを載せた骨の髑髏と、その周りの高い・中くらい・短いろうそく 3 本です。12 コマのループで、炎はそれぞればらばらに揺れ、高いろうそくの炎はループの途中ですきま風に傾きます。炎は階調を一つ差し替えて炎・魂火・魔女火になり、新しい色は足していません。
6×
動くスプライト素材: Arcane Curiosities Vol.1(itch.io)動く小物 12 点・52 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
skull_candles.js · 1–4
// Skull & candles — 32x32 floor cluster, top-down 3/4, key light top-left.// A bone skull with a melted candle on its crown, plus three candles (tall / medium / stub) around it.// Loop: 12 frames @ 10 fps. Each flame flickers on its own seed; the tall one leans in a draft mid-loop.// Variants: flame colour fire / soul / witch (fl ramp swap — no new colours).最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
skull_candles.js · 162–171
submit(SkullCandles, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 16, y: 29 }, variants: [ { name: "idle-fire" }, { name: "idle-soul", repalette: swap("fl", "soul") }, { name: "idle-witch", repalette: swap("fl", "witch") }, ],});プログラム全体を表示 (171 行)
skull_candles.js · 171 行
// Skull & candles — 32x32 floor cluster, top-down 3/4, key light top-left.// A bone skull with a melted candle on its crown, plus three candles (tall / medium / stub) around it.// Loop: 12 frames @ 10 fps. Each flame flickers on its own seed; the tall one leans in a draft mid-loop.// Variants: flame colour fire / soul / witch (fl ramp swap — no new colours).const GROUND = 29;const SK = 16.5; // skull centre line (symmetric about column 16)const WAX = { 3: ["n5", "w4", "w3"], 4: ["n5", "n5", "w4", "w3"], 5: ["n5", "n5", "w4", "w4", "w3"] };const DIM = { n5: "w4", w4: "w3", w3: "w2" }; // wax is translucent: shallow low steps, one step dimmer near the floor// x0 = left column, w = width, top = top-face row, base = bottom row, h = flame height// drips: [column offset (-1 = bulge left, w = bulge right, else down the face), length]const CANDLES = { A: { x0: 7, w: 4, top: 9, base: 21, drips: [[-1, 4], [1, 7], [4, 2]], seed: 0.0, h: 6, gust: true }, B: { x0: 24, w: 4, top: 14, base: 22, drips: [[0, 3], [4, 4]], seed: 0.37, h: 5 }, C: { x0: 2, w: 3, top: 23, base: 28, drips: [[0, 2], [3, 2]], seed: 0.71, h: 5 }, S: { x0: 14, w: 4, top: 12, base: 16, drips: [[-1, 2], [2, 1], [4, 3]], seed: 0.52, h: 6 },};// ---- flames -----------------------------------------------------------------------------------// periodic flicker drivers (integer frequencies so t=1 closes the loop)function flick(t, s) { const dh = Math.round(0.75 * Math.sin(TAU * (2 * t + s)) + 0.45 * Math.sin(TAU * (3 * t + 2.3 * s))); const lean = 0.7 * Math.sin(TAU * (t + s)) + 0.45 * Math.sin(TAU * (3 * t + 1.7 * s)); return { dh, lean };}// a draft: smooth bump over t in [0.35, 0.8], zero elsewhere (so the loop still closes)function gust(t) { const u = (t - 0.35) / 0.45; return u <= 0 || u >= 1 ? 0 : Math.pow(Math.sin(Math.PI * u), 2);}// Small candle flame, built the pixel-art way: rows of odd width stacked on a staircase axis that// bends with lean. Edge pixels are the dark shell (fl0, warming to fl1 where the flame is fattest);// the axis runs body -> core -> bright -> body -> shell tip. fx = wick column, fy = lowest flame row.function candleFlame(g, fx, fy, h, lean) { for (let i = 0; i < h; i++) { const k = (i + 0.5) / h, y = fy - i; const ax = fx + Math.round(lean * k * k); const r = k > 0.12 && k < 0.62 ? 1 : 0; if (r) { const edge = k > 0.18 && k < 0.5 ? "fl1" : "fl0"; g.pixel(ax - 1, y, edge); g.pixel(ax + 1, y, edge); } g.pixel(ax, y, k < 0.12 ? "fl1" : k < 0.5 ? "fl3" : k < 0.68 ? "fl2" : k < 0.86 ? "fl1" : "fl0"); }}function flames(g, t) { for (const [name, c] of Object.entries(CANDLES)) { const f = flick(t, c.seed); const lean = f.lean + (c.gust ? 2.6 * gust(t) : 0); const fx = c.x0 + (c.w === 3 ? 1 : 2), h = c.h + f.dh; g.part("flame" + name, () => { g.pixel(fx, c.top - 1, "ink"); // wick candleFlame(g, fx, c.top - 2, h, lean); }); // as a tall flame shrinks back, its tip tears off and floats up for a frame or two const ph = TAU * (2 * t + c.seed); const ly = c.top - 2 - h - 1; if (c.h >= 6 && ly >= 0 && Math.sin(ph) > 0.2 && Math.cos(ph) < -0.2) { g.detached("lick" + name, () => g.pixel(fx + Math.round(lean), ly, "fl0")); } }}// ---- wax ----------------------------------------------------------------------------------------// wax dims a step once it is far below its flame; the lit column dims later than the shaded one,// so the falloff boundary curves round the cylinderfunction waxAt(c, key, y, i) { const u = i === undefined ? 0.5 : i / Math.max(1, c.w - 1); return y - c.top > 8 - 3 * u ? DIM[key] : key;}function candle(g, name, c) { g.part(name, () => { const cols = WAX[c.w]; for (let i = 0; i < c.w; i++) for (let y = c.top + 1; y <= c.base; y++) g.pixel(c.x0 + i, y, waxAt(c, cols[i], y, i)); // top face: lit rim, the far-right corner slumped where the wax overflowed g.line([c.x0, c.top], [c.x0 + c.w - 2, c.top], "n5"); g.pixel(c.x0 + c.w - 1, c.top + 1, "w4"); g.pixel(c.x0 + 1, c.top + 1, "fl3"); // flame light through the wax for (const [dx, len] of c.drips) { const x = c.x0 + dx, y0 = c.top + 1, y1 = c.top + len; for (let y = y0; y <= y1; y++) { if (dx < 0) g.pixel(x, y, waxAt(c, "n5", y, 0)); else if (dx >= c.w) g.pixel(x, y, y === y0 ? "w4" : waxAt(c, "w3", y, c.w - 1)); else g.pixel(x, y, waxAt(c, dx < c.w / 2 ? "n5" : "w4", y, dx)); } if (dx >= 0 && dx < c.w) g.pixel(x, y1 + 1, waxAt(c, "w3", y1 + 1, dx)); // the drip's bead casts a shadow } });}function puddle(g, name, cx, cy, rx, ry) { g.part(name, () => 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; if (nx * nx + ny * ny > 1) return null; if (ny > 0.35) return "w3"; return nx < -0.2 && ny < 0 ? "n5" : "w4"; }));}// ---- skull --------------------------------------------------------------------------------------function skull(g) { g.part("skull", () => { // cranium: a lit sphere (bone ramp n2..n5) g.field({ x: [9, 23], y: [15, 26] }, (x, y) => { const i = litIndex(x, y, SK, 21, 6.5, 5.6, 4, -0.05); return i < 0 ? null : ["n2", "n3", "n4", "n5"][i]; }); // face: cheekbones, maxilla and jaw narrowing downwards; front-facing, so a step darker than the dome g.field({ x: [10, 23], y: [23, 28] }, (x, y) => { const dx = x + 0.5 - SK; const hw = y <= 24 ? 6.2 : y === 25 ? 5.2 : y <= 27 ? 4.4 : 3.2; if (Math.abs(dx) > hw) return null; const side = dx / hw; return side > 0.5 || y >= 28 ? "n2" : side < -0.45 ? "n4" : "n3"; }); // eye sockets: deep ink hollows under the brow ridge for (const [a, b, s] of [[12, 14, -1], [18, 20, 1]]) { g.line([a, 20], [b, 20], s < 0 ? "n3" : "n2"); g.line([a, 21], [b, 21], "ink"); g.line([a, 22], [b, 22], "ink"); g.line([s < 0 ? a + 1 : a, 23], [s < 0 ? b : b - 1, 23], "ink"); } g.pixel(11, 24, "n5"); g.pixel(21, 24, "n3"); // cheekbones g.pixel(16, 24, "ink"); g.pixel(15, 25, "n1"); g.pixel(16, 25, "ink"); g.pixel(17, 25, "n1"); // nose for (let x = 13; x <= 19; x++) g.pixel(x, 26, (x & 1) ? "n5" : "n2"); // upper teeth g.line([13, 27], [19, 27], "ink"); // bite for (let x = 14; x <= 18; x++) g.pixel(x, 28, (x & 1) ? "n4" : "n1"); // lower teeth g.pixel(22, 19, "fl1"); g.pixel(22, 20, "fl1"); // rim light from candle B // melt pooled on the crown; its lower lip casts a shadow on the bone g.field({ x: [11, 22], y: [15, 19] }, (x, y) => { const nx = (x + 0.5 - 16) / 3.7, ny = (y + 0.5 - 16.6) / 1.3; if (nx * nx + ny * ny <= 1) return ny > 0.4 ? "w3" : nx < -0.25 ? "n5" : "w4"; const sy = (y + 0.5 - 16.6) / 1.9; return nx * nx + sy * sy <= 1 && sy > 0 ? "n2" : null; }); });}function crownDrips(g) { g.part("crown drips", () => { g.line([12, 18], [12, 19], "w4"); g.pixel(12, 20, "w3"); g.line([20, 18], [20, 20], "w4"); g.pixel(20, 21, "w3"); });}// ---- sprite -------------------------------------------------------------------------------------const SkullCandles = sprite(pal, (g, t) => { const { A, B, C, S } = CANDLES; contactShadow(g, 15, GROUND, 14, 2); g.outlined("ink", () => { puddle(g, "puddleA", 9, 21.5, 4, 1.5); candle(g, "candleA", A); }); g.outlined("ink", () => { puddle(g, "puddleB", 26, 22, 3.6, 1.5); candle(g, "candleB", B); }); g.outlined("ink", () => { puddle(g, "puddle", SK, 28.3, 7.4, 1.3); skull(g); }); g.outlined("ink", () => candle(g, "candleS", S)); crownDrips(g); g.outlined("ink", () => { puddle(g, "puddleC", 3.5, 28, 2.8, 1.2); candle(g, "candleC", C); }); flames(g, t);});submit(SkullCandles, { size: [32, 32], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: 1.2, loop: true }, anchor: { x: 16, y: 29 }, variants: [ { name: "idle-fire" }, { name: "idle-soul", repalette: swap("fl", "soul") }, { name: "idle-witch", repalette: swap("fl", "witch") }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。