diff --git a/canvas.go b/canvas.go index 1e3a540..bcc4582 100644 --- a/canvas.go +++ b/canvas.go @@ -27,9 +27,10 @@ type Canvas struct { state drawState stateStack []drawState - images map[interface{}]*Image - fonts map[interface{}]*Font - fontCtxs map[fontKey]*frCache + images map[interface{}]*Image + fonts map[interface{}]*Font + fontCtxs map[fontKey]*frCache + fontTriCache map[*Font]*fontTriCache shadowBuf []backendbase.Vec } @@ -138,11 +139,12 @@ var Performance = struct { // coordinates given here also use the bottom left as origin func New(backend backendbase.Backend) *Canvas { cv := &Canvas{ - b: backend, - stateStack: make([]drawState, 0, 20), - images: make(map[interface{}]*Image), - fonts: make(map[interface{}]*Font), - fontCtxs: make(map[fontKey]*frCache), + b: backend, + stateStack: make([]drawState, 0, 20), + 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) diff --git a/text.go b/text.go index 672a2e1..3b2fd90 100644 --- a/text.go +++ b/text.go @@ -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