109 lines
2.2 KiB
Go
109 lines
2.2 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
|
||
|
|
}
|
||
|
|
|
||
|
|
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")
|
||
|
|
}
|
||
|
|
return &textFace{
|
||
|
|
face: face,
|
||
|
|
cellW: adv.Ceil(),
|
||
|
|
cellH: (m.Ascent + m.Descent).Ceil(),
|
||
|
|
ascent: m.Ascent.Ceil(),
|
||
|
|
cache: make(map[rune]*glyph),
|
||
|
|
}, 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
|
||
|
|
}
|