Commands
pxart help recipes walks through seven workflows end to end, command by command: port a pack
and prove it lossless, merge packs with variants, build a dusk or night, slice a sheet, make a
scene from a map, check an animation's feet, stamp an overlay behind an animation. pxart -h is
a short overview: the format in a few lines and the commands by topic.
pxart help all has the full reference, pxart help TOPIC one part of it (FORMAT,
LOOKING, CHECKING, EDITING, DRAWING, CONVERTING, HELP, ERRORS), and pxart CMD -h (or
pxart help CMD) prints one command's part (pxart poly -h): its usage and a line or three
saying what it's for, its options one per line, then the details and heuristics, then a
see-also line naming the shared notes of the reference it relies on (EDITING, FORMAT:
selecting frames, ...) rather than repeating them. zsh users: write "${F}:walk", not
"$F:walk"; pxart -h says so in a box. compose and palette, the two longest, open with a short list of rules
(a new OUT imports the layers' shared @palette; variants merge by name; ...) and examples;
pxart help compose-rules and pxart help palette-rules print only the rules.
- Looking:
renderandsheetdraw a grey checkerboard behind each frame, so a dark outline and the transparent pixels both show (--bg COLORfor a flat backdrop instead).render(--pngalso writes each single-frame.pxat 1x,FILE.pngbeside it or in--png DIR, and prints the path;--plainwrites one frame alone at its exact size, no grid, rulers or labels, todiffagainst a scene's PNG),sheet(a directory stands for every.pxunder it, sorted by path, and palette files are skipped with a note;--exclude GLOBleaves files out,room.pxor a folderwip, incheckandstatstoo; frames with one id from several files are labeledhero:idle/0,beast:idle/0; every cell is the largest frame's size, or with--fiteach frame's own, rows as tall as their tallest, and a frame over 8x the median frame's area gets a note saying what it costs;--align pivotlines up each animation's frames by pivot, asanimdoes;--rows groupputs each animation group on a row of its own),anim(one GIF, each frame at--scalewith its 1x and 2x copies beside it in the same picture, plus a motion strip; without-oit prints only the per-frame numbers, likeshift +0,-1 then 72px (20%)(the percent is of the frame's opaque pixels, or of both frames' when more changed, so never over 100%), and writes nothing; a file of several groups animates each on its own, a block of numbers per group (its top-level frames and@stillgroups left out, with a note), and-o DIRwrites one GIF per group (DIR/walk/down.gif); "rows Y+ still" only when those rows are pixel-identical and the legs keep their shape in every frame (an idle; a walk frame whose legs happen to stay put reads as its bob,shift +0,+1 then 23px), and for the rise and the fall of one breath alike; a ground tile or a sparse overlay like falling snow that scrolls with wrap-around readsshift -1,+4 (wrap)),onion(B over A drawn as a red silhouette, and a printed readout of how B's edges moved from A's, liketop -1, bottom +0, for a 1px jump too faint to see, and the best shift, or for two different charactersdifferent sprites: edges only;--feet Nor--rows Y0-Y1reads only that band, so a weapon swing doesn't hide the feet;--tint-a COLORpicks the silhouette's color and--fade-adraws A faded instead;--variant nightdraws both at night, so a halo lit only then counts),scene(.px/.png items at x,y, negative allowed, on a 96x64 scene unless--sizeor--mapsays otherwise, pixels past its edge cropped with a note, mirrored with a+h/+vsuffix as inhero.px:walk/0+h@3,4;--variant Vrecolors the whole room;--tint '#10183080'lays a translucent color over the finished scene for night; an item or legend entry ending in%basekeeps its base palette, a lamp in a night room;--bg transparentworks, as doestransparentanywhere a color is typed), andtint, which does the same to a PNG.--dry-runonrender,sheet,anim,onionandsceneprints the readout (notes and WARNINGs too) and each output's size and layout (sheet.png: 700x174 px, 5 frames in one row, every cell 128x128 ...) and writes nothing, saying(dry run; nothing written);-omay then be left off. An image over 4096 px on a side or 16M px gets aWARNING, dry run or not. - Maps (
scene --map): a legend line is<char> <path>, and the rest of the line is the path, so a pack folder with spaces works as is (quotes optional). A legend entry that can't load is an error at its legend line. A#line before the rows is a comment unless it is exactly# FILE.px[:frame][%variant]or# FILE.png(or a quoted path), which makes#a map char (a wall row####);checkandscenenote it. A line---after the rows starts another layer of rows over the same legend (a tile and a sprite in one cell); later layers draw on top, and.is empty. Each item draws from its cell's top-left, so a prop bigger than a tile hangs right and down; a legend entry ending in+b(+hbwith a flip) stands it on its cell instead, bottom-aligned and centered (L props/lamp.px+b).pxart help LOOKINGhas a worked map.compose --map room.map -o room.pxbuilds the map's room as a.pxframe instead of a PNG, with compose's palette,--rekeyand--variant-maprules; it renders asscene --mapdoes, pixel for pixel, in every variant. A new OUT's header says how it was made (# composed by: pxart compose --map room.map -o room.px, paths from its folder), so a variantWARNINGabout it can offer composing it again once the map's sprites have that variant. - Checking:
check(format errors, size, off-palette colors, color budget, unused keys;.maptilemaps too;check crossover/checks every.pxand.mapunder it; notes Cyrillic/Greek/fullwidth letters posing as ASCII and@anim/@stilllines with no frames, which--strictfails; one line per file, the failing frames under it, and a summary like6 files, 150 frames, 3 warnings;-vfor a line per frame; exits 1),stats(a directory too, as forsheet;stats hero.px:idle/0%night --colorslists the colors a variant renders, with their keys (--variant night: every frame's; a frame drawn only in keys lit at night saysbbox none (96 px drawn, all transparent in the base; visible in night)), and--at 3,4one pixel's key and color in every variant),diff A B(renders pixel by pixel, one line per pair: how many pixels differ and where, exit 1 when they do, so a copy can be proved to render as its original;diff town.px pack/ --labels pack/labels.csvchecks every frame against the pack's PNG its row names,diff hero.px out/againstout/<id>.png,diff town/ pack/ --labels pack/labels.csvevery frame of every.pxundertown/(--exclude GLOBleaves some out), and two other directories pair files by path; a transparent pixel matches any other unless--strict-alpha;-o DIFF.pngdraws A, B and the differing pixels in magenta side by side (of several pairs, the one that differs, named), or for several pairs-o DIRone picture per pair that differs; a size mismatch against a render or a scaled copy gets a note namingrender --plain),frames(--rm/--moveprint only what they did, and a move to where the frames already are saysalready in place; with a selector,frames hero.px:walk/leftlists those frames,--rmremoves them and--after IDmoves them; removing a group's last frame removes its@anim/@stillline;frames hero.px:walk --copy-to beast.px [--after ID]copies frames into another file, in order, with their ms, pivots and@animline (a copy that lands as a still, a top-level id, drops its ms and keeps its pivot), under other ids with--prefix wick/or--rename walk wick/walk;frames hero.px --rename walk hero/walkrenames a group in place,@animand@stilllines too, and is the rename there is norenamecommand for; a move puts the@animlines in play order). - Editing:
new(a blank or filled frame, in a new or existing file;new party.px --empty --palette palette.pxstarts a file with no frames that imports a palette, forframes --copy-toto fill),put(put hero.px:walk/1 < rows.txtreplaces one frame's grid with rows from stdin, with optional palette lines merged likecompose's; checked like a file, errors at stdin's lines, nothing written on an error, and only that frame's lines change),fill(a region or the whole frame with one key;fill hero.px:walk/2 .clears a frame),flip,shift(the pixels it leaves behind become., or--fill KEY),set(set hero.px:idle/0 . 16,10erases one stray pixel and sayserased 1 px; it andfillcount what they paint, as the drawing commands do),crop,recolor(optionally within a region;'a<>b'swaps two keys, quoted for the shell;'a>b'gives a's pixels a new key b in a's color, a rename when nothing keeps a, and onto a key already in a's color the error saysa=b; the key moves of one call apply together, so none feeds another, and a key renamed away is free for another:'a>b' 'b>a'swaps two names;'b>t' 't=#1e4548'gives the new key its own color in the same call, and--variant nightsets night's color instead of the base),mask(erase outside--keep x,y,w,hor--keep-circle cx,cy,r, with a--dither Nedge;--inverterases the inside instead; both flags repeat, and the kept area is the union, so two lamps are one call; works on a rendered PNG too;--keep-keys W,T,tor--drop-keysmask a .px by key),paste(+h/+vmirror the source, as incompose;--underfills only the empty pixels; into an animation,--align shiftmoves the paste with the body's bob, frame by frame, asanim's best shifts add up, where--align bboxwould follow wings that stick out),compose(stack layers into a frame;--dry-runprints all it says, notes, conflicts and--rekey's moves, and the diff of OUT it would write, a new OUT's every line, and writes nothing; a new frame lands after its animation; a new file gets the layers' whole palette, their shared@palettere-pointed when they all import the same one, so a latershaderamp finds its keys, or with--used-keys-onlyonly the keys its frame uses, and the comments on their key and@variantlines, each naming its file when there are several (from keeper.px's @variant night, dusk here; a@variant's names only the files whose variant lines gave it keys,the rest from pal.px's night, and a line# night: also from lamp.px's nightcredits a file that gave it keys with no comment of its own), with the palette files' headers at the top; a key its file needs (drawn in other frames, named, or kept lit by a variant) that it has to leave out is aWARNING; it reports per source file, one line per reason, with what--rekeymoved and why, and anE_KEY_CONFLICToffers the same moves; each variant colors only the layers whose file has it, a note says which stay at their base colors, and--variant-map dusk=night,darkmerges several files' variants into one (it adds to the same-name lookup, never replaces it: OUT's night still reads each file's night); a plain OUT that exists keeps its palette, with a note, and--replacestarts it as if new;--underdraws the layers behind the frame that's there, in its empty pixels; a key a layer has in another color than OUT's isE_KEY_CONFLICT, one line per source file with free keys for it, and--rekeygives it those keys in OUT and leaves the source file alone, as it does forcrop,pasteandframes --copy-to;--rekeyalso gives free keys to a key two files have in one color but recolor differently in their variants, into a new file or an existing one (frames keeper.px:walk --copy-to party.px --rekey --variant-map dusk=nightreads the keeper's night as party.px's dusk), and in a new OUT to a needed key it would leave out; a key OUT's variants would recolor is aWARNINGof its own;--rekey o,rmoves only those keys, and--rekey k=j,n=qputs k and n on OUT's own j and q, with aWARNINGwhere their variant colors differ;--rekey girl.px:T=Vis for one source file's T only; a color two files share, alike in every variant, keeps the one key it got first),dup(copy a frame; or a group,dup hero.px:walk/right walk/left, with its frames' timing and pivots and its@animline;flip hero.px:walk/left -o hero.px:walk/rightcopies a group and flips the copy in one step, asrotateandtransposeturn one; onto a group or frame that exists,--replaceputs the copy in its place),anim-set(timing),palette --add(with--variant nightit sets keys in that variant, making it if needed;--variant night --keep l,glets keys inherit the base colors;--variant night --derive-from base --darken 0.35 --tint '#10183060' --keep-lit y,Wbuilds a whole night from the base colors, asscene --tintwould, with the lamps still lit;--match mossback.px%duskfirst maps each channel as another palette's base to dusk does (a fitted gain and offset: warm lights, blue shadows), and a key darker than a quarter is never brightened, by Rec. 709 luma (hue kept), unless--lift-darks;--hoist l,gmoves a sprite's own keys into the palette file it imports;--import pal.pxadds a@palette pal.pxline to a sprite and drops its key lines pal.px has in the same colors, so it renders as before;--order o,t,kputs those key lines first, comments and all;--remove k,ntakes out keys no frame draws with (--to jrepaints their pixels as j first; an imported key then goes from its palette file only when no other.pxunder the directory holding both, or--in DIR, uses it);--comment k 'text',--comment @variant night 'text'and--comment-header 'text'write the comment above a key line, a variant or the file;palette FILEalone lists the keys with their comments, then what each variant recolors darker or brighter, relists in its base color and inherits, with its comment:night: recolors (darker) k w; brightens y; relists unchanged: l g; inherits: e E q;--in DIRsays how many files under DIR import a palette file and draw with each key; any edit takes-o OUTfor an edited copy, and--dry-runprints the diff it would make and writes nothing). Every edit, the drawing commands andnew,frames,dup,anim-setandcomposetoo, takes--dry-run: it prints what the edit says and the diff it would make, then(dry run; nothing written). There is no undo: keep sprites in git, or edit a copy with-o. Edits rewrite only the lines that changed, keeping the file's blank lines and comments, and an edit that changes nothing saysno changeand leaves the file alone.-o OUTalways writes the whole file:flip hero.px:walk/0 -o out.pxis a copy of hero.px with that frame flipped, and anOUTin another directory gets its@palettelines re-pointed from there, so it imports the same palette file.extract hero.px:walk -o walk.pxwrites only the selected frames, with the same palette and imports (and their@animlines, in the order of the frames' groups);--inline-palettecopies the imported keys they use (and the variants' colors for them) into the file and drops@palette, so the hand-off renders the same with nothing beside it. extract writes a new file: onto one that exists it is an error (its frames would be lost), unless--replace;frames --copy-toadds to it instead. - Drawing (for 32x48 heroes, 64x64 beasts and 256-wide layers, where typing every
pixel is the bottleneck; each draws a palette key, or
.to erase (ellipse hero.px:idle/0 . 8,8,3,3 --fillcuts a hole, and sayserased 37 px), onFILE[:SEL], clipped to the frame, and rewrites only the rows it changed):line hero.px:attack/2 W 3,40 28,12 --width 2: Bresenham, no doubled corners.poly … x,y x,y x,y … [--fill]: a closed polygon from line's pixels (filled by nonzero winding, so a star is solid).rect … x,y,w,h [--fill],ellipse … cx,cy,rx,ry [--fill](clean, symmetric pixel ellipses;.5centers and radii for even sizes, or--box x,y,w,h, the pixel box asrecttakes it, with no half pixels at all),flood … x,y [--diagonal].arc … cx,cy,r a0,a1 --width 3: a smear or swoosh (degrees, 0 = right, counter-clockwise).shade hero.px:idle/0 --ramp XxcCw --keys c --light nw: re-shades a material with a darkest-to-lightest ramp. Each pixel's tone comes from its outward normal against the light, fading to the base tone--strengthpx in from the edge: banded, deterministic, no noise (--ditherfor an ordered blend at band boundaries,--preview p.pngto look first;--regionrepaints only inside it, shaded as part of the whole material, so its border is no edge).outline beast.px:idle --key o --lit m: an outline around the shape (or--insideon its edge), pixel-perfect by default (--cornerskeeps square corners), and selective: the edges facing the light get the lighter--litkey (--preview p.pngas for shade).- Both print only the pixels they changed, by the key they got:
changed 41 px: 29->C, 12->X. rotate FILE 90|180|270andtransposeturn path tiles into their edges and corners (then re-light withshadeandoutline --lit, since the light turned too).
- Converting:
export --frames DIRwrites one PNG per frame.export town/ --frames out/exports every.pxundertown/(several files and directories,--exclude GLOBas forcheck); an id two files share, or two that differ only in case (one file on macOS), is an error rather than one PNG replacing another, and--prefix-fileids themroofs/roof-red.export --aseprite x.jsonwrites a sprite sheet and JSON with frameTags (and pivots asmeta.slices, the shape Aseprite's own export uses).export --tiled x.tsjwrites a tileset with tile animations.export harbor.px:cobble harbor.px:water --tiled t.tsjexports only those frames (selectors of one file add up), so a tileset can leave out the big props.- Frame and tile ids count over the exported frames with each animation group
contiguous, groups in order of first appearance, and all top-level frames together as
one group. A file that already keeps each group together gets ids in file order;
otherwise a frame moves up to its group (
a/0 b/0 a/1givesa/0 a/1 b/0). palette --export x.gpl|x.hexwrites the palette;palette hero.px --extract-to pal.pxwrites it as a palette file for@palette, variants included, with the comments on its key and@variantlines (and a palette file's header), and--repointthen makes hero.px import it.from-pngconverts a PNG to a sprite;from-png Walk.png --grid 16x16 --by cols --names walk/down,walk/up,walk/left,walk/right -o boy.pxslices a sheet into frames, a group per column (or row), skipping empty cells. Two PNGs one run would give one id (two packs'tile_0002.png) are an error rather than one replacing the other;--prefix-dirids themdungeon/tile_0002andcreatures/tile_0002. Loose PNGs take names from--names a,b,...(one per PNG) or from the CSV a pack ships:--labels dungeon/labels.csvnames each PNG by its row'sproposed_name(--label-col,--file-colpick other columns).
README.md in the repository.Edit on GitHub