Files
cabletest/text.go
T

137 lines
3.1 KiB
Go

package main
import (
"embed"
"fmt"
"image"
"image/draw"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
)
//go:embed font/AtkinsonHyperlegibleMono-Regular.ttf font/AtkinsonHyperlegibleMono-Bold.ttf
var fontFS embed.FS
type glyph struct {
// Coverage values, cellW*cellH, origin at the cell's top left.
cov []uint8
w, h int
}
type textFace struct {
face font.Face
cellW int
cellH int
ascent int
cache map[rune]*glyph
// Where a line of text starts and stops as far as the eye is concerned: the
// top of a digit or capital, down to the baseline. The cell is taller at
// both ends, reserving space above for accents nothing here uses and below
// for descenders, which hang past the line without being read as part of
// it. Laying out by the cell therefore puts visibly more air around text
// than around a bordered box the same distance away.
capTop int
lineH int
}
// Digits carry no ascender or descender, so the first row one marks is the top
// of the line and the baseline is the bottom.
func (t *textFace) measureLine() error {
g := t.glyph('0')
for y := 0; y < g.h; y++ {
for x := 0; x < g.w; x++ {
if g.cov[y*g.w+x] != 0 {
t.capTop, t.lineH = y, t.ascent-y
return nil
}
}
}
return fmt.Errorf("font rasterised no ink for a digit")
}
func loadFace(bold bool, sizePx float64) (*textFace, error) {
name := "font/AtkinsonHyperlegibleMono-Regular.ttf"
if bold {
name = "font/AtkinsonHyperlegibleMono-Bold.ttf"
}
raw, err := fontFS.ReadFile(name)
if err != nil {
return nil, err
}
parsed, err := opentype.Parse(raw)
if err != nil {
return nil, err
}
face, err := opentype.NewFace(parsed, &opentype.FaceOptions{
Size: sizePx,
DPI: 72,
})
if err != nil {
return nil, err
}
m := face.Metrics()
adv, ok := face.GlyphAdvance('0')
if !ok {
return nil, fmt.Errorf("font has no digit glyphs")
}
t := &textFace{
face: face,
cellW: adv.Ceil(),
cellH: (m.Ascent + m.Descent).Ceil(),
ascent: m.Ascent.Ceil(),
cache: make(map[rune]*glyph),
}
if err := t.measureLine(); err != nil {
return nil, err
}
return t, nil
}
// Rasterises once per rune and keeps the coverage mask, since the same few
// dozen runes are redrawn every frame.
func (t *textFace) glyph(r rune) *glyph {
if g, ok := t.cache[r]; ok {
return g
}
g := &glyph{w: t.cellW, h: t.cellH, cov: make([]uint8, t.cellW*t.cellH)}
dr, mask, maskp, _, ok := t.face.Glyph(
fixed.P(0, t.ascent), r)
if ok {
alpha := image.NewAlpha(image.Rect(0, 0, t.cellW, t.cellH))
draw.DrawMask(alpha, dr, image.NewUniform(image.White.C), image.Point{},
mask, maskp, draw.Src)
for y := 0; y < t.cellH; y++ {
for x := 0; x < t.cellW; x++ {
g.cov[y*t.cellW+x] = alpha.AlphaAt(x, y).A
}
}
}
t.cache[r] = g
return g
}
func (t *textFace) draw(fb *framebuffer, x, y int, s string, c rgb) int {
for _, r := range s {
if r == ' ' {
x += t.cellW
continue
}
g := t.glyph(r)
for gy := 0; gy < g.h; gy++ {
row := gy * g.w
for gx := 0; gx < g.w; gx++ {
if cov := g.cov[row+gx]; cov != 0 {
fb.blend(x+gx, y+gy, c, cov)
}
}
}
x += t.cellW
}
return x
}