The World Vol 2 · Drawing the World
ch 17 / 105
Chapter 17

Sixteen Pixels Across

The 256 sprite decisions

The client can now copy a rectangle of pixels out of a sheet and onto the framebuffer, leaving every pixel the alpha channel says to leave alone. It has been doing that with a sheet a loop wrote from a character mask, because a loader needs something to load and nobody had drawn anything.

Sixteen pixels is a strange size to hear proposed, on a display that has thousands of them across its width. A sixteen-by-sixteen sprite is 256 pixels, so one pixel is nearly half a percent of everything you are allowed to say. A sprite this small is 256 decisions, and the way to make 256 good ones is to make most of them once, before you draw: one fixed palette, one light direction, and a silhouette that reads before a single interior pixel exists.

Move an eye one pixel left and the animal is looking somewhere else. Nothing at that size is incidental, and nothing gets fixed by adding detail outside the cell; the cell has no spare room.

The renderer is ready for art, so the world now looks like whatever a person puts in assets/. The directory is laid out for every asset this book ships, a sixteen-colour palette is written down as a file both the editor and a checker read, and the first creature in The Hollow is a walker: sixteen pixels across, four frames, drawn a pixel at a time and saved where the loader can find it.

A small program opens the finished art and refuses it over one pixel of a colour that is not on the palette. The bar every asset in this book gets held to is one question: would you take a screenshot of this world without being asked to. Art that clears that bar at this size was not drawn with a steadier hand than art that misses it. It followed rules, and the rules are learnable in an afternoon.

Sixteen colours, chosen once, written to a file

▧ Tool: a pixel editor, free, either one

Every drawing step below is written for LibreSprite (libresprite.github.io), an open fork of an older Aseprite release: install it, open it, and you have a canvas, a palette dock and a pencil that draws exactly one pixel. Pixelorama (github.com/Orama-Interactive/Pixelorama) does every one of those jobs under slightly different menu names: it imports the same .gpl palette, has the same one-pixel pencil, and exports a PNG with a real alpha channel. Pick one. Only the menu names move.

Start with colour, because colour is the decision that goes worst when it is made one pixel at a time. An editor hands you a wheel with sixteen million answers on it, and reaching into that wheel every time a pixel needs shading gives you a hundred slightly different browns that never look like they came from one place.

A palette is the alternative: a small fixed set of colours, decided in advance, with nothing outside it allowed anywhere. Sixteen is enough for a valley and few enough to hold in your head. Two are anchors, the darkest dark and the lightest light, and every other colour lives between them. Twelve are four ramps of three: soil, rock, water and growing green, each holding a shadow, a midtone and a lit tone of one material. The last two are a warm accent pair, ember and gold, kept for things that should catch the eye.

▣ Build · stage 1: the palette becomes a file
mkdir -p assets/palette assets/sprites assets/tiles
assets/palette/valley-16.gpl

GIMP Palette
Name: Valley 16
Columns: 16
# The World - the valley palette. Four three-step ramps between
# two shared anchors, plus a warm accent pair.
#
 24  26  41	ink
 76  47  34	soil-d
117  86  59	soil-m
161 135  90	soil-l
 50  56  71	rock-d
 90 100 112	rock-m
138 151 158	rock-l
 36  62 107	water-d
 61 119 153	water-m
109 190 199	water-l
 37  76  57	green-d
 76 120  65	green-m
144 168  88	green-l
199  89  56	ember
240 190  53	gold
245 235 215	bone
$ cat assets/palette/valley-16.hex
181A29
4C2F22
75563B
A1875A
323847
5A6470
8A979E
243E6B
3D7799
6DBEC7
254C39
4C7841
90A858
C75938
F0BE35
F5EBD7

Two files, same sixteen colours. The .gpl is what the editor eats: in LibreSprite, Palette > Load Palette, point it at the file, and the palette dock becomes those sixteen swatches and nothing else. Every colour you can now click is a colour you chose in advance. The .hex is the same list stripped to one six-digit colour per line, which is the version a program can read without a parser, and the checker at the end of this chapter reads exactly that.

The two files say identical things in different bases: 24, 26, 41 is 18, 1A, 29, one byte per channel, packed the way the framebuffer packs a colour minus its alpha byte. Every palette entry is opaque, so FF is left out of both files.

A strip of sixteen colours: a near-black blue, three browns, three blue-greys, three blues, three greens, an orange, a gold, and a warm off-white

Figure 17.1: assets/palette/valley-16.png, the whole palette as a sixteen-by-one image. Left to right: the dark anchor, soil, rock, water, green, the two accents, the light anchor.

Now the part that decides whether a palette is any good. The tempting way to build a ramp is to pick a brown and drag the brightness slider up and down, which gives a tint ramp: one colour, three amounts of light on it. It looks flat and slightly plastic, and everybody who draws pixel art makes one before learning why.

Real light does something else. A surface in shadow is not lit only by the sun; it is lit by the sky, which is blue, so shadows run cool. A surface in direct light picks up the warmth of the light itself, so highlights run warm and, since strong light washes colour out, less saturated. Three numbers therefore move together on the way up a ramp, and only one of them is brightness. Look at the soil ramp as three numbers each: hue 18, saturation 55, value 30 for the shadow; hue 28, saturation 50, value 46 for the midtone; hue 38, saturation 44, value 63 for the lit tone. Ten degrees of hue per step toward yellow, five or six points of saturation shed per step, and the brightness climbing all the while.

Two rows of five squares. The top row runs from near-black brown to a bright orange-tan, every square the same brown hue. The bottom row runs from a blue-black through browns to a warm cream, each square a slightly different hue.

Figure 17.2: the same five brightness values, twice. Top: hue and saturation locked, only value moving (291E14, 4C3826, 75563B, A17650, F5B47A). Bottom: the shipped soil ramp between its two anchors (181A29, 4C2F22, 75563B, A1875A, F5EBD7). The middle square is the identical colour in both rows.

The middle square proves the comparison is fair: both rows pass through 75563B and both use the same five brightness values, and only one of them has a valley in it. The top row goes chalky at the light end and muddy at the dark end, because a colour losing brightness with nothing else moving just fades. The bottom row leans blue going down and cream going up, which is what a lump of earth does between noon and dusk.

A hue-shifted ramp against a value-only ramp, in numbers Two rows of five boxes. The upper row is the shipped soil ramp, each box holding a hex colour and its hue, saturation and value; the hue steps between boxes are labelled plus 146, plus 10, plus 10 and plus 2 degrees. The lower row holds the same five values with hue locked at 28 and saturation locked at 50, and every hue step between its boxes is labelled plus 0 degrees. ONE RAMP, FIVE STEPS, THREE NUMBERS MOVING hue-shifted: the ramp this book ships 181A29 4C2F22 75563B A1875A F5EBD7 H232 S42 V16 H18 S55 V30 H28 S50 V46 H38 S44 V63 H40 S12 V96 ink soil-d soil-m soil-l bone hue step: +146 +10 +10 +2 value only: hue and saturation locked at the midtone 291E14 4C3826 75563B A17650 F5B47A H28 S50 V16 H28 S50 V30 H28 S50 V46 H28 S50 V63 H28 S50 V96 hue step: +0 +0 +0 +0 V is identical down every column. Only H and S separate the rows.

Figure 17.3: figure 17.2 written as numbers. The five brightness values match column for column, so everything the eye sees between the two rows was done by hue and saturation.

∑ Math Interlude: three numbers instead of three channels

A colour has been three numbers since the framebuffer was built: red, green and blue, each 0 to 255. That form is right for a screen and useless for choosing, because nobody knows offhand what raising red by 30 does to a brown. Editors offer a second set of three numbers for the same colour, and those are the ones to think in.

Hue is a position on a colour wheel in degrees: 0 red, 60 yellow, 120 green, 180 cyan, 240 blue, 300 magenta, 360 back to red. Saturation is how much colour there is, 0 grey to 100 as vivid as the display allows. Value is brightness, 0 to 100, and at 0 the colour is black whatever the other two say.

Check one against the palette. Soil's midtone is hue 28, saturation 50, value 46: an orange, since 28 sits between red and yellow, muted by the saturation and darkened by the value. A muted dark orange is brown, and the editor turns those three numbers into 117, 86, 59, which the file writes as 75563B. The rule that built the ramp fits on one line:

lighter step = hue + 10°, saturation − 6, value + 17

Ten degrees is a judgement, and it varies by material. Green moves hue 150, 108, 78, big steps, because a leaf in shadow really is blue-green and the same leaf in sun really is yellow-green. Rock moves 222, 212, 200 and sheds saturation hard, 30 to 20 to 13, since stone is nearly grey and the little colour it has belongs to the sky. What never varies is the direction: up the ramp is warmer and less saturated.

Hhue: where on the colour wheel, 0–359 degrees (0 red, 120 green, 240 blue)
Ssaturation: how much colour, 0 (grey) to 100 (vivid)
Vvalue: brightness, 0 (black) to 100 (as bright as this hue gets)
rampthree colours of one material: shadow, midtone, lit
anchora colour shared by every ramp: here ink at the dark end, bone at the light end

Two more decisions are baked into those sixteen numbers, both about reading a frame at a glance. The anchors are shared: every ramp bottoms out at 181A29 and tops out at F5EBD7, so the darkest pixel of a rock and the darkest pixel of a creature are the same colour and the world looks lit by one sky. And three of the four ramps sit at the same three brightnesses, soil at value 30, 46, 63, rock at 28, 44, 62, green at 30, 47, 66, so soil, stone and grass carry the same weight and form comes through before colour does. Water is the deliberate exception at 42, 60, 78, a full step brighter, because it reflects the sky and should be the first thing you find on a map.

Silhouette, then interior, then light

Open a new sixteen-by-sixteen file, load the palette, and zoom to 800%. Everything below gets drawn at that zoom and judged at 100%, and the two are different jobs: at 800% you are placing pixels, at 100% you are looking at an animal the size of a full stop. Toggle between them constantly, because a sprite that only works zoomed in does not work. Then draw the outside first, in one flat colour, and leave the inside alone until the outside is right.

▣ Build · stage 2: a silhouette, in one colour, that reads
assets/sprites/walker.png, frame 0 — silhouette only

................
..........####..
.........######.
......##.#######
.....###.#######
.##.####.######.
##############..
##############..
.############...
..###########...
..##########....
...#########....
...###....###...
...###....###...
...###....####..
...####.........

Pencil, one pixel wide, ink from the palette, and every marked cell above filled solid. Now the test: this panel is a black shape on nothing, and it should still be obviously an animal facing right. Head high on the right with a snout poking past the body, a small fin on the shoulder, a stubby tail on the left, four legs in two pairs, and the whole thing standing on the bottom row. Cover the rest of this page and the silhouette alone tells you which way it is walking.

The features that carry the read are the ones that break the outline: the fin, the snout, the gap between the legs. Those are what a viewer sees at 100%, where the interior is a dozen pixels of grey and the outline is the picture. Shading rescues nothing here, so this is the stage to redraw five times.

Two habits belong here, and both are about where pixels go, not what colour they are. The first is the outline: every pixel on the boundary of the silhouette is ink, all the way round, no gaps. That closed dark edge is what lets a small sprite sit on any background; let the body colour touch the background directly and the creature vanishes the moment it walks onto a tile of similar brightness. The second is clustering. A single stray light pixel in a field of midtone reads as dust on the monitor, not as a detail, so pixels travel in groups: on the shipped soil tile, three colours over 256 pixels, every mark is two or three pixels together.

▣ Build · stage 3: flat interior, then one shadow
legend  . transparent   o ink      M rock-m   D rock-d
        L rock-l        H bone     E ember    G gold

flat fill                      shadow added
................               ................
..........oooo..               ..........oooo..
.........oMMMMo.               .........oLLMMo.
......oo.oMMoMMo               ......oo.oMMoMMo
.....oMo.oMMMMMo               .....oEo.oMMMDDo
.oo.oMMo.oMMMMo.               .oo.oEEo.oMMMDo.
oMMoMMMMoMMMMo..               oMMoLLMMoDDMMo..
oMMMMMMMMMMMMo..               oMMMMMMMMMMMDo..
.oMMMMMMMMMMo...               .oMMMMMMMMMDo...
..oMMMMMMMMMo...               ..oMMMMMMMDDo...
..oMMMMMMMMo....               ..oMDDDDDDDo....
...oMMooooMo....               ...oDDooooDo....
...oMo....oMo...               ...oMo....oMo...
...oMo....oMo...               ...oMo....oMo...
...oMo....oMo...               ...oDo....oMo...
...oooo.........               ...oooo.........

The left panel is the bucket fill: every interior pixel rock-m, the middle of the rock ramp, plus the one ink pixel at column 12 row 3 that is the eye. It is the sprite with no lighting information in it at all, and that is the honest starting point, because every darker or lighter pixel from here has to be justified by a light that comes from somewhere.

Pick the somewhere and never move it: the light in this world comes from the upper left. That one sentence decides every shading question in the book. The right panel spends rock-d on the surfaces facing away from it: the belly, the underside of the flank, the back of the far leg, and the wedge of neck directly beneath the head, dark because the head is above it. That last one is occlusion, not angle, and it is the pixel most often left out; without it a head reads as a sticker.

The right panel also gets its first accent: the shoulder fin, in ember, three pixels. That is the whole budget for the loudest colour on the palette, and three is plenty, because C75938 at value 78 is brighter and far more saturated than anything else on the creature.

▣ Build · stage 4: where the light actually lands
assets/sprites/walker.png, frame 0 — finished

................
..........oooo..
.........oHHLLo.
......oo.oLLoLMo
.....oGo.oMMMDDo
.oo.oEEo.oMMMDo.
oMMoHHLLoDDMMo..
oMMLLLLLMMMMDo..
.oMMMMMMMMMDo...
..oMMMMMMMDDo...
..oMDDDDDDDo....
...oDDooooDo....
...oMo....oMo...
...oMo....oMo...
...oDo....oooo..
...oooo.........

Four pixels of bone, the light anchor, and that is the entire highlight: two on the top of the skull and two on the ridge of the back, each pair on the upper-left face of a curve. One pixel of gold at the tip of the fin. Everything between highlight and shadow is rock-l and rock-m, laid down as a broad band across the body instead of a line along the outline.

Count the entries this sprite uses and you get seven of sixteen: ink, three steps of rock, bone, ember, gold. Not one colour was mixed, sampled or nudged, and that restraint is why the walker belongs on the same ground as the soil tile, which was drawn from three entirely different entries of the same sixteen.

Four small grey creatures in a row: a solid black silhouette, the same shape in flat grey with a black outline, then with dark shading and an orange fin, then finished with cream highlights and a gold fin tip

Figure 17.4: assets/sprites/walker-stages.png: the four stages above, in order, at four pixels to one on this page. Each panel is the same 256 cells; only what fills them changed.

Pillow shading and banding

Two shading mistakes account for most bad small sprites, and both come of shading that follows the outline instead of the form. They show up clearest on something simpler than a creature, so here is one rock, drawn three times.

Three grey rounded rocks: the first bright in the middle and dark all round the rim, the second with a cream rim tracing its whole upper edge, the third lit from the upper left with a small bright patch and a broad dark lower right

Figure 17.5: assets/palette/shading-compare.png: pillow shading, banding, and the same rock lit on purpose. All three use the identical four entries of the rock ramp plus bone.

The first rock is pillow shaded: its colour was chosen by distance from the edge, the further in the lighter, all the way round. Easy to draw, and it describes no light source at all, since real light comes from one direction and leaves one side dark. The giveaway is that you cannot say where the sun is, and the object looks inflated.

The second rock is banded. Its light does come from the upper left, so the direction is right, but each colour boundary runs one pixel inside the boundary before it, hugging the outline the whole way, and the result is a bright rim tracing the top like a stripe of paint. Banding turns up any time two colour edges run parallel for more than a few pixels: the eye stops reading a curved surface and starts reading a ring.

The third rock is the one to copy. One light, upper left. A compact highlight of five bone pixels, clustered, set in from the rim so it never becomes a rim itself. A broad midtone doing most of the work. A crescent of rock-d cutting diagonally through the lower right instead of following the edge, and a hard dark bottom where the ground blocks the light. No two of those boundaries are parallel for long, and none of them stays the same distance from the outline twice.

The third mistake is not a drawing mistake, and it is the one that survives review, which is why it needs a program.

⚠ Worked failure: the highlight that came from the slider

Halfway through stage 4 the highlight looks too dark, so the obvious move is to fix it in place: click the shoulder colour, drag the brightness slider up until it looks right, paint. The editor is happy to do that. It keeps hue and saturation exactly where they were and raises value alone, which is the tint move from figure 17.2 performed one pixel at a time. The sprite looks fine on the canvas. Save it and run the checker:

$ go run ./cmd/palcheck -pal assets/palette/valley-16.hex assets/sprites/walker-slider.png
assets/palette/valley-16.hex: 16 colors loaded
assets/sprites/walker-slider.png   64x16   539/1024 opaque   7 colors  1 OFF PALETTE
    pixels sha256 6249260c0ce23133df6f45a65fc16d3cf0b7f7ff9f094e358bdacbd9f208d96c
    7E8D9E first at (12,2)
1 files checked, 1 off-palette color(s)

One colour, 7E8D9E, first seen at column 12 of row 2, which is the top of the skull. Put it beside the palette entry it was standing in for and the numbers say what happened. rock-m is hue 212, saturation 20, value 44. The slider took it to hue 212, saturation 20, value 62: same hue, same saturation, brighter. rock-l, the colour that belonged there, is hue 200, saturation 13, value 62. Identical brightness, twelve degrees warmer, and seven points less saturated.

The two colours sit the same distance up the ramp and only one of them says light fell on the surface; the other says somebody turned the same grey up louder. Against six hue-shifted neighbours it reads as a slightly plastic patch on an otherwise convincing animal, which is hard to catch by eye at 100% zoom and trivial to catch by equality. The palette is a fixed set, membership is a lookup, and a pass over 1,024 pixels does in a millisecond what staring cannot do at all.

▣ Build · stage 5: three more frames, and a program that checks them
assets/sprites/walker.png — rows 11 to 15 of all four frames

frame 0            frame 1            frame 2            frame 3
...oDDooooDo....   ...oDDDoooDo....   ...oDDDooDDo....   ...ooDDDoDDo....
...oMo....oMo...   ....oMo...oMo...   ....oMo..oMo....   .....oMo.oMo....
...oMo....oMo...   ....oMo...oMo...   ....oMo..oMo....   .....oMo.oMo....
...oDo....oooo..   ....oDo...oDo...   ....oooo.oDo....   .....oDo.oooo...
...oooo.........   ....oooo..oooo..   .........oooo...   .....oooo.......
// cmd/palcheck/main.go — the palette as an assertion
func loadPalette(path string) ([]render.Color, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var out []render.Color
	for n, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		v, err := strconv.ParseUint(line, 16, 32)
		if err != nil || len(line) != 6 {
			return nil, fmt.Errorf("%s:%d: %q is not a six-digit hex color", path, n+1, line)
		}
		out = append(out, render.Color(0xFF000000|uint32(v)))
	}
	return out, nil
}
// cmd/palcheck/main.go — one pass per file, over pixels the loader already
// knows how to read
	for _, path := range flag.Args() {
		b, err := render.LoadPNG(path)
		if err != nil {
			fmt.Fprintln(os.Stderr, "palcheck:", err)
			os.Exit(1)
		}
		used := map[render.Color]bool{}
		opaque := 0
		off := map[render.Color][2]int{}
		for i, c := range b.Pix {
			if c.A() == 0 {
				continue
			}
			opaque++
			used[c] = true
			if _, ok := on[c]; !ok {
				if _, seen := off[c]; !seen {
					off[c] = [2]int{i % b.W, i / b.W}
				}
			}
		}
		...
	}
$ go run ./cmd/palcheck -pal assets/palette/valley-16.hex assets/sprites/walker.png assets/sprites/walker-stages.png assets/tiles/soil.png
assets/palette/valley-16.hex: 16 colors loaded
assets/sprites/walker.png          64x16   539/1024 opaque   7 colors  all on palette
    pixels sha256 c9e05e61cee421c397b41e708053abe43813b39aee37f2070dddee2b4fcd4897
assets/sprites/walker-stages.png   64x16   536/1024 opaque   7 colors  all on palette
    pixels sha256 73720cc225945b116ec6988fabbc077d8b2440eb776239b47deafe6d0f15c77d
assets/tiles/soil.png              16x16   256/256  opaque   3 colors  all on palette
    pixels sha256 f644eecfc886c0cf180bfb3ff5a2a6d749a105ae61e86ffc7e978bb34c701ba3
3 files checked, 0 off-palette color(s)

The three frames after the first are the finished sprite with rows 12 to 15 redrawn and everything above row 11 untouched: the hind leg walks forward from column 3 to column 5 and lifts on the way through, the fore leg repeats the lift one frame out of phase, so the two are never off the ground together, and row 11 moves only because a leg beneath a belly pixel makes that pixel interior instead of outline. Four cells side by side is a 64-by-16 sheet with a cell size of 16, exactly the layout the atlas loader was built around: source rectangle 16i, 0, 16, 16 for frame i. Nothing in the loader changes. The stand-in stops being loaded, assets/sprites/walker.png takes its place, and the same blit puts a drawn animal on the ground.

The whole program is under a hundred lines and reuses everything beneath it. Loading a PNG into a buffer and hashing its pixels were both built for snapshot tests; here the same two functions become an art check. The hash line is the useful side effect: those 64 characters name the picture, not the file, so re-exporting at a different compression setting leaves them alone and one moved pixel changes them completely.

Four frames of the same small grey-blue quadruped side by side, each with a cream highlight on its back and skull and an orange fin on the shoulder, its legs in a different position in every frame

Figure 17.6: assets/sprites/walker.png, the shipped sheet: 64 by 16, four cells of 16, seven palette colours, 539 opaque pixels. Shown here at about seven pixels to one; in the client each frame is the size of a full stop.

The palette file as interface

The walker did not need talent. It needed a fixed set of colours, a fixed light direction, an order of operations that puts the silhouette before the shading, and a program that enforces the first of those. Constraints, doing the job here that a type does in the simulation: cutting the space of possible answers down until the remaining ones are mostly right. A colour picker offers 16,777,216 answers to every shading question. The palette offers sixteen, and once the pixel is known to be rock, three. A world made of decisions among three comes out coherent for a reason that has nothing to do with taste: no two objects in it can disagree about what colour a shadow is.

The palette is also a genuine interface, in the sense the code has used all volume. On one side is art, made by a person looking at a screen; on the other a program that indexes a sheet by rectangle and copies bytes. The file in assets/palette/ is the agreement between them, and being a file instead of a habit, both sides can be checked against it. That is the move behind hashing a framebuffer as well: take what everybody was assuming and write it down where a test can read it.

The layout under assets/ is deliberately boring: palette/ holds the colour agreement, sprites/ holds anything that moves, tiles/ holds anything the ground is made of. Every asset this book ships lands in one of those three, comes out of the sixteen colours, and passes the checker before it counts as done.

Checkpoint

✓ Checkpoint: what you can now do
  • Build a three-step ramp by hand, moving hue, saturation and value together, and say which way each one goes as the ramp lightens.
  • Given two five-step ramps with identical brightness values, tell which one locked its hue and explain what the locked one costs.
  • Draw a sixteen-pixel silhouette, judge it at 100% zoom with the interior still empty, and name the three or four features that carry the read.
  • Shade a small object from one light direction, place a highlight that does not trace the outline, and say why the pixel under a creature's head is dark.
  • Given a sprite, point at pillow shading and at banding and describe the rule each one broke.
  • Run palcheck over a directory of art and read its report: size, opaque pixels, colours used, off-palette colours with the first coordinate of each.
⚡ Exercises: try first, then reveal
Exercise 1: the fifth ramp. The palette has no sand. Build a three-step sand ramp in the editor's HSV fields, following the direction rule, and write down the three hex values. Then say why you should probably not add it.

Sand is a pale warm yellow, so start the midtone near hue 44, saturation 38, value 72, which the editor reports as B8A572. Step down by the rule and up by it: hue 34, saturation 44, value 54 gives 8A6F4D; hue 54, saturation 32, value 88 gives E0D999. Three colours that behave, and dropping them into the .gpl takes a minute.

The reason to hesitate is the value tier, not the hue. Sand at 54, 72 and 88 sits a step above soil and nearly on top of water, so a beach drawn with it would compete with the ponds for the eye at the moment the map most needs to be readable. Nineteen colours is also more than you can hold in your head. The cheap version is soil-l with clustered bone speckle, and it costs no entries at all.

Exercise 2: break it on purpose. Open the finished walker, paint the whole flank bone, export it as assets/sprites/walker-broken.png, and run the checker. Predict both what it will say and what it will not say.

The checker passes it. Bone is the last entry on the palette, so nothing is off-palette; the file still reports 64x16, still 539 opaque pixels, and the verdict is still all on palette. The one line that moves is the pixel hash, because a few dozen pixels changed value.

That is the honest limit of the tool. palcheck enforces membership, not judgement: it knows every colour came from the agreed set and has no opinion about the light direction, the rim, or whether the animal reads at 100%. Those stay with the artist. What it buys you is the one failure an eye is worst at finding.

Exercise 3: a second creature, in the same sixteen. Draw a sixteen-pixel forager: rounder than the walker, no fin, and coloured from the green ramp instead of rock. Ship it as assets/sprites/forager.png and get it past the checker.

Work in the same order and it takes twenty minutes. Silhouette first, in ink, differing from the walker in outline alone: low round body, a head that is a bump and not a raised skull, short legs. Bucket the interior green-m, put green-d on the belly and under the head, put green-l on the upper-left curve of the back in clusters of two or three, and spend two pixels of bone at most.

Then the test that matters: drop both creatures on the soil tile at 100% zoom and look. The forager should read as a different animal from its silhouette alone, and both should sit there as though the same sun were up. If the green one looks cut out of a different picture, the culprit is nearly always a highlight that drifted off the ramp, and palcheck will name the colour and the pixel.