pxl:用程式碼繪製像素藝術
您撰寫一個小的 .px.js 檔案;pxl 會輸出 PNG 與放大的預覽圖供您觀看。這個 DSL
的設計目的是避免重複繪製同一個像素:鏡像、自動輪廓線、調色板切換變體、可重複使用的部件、
基本圖形、畫筆與圖塊地圖。需要 Node ≥ 22.18,無需安裝步驟。
下方 PXL 代表 node <此技能所在目錄>/bin/pxl.js。
工作流程
- 撰寫
art.px.js(從下方的模板開始;examples/目錄內有範例)。 - 只建構您正在處理的部分:
PXL build art.px.js --only slime,heart - 查看結果:閱讀
out/_preview/<name>.png(放大圖片,棋盤格 = 透明區域,每個像素都有格線, 每 8 個像素一組較深的格線)。若需要精確的像素修正,請使用PXL ascii art.px.js <name>(按鍵 + x/y 尺規); 它比圖片更省成本。 - 用小幅編輯修正(一個
grid(x, y, ...)修補、一個運算),然後重新建構。重複此步驟。 - 如果使用者想要觀看:在背景啟動
PXL serve art.px.js並給他們網址。 它會在每次儲存時重新建構,並重播已編輯素材的繪畫步驟;同時也會寫入相同的out/檔案,所以照常閱讀預覽即可。
錯誤訊息會指出檔案:行號:欄號、素材/圖層/運算路徑,並建議修正方式(例如「您是指 'r' 嗎?」)。 單一素材錯誤不會阻止其他素材建構。
模板
pal({ k: "#1b1630", g: "#8ccf63", G: "#4c8a44", t: "#2a5560", w: "#fff6e0", r: "#e2585e", R: "#a63a4c" })
ramps("ktGg", "kRr") // 深色 -> 淺色,驅動 shade()/light()
// 對稱:寫左半邊,鏡像,加上輪廓線。
sprite("slime", { mx: true, outline: "k" }, `
....ggg
..ggGGg
.ggGwGg
gggkwgg
gggkkgg
tgggggg
.tttttt`)
sprite("slime_red", { base: "slime", swap: { g: "r", G: "R" } }) // 變體:一行程式碼
sprite("_eye", "wk\nkk") // "_" = 部件,不會被匯出
sprite("chest", { w: 16, h: 12, outline: "k" },
rect(0, 3, 16, 9, "R"), rect(0, 0, 16, 4, "r"), stamp("_eye", 7, 4), shade([0, 9, 16, 3]))
scene("meadow", { w: 160, h: 96, bg: "w" },
layer("ground", { y: 64 }, rect(0, 0, 160, 32, "G"), noise(0, 0, 160, 32, "g", 0.1, 3)),
layer("props", stamp("slime", 40, 54), stamp("slime_red", 70, 54, { flip: "x" })),
layer("shadow", { shade: true }, poly([38, 64, 88, 64, 92, 67, 34, 67], "k"))) // 投射陰影
sheet("chars", ["slime*", "chest"], { pad: 1 })
要點
- 顏色:來自
pal的單字元金鑰、"#hex"、"."跳過、"_"清除。預設調色板為 pico8 (0-f);可使用use("sweetie16" | "db16" | "endesga32" | "gray8")切換其他調色板。 - 項目依序執行:網格字串(在 0,0 繪製)、運算、
(c) => { c.rect(...) }函式、layer(...)。之後依序執行:鏡像 → 交換 → 翻轉 → 旋轉 → 輪廓線(每邊擴大 1 像素)→ 裁切。 - 形狀:
px rect box line(…, width) hline vline circ ring ellipse poly fill wire text。 - 色調:
dither grad noise、shade/light(區域, 步驟, 密度)、recolor,以及用於投射陰影和發光的shade:/light:圖層。區域:[cx,cy,r]、[x,y,w,h]、多邊形或一個函式。clip(區域, ...項目)可限制任何項目(半形狀、斜面)。 - 重複使用:
stamp tile pattern tilemap scatter。名稱可以是群組或萬用字元 (group("rocks", ["rock_a", "rock_a", "rock_b"])、"tree_*")。每次放置會隨機選取但結果可重現。使用rflip、rswap、seed和reseed: true來為每個副本增加變化。 - 畫筆:
scatter("_leaf*", x, y, w, h, 30, { on: "g", clip: true, rflip: "xy" })使用微小部件精靈繪製紋理,僅在指定顏色上繪製。 - 動畫:
anim("cat", { outline: "k", fps: 10 }, grid, frame(), frame(grid(23, 0, "_K"))): 影格是對共享項目或base的修補;或是frames: 6+(c) => ... c.phase ...來建立程序化循環。在場景中蓋印一個動畫會讓場景動起來(rdelay: true讓副本不同步,frame: n凍結某一影格)。查看_preview/<name>.png(所有影格並排顯示)或每個影格的pxl ascii。 - 發光:
glow("zgQ", { r: 6, strength: 0.5 })直接寫在 sprite/scene 主體中,會為這些色彩金鑰的每個像素加上光暈(火焰、藥水、月亮、霓虹燈);使用ramp: 2則會保持在調色板內。讓發光物件使用自己的色彩金鑰,這樣才不會影響其他部分,或使用where限制範圍。
完整 API 與所有選項:reference.md。 動態光照(燈光、被不透明圖層阻擋的光、天空光,Terraria 風格):僅在場景需要光照時才閱讀 lighting.md。
節省 token 並獲得更好藝術效果的習慣
- 對稱的東西只寫一半(
mx),讓outline繪製輪廓線。絕不手動繪製這兩者。 - 重新著色使用
base+swap;重複的細節使用_parts+stamp;同類系列使用group或 迴圈:for (let i = 1; i <= 3; i++) sprite("tree_" + i, ...)。 - 大面積:使用基本圖形、
grad、noise、pattern、tilemap。永遠不要用大型手寫網格。 - 有機紋理(樹葉、草地、石頭、鏽蝕):2–3 個微小畫筆部件 + 帶
on/clip的scatter。 - 不要用無意義的雜訊。 不要在牆壁、地板或灰泥上撒
noise來讓它們「看起來不那麼平坦」: 那看起來只會像隨機的點,不會像材質。表面上的每個標記都需要一個能說出名字的原因, 而且它應該出現在那個原因會造成的位置:- 人造結構:壁板/木板的線條、磚或石頭的砌縫(錯縫)、磁磚接縫;
- 風化:屋簷和窗台下的雨水污漬條紋、從地面向上漸淡的濺泥、 金屬支架下方的鏽蝕痕跡、木板上的剝落油漆(沿著木板紋理);
- 能講述故事的雜物:海報、管子、通風口、從角落開始的裂縫。
單色是完全可以的;乾淨的牆壁勝過滿是點的牆壁。
noise只適合用在材質本身 是顆粒狀的時候(礫石、沙子、柏油、瀝青屋頂、粗糙的石頭),而且要讓它稀疏。在從某個角度 繪製的臉部(3/4 視角)上,要讓標記順著臉部的傾斜方向。
- 光照:定義
ramps,然後使用light/shade區域;疊加一個density: 0.5的圖層來獲得柔軟邊緣。 使用附帶poly且shade: true的圖層來投射跨越許多物體的陰影。 - 使用好記的金鑰(
g/G表示淺/深綠色)。每個材質保持 2–4 個色階,以獲得清晰的像素藝術外觀。 - 迭代時使用
--only建構;完整建構也會寫出所有預覽圖。