pxart help all

The whole reference

Everything pxart help all prints, on one page. Search it with your browser's find.

pxart: pixel art as text. Sprites are text files; renders are PNGs you can look at.

FORMAT (.px)
    # comments start with '#'
    pxart 1                  optional version line (first non-comment line)
    @palette base.px         optional: import palette keys from another .px file
    k #3f2631                palette: one key char, then #rrggbb, #rrggbbaa, or 'transparent'
    g #43e1b3                ('.' is always transparent; you may omit it)

    ....kkkk....             grid rows: all the same width, only palette keys.
    ...kggggk...             The sprite's size is the grid's size: nothing to count.

  A comment right above a line is that line's; a header is above 'pxart 1' or a blank line.
  Several frames per file: name each grid with @frame; the id is a path, and frames
  sharing a parent path form an animation (walk/down/0, walk/down/1 -> "walk/down"):
    @anim walk/down direction=forward repeat=0 ms=125
    @frame walk/down/0
    ....kkkk....
    @frame walk/down/1 ms=250          per-frame duration overrides the anim's
    ...
  An animation plays its frames in file order (not by the number in the id).
  pivot=x,y (optional) on '@frame ID' or '@anim GROUP' is the frame's anchor pixel, counted
  from its top-left (the feet: pivot=8,23 on a 16x24 frame). A frame's own wins over its
  anim's; no pivot is the default. anim and onion line frames up by pivot instead of bottom-
  centre (a frame without one then uses its bottom-centre pixel, w // 2, h - 1); export
  writes pivots (Aseprite slices, --frames pivots.json). A pivot may lie outside the frame (a
  hand, the ground under a jump); check notes it, in case of a typo. flip, rotate and
  transpose move a frame's pivot with its pixels: a whole group's @anim pivot moves on its
  @anim line, and a frame flipped alone gets its own.
  direction: forward | reverse | pingpong | pingpong_reverse (Aseprite's words).
  repeat: 0 or absent = loop forever; N = play N times. ms: default frame duration.
  'anim-set hero.px:walk/down ms=125' writes these, pivot=8,23 too (FILE:GROUP/ID: a frame's).
  Frame groups that aren't animations (UI icons, a parts file): '@still ui/life' keeps them
  out of animation exports and checks; '@still *' marks every frame in the file, top-level
  ids with no '/' included (a parts file; frames then lists them as 'still'). A frame copied
  as a still (a top-level id, or a @still group) drops its ms and keeps its pivot, with a note.
  Palette variants (recolors): keys listed after '@variant night' override the base
  palette. Variants in a @palette file are inherited; local keys (and local variant keys)
  override imported ones, and check notes the override, and a local key that repeats an
  imported one's color (with the palette --remove that drops its line when nothing changes).
  A variant a file gets only from its @palette leaves the file's own keys at base colors;
  commands that render it print a WARNING naming them and the fix, and check notes it.
  Slots, a key's alternatives for runtime recolors ('f #e0ac69 | #f5cfa0'): help slots.

  Anywhere a command takes FILE, FILE:SEL picks frames: SEL is a frame id or a parent
  path (FILE:walk/down = every walk/down/* frame), or a list of them, FILE:idle,walk/0.
  No SEL (or *) means every frame. A file with one unnamed grid (no @frame) calls it by
  the file's name, as frames lists it: ant.px's grid is ant.px:ant. Writing a named frame into such a file (compose, crop, new, put -o
  ant.px:ID, or from-png into it) first makes the grid '@frame ant', with a note; with ID
  ant that is the frame written. Add %VARIANT to render with a variant: FILE:idle/0%night.
  +----------------------------------------------------------------------------------------+
  | zsh users: write "${F}:walk", not "$F:walk" (zsh reads ':w' as a modifier), or quote   |
  | the whole argument. A missing hero.pxalk/0 or hero.pxidle is reported as that mistake. |
  +----------------------------------------------------------------------------------------+
  An output under a file (-o hero.px/walk/0) is E_FILE; an image output with no image
  extension (-o /dev/null, -o x.px) is E_BAD_ARG.
  Paths: a path typed on the command line is read from the current directory, as the shell's
  are: FILE, -o OUT, layers and items, and every path option (--palette, --import, --match,
  --copy-to, --into, --map, --labels, --in, --extract-to, --export, --preview, --frames,
  --aseprite, --tiled, --indexed). A path written inside a file is read from that file's
  directory: a .px's '@palette pal.px', a .map's legend entries, a --labels CSV's file
  names. A file pxart writes elsewhere gets its @palette lines re-pointed (see EDITING). An
  output's missing directory is made, and said: 'created out/'. An E_FILE for a relative
  path says so, and a path option's names it: 'palette: --match (../wick/pal.px%dark):
  ../wick/pal.px (from the current directory): E_FILE: No such file or directory'.

LOOKING
  render FILE... [-o preview.png] [--scale 8] [--no-grid] [--variant V] [--png [DIR]] [--plain]
         [--slots N|K=I] [--dry-run]
      Preview sheet with a pixel grid and x/y rulers every 4px (default --scale 8; sheet,
      anim and onion too). --png also writes each single-frame .px at 1x, FILE.png beside it
      or in DIR, and says so. --plain writes -o as the one frame alone, its exact size
      (--scale 1 by default), no grid, rulers, labels or --bg: for diff (--no-grid still
      pads and labels).
  sheet FILE|DIR... -o sheet.png [--scale 8] [--cols 8] [--grid] [--variant V] [--bg COLOR]
        [--fit] [--align bottom|pivot] [--rows cols|group] [--slots N|K=I] [--exclude GLOB]
        [--dry-run]
      Compare any mix of .px/.png frames, labeled with id, WxH and color count. A directory
      stands for every .px under it, recursively, sorted by path (PNGs in it are left out, a
      sheet rendered there too). --exclude GLOB (repeatable) leaves files out: one whose
      name or path under the directory matches ('_*.px', 'wip/*.px'), or every file under a
      directory that does ('wip'). A glob that leaves out every file is E_FILE. A palette
      file (no frames) among the inputs, a directory's or a glob's, is skipped with a note
      ('note: sheet skips palette.px: a palette file, no frames'); given alone it is
      E_NO_FRAMES. Every cell is the largest frame's size (a frame over 8x the median
      frame's area gets a note); --fit makes each cell its own frame's width (or its
      label's, if wider) and each row as tall as its tallest frame, --cols cells to a row,
      frames bottom-aligned in their row. A PNG whose four corners are exactly the --bg
      color (a scene rendered with the same --bg) doesn't count that color: it's the
      backdrop. Frames with the same id from different files are labeled with their file's
      stem in front (hero:idle/0, beast:idle/0); render and anim label them the same way.
      --align pivot lines up each animation group's frames by pivot, as anim and onion do:
      the group's frames are drawn on one canvas, every pivot on the same pixel. The
      default, --align bottom, bottom-aligns each frame in its cell.
      --rows group starts a row for each animation group (one file's walk/down, then its
      walk/up, ...; a file's top-level frames share one; a PNG has its own) and wraps within
      a group only when it has more than --cols frames: 'sheet party.px --fit --rows group
      --align pivot -o party.png' is one walk per row. The default, --rows cols, fills each
      row with --cols frames.
  anim FILE... [-o walk.gif|DIR] [--scale 8] [--fps N] [--variant V] [--dry-run]
      walk.gif (one file: each frame at --scale, its 1x and 2x copies beside it in the same
      picture), plus walk.strip.png: row 1 = frames, row 2 = what changed from the previous
      frame after removing the whole-sprite shift ("shift dx,dy then N px (P%) (no shift: M px)";
      P: N as a percent of the frame's opaque pixels, or of both frames' if more; a bob alone
      shows "then 0px (0%)"). The numbers print too, a line per frame (a FILE of several
      groups: a block each, top-level and @still frames skipped; -o a DIR);
      without -o, anim prints only those lines and writes nothing. Read the strip: the Read
      tool shows only a GIF's first frame. Durations come from the file (@anim/@frame ms)
      unless --fps. A group with repeat=1 plays once: frame 0 gets no wrap-around diff.
      --variant V counts V's render: a halo transparent in the base counts, at any alpha.
      An idle: when the bottom stays exactly put (rows Y down identical, 0 px changed) and
      only the part above moves (a breath), shifting would light up the legs, so the strip
      shows the unshifted diff: "no shift then M px (P%) (rows Y+ still; shift dx,dy: N px)".
      Both counts are always shown. That is for an idle, whose legs keep their shape in every
      frame of the animation: a walk whose leg moved even 1px keeps the shift, and in a walk
      a frame whose legs happen to stay put is the body's bob: "shift +0,+1 then 23px (6%) (no
      shift: 201px)". A frame recolored whole (a glow) doesn't count as the legs moving; a
      shadow that changes shape does. So that the rise and the fall of a breath read alike,
      once one frame shows rows still and rows Y down are identical in every frame of the
      animation, every frame whose shift would light them up shows the unshifted diff too; a
      walk over a static shadow keeps every shift.
      Tiles and overlays: frames that fill the canvas and are a ground tile (every pixel
      opaque) or a sparse overlay (at most 1/4 opaque: snow) also try every wrap-around scroll
      (shift --wrap), shown when it leaves strictly fewer pixels changed than the best plain
      shift: "shift dx,dy (wrap) then N px (P%) (no shift: M px)". A character sprite never wraps.
  onion A B -o x.png [--scale 8] [--rows Y0-Y1 | --feet N] [--tint-a [COLOR] | --fade-a]
        [--variant V] [--dry-run]
      B at 80% opacity drawn over A as a flat silhouette in a translucent red (#ff4060a0),
      with render's grid and rulers: where A shows past B is plain.
      Prints where each frame's opaque pixels sit on the shared canvas and how B's edges moved
      from A's, then the whole-sprite shift that best explains B, as anim finds it:
      "B vs A: left +0, right +0, top -1, bottom +0; best shift +0,-1 then 4px changed (no
      shift: 20px)": the head rose 1px, the feet stayed. Two different sprites (other sizes, or
      more than half the larger one's opaque pixels still changed at the whole sprites' best
      shift; --rows and --feet don't change that; two frames of one animation in one file
      are always one sprite, however much changed) print
      "...; different sprites: edges only": their edges still compare where the feet stand,
      but a best shift between two characters means nothing. y grows down, so +1 is lower.
      The edges are the sides of each frame's opaque bounding box: 'top -1' is B's top row 1px
      above A's.
      --rows Y0-Y1 (canvas rows as the readout prints them, both included; one row: --rows Y)
      or --feet N (the bottom N rows) limit the edges and the best shift to that band, so a
      weapon swing above doesn't hide what the feet did: 'B vs A (bottom 4 canvas rows 20-23;
      A opaque in 20-23, B in 21-23): left +0, ...'. The shift still moves all of A (pixels
      come into the band from above) and counts only the band's pixels; when the bottom edges
      agree, one up or down only lines up what moved above them, and the readout says no
      shift. The PNG darkens the rows outside the band.
      --tint-a COLOR draws the silhouette in another color. --fade-a draws A itself at 35%
      opacity instead.
  scene -o s.png [--scale 4] [--size WxH] [--bg #472d3c] [--map M --tile 16x16] [--variant V]
        [--tint #rrggbbaa] [--dry-run] ITEM@x,y ...
      Default --scale 4 (render's: 8).
      Size: --size, else the map's, else 96x64 (six 16x16 tiles by four). Pixels past the
      edge are cropped, with a note per item (and one for the map) saying how many; at the
      default size a note also names the --size that holds every item.
      ITEM is FILE[:frame][%variant][+h|+v|+hv]: +h mirrors it left-right, +v top-bottom
      (hero.px:walk/0+h@3,4; '+' needs no quoting, where '!' would be history expansion). Map legend entries (which also take +b, see
      Placement) and compose layers take +h/+v too. --map draws a text tilemap first:
      legend lines '<char> <FILE[:frame][%variant]>', a blank line, then rows of legend
      chars ('.' = empty). In a legend line the rest of the line is the path, relative to
      the map file: spaces are fine ('b ../png/trees and bushes/bush.png'), "quotes"
      optional. A legend entry that can't load is an error at its legend line.
      Items are then drawn on top. '#' lines are comments only before the first row;
      after that every non-blank line is a row, so '#' works as a map char (a wall row
      '####'). Before the rows, a line that is exactly
      '# FILE.px[:frame][%variant]' or '# FILE.png' (one token after '#', no spaces) is the
      legend line for '#', not a comment, and so is a quoted path ('# "my tiles/wall.png"');
      check and scene print a note for it. Any other line starting with '#' is a comment
      ('# see wall.px' too).
      Layers (a tile and a sprite in one cell): a line '---' after the rows starts another
      grid of rows over the same legend. Layers draw in order, later over earlier, and '.'
      is empty in each; the map is as big as its widest row and tallest layer.
      Placement: a map item draws with its top-left at its cell's top-left (column * tile
      w, row * tile h), so a 32x32 stall on 16x16 tiles hangs right and down over the next
      cells.
      A legend entry ending in +b (with flips: +hb, +vb, +hvb) draws bottom-aligned to its
      cell and centered across it, odd pixel left: x = cell x + (tile w - w) // 2,
      y = cell y + tile h - h. A tall lamp then stands on its cell and rises into the row
      above (cut off at the top edge on row 0). +b is for legend entries only.
      A worked map, market.map, on 16x16 tiles (tiles.px:cobble, :water/0 and :crate are
      16x16, stall.px is 32x32, lamp.px is 16x32):
          # ground layer, then props
          c tiles.px:cobble
          w tiles.px:water/0
          x tiles.px:crate+h
          S stall.px+b
          L lamp.px+b
          l lamp.px+hb

          cccccc
          cccccc
          wwwwww
          ---
          ......
          .S.L.l
          x.....
      'pxart scene --map market.map -o market.png' is 96x48: cobbles and water, then the
      props over them (compose --map builds the same room as a .px). The stall's cell is
      16,16, so it draws at 16 + (16-32)//2, 16 + 16 - 32 = 8,0; the lamps at 48,0 and 80,0
      (mirrored); the crate, mirrored, at its cell's top-left, 0,32.
      --variant V renders every map tile and .px item with V (a whole dark room), except
      those with their own %variant, which wins; a .px without V is an E_SELECT error.
      %base is the base palette, so 'lamp.px%base@40,20' (or a legend entry 'L lamp.px%base')
      stays unrecolored in a --variant night scene (a lit window, a glowing lamp). '%base'
      works wherever %variant does, and a variant can't be named base.
      Items and legend entries can be PNGs (hero.png@3,4). x,y may be negative (drawn
      partly off the left/top edge), in scene and in compose.
      --tint '#10183080' lays that color, at its alpha, over the whole finished scene (bg,
      map, every item, those with their own %variant too) at 1x, before --scale: a night
      scene in one step. To keep a light bright, mask its circle out of the untinted render
      and lay that over the tinted one.
  tint IN.png '#rrggbbaa' [-o OUT.png]
      scene --tint on a PNG (a rendered scene): lays the color, at its alpha, over every
      pixel; each pixel keeps its alpha, so transparent pixels stay transparent. Without -o,
      IN is rewritten.
  world W.world|ROOM.tmj -o world.png [--scale 2] [--dry-run]
  Centering: frames of different sizes are bottom-aligned and centered, the odd pixel
  left (x = (canvas - frame) // 2). render and sheet put a grey checkerboard
  behind frames; --bg (render, sheet, scene) takes a flat #rrggbb, #rrggbbaa or 'transparent'
  (as does every color typed on the command line: --tint, tint, palette --add
  k=transparent; the '#' may be left off, and in a script a '#' color needs quotes, or the
  shell reads a comment).
  --dry-run (render, sheet, anim, onion, scene) prints the readout and each output's size and
  layout, writes nothing and says '(dry run; nothing written)'. -o may then be left off. An
  image over 4096 px on a side or 16M px gets a WARNING, dry run or not.

CHECKING
  check FILE|DIR... [--palette P] [--size WxH] [--max-colors N] [--strict] [-v] [--exclude GLOB]
      Every format error with a code and location, then size / off-palette colors / color budget /
      unused keys / see-through px. One line per file: 'ok   party.px: 40 frames, 32x32, 16x16,
      3-14c', or 'FAIL party.px: 2 of 40 frames fail' and a line for each of those frames (a file
      of one frame gets that frame's line); -v prints a line for every frame, as
      'ok   party.px:walk/down/0: 32x32 9c'. Notes follow their file's line, and checking more than
      one file ends with a summary: '6 files, 150 frames, 3 warnings' (', 1 failed' when one did;
      a warning is a note line). A directory checks every .px and .map under it, recursively,
      sorted by path; a palette file (no frames) is checked as one: 'ok   palette.px: palette file,
      17 key(s), variants night'. P is a .px, .gpl, .hex, or text of #rrggbb. --strict also
      rejects unknown @sections and @anim/@still lines whose group has no frames (else a note).
      Exit 1 on any failure. --exclude GLOB leaves files out, as for sheet.
      A .map (scene --map) is checked too: every row char has a legend line and every legend
      entry loads as one frame; .world, .tmj: help worlds.
      Non-ASCII chars that look like ASCII (Cyrillic/Greek 'а е о р с х у', fullwidth 'k') get
      a note naming the line, row and column and the letter they pass for.
  stats FILE|DIR... [--colors] [--at x,y] [--exclude GLOB] [--variant V]
      Size, bbox ('x 1..22, y 0..20': ends included), colors (a directory, palette files and
      --exclude as for sheet).
      FILE:SEL%VARIANT reads the colors a variant renders (--variant V: every FILE's): 'stats
      hero.px:idle/0%night'. --colors lists each frame's rendered colors, most pixels first,
      each with its pixel count and the keys that draw it ('#120e22 40 px (k)'). --at x,y
      (repeatable) prints that pixel's key and its color in the base palette and in every
      variant ('at 3,4: key k; base #3f2631, night #120e22'), or only in the one named.
  diff A B [--variant V] [--strict-alpha] [--exclude GLOB] [-o DIFF.png|DIR [--scale N]]
       [--labels CSV [--label-col C] [--file-col C]]
      Compare renders pixel by pixel, one line per pair ('same: 16x16, every pixel', or what
      differs: '12 px differ in 3,4,6,6 (x,y,w,h)', 'sizes 16x16 and 16x24') and for several a
      count ('132 frame(s): 130 same, 1 differ, 1 unpaired'). It exits 1 when anything differs
      or has no pair, as check does.
      A and B are:
        two files, FILE[:SEL][%VARIANT] or PNGs: one frame each, or frames paired by id (in
          order when their ids differ but their counts match: diff wick.px:walk
          party.px:wick/walk, copies made with --prefix wick/);
        a file and a directory of PNGs: each frame against DIR/<id>.png, as export --frames
          writes them, or with --labels CSV against the PNG whose row names it so: 'diff
          town.px pack/ --labels pack/labels.csv' proves a port lossless in one call;
        a directory of .px files and one of PNGs: every frame of every .px, as above ('diff
          town/ pack/ --labels pack/labels.csv'; an id two files share is E_BAD_ARG; a note
          counts the PNGs left over);
        two other directories: the .png and .px files under them paired by path (with
          --labels, a PNG goes by its row's name), a .px pair frame by frame.
      A frame with no PNG, or a file only one side has, is unpaired. --exclude GLOB leaves
      a directory's files out, as for check.
      Transparency: a pixel whose alpha is 0 matches any other whose alpha is 0, whatever
      its rgb; any other pixel compares on all four channels. --strict-alpha compares all
      four everywhere. --variant V renders both sides with V, and a side's own %VARIANT
      wins. -o DIFF.png draws A, B and the differing pixels in magenta side by side
      (top-left aligned) of the one pair that differs; -o DIR gets one per pair that differs.
      A render or scaled copy of a frame gets a note naming render --plain.
  frames FILE[:SEL] [--rm [ID...]] [--move ID --after|--before ID] [--rename GROUP NEWGROUP]
         [--copy-to DST [ID...] [--rekey [KEYS]] [--variant-map NAME=V1,V2]
          [--prefix P | --rename GROUP NEWGROUP]]
      List frames, sizes, durations (only for animation frames; 'still' for @still groups
      and every frame under '@still *') and animations; or delete / reorder frames (prints
      what it removed or moved, not the listing; a move to where the frames already are
      prints "already in place" and writes nothing). FILE:SEL lists only those frames; 'frames
      hero.px:walk/left --rm' removes them (ids after --rm are frame ids, in SEL), and 'frames
      hero.px:walk/left --after idle/3' moves them there as a block, in order. --move ID
      takes a plain FILE and one frame id (a group moves with FILE:GROUP --after ID).
      Removing a group's last frame removes its @anim and @still lines too. A move puts the
      @anim lines in the order of their groups' first frames (with their comments; lines of
      groups with no frames after), so the file reads in play order, and says so.
      --copy-to DST copies FILE's frames (FILE:SEL's, or the ids after DST) into the existing
      DST as they play in FILE: each frame's ms and pivot (on its @frame line when DST's @anim
      would change them), the @anim line of a group DST lacks, and @still. They keep FILE's
      order and land after their group's last frame in DST, at the end for a new group, or
      at --after/--before a DST frame: 'frames hero.px:walk --copy-to beast.px --after idle/3'.
      Their keys join DST's palette and variants as compose's layers join an existing OUT
      (see compose): E_KEY_CONFLICT, the variant WARNINGs, --rekey [KEYS] and --variant-map
      dusk=night work alike. A frame id DST already has is E_DUP_FRAME, and so are two
      copies --rename would give one id (--rename walk w --rename run w).
      DST must exist: 'extract FILE:SEL -o DST' starts one with FILE's palette, and 'new DST
      --empty --palette P.px' one with no frames that imports P.
      Copies under other ids, when DST has those already (two packs' walk/*): --prefix wick/
      puts wick/ in front of every copy's id ('frames wick.px:walk --copy-to party.px --prefix
      wick/' writes wick/walk/down/0 and '@anim wick/walk/down'), and --rename walk wick/walk
      renames only the group (or frame) it names; FILE stays as it is. --rename repeats.
      --rename GROUP NEWGROUP without --copy-to renames in FILE itself: GROUP's frames (walk/0,
      walk/1, or the frame GROUP) become NEWGROUP's (wick/walk/0, ...), and the @anim and
      @still lines of GROUP and the groups under it follow, with their comments. A NEWGROUP
      FILE already has is E_DUP_FRAME.

EDITING (writes .px; -o defaults to editing the input in place)
  -o OUT always gets the whole file: with FILE:SEL, OUT is a copy of FILE with the selected
  frames edited and every other frame as it was, and a note says so.
  To get only some frames, extract them. An OUT in another directory gets its @palette
  lines re-pointed from there ('@palette ../pal.px' in -o art/x.px), so it imports the
  same palette file. Edits rewrite only what changed: other lines keep their spelling and
  the blank lines and comments above them, and new frames get the file's spacing between
  @frame blocks. An edit changing nothing (set to the same key, flip of a symmetric frame) prints
  "no change: FILE" and writes nothing; one that edits several frames names them.
  --dry-run (any edit) prints its diff and writes nothing. There's no undo: use git.
  Coordinates (x,y, --region, --at) need FILE:SEL on a file of several frames, saying which
  ('FILE:*': all).
  Limits: sections are written in a fixed order (palette, @variant, @anim/@still, frames,
  unknown @sections), so an @anim written between frames moves up; a comment inside the
  file stays with the line below it and goes when that line goes (a removed frame, cut rows).
  flip FILE [-o OUT] [--v]          mirror selected frames left-right (--v: top-bottom)
      'flip hero.px:walk/left -o hero.px:walk/right' flips a copy (--replace: over its frames).
  shift FILE [-o OUT] --dx N --dy N [--region x,y,w,h] [--wrap] [--fill KEY]
      Move the frame's pixels (or only the region's) by dx,dy. Pixels moved past the frame's
      edge are dropped, and the pixels the move leaves behind (vacated) become '.', or KEY
      with --fill KEY (a floor under a moved prop). With --region the moved block may land
      outside the region; its '.' pixels don't overwrite what they land on.
      --wrap scrolls pixels around the edges (for animating tiles) instead of dropping them.
  set FILE[:frame] KEY x,y [x,y ...] [-o OUT]    paint single pixels ('.' erases)
      Prints "painted N px" ("erased" for '.').
  fill FILE[:frame] KEY [--region x,y,w,h] [-o OUT]   paint a rectangle (default: the frame; '.' clears)
  new OUT[:frame] --size WxH [--key K] [--palette P.px] [--still]
  new OUT --empty [--palette P.px]
      A blank frame ('.'), or one filled with K, in a new file or added to an existing one
      (placed like compose). --palette P.px starts a new OUT that imports P. A frame that
      already exists is E_DUP_FRAME: fill it instead. --still marks its group '@still GROUP'
      (new ui.px:icons/life --still).
      --empty starts a new OUT with no frames: the version line and, with --palette, its
      @palette line. It is where frames --copy-to puts another file's frames against a
      shared palette: 'new party.px --empty --palette palette.px', then 'frames
      keeper.px:walk --copy-to party.px --rekey'.
  put FILE[:frame] [-o OUT] < grid.txt
      Replace one frame's grid with the rows on stdin. Stdin is rows, or palette lines then
      rows; the keys the rows use join FILE's palette the way compose's layers do (a key
      FILE has in another color is E_KEY_CONFLICT), and other keys must be FILE's. Rows are
      checked like a file's (widths, keys), errors point at stdin's lines, and
      nothing is written on an error. Only that frame's lines change.
      A frame that doesn't exist yet is added, placed like new; a new FILE is started.
      Stdin has no variants: a key in FILE's color is FILE's key, variant colors and all, and
      a new key stays at its base color in FILE's variants.
  mask FILE[:frame] --keep x,y,w,h | --keep-circle cx,cy,r ... [--dither N] [--invert]
       [--keep-keys K,K | --drop-keys K,K] [-o OUT]
      Erase (set to '.') every pixel outside the rectangle or circle (kept: distance from
      the pixel to cx,cy <= r). --dither N fades the circle's last N px inside its edge
      with a 4x4 ordered (Bayer) dither: a light radius. --invert erases the inside and
      keeps the outside, dither band mirrored, so a mask and its --invert split the image
      with no overlap or gap. --keep and --keep-circle repeat, and mix: the kept area is
      their union, and --dither and --invert work over the union.
      FILE may be a PNG (a scene rendered at --scale 1: coordinates are its pixels): outside
      pixels become transparent. -o must be a .png too.
      --keep-keys W,T,t (or WTt) erases every pixel whose key isn't one of those; --drop-keys
      erases those keys' pixels. Alone, they mask by key over the whole frame; with shapes, a
      pixel stays only when both keep it (the shapes, --invert and --dither as above). .px only.
  crop FILE:frame x,y,w,h -o OUT[:frame] [--rekey [KEYS]] [--used-keys-only]
       [--variant-map NAME=V1,V2]
      Cut the w x h rectangle at x,y out of one frame into a frame of its own: 'crop
      hero.px:idle/0 4,0,8,8 -o parts.px:head'. The pixels left outside get no note (they're
      what crop is for); a rectangle that runs past the frame's edge (or starts at a
      negative x,y) gets '.' there. FILE:frame must be one .px frame.
      OUT is written as compose writes it, FILE's frame its one layer (see compose): a new
      OUT gets FILE's whole palette (--used-keys-only: the cut's keys), OUT:frame adds or
      replaces that frame in an existing OUT, and a plain OUT with one unnamed grid has it
      replaced. A key OUT has in another color is E_KEY_CONFLICT, with free keys; --rekey
      gives the cut those keys in OUT and leaves FILE as it is.
  extract FILE:SEL -o OUT [--inline-palette] [--replace]
      Write only the selected frames to a new OUT, with FILE's palette, @palette imports,
      variants, and @anim/@still lines (minus those of groups left behind;
      @anim lines in the order of the frames' groups): 'extract hero.px:walk/down -o
      walk.px'. An OUT that exists is E_FILE, since its frames would be lost (E_DUP_FRAME
      when it has one of the ids): 'frames FILE:SEL --copy-to OUT' adds them to it, and
      --replace overwrites it. --inline-palette makes OUT self-contained for a hand-off: the
      imported keys it uses become key lines, with their variant colors, and the @palette
      lines go. OUT renders exactly like the source frames, in every variant.
  recolor FILE a=b ['a<>b'] ['a>b'] [c=#rrggbb] [-o OUT] [--region x,y,w,h] [--variant V]
      a=b repaints key a's pixels as key b (optionally only inside --region); 'a<>b' swaps
      keys a and b (in the region) in one step; quote it: unquoted < and > are shell
      redirections. 'a>b' gives a's pixels a new key b, in a's color (and a's variant
      colors): when no pixel keeps a and a is FILE's own key, a's palette lines become b's
      (a rename), else b is added and a stays: it frees a key without a visible change;
      onto a key b already in a's color (a near-duplicate), the error says to write a=b, a repaint that
      looks the same. c=#hex changes key c's color everywhere. '.' works as a source key.
      Order: the key moves of one call apply together, each pixel by the key it had before
      the call, so no move feeds another: 'a<>b' c=a turns a's pixels to b and b's and c's
      to a, and a=b b=a is a swap too. The renames apply together as well: a key another
      'a>b' of the call renames away is free for a new key, so 'g>r' 'd>g' renames g to r and
      d to g, and 'a>b' 'b>a' swaps two keys' names (every pixel keeps its look). A key the
      call keeps (pixels outside --region or the selection draw with it, it is imported, or
      c=g paints with it) isn't free. A key moved twice is E_BAD_ARG. Color changes (V's:
      --variant V) set the palette, moving no pixels: c=#hex, c=d and 'b>t' t=#hex share a
      call.
  paste SRC[+h|+v|+hv] --into DST[:frame] --at x,y [--region x,y,w,h] [--under] [--rekey [KEYS]]
        [--variant-map NAME=V1,V2] [--align shift|pivot|bottom|bbox] [-o OUT]
      Copy SRC's frame (or --region of it) onto DST at x,y; '.' never overwrites. +h / +v
      mirror SRC first, as for compose layers and scene items (--region is then in the
      mirrored frame's coordinates). --under fills only DST's empty pixels: SRC goes behind.
      SRC's keys join DST as a compose layer's join an existing OUT: E_KEY_CONFLICT, the
      variant WARNINGs, --rekey [KEYS] and --variant-map work alike. --align: one paste
      follows a bob across frames.
  compose -o OUT[:frame] [--size WxH] [--under] [--rekey [KEYS]] [--used-keys-only]
          [--variant-map NAME=V1,V2] [--replace] [--dry-run] LAYER@x,y ...
  compose --map MAP [--tile N] -o OUT[:frame] [the options above] [LAYER@x,y ...]
      Stack single frames (later layers on top; '.' never overwrites) into one frame.
      Rules ('pxart help compose-rules' prints only these):
        - A new OUT gets the layers' whole palettes, drawn with or not (for shade ramps). When
          every layer imports the same @palette file, OUT imports it too; else its keys become
          key lines.
        - An existing OUT keeps its own palette, @palette and variants; each layer's keys join
          it.
        - Variants merge by name: OUT's night colors each layer's pixels as that layer's own
          file's night does, and a layer whose file has no night stays at its base colors in
          it. --variant-map dusk=night reads another name as OUT's dusk.
        - A key a layer has in another color than OUT's is E_KEY_CONFLICT; --rekey gives it a
          free key in OUT. The layers' files are read, never written.
        - OUT:frame adds that frame to OUT, or replaces it; a plain OUT is one unnamed grid.
      Examples: 'compose -o room.px tiles.px:cobble@0,0 hero.px:idle/0@4,2', 'compose -o
      party.px:keeper/walk keeper.px:walk/0@0,0 --rekey' (keeper.px's k is another color in
      party.px: it gets a free key there). --dry-run prints what compose says (notes,
      conflicts, --rekey's moves) and OUT's diff (a new OUT: every line); nothing is written.

      A new OUT (rule 1; a shared import re-pointed from OUT's directory): local keys follow
      the import, the keys the layers use first: a key layers have in different colors gets
      the color of the layer that uses it, else the earlier layer's, and one line per source
      file names its colors left out and why: a key none of the file's layers here
      draw with is no conflict, and OUT has the color of the layer named (which draws with it,
      or is the earlier layer). That line is a WARNING when the file needs a key it lost (its
      other frames draw with it, named, or a variant lists it unchanged or keeps it while
      recoloring most keys: a lamp kept lit); --rekey then keeps such keys under free keys in
      OUT.

      --used-keys-only gives a new OUT only the keys its frame uses (and their variant colors;
      a @palette they all import is still imported, since it adds no key lines), so check has
      no 'unused keys' to note. Not the default: the unused keys are often a material's
      ramp: a cloak drawn in its base key c still needs X x C w for 'shade --ramp XxcCw' to
      re-shade it. An existing OUT only ever gets the used keys. So they're no
      surprise, a new OUT's note names the keys its frame doesn't draw with, by file ('...
      from its layers' whole palettes (for shade ramps and recolors): wick.px's E'), and
      check's 'unused keys' note offers the palette --remove that drops them.

      An existing OUT (rule 2): a layer's key is added unless OUT has it in that color, in
      the colors the layer's file gives it in OUT's variants of the same name (a variant the
      file hasn't got leaves the key at its base color there, and a file with variants, none
      of them OUT's, gets a note saying so and naming the --variant-map that would read one
      as OUT's). A plain OUT that exists (no :frame) keeps its palette too, keys from an
      earlier run included, and a note says so: 'note: glade.px exists: keeping its palette
      (61 keys, @variant dusk); --replace starts it fresh' (as does an E_KEY_CONFLICT).
      --replace starts it as if new: the layers' palettes, nothing of the old file (not with
      OUT:frame, whose other frames it would drop, or with --under).

      Key conflicts: a key a layer uses in another color than OUT's (or an earlier layer's) is
      E_KEY_CONFLICT, one line per source file (all its layers: 'layers 1-4, 7 (field.px)')
      naming every key and both colors, and free keys for them ('s>a' 't>b'). The free keys
      are chosen once for the whole compose, so no two lines' suggestions collide: a key OUT
      already has in that color first, then the key an earlier file of this compose was given
      for a color that looks the same in every variant (two packs' one outline share one key,
      and the note says so), then letters and digits, then % + - / : ^ _, and only when those
      run out the keys a shell reads (! $ ` ' * ? [ ] { } ~ & ; | < > ( )) or pxart does (,
      and =). A key any layer's file has isn't free, used here or not. Two ways to use them:
        --rekey: compose gives those keys the free ones in OUT as it goes, and a note says
          which: 'note: --rekey gives field.px's keys free ones in scene.px: 's>a' 't>b'
          (field.px is unchanged)'.
        a copy: 'pxart recolor field.px 's>a' 't>b' -o rekeyed/field.px' (rekeyed/ beside
          OUT), then compose from rekeyed/field.px; the line prints it ready to run.

      --rekey KEYS touches only the keys it lists. --rekey o,r gives o and r free keys and
      leaves the others as they are (a key of another color is still E_KEY_CONFLICT, a variant
      clash still a WARNING). KEY=OUTKEY puts KEY on OUT's key OUTKEY instead of a free one:
      --rekey k=j,n=q uses OUT's j and q, which must be in k's and n's base colors (else
      E_KEY_CONFLICT), and must not be keys of the layer's file; where OUT's variants color
      them otherwise, a WARNING per key names each color that changes. Both mix: k=j,n,s gives
      n and s free keys, and r=r keeps r as it is on purpose. An entry FILE.px:KEY[=OUTKEY] is
      for that source file alone: --rekey o,girl.px:T=V puts girl.px's T on V, gives every
      file's o a free key, and leaves the other files' T alone (a file's own entry wins over
      one for every file; FILE.px is a path, or the name of one source file). When --rekey
      gives a key a free one though an existing OUT has its base color under another key (in
      other variant colors, so it wasn't reused), a note names that key and the --rekey list
      that uses it anyway: 'note: party.px has the base colors of k n as j q, in other variant
      colors; --rekey k=j,n=q uses those anyway'. After crop's --rekey, four digits are the
      rectangle; write --rekey=1,2,3,4 to name four digit keys there.

      The report: compose reports per source file, in layer order, one line per reason: what
      --rekey moved and why (another color there; OUT's base color but other variant colors; a
      key its layers here don't draw with that the file needs; a key --rekey KEY=OUTKEY
      named), the colors a new OUT left out, which of the file's frames draw with a key it
      lost, and one WARNING per key OUT's variants color otherwise ('WARNING: layer 2
      (keeper.px:idle) draws 'r' #c4473a in party.px's variant colors, not keeper.px's: dusk
      #a33a4c (keeper.px: #83344e); compose --rekey r gives it a key of its own'). An
      E_KEY_CONFLICT line offers the same moves --rekey makes, with the same reasons.

      Variants (rule 3) come along for the keys OUT has, in the colors of the layer whose
      key OUT has. Layers from files with different variants (a market's dusk, a keeper's
      night, a candle's dark): each of OUT's variants covers only the layers whose file has
      it, and a note per variant says which layers stay at their base colors in it.
      --variant-map dusk=night,dark (repeatable) builds OUT's dusk from each layer's first
      of dusk, night, dark, so every layer dims together; a new OUT's palette is then
      inlined (its variants are built, not imported). The map adds to the same-name lookup,
      never replaces it: it says only where OUT's dusk comes from, and OUT's other variants,
      night among them, still read each file's variant of the same name (a keeper's night is
      then OUT's dusk and OUT's night too), and may feed several maps. A new OUT merges a variant into the map's only
      when every file that has it gave it to the map (the keeper's night, read as dusk):
      then it gets no variant of its own. One that a file keeps as its own (its file has a
      dusk too) stays one of OUT's variants. With an existing OUT, whose variants stay its
      own, the map says which of each layer's variants to read as OUT's dusk (dusk must be
      one of OUT's). A key a layer draws in OUT's base color but that its file's variants
      color otherwise (one red awning, recolored by one pack's dusk and another's night), in
      a new OUT or an existing one, gets a WARNING: OUT's variants color those pixels OUT's
      way. --rekey gives such a key a free key of its own, as it does a key of another
      color, reusing a key of OUT only when it looks the same in every variant. crop writes
      a new OUT the same way, and crop, paste and frames --copy-to bring keys in the same
      way.

      Comments: the comments above the layers' key and @variant lines come along, as for
      palette --extract-to; a comment naming a key --rekey renamed says so: '# lamp colors
      (l, g) stay lit (renamed l>I g>J)'. The keys right below a key's comment in its file
      (the keys it is about: '# light-emitting keys' above f a i) stay below it. From
      several files, each saying which file's it is ('from keeper.px's @variant night, dusk
      here'), and one that names a variant --variant-map merged into another says so ('from
      player.px, whose dark is dusk here'). A @variant's comment names only the files whose
      variant lines gave OUT's own @variant keys, and when OUT imports the rest, says so
      ('from visitors.px's @variant night; the rest from pal.px's night'). An OUT whose
      palette is inlined also gets the palette files' header comments at the top of its
      palette, each with the files it came from.
      A new OUT's header says how it was made: '# composed by: pxart compose --map room.map -o
      room.px', its paths from OUT's folder (composing OUT whole again updates it). A variant
      WARNING about OUT then also offers composing it again, with --replace, once the sources
      have that variant.

      Frames and canvas: layers can be frames of one parts file: parts.px:hat@3,0
      parts.px:body@0,8. OUT:frame keeps OUT's other frames (OUT may be a palette-only file),
      and the 'wrote' line says when it replaced one. A new frame goes after the last frame of
      its animation (like dup), or at the end for a new animation. Canvas size: --size,
      else --map's, else the frame being replaced, else the other frames of its animation,
      else the first layer. Pixels outside the canvas are cropped; a note counts them.
      --under keeps OUT's frame and draws the layers behind it: they fill only its empty
      pixels (a floor or a shadow under a finished sprite). The frame must exist.

      From a map: --map MAP reads scene's tilemap (legend, rows, '---' layers, +b, a '#'
      legend line; see scene) and makes each cell a layer, drawn where scene draws it, then
      the LAYER@x,y given over them; --tile is scene's (N means NxN). Palette, --rekey and
      --variant-map work as for any layers. A legend entry with its own %VARIANT comes in
      that variant's colors, as a file of its own (tiles.px%dark; --rekey tells it from
      tiles.px), and a PNG in keys of its own: OUT's variants leave both as they are, as
      scene --variant does. So 'compose --map market.map -o market.px' renders as 'scene
      --map market.map --bg transparent --scale 1 -o market.png' does, pixel for pixel, in
      every variant too, except where a translucent pixel lands on another: a .px pixel is
      one key, so it replaces what scene blends.
  dup FILE:ID NEWID [--after ID | --replace] [-o OUT]
  dup FILE:GROUP NEWGROUP [--after ID] [--replace] [-o OUT]
      Copy a frame under a new id, placed after the last frame of NEWID's animation, or
      when that animation is new, after the source's whole animation (or after --after).
      A new animation inherits the source's @anim timing.
      A GROUP copies its frames (walk/0 -> run/0), @anim and @still lines too.
  anim-set FILE:GROUP [ms=N] [direction=D] [repeat=N] [pivot=X,Y] [--still | --no-still] [-o OUT]
      Write timing: updates the '@anim GROUP' line, or adds one, in the order of the frames.
      FILE:GROUP/ID (one frame) takes only ms=N and pivot=X,Y and sets that frame's own
      ('@frame ID ms=N pivot=X,Y'), which wins over the group's. KEY= with no value clears
      a setting.
      A path that is both a group and a frame means the group. Only that one line changes.
      --still adds '@still GROUP' (the group is no animation: UI icons, parts), --no-still
      removes it; FILE with no :GROUP (or FILE:*) --still writes '@still *' (every frame),
      unless every frame already is in a @still group: then it says so and adds nothing. An
      @anim line stays, unused while the group is still.
  palette FILE [--add k=#hex ...] [--variant NAME [--add k=#hex ...] [--keep KEYS]]
          [--variant NAME --derive-from base|VARIANT [--match FILE%V] [--darken F] [--tint COLOR]
           [--keep-lit KEYS] [--lift-darks]]
          [--comment KEY|@variant NAME 'text' [KEY 'text' ...]] [--comment-header 'text']
          [--hoist KEYS] [--export out.gpl|out.hex [--used]] [--extract-to P.px [--repoint]]
          [--remove KEYS [--to KEY]] [--in DIR] [--import P.px] [--order KEYS] [-o OUT] [--dry-run]
      Rules ('pxart help palette-rules' prints only these):
        - No flags lists the palette: each key, its color, where it comes from and how often
          it's drawn, then what each variant recolors.
        - Edits write FILE's own lines (-o OUT: a copy; --dry-run: a diff, nothing written). An
          imported key or variant is edited in its palette file: --hoist moves FILE's keys
          there, and --remove of an imported key takes it out there when no other sprite under
          the directory uses it.
        - A variant is key lines over the base: --variant NAME --add sets keys in it (a key in
          its base color stays lit), --keep lets keys inherit, and --derive-from builds a
          whole variant from the base (--match fits another palette's mood; keys darker than a
          quarter are held dark).
        - Only a variant's edits (--variant with --add, --keep or --derive-from) and --remove
          --to change how FILE renders. --add of base keys, --import, --hoist, --extract-to
          --repoint, --order, --comment and --remove of undrawn keys keep every pixel, in
          every variant.
        - Sharing one palette file: --extract-to P.px [--repoint] writes it from FILE,
          --import P.px points FILE at one, and --in DIR says which sprites under DIR import
          it.
      Examples: 'palette party.px' lists it; 'palette pal.px --variant night --add k=#120e22
      w=#9fb0d4'; 'palette pal.px --variant dusk --derive-from base --match mossback.px%dusk'.

      Listing: after the keys (rule 1), each variant's line: 'dusk: recolors (darker) o x X
      c C; inherits: e E q' (the keys it recolors, then the base keys it leaves alone, both
      in palette order). A recolor is darker, brighter ('brightens y W': lamps lit brighter
      at night) or as bright (Rec. 709 luma, times alpha). A key the variant lists in its
      base color (a lamp that stays lit at night) is neither: 'night: recolors (darker) k w;
      relists unchanged: l g; inherits: nothing'. The palette's comments come along: the
      palette files' header on top, a key's comment after its line ('E #ffe07a local #
      light-emitting keys'), each variant's comment under its line, and the comments on its
      key lines as 'y: # lit in rain: harbor lamp'. A palette file has no 'used' column; --in
      DIR counts the .px files under DIR that import it ('imported by 3 of the .px files under
      crossover/') and, per key, how many of them draw with it ('used by 2 files').

      Base keys: --add k=#hex (or a palette line as the file has it, 'k #hex') adds them.

      Authoring a variant: with --variant NAME, --add sets the keys in that variant instead,
      over what it had, and makes the variant when FILE has none by that name (the example
      above). --variant NAME --keep l,g lets keys inherit the
      base colors: their lines in the variant go, and a key an imported variant recolors gets
      its base color on a line of FILE's own, since the import can't change from here. --add
      and --keep can share one call; the keys must be in the base palette. A key --add gives
      the color it already has is left, and said so: 'k is already #0f0f22 in night;
      unchanged'.

      Deriving a variant: --variant night --derive-from base --darken 0.35 --tint '#10183060'
      --keep-lit y,W sets every key of FILE's palette (imported ones too, unless FILE imports
      a night) in night from its base color (or from another variant's: --derive-from
      dusk), each channel times 1 - F (--darken 0.35 keeps 65%), then the --tint color laid
      over at its alpha, the math of scene --tint; the --keep-lit keys keep their
      --derive-from color, listed as lamps kept lit. A key that comes out in its base color
      gets no line. --add in the same call then sets single keys over the derived ones.

      --match FILE%dusk (or FILE:dusk; FILE alone means the variant being made) first maps
      each channel the way FILE's own base -> dusk does, a gain and an offset per channel
      fitted by least squares over the keys that variant recolors (not the ones it relists,
      the lamps), then --darken and --tint as above: the example above gives the cast
      another pack's dusk (a red gain, a blue offset), where one darken and one tint move
      every channel alike. The output prints the fit: 'r x0.92 -17, g x0.81 -11, b x0.78
      +10'. The hold: a key darker than a quarter (Rec. 709 luma, .2126 R + .7152 G + .0722
      B of the sRGB values, under 64) never comes out with a higher luma than its
      --derive-from color: it is scaled back to that luma, its hue kept (a warmer red may
      look a shade lighter), and listed as held, so a near-black outline stays dark under a
      blue tint or offset; else 'held: none'.
      --lift-darks lets the derive brighten them (a fog), and names the ones it did; --add
      sets one anyway.

      Comments: --comment KEY 'text' sets the comment right above FILE's line for KEY
      (replacing the comment lines there; blank lines stay); --comment @variant night 'text'
      the one above '@variant night', which --extract-to and compose carry as the variant's
      section note; with --variant NAME, --comment KEY is KEY's line in that variant. A line
      of FILE's own: an imported key or variant is commented in its palette file.
      --comment-header 'text' sets the comment at the top of FILE. '' removes a comment; a
      newline in the text makes two comment lines. One --comment takes several in turn
      ('palette pal.px --comment y "lamp" E "flame"'), and both repeat and go with --add in
      one call (the key added first): 'palette pal.px --variant night --add k=#120e22
      --comment @variant night "night: only lamps glow"'.

      Removing keys: --remove k,n takes FILE's own keys out: their key lines, their lines in
      FILE's variants, and the comments above those. A key a frame still draws with is
      E_SELECT (naming the frames and how many px), unless --to j repaints those pixels as j
      first: 'palette party.px --remove k,n --to j'. An imported key is repainted the same way
      in FILE, then removed from the palette file that defines it only when no other .px under
      DIR uses it (draws with it, or lists it in a variant, with no key line of its own); DIR
      is --in DIR, else the directory holding both FILE and the palette file. Otherwise it
      stays there, and a line names who uses it: 'b stays in pal.px: cavegirl.px uses it'.
      A palette file's own --remove can't see the sprites that import it: check them after
      (palette pal.px --in DIR shows who draws with each key).

      Order: --order o,t,k (or otk) moves FILE's own key lines to the top of its palette in
      that order, each with the comment and blank lines above it, the other keys after them as
      they were: group a material's ramp, or put the outline first. Variants and a
      '. transparent' line keep their places.

      Sharing a palette file: --import P.px adds '@palette P.px' to FILE (re-pointed from
      FILE's directory) and drops FILE's key lines P has in the same colors (and their variant
      lines P's variants say already), saying which: 'dropped o t k (their key lines: the same
      colors in pal.px)'. A key FILE has in another color than P's is E_KEY_CONFLICT, with
      free keys for FILE's and the recolor that moves them ('pxart recolor boy.px 'k>a''),
      then --import again. FILE renders as before, in every variant it had: where P's variant
      of that name would recolor a key FILE keeps, FILE's variant lists the key's color, and
      says so. A variant only P has comes along (a note says so).

      --hoist l,g moves FILE's own keys into the palette file it imports (its one @palette),
      with their lines in FILE's variants and the comments above both, so every sprite that
      imports it gets them; FILE renders as before. A key the palette file has in another
      color is E_KEY_CONFLICT (give FILE's a free key first: recolor FILE 'l>L'); a variant
      line the palette file has in another color stays in FILE.

      --extract-to P.px writes FILE's whole palette as a palette file for @palette: every key
      FILE renders with (imported ones too, local ones winning) and every variant, with the
      comments that document them: those above key and @variant lines (a section comment, '#
      glow: left out of dusk on purpose'), from FILE and the palette files it imports, and a
      palette file's header comment (a sprite's header is about the sprite and stays).
      --repoint then replaces FILE's @palette, key and @variant lines with '@palette P.px'
      (re-pointed from FILE's directory): FILE renders the same, and other sprites can share
      P.px.

DRAWING (edits like EDITING: FILE[:SEL] draws on every selected frame, -o OUT, only changed rows
  are rewritten; KEY is a palette key, or '.' to erase). Shapes are clipped to the frame (a
  note says how many px fell outside); x,y may be negative. line, rect, poly, ellipse, arc and
  flood print "painted N px" ("erased" for '.').
  line FILE[:frame] KEY x0,y0 x1,y1 [--width N]
      Bresenham: one pixel per step along the longer axis, 8-connected, no doubled corners;
      the same pixels whichever end comes first (0,0 8,2 is three runs of 3). --width N
      paints N px across the line (down for a mostly-horizontal line, right for a mostly-
      vertical one), centered, the odd pixel down/right.
  rect FILE[:frame] KEY x,y,w,h [--fill]          a 1px border, or filled
  poly FILE[:frame] KEY x,y x,y x,y ... [--fill]
      A closed polygon through the points in order: line's pixels from each point to the
      next and from the last back to the first (1px, the same whichever way round it goes).
      --fill adds every pixel whose center is inside (nonzero winding: a self-crossing star
      is solid). Two points are a line.
  ellipse FILE[:frame] KEY cx,cy,rx,ry | --box x,y,w,h [--fill | --ring N]
      The ellipse inscribed in the box cx-rx..cx+rx, cy-ry..cy+ry: 2*rx+1 wide, so a whole
      center and radius give odd sizes (4,4,3,3 is 7x7) and both ending in .5 give even ones
      (3.5,3.5,3.5,2.5 is 8x6 at 0,1; so is --box 0,1,8,6, rect's x,y,w,h: no halves). A thin
      8-connected outline (Zingl's algorithm), mirror-symmetric, no stray pixels; boxes 1 or
      2 px across are filled. --ring N: N px thick, inward (a halo).
  arc FILE[:frame] KEY cx,cy,r a0,a1 [--width N]
      Part of the circle ellipse cx,cy,r,r draws, from angle a0 to a1 in degrees, counter-
      clockwise, 0 = right, 90 = up (0,90 is the upper-right quarter; 300,60 wraps through 0;
      a1 - a0 >= 360 is the whole circle). A pixel is on the arc when the direction from the
      center to it is in the range, ends included. --width N thickens it inward: a smear or
      swoosh.
  flood FILE[:frame] KEY x,y [--diagonal]
      Bucket fill: repaint the region of x,y's key that touches x,y through sides (4-connected),
      or corners too with --diagonal.
  rotate FILE[:SEL] 90|180|270 [-o OUT]    turn frames clockwise (a WxH frame becomes HxW)
  transpose FILE[:SEL] [-o OUT]            mirror across the top-left/bottom-right diagonal
      For deriving path edges and corners from one tile. -o FILE:NEWGROUP turns a copy, as
      flip's. The shading turns with the pixels (a note says where the light went), so
      re-light with shade and outline --selective after.
  shade FILE[:frame] --ramp d2,d1,base,l1[,l2] [--keys k1,k2] [--base K] [--light nw]
        [--strength N] [--region x,y,w,h] [--dither] [--preview P.png]
      Re-shade a material: the pixels whose key is in --keys (default: the ramp's keys, so a
      shaded material re-shades) get ramp tones by how they face the light. The ramp runs
      darkest to lightest; the base is its middle key (d2,d1,base,l1: the extra key goes
      dark), or --base K. Keys are 'a,b,c' or 'abc'. Nothing outside the material changes.
      --region x,y,w,h repaints only the material inside it, shaded as part of the whole
      frame's material: the region's border is not an edge, only a real one is (an empty
      pixel, another material, or the frame's side).
      Algorithm, per pixel of the material (the shape: those pixels, over the whole frame;
      everything else, and off the frame, is outside):
        1. depth: its distance to the shape's edge (a vector distance transform from the
           edge pixels, those with an outside side neighbor; ~Euclidean, 0 on the edge);
        2. normal: which way is out from it: the pull of the outside pixels within depth + 4
           px minus that of the shape's, 1/distance-weighted (a staircase reads as its slope);
        3. lighting = (normal . light direction) * (1 - depth / strength), in -1..1: an edge
           facing the light is +1, one facing away -1, and the value fades to 0 (the base
           tone) --strength px in (default 2: a rim; the shape's radius: full form shading);
        4. banding: 0..1 splits evenly over the lights, -1..0 over the darks, rounding to the
           nearest step, ties toward the base; then a stray pixel (no 8-neighbor of its own
           tone) takes its neighbors' commonest tone. No noise.
      --dither mixes adjacent tones with a 4x4 ordered (Bayer) pattern where the lighting is
      within a quarter step of a band boundary (and skips the stray-pixel pass). --light: n
      ne e se s sw w nw (default nw). --preview P.png renders the result (render's grid and
      rulers) and writes nothing else. Prints the pixels it changed, by the tone they got,
      darkest first: "changed 24 px: 4->A, 8->B, 8->D, 4->E" (with --preview, "would change").
      'shade hero.px:idle/0 --ramp XxcCw --keys c' shades the cloak c with the ramp X x c C w.
  outline FILE[:frame] --key K [--outside | --inside] [--lit L [--selective]] [--light nw]
          [--corners] [--preview P.png] [--min-alpha A] [--pad]
      Outline the frame's shape (every pixel that draws in some variant; with --min-alpha A, at
      alpha A+). --outside (the default) paints the empty pixels touching the shape on a side
      (--pad: first grow the frame 1px where the shape meets its edge, pivots and all); --inside
      repaints the shape's own pixels that have an empty side (off the frame is empty). Sides only
      is the pixel-perfect rule: a diagonal edge gets a 1px staircase and a square corner is cut,
      so no doubled (L-shaped) corners; --corners also takes the pixels touching only at a corner
      (square corners, a 2px staircase). Selective outline (--lit L, or --selective --lit L):
      outline pixels facing the light get L (a darker tone of the material), the rest K. Facing:
      an outline pixel's outward normal (shade's, over 2px) dotted with the light's direction;
      above 0 is lit, so a nw light lights the top and left edges and a 45-degree edge (ne, sw)
      stays K. --light and --preview as for shade. Prints "changed 12 px: 6->o, 6->l" (by key; px
      that had it already don't count).

CONVERTING
  export FILE|DIR[:SEL]... [--frames DIR] [--aseprite sheet.json] [--tiled tiles.tsj] [--variant V]
         [--prefix-file] [--exclude GLOB] [--indexed X.json]
      --frames: one PNG per frame at DIR/<frame id>.png, and DIR/pivots.json when frames have
        pivots: {"walk/0": {"x": 8, "y": 23}, ...} (frames without one are left out)
      --aseprite: sheet PNG + Aseprite-style JSON (frames, durations, frameTags; pivots as
        Aseprite writes them: meta.slices = one slice "pivot" with a key per frame index,
        {"frame": N, "bounds": {"x": 0, "y": 0, "w": W, "h": H}, "pivot": {"x": X, "y": Y}},
        bounds = the whole frame, pivot relative to it, "pivot" left out for a frame without)
      --tiled: sheet PNG + Tiled tileset JSON with per-tile animations (worlds: help worlds)
      (--aseprite x.json and --tiled x.tsj write the same x.png)
      FILE:SEL exports only those frames; several selectors of one file add up, in file
      order: 'export harbor.px:cobble harbor.px:water --tiled t.tsj' (no 32x32 props).
      Several files and directories export together, file by file in the order named; a
      directory stands for every .px under it (palette files skipped, --exclude as for
      sheet): 'export town/ --frames out/'. No two frames may get one name: an id two files
      share (each file's idle/0; two animations of one name too), or, with --frames, two ids
      that differ only in case (Door, door: one file on macOS), is E_DUP_FRAME, and nothing
      is written. --prefix-file ids them FILE/ID, FILE being the file's path under its
      directory without .px, or a named file's stem (roofs/roof-red, props/well/well; an
      unnamed grid is FILE alone), in the PNG paths, the Aseprite filenames and tags and the
      Tiled animations.
      Id order (the Aseprite frame index, the Tiled tile id, the sheet position): 0, 1, 2...
      over the exported frames, file by file, with each animation group contiguous, groups
      in order of first appearance, and all top-level frames (no '/' in the id) together as
      one group where the first of them appears. A file that keeps each group together, and
      its top-level frames together, gets ids in file order; otherwise a frame moves up to
      its group: a/0 b/0 a/1 -> a/0=0 a/1=1 b/0=2, and icon walk/0 badge -> icon=0 badge=1
      walk/0=2. Adding, removing or moving frames can renumber others, and a Tiled map
      painted with the old tileset keeps the old ids.
  from-png A.png [B.png ...] [-o OUT.px] [--id PREFIX] [--prefix-dir] [--palette P.px] [--colors N]
           [--hard-edges] [--names A,B,... | --labels FILE.csv [--label-col proposed_name] [--file-col filename]]
  from-png SHEET.png --grid WxH [--names A,B,...] [--by rows|cols] [-o OUT.px] [--id PREFIX] [--palette P.px]
           [--colors N] [--hard-edges]
      PNG -> .px with exact pixels. One PNG and no --id: a single unnamed grid. Several PNGs,
      --id, or an existing OUT: frames named PREFIX/<png stem>, added to OUT (replacing same-id
      frames, which the 'wrote' line names). Two PNGs of one run that would get one id (two packs'
      tile_0002.png) are E_DUP_FRAME, naming both: --prefix-dir ids each one FOLDER/STEM by its
      directory's name (dungeon/tile_0002, creatures/tile_0002). Colors already in OUT keep their
      keys, so separate runs share one palette. --palette P.px starts a new OUT that imports P and
      reuses its keys.
      Naming loose PNGs: --names A,B,... gives one frame id per PNG, in order (as many names as
      PNGs; '' skips one). --labels FILE.csv names them from a CSV, the way packs ship one
      ('filename,proposed_name,...'): each PNG takes the --label-col (default proposed_name) of
      the row whose --file-col (default filename) names it, a path relative to the CSV's
      directory, or else its file name alone. --labels repeats, one CSV per pack (with
      --prefix-dir: dungeon/wall-stone-top, creatures/skeleton). A PNG no row names is E_SELECT.
      --prefix-dir and --id PREFIX go in front of either.
      --grid 16x16 slices one sheet into 16x16 cells, a frame each; a cell with no opaque pixel is
      skipped. Each row of cells is a group (--by cols: each column), its cells left to right (top
      to bottom) frames 0, 1, ...: --names names the groups in turn (an empty name skips its row),
      else they are STEM/row0, STEM/row1 (col0, ...); --id PREFIX goes in front. A pack whose
      columns are directions and rows are steps: 'from-png Walk.png --grid 16x16 --by cols --names
      walk/down,walk/up,walk/left,walk/right -o boy.px' writes walk/down/0-3 and so on. A sheet
      that isn't a whole number of cells is E_BAD_ARG, unless the strip left over is empty (then a
      note says so).
      Too many colors (a photo)? --colors N reduces them to OUT's colors and at most N new ones;
      says how far. --hard-edges first: alpha 128+ opaque, the rest clear.

HELP
  help [all | recipes | TOPIC | CMD]
      'pxart help recipes': seven workflows, command by command; 'help worlds': Tiled worlds.
      'pxart help all' prints this whole reference; 'pxart help TOPIC' one part of
      it (a heading here, any case); 'pxart help CMD' is 'pxart CMD -h'.
      'pxart help compose-rules' and 'pxart help palette-rules' print only the rules those
      sections open with.

ERROR CODES
  E_VERSION E_BAD_KEY E_DOT_RESERVED E_BAD_COLOR E_DUP_KEY E_PALETTE_AFTER_GRID
  E_PALETTE_FILE E_BAD_ROW E_ROW_WIDTH E_UNKNOWN_KEY E_EMPTY_FRAME E_NO_FRAMES
  E_BAD_ID E_DUP_FRAME E_MIXED_FRAMES E_BAD_ARG E_VARIANT_KEY E_UNKNOWN_SECTION
  E_SELECT E_KEY_CONFLICT E_TILE_SIZE E_FILE
  Every error line starts with the command ('ellipse: E_BAD_ARG: cy=1.5 and ry=1 ...'). An
  error in an input file also says which input it came from, then where in the file:
  'compose: layer 2 (parts.px:hat): parts.px:4: E_ROW_WIDTH (frame hat, ...'.
  A file that can't be read is one E_FILE line too (see FORMAT: paths).
  Inputs are named like -h names them: FILE, SRC, --into, -o, OUT, A/B, layer N, item N,
  file N (the Nth of several), --map, --palette, stdin. check reports per file instead.
  A command that fails prints none of its notes or WARNINGs: they describe the write it was
  about to make (a grid renamed, keys rekeyed), and nothing was written.
This page is built from site/src/data/reference.json in the repository.Edit on GitHub