Rectangles First, Then Lines
Fill line and clip
Ground tiles are small rectangles, the tick panel is a filled rectangle, and a cursor box is four straight runs of pixels. The drawing rule is the wall around all of them: every buffer holds a clip rectangle; no drawing operation writes a pixel outside it, and the check is made as early as the operation can make it: once for a whole rectangle, once per pixel for a line.
There is no third kind of drawing hiding in that list. Fill a rectangle, draw a line between two points, and the volume is those two calls with better arguments.
Chapter 12 left both of them one step away and one promise short. The loops that painted
the rim and the pond said Set once per pixel, and Set quietly
drops any coordinate that is not on the buffer.
The note attached to that decision made a commitment: the bounds check does not
disappear, it moves up into the operations that draw, where the question can be asked once
for a whole rectangle instead of once per pixel. Collecting that promise means writing
drawing code that skips Set entirely.
The buffer's own edge is not enough. A minimap sits in a corner, a dialogue panel covers the bottom third, and a viewport shows the valley in the space left over; each needs drawing to stop at a border nowhere near the edge of the buffer.
In only ever knew one rectangle, the one with the buffer's own width and
height. The wall stops being a hard-coded comparison and becomes a piece of data the
buffer carries.
The {-4 7 4 15} rectangle
Start with the convention, because every rectangle bug in graphics starts with the
other one. A rectangle here is half-open: it holds every pixel from
X0 up to but not including X1, and from Y0 up
to but not including Y1. So {3 2 9 6} covers columns 3
through 8 and rows 2 through 5, six pixels wide and four tall. Width becomes a plain
subtraction with no stray +1 to forget, two rectangles side by side share an edge
number without sharing a pixel, so tiles tile, and a rectangle holding nothing is
one where X1 == X0.
// internal/render/draw.go
package render
// Rect is a half-open rectangle of pixels: it holds every pixel with
// X0 <= x < X1 and Y0 <= y < Y1, so its width is X1-X0 and its height
// is Y1-Y0.
type Rect struct {
X0, Y0, X1, Y1 int
}
// Empty reports whether the rectangle holds no pixels at all.
func (r Rect) Empty() bool { return r.X1 <= r.X0 || r.Y1 <= r.Y0 }
// Contains reports whether one pixel lies inside the rectangle.
func (r Rect) Contains(x, y int) bool {
return x >= r.X0 && x < r.X1 && y >= r.Y0 && y < r.Y1
}
// Intersect returns the pixels the two rectangles share, which is
// empty if they overlap nowhere.
func (r Rect) Intersect(s Rect) Rect {
if s.X0 > r.X0 {
r.X0 = s.X0
}
if s.Y0 > r.Y0 {
r.Y0 = s.Y0
}
if s.X1 < r.X1 {
r.X1 = s.X1
}
if s.Y1 < r.Y1 {
r.Y1 = s.Y1
}
if r.X1 < r.X0 {
r.X1 = r.X0
}
if r.Y1 < r.Y0 {
r.Y1 = r.Y0
}
return r
}
// internal/render/buffer.go — the buffer carries its wall
type Buffer struct {
W, H int
Pix []Color
clip Rect
}
// NewBuffer allocates a w-by-h buffer in a single allocation. Every
// pixel starts at the zero value, Clear, and drawing may write
// anywhere on it until somebody narrows the clip rectangle.
func NewBuffer(w, h int) *Buffer {
b := &Buffer{W: w, H: h, Pix: make([]Color, w*h)}
b.clip = b.Bounds()
return b
}
// Set writes one pixel, if the clip rectangle allows it.
func (b *Buffer) Set(x, y int, c Color) {
if !b.clip.Contains(x, y) {
return
}
b.Pix[b.index(x, y)] = c
}
// internal/render/draw.go — the wall's controls, and the first user of it
// Bounds is every pixel the buffer owns: the widest the clip can be.
func (b *Buffer) Bounds() Rect { return Rect{0, 0, b.W, b.H} }
// Clip is the rectangle drawing is currently allowed to write inside.
func (b *Buffer) Clip() Rect { return b.clip }
// SetClip narrows the writable area, and can only ever narrow it: the
// requested rectangle is intersected with the buffer's own bounds.
func (b *Buffer) SetClip(r Rect) { b.clip = r.Intersect(b.Bounds()) }
// FillRect paints every pixel of r that the clip rectangle allows.
func (b *Buffer) FillRect(r Rect, c Color) {
r = r.Intersect(b.clip)
if r.Empty() {
return
}
for y := r.Y0; y < r.Y1; y++ {
row := b.Pix[b.index(r.X0, y):b.index(r.X1, y)]
for i := range row {
row[i] = c
}
}
}
// cmd/worldc/main.go — three rectangles: one inside, one hanging off, one nowhere near
b := render.NewBuffer(24, 12)
b.Fill(render.Soil)
pond := render.Rect{X0: 3, Y0: 2, X1: 9, Y1: 6}
scree := render.Rect{X0: -4, Y0: 7, X1: 4, Y1: 15}
fmt.Println("clip: ", b.Clip())
fmt.Println("pond: ", pond, "->", pond.Intersect(b.Clip()))
fmt.Println("scree: ", scree, "->", scree.Intersect(b.Clip()))
b.FillRect(pond, render.Water)
b.FillRect(scree, render.Rock)
b.FillRect(render.Rect{X0: 30, Y0: 2, X1: 40, Y1: 4}, render.Rock)
fmt.Print(b.Preview())
$ go run ./cmd/worldc
clip: {0 0 24 12}
pond: {3 2 9 6} -> {3 2 9 6}
scree: {-4 7 4 15} -> {0 7 4 12}
........................
........................
...~~~~~~...............
...~~~~~~...............
...~~~~~~...............
...~~~~~~...............
........................
####....................
####....................
####....................
####....................
####....................
The scree is the one to study. It was asked for at {-4 7 4 15}, four
columns west of the buffer and three rows below it, and Intersect
turned it into {0 7 4 12} before a single pixel moved: four
comparisons, one per side, each keeping whichever edge is further in. The clamps
underneath handle the case where that leaves nothing, which is what the third call
hit. {30 2 40 4} against a 24-wide buffer comes back as
{30 2 30 4}, zero pixels wide, and Empty sends
FillRect home before the loop starts. Nothing drawn, nothing said,
because a rectangle entirely off the buffer is routine: in a scrolling map most of
the world is off-screen every frame.
Look at what the fill loop does not do: it never calls Set.
It takes a row slice, b.Pix[b.index(r.X0, y):b.index(r.X1, y)], and
writes straight through it. Both ends of that slice are checked once, when the
slice is made, and the for i := range row that follows writes through
an index the compiler can prove is inside it. That is chapter 12's promise
collected: 24 pixels of pond cost one intersection and four bounds checks instead
of 24 coordinate tests, and the saving grows with the rectangle. A row is also
contiguous in memory, so the fill walks forward instead of hopping.
Two smaller decisions carry weight across the drawing package. Set now asks
clip instead of In, so every per-pixel write inherits the
movable wall, and because NewBuffer starts the clip at
Bounds(), nothing chapter 12 wrote behaves differently today.
At keeps asking In: reading a pixel outside the clip is
sensible, and the clip governs writing only.
Seven columns and three rows
Now the harder half. Draw a stream from {2 2} to {9 5}:
seven columns east, three rows south. Every column the true line drops three sevenths
of a row, and three sevenths of a pixel does not exist. Some columns have to take a
whole row's drop and others none, and the question is which, answered the same way
every time on every machine. Before any code, walk it with a pencil.
Call the run dx = 7 and the rise dy = 3. Instead of tracking the true line's height as a fraction, track how far it has risen above the row being drawn on, measured in sevenths of a row and stored as a whole number of them. Call that number e and start it at 0, with the pen on row 2.
Each column east, the line rises dy sevenths, so add 3 to e. Once the accumulated rise passes half a row, the nearest pixel is the next row down: step down, and take a whole row, dx sevenths, back out of e. Half a row is dx/2, and halves are exactly what integer arithmetic hates, so double both sides of the question. Instead of asking whether e has reached dx/2, ask whether 2e has reached dx. Same question, no fractions, no division anywhere.
| x | e | 2e | 2e ≥ 7? | y |
|---|---|---|---|---|
| 2 | 0 | 0 | no | 2 |
| 3 | 3 | 6 | no | 2 |
| 4 | 6 | 12 | yes | 3 |
| 5 | 2 | 4 | no | 3 |
| 6 | 5 | 10 | yes | 4 |
| 7 | 1 | 2 | no | 4 |
| 8 | 4 | 8 | yes | 5 |
| 9 | 0 | 0 | no | 5 |
Read the row for x = 4. e was 3, adding dy makes it
6, and 12 has reached 7, so the pen drops to row 3 and e becomes
6 − 7 = −1. Negative is not a mistake, it is the point: after the step the
true line sits one seventh of a row above the pixel row, so e is a
signed distance from the row being drawn on, never more than half a row either way.
The last table row holds the property that makes this exact. Over seven columns the total added is 7·3 = 21 sevenths and the total taken back out is 3·7 = 21, so e returns to 0 and y arrives at 5, the endpoint asked for. It could not come out otherwise: e is pinned within half a row of zero, so after dx columns the number of rows stepped can only be dy. A line drawn this way cannot miss its own endpoint.
dxdye2e ≥ dx// cmd/worldc/main.go — a throwaway: lines that only run east and south
// lineShallow draws a line running left to right whose rise is no
// greater than its run, keeping the error as a whole number.
func lineShallow(b *render.Buffer, x0, y0, x1, y1 int, c render.Color) {
dx, dy := x1-x0, y1-y0
e, y := 0, y0
for x := x0; x <= x1; x++ {
b.Set(x, y, c)
e += dy
if 2*e >= dx {
y++
e -= dx
}
}
}
$ go run ./cmd/worldc (the walk printed, then the pixels it chose) x e 2e 2e >= 7 y 2 0 0 false 2 3 3 6 false 2 4 6 12 true 3 5 2 4 false 3 6 5 10 true 4 7 1 2 false 4 8 4 8 true 5 9 0 0 false 5 ........................ ........................ ..~~.................... ....~~.................. ......~~................ ........~~.............. ........................ ........................ ........................ ........................ ........................ ........................
Eight columns, eight pixels, and the staircase steps down exactly where the pencil said it would. Per pixel the method costs one addition, one comparison and occasionally one subtraction, and the division that would have produced 0.428571… never happens: it was replaced by the decision to count in sevenths. That trick is much older than graphics and turns up wherever a rate must be tracked without drift, from audio resampling to scheduling. Multiply the units up until the fractions disappear, then stay in whole numbers.
Which invites the obvious objection. Computers have floating-point hardware, and a
loop that adds 3/7 to a float64 is shorter than the one above. Write
that version, point it at two lines, and look at what comes out.
// cmd/worldc/main.go — add the slope, cut the result down to a pixel
func lineFloat(b *render.Buffer, x0, y0, x1, y1 int, c render.Color) {
slope := float64(y1-y0) / float64(x1-x0)
y := float64(y0)
for x := x0; x <= x1; x++ {
b.Set(x, int(y), c)
y += slope
}
}
func main() {
b := render.NewBuffer(24, 12)
b.Fill(render.Soil)
lineFloat(b, 2, 2, 9, 5, render.Water)
lineFloat(b, 2, 8, 12, 9, render.Water)
fmt.Print(b.Preview())
slope := 1.0 / 10.0
y := 8.0
for x := 2; x < 12; x++ {
y += slope
}
fmt.Printf("asked for (12,9)\n")
fmt.Printf("y ends at %.17f\n", y)
fmt.Printf("as a pixel %d\n", int(y))
fmt.Printf("exact? %v\n", y == 9.0)
}
$ go run ./cmd/worldc
........................
........................
..~~~...................
.....~~.................
.......~~...............
.........~..............
........................
........................
..~~~~~~~~~~~...........
........................
........................
........................
asked for (12,9)
y ends at 8.99999999999999645
as a pixel 8
exact? false
Two failures, one of them fatal. The first stream sags: its runs are three pixels,
then two, two, one, where the integer walk laid down an even two, two, two, two.
That is int(y) cutting the fraction off, which discards up to a whole
pixel of rise every column and always in the same direction, so the line rides low
and its final column arrives alone.
The second stream never arrives at all. It was told to run from {2 8}
to {12 9}, a slope of exactly one tenth, and the preview shows it flat
along row 8 with row 9 untouched. The printed arithmetic says why: one tenth has no
exact form in binary, ten additions of it land on 8.99999999999999645 instead of 9,
and cutting the fraction off reads that as row 8. The line missed its own endpoint
by a pixel. Rounding to nearest would rescue this particular line, at the price of
an extra operation per pixel and a patch laid over a method that is approximate by
construction. The integer walk needs no rescue: its error is a whole number that
provably returns to zero.
lineShallow only knows one direction, though. Real calls will run west,
north, and straight up, and eight copies of the loop is not an answer. The trick is
to carry one error term for both axes and let each axis decide separately whether
this iteration is its turn to step.
// internal/render/draw.go
// Line draws the pixels nearest the straight line from (x0,y0) to
// (x1,y1), in integers only.
func (b *Buffer) Line(x0, y0, x1, y1 int, c Color) {
dx, sx := x1-x0, 1
if dx < 0 {
dx, sx = -dx, -1
}
dy, sy := y1-y0, 1
if dy < 0 {
dy, sy = -dy, -1
}
dy = -dy
err := dx + dy
for {
b.Pix[b.index(x0, y0)] = c // straight into the slice, like FillRect
if x0 == x1 && y0 == y1 {
return
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
// cmd/worldc/main.go — the hand-walked line, then a fan of twelve
b.Line(2, 2, 9, 5, render.Water)
fmt.Print(b.Preview())
f := render.NewBuffer(24, 12)
f.Fill(render.Soil)
cx, cy := 11, 5
for _, d := range [][2]int{{9, 0}, {9, 4}, {4, 5}, {0, 5}, {-4, 5}, {-9, 4},
{-9, 0}, {-9, -4}, {-4, -5}, {0, -5}, {4, -5}, {9, -4}} {
f.Line(cx, cy, cx+d[0], cy+d[1], render.Water)
}
fmt.Print(f.Preview())
$ go run ./cmd/worldc
........................
........................
..~~....................
....~~..................
......~~................
........~~..............
........................
........................
........................
........................
........................
........................
.......~...~...~........
..~~....~..~..~....~~...
....~~...~.~.~...~~.....
......~~.~.~.~.~~.......
........~~~~~~~.........
..~~~~~~~~~~~~~~~~~~~...
........~~~~~~~.........
......~~.~.~.~.~~.......
....~~...~.~.~...~~.....
..~~....~..~..~....~~...
.......~...~...~........
........................
Check the first preview first: those are the same eight pixels the pencil chose and
lineShallow drew, produced by a loop that never heard of octants. The
fan is the general case, twelve spokes shallow and steep across all four quadrants,
every one of them ending on the pixel it was aimed at.
The signs come off at the top: dx and dy are made
positive with their directions kept in sx and sy, so the
arithmetic is about magnitudes and the direction of travel is a separate fact.
Negating dy afterwards is what lets one error term serve both axes.
err carries the x-error and the y-error against each other, and
e2, its double, plays the part 2*e played in the
interlude, asking each axis in turn whether the true line has moved far enough to
justify a step. On a shallow line the x test passes every iteration and the y test
occasionally; on a steep line it is the other way round; at 45 degrees both pass
every time and the walk goes diagonally. The loop stops on the coordinate test and
not on a counter, so both endpoints are drawn exactly.
This method is Bresenham's line algorithm, worked out at IBM in 1962 to drive a plotter. It survived into every framebuffer renderer since for the reasons that fan demonstrates. Integers only, a couple of adds and compares per pixel, endpoints exact, and the same answer everywhere.
The {-1 6} write
Look again at the write in the middle of that loop. FillRect earned the
right to skip Set by clipping its rectangle first, and the line above
copied the style without the earning: it indexes Pix directly, and every
pixel in that fan happened to be inside the buffer. Point a line at a coordinate that
is not.
// cmd/worldc/main.go
b := render.NewBuffer(24, 12)
b.Fill(render.Soil)
// the stream, running west off the edge of the buffer
b.Line(20, 4, -6, 7, render.Water)
fmt.Print(b.Preview())
// the same mistake, made on row 0
b.Line(3, 0, -5, 0, render.Water)
fmt.Println("still drawing")
$ go run ./cmd/worldc
........................
........................
........................
........................
................~~~~~...
........~~~~~~~~.......~
~~~~~~~~..........~~~~~.
........................
........................
........................
........................
........................
panic: runtime error: index out of range [-1]
goroutine 1 [running]:
theworld/internal/render.(*Buffer).Line(...)
/home/you/theworld/internal/render/draw.go:80
main.main()
/home/you/theworld/cmd/worldc/main.go:18 +0x150
exit status 2
The stream is drawn correctly for as long as it is on the map, down and west from
{20 4} to column 0. Then look at the two clusters with no business
existing: one lone pixel at the very end of row 5, past a gap of dry soil, and five
more at columns 18 through 22 of row 6, east of a line that was travelling west.
Do the arithmetic on the first stray. The walk wanted a pixel at
{-1 6}, so index computed 6·24 + (−1) = 143.
That is a legal slot in a 288-pixel slice, and 143 is also 5·24 + 23,
which is the pixel at {23 5}: the far east end of the row above. The
other five wanted columns −2 through −6 on row 7 and landed on slots 166 down to
162, which is columns 22 through 18 of row 6. A line that walked off the west edge
came back on the east edge one row up, the same wraparound chapter 6's walker
suffered when it asked for {-1 5} and was handed the terrain at
{11 4}, for the same reason. The flattening formula is arithmetic, not
geography. It has no idea that x = −1 means "outside"; it just returns a smaller
number.
Then the second call kills the process. On row 0 there is no row above to wrap into,
so {-1 0} computes 0·24 + (−1) = −1, the slice bounds check
fires, and the whole client dies with its picture half drawn. Two faces of one bug,
and the loud one is the lucky one: everywhere except row 0 this mistake corrupts
the image quietly, in a place on screen unrelated to where the drawing went wrong.
Note what did not save it. Chapter 12's per-pixel guard is still sitting in
Set, working perfectly for every caller that uses it, and
Line walked around it.
Figure 13.1: the slice has no edges, only slots: a column of −1 is a subtraction, and the write lands wherever that subtraction points.
The repair is one word. A line discovers its coordinates one at a time, so it has no larger unit to clip, and the test goes where the pixels are.
// internal/render/draw.go — Line's loop, one line different
for {
b.Set(x0, y0, c)
if x0 == x1 && y0 == y1 {
return
}
$ go run ./cmd/worldc
........................
........................
........................
........................
................~~~~~...
........~~~~~~~~........
~~~~~~~~................
........................
........................
........................
........................
........................
still drawing
Same two calls, same coordinates, and the stream now stops dead at column 0 with
the east side of rows 5 and 6 untouched. The row 0 line draws its four on-map
pixels and returns, and the client is still running to say so. The fix
does not shorten the walk or leave the loop early. The error term
keeps stepping through coordinates nobody will ever see, out to {-6 7},
and Set throws each one away. That matters for correctness, because
the pixels a clipped line draws must be the pixels it would have drawn unclipped.
A line that changed its staircase depending on where the window edge fell would
shift by a pixel every time the camera moved.
Four comparisons per pixel is a real cost, and there is a known way to avoid most of it: compute where the line crosses the clip rectangle, move the endpoints onto those crossings, and run an unguarded loop between them. The catch is that moving an endpoint moves the error term with it, and an adjustment that is slightly wrong shifts the whole staircase by a pixel, in a bug that appears only when something is half off screen. At the sizes this volume draws, the guarded write is fast enough and provably identical to the unclipped line, so it comes first.
One thing is still untested. The clip rectangle has been the whole buffer this entire chapter, and its reason for existing is being something smaller.
// cmd/worldc/main.go — draw far too much, inside a small window
// border draws the four sides of a rectangle, one pixel wide.
func border(b *render.Buffer, r render.Rect, c render.Color) {
b.Line(r.X0, r.Y0, r.X1-1, r.Y0, c)
b.Line(r.X0, r.Y1-1, r.X1-1, r.Y1-1, c)
b.Line(r.X0, r.Y0, r.X0, r.Y1-1, c)
b.Line(r.X1-1, r.Y0, r.X1-1, r.Y1-1, c)
}
func main() {
b := render.NewBuffer(24, 12)
b.Fill(render.Soil)
view := render.Rect{X0: 6, Y0: 3, X1: 18, Y1: 10}
full := b.Clip()
b.SetClip(view)
b.FillRect(render.Rect{X0: 2, Y0: 6, X1: 22, Y1: 8}, render.Water)
b.Line(-4, 1, 27, 12, render.Rock)
b.SetClip(full)
border(b, view, render.Rock)
fmt.Print(b.Preview())
wide := render.Rect{X0: -100, Y0: -100, X1: 900, Y1: 900}
b.SetClip(wide)
fmt.Println("asked to clip to", wide)
fmt.Println("clip is ", b.Clip())
}
$ go run ./cmd/worldc
........................
........................
........................
......############......
......#..........#......
......###........#......
......#~~###~~~~~#......
......#~~~~~###~~#......
......#........###......
......############......
........................
........................
asked to clip to {-100 -100 900 900}
clip is {0 0 24 12}
The pond was asked for at columns 2 through 21 and appears only between 7 and 16.
The rock line was told to run from {-4 1}, off the buffer entirely, to
{27 12}, off the other side, and only its middle survives, entering
and leaving the window on the pixels it would have used anyway: clipping removed
pixels without moving any. The border exists because it is drawn afterwards with
the clip restored, four calls to Line, each using X1-1
because half-open means the last pixel is one short of the edge number.
The last two printed lines are what makes this a wall and not a suggestion. Asked
to open the clip to a thousand pixels in every direction, the buffer reports
{0 0 24 12}. Because SetClip intersects with
Bounds(), the writable area can be cut down by any caller and widened
by none, and that guarantee holds no matter what a camera or a panel layout
computes.
Pix is written in two places
The arrangement is the one chapter 6 settled on for the simulation, moved down a
layer. There, every route into the world's state ran through a handful of methods
that stopped trusting their callers, so other systems could not corrupt the grid
however confused its arithmetic became. The framebuffer has that property now for the
same reason: Pix is written by two functions, and both consult
clip before anything else happens. Sprites, tilemaps and text will draw
through those two, so none of them has to remember the check and none can forget it.
Callers multiply. Writers do not.
What changed between the layers is what the check may cost, and that is where the
rule splits in two. FillRect knows its whole job before it starts, so it
tests the description of the work: four comparisons decide the fate of thousands of
pixels. Line cannot, because its coordinates are produced as it goes, so
the test moves inside the loop and is paid per pixel. The principle under both: clip
the largest unit you can name in advance, and pay per item only for work whose extent
you learn while doing it. Every renderer here sorts into one of those two
cases.
The integer arithmetic settles a different debt. Volume 1 proved a run correct by replaying it and diffing the log, and that discipline is only available to code whose output is decided instead of measured. A staircase computed from accumulated floating-point error is decided by rounding, and stage 3 showed the endpoint that method can lose. Bresenham's walk is a sequence of integer additions: the same call draws the same pixels, in any run, on any machine, which is the precondition for a picture two runs can be compared by. Exact endpoints pay off more plainly as well. Tiles and panels are drawn edge to edge, and a renderer that missed an endpoint now and then would leave one-pixel seams between them, obvious on screen and unexplainable to anyone who did not know the line code was approximate.
Checkpoint
- Intersect two half-open rectangles by hand and say why
{-4 7 4 15}becomes{0 7 4 12}against a 24 by 12 buffer, and why{30 2 40 4}comes back empty. - Run the error walk for a line on paper, explain what
2*e >= dxasks about the true line, and argue why the walk cannot finish anywhere except on its endpoint. - Explain why the accumulated-slope stream ended on row 8 when it was told to end on row 9, and read 8.99999999999999645 as the cause.
- Take a stray pixel out of the wrapped preview, compute the slot it wrote to, and name the pixel that slot really belongs to.
- Say where the clip test goes for a rectangle, where it goes for a line, and why the two answers differ.
- Narrow the clip to a window, draw something twice its size through it, and predict every pixel that survives.
Exercise 1: rectangles that hold nothing. Call
FillRect four times on a fresh buffer, with {3 2 4 6},
{3 2 3 6}, {9 2 3 6} and {30 2 40 4}.
Predict how many pixels each one paints, then count them.
Four, zero, zero, zero. {3 2 4 6} is one column wide, since
half-open makes the width 4 − 3, and it paints rows 2 through 5.
{3 2 3 6} has zero width and Empty catches it. The
backwards one is the case to sit with: Intersect leaves
X1 below X0, the clamp lifts X1 up to 9,
and {9 2 9 6} is empty. Without that clamp the row slice would be
asked for a negative length and the client would die, so those four lines turn
a swapped pair of arguments into nothing drawn.
Exercise 2: draw it backwards. For every pair of points on a 24 by 12 buffer, draw the line one way, draw it the other way into a second buffer, and compare the two previews. How many of the 82,944 pairs disagree?
26,560 of them, close to a third. The cause is the tie: when e2
lands exactly on the boundary, two pixels are equally close to the true line,
and the loop resolves that in favour of the direction it is walking, so
starting from the other end resolves it the other way. The smallest example is
{0 0} to {1 2}, where one direction bends late and
the other early. Neither is wrong, and the property matters: if two parts of a
renderer draw the same wall from opposite ends, they will disagree about its
pixels. The usual answer is to order the endpoints first, always starting from
the smaller x, so one geometric line has exactly one drawing.
Exercise 3: count the corners. Write
border from stage 6 yourself and draw the outline of
{6 3 18 10}. Before running it, work out how many pixels it should
paint, then count them in Pix.
34, and the arithmetic is 2·(12 + 7) − 4 for a 12 by 7
rectangle: two rows plus two columns, minus the four corners each counted
twice. Counting that colour in Pix gives exactly 34, because
drawing a pixel twice paints it once, and the agreement between prediction and
count is the exercise. The formula survives a rectangle one pixel wide, where
2·(1 + 7) − 4 = 12 describes a single column drawn twice
with its two ends shared.