The Box the Words Sit In
Six hundred and sixty-six pixels on a ground a hundred and ninety-two wide
Halla's persona begins with a sentence of a hundred and eleven bytes. The font the
client has had since chapter 21 puts each byte in a cell six pixels wide, so drawn
the only way the renderer knows how to draw a string, the sentence is six hundred
and sixty-six pixels long, and the ground she stands on is a hundred and ninety-two
pixels from rim to rim. Her face is sixteen pixels square and her name is thirty
pixels of the same glyphs. The renderer can put all three on the frame today: a
face is four blits through a table, a name is DrawTextIn, and the
sentence is the same call, which advances the pen six pixels a byte and stops when
the bytes run out, three and a half grounds to the right of where it started.
What is missing is a box. Not a rectangle, which FillRect has drawn
since chapter 13, but a box a reader takes as the place words are said from: an
edge that stands off ground, water and plants alike, a name on it, a face in it,
and inside it a column of text that ends where the box does and goes on to the next
line. The frame is one small picture cut into nine pieces, the corners put
down once, the edges and the middle repeated until the box is the size the words need;
and the words are cut at spaces into lines no wider than a column measured in the font's
own pixels, before a glyph of them is drawn.
Two ways of making one are refused before the page starts. A box drawn in code, a filled rectangle and eight lines round it, works, and then every corner, every highlight and every change of style is a change to the drawing code, which is the wrong place for a picture to live. A box saved as a picture at its finished size is one box: a hundred and eighty-four by fifty-nine pixels for five lines of Halla, a second file for two lines of Ander, and ten thousand pixels a file for a box that is mostly one flat colour.
The frame this page draws is twelve pixels square, and the box on the last figure is a hundred and eighty-four by fifty-nine, with a name plate over its top edge made from the same twelve pixels at forty-two by nineteen. Both halves are arithmetic over a measurement: the box measures the words, and the words measure the column. Nothing on the page asks a model anything, opens a window, a socket or a database, and every picture on it is written headlessly by the chapter's own bench and compared by hash.
By the end of the page assets/ui/panel.png and
assets/palette/panel-6.png exist, written by a program that holds the
frame as twelve rows of characters and compared byte for byte with the pair the book
ships; internal/render has a nine-slice blit, a word-wrapper over the
six-by-eight font, and a panel that lays a slot, a plate and a column out from a
width and a string; and five pictures have been written by the bench, one of them
the counterfactual with no wrapper in it, and one of them Halla in her frame over the
valley, saying the first sentence of her persona.
Twelve pixels square, six of the sixteen, cut at four
The frame is drawn once, at the smallest size that shows every part of it, and twelve pixels is that size. Cut four pixels in from every edge and the picture falls into nine pieces: four corners of four by four, four edges of four by four, and a middle of four by four. The corners hold everything about the frame that cannot repeat, the rounded turn where the top line meets the left. An edge holds one run of the frame's lines and the field beside them, and it is what the box is made of along its sides, laid down over and over. The middle is the field, and nothing but the field, because the words sit on it and a field with a pattern in it is a field that puts pixels beside every glyph.
Three lines of frame stand between the outside and the field, and six of the valley's sixteen colours draw them. The outermost line is ink all the way round, for the reason chapter 17 gave when the walker got its outline: a closed dark edge is what lets a small thing sit on any background, and the panel is going to sit on soil, on water, on the green of a plant, and on all three at once. Inside that are two lines that are lit on the top and left and shaded on the bottom and right, because the light in this world has come from the upper left since the walker was drawn: bone over rock-m where the light lands, rock-l over rock-d where it does not, with the horizontal lines owning the corners where the two meet. The field is water-d, the darkest step of the water ramp and one entry above ink. The corner pixel of the outer line is left off, so the corner is rounded by one pixel and what is behind the box shows through it, which is the alpha channel doing for a frame what it has done for every sprite since chapter 16.
Two of those six were chosen by measuring and not by looking, and the run that measures them is on the page further down. The field is not ink, though ink would make the words stand off it hardest, because a villager's face is going into this box and every outline on that face is ink: on an ink field the face has no edge. And the lines round the portrait are not ink either, for the same reason, so the slot the face sits in gets a field of its own, the water midtone chapter 99's card stood three faces on, and a ring of rock-m, the frame's own lit inner line.
The frame below is twelve rows of twelve characters and it needs no editor. If
you would rather see it while you change it, LibreSprite
(libresprite.github.io) loads assets/palette/valley-16.gpl as a
palette, reduces the colour picker to the sixteen this world allows, and shows a
twelve-by-twelve canvas at any zoom; draw it there with a four-pixel grid on, and
carry each row into the table a character at a time. The table is what ships,
because the table is what a program can rebuild.
Create cmd/mkpanel/art.go. Every pixel is a letter and every letter
is an entry of the frame's own strip, exactly as chapter 99's parts are; a space is
a pixel that is not there. The comment above the table is the key, and the four
spaces are the corners.
// cmd/mkpanel/art.go
package main
// The frame, written as characters.
//
// Twelve pixels square, and cut four from each edge into nine pieces:
// four corners that are put down once, four edges that are repeated
// along their sides, and a middle that is repeated across the inside.
// Everything the box is made of is in these twelve rows, and a box of
// any size from eight pixels upward is these rows put down in pieces.
//
// Three lines of frame stand between the outside and the field. The
// outermost is ink all the way round, the closed dark edge that lets
// the box sit on ground, water or a plant without its lines going
// missing; the corner pixel is left off, so the corner is rounded and
// what is behind shows through it. Inside that, two lines that are lit
// on the top and left and shaded on the bottom and right, because the
// light in this world has come from the upper left since the walker:
// bone over rock-m where the light lands, rock-l over rock-d where it
// does not, the horizontal lines owning the corners they meet at. The
// field is water-d, the darkest step of the water ramp and one entry
// above ink, chosen for the words to sit on: bone glyphs on it differ
// in every channel by more than a hundred, and a portrait's ink
// outline never touches it, because the slot has a field of its own.
//
// o ink the outer line, never anything else
// b bone the lit outer line, top and left
// m rock-m the lit inner line, top and left
// l rock-l the shaded outer line, bottom and right
// d rock-d the shaded inner line, bottom and right
// f water-d the field, which is every pixel of the middle piece
//
// A space is a pixel that is not there: the four corners only.
var frame = []string{
" oooooooooo ",
"obbbbbbbbbbo",
"obmmmmmmmmlo",
"obmffffffdlo",
"obmffffffdlo",
"obmffffffdlo",
"obmffffffdlo",
"obmffffffdlo",
"obmffffffdlo",
"obddddddddlo",
"ollllllllllo",
" oooooooooo ",
}
Read it as the nine pieces. Rows 0 to 3 of columns 0 to 3 are the top-left
corner: the missing pixel and three of ink, then b and m
turning the corner, then one pixel of field. Rows 0 to 3 of columns 4 to 7 are
the top edge, one row each: ink, bone, rock-m, field. Rows 4 to 7 of columns 4 to
7 are sixteen pixels of f, the middle. The bottom-right corner is the same shape mirrored,
with d and l where the top-left has m and
b. The whole frame is a hundred and forty pixels and four holes, and
a box of any size is made of nothing else.
Create cmd/mkpanel/main.go. The first half is chapter 99's
mkfolk with a frame in place of a sheet: a key from characters to
entries of the sixteen, a painter that refuses a character the key does not hold,
and a mode that writes the frame and its strip and, given -to,
compares what it wrote with what the book ships. main names every
mode of the bench in one switch; the modes are printed where the page uses them,
and the file compiles once the last of them is in.
// cmd/mkpanel/main.go
package main
import (
"bytes"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"theworld/exemplars"
"theworld/internal/field"
"theworld/internal/gene"
"theworld/internal/render"
"theworld/internal/scene"
"theworld/internal/sim"
"theworld/internal/terra"
"theworld/internal/village"
)
// Where the two files live, the font the words are set in, and the
// sheet a face is composed from.
const (
Frame = "assets/ui/panel.png"
Strip = "assets/palette/panel-6.png"
Font = "assets/sprites/font6x8.png"
Sheet = "assets/sprites/folk.png"
)
// Corner is where the frame is cut: the corners are this many pixels
// square, and the middle piece is what the twelve leave between them.
const Corner = 4
// key is the character each entry of the strip is drawn as, in the
// order the strip lists them. A pixel that is none of them is a
// mistake this program refuses to write out.
var key = []struct {
ch byte
at int
what string
}{
{'o', gene.Ink, "the outer line"},
{'b', gene.Bone, "lit outer line, top and left"},
{'m', gene.RockM, "lit inner line, top and left"},
{'l', gene.RockL, "shaded outer line, bottom and right"},
{'d', gene.RockD, "shaded inner line, bottom and right"},
{'f', gene.WaterD, "the field"},
}
// ...
// at is the strip position one character stands for.
func at(ch byte) (int, bool) {
for i, k := range key {
if k.ch == ch {
return i, true
}
}
return 0, false
}
// colour is the valley colour a strip position is drawn in.
func colour(pos int) render.Color { return gene.Sixteen[key[pos].at] }
// letter is the character a frame colour stands for, for printing a
// piece back out as it was authored.
func letter(c render.Color) byte {
if c == render.Clear {
return '.'
}
for i := range key {
if colour(i) == c {
return key[i].ch
}
}
return '?'
}
// build paints the frame and the strip from the characters.
func build() (*render.Buffer, *render.Buffer) {
if len(frame) != len(frame[0]) {
die(fmt.Errorf("the frame is %d rows of %d: it has to be square", len(frame), len(frame[0])))
}
n := len(frame)
pic := render.NewBuffer(n, n)
for y, line := range frame {
if len(line) != n {
die(fmt.Errorf("frame row %d is %d characters, want %d", y, len(line), n))
}
for x := 0; x < n; x++ {
ch := line[x]
if ch == ' ' {
continue
}
pos, ok := at(ch)
if !ok {
die(fmt.Errorf("frame row %d column %d: no palette entry for %q", y, x, ch))
}
pic.Set(x, y, colour(pos))
}
}
strip := render.NewBuffer(len(key), 1)
for i := range key {
strip.Set(i, 0, colour(i))
}
return pic, strip
}
// nine is the frame cut at Corner, from the characters.
func nine() *render.Nine {
pic, _ := build()
n, err := render.NewNine(pic, Corner, "the frame")
if err != nil {
die(err)
}
return n
}
func main() {
mode := flag.String("mode", "frame", "frame, slices, boxes, wrap, fit, contrast, pads, pitches or panel")
to := flag.String("to", "", "frame: write the pair into this directory and compare them with the shipped pair")
w := flag.Int("w", 184, "the box's width in pixels: slices, wrap, fit, pads, pitches, panel")
h := flag.Int("h", 40, "slices: the box's height in pixels")
pad := flag.Int("pad", 3, "pixels between the frame's inner line and the content")
pitch := flag.Int("pitch", 10, "pixels from the top of one line of text to the next")
gap := flag.Int("gap", 4, "pixels between the slot's ring and the first glyph")
plate := flag.Int("plate", Corner, "pixels of the name plate that lie over the box's top edge")
who := flag.String("who", "Halla", "panel: the villager whose face and name are drawn")
text := flag.String("text", "", "the words; blank takes the villager's persona, or the sentence -sentence names")
sentence := flag.Int("sentence", 1, "which sentence of the persona is the words; 0 is all of it")
memory := flag.Int("memory", 0, "panel: take the words from this memory of the committed stream instead")
nowrap := flag.Bool("nowrap", false, "panel: draw the words as one line, wrapped to nothing")
seed := flag.Uint64("seed", 1, "panel: the seed the ground behind the panel is generated from")
shot := flag.String("shot", "", "boxes, pads, pitches, panel: write the picture to this file, scaled")
scale := flag.Int("scale", 4, "how many times a picture is scaled before it is written")
flag.Parse()
switch *mode {
case "frame":
frameMode(*to)
case "slices":
slicesMode(*w, *h)
case "boxes":
boxesMode(*shot, *scale)
case "wrap":
wrapMode(panel(*pad, *pitch, *gap, *plate), *w, words(*who, *text, *sentence, *memory))
case "fit":
fitMode(*w, *gap, *plate, words(*who, *text, *sentence, *memory))
case "contrast":
contrastMode()
case "pads":
padsMode(*shot, *scale, *w, *pitch, *gap, *plate, *who, words(*who, *text, *sentence, *memory))
case "pitches":
pitchesMode(*shot, *scale, *w, *pad, *gap, *plate, *who, words(*who, *text, *sentence, *memory))
case "panel":
panelMode(*shot, *scale, panel(*pad, *pitch, *gap, *plate), *w, *who, words(*who, *text, *sentence, *memory), *nowrap, *seed)
default:
die(fmt.Errorf("-mode %q: frame, slices, boxes, wrap, fit, contrast, pads, pitches or panel", *mode))
}
}
// frameMode writes the pair and reports what is on them.
func frameMode(to string) {
pic, strip := build()
framePath, stripPath := Frame, Strip
if to != "" {
framePath = filepath.Join(to, filepath.Base(Frame))
stripPath = filepath.Join(to, filepath.Base(Strip))
}
write(pic, framePath)
write(strip, stripPath)
n, err := render.NewNine(pic, Corner, framePath)
if err != nil {
die(err)
}
tw, th := n.Tile()
fmt.Printf("mkpanel: %s, %dx%d, cut %d from each edge: corners %dx%d, edges and middle tiled at %dx%d, %d bytes\n",
framePath, pic.W, pic.H, Corner, Corner, Corner, tw, th, size(framePath))
fmt.Printf(" pixels sha256 %s\n", pic.Hash())
fmt.Printf("\n the six entries the frame is drawn in, by their index in the valley's sixteen\n")
for i, k := range key {
c := colour(i)
fmt.Printf(" %c entry %2d %02X%02X%02X %s\n", k.ch, k.at, c.R(), c.G(), c.B(), k.what)
}
fmt.Printf("\n the nine pieces, as characters, each one cut from the picture above\n")
for j := 0; j < 3; j++ {
for y := n.Slice(0, j).Y0; y < n.Slice(0, j).Y1; y++ {
fmt.Print(" ")
for i := 0; i < 3; i++ {
r := n.Slice(i, j)
if i > 0 {
fmt.Print(" ")
}
for x := r.X0; x < r.X1; x++ {
fmt.Printf("%c", letter(pic.At(x, y)))
}
}
fmt.Println()
}
fmt.Println()
}
// Nothing outside the six may reach the frame. The check is here
// and not in a test because this is the program that could put it
// there.
loose, clear := 0, 0
for _, c := range pic.Pix {
if c == render.Clear {
clear++
continue
}
if !onStrip(strip, c) {
loose++
}
}
fmt.Printf("mkpanel: %s, %dx%d, %d entries, %d bytes\n", stripPath, strip.W, strip.H, len(key), size(stripPath))
fmt.Printf(" pixels sha256 %s\n", strip.Hash())
fmt.Printf(" pixels on the frame the strip does not hold: %d; pixels not there: %d\n", loose, clear)
if loose != 0 {
os.Exit(1)
}
if to != "" {
same(framePath, Frame)
same(stripPath, Strip)
}
}
// ...
$ go run ./cmd/mkpanel -to .
mkpanel: panel.png, 12x12, cut 4 from each edge: corners 4x4, edges and middle tiled at 4x4, 159 bytes
pixels sha256 c111d3e64b3e49468441d7451ff8344280236a29c0ae661ff1a5dd7905fdfd27
the six entries the frame is drawn in, by their index in the valley's sixteen
o entry 0 181A29 the outer line
b entry 15 F5EBD7 lit outer line, top and left
m entry 5 5A6470 lit inner line, top and left
l entry 6 8A979E shaded outer line, bottom and right
d entry 4 323847 shaded inner line, bottom and right
f entry 7 243E6B the field
the nine pieces, as characters, each one cut from the picture above
.ooo oooo ooo.
obbb bbbb bbbo
obmm mmmm mmlo
obmf ffff fdlo
obmf ffff fdlo
obmf ffff fdlo
obmf ffff fdlo
obmf ffff fdlo
obmf ffff fdlo
obdd dddd ddlo
olll llll lllo
.ooo oooo ooo.
mkpanel: panel-6.png, 6x1, 6 entries, 89 bytes
pixels sha256 076ea86026d6adf62e82f6e2fac77387cdb4274b5bd41d5b577a5bd7d622ffbe
pixels on the frame the strip does not hold: 0; pixels not there: 4
panel.png is byte for byte assets/ui/panel.png
panel-6.png is byte for byte assets/palette/panel-6.png
A hundred and fifty-nine bytes for the frame and eighty-nine for the strip, and
the last two lines are the reason the frame is characters: the program writes the
pair beside the module and says whether it is the pair the book ships, every time
it runs, so nobody has to trust that the committed file is what the table makes.
The nine pieces are printed as they are cut, and the print is the check a reader
can do by eye. Every corner is four by four with its outer pixel missing; every
edge is one run of the lines; the middle is field alone. The loose-pixel count is
the same refusal mkfolk makes: a colour on the frame that the strip
does not hold is a mistake, and the program exits before it becomes a file.
Create cmd/mkpanel/art_test.go. The tests hold what the prose
claimed about the table: the frame is three corners' worth of rows, so every
piece is a corner's size; the outermost ring is ink except at the four corners,
and no pixel inside it is ink, so nothing in the frame can be mistaken for a
face's outline; the middle piece is field and nothing else; and the two lines on
the top and left are brighter, summed over red, green and blue, than the two on
the bottom and right. Each was shown to fail before the frame was right: ink in
the middle piece fails the second and the third at once.
$ go test -count=1 ./cmd/mkpanel/ -run 'TestTheFrameIsTwelveSquareAndDrawnInTheStrip|TestTheOuterLineIsInkAllRoundButTheCorners|TestTheMiddlePieceIsFieldAlone|TestTheLightIsFromTheUpperLeft' -v
=== RUN TestTheFrameIsTwelveSquareAndDrawnInTheStrip
--- PASS: TestTheFrameIsTwelveSquareAndDrawnInTheStrip (0.00s)
=== RUN TestTheOuterLineIsInkAllRoundButTheCorners
--- PASS: TestTheOuterLineIsInkAllRoundButTheCorners (0.00s)
=== RUN TestTheMiddlePieceIsFieldAlone
--- PASS: TestTheMiddlePieceIsFieldAlone (0.00s)
=== RUN TestTheLightIsFromTheUpperLeft
--- PASS: TestTheLightIsFromTheUpperLeft (0.00s)
PASS
ok theworld/cmd/mkpanel 0.003s
Fifty-five blits for a box of forty-two by nineteen
The blit that makes a box from the frame is chapter 16's Blit called in
a loop with the clip narrowed, and nothing more. Each of the nine pieces has a
source rectangle on the frame and a destination rectangle on the box. A corner's
destination is the box's corner, four by four, so a corner is copied once. An
edge's destination is the run between two corners, four pixels wide or tall and as
long as the box leaves it, so the edge piece is copied at its own size starting at
one end and stepping by its own width until the run is covered. The middle's
destination is everything inside the four runs, and the middle piece is copied
across it the same way, row after row. Wherever the run does not divide by four,
the last copy in that direction would spill past the box, and the clip cuts it: the
clip is set to the destination rectangle before each piece is laid down and put
back afterwards, so the loop can step freely and nothing lands outside the box.
The pieces are the frame addressed by arithmetic, which is chapter 21's font one
more time. A glyph was cell i of a sheet, found by subtracting, dividing
and multiplying; a piece is column i and row j of a frame,
each 0, 1 or 2, and the four boundaries in each direction are 0, the corner, the
picture's width less the corner, and the picture's width. The corner size is the
only number the cut needs, and it is the one number a frame carries beside its
pixels.
Create internal/render/nine.go. It lives in render
beside the sprite and the font because it is the same kind of thing: a picture
and a rule for cutting it. A frame with no room between its corners is refused at
load, because a loop that repeats a piece of no width steps by nothing.
// internal/render/nine.go
package render
import "fmt"
// Nine is a frame drawn once at its smallest and cut into nine pieces:
// four corners, four edges and a middle. A box of any size from two
// corners upward is made from it by putting the corners down unchanged,
// repeating the edge pieces along the sides, and repeating the middle
// piece across the inside. The picture is one small file, and the box
// it makes is as wide as the words need.
//
// The cut is one number, C. The corners are C pixels square, so the
// edges and the middle are whatever the picture has left between them:
// on a picture W pixels wide the top edge piece is W-2C wide and C
// tall, and that width is the tile the edge repeats at.
type Nine struct {
Pix *Buffer // the frame as drawn, every piece in place
C int // the corner size in pixels; the middle tile is what is left
sheet *Sheet // Pix wrapped for Blit, which copies rectangles of sheets
}
// LoadNine reads a frame and cuts it at c. A picture with no room for a
// middle piece is refused: a frame whose corners meet has no edge to
// repeat, and the loop that repeats one would step by nothing.
func LoadNine(path string, c int) (*Nine, error) {
pix, err := loadStraight(path)
if err != nil {
return nil, err
}
return NewNine(pix, c, path)
}
// NewNine cuts a frame already in memory; what is the picture's name in
// a complaint.
func NewNine(pix *Buffer, c int, what string) (*Nine, error) {
if c < 1 || pix.W <= 2*c || pix.H <= 2*c {
return nil, fmt.Errorf("%s is %dx%d, which leaves nothing between corners of %d", what, pix.W, pix.H, c)
}
return &Nine{Pix: pix, C: c, sheet: &Sheet{Pix: pix, Cell: 1}}, nil
}
// Tile is the size of the piece the edges and the middle repeat at:
// the picture's width and height with the two corners taken off.
func (n *Nine) Tile() (int, int) { return n.Pix.W - 2*n.C, n.Pix.H - 2*n.C }
// Slice is the source rectangle of one of the nine pieces, column i
// and row j counted from the top left, each 0, 1 or 2. Column 0 is C
// wide, column 1 is the tile, column 2 is C wide again; the rows are
// cut the same way.
func (n *Nine) Slice(i, j int) Rect {
tw, th := n.Tile()
xs := [4]int{0, n.C, n.C + tw, n.Pix.W}
ys := [4]int{0, n.C, n.C + th, n.Pix.H}
return Rect{X0: xs[i], Y0: ys[j], X1: xs[i+1], Y1: ys[j+1]}
}
// Tiles is how many times the middle piece is put down across and down
// a box, the last one cut short where the box does not divide. A box
// with no room between its corners has no middle and counts nothing.
func (n *Nine) Tiles(box Rect) (across, down int) {
tw, th := n.Tile()
w, h := box.X1-box.X0-2*n.C, box.Y1-box.Y0-2*n.C
if w <= 0 || h <= 0 {
return 0, 0
}
return (w + tw - 1) / tw, (h + th - 1) / th
}
// ...
Tiles is the count the interlude below works by hand and the bench
prints: how many times the middle piece goes down across and down a box, the
division rounded up because a run of thirty-four pixels needs nine tiles of four,
the ninth of them cut to two. It is here, and not left to the bench, so that a
test can ask the frame how many blits a box costs without drawing it.
The name plate on the last figure is a box forty-two pixels wide and nineteen tall, from a frame twelve square cut at four, and every number in laying it down comes from those four. The tile is the picture less two corners: 12 − 2 × 4 = 4, in both directions. The top edge's run is the box less two corners: 42 − 8 = 34 pixels. Thirty-four is 8 × 4 + 2, so the edge piece goes down nine times along the top, eight of them whole and the ninth cut to two pixels wide by the clip. The left edge's run is 19 − 8 = 11 pixels, which is 2 × 4 + 3: three copies, the third cut to three tall. The middle is 34 by 11, so 9 × 3 = 27 copies of the middle piece, and the copy in the far corner of it is cut both ways, to two wide and three tall.
Counting the blits: four corners once each, the top and bottom edges nine each, the left and right edges three each, and twenty-seven in the middle, which is 4 + 18 + 6 + 27 = 55 copies of a four-by-four piece to make a box of 42 × 19 = 798 pixels, out of a picture of 144. The column inside the box is the panel's business and the section after next measures it, but the arithmetic is the same kind: a glyph is six wide, and a column of a hundred and fifty pixels holds 150 ÷ 6 = 25 of them with nothing left over.
Extend internal/render/nine.go with the loop. The nine destinations
are built from the box the same way the nine sources are built from the frame, so
the pairs line up by index, and the only branch is a box with no room for its
corners, which draws nothing instead of a frame folded over itself.
// internal/render/nine.go
// DrawNine makes a box from the frame. Each of the nine pieces has a
// destination: the corners are the box's corners, C square; the edges
// are the runs between them; the middle is what is left. Every piece
// is blitted at its own size, starting at its destination's top left
// and stepping by its own width and height until the destination is
// covered, with the clip narrowed to the destination so the last tile
// in each direction is cut where the box ends. A corner's destination
// is exactly one tile, so a corner is put down once and never cut.
//
// The pixels go through the same Blit every sprite goes through, so a
// pixel the frame does not hold leaves the buffer alone: that is how a
// corner is rounded, and how the box sits on whatever is behind it.
//
// The edges are repeated and not stretched. Stretching a four-pixel
// edge across two hundred reads each source pixel fifty times in a
// row, and a mark drawn one pixel wide comes out fifty wide; repeating
// puts the mark down at the size it was drawn, every time.
func (b *Buffer) DrawNine(n *Nine, box Rect) {
if box.X1-box.X0 < 2*n.C || box.Y1-box.Y0 < 2*n.C {
return
}
// Where each column and row of the box begins and ends, in the
// same terms Slice cuts the source: corner, run, corner.
xs := [4]int{box.X0, box.X0 + n.C, box.X1 - n.C, box.X1}
ys := [4]int{box.Y0, box.Y0 + n.C, box.Y1 - n.C, box.Y1}
was := b.Clip()
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
src := n.Slice(i, j)
dst := Rect{X0: xs[i], Y0: ys[j], X1: xs[i+1], Y1: ys[j+1]}
if dst.Empty() {
continue
}
b.SetClip(dst.Intersect(was))
sw, sh := src.X1-src.X0, src.Y1-src.Y0
for y := dst.Y0; y < dst.Y1; y += sh {
for x := dst.X0; x < dst.X1; x += sw {
b.Blit(n.sheet, src, x, y)
}
}
}
}
// ...
$ go run ./cmd/mkpanel -mode slices -w 42 -h 19
mkpanel: a 12x12 frame cut at 4, making a box 42 by 19
piece from to put down
top left 0,0 4x4 0,0 4x4 once, 4x4
top middle 4,0 4x4 4,0 34x4 9 across, 1 down, the last cut to 2x4
top right 8,0 4x4 38,0 4x4 once, 4x4
middle left 0,4 4x4 0,4 4x11 1 across, 3 down, the last cut to 4x3
middle middle 4,4 4x4 4,4 34x11 9 across, 3 down, the last cut to 2x3
middle right 8,4 4x4 38,4 4x11 1 across, 3 down, the last cut to 4x3
bottom left 0,8 4x4 0,15 4x4 once, 4x4
bottom middle 4,8 4x4 4,15 34x4 9 across, 1 down, the last cut to 2x4
bottom right 8,8 4x4 38,15 4x4 once, 4x4
the box is 42 pixels wide: two corners of 4 leave 34, which is 9 tiles of 4
and 19 tall: two corners leave 11, which is 3 tiles of 4
27 middle tiles, 24 edge tiles, 4 corners: 55 blits for a box of 798 pixels, from a picture of 144
The bench's -mode slices is the interlude with the numbers filled in
by the frame instead of by hand, and the two agree line for line: nine across
with the last cut to two, three down with the last cut to three, fifty-five blits.
The mode is Slice, Tiles and a table.
// cmd/mkpanel/main.go
// slicesMode prints the nine-slice arithmetic for one box: where each
// piece comes from, where it goes, and how many times the repeated
// pieces are put down.
func slicesMode(w, h int) {
n := nine()
tw, th := n.Tile()
box := render.Rect{X0: 0, Y0: 0, X1: w, Y1: h}
names := [3]string{"left", "middle", "right"}
rows := [3]string{"top", "middle", "bottom"}
fmt.Printf("mkpanel: a %dx%d frame cut at %d, making a box %d by %d\n\n", n.Pix.W, n.Pix.H, n.C, w, h)
fmt.Printf(" %-14s %-14s %-16s %s\n", "piece", "from", "to", "put down")
xs := [4]int{0, n.C, w - n.C, w}
ys := [4]int{0, n.C, h - n.C, h}
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
src := n.Slice(i, j)
dst := render.Rect{X0: xs[i], Y0: ys[j], X1: xs[i+1], Y1: ys[j+1]}
sw, sh := src.X1-src.X0, src.Y1-src.Y0
dw, dh := dst.X1-dst.X0, dst.Y1-dst.Y0
across, down := (dw+sw-1)/sw, (dh+sh-1)/sh
how := fmt.Sprintf("once, %dx%d", sw, sh)
if dst.Empty() {
how = "nowhere: the corners leave it no room"
} else if i == 1 || j == 1 {
how = fmt.Sprintf("%d across, %d down", across, down)
cutW, cutH := dw-(across-1)*sw, dh-(down-1)*sh
if cutW != sw || cutH != sh {
how += fmt.Sprintf(", the last cut to %dx%d", cutW, cutH)
}
}
fmt.Printf(" %-14s %-14s %-16s %s\n", rows[j]+" "+names[i],
fmt.Sprintf("%d,%d %dx%d", src.X0, src.Y0, sw, sh),
fmt.Sprintf("%d,%d %dx%d", dst.X0, dst.Y0, dw, dh), how)
}
}
a, d := n.Tiles(box)
fmt.Printf("\n the box is %d pixels wide: two corners of %d leave %d, which is %d tiles of %d\n",
w, n.C, w-2*n.C, a, tw)
fmt.Printf(" and %d tall: two corners leave %d, which is %d tiles of %d\n", h, h-2*n.C, d, th)
fmt.Printf(" %d middle tiles, %d edge tiles, 4 corners: %d blits for a box of %d pixels, from a picture of %d\n",
a*d, 2*a+2*d, a*d+2*a+2*d+4, w*h, n.Pix.W*n.Pix.H)
}
// ...
Run it for the box Halla's sentence needs and the counts grow without the frame changing: forty-four tiles across, thirteen down with the last cut to three, five hundred and seventy-two copies of the middle and six hundred and ninety blits in all for a box of ten thousand eight hundred and fifty-six pixels, still from a picture of a hundred and forty-four.
$ go run ./cmd/mkpanel -mode slices -w 184 -h 59
mkpanel: a 12x12 frame cut at 4, making a box 184 by 59
piece from to put down
top left 0,0 4x4 0,0 4x4 once, 4x4
top middle 4,0 4x4 4,0 176x4 44 across, 1 down
top right 8,0 4x4 180,0 4x4 once, 4x4
middle left 0,4 4x4 0,4 4x51 1 across, 13 down, the last cut to 4x3
middle middle 4,4 4x4 4,4 176x51 44 across, 13 down, the last cut to 4x3
middle right 8,4 4x4 180,4 4x51 1 across, 13 down, the last cut to 4x3
bottom left 0,8 4x4 0,55 4x4 once, 4x4
bottom middle 4,8 4x4 4,55 176x4 44 across, 1 down
bottom right 8,8 4x4 180,55 4x4 once, 4x4
the box is 184 pixels wide: two corners of 4 leave 176, which is 44 tiles of 4
and 59 tall: two corners leave 51, which is 13 tiles of 4
572 middle tiles, 114 edge tiles, 4 corners: 690 blits for a box of 10856 pixels, from a picture of 144
The edges are repeated and not stretched, and the choice was made on this
frame's arithmetic and not its looks, because on this frame it makes no
visible difference. Chapter 68's BlitMap stretches: drawing the top
edge's four pixels across a run of a hundred and seventy-six, it reads source
column x·4/176 for every destination column, so source column 0
is read forty-four times in a row, then column 1 forty-four times. On four
pixels of unbroken bone that comes out as a hundred and seventy-six pixels of
unbroken bone, the same as repeating. Put one mark on the edge, a single pixel of
rock-l at column 2 as a stitch, and stretching draws it forty-four pixels long
while repeating draws it forty-four times at one pixel each. The frame the page
ships has no stitch, and the blit is written for the frame a reader draws next,
which may.
-mode boxes draws two boxes from the one frame on a field of the
water midtone and writes the card at four times. The small one has room for
three middle tiles across and one down; the large one for twenty and five.
Neither has a glyph in it, so what is on the card is the frame and nothing else.
$ go run ./cmd/mkpanel -mode boxes -shot assets/frames/panel-boxes.png
mkpanel: one frame, two boxes, on a card 124 by 36
small 20 by 12 at 4,4: middle tiles 3 across, 1 down
large 88 by 28 at 32,4: middle tiles 20 across, 5 down
the small box, row by row, one character a pixel; a dot is the card showing through a corner
.oooooooooooooooooo.
obbbbbbbbbbbbbbbbbbo
obmmmmmmmmmmmmmmmmlo
obmffffffffffffffdlo
obmffffffffffffffdlo
obmffffffffffffffdlo
obmffffffffffffffdlo
obmffffffffffffffdlo
obmffffffffffffffdlo
obddddddddddddddddlo
ollllllllllllllllllo
.oooooooooooooooooo.
the card 124 by 36 pixels, 1224c01bf41cb379e2871d60f6e0a3d5535c168cf11b38fce8370dcf0297beaf
wrote assets/frames/panel-boxes.png at 4 times, 496 by 144
assets/frames/panel-boxes.png: one frame, two boxes, at four pixels
to one. The lines are the same thickness on both because the edges are repeated
and not stretched, and the corners are the same four pixels on both because a
corner is copied once whatever the box's size.
Create internal/render/nine_test.go. The frame under test is not
the shipped one but a twelve-square picture whose nine pieces are nine colours,
one each, with the outer corner pixels left off, so that a test can read which
piece any pixel of a box came from. The first test draws a box that no piece
divides evenly and checks every pixel of the buffer, inside the box against the
piece it should have come from and outside it against the field; the second draws
every box from eight square up to twenty-one square and refuses one seven wide;
the third holds the nine source rectangles to the picture, each pixel of it in
exactly one.
$ go test -count=1 ./internal/render/ -run 'TestACornerIsPutDownOnceAndTheEdgesRepeat|TestOneFrameMakesABoxOfAnySizeFromTwoCornersUp|TestTheNinePiecesTileTheFrame' -v
=== RUN TestACornerIsPutDownOnceAndTheEdgesRepeat
--- PASS: TestACornerIsPutDownOnceAndTheEdgesRepeat (0.00s)
=== RUN TestOneFrameMakesABoxOfAnySizeFromTwoCornersUp
--- PASS: TestOneFrameMakesABoxOfAnySizeFromTwoCornersUp (0.00s)
=== RUN TestTheNinePiecesTileTheFrame
--- PASS: TestTheNinePiecesTileTheFrame (0.00s)
PASS
ok theworld/internal/render 0.004s
Twenty words into five lines of twenty-five glyphs
Before the wrapper is written, the panel is run without one. The bench's
-mode panel draws the box, the slot with Halla's face in it and the
plate with her name, over the valley the village stands on, and then puts her
sentence down with DrawTextIn as chapter 21 left it, one line, from
where the first line of the column would start. Behind -nowrap that is
all it does; the flag is the counterfactual, and it is run first because the
picture makes the case for the rest of the section faster than a paragraph about
legibility would.
$ go run ./cmd/mkpanel -mode panel -nowrap -shot assets/frames/panel-nowrap.png
mkpanel: the ground from seed 1 at tick 901, 192 by 128 pixels, 11 plants standing
Halla, home 8,5, the face off the row; the words: 111 bytes, 20 words
a box 184 wide, pad 3, pitch 10, gap 4, the plate 4 over the edge: the column is 150 pixels, 25 glyphs
wrapped to nothing: one line of 111 glyphs, 666 pixels wide, from x 32 to 698
the box ends at x 188 and the picture at 192: 516 pixels of the line cross the frame and 506 are off the picture
25 glyphs of 111 are wholly inside the column
the picture 192 by 128 pixels, ff57ee113dfadaa38a0f20fc4886510e4e8bf58376c606e02d30cd0c62855318
wrote assets/frames/panel-nowrap.png at 4 times, 768 by 512
assets/frames/panel-nowrap.png: the box, the slot, the plate, and
a hundred and eleven glyphs drawn as one line. Twenty-five of them are inside
the column; the rest cross the frame and leave the picture.
The symptom is on the figure and in the third line of the run: one line of a
hundred and eleven glyphs, six hundred and sixty-six pixels wide, starting at
x 32 and ending at 698 on a picture that ends at 192. Five hundred and
six pixels of it are drawn to nowhere, the clip in drawGlyph throwing
each of them away, and before that the line crosses the panel's right-hand frame,
because the clip was the whole picture and nothing between DrawTextIn
and the frame knew the frame was there. Work back from the symptom. The pen
advanced six pixels a byte and was never asked whether it had room; the font can
measure a string, TextWidth has been there since chapter 21, and no
caller measured. Narrowing the clip to the box's inside is the obvious fix and
it is not one: the line then stops at the frame instead of crossing it, and
eighty-six of the hundred and eleven glyphs are still never seen. A clip decides
what is drawn. The question here is what is said, and that has to be
decided before the first glyph goes down, by measuring the column and cutting
the string to fit it.
The cut is at spaces, because a space is the one place in prose where a break costs nothing. The wrapper walks the words and keeps a line; a word joins the line if the line, a space and the word together are no wider than the column, measured with the font's own widths, and starts the next line otherwise. That leaves one case: a word wider than the column on its own, which fits on no line and would otherwise stall the loop. It is cut at the last glyph that fits, and the rest of it starts the next line, cut again if it has to be. The alternative, letting it run past the column for the clip to take, puts the end of the word where nobody can read it; a word broken across two lines with nothing inserted at the break is still every one of its letters, in order. The wrapper inserts no hyphen, because a hyphen is a byte the string did not have and the text drawn is the text given.
Create internal/render/wrap.go. It is a method on Font
because the column is in pixels and only the font knows how wide a string comes
out; a wrapper that counted characters would be right for this font and wrong for
the first proportional one. Column is the count the wrapper cannot
exceed on any line, and Tall is read off the sheet rather than
written down beside the cell size: it walks the cells from the bottom row up and
stops at the first row with ink on it, which for chapter 21's sheet is the seventh
of eight.
// internal/render/wrap.go
package render
import "strings"
// Wrap breaks s into lines no wider than width pixels, measured with
// this font's own widths. Words are what spaces separate, and a line
// breaks only at a space: a word is put on the current line if the line
// with a space and the word still fits, and starts the next line if it
// does not. Runs of spaces count as one, and a line never begins or
// ends with one.
//
// A word wider than the column on its own is cut at the last glyph that
// fits, and the rest of it starts the next line, cut again if it has
// to be. The alternative, letting it run past the column, puts glyphs
// where the clip throws them away, and a word with its end cut off is
// a word the reader cannot read; a word broken across two lines is
// still every one of its letters. Nothing is inserted at the break, so
// the text drawn is the text given, byte for byte, in more lines.
//
// A column narrower than one glyph still makes progress: every line
// takes at least one glyph, so the loop cannot stall on a word.
func (f *Font) Wrap(s string, width int) []string {
var lines []string
line := ""
for _, word := range strings.Fields(s) {
for word != "" {
switch {
case line == "" && f.TextWidth(word) <= width:
line = word
word = ""
case line == "":
// Too wide for a line of its own: take what fits.
n := f.fits(word, width)
line, word = word[:n], word[n:]
lines = append(lines, line)
line = ""
case f.TextWidth(line+" "+word) <= width:
line += " " + word
word = ""
default:
lines = append(lines, line)
line = ""
}
}
}
if line != "" {
lines = append(lines, line)
}
return lines
}
// fits is how many leading bytes of word fit in width pixels, and never
// fewer than one.
func (f *Font) fits(word string, width int) int {
n := 1
for n < len(word) && f.TextWidth(word[:n+1]) <= width {
n++
}
return n
}
// Column is how many glyphs of this font stand side by side in width
// pixels: the count the wrapper cannot exceed on any line.
func (f *Font) Column(width int) int { return width / f.Advance }
// Tall is how many rows of a glyph cell the sheet actually inks: the
// cell's height less the blank rows at the bottom of every cell. A
// six-by-eight cell with a gap row under every glyph is seven tall.
// It is read off the sheet, once, and not written down beside the
// cell size, because the sheet is the authority on where its ink is.
func (f *Font) Tall() int {
pix := f.Sheet.Pix
for tall := f.GH; tall > 0; tall-- {
for cell := 0; cell < f.Count; cell++ {
r := f.Glyph(byte(int(f.First) + cell))
y := r.Y0 + tall - 1
for x := r.X0; x < r.X1; x++ {
if pix.At(x, y).A() != 0 {
return tall
}
}
}
}
return 0
}
-mode wrap builds the panel of the next section from the flags,
takes the column it leaves in a box of the width asked for, and prints every
line with its width in glyphs and in pixels.
// cmd/mkpanel/main.go
// wrapMode wraps the words to the column a box w wide leaves and
// prints every line with its width.
func wrapMode(p *render.Panel, w int, text string) {
col := p.Column(w)
f := p.Font
fmt.Printf("mkpanel: a box %d wide, pad %d, a slot of %d with its ring, a gap of %d: the column is %d pixels, %d glyphs of %d\n",
w, p.Pad, p.Slot, p.Gap, col, f.Column(col), f.Advance)
fmt.Printf(" the words: %d bytes, %d words\n", len(text), len(strings.Fields(text)))
lines := f.Wrap(text, col)
fmt.Printf(" wrapped to %d lines, none wider than %d\n\n", len(lines), col)
fmt.Printf(" %-4s %6s %6s %s\n", "line", "glyphs", "pixels", "the line")
for i, l := range lines {
fmt.Printf(" %-4d %6d %6d %s\n", i+1, len(l), f.TextWidth(l), l)
}
fmt.Printf("\n %d lines at a pitch of %d and %d rows of ink on the last: the box is %d tall\n",
len(lines), p.Pitch, f.Tall(), p.Height(w, text))
}
// ...
$ go run ./cmd/mkpanel -mode wrap
mkpanel: a box 184 wide, pad 3, a slot of 16 with its ring, a gap of 4: the column is 150 pixels, 25 glyphs of 6
the words: 111 bytes, 20 words
wrapped to 5 lines, none wider than 150
line glyphs pixels the line
1 24 144 Halla came to Firstlight
2 18 108 before there was a
3 22 132 Firstlight and put her
4 21 126 house on the flattest
5 22 132 ground she could find.
5 lines at a pitch of 10 and 7 rows of ink on the last: the box is 59 tall
Twenty-five glyphs is the column: a box a hundred and eighty-four wide, less six
of inset on each side, less the slot's eighteen and the gap's four, is a hundred
and fifty pixels, and a hundred and fifty is twenty-five sixes with nothing over.
Halla's twenty words go into five lines of it, the widest a hundred and forty-four
pixels, and every break is at a space. The first line could not take
before, because twenty-four glyphs, a space and six more is
thirty-one; the second stops at a because Firstlight is
ten and eighteen, a space and ten is twenty-nine. Five lines at a pitch of ten,
with seven rows of ink on the last, and the box is fifty-nine tall, which is the
number the interlude's second box came from.
$ go run ./cmd/mkpanel -mode wrap -text "and the abcdefghijklmnopqrstuvwxyz is one word"
mkpanel: a box 184 wide, pad 3, a slot of 16 with its ring, a gap of 4: the column is 150 pixels, 25 glyphs of 6
the words: 46 bytes, 6 words
wrapped to 3 lines, none wider than 150
line glyphs pixels the line
1 7 42 and the
2 25 150 abcdefghijklmnopqrstuvwxy
3 13 78 z is one word
3 lines at a pitch of 10 and 7 rows of ink on the last: the box is 39 tall
The alphabet is twenty-six letters and the column is twenty-five, so it is the
word the rule was written for. It does not join the first line, because
and the plus a space plus twenty-six is thirty-four; it does not fit a
line of its own; so it is cut at the twenty-fifth glyph and z starts
the third line, where is one word joins it. Nothing was added, and
one byte was spent: the three lines hold forty-five glyphs, spaces included, and
the forty-sixth byte is the space between the and the alphabet, which
is where the first break was made. A break at a space costs the space; the break
inside the alphabet cost nothing, because there was nothing there to spend.
$ go run ./cmd/mkpanel -mode wrap -sentence 0
mkpanel: a box 184 wide, pad 3, a slot of 16 with its ring, a gap of 4: the column is 150 pixels, 25 glyphs of 6
the words: 217 bytes, 40 words
wrapped to 10 lines, none wider than 150
line glyphs pixels the line
1 24 144 Halla came to Firstlight
2 18 108 before there was a
3 22 132 Firstlight and put her
4 21 126 house on the flattest
5 22 132 ground she could find.
6 25 150 She keeps more grain than
7 23 138 she needs and gives the
8 24 144 difference away, and she
9 25 150 has never once been asked
10 4 24 why.
10 lines at a pitch of 10 and 7 rows of ink on the last: the box is 109 tall
The whole persona is two hundred and seventeen bytes, ten lines at this column, and a box of a hundred and nine pixels to hold them on a ground a hundred and twenty-eight tall: the box would cover the valley from the rim down. The wrapper is doing its job, and the number is the measurement of what a spoken line can be at this width: a persona is authored prose in the register of the founding document, and a line said from a face is one breath, which at twenty-five glyphs a line is three lines and seventy-five glyphs at the most. The panel draws whatever it is given and grows to fit it; what it is given is a decision made by whoever writes the words, with this table in front of them.
Create internal/render/wrap_test.go. The font under test is built in
memory with chapter 21's cell and a glyph inked to the seventh row in every cell,
so the tests need no file. The first wraps Halla's sentence at every column from
one glyph to the whole sentence and holds three things at each: no line wider
than the column, every glyph present in order, and no line that had room for the
first word of the next and did not take it. The second is the alphabet, at
twenty-five glyphs, at one, and at a column of nothing, which still gives
twenty-six lines because every line takes a glyph. The third reads seven off the
sheet and eight after one pixel is put on the gap row.
$ go test -count=1 ./internal/render/ -run 'TestWrapBreaksOnSpacesAndNeverPastTheColumn|TestAWordWiderThanTheColumnIsCutAtTheLastGlyphThatFits|TestTallIsReadOffTheSheet' -v
=== RUN TestWrapBreaksOnSpacesAndNeverPastTheColumn
--- PASS: TestWrapBreaksOnSpacesAndNeverPastTheColumn (0.00s)
=== RUN TestAWordWiderThanTheColumnIsCutAtTheLastGlyphThatFits
--- PASS: TestAWordWiderThanTheColumnIsCutAtTheLastGlyphThatFits (0.00s)
=== RUN TestTallIsReadOffTheSheet
--- PASS: TestTallIsReadOffTheSheet (0.00s)
PASS
ok theworld/internal/render 0.004s
Three pixels of pad and ten of pitch, measured
The panel is the frame, the font, the wrapper and five distances, and the distances are the design. The inset is how far the content stands from the box's edge: the frame's three lines, which the panel reads off the corner size instead of being told twice, and then a pad. The gap is what stands between the slot's ring and the first glyph. The pitch is from the top of one line to the top of the next. The plate lies some pixels over the box's top edge. And the slot is chapter 99's sixteen with a one-pixel ring round it. The pad and the pitch were tried at several values with the same words in the box, by a bench mode that draws them one under the next, and the values the page keeps are the ones that survived being looked at through the inspector at one, two and four pixels to one; the gap and the plate's overlap were set once, at four, the corner's size, and read against the same pictures.
Create internal/render/panel.go. Draw takes the box's
top-left corner and its width and works the height out from the words, because
the words decide the box and not the other way round; it returns the slot's inside
for the caller to draw a face into, since render knows a rectangle and
does not know a person. The words are drawn with the clip narrowed to the inset,
so a wrapper fault cannot reach the frame, and the plate goes down last, over the
top edge, as the same nine pieces at another size.
// internal/render/panel.go
package render
// Panel is the box words are said from: a frame made from a Nine, a
// slot for a portrait on the left, a plate over the top edge carrying
// a name, and lines of text wrapped to the column that is left. It is
// screen space, drawn after the world in raw pixels; nothing in it
// goes through a camera.
//
// Every distance is a whole number of pixels and every one is a design
// decision the page measures: the pad between the frame's inner line
// and anything inside, the gap between the slot and the words, and the
// pitch from one line of text to the next.
type Panel struct {
Frame *Nine // the box, and the plate: one picture at two sizes
Font *Font // the six-by-eight glyphs
Text Color // what the words are drawn in
Pitch int // pixels from the top of one line to the top of the next
Pad int // pixels between the frame's inner line and the content
Slot int // the portrait's side in pixels; 0 draws no slot
Ring Color // the one-pixel line round the slot
Field Color // what the slot is filled with, under the portrait
Gap int // pixels between the slot's ring and the first glyph
Plate int // pixels the plate rises above the box's top edge
}
// Rim is how many pixels of the frame stand between the box's edge and
// the field: the corner less the one field pixel the corner piece
// carries. It is a fact about the picture the frame was drawn as, and
// the panel reads it off the corner instead of being told twice.
func (p *Panel) Rim() int { return p.Frame.C - 1 }
// Inset is the distance from the box's edge to the content: the rim,
// then the pad.
func (p *Panel) Inset() int { return p.Rim() + p.Pad }
// Column is the width in pixels the words may take in a box w wide:
// the box less two insets, the slot with its ring, and the gap.
func (p *Panel) Column(w int) int {
col := w - 2*p.Inset()
if p.Slot > 0 {
col -= p.Slot + 2 + p.Gap
}
return col
}
// Lines wraps text to the column a box w wide leaves it.
func (p *Panel) Lines(w int, text string) []string {
return p.Font.Wrap(text, p.Column(w))
}
// Height is how tall a box w wide has to be to hold text: two insets
// round whichever is taller, the wrapped lines or the slot with its
// ring. The last line takes the font's inked height and not a whole
// pitch, because the pitch is the distance to a next line and there is
// none, and a cell's blank gap row would sit under the last line as a
// pad the top does not have.
func (p *Panel) Height(w int, text string) int {
lines := p.Lines(w, text)
inner := 0
if len(lines) > 0 {
inner = (len(lines)-1)*p.Pitch + p.Font.Tall()
}
if p.Slot > 0 && p.Slot+2 > inner {
inner = p.Slot + 2
}
return 2*p.Inset() + inner
}
// Draw puts the panel on the buffer with its top-left corner at (x, y)
// and its width w, sized to text, and returns the slot's inside, the
// square the caller draws a portrait into, and the box it drew. The
// box goes down first, then the slot's ring and field, then the words,
// then the plate over the top edge with the name on it; the portrait
// is the caller's, because the panel knows a rectangle and not a
// person. With no name, no plate is drawn.
func (p *Panel) Draw(b *Buffer, x, y, w int, name, text string) (slot, box Rect) {
h := p.Height(w, text)
box = Rect{X0: x, Y0: y, X1: x + w, Y1: y + h}
b.DrawNine(p.Frame, box)
in := p.Inset()
cx := x + in
if p.Slot > 0 {
ring := Rect{X0: cx, Y0: y + in, X1: cx + p.Slot + 2, Y1: y + in + p.Slot + 2}
b.FillRect(ring, p.Ring)
slot = Rect{X0: ring.X0 + 1, Y0: ring.Y0 + 1, X1: ring.X1 - 1, Y1: ring.Y1 - 1}
b.FillRect(slot, p.Field)
cx = ring.X1 + p.Gap
}
was := b.Clip()
b.SetClip(Rect{X0: x + in, Y0: y + in, X1: x + w - in, Y1: y + h - in}.Intersect(was))
ty := y + in
for _, line := range p.Lines(w, text) {
b.DrawTextIn(p.Font, cx, ty, line, p.Text)
ty += p.Pitch
}
b.SetClip(was)
if name != "" {
p.plate(b, x+in, y+p.Plate-p.PlateHeight(), name)
}
return slot, box
}
// PlateHeight is how tall the plate is: the font's inked rows and two
// insets, the same inset the box keeps.
func (p *Panel) PlateHeight() int { return 2*p.Inset() + p.Font.Tall() }
// plate draws the name in a second box from the same frame: as wide as
// the name and two insets, as tall as the font's ink and two insets,
// with its top-left corner at (x, y). It is the same nine pieces at another
// size, and nothing about the frame knows which box it is making.
func (p *Panel) plate(b *Buffer, x, y int, name string) Rect {
in := p.Inset()
box := Rect{X0: x, Y0: y, X1: x + 2*in + p.Font.TextWidth(name), Y1: y + p.PlateHeight()}
b.DrawNine(p.Frame, box)
b.DrawTextIn(p.Font, x+in, y+in, name, p.Text)
return box
}
Height gives the last line its inked rows and not a whole pitch,
for a reason the plate makes visible: a glyph cell is eight tall and the eighth
row is blank, so a box that counted the cell would carry one more pixel of pad
under the words than over them, and on a plate three pixels of pad the extra one
shows. The slot sets a floor on the height, so a box with one line of words is
still tall enough for the face, and with no words at all it is the slot and the
insets, which is the box the failure above drew.
-mode fit is the table: for each pad, the inset, the column in pixels
and in glyphs, the lines Halla's sentence needs, and the box's height at four
pitches. -mode pads and -mode pitches draw the rows of
that table, one panel under the next, at three pixels to one.
$ go run ./cmd/mkpanel -mode fit
mkpanel: a box 184 wide holding 111 bytes in 20 words, at a slot of 16 and a gap of 4
pad inset column glyphs lines box height at a pitch of 8, 9, 10, 12
1 4 154 25 5 47 51 55 63
2 5 152 25 5 49 53 57 65
3 6 150 25 5 51 55 59 67
4 7 148 24 5 53 57 61 69
5 8 146 24 5 55 59 63 71
a glyph is 6 wide and 8 tall with 7 rows of ink; at a pitch of 8 one blank row stands between lines, at 10 three, at 12 five
$ go run ./cmd/mkpanel -mode pads -shot assets/frames/panel-pads.png -scale 3
mkpanel: Halla's words in a box 184 wide at a pitch of 10, once at each pad from 1 to 5
pad 1: the box 184 by 55 at 4,15, 5 lines
pad 2: the box 184 by 57 at 4,87, 5 lines
pad 3: the box 184 by 59 at 4,163, 5 lines
pad 4: the box 184 by 61 at 4,243, 5 lines
pad 5: the box 184 by 63 at 4,327, 5 lines
the card 192 by 394 pixels, 1d6ce9bf5215e17fedabfe3c79c39a4b787fec80ef869a643579c54b662e778d
wrote assets/frames/panel-pads.png at 3 times, 576 by 1182
assets/frames/panel-pads.png: pads 1 to 5, top to bottom, at three
pixels to one. At pad 1 the plate's bottom line lands on the slot's ring; at pad
3 there are two pixels between them, and the glyphs stand three pixels off the
inner line, which is half a glyph's width.
Pad 3 is kept. At pad 1 the inset is four, the plate, which lies four pixels over the edge, ends exactly where the slot's ring begins, and the first glyph of every line of glyphs stands one pixel under the rock-m line, which is the same gap as between two glyphs, so the frame reads as one more row of letters. Pad 2 clears the ring by a pixel and the glyphs by two. Pad 3 puts two pixels between plate and ring and three between line and glyph, half a glyph's width, and the column is still twenty-five glyphs. Pads 4 and 5 lose a glyph from the column, twenty-four, for a margin nobody asked for, and cost two and four pixels of height on every box.
$ go run ./cmd/mkpanel -mode pitches -shot assets/frames/panel-pitches.png -scale 3
mkpanel: Halla's words in a box 184 wide at a pad of 3, once at each pitch of 8, 9, 10 and 12
pitch 8: the box 184 by 51 at 4,19, 5 lines
pitch 9: the box 184 by 55 at 4,89, 5 lines
pitch 10: the box 184 by 59 at 4,163, 5 lines
pitch 12: the box 184 by 67 at 4,241, 5 lines
the card 192 by 312 pixels, 8a72deab3d6ba59ea6371049f5392f34fc8244e777ccc707086ba359f2c8712a
wrote assets/frames/panel-pitches.png at 3 times, 576 by 936
assets/frames/panel-pitches.png: pitches 8, 9, 10 and 12, top to
bottom. The glyph is seven rows of ink in a cell of eight, so the pitch less
seven is the blank rows between lines: one, two, three and five.
Pitch 10 is kept, and it is the pitch chapter 21's readout has used since it was written. At 8 the blank row between lines is one, the same as the blank column between glyphs, and the block reads as a grid of letters and not as lines; at 9 it is two, and the lines are lines, but at one pixel to one the eye still has to find them; at 10 the three blank rows are close to half a glyph's height and the lines read at one to one; at 12 five rows cost eight pixels over five lines and the words start to float. The whole cost of the choice is on the last column of the table: fifty-nine pixels for five lines at 10 against fifty-one at 8, and a box eight pixels taller is the price of a box that reads.
-mode contrast prints how far apart each pair of colours that touch
on the panel are, channel by channel, which is the measurement chapter 21 made
when bone landed on gold and the water count vanished.
$ go run ./cmd/mkpanel -mode contrast
mkpanel: pairs of colours that meet on the panel, and how far apart they are in red, green and blue
one other r g b where they meet
F5EBD7 243E6B 209 173 108 the words on the field
5A6470 243E6B 54 38 5 the lit inner line on the field
323847 243E6B 14 6 36 the shaded inner line on the field
F5EBD7 5A6470 155 135 103 the lit outer line on the lit inner line
8A979E 323847 88 95 87 the shaded outer line on the shaded inner line
181A29 3D7799 37 93 112 a face's outline on the slot's field
181A29 243E6B 12 36 66 a face's outline on the box's field, had the slot no field of its own
5A6470 3D7799 29 19 41 the slot's ring on the slot's field
5A6470 243E6B 54 38 5 the slot's ring on the box's field
181A29 6B5A3E 83 64 21 the outer line on the painter's soil
181A29 4A90D2 50 118 169 the outer line on the painter's water
181A29 4C7841 52 94 24 the outer line on a plant's midtone
The first line is the one the panel exists for: bone on water-d differs by 209, 173 and 108, and the readout's shadow, drawn under every glyph in chapter 21 because the world behind the words could be anything, is not needed here, because the field behind the words is the panel's own and is one flat colour. The sixth and seventh lines are why the slot has a field. A face's ink outline on the box's field would differ by 12, 36 and 66; on the water midtone it differs by 37, 93 and 112, and the face has an edge. The fourth and fifth are the bevel: the lit outer line stands off the lit inner one by 155, 135, 103, and the shaded pair are 88, 95, 87 apart, and then the shaded inner line, rock-d, is 14, 6 and 36 from the field, which is to say it falls into the field, and that is what a shadow does. The last three are the outer line on the three grounds the panel stands on in the figure below: never closer than 21 in every channel, and on water never closer than 50.
The rest of cmd/mkpanel/main.go: the panel with its distances off
the flags, the words from the roll or the committed stream, the ground, and
-mode panel. The ground is the village bench's own: seed 1, the same
air and soil, taken to the boundary the village is founded at and painted by
volume 6's painter, so the picture is of the ground the volume runs on and not of
a flat field. Nobody stands on it, because nothing in this module draws a body on
the map. The words are the first sentence of Halla's persona, which is authored
prose in the founding register and the only in-world text there is to say; no
model is asked for a line and no line on this page came from one.
// cmd/mkpanel/main.go
// font loads the six-by-eight glyphs.
func font() *render.Font {
f, err := render.LoadFont(Font, 6, 8, ' ')
if err != nil {
die(err)
}
return f
}
// panel is the panel with its distances off the flags.
func panel(pad, pitch, gap, plate int) *render.Panel {
return &render.Panel{
Frame: nine(),
Font: font(),
Text: gene.Sixteen[Words],
Pitch: pitch,
Pad: pad,
Slot: village.Cell,
Ring: gene.Sixteen[SlotRing],
Field: gene.Sixteen[SlotField],
Gap: gap,
Plate: plate,
}
}
// ...
// person finds one villager in the shipped roll.
func person(who string) village.Person {
roll, err := village.Read()
if err != nil {
die(err)
}
for _, p := range roll.People {
if p.Name == who {
return p
}
}
die(fmt.Errorf("nobody called %q in the roll", who))
return village.Person{}
}
// words is what the panel says: the text given, a memory of the
// committed stream, or the villager's own persona, whole or one
// sentence of it. A sentence ends at a full stop followed by a space
// or the end.
func words(who, text string, sentence, memory int) string {
if text != "" {
return text
}
if memory != 0 {
rows, err := exemplars.Stream()
if err != nil {
die(err)
}
for _, m := range rows {
if int(m.ID) == memory {
return m.Text
}
}
die(fmt.Errorf("no memory %d in the committed stream", memory))
}
persona := person(who).Persona
if sentence == 0 {
return persona
}
sentences := sentencesOf(persona)
if sentence > len(sentences) {
die(fmt.Errorf("%s's persona has %d sentences, not %d", who, len(sentences), sentence))
}
return sentences[sentence-1]
}
// sentencesOf cuts prose at every full stop that ends a sentence.
func sentencesOf(s string) []string {
var out []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '.' && (i+1 == len(s) || s[i+1] == ' ') {
out = append(out, strings.TrimSpace(s[start:i+1]))
start = i + 1
}
}
if rest := strings.TrimSpace(s[start:]); rest != "" {
out = append(out, rest)
}
return out
}
// ...
// ground is the valley the panel is drawn over: the village bench's
// ground from its seed, taken to the boundary the village is founded
// at, painted by volume 6's painter. Nobody stands on it: nothing in
// this module draws a body on the map.
func ground(seed uint64) (*render.Buffer, *terra.Valley) {
g := sim.Generate(12, 8, seed)
a := terra.NewAir(scene.Wind(g), air, g.Count(sim.Soil)*90, seed)
v := terra.NewValley(g, seed, soil, 12, a)
v.Fall, v.Sun, v.Decay = 0.60, 0.05, 0.01
v.Wear = 1
v.Now = terra.Quarter / 2
v.Found(3)
for {
v.Tick()
if v.Now%terra.Year == terra.Quarter+1 {
break
}
}
return scene.Paint(v), v
}
// panelMode draws the panel over the valley, a villager's face in the
// slot and the name on the plate, and writes it out scaled. Behind the
// flag the words are drawn as one line, wrapped to nothing, from where
// the first line would start, with the clip left at the whole picture:
// what a panel is without a wrapper.
func panelMode(shot string, scale int, p *render.Panel, w int, who, text string, nowrap bool, seed uint64) {
sh, err := render.LoadSheet(Sheet, village.Cell)
if err != nil {
die(err)
}
pic, v := ground(seed)
per := person(who)
const edge = 4
x := (pic.W - w) / 2
h := p.Height(w, text)
if nowrap {
h = 2*p.Inset() + p.Slot + 2
}
y := pic.H - edge - h
fmt.Printf("mkpanel: the ground from seed %d at tick %d, %d by %d pixels, %d plants standing\n",
seed, v.Now, pic.W, pic.H, len(v.Stands))
fmt.Printf(" %s, home %d,%d, the face off the row; the words: %d bytes, %d words\n",
per.Name, per.Home.X, per.Home.Y, len(text), len(strings.Fields(text)))
fmt.Printf(" a box %d wide, pad %d, pitch %d, gap %d, the plate %d over the edge: the column is %d pixels, %d glyphs\n",
w, p.Pad, p.Pitch, p.Gap, p.Plate, p.Column(w), p.Font.Column(p.Column(w)))
if nowrap {
// The box is drawn for the slot alone, and the words go down
// in one line with nothing measuring them.
box := render.Rect{X0: x, Y0: y, X1: x + w, Y1: y + h}
slot, _ := p.Draw(pic, x, y, w, per.Name, "")
village.FaceOf(per).Draw(pic, sh, slot.X0, slot.Y0)
start := slot.X1 + 1 + p.Gap
end := pic.DrawTextIn(p.Font, start, y+p.Inset(), text, p.Text)
wide := p.Font.TextWidth(text)
fmt.Printf(" wrapped to nothing: one line of %d glyphs, %d pixels wide, from x %d to %d\n", len(text), wide, start, end)
fmt.Printf(" the box ends at x %d and the picture at %d: %d pixels of the line cross the frame and %d are off the picture\n",
box.X1, pic.W, end-(box.X1-p.Inset()), end-pic.W)
fmt.Printf(" %d glyphs of %d are wholly inside the column\n", p.Column(w)/p.Font.Advance, len(text))
} else {
lines := p.Lines(w, text)
slot, box := p.Draw(pic, x, y, w, per.Name, text)
village.FaceOf(per).Draw(pic, sh, slot.X0, slot.Y0)
fmt.Printf(" %d lines; the box %d by %d at %d,%d, the slot at %d,%d, the plate %d wide\n",
len(lines), box.X1-box.X0, box.Y1-box.Y0, box.X0, box.Y0, slot.X0, slot.Y0,
2*p.Inset()+p.Font.TextWidth(per.Name))
for i, l := range lines {
fmt.Printf(" %d %s\n", i+1, l)
}
}
fmt.Printf(" the picture %d by %d pixels, %s\n", pic.W, pic.H, pic.Hash())
save(pic, shot, scale)
}
// ...
$ go run ./cmd/mkpanel -mode panel -shot assets/frames/panel-halla.png
mkpanel: the ground from seed 1 at tick 901, 192 by 128 pixels, 11 plants standing
Halla, home 8,5, the face off the row; the words: 111 bytes, 20 words
a box 184 wide, pad 3, pitch 10, gap 4, the plate 4 over the edge: the column is 150 pixels, 25 glyphs
5 lines; the box 184 by 59 at 4,65, the slot at 11,72, the plate 42 wide
1 Halla came to Firstlight
2 before there was a
3 Firstlight and put her
4 house on the flattest
5 ground she could find.
the picture 192 by 128 pixels, 251258b4343ba2438a6a623d50b11f9c7f79bd260c87d9eb2fcc59c47e4d36f3
wrote assets/frames/panel-halla.png at 4 times, 768 by 512
assets/frames/panel-halla.png: the box, a hundred and eighty-four by
fifty-nine, over the valley at four pixels to one. The plate is the same frame at
forty-two by nineteen; the face is chapter 99's, composed off her row into the
slot the panel handed back; the five lines are the wrapper's, at twenty-five
glyphs and a pitch of ten. Every pixel of the panel is one of the valley's
sixteen.
The bar every asset in this book is held to is chapter 17's, and it applies to a box as much as to a walker: would you take a screenshot of this world without being asked to. The panel has to earn that on a ground a hundred and ninety-two pixels wide, where five lines of a sentence cover half the valley, and what it has going for it is measured, not felt. Its edge stands off soil by 83, 64, 21 and off water by 50, 118, 169. Its words stand off their field by more than a hundred in every channel and stand three pixels off the frame. Its face has an edge because the slot's field was chosen for the face's outline and not for the words. And its corners are the same four pixels on the plate as on the box, because a corner is drawn once. What it costs is the five lines: at this width a breath is three, and a sentence of a persona is not a breath.
Create internal/render/panel_test.go. The first holds the height to
its arithmetic: an inset of six, a column of a hundred and fifty, five lines for
the sentence, fifty-nine for the box, and the slot's floor for one line or none.
The second draws the panel twice, once with the words in the field's own colour so
that they change nothing, and checks that every pixel the words changed lies
inside the inset, that the slot is where Draw says, and that the plate
is pixel for pixel a box of its size drawn alone.
$ go test -count=1 ./internal/render/ -run 'TestTheBoxIsAsTallAsTheWordsNeed|TestNothingIsDrawnOutsideTheInset' -v
=== RUN TestTheBoxIsAsTallAsTheWordsNeed
--- PASS: TestTheBoxIsAsTallAsTheWordsNeed (0.00s)
=== RUN TestNothingIsDrawnOutsideTheInset
--- PASS: TestNothingIsDrawnOutsideTheInset (0.00s)
PASS
ok theworld/internal/render 0.004s
Why words set the box size
Two measurements run in opposite directions through this page, and the whole design is that neither one is skipped. The column is measured from the box: a width, less insets, a slot and a gap, in pixels, by the font. The box is measured from the words: the wrapper cuts them to that column, counts the lines, and the height is lines, a pitch and an inset apiece. Draw either without the other and the result is the failure box or a box with empty field under two lines of words. Chapter 21's readout could skip both because its three lines were ten characters each by construction; a persona is not, and a line a villager says is not, so the measuring moved into code, where it is the same for every string.
The frame underneath is chapter 19's tile map turned inward. A map was a small set
of cells addressed by arithmetic and laid down in a grid as large as the world; a
nine-slice is nine cells addressed by the same arithmetic and laid down in a grid
as large as the box, with the one difference that the first and last column and
row are fixed and only the middle repeats. Nothing in DrawNine knows it
is drawing a frame. It knows nine rectangles and a step, and it would make a box
of shadow, of parchment or of water from any twelve-square picture with the same
four-pixel cut, which is what lets the frame be data in assets/ and the
blit be code in render, on either side of the same seam every sheet in
this book has crossed.
What the design costs is on the page too. Six hundred and ninety blits of sixteen
pixels each for a box of ten thousand eight hundred and fifty-six, which is every
pixel of the box written once and the clip consulted six hundred and ninety times,
and a client that draws the panel every frame pays it every frame; the frame is
small, the box is not, and the cost is the box's. A word wider than the column is
cut plainly, so Firstlight in a column of nine glyphs comes out as
Firstligh and t, which is legible and ugly, and the page
prefers ugly and legible to a hyphen the text did not contain. The font has one
case, so the panel speaks in capitals whatever is typed at it; the wrapper measures
bytes and the fold to capitals happens in Glyph, so the widths are the
same either way. And a box is only ever as wide as it is told: on a ground a
hundred and ninety-two pixels across, a hundred and eighty-four is the widest a
box can be with any ground showing beside it, twenty-five glyphs is the column that
width buys, and a line meant to be said from a face is written to that column or it
is not one line.
- Cut a frame of a stated size at a stated corner into its nine source rectangles on paper, and say which piece a given pixel of the picture is in.
- Work out, for a box of any width and height, how many times each edge and the
middle are copied and how much of the last copy the clip keeps, and check the
counts against
-mode slices. - Say why the edges are repeated rather than stretched, with the arithmetic of what a one-pixel mark on a four-pixel edge becomes each way.
- Say why the field and the slot's ring are not ink, and give the two channel differences that decided the slot's field.
- Wrap a sentence to a column by hand, breaking at spaces, and say what the wrapper does with a word wider than the column and why it inserts nothing at the cut.
- Say what pitch 8 does to five lines of a seven-row glyph, what pitch 10 costs in pixels over it, and where the readout has used ten since.
Exercise 1 — Ander, saying a memory. Memory 12 of the
committed stream is Ander's: took 40 parcels from Halla, 20.0
grams, thirty-eight bytes. Work out the lines and the box's height before
running -mode panel -who Ander -memory 12.
Twenty-five glyphs a line: took 40 parcels from is twenty, and
Halla, would make twenty-seven, so it starts the second line with
20.0 grams. Two lines is one pitch and seven rows of ink, seventeen,
plus two insets of six, twenty-nine; the slot's floor is eighteen and two
insets, thirty, and the floor wins, so the box is thirty tall and the second
line sits level with the bottom of the face. The plate is forty-two wide for
the second time, because Ander is five letters like
Halla.
$ go run ./cmd/mkpanel -mode panel -who Ander -memory 12
mkpanel: the ground from seed 1 at tick 901, 192 by 128 pixels, 11 plants standing
Ander, home 9,5, the face off the row; the words: 38 bytes, 7 words
a box 184 wide, pad 3, pitch 10, gap 4, the plate 4 over the edge: the column is 150 pixels, 25 glyphs
2 lines; the box 184 by 30 at 4,94, the slot at 11,101, the plate 42 wide
1 took 40 parcels from
2 Halla, 20.0 grams
the picture 192 by 128 pixels, f3ec6ab0474ae2a107567598de2b3f26f4f25c1b356dcb6423b944756774c8d3
Exercise 2 — the smallest box. Run -mode slices
-w 8 -h 8 and say, before you do, which pieces are put down and how
many blits a box of exactly two corners costs. Then say what a box seven wide
draws.
Four blits. Two corners of four leave nothing, so every edge's run and the
middle are zero pixels wide or tall, and the loop over them puts down nothing;
the four corners are copied once each and meet, so the box is four corners and
no field at all. Seven wide is under two corners, and DrawNine
returns before it draws anything, instead of laying two corners over each
other by a pixel; the second test in stage 7 holds exactly that for a box
seven wide.
$ go run ./cmd/mkpanel -mode slices -w 8 -h 8
mkpanel: a 12x12 frame cut at 4, making a box 8 by 8
piece from to put down
top left 0,0 4x4 0,0 4x4 once, 4x4
top middle 4,0 4x4 4,0 0x4 nowhere: the corners leave it no room
top right 8,0 4x4 4,0 4x4 once, 4x4
middle left 0,4 4x4 0,4 4x0 nowhere: the corners leave it no room
middle middle 4,4 4x4 4,4 0x0 nowhere: the corners leave it no room
middle right 8,4 4x4 4,4 4x0 nowhere: the corners leave it no room
bottom left 0,8 4x4 0,4 4x4 once, 4x4
bottom middle 4,8 4x4 4,4 0x4 nowhere: the corners leave it no room
bottom right 8,8 4x4 4,4 4x4 once, 4x4
the box is 8 pixels wide: two corners of 4 leave 0, which is 0 tiles of 4
and 8 tall: two corners leave 0, which is 0 tiles of 4
0 middle tiles, 0 edge tiles, 4 corners: 4 blits for a box of 64 pixels, from a picture of 144
Exercise 3 — a column of nine. Run -mode wrap
-w 88. Say first how wide the column is in glyphs, which word of Halla's
sentence is cut and where, and whether the box fits on the ground.
Eighty-eight less twelve of inset less eighteen of slot less four of gap is
fifty-four pixels, nine glyphs. Firstlight is ten, so it is cut
both times it occurs, at the ninth glyph, and t starts the next
line with whatever follows it; every other word fits a line of its own.
Fourteen lines at a pitch of ten with seven rows of ink on the last is a
hundred and thirty-seven, and with two insets the box is a hundred and
forty-nine tall, twenty-one more than the ground. The panel does not refuse
it; drawn at the bottom of the picture its top would start above the picture,
and the clip would take the first two lines. A box taller than the ground is a
measurement the caller has to make before asking for it, and the height is
there to be asked for.
$ go run ./cmd/mkpanel -mode wrap -w 88
mkpanel: a box 88 wide, pad 3, a slot of 16 with its ring, a gap of 4: the column is 54 pixels, 9 glyphs of 6
the words: 111 bytes, 20 words
wrapped to 14 lines, none wider than 54
line glyphs pixels the line
1 5 30 Halla
2 7 42 came to
3 9 54 Firstligh
4 8 48 t before
5 9 54 there was
6 1 6 a
7 9 54 Firstligh
8 9 54 t and put
9 9 54 her house
10 6 36 on the
11 8 48 flattest
12 6 36 ground
13 9 54 she could
14 5 30 find.
14 lines at a pitch of 10 and 7 rows of ink on the last: the box is 149 tall
A face in a slot, a name on a plate, and a sentence wrapped to twenty-five glyphs a line: the box exists, and what is in it is the first sentence of a persona, because that is the only in-world text a villager has that is meant to be read. The console has no verb that puts a person's words in front of a villager, a stream of any villager holds no row of the heard kind, and the model that would answer is not called on this page; the box was measured for a breath, and nothing in the module has one to give it.