zworld/engine/renderapi/font/font.go
2024-01-14 22:56:06 +08:00

181 lines
3.6 KiB
Go

package font
import (
"errors"
"fmt"
"sync"
"zworld/engine/renderapi/color"
"zworld/engine/renderapi/image"
"zworld/engine/util"
"zworld/plugins/math"
"zworld/plugins/math/vec2"
fontlib "golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
var ErrNoGlyph = errors.New("no glyph for rune")
type T interface {
Name() string
Glyph(rune) (*Glyph, error)
Kern(rune, rune) float32
Measure(string, Args) vec2.T
Size() float32
}
type Args struct {
Color color.T
LineHeight float32
}
type font struct {
size float32
scale float32
name string
face fontlib.Face
drawer *fontlib.Drawer
mutex *sync.Mutex
glyphs *util.SyncMap[rune, *Glyph]
kern *util.SyncMap[runepair, float32]
}
type runepair struct {
a, b rune
}
func (f *font) Name() string { return f.name }
func (f *font) Size() float32 { return f.size }
func (f *font) Glyph(r rune) (*Glyph, error) {
if cached, exists := f.glyphs.Load(r); exists {
return cached, nil
}
// grab the font lock
f.mutex.Lock()
defer f.mutex.Unlock()
bounds, advance, ok := f.face.GlyphBounds(r)
if !ok {
return nil, ErrNoGlyph
}
// calculate bearing
bearing := vec2.New(FixToFloat(bounds.Min.X), FixToFloat(bounds.Min.Y))
// texture size
size := vec2.New(FixToFloat(bounds.Max.X), FixToFloat(bounds.Max.Y)).Sub(bearing)
// glyph texture
_, mask, offset, _, _ := f.face.Glyph(fixed.Point26_6{X: 0, Y: 0}, r)
width, height := int(size.X), int(size.Y)
img := &image.Data{
Width: width,
Height: height,
Buffer: make([]byte, 4*width*height),
Format: image.FormatRGBA8Unorm,
}
i := 0
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
// grab alpha value as 16-bit integer
_, _, _, alpha := mask.At(offset.X+x, offset.Y+y).RGBA()
img.Buffer[i+0] = 0xFF // red
img.Buffer[i+1] = 0xFF // green
img.Buffer[i+2] = 0xFF // blue
img.Buffer[i+3] = uint8(alpha >> 8)
i += 4
}
}
scaleFactor := 1 / f.scale
glyph := &Glyph{
key: fmt.Sprintf("glyph:%s:%dx%.2f:%c", f.Name(), int(f.size), f.scale, r),
Size: size.Scaled(scaleFactor),
Bearing: bearing.Scaled(scaleFactor),
Advance: FixToFloat(advance) * scaleFactor,
Mask: img,
}
f.glyphs.Store(r, glyph)
return glyph, nil
}
func (f *font) Kern(a, b rune) float32 {
pair := runepair{a, b}
if k, exists := f.kern.Load(pair); exists {
return k
}
f.mutex.Lock()
defer f.mutex.Unlock()
k := FixToFloat(f.face.Kern(a, b))
f.kern.Store(pair, k)
return k
}
func (f *font) MeasureLine(text string) vec2.T {
size := vec2.Zero
var prev rune
for i, r := range text {
g, err := f.Glyph(r)
if err != nil {
panic("no such glyph")
}
if i > 0 {
size.X += f.Kern(prev, r)
}
if i < len(text)-1 {
size.X += g.Advance
} else {
size.X += g.Bearing.X + g.Size.X
}
size.Y = math.Max(size.Y, g.Size.Y)
prev = r
}
return size.Scaled(f.scale)
}
func (f *font) Measure(text string, args Args) vec2.T {
if args.LineHeight == 0 {
args.LineHeight = 1
}
lines := 1
width := float32(0)
s := 0
for i, c := range text {
if c == '\n' {
line := text[s:i]
// w := f.drawer.MeasureString(line).Ceil()
w := f.MeasureLine(line)
if w.X > width {
width = w.X
}
s = i + 1
lines++
}
}
r := len(text)
if s < r {
line := text[s:]
// w := f.drawer.MeasureString(line).Ceil()
w := f.MeasureLine(line)
if w.X > width {
width = w.X
}
}
lineHeight := int(math.Ceil(f.size * f.scale * args.LineHeight))
height := lineHeight*lines + (lineHeight/2)*(lines-1)
return vec2.New(width, float32(height)).Scaled(1 / f.scale).Ceil()
}
func FixToFloat(v fixed.Int26_6) float32 {
const scalar = 1 / float32(1<<6)
return float32(v) * scalar
}