new way to fill text (work in progress)

This commit is contained in:
Thomas Friedel 2020-03-22 14:57:03 +01:00
parent 39e9e6400b
commit 30531aaab7
2 changed files with 323 additions and 146 deletions

View file

@ -30,6 +30,7 @@ type Canvas struct {
images map[interface{}]*Image
fonts map[interface{}]*Font
fontCtxs map[fontKey]*frCache
fontTriCache map[*Font]*fontTriCache
shadowBuf []backendbase.Vec
}
@ -143,6 +144,7 @@ func New(backend backendbase.Backend) *Canvas {
images: make(map[interface{}]*Image),
fonts: make(map[interface{}]*Font),
fontCtxs: make(map[fontKey]*frCache),
fontTriCache: make(map[*Font]*fontTriCache),
}
cv.state.lineWidth = 1
cv.state.lineAlpha = 1
@ -479,6 +481,7 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
var total int
oldest := time.Now()
var oldestFontKey fontKey
var oldestFontKey2 *Font
var oldestImageKey interface{}
for src, img := range cv.images {
w, h := img.img.Size()
@ -496,6 +499,15 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
oldestImageKey = nil
}
}
for fnt, cache := range cv.fontTriCache {
total += cache.size()
if cache.lastUsed.Before(oldest) {
oldest = cache.lastUsed
oldestFontKey2 = fnt
oldestFontKey = fontKey{}
oldestImageKey = nil
}
}
if total <= keepSize {
return
}
@ -503,6 +515,8 @@ func (cv *Canvas) reduceCache(keepSize, rec int) {
if oldestImageKey != nil {
cv.images[oldestImageKey].Delete()
delete(cv.images, oldestImageKey)
} else if oldestFontKey2 != nil {
delete(cv.fontTriCache, oldestFontKey2)
} else {
cv.fontCtxs[oldestFontKey].ctx = nil
delete(cv.fontCtxs, oldestFontKey)

439
text.go
View file

@ -9,6 +9,7 @@ import (
"math"
"os"
"time"
"unsafe"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
@ -33,11 +34,27 @@ type frCache struct {
lastUsed time.Time
}
type fontTriCache struct {
cache map[truetype.Index]*Path2D
lastUsed time.Time
}
func (ftc *fontTriCache) size() int {
size := 0
pps := int(unsafe.Sizeof(pathPoint{}))
for _, p := range ftc.cache {
size += len(p.p) * pps
}
return size
}
var zeroes [alphaTexSize]byte
var textImage *image.Alpha
var defaultFont *Font
var baseFontSize = fixed.I(42)
// LoadFont loads a font and returns the result. The font
// can be a file name or a byte slice in TTF format
func (cv *Canvas) LoadFont(src interface{}) (*Font, error) {
@ -102,8 +119,8 @@ func (cv *Canvas) getFRContext(font *Font, size fixed.Int26_6) *frContext {
cv.reduceCache(Performance.CacheSize, 0)
frctx := newFRContext()
frctx.fontSize = k.size
frctx.f = k.font.font
frctx.fontSize = size
frctx.f = font.font
frctx.recalc()
cv.fontCtxs[k] = &frCache{ctx: frctx, lastUsed: time.Now()}
@ -123,126 +140,19 @@ func (cv *Canvas) FillText(str string, x, y float64) {
scale := (scaleX + scaleY) * 0.5
fontSize := fixed.Int26_6(math.Round(float64(cv.state.fontSize) * scale))
// if fontSize > fixed.I(30) {
// cv.fillText2(str, x, y, fontSize)
// return
// }
frc := cv.getFRContext(cv.state.font, fontSize)
fnt := cv.state.font.font
// measure rendered text size
var p fixed.Point26_6
prev, hasPrev := truetype.Index(0), false
var textOffset image.Point
var strWidth, strMaxY int
for i, rn := range str {
idx := fnt.Index(rn)
if idx == 0 {
idx = fnt.Index(' ')
}
advance, bounds, err := frc.glyphMeasure(idx, p)
if err != nil {
continue
}
var kern fixed.Int26_6
if hasPrev {
kern = fnt.Kern(fontSize, prev, idx)
if frc.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
}
if i == 0 {
textOffset.X = bounds.Min.X
}
if bounds.Min.Y < textOffset.Y {
textOffset.Y = bounds.Min.Y
}
if bounds.Max.Y > strMaxY {
strMaxY = bounds.Max.Y
}
p.X += advance + kern
}
strWidth = p.X.Ceil() - textOffset.X
strHeight := strMaxY - textOffset.Y
strWidth, strHeight, textOffset, str := cv.measureTextRendering(str, &x, &y, frc, scale)
if strWidth <= 0 || strHeight <= 0 {
return
}
fstrWidth := float64(strWidth) / scale
fstrHeight := float64(strHeight) / scale
// calculate offsets
if cv.state.textAlign == Center {
x -= float64(fstrWidth) * 0.5
} else if cv.state.textAlign == Right || cv.state.textAlign == End {
x -= float64(fstrWidth)
}
metrics := cv.state.fontMetrics
switch cv.state.textBaseline {
case Alphabetic:
case Middle:
y += (-float64(metrics.Descent)/64 + float64(metrics.Height)*0.5/64) / scale
case Top, Hanging:
y += (-float64(metrics.Descent)/64 + float64(metrics.Height)/64) / scale
case Bottom, Ideographic:
y += -float64(metrics.Descent) / 64 / scale
}
// find out which characters are inside the visible area
p = fixed.Point26_6{}
prev, hasPrev = truetype.Index(0), false
var insideCount int
strFrom, strTo := 0, len(str)
curInside := false
curX := x
for i, rn := range str {
idx := fnt.Index(rn)
if idx == 0 {
idx = fnt.Index(' ')
}
advance, bounds, err := frc.glyphMeasure(idx, p)
if err != nil {
continue
}
var kern fixed.Int26_6
if hasPrev {
kern = fnt.Kern(fontSize, prev, idx)
if frc.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
curX += float64(kern) / 64 / scale
}
w, h := cv.b.Size()
fw, fh := float64(w), float64(h)
p0 := cv.tf(backendbase.Vec{float64(bounds.Min.X)/scale + curX, float64(bounds.Min.Y)/scale + y})
p1 := cv.tf(backendbase.Vec{float64(bounds.Min.X)/scale + curX, float64(bounds.Max.Y)/scale + y})
p2 := cv.tf(backendbase.Vec{float64(bounds.Max.X)/scale + curX, float64(bounds.Max.Y)/scale + y})
p3 := cv.tf(backendbase.Vec{float64(bounds.Max.X)/scale + curX, float64(bounds.Min.Y)/scale + y})
inside := (p0[0] >= 0 || p1[0] >= 0 || p2[0] >= 0 || p3[0] >= 0) &&
(p0[1] >= 0 || p1[1] >= 0 || p2[1] >= 0 || p3[1] >= 0) &&
(p0[0] < fw || p1[0] < fw || p2[0] < fw || p3[0] < fw) &&
(p0[1] < fh || p1[1] < fh || p2[1] < fh || p3[1] < fh)
if inside {
insideCount++
}
if !curInside && inside {
curInside = true
strFrom = i
x = curX
} else if curInside && !inside {
strTo = i
break
}
p.X += advance + kern
curX += float64(advance) / 64 / scale
}
if strFrom == strTo || insideCount == 0 {
return
}
// make sure textImage is large enough for the rendered string
if textImage == nil || textImage.Bounds().Dx() < strWidth || textImage.Bounds().Dy() < strHeight {
var size int
@ -257,9 +167,6 @@ func (cv *Canvas) FillText(str string, x, y float64) {
textImage = image.NewAlpha(image.Rect(0, 0, size, size))
}
curX = x
p = fixed.Point26_6{}
// clear the render region in textImage
for y := 0; y < strHeight; y++ {
off := textImage.PixOffset(0, y)
@ -270,8 +177,10 @@ func (cv *Canvas) FillText(str string, x, y float64) {
}
// render the string into textImage
prev, hasPrev = truetype.Index(0), false
for _, rn := range str[strFrom:strTo] {
curX := x
p := fixed.Point26_6{}
prev, hasPrev := truetype.Index(0), false
for _, rn := range str {
idx := fnt.Index(rn)
if idx == 0 {
prev = 0
@ -301,9 +210,9 @@ func (cv *Canvas) FillText(str string, x, y float64) {
// render textImage to the screen
var pts [4]backendbase.Vec
pts[0] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + y})
pts[1] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + fstrHeight + y})
pts[2] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + fstrWidth + x, float64(textOffset.Y)/scale + fstrHeight + y})
pts[3] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + fstrWidth + x, float64(textOffset.Y)/scale + y})
pts[1] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + x, float64(textOffset.Y)/scale + float64(strHeight) + y})
pts[2] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + float64(strWidth) + x, float64(textOffset.Y)/scale + float64(strHeight) + y})
pts[3] = cv.tf(backendbase.Vec{float64(textOffset.X)/scale + float64(strWidth) + x, float64(textOffset.Y)/scale + y})
mask := textImage.SubImage(image.Rect(0, 0, strWidth, strHeight)).(*image.Alpha)
@ -313,6 +222,50 @@ func (cv *Canvas) FillText(str string, x, y float64) {
cv.b.FillImageMask(&stl, mask, pts)
}
func (cv *Canvas) fillText2(str string, x, y float64, fontSize fixed.Int26_6) {
if cv.state.font == nil {
return
}
frc := cv.getFRContext(cv.state.font, fontSize)
fnt := cv.state.font.font
strWidth, strHeight, _, str := cv.measureTextRendering(str, &x, &y, frc, 1)
if strWidth <= 0 || strHeight <= 0 {
return
}
scale := float64(fontSize) / float64(baseFontSize)
scaleMat := backendbase.MatScale(backendbase.Vec{scale, scale})
prev, hasPrev := truetype.Index(0), false
for _, rn := range str {
idx := fnt.Index(rn)
if idx == 0 {
idx = fnt.Index(' ')
}
if hasPrev {
kern := fnt.Kern(fontSize, prev, idx)
if frc.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
x += float64(kern) / 64
}
advance, _, err := frc.glyphMeasure(idx, fixed.Point26_6{})
if err != nil {
continue
}
path := cv.runePath(rn)
tf := scaleMat.Mul(backendbase.MatTranslate(backendbase.Vec{x, y})).Mul(cv.state.transform)
cv.fillPath(path, tf)
x += float64(advance) / 64
}
}
// StrokeText draws the given string at the given coordinates
// using the currently set font and font height and using the
// current stroke style
@ -324,8 +277,30 @@ func (cv *Canvas) StrokeText(str string, x, y float64) {
frc := cv.getFRContext(cv.state.font, cv.state.fontSize)
fnt := cv.state.font.font
prevPath := cv.path
cv.BeginPath()
strWidth, strHeight, _, str := cv.measureTextRendering(str, &x, &y, frc, 1)
if strWidth <= 0 || strHeight <= 0 {
return
}
// calculate offsets
if cv.state.textAlign == Center {
x -= float64(strWidth) * 0.5
} else if cv.state.textAlign == Right || cv.state.textAlign == End {
x -= float64(strWidth)
}
metrics := cv.state.fontMetrics
switch cv.state.textBaseline {
case Alphabetic:
case Middle:
y += (-float64(metrics.Descent)/64 + float64(metrics.Height)*0.5/64)
case Top, Hanging:
y += (-float64(metrics.Descent)/64 + float64(metrics.Height)/64)
case Bottom, Ideographic:
y += -float64(metrics.Descent) / 64
}
scale := float64(cv.state.fontSize) / float64(baseFontSize)
basetf := backendbase.MatScale(backendbase.Vec{scale, scale}).Mul(cv.state.transform)
prev, hasPrev := truetype.Index(0), false
for _, rn := range str {
@ -346,18 +321,197 @@ func (cv *Canvas) StrokeText(str string, x, y float64) {
continue
}
cv.runePath(rn, backendbase.Vec{x, y})
path := cv.runePath(rn)
tf := backendbase.MatTranslate(backendbase.Vec{x, y}).Mul(basetf)
cv.strokePath(path, tf, false)
x += float64(advance) / 64
}
cv.Stroke()
cv.path = prevPath
}
func (cv *Canvas) runePath(rn rune, pos backendbase.Vec) {
func (cv *Canvas) measureTextRendering(str string, x, y *float64, frc *frContext, scale float64) (int, int, image.Point, string) {
// measure rendered text size
var p fixed.Point26_6
prev, hasPrev := truetype.Index(0), false
var textOffset image.Point
var strWidth, strMaxY int
strMinY := math.MaxInt32
for i, rn := range str {
idx := frc.f.Index(rn)
if idx == 0 {
idx = frc.f.Index(' ')
}
advance, bounds, err := frc.glyphMeasure(idx, p)
if err != nil {
continue
}
var kern fixed.Int26_6
if hasPrev {
kern = frc.f.Kern(frc.fontSize, prev, idx)
if frc.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
}
if i == 0 {
textOffset.X = bounds.Min.X
}
if bounds.Max.Y > strMaxY {
strMaxY = bounds.Max.Y
}
if bounds.Min.Y < strMinY {
strMinY = bounds.Min.Y
}
p.X += advance + kern
}
textOffset.Y = strMinY
strWidth = p.X.Ceil() - textOffset.X
strHeight := strMaxY - textOffset.Y
if strWidth <= 0 || strHeight <= 0 {
return 0, 0, image.Point{}, ""
}
// calculate offsets
if cv.state.textAlign == Center {
*x -= float64(strWidth) / scale * 0.5
} else if cv.state.textAlign == Right || cv.state.textAlign == End {
*x -= float64(strWidth) / scale
}
metrics := cv.state.fontMetrics
switch cv.state.textBaseline {
case Alphabetic:
case Middle:
*y += (-float64(metrics.Descent)/64 + float64(metrics.Height)*0.5/64) / scale
case Top, Hanging:
*y += (-float64(metrics.Descent)/64 + float64(metrics.Height)/64) / scale
case Bottom, Ideographic:
*y += -float64(metrics.Descent) / 64 / scale
}
// find out which characters are inside the visible area
p = fixed.Point26_6{}
prev, hasPrev = truetype.Index(0), false
var insideCount int
strFrom, strTo := 0, len(str)
curInside := false
curX := *x
for i, rn := range str {
idx := frc.f.Index(rn)
if idx == 0 {
idx = frc.f.Index(' ')
}
advance, bounds, err := frc.glyphMeasure(idx, p)
if err != nil {
continue
}
var kern fixed.Int26_6
if hasPrev {
kern = frc.f.Kern(frc.fontSize, prev, idx)
if frc.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
curX += float64(kern) / 64 / scale
}
w, h := cv.b.Size()
fw, fh := float64(w), float64(h)
p0 := cv.tf(backendbase.Vec{float64(bounds.Min.X)/scale + curX, float64(strMinY)/scale + *y})
p1 := cv.tf(backendbase.Vec{float64(bounds.Min.X)/scale + curX, float64(strMaxY)/scale + *y})
p2 := cv.tf(backendbase.Vec{float64(bounds.Max.X)/scale + curX, float64(strMaxY)/scale + *y})
p3 := cv.tf(backendbase.Vec{float64(bounds.Max.X)/scale + curX, float64(strMinY)/scale + *y})
inside := (p0[0] >= 0 || p1[0] >= 0 || p2[0] >= 0 || p3[0] >= 0) &&
(p0[1] >= 0 || p1[1] >= 0 || p2[1] >= 0 || p3[1] >= 0) &&
(p0[0] < fw || p1[0] < fw || p2[0] < fw || p3[0] < fw) &&
(p0[1] < fh || p1[1] < fh || p2[1] < fh || p3[1] < fh)
if inside {
insideCount++
}
if !curInside && inside {
curInside = true
strFrom = i
*x = curX
} else if curInside && !inside {
strTo = i
break
}
p.X += advance + kern
curX += float64(advance) / 64 / scale
}
if strFrom == strTo || insideCount == 0 {
return 0, 0, image.Point{}, ""
}
// if necessary, measure rendered text size again with the substring
if strFrom > 0 || strTo < len(str) {
str = str[strFrom:strTo]
p = fixed.Point26_6{}
prev, hasPrev = truetype.Index(0), false
textOffset = image.Point{}
strWidth, strMaxY = 0, 0
for i, rn := range str {
idx := frc.f.Index(rn)
if idx == 0 {
idx = frc.f.Index(' ')
}
advance, bounds, err := frc.glyphMeasure(idx, p)
if err != nil {
continue
}
var kern fixed.Int26_6
if hasPrev {
kern = frc.f.Kern(frc.fontSize, prev, idx)
if frc.hinting != font.HintingNone {
kern = (kern + 32) &^ 63
}
}
if i == 0 {
textOffset.X = bounds.Min.X
}
if bounds.Min.Y < textOffset.Y {
textOffset.Y = bounds.Min.Y
}
if bounds.Max.Y > strMaxY {
strMaxY = bounds.Max.Y
}
p.X += advance + kern
}
strWidth = p.X.Ceil() - textOffset.X
strHeight = strMaxY - textOffset.Y
if strWidth <= 0 || strHeight <= 0 {
return 0, 0, image.Point{}, ""
}
}
return strWidth, strHeight, textOffset, str
}
func (cv *Canvas) runePath(rn rune) *Path2D {
idx := cv.state.font.font.Index(rn)
if idx == 0 {
idx = cv.state.font.font.Index(' ')
}
if cache, ok := cv.fontTriCache[cv.state.font]; ok {
if path, ok := cache.cache[idx]; ok {
cache.lastUsed = time.Now()
return path
}
}
path := &Path2D{cv: cv, p: make([]pathPoint, 0, 50), standalone: true}
const scale = 1.0 / 64.0
gb := &truetype.GlyphBuf{}
gb.Load(cv.state.font.font, cv.state.fontSize, cv.state.font.font.Index(rn), font.HintingNone)
gb.Load(cv.state.font.font, baseFontSize, idx, font.HintingNone)
from := 0
for _, to := range gb.Ends {
@ -388,14 +542,14 @@ func (cv *Canvas) runePath(rn rune, pos backendbase.Vec) {
}
p0, on0 := gb.Points[from], false
cv.MoveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64)
path.MoveTo(float64(p0.X)*scale, -float64(p0.Y)*scale)
for _, p := range others {
on := p.Flags&0x01 != 0
if on {
if on0 {
cv.LineTo(float64(p.X)/64+pos[0], pos[1]-float64(p.Y)/64)
path.LineTo(float64(p.X)*scale, -float64(p.Y)*scale)
} else {
cv.QuadraticCurveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64, float64(p.X)/64+pos[0], pos[1]-float64(p.Y)/64)
path.QuadraticCurveTo(float64(p0.X)*scale, -float64(p0.Y)*scale, float64(p.X)*scale, -float64(p.Y)*scale)
}
} else {
if on0 {
@ -405,22 +559,31 @@ func (cv *Canvas) runePath(rn rune, pos backendbase.Vec) {
X: (p0.X + p.X) / 2,
Y: (p0.Y + p.Y) / 2,
}
cv.QuadraticCurveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64, float64(mid.X)/64+pos[0], pos[1]-float64(mid.Y)/64)
path.QuadraticCurveTo(float64(p0.X)*scale, -float64(p0.Y)*scale, float64(mid.X)*scale, -float64(mid.Y)*scale)
}
}
p0, on0 = p, on
}
if on0 {
cv.LineTo(float64(start.X)/64+pos[0], pos[1]-float64(start.Y)/64)
path.LineTo(float64(start.X)*scale, -float64(start.Y)*scale)
} else {
cv.QuadraticCurveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64, float64(start.X)/64+pos[0], pos[1]-float64(start.Y)/64)
path.QuadraticCurveTo(float64(p0.X)*scale, -float64(p0.Y)*scale, float64(start.X)*scale, -float64(start.Y)*scale)
}
cv.ClosePath()
cv.Stroke()
path.ClosePath()
from = to
}
cache, ok := cv.fontTriCache[cv.state.font]
if !ok {
cache = &fontTriCache{cache: make(map[truetype.Index]*Path2D, 1024)}
cv.fontTriCache[cv.state.font] = cache
}
cache.lastUsed = time.Now()
cache.cache[idx] = path
return path
}
// TextMetrics is the result of a MeasureText call