ebiten/text/text.go

289 lines
6.2 KiB
Go
Raw Normal View History

2017-07-15 20:25:57 +02:00
// Copyright 2017 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package text offers functions to draw texts on an Ebiten's image.
2017-07-16 23:39:06 +02:00
//
// Note: This package is experimental and API might be changed.
2017-07-15 20:25:57 +02:00
package text
import (
"image"
"image/color"
"math"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/internal/graphics" // TODO: Move NextPowerOf2Int to a new different package
"github.com/hajimehoshi/ebiten/internal/sync"
)
var (
monotonicClock int64
)
func now() int64 {
monotonicClock++
return monotonicClock
}
type char struct {
face font.Face
rune rune
}
type glyph struct {
char char
index int
bounds fixed.Rectangle26_6
atime int64
}
func (g *glyph) size() (int, int) {
p := g.bounds.Max.Sub(g.bounds.Min)
return p.X.Ceil(), p.Y.Ceil()
}
func (g *glyph) empty() bool {
w, h := g.size()
return w == 0 || h == 0
}
func (g *glyph) atlasGroup() int {
w, h := g.size()
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
}
return graphics.NextPowerOf2Int(t)
}
func (g *glyph) draw(dst *ebiten.Image, x, y int, clr color.Color) {
cr, cg, cb, ca := clr.RGBA()
if ca == 0 {
return
}
a := atlases[g.atlasGroup()]
sx, sy := a.at(g)
ox := g.bounds.Min.X.Ceil()
oy := g.bounds.Min.Y.Ceil()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(x), float64(y))
op.GeoM.Translate(float64(ox), float64(oy))
rf := float64(cr) / float64(ca)
gf := float64(cg) / float64(ca)
bf := float64(cb) / float64(ca)
af := float64(ca) / 0xffff
op.ColorM.Scale(rf, gf, bf, af)
r := image.Rect(sx, sy, sx+a.size, sy+a.size)
op.SourceRect = &r
dst.DrawImage(a.image, op)
}
var (
glyphs = map[char]*glyph{}
atlases = map[int]*atlas{}
)
type atlas struct {
2017-07-16 18:54:10 +02:00
// image is the back-end image to hold glyph cache.
image *ebiten.Image
// tmpImage is the temporary image as a renderer source for glyph.
2017-07-15 20:25:57 +02:00
tmpImage *ebiten.Image
2017-07-16 18:54:10 +02:00
// size is the size of one glyph in the cache.
// This value is always power of 2.
size int
// glyphs is the set of glyph information.
glyphs []*glyph
// num is the number of glyphs the atlas holds.
num int
2017-07-15 20:25:57 +02:00
}
func (a *atlas) at(glyph *glyph) (int, int) {
if a.size != glyph.atlasGroup() {
panic("not reached")
}
w, _ := a.image.Size()
xnum := w / a.size
x, y := glyph.index%xnum, glyph.index/xnum
return x * a.size, y * a.size
}
func (a *atlas) append(glyph *glyph) {
2017-07-16 18:54:10 +02:00
if a.num == len(a.glyphs) {
2017-07-15 20:25:57 +02:00
idx := -1
t := int64(math.MaxInt64)
for i, g := range a.glyphs {
if g.atime < t {
t = g.atime
idx = i
}
}
if idx < 0 {
panic("not reached")
}
oldest := a.glyphs[idx]
delete(glyphs, oldest.char)
glyph.index = idx
a.glyphs[idx] = glyph
a.draw(glyph)
return
}
idx := -1
for i, g := range a.glyphs {
if g == nil {
idx = i
break
}
}
if idx < 0 {
panic("not reached")
}
2017-07-16 18:54:10 +02:00
a.num++
2017-07-15 20:25:57 +02:00
glyph.index = idx
a.glyphs[idx] = glyph
a.draw(glyph)
}
func (a *atlas) draw(glyph *glyph) {
if a.tmpImage == nil {
a.tmpImage, _ = ebiten.NewImage(a.size, a.size, ebiten.FilterNearest)
}
dst := image.NewRGBA(image.Rect(0, 0, a.size, a.size))
d := font.Drawer{
Dst: dst,
Src: image.White,
Face: glyph.char.face,
}
ox := -glyph.bounds.Min.X.Ceil()
oy := -glyph.bounds.Min.Y.Ceil()
d.Dot = fixed.P(ox, oy)
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, r}
g, ok := glyphs[ch]
if ok {
g.atime = now
return g
}
2017-07-16 18:54:10 +02:00
b, _, ok := face.GlyphBounds(r)
if !ok {
return nil
}
2017-07-15 20:25:57 +02:00
g = &glyph{
2017-07-16 18:54:10 +02:00
char: ch,
bounds: b,
atime: now,
2017-07-15 20:25:57 +02:00
}
if g.empty() {
return g
}
a, ok := atlases[g.atlasGroup()]
if !ok {
// 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)
a = &atlas{
image: i,
size: g.atlasGroup(),
}
w, h := a.image.Size()
xnum := w / a.size
ynum := h / a.size
a.glyphs = make([]*glyph, xnum*ynum)
atlases[g.atlasGroup()] = a
}
a.append(g)
glyphs[g.char] = g
return g
}
var textM sync.Mutex
2017-07-17 09:17:01 +02:00
// Draw draws a given text on a given destination image dst.
2017-07-15 20:25:57 +02:00
//
// face is the font for text rendering.
// (x, y) represents a 'dot' position. Be careful that this doesn't represent left-upper corner position.
// clr is the color for text rendering.
//
2017-07-16 19:15:00 +02:00
// Glyphs used for rendering are cached in least-recently-used way.
2017-07-16 19:13:30 +02:00
// It is OK to call this function with a same text and a same face at every frame.
//
2017-07-15 20:25:57 +02:00
// This function is concurrent-safe.
2017-07-16 23:42:48 +02:00
func Draw(dst *ebiten.Image, face font.Face, text string, x, y int, clr color.Color) {
2017-07-15 20:25:57 +02:00
textM.Lock()
n := now()
fx := fixed.I(x)
prevC := rune(-1)
runes := []rune(text)
for _, c := range runes {
if prevC >= 0 {
fx += face.Kern(prevC, c)
}
2017-07-16 18:54:10 +02:00
if g := getGlyphFromCache(face, c, n); g != nil {
if !g.empty() {
g.draw(dst, fx.Ceil(), y, clr)
}
a, _ := face.GlyphAdvance(c)
fx += a
2017-07-15 20:25:57 +02:00
}
prevC = c
}
textM.Unlock()
}