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 }