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

The Pixels a Sprite Skips

The 98 walker pixels

The renderer fills rectangles, draws lines, refuses to write outside its clip rectangle, and reduces a finished frame to a number a test can assert. Everything it has produced is made of solid blocks, which is enough for ground, water and a ridge, then fails the moment something has to stand on top of them.

A picture of a creature is a small rectangle of pixels, most of which are not the creature. A sprite is a rectangle of pixels in which every pixel carries its own instruction: alpha 0 means leave the framebuffer alone, alpha 255 means overwrite it, and anything in between means mix the two, in arithmetic wide enough to hold the product.

Try it with what the client already has. A walker is roughly twelve pixels by twelve, so draw one as a filled rectangle in a creature green. What lands on the frame is a brick: green where the animal is, green in the gap between its legs, green in the four corners where there is nothing at all, and the pond it was standing at the edge of disappears behind a square.

Trim the rectangle and the brick gets smaller. It stays a brick.

In the sixteen-by-sixteen cell this chapter builds, 98 pixels are the walker and the other 158 are whatever happens to be behind it: soil, water, another creature, sky. A sprite's colors are only half of what it carries. The other half is the list of pixels it must leave alone.

Chapter 12 packed a byte for exactly this and then left it idle. Alpha rides in the top eight bits of every color, does nothing to a lamp, and was introduced as the number that says what should happen when this color is drawn on top of another one. Nothing has been drawn on top of anything yet, so nothing has read it; every pixel of the scene carries alpha 255, and the PNG encoder noticed and dropped the channel from the file as pointless.

Four things follow from the rule: a program that generates a sprite sheet, a loader that reads it back, one drawing operation called Blit, and the arithmetic for the third case. The blend is worked out on paper before it is written down, because a blend that comes out one unit light is invisible to a person and obvious to a hash.

The 328-byte atlas

The obvious arrangement is one file per picture: walker-stand.png, walker-step.png, loaded by name. Nothing about it is wrong and almost no game does it. Count the files. A creature with four facings and four frames of walk is sixteen pictures; a second creature is sixteen more; props, tiles and portraits run into the hundreds. Each one is an open, a decode and an allocation, and each carries a PNG's fixed overhead of signature, header chunk, checksums and its own compression dictionary. The four small pictures here cost 601 bytes as four files and 328 bytes as one image.

So games pack. One image holds many pictures in a grid of cells, and a frame stops being a file and becomes a rectangle: column times cell size across, row times cell size down. The type for that exists already. Rect was built for clipping in chapter 13, it is half-open so cells tile without sharing a pixel, and it describes a region of a sheet as happily as a region of the screen. One decode, one allocation, one contiguous run of memory, and every frame inside it addressed by arithmetic.

Which leaves the awkward part: there is no art. Nobody has drawn a walker, the tools for drawing one are their own chapter, and a loader with nothing to load cannot be tested. So the first sheet is generated. The stand-in is a character mask, one character per pixel against a color key, with a space for a pixel that is not there. It is programmer art and it looks like programmer art. It is also the right stand-in, because the loader wants four bytes per pixel and a cell size, and holds no opinion about whether a person or a loop put them there.

▣ Build · stage 1: a sheet made out of characters
// cmd/mkatlas/main.go
package main

import (
	"fmt"
	"os"

	"theworld/internal/render"
)

// Cell is the size of every frame on the sheet.
const Cell = 16

// The stand-in palette. ' ' is the pixel that is not there.
var key = map[byte]render.Color{
	' ': 0x00000000, // nothing at all: alpha 0
	'o': 0xFF2B3524, // outline
	'b': 0xFF6E8F4A, // body
	'l': 0xFF9DBF6B, // belly
	'e': 0xFFF2EEDC, // eye
}

// shade is the shadow's color: a cool near-black, one quarter there.
const shade render.Color = 0x40101418

// stand is one walker, drawn as characters so the transparent pixels
// are the ones you can see through in the source too.
var stand = []string{
	"                ",
	"                ",
	"          oooo  ",
	"        oobbbbo ",
	"   oooooobbebbo ",
	"  obbbbbbbbbbbo ",
	" obllbbbbbbbbbo ",
	" obllllbbbbbbo  ",
	" obbllbbbbbbo   ",
	"  obbbbbbbbbo   ",
	"   bb    bb     ",
	"   bb    bb     ",
	"   oo    oo     ",
	"                ",
	"                ",
	"                ",
}

// step is the same walker mid-stride: two frames of an animation share
// everything except the rows that move.
var step = withLegs(stand, []string{
	"  bb       bb   ",
	"  bb       bb   ",
	"  oo       oo   ",
})

// withLegs copies a mask and replaces rows 10 through 12.
func withLegs(mask []string, legs []string) []string {
	out := append([]string(nil), mask...)
	copy(out[10:13], legs)
	return out
}
// cmd/mkatlas/main.go — the mask becomes pixels

// paint writes one character mask into the sheet at a cell origin.
func paint(sheet *render.Buffer, col int, mask []string) {
	if len(mask) != Cell {
		fmt.Fprintf(os.Stderr, "mkatlas: cell %d has %d rows, want %d\n", col, len(mask), Cell)
		os.Exit(1)
	}
	for y, row := range mask {
		if len(row) != Cell {
			fmt.Fprintf(os.Stderr, "mkatlas: cell %d row %d is %d characters, want %d\n", col, y, len(row), Cell)
			os.Exit(1)
		}
		for x := 0; x < Cell; x++ {
			c, ok := key[row[x]]
			if !ok {
				fmt.Fprintf(os.Stderr, "mkatlas: cell %d row %d: no color for %q\n", col, y, row[x])
				os.Exit(1)
			}
			sheet.Set(col*Cell+x, y, c)
		}
	}
}

// shadow draws a squat oval across the bottom of a cell, every pixel of
// it one quarter opaque.
func shadow(sheet *render.Buffer, col int) {
	for _, r := range []render.Rect{{X0: 4, Y0: 12, X1: 12, Y1: 13},
		{X0: 2, Y0: 13, X1: 14, Y1: 14},
		{X0: 4, Y0: 14, X1: 12, Y1: 15}} {
		for x := r.X0; x < r.X1; x++ {
			sheet.Set(col*Cell+x, r.Y0, shade)
		}
	}
}

// testCard fills a cell with sixteen columns of one color at sixteen
// alphas, 0 on the left and 255 on the right: an asset that exists to
// be measured.
func testCard(sheet *render.Buffer, col int) {
	for x := 0; x < Cell; x++ {
		a := uint8(x * 17)
		for y := 0; y < Cell; y++ {
			sheet.Set(col*Cell+x, y, render.RGBA(a, 0xE8, 0xD8, 0xA0))
		}
	}
}

func main() {
	sheet := render.NewBuffer(4*Cell, Cell)
	paint(sheet, 0, stand)
	paint(sheet, 1, step)
	shadow(sheet, 2)
	testCard(sheet, 3)

	const path = "assets/atlas.png"
	if err := sheet.SaveSheet(path); err != nil {
		fmt.Fprintln(os.Stderr, "mkatlas:", err)
		os.Exit(1)
	}
	fi, err := os.Stat(path)
	if err != nil {
		fmt.Fprintln(os.Stderr, "mkatlas:", err)
		os.Exit(1)
	}
	fmt.Printf("wrote %s: %dx%d, %d cells of %d, %d bytes\n",
		path, sheet.W, sheet.H, sheet.W/Cell, Cell, fi.Size())
	fmt.Printf("pixels sha256 %s\n", sheet.Hash())
	fmt.Print(render.AlphaMap(sheet, sheet.Bounds()))
}
$ go run ./cmd/mkatlas
wrote assets/atlas.png: 64x16, 4 cells of 16, 328 bytes
pixels sha256 32b9647db215d2efe2da7a7ee45ac09cda38bac87273bc043f9db185dc88f96b
.................................................01123344566778#
.................................................01123344566778#
..........####............####...................01123344566778#
........#######.........#######..................01123344566778#
...############....############..................01123344566778#
..#############...#############..................01123344566778#
.##############..##############..................01123344566778#
.#############...#############...................01123344566778#
.############....############....................01123344566778#
..###########.....###########....................01123344566778#
...##....##.......##.......##....................01123344566778#
...##....##.......##.......##....................01123344566778#
...##....##.......##.......##.......22222222.....01123344566778#
..................................222222222222...01123344566778#
....................................22222222.....01123344566778#
.................................................01123344566778#

That block of characters is AlphaMap, which prints one character per pixel from the alpha channel alone: . for a pixel that is not there, # for a solid one, and a digit for everything in between. It is the same debugging trick Preview plays with color, aimed at the byte this chapter is about, and it shows the sheet's whole design in sixteen lines. Two solid silhouettes with different legs. A shadow of 28 pixels, every one of them a 2, which is alpha 64 of a possible 255. A test card whose sixteen columns climb from nothing on the left to solid on the right, seventeen units of alpha at a time.

The test card is there to be measured. Art is judged by eye, which leaves a blend bug room to hide inside a picture nobody has seen before; sixteen known alphas of one color over a known background produce sixteen numbers you can predict on paper.

The run above went out through SaveSheet, which has not been written yet. It is a dozen lines, and one decision inside it is the reason the sheet does not leave the program through the SavePNG chapter 14 built.

▣ Build · stage 2: four bytes out, unscaled
// internal/render/sprite.go

// SaveSheet writes a buffer as an 8-bit RGBA PNG whose channels are
// straight, not scaled by alpha: the form a sprite sheet is authored
// and read in.
func (b *Buffer) SaveSheet(path string) error {
	m := image.NewNRGBA(image.Rect(0, 0, b.W, b.H))
	for y := range b.H {
		for x := range b.W {
			c := b.At(x, y)
			i := m.PixOffset(x, y)
			m.Pix[i], m.Pix[i+1], m.Pix[i+2], m.Pix[i+3] = c.R(), c.G(), c.B(), c.A()
		}
	}
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	if err := png.Encode(f, m); err != nil {
		f.Close()
		return fmt.Errorf("encode %s: %w", path, err)
	}
	return f.Close()
}
$ xxd -l 32 assets/atlas.png
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0040 0000 0010 0806 0000 00a6 e779  ...@...........y

SavePNG hands pixels to the encoder as color.RGBA, a type whose three color channels are defined to be scaled by alpha already. Every pixel of a frame is opaque, and for an opaque pixel the scaled and unscaled forms are the same three numbers, so the distinction has cost nothing so far. Half the cells on this sheet are not opaque. image.NRGBA is the standard library's name for the other convention, four bytes per pixel with the colors untouched, which is what the mask wrote and what the file has to carry.

The header says the file agrees. Width 00000040 is 64, height 00000010 is 16, then bit depth 8 and color type 6. The scene's PNG came out color type 2, three channels and no alpha, because the encoder inspected the pixels and found nothing to keep. This one is type 6, and that single byte is the file admitting it has holes in it.

◆ Note: what the loader promises whoever draws the art

The loader knows three things about assets/atlas.png: it is an 8-bit RGBA PNG, its cells are 16 pixels square, and columns are counted from the left. Any file meeting those terms drops into place, whatever produced it. The masks above are a placeholder for drawn art, and nothing downstream can tell the difference when a hand-drawn sheet follows the same terms.

The copy loop that skips

A sheet is a buffer plus one number, the cell size, and a frame is arithmetic on that number. The copy itself is two nested loops with a question asked once per pixel. Every run on this page is headless: the client can open a window, and it can also draw one frame, hash it and write a PNG without one, and that second path is the one a test and a container take.

▣ Build · stage 3: cells, and a blit that leaves gaps alone
// internal/render/sprite.go

// Sheet is one decoded image plus the size of the square cells its
// frames are cut on. Frames are rectangles into it, not files.
type Sheet struct {
	Pix  *Buffer
	Cell int
}

// Frame is the source rectangle of one cell, counted left to right and
// top to bottom, in the same half-open convention every other rectangle
// in this package uses.
func (s *Sheet) Frame(col, row int) Rect {
	return Rect{
		X0: col * s.Cell,
		Y0: row * s.Cell,
		X1: (col + 1) * s.Cell,
		Y1: (row + 1) * s.Cell,
	}
}

// Blit copies the source rectangle src of a sheet onto the buffer with
// its top-left corner at (dx,dy).
func (b *Buffer) Blit(s *Sheet, src Rect, dx, dy int) {
	// A frame that hangs off the sheet is cut to the sheet, and the
	// destination corner moves with it.
	cut := src.Intersect(s.Pix.Bounds())
	dx, dy = dx+(cut.X0-src.X0), dy+(cut.Y0-src.Y0)
	src = cut

	dst := Rect{dx, dy, dx + (src.X1 - src.X0), dy + (src.Y1 - src.Y0)}.Intersect(b.clip)
	if dst.Empty() {
		return
	}
	// Whatever the clip took off the destination comes off the source
	// too: this pair of offsets carries a destination pixel back to the
	// sheet pixel it came from.
	ox, oy := src.X0-dx, src.Y0-dy

	for y := dst.Y0; y < dst.Y1; y++ {
		for x := dst.X0; x < dst.X1; x++ {
			c := s.Pix.Pix[s.Pix.index(x+ox, y+oy)]
			if c.A() == 0 {
				continue // this pixel is not there
			}
			b.Pix[b.index(x, y)] = c
		}
	}
}
// cmd/worldc/main.go — load the sheet, count what is in one cell, draw
	pix, err := render.LoadPNG("assets/atlas.png")
	if err != nil {
		fmt.Fprintln(os.Stderr, "worldc:", err)
		os.Exit(1)
	}
	sheet := &render.Sheet{Pix: pix, Cell: 16}
	fmt.Printf("worldc %s sheet %dx%d, %d cells of %d, sha256 %s\n",
		version, sheet.Pix.W, sheet.Pix.H, sheet.Pix.W/sheet.Cell, sheet.Cell, sheet.Pix.Hash())

	// How much of one cell is actually there.
	stand := sheet.Frame(render.CellStand, 0)
	there := 0
	for y := stand.Y0; y < stand.Y1; y++ {
		for x := stand.X0; x < stand.X1; x++ {
			if sheet.Pix.At(x, y).A() != 0 {
				there++
			}
		}
	}
	fmt.Printf("cell %d: %d pixels, %d of them carry color\n", render.CellStand, 16*16, there)

	before := render.NewBuffer(render.SceneW, render.SceneH)
	render.Scene(before)

	b := render.NewBuffer(render.SceneW, render.SceneH)
	render.Scene(b)
	b.Blit(sheet, stand, 40, 58)
	b.Blit(sheet, sheet.Frame(render.CellStep, 0), 92, 66)
	b.Blit(sheet, stand, -6, 74) // half of it is off the west edge

	fmt.Printf("blitting three cells changed %d pixels of the frame\n", render.Diff(before, b).Count)
	fmt.Printf("frame %dx%d sha256 %s\n", b.W, b.H, b.Hash())
$ go run ./cmd/worldc -shot walkers.png
worldc 0.0.1 sheet 64x16, 4 cells of 16, sha256 befd50436a855f41cb45c7ff45d804b5f5e73956e650b1db3aa167e9a9f45e54
cell 0: 256 pixels, 98 of them carry color
blitting three cells changed 262 pixels of the frame
frame 160x96 sha256 560c9e0bad113c4410441a0fd25f2319c50414fcf0b0295d11eddf1ba6936857
wrote walkers.png

Open walkers.png. Two creatures stand on the ground, one of them at the north edge of the pond with water plainly visible between its front and back legs, and a third is walking in from the west with only its head and front legs on the frame. None of them sits in a box. The 158 transparent pixels of each cell did nothing, and doing nothing is the entire feature.

The count on the third line is the claim, and it is checkable. Two full cells at 98 colored pixels each is 196. The third blit starts at column −6, so its first six columns fall off the west edge and ten survive, and those ten columns hold 66 colored pixels. 98 + 98 + 66 = 262, exactly what Diff reports against the same scene drawn without sprites. No blitted pixel landed on ground that already happened to be its color.

Follow the clipped one through the code, because it is where blitting differs from filling. The destination rectangle is {-6 74 10 90}, and intersecting it with the clip gives {0 74 10 90}, six columns narrower on the left. The source has to lose the same six columns from the same side, and ox = src.X0 - dx is what does it: with src.X0 at 0 and dx at −6, ox is 6, so destination column 0 reads sheet column 6 and the walker's rear half is never read at all. That is chapter 13's rule applied to two rectangles at once. Clip the largest unit you can name in advance, then carry the same adjustment across to wherever the pixels are coming from.

Now point that loop at the other two cells and it produces nonsense twice over. Every pixel of the shadow oval carries alpha 64, and 64 is not 0, so all 28 of them get copied: a slab of near-black under the creature's feet where the ground should be showing through, darker. Worse, the copy puts alpha 64 into the framebuffer, which every operation so far has kept opaque, so the frame heading for the window now has holes nobody asked for. A copy loop knows two answers. This pixel needs a third.

Mixing two colors in whole numbers

∑ Interlude: one shadow pixel over one pixel of ground

Take the two pixels the client is about to combine. The ground is soil, 0xFF6B5A3E: red 107, green 90, blue 62. The shadow pixel above it is 0x40101418: alpha 64, red 16, green 20, blue 24. Alpha 64 out of 255 says the shadow is a little over a quarter present, so the answer should be mostly soil with some near-black mixed into it. "Mostly" is not a number. Here is the number.

Each channel is a weighted average of the two. The source's weight is a, the destination gets whatever is left of 255, and dividing the total by 255 brings the result back into one byte:

out = (src·a + dst·(255 − a)) / 255

channelsrc·adst·(255−a)sum÷ 255byte
red16·64 = 1024107·191 = 204372146184.1684
green20·64 = 128090·191 = 171901847072.4372
blue24·64 = 153662·191 = 118421337852.4652

84, 72 and 52 pack to 0xFF544834, and the alpha is 255 because the destination was opaque and a quarter of a shadow does not make it less so. Every product in that table exceeds a byte, which is chapter 12's lesson arriving on schedule: 107·191 is 20,437, and a channel-sized variable would have wrapped it into garbage long before the division. Integer division then throws the fraction away, so 84.16 becomes 84 and each channel lands at or a fraction below the exact answer.

The divisor is the part that gets fumbled. Dividing by 255 is awkward and dividing by 256 is a shift, so the tempting version is (src·a + dst·(255 − a)) >> 8. Test it where the answer is not a matter of taste. At alpha 255 the mix has to return the source untouched, because the sprite is solid there: 232·255 divided by 255 is 232, and the shifted form gives 231. At alpha 0 it has to return the destination untouched: 107 stays 107 with the division and becomes 106 with the shift. There is a third property the shift also fails, and it is the one to remember. Mixing a color with itself must return that color, at every alpha, since (v·a + v·(255 − a))/255 is v·255/255. Divide by 256 instead and any flat area loses a unit under every sprite drawn over it.

srcone channel of the sheet pixel, 0 to 255: the color the artist chose
dstthe same channel of the framebuffer pixel already there
athe source pixel's alpha, 0 to 255: how much of it is present
255 − ahow much of the destination survives, so the two weights always total 255
·multiplication, written out to keep it separate from the x of a coordinate
▣ Build · stage 4: the third case, in code
// internal/render/sprite.go

// Over mixes one source pixel into one destination pixel, source-over:
// each channel is (src*a + dst*(255-a)) / 255. The destination is a
// framebuffer pixel, always opaque, so the answer is opaque too.
func Over(src, dst Color) Color {
	a := int(src.A())
	inv := 255 - a
	r := (int(src.R())*a + int(dst.R())*inv) / 255
	g := (int(src.G())*a + int(dst.G())*inv) / 255
	b := (int(src.B())*a + int(dst.B())*inv) / 255
	return RGBA(255, uint8(r), uint8(g), uint8(b))
}
// internal/render/sprite.go — Blit's inner loop, with the case it was missing
			c := s.Pix.Pix[s.Pix.index(x+ox, y+oy)]
			switch c.A() {
			case 0: // not there: leave the framebuffer alone
			case 255: // solid: overwrite
				b.Pix[b.index(x, y)] = c
			default: // partly there: mix
				i := b.index(x, y)
				b.Pix[i] = Over(c, b.Pix[i])
			}
// cmd/worldc/main.go — print the hand calculation, then draw everything
	shadowPx := sheet.Pix.At(render.CellShade*16+8, 13)
	cardPx := sheet.Pix.At(render.CellCard*16+8, 4)
	soil, sky := b.At(98, 79), b.At(128, 12)
	fmt.Printf("shadow %08X over soil %08X -> %08X\n",
		uint32(shadowPx), uint32(soil), uint32(render.Over(shadowPx, soil)))
	fmt.Printf("card   %08X over sky  %08X -> %08X\n",
		uint32(cardPx), uint32(sky), uint32(render.Over(cardPx, sky)))

	render.SpriteScene(b, sheet)
	fmt.Printf("frame %dx%d sha256 %s\n", b.W, b.H, b.Hash())
	fmt.Printf("the frame holds %08X at (98,79) and %08X at (128,12)\n",
		uint32(b.At(98, 79)), uint32(b.At(128, 12)))
// internal/render/scene.go — the standing composition, with sprites on it

// SpriteScene is Scene with the sheet drawn over it: two creatures with
// shadows, a third walking off the west edge, and the test card in the
// sky, in that order.
func SpriteScene(b *Buffer, s *Sheet) {
	Scene(b)

	stand, step := s.Frame(CellStand, 0), s.Frame(CellStep, 0)
	shade, card := s.Frame(CellShade, 0), s.Frame(CellCard, 0)

	b.Blit(s, shade, 40, 58)
	b.Blit(s, stand, 40, 58)

	b.Blit(s, shade, 92, 66)
	b.Blit(s, step, 92, 66)

	b.Blit(s, stand, -6, 74) // half of it is off the west edge
	b.Blit(s, card, 120, 8)
}
$ go run ./cmd/worldc -shot dark.png
worldc 0.0.1 sheet 64x16, 4 cells of 16, sha256 befd50436a855f41cb45c7ff45d804b5f5e73956e650b1db3aa167e9a9f45e54
shadow 40040506 over soil FF6B5A3E -> FF51442F
card   887C7355 over sky  FF6E8CA0 -> FF757E78
frame 160x96 sha256 119f1222fdc2003de1c564185cfecc9a2cfa95a992ad5bc2a4801e92f9d941e7
the frame holds FF51442F at (98,79) and FF757E78 at (128,12)
wrote dark.png

The hand calculation said 0xFF544834 and the program says 0xFF51442F. Open the file and the test card, which should climb evenly from sky blue to pale, sags through a muddy grey in its middle columns before snapping to the right color in its last one. Something is wrong, and the interlude was checked twice.

⚠ Worked failure: the sheet was lying about its own colors

The client's own output names the culprit on its second line, ahead of any picture. mkatlas wrote that shadow pixel as 0x40101418, and the sheet sitting in memory says 0x40040506. Alpha came through and the three colors did not: 16 became 4, 20 became 5, 24 became 6. Each one is its original multiplied by 64 and divided by 255, which is to say multiplied by its own alpha. The test card's pixel tells the same story at a louder volume: 0x88E8D8A0 arrived as 0x887C7355, 232 scaled down to 124 by an alpha of 136.

The culprit is the loader, and it was the reasonable choice. LoadPNG reads a pixel with img.At(x, y).RGBA(), and that method is documented to return alpha-premultiplied values: colors already scaled by their alpha. Chapter 14 said as much and added that it holds as long as the buffer stays opaque. Every buffer was opaque then. The sheet is the first thing in this program that is not, so the scaling has been happening since the first line of stage 3 and nothing could tell: the walkers are solid, and scaling a solid pixel by 255/255 changes nothing.

Then Over multiplies by alpha a second time. Follow the shadow's red through both hits: 16 becomes 4 in the loader, and the mix computes (4·64 + 107·191)/255 = 81 where the honest answer is 84. On a dark shadow that is three units, invisible. On the test card at alpha 136 the same double scaling turns 175 into 117, and 58 units is a picture anyone can see is wrong. Two multiplications where the arithmetic wanted one, and the size of the error tracks how bright and how translucent the source is.

▣ Build · stage 5: a loader that takes the bytes as written
// internal/render/sprite.go

// LoadSheet reads a sprite sheet, taking its four bytes per pixel
// exactly as they were authored.
func LoadSheet(path string, cell int) (*Sheet, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	img, err := png.Decode(f)
	if err != nil {
		return nil, fmt.Errorf("decode %s: %w", path, err)
	}
	n, ok := img.(*image.NRGBA)
	if !ok {
		return nil, fmt.Errorf("%s decoded as %T, want *image.NRGBA: save the sheet as 8-bit RGBA", path, img)
	}
	b := NewBuffer(n.Rect.Dx(), n.Rect.Dy())
	for y := range b.H {
		for x := range b.W {
			i := n.PixOffset(n.Rect.Min.X+x, n.Rect.Min.Y+y)
			b.Pix[b.index(x, y)] = RGBA(n.Pix[i+3], n.Pix[i], n.Pix[i+1], n.Pix[i+2])
		}
	}
	if b.W%cell != 0 || b.H%cell != 0 {
		return nil, fmt.Errorf("%s is %dx%d, which does not divide into %d-pixel cells", path, b.W, b.H, cell)
	}
	return &Sheet{Pix: b, Cell: cell}, nil
}
// cmd/worldc/main.go — one line different
	sheet, err := render.LoadSheet("assets/atlas.png", 16)
	if err != nil {
		fmt.Fprintln(os.Stderr, "worldc:", err)
		os.Exit(1)
	}
$ go run ./cmd/worldc -shot frame.png
worldc 0.0.1 sheet 64x16, 4 cells of 16, sha256 32b9647db215d2efe2da7a7ee45ac09cda38bac87273bc043f9db185dc88f96b
shadow 40101418 over soil FF6B5A3E -> FF544834
card   88E8D8A0 over sky  FF6E8CA0 -> FFAFB4A0
frame 160x96 sha256 eb3c5619ec6474601ee8d8d80c22af448a17b6f6f87e712846d1afaefc236a83
the frame holds FF544834 at (98,79) and FFAFB4A0 at (128,12)
wrote frame.png

0xFF544834, the three numbers the table produced, and the last line confirms that the pixel sitting in the framebuffer at (98,79) is the one Over promised. The sheet's own hash changed too, from befd5043… to 32b9647d…, and the second of those is the number mkatlas printed when it wrote the file. Two programs now agree about what is in that PNG.

The card line is the more interesting proof. Blue was 160 in the sheet and 160 in the sky, and it comes back 160: mixing a color with itself returned that color, which is the property the shift would have broken. Red climbed from 110 to 175 and green from 140 to 180, both a little over half way to the card's own color, because alpha 136 out of 255 is a little over half. Open frame.png and the card is a clean ramp from sky to pale, with the ridge line behind it faintly visible through its middle columns.

dark.png is still on disk, so measure how far the bug reached, using the comparison the client grew in chapter 14:

$ go run ./cmd/worldc -diff frame.png dark.png
frame.png sha256 eb3c5619ec6474601ee8d8d80c22af448a17b6f6f87e712846d1afaefc236a83
dark.png sha256 119f1222fdc2003de1c564185cfecc9a2cfa95a992ad5bc2a4801e92f9d941e7
276 of 15360 pixels differ, first at (121,8): ff7691a0 became ff678396

276 pixels of 15,360, and their addresses finish the story. 224 of them are in the test card: fourteen of its sixteen columns, all but the two ends. 25 and 27 more are the two shadows, a little short of 28 each because the creatures' feet were drawn over them afterwards. Outside those three rectangles nothing differs at all. Both creatures are identical in both files, the card's alpha-0 column is identical, and so is its alpha-255 column. The bug could only ever touch a pixel that went through the default branch, which is exactly the set of pixels whose color the loader had scaled.

What Blit does with one source pixel, by its alpha Three rows. Each row runs left to right: a sheet pixel written as packed hexadecimal with its alpha underneath, an arrow to what Blit does with it, and an arrow to the resulting framebuffer pixel. The first row, alpha 0, is skipped and the framebuffer is unchanged. The second row, alpha 64, is mixed by the weighted average and produces a new color. The third row, alpha 255, is copied whole. ONE SOURCE PIXEL, THREE DECISIONS SHEET WHAT BLIT DOES FRAMEBUFFER 00000000 alpha 0 skipped nothing is written FF6B5A3E unchanged 40101418 alpha 64 (16·64 + 107·191) / 255 mixed, one channel at a time FF544834 red lands on 84 FF6E8F4A alpha 255 copied whole no arithmetic at all FF6E8F4A the sheet's color

Figure 16.1: the instruction travels with the pixel it applies to, so the loop needs no list of which parts of a sprite are solid.

Alpha 0, alpha 255 and flat color

The blend is arithmetic, so it can be asserted directly instead of through a picture. The test pins three claims: alpha 0 changes nothing, alpha 255 delivers the source exactly, and a color mixed with itself comes back unaltered at every alpha. The third is 256 assertions and costs nothing to run.

▣ Build · stage 6: the tests, and what they catch
// internal/render/sprite_test.go

// sheetPath is the stand-in atlas, written by cmd/mkatlas.
var sheetPath = filepath.Join("..", "..", "assets", "atlas.png")

// frameHash is the SHA-256 of the pixels SpriteScene draws over Scene.
const frameHash = "eb3c5619ec6474601ee8d8d80c22af448a17b6f6f87e712846d1afaefc236a83"

// TestOverKeepsItsEnds pins the three properties the mix must have for a
// sprite to look like the art it was drawn from.
func TestOverKeepsItsEnds(t *testing.T) {
	dst := Soil
	src := RGBA(255, 0xE8, 0xD8, 0xA0)

	if got := Over(RGBA(0, 0xE8, 0xD8, 0xA0), dst); got != dst {
		t.Errorf("alpha 0 moved the destination: %08X, want %08X", uint32(got), uint32(dst))
	}
	if got := Over(src, dst); got != src {
		t.Errorf("alpha 255 did not deliver the source: %08X, want %08X", uint32(got), uint32(src))
	}
	// Mixing a color with itself has to return that color at every alpha,
	// or flat areas crawl darker every time something is drawn over them.
	flat := RGBA(255, 90, 90, 90)
	for a := 0; a <= 255; a++ {
		if got := Over(RGBA(uint8(a), 90, 90, 90), flat); got != flat {
			t.Fatalf("alpha %d mixed %08X with itself and got %08X", a, uint32(flat), uint32(got))
		}
	}
}

// TestSheetIsStraight checks the sheet arrives with the bytes mkatlas
// wrote, not with its colors already scaled by alpha.
func TestSheetIsStraight(t *testing.T) {
	s, err := LoadSheet(sheetPath, 16)
	if err != nil {
		t.Fatal(err)
	}
	if got, want := s.Pix.At(CellShade*16+8, 13), Color(0x40101418); got != want {
		t.Errorf("shadow pixel is %08X, want %08X: the loader scaled it", uint32(got), uint32(want))
	}
	if got, want := s.Pix.At(CellCard*16+8, 4), Color(0x88E8D8A0); got != want {
		t.Errorf("card pixel is %08X, want %08X: the loader scaled it", uint32(got), uint32(want))
	}
}

func TestSpriteSceneMatchesItsHash(t *testing.T) {
	s, err := LoadSheet(sheetPath, 16)
	if err != nil {
		t.Fatal(err)
	}
	b := NewBuffer(SceneW, SceneH)
	SpriteScene(b, s)
	got := b.Hash()
	if got == frameHash {
		return
	}
	gotPath := filepath.Join("testdata", "sprites.got.png")
	if err := b.SavePNG(gotPath); err != nil {
		t.Fatal(err)
	}
	t.Fatalf("pixels hash %s\n          want %s\n    wrote %s", got, frameHash, gotPath)
}
$ go test -count=1 -v ./internal/render/
=== RUN   TestOverKeepsItsEnds
--- PASS: TestOverKeepsItsEnds (0.00s)
=== RUN   TestSheetIsStraight
--- PASS: TestSheetIsStraight (0.00s)
=== RUN   TestSpriteSceneMatchesItsHash
--- PASS: TestSpriteSceneMatchesItsHash (0.00s)
PASS
ok  	theworld/internal/render	0.003s

Now change the three divisions in Over to >> 8 and run the first test alone, to see whether an argument made on paper is one a machine can hold you to:

$ go test -count=1 -run TestOverKeepsItsEnds ./internal/render/
--- FAIL: TestOverKeepsItsEnds (0.00s)
    sprite_test.go:21: alpha 0 moved the destination: FF6A593D, want FF6B5A3E
    sprite_test.go:24: alpha 255 did not deliver the source: FFE7D79F, want FFE8D8A0
    sprite_test.go:31: alpha 0 mixed FF5A5A5A with itself and got FF595959
FAIL
FAIL	theworld/internal/render	0.002s
FAIL

Every channel of every line is exactly one short. That is a change no screenshot would ever have surfaced, and it would have quietly put a one-unit seam between every sprite and the flat color beside it.

The alpha byte on each pixel

Blit contains no knowledge of creatures. It does not consult a list of which parts of a frame are solid, it holds no mask, and it has never been told where the legs are. Each pixel arrives carrying the instruction for its own handling, and the loop is a three-way branch on that byte. Add a cell with soft edges, a translucent pane, a puff of dust: the same loop draws all of them, because the new art arrives with its own alphas and the loop asks each pixel the same question. Data steering a loop that stays fixed is the cheapest kind of extensibility there is, and it is why the alpha byte was packed into every color four chapters ago instead of being bolted on here.

The clipping repeats chapter 13's rule with one addition. There, the check was made against the largest unit the operation could name in advance: once for a whole rectangle, once per pixel only when the extent was discovered as the drawing went. A blit knows its rectangle up front, so the intersection happens once. What is new is that two rectangles have to stay in step, and the offsets ox, oy keep them there. Every pixel the clip took off the destination is a pixel never read from the source, and the ones that survive read exactly the sheet cells they would have read unclipped: a sprite half off the screen shows the same half it would have shown with the window a little wider.

The failure generalizes furthest. A PNG's four bytes per pixel are unscaled by definition, and the instant they are decoded nothing carries that fact any further: not the type Color, not color.Color, not a debugger. Both conventions are four bytes, both look correct under inspection, and both come out of the standard library under names one keystroke apart. Which one a number obeys is an agreement between the code that wrote it and the code that reads it, exactly like the alpha-red-green-blue byte order chapter 12 settled on, and agreements at seams are where the ugly bugs live. When two pieces of code both do defensible arithmetic and the picture still comes out wrong, stop reading the arithmetic and start asking what each side believes the numbers already mean.

One last thing the run confirmed: the double scaling had been in the program for two stages and drew perfect frames the whole time, because the only sprites on the sheet were solid. Bugs that need a specific input to show themselves are patient. The test card exists to be that input on purpose, and it is why the sheet ships with a cell that is not art at all.

Checkpoint

✓ Checkpoint: what you can now do
  • Name the three cases Blit branches on, and say what each one writes to the framebuffer.
  • Given a sheet pixel and the pixel under it, compute the mix on paper, channel by channel, and predict the eight hex digits the client will print.
  • Argue why the divisor is 255 and name the three properties that break at 256, including the one that only shows up when a color is mixed with itself.
  • Given a blit clipped at an edge, work out ox and say which sheet column a given destination column read.
  • Tell a premultiplied color from a straight one by comparing its channels against its alpha, and say which standard library types speak which convention.
  • Regenerate the sheet, hash it, and assert a whole composed frame against a constant that a person put in the source on purpose.
⚡ Exercises: try first, then reveal
Exercise 1: one sheet or four files. Write a short program that cuts each of the four cells into its own 16-by-16 PNG through SaveSheet. Add up the four file sizes and compare them against the sheet's.

195, 205, 107 and 94 bytes, which is 601 against the sheet's 328. The pixels are identical, so the whole difference is per-file overhead: four signatures, four header chunks, four sets of checksummed chunk frames, and four compression runs that each start from nothing instead of sharing one dictionary. Four frames is the smallest case anyone would bother with and the sheet already wins by 45 percent. Then count the startup work: one open, one decode and one allocation against four of each.

Exercise 2: draw it in the wrong order. In SpriteScene, swap the two calls at (92,66) so the walker is blitted before its shadow. Predict how many pixels change, then write the result to swapped.png and compare it against the frame you already have.

One.

$ go run ./cmd/worldc -diff frame.png swapped.png
frame.png sha256 eb3c5619ec6474601ee8d8d80c22af448a17b6f6f87e712846d1afaefc236a83
swapped.png sha256 45c00022ef54b07b5f683321863871d02d416db1baf41687aea92d39aa5b88c9
1 of 15360 pixels differ, first at (103,78): ff2b3524 became ff242c20

The shadow oval is three rows tall and the walker's feet reach into the top row of it at exactly one column, so a single outline pixel gets the shadow mixed over it and comes out darker. The count is not the lesson. Painter's order is part of the drawing, whatever is blitted last wins the pixels it shares, and here the overlap happened to be one pixel wide. Give the creature a wider stance, or the shadow a larger oval, and the same one-line swap lays a haze across the whole animal.

Exercise 3: a heavier shadow. Change shade in mkatlas to 0x80101418, alpha 128. Before regenerating, work out by hand what the pixel at (98,79) becomes.

Alpha 128 leaves 127 for the ground. Red is (16·128 + 107·127)/255 = 15637/255 = 61; green is (2560 + 11430)/255 = 54; blue is (3072 + 7874)/255 = 42. So 0xFF3D362A. Rebuild the sheet, then run the client:

$ go run ./cmd/worldc
worldc 0.0.1 sheet 64x16, 4 cells of 16, sha256 73bba5f3bd0d2671c1a7fd29ca675c46b51bb8f253cf006a518f40f2ed87ff88
shadow 80101418 over soil FF6B5A3E -> FF3D362A
card   88E8D8A0 over sky  FF6E8CA0 -> FFAFB4A0
frame 160x96 sha256 584d21049dbd1a5a7bbe276ce1ee115a11efe87f420e007d698d2c7caf864fda
the frame holds FF3D362A at (98,79) and FFAFB4A0 at (128,12)

The card is untouched, since only the shadow's alpha changed, and the frame hash moved because one asset did. TestSpriteSceneMatchesItsHash fails now and is right to: the composed picture is not the one the constant was written against. Put the alpha back to 64, or keep the heavier shadow, look at the picture, and change the constant on purpose.