mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-26 11:48:55 +01:00
parent
7da65d64be
commit
c581219bb5
345
text/text.go
345
text/text.go
@ -27,7 +27,6 @@ import (
|
|||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
emath "github.com/hajimehoshi/ebiten/internal/math"
|
|
||||||
"github.com/hajimehoshi/ebiten/internal/sync"
|
"github.com/hajimehoshi/ebiten/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,74 +39,14 @@ func now() int64 {
|
|||||||
return monotonicClock
|
return monotonicClock
|
||||||
}
|
}
|
||||||
|
|
||||||
type cacheEntry struct {
|
|
||||||
// Use pointers to avoid copying on browsers.
|
|
||||||
bounds map[rune]*fixed.Rectangle26_6
|
|
||||||
|
|
||||||
atlases map[int]*atlas
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
cache = map[font.Face]*cacheEntry{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type char struct {
|
|
||||||
face font.Face
|
|
||||||
rune rune
|
|
||||||
atlasG int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *char) bounds() *fixed.Rectangle26_6 {
|
|
||||||
e := cache[c.face]
|
|
||||||
if b, ok := e.bounds[c.rune]; ok {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
b, _, _ := c.face.GlyphBounds(c.rune)
|
|
||||||
e.bounds[c.rune] = &b
|
|
||||||
return &b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *char) size() (fixed.Int26_6, fixed.Int26_6) {
|
|
||||||
b := c.bounds()
|
|
||||||
return b.Max.X - b.Min.X, b.Max.Y - b.Min.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *char) empty() bool {
|
|
||||||
x, y := c.size()
|
|
||||||
return x == 0 || y == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *char) atlasGroup() int {
|
|
||||||
if c.atlasG != 0 {
|
|
||||||
return c.atlasG
|
|
||||||
}
|
|
||||||
|
|
||||||
x, y := c.size()
|
|
||||||
w, h := x.Ceil(), y.Ceil()
|
|
||||||
t := w
|
|
||||||
if t < h {
|
|
||||||
t = h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different images for small runes are inefficient.
|
|
||||||
// Let's use a same texture atlas for typical character sizes.
|
|
||||||
if t < 32 {
|
|
||||||
return 32
|
|
||||||
}
|
|
||||||
c.atlasG = emath.NextPowerOf2Int(t)
|
|
||||||
return c.atlasG
|
|
||||||
}
|
|
||||||
|
|
||||||
type glyph struct {
|
|
||||||
char char
|
|
||||||
index int
|
|
||||||
atime int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
|
func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
|
||||||
return float64(x) / (1 << 6)
|
return float64(x) / (1 << 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
cacheLimit = 512 // This is an arbitrary number.
|
||||||
|
)
|
||||||
|
|
||||||
type colorMCacheKey uint32
|
type colorMCacheKey uint32
|
||||||
|
|
||||||
type colorMCacheEntry struct {
|
type colorMCacheEntry struct {
|
||||||
@ -119,7 +58,7 @@ var (
|
|||||||
colorMCache = map[colorMCacheKey]*colorMCacheEntry{}
|
colorMCache = map[colorMCacheKey]*colorMCacheEntry{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
|
func drawGlyph(dst *ebiten.Image, face font.Face, r rune, x, y fixed.Int26_6, clr color.Color) {
|
||||||
// RGBA() is in [0 - 0xffff]. Adjust them in [0 - 0xff].
|
// RGBA() is in [0 - 0xffff]. Adjust them in [0 - 0xff].
|
||||||
cr, cg, cb, ca := clr.RGBA()
|
cr, cg, cb, ca := clr.RGBA()
|
||||||
cr >>= 8
|
cr >>= 8
|
||||||
@ -130,7 +69,12 @@ func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b := g.char.bounds()
|
img := getGlyphImage(face, r)
|
||||||
|
if img == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := getGlyphBounds(face, r)
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.GeoM.Translate(fixed26_6ToFloat64(x+b.Min.X), fixed26_6ToFloat64(y+b.Min.Y))
|
op.GeoM.Translate(fixed26_6ToFloat64(x+b.Min.X), fixed26_6ToFloat64(y+b.Min.Y))
|
||||||
|
|
||||||
@ -139,17 +83,18 @@ func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
|
|||||||
if ok {
|
if ok {
|
||||||
e.atime = now()
|
e.atime = now()
|
||||||
} else {
|
} else {
|
||||||
if len(colorMCache) >= 256 {
|
if len(colorMCache) > cacheLimit {
|
||||||
var oldest colorMCacheKey
|
oldest := int64(math.MaxInt64)
|
||||||
t := int64(math.MaxInt64)
|
oldestKey := colorMCacheKey(0)
|
||||||
for key, e := range colorMCache {
|
for key, c := range colorMCache {
|
||||||
if e.atime < t {
|
if c.atime < oldest {
|
||||||
t = g.atime
|
oldestKey = key
|
||||||
oldest = key
|
oldest = c.atime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(colorMCache, oldest)
|
delete(colorMCache, oldestKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
cm := ebiten.ColorM{}
|
cm := ebiten.ColorM{}
|
||||||
rf := float64(cr) / float64(ca)
|
rf := float64(cr) / float64(ca)
|
||||||
gf := float64(cg) / float64(ca)
|
gf := float64(cg) / float64(ca)
|
||||||
@ -164,164 +109,108 @@ func (g *glyph) draw(dst *ebiten.Image, x, y fixed.Int26_6, clr color.Color) {
|
|||||||
}
|
}
|
||||||
op.ColorM = e.m
|
op.ColorM = e.m
|
||||||
|
|
||||||
a := cache[g.char.face].atlases[g.char.atlasGroup()]
|
_ = dst.DrawImage(img, op)
|
||||||
sx, sy := a.at(g)
|
|
||||||
r := image.Rect(sx, sy, sx+a.glyphSize, sy+a.glyphSize)
|
|
||||||
op.SourceRect = &r
|
|
||||||
|
|
||||||
dst.DrawImage(a.image, op)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type atlas struct {
|
var (
|
||||||
// image is the back-end image to hold glyph cache.
|
fontFaces = map[font.Face]struct{}{}
|
||||||
image *ebiten.Image
|
)
|
||||||
|
|
||||||
// tmpImage is the temporary image as a renderer source for glyph.
|
|
||||||
tmpImage *ebiten.Image
|
|
||||||
|
|
||||||
// glyphSize is the size of one glyph in the cache.
|
|
||||||
// This value is always power of 2.
|
|
||||||
glyphSize int
|
|
||||||
|
|
||||||
runeToGlyph map[rune]*glyph
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *atlas) at(glyph *glyph) (int, int) {
|
|
||||||
if a.glyphSize != glyph.char.atlasGroup() {
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
w, _ := a.image.Size()
|
|
||||||
xnum := w / a.glyphSize
|
|
||||||
x, y := glyph.index%xnum, glyph.index/xnum
|
|
||||||
return x * a.glyphSize, y * a.glyphSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *atlas) maxGlyphNum() int {
|
|
||||||
w, h := a.image.Size()
|
|
||||||
xnum := w / a.glyphSize
|
|
||||||
ynum := h / a.glyphSize
|
|
||||||
return xnum * ynum
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *atlas) appendGlyph(char char, now int64) *glyph {
|
|
||||||
g := &glyph{
|
|
||||||
char: char,
|
|
||||||
atime: now,
|
|
||||||
}
|
|
||||||
if len(a.runeToGlyph) == a.maxGlyphNum() {
|
|
||||||
var oldest *glyph
|
|
||||||
t := int64(math.MaxInt64)
|
|
||||||
for _, g := range a.runeToGlyph {
|
|
||||||
if g.atime < t {
|
|
||||||
t = g.atime
|
|
||||||
oldest = g
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if oldest == nil {
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
idx := oldest.index
|
|
||||||
delete(a.runeToGlyph, oldest.char.rune)
|
|
||||||
|
|
||||||
g.index = idx
|
|
||||||
} else {
|
|
||||||
g.index = len(a.runeToGlyph)
|
|
||||||
}
|
|
||||||
a.runeToGlyph[g.char.rune] = g
|
|
||||||
a.draw(g)
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *atlas) draw(glyph *glyph) {
|
|
||||||
if a.tmpImage == nil {
|
|
||||||
a.tmpImage, _ = ebiten.NewImage(a.glyphSize, a.glyphSize, ebiten.FilterNearest)
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := image.NewRGBA(image.Rect(0, 0, a.glyphSize, a.glyphSize))
|
|
||||||
d := font.Drawer{
|
|
||||||
Dst: dst,
|
|
||||||
Src: image.White,
|
|
||||||
Face: glyph.char.face,
|
|
||||||
}
|
|
||||||
b := glyph.char.bounds()
|
|
||||||
d.Dot = fixed.Point26_6{-b.Min.X, -b.Min.Y}
|
|
||||||
d.DrawString(string(glyph.char.rune))
|
|
||||||
a.tmpImage.ReplacePixels(dst.Pix)
|
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
|
||||||
x, y := a.at(glyph)
|
|
||||||
op.GeoM.Translate(float64(x), float64(y))
|
|
||||||
op.CompositeMode = ebiten.CompositeModeCopy
|
|
||||||
a.image.DrawImage(a.tmpImage, op)
|
|
||||||
|
|
||||||
a.tmpImage.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGlyphFromCache(face font.Face, r rune, now int64) *glyph {
|
|
||||||
ch := char{
|
|
||||||
face: face,
|
|
||||||
rune: r,
|
|
||||||
}
|
|
||||||
at, ok := cache[face].atlases[ch.atlasGroup()]
|
|
||||||
if ok {
|
|
||||||
g, ok := at.runeToGlyph[r]
|
|
||||||
if ok {
|
|
||||||
g.atime = now
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ch.empty() {
|
|
||||||
// The glyph doesn't have its size but might have valid 'advance' parameter
|
|
||||||
// when ch is e.g. space (U+0020).
|
|
||||||
return &glyph{
|
|
||||||
char: ch,
|
|
||||||
atime: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if at == nil {
|
|
||||||
// Don't use ebiten.MaxImageSize here.
|
|
||||||
// It's because the back-end image pixels will be restored from GPU
|
|
||||||
// whenever a new glyph is rendered on the image, and restoring cost is
|
|
||||||
// expensive if the image is big.
|
|
||||||
// The back-end image is updated a temporary image, and the temporary image is
|
|
||||||
// always cleared after used. This means that there is no clue to restore
|
|
||||||
// the back-end image without reading from GPU
|
|
||||||
// (see the package 'restorable' implementation).
|
|
||||||
//
|
|
||||||
// TODO: How about making a new function for 'flagile' image?
|
|
||||||
const size = 1024
|
|
||||||
i, _ := ebiten.NewImage(size, size, ebiten.FilterNearest)
|
|
||||||
at = &atlas{
|
|
||||||
image: i,
|
|
||||||
glyphSize: ch.atlasGroup(),
|
|
||||||
runeToGlyph: map[rune]*glyph{},
|
|
||||||
}
|
|
||||||
cache[face].atlases[ch.atlasGroup()] = at
|
|
||||||
}
|
|
||||||
|
|
||||||
return at.appendGlyph(ch, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqFace(f font.Face) font.Face {
|
func uniqFace(f font.Face) font.Face {
|
||||||
if _, ok := cache[f]; ok {
|
if _, ok := fontFaces[f]; ok {
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
// If the (DeepEqual-ly) same font exists,
|
// If the (DeepEqual-ly) same font exists,
|
||||||
// reuse this to avoid to consume a lot of cache (#498).
|
// reuse this to avoid to consume a lot of cache (#498).
|
||||||
for key := range cache {
|
for key := range fontFaces {
|
||||||
if reflect.DeepEqual(key, f) {
|
if reflect.DeepEqual(key, f) {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache[f] = &cacheEntry{
|
fontFaces[f] = struct{}{}
|
||||||
bounds: map[rune]*fixed.Rectangle26_6{},
|
|
||||||
atlases: map[int]*atlas{},
|
|
||||||
}
|
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Use pointers to avoid copying on browsers.
|
||||||
|
glyphBoundsCache = map[font.Face]map[rune]*fixed.Rectangle26_6{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func getGlyphBounds(face font.Face, r rune) *fixed.Rectangle26_6 {
|
||||||
|
if _, ok := glyphBoundsCache[face]; !ok {
|
||||||
|
glyphBoundsCache[face] = map[rune]*fixed.Rectangle26_6{}
|
||||||
|
}
|
||||||
|
if b, ok := glyphBoundsCache[face][r]; ok {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
b, _, _ := face.GlyphBounds(r)
|
||||||
|
glyphBoundsCache[face][r] = &b
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
type glyphImageCacheEntry struct {
|
||||||
|
image *ebiten.Image
|
||||||
|
atime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
glyphImageCache = map[font.Face]map[rune]*glyphImageCacheEntry{}
|
||||||
|
emptyGlyphs = map[font.Face]map[rune]struct{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func getGlyphImage(face font.Face, r rune) *ebiten.Image {
|
||||||
|
if _, ok := emptyGlyphs[face]; !ok {
|
||||||
|
emptyGlyphs[face] = map[rune]struct{}{}
|
||||||
|
}
|
||||||
|
if _, ok := glyphImageCache[face]; !ok {
|
||||||
|
glyphImageCache[face] = map[rune]*glyphImageCacheEntry{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := emptyGlyphs[face][r]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if e, ok := glyphImageCache[face][r]; ok {
|
||||||
|
e.atime = now()
|
||||||
|
return e.image
|
||||||
|
}
|
||||||
|
|
||||||
|
b := getGlyphBounds(face, r)
|
||||||
|
w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil()
|
||||||
|
if w == 0 || h == 0 {
|
||||||
|
emptyGlyphs[face][r] = struct{}{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(glyphImageCache[face]) > cacheLimit {
|
||||||
|
oldest := int64(math.MaxInt64)
|
||||||
|
oldestKey := rune(-1)
|
||||||
|
for r, e := range glyphImageCache[face] {
|
||||||
|
if e.atime < oldest {
|
||||||
|
oldestKey = r
|
||||||
|
oldest = e.atime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glyphImageCache[face][oldestKey].image.Dispose()
|
||||||
|
delete(glyphImageCache[face], oldestKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
d := font.Drawer{
|
||||||
|
Dst: rgba,
|
||||||
|
Src: image.White,
|
||||||
|
Face: face,
|
||||||
|
}
|
||||||
|
d.Dot = fixed.Point26_6{-b.Min.X, -b.Min.Y}
|
||||||
|
d.DrawString(string(r))
|
||||||
|
|
||||||
|
img, _ := ebiten.NewImageFromImage(rgba, ebiten.FilterDefault)
|
||||||
|
glyphImageCache[face][r] = &glyphImageCacheEntry{
|
||||||
|
image: img,
|
||||||
|
atime: now(),
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
var textM sync.Mutex
|
var textM sync.Mutex
|
||||||
|
|
||||||
// Draw draws a given text on a given destination image dst.
|
// Draw draws a given text on a given destination image dst.
|
||||||
@ -338,23 +227,19 @@ var textM sync.Mutex
|
|||||||
func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) {
|
func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) {
|
||||||
textM.Lock()
|
textM.Lock()
|
||||||
|
|
||||||
n := now()
|
|
||||||
fx := fixed.I(x)
|
fx := fixed.I(x)
|
||||||
prevC := rune(-1)
|
prevR := rune(-1)
|
||||||
|
|
||||||
runes := []rune(text)
|
runes := []rune(text)
|
||||||
for _, c := range runes {
|
for _, r := range runes {
|
||||||
if prevC >= 0 {
|
if prevR >= 0 {
|
||||||
fx += face.Kern(prevC, c)
|
fx += face.Kern(prevR, r)
|
||||||
}
|
}
|
||||||
fa := uniqFace(face)
|
fa := uniqFace(face)
|
||||||
if g := getGlyphFromCache(fa, c, n); g != nil {
|
drawGlyph(dst, fa, r, fx, fixed.I(y), clr)
|
||||||
if !g.char.empty() {
|
fx += glyphAdvance(fa, r)
|
||||||
g.draw(dst, fx, fixed.I(y), clr)
|
|
||||||
}
|
prevR = r
|
||||||
fx += glyphAdvance(fa, c)
|
|
||||||
}
|
|
||||||
prevC = c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textM.Unlock()
|
textM.Unlock()
|
||||||
|
Loading…
Reference in New Issue
Block a user