135 lines
3.0 KiB
Go
135 lines
3.0 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
|
|
|
|
// The line as the eye reads it: top of a digit down to the baseline. The
|
|
// cell is taller at both ends, holding accent space nothing here uses and
|
|
// descenders that hang past the line without being read as part of it, so
|
|
// laying out by the cell puts more air around text than around a box.
|
|
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
|
|
}
|