text/v2: add LayoutOptions

Now AppendGlyphs can treat multiple lines and alignments.
This commit is contained in:
Hajime Hoshi 2023-11-13 23:15:54 +09:00
parent cca4e78651
commit ea1d9dde4e
3 changed files with 66 additions and 57 deletions

View File

@ -18,7 +18,6 @@ import (
"image/color"
"log"
"math"
"strings"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
@ -75,15 +74,15 @@ type Game struct {
counter int
kanjiText []rune
kanjiTextColor color.RGBA
glyphs [][]text.Glyph
glyphs []text.Glyph
}
func (g *Game) Update() error {
// Initialize the glyphs for special (colorful) rendering.
if len(g.glyphs) == 0 {
for _, line := range strings.Split(sampleText, "\n") {
g.glyphs = append(g.glyphs, text.AppendGlyphs(nil, line, mplusNormalFace, 0, 0))
}
op := &text.LayoutOptions{}
op.LineHeightInPixels = mplusNormalFace.Metrics().Height
g.glyphs = text.AppendGlyphs(g.glyphs, sampleText, mplusNormalFace, op)
}
return nil
}
@ -137,33 +136,30 @@ func (g *Game) Draw(screen *ebiten.Image) {
text.Draw(screen, sampleText, mplusBigFace, op)
}
{
const x, y = 240, 380
const x, y = 240, 360
op := &ebiten.DrawImageOptions{}
// g.glyphs is initialized by text.AppendGlyphs.
// You can customize how to render each glyph.
// In this example, multiple colors are used to render glyphs.
for j, line := range g.glyphs {
for i, gl := range line {
op.GeoM.Reset()
op.GeoM.Translate(x, y)
op.GeoM.Translate(0, float64(j)*mplusNormalFace.Metrics().Height)
op.GeoM.Translate(gl.X, gl.Y)
op.ColorScale.Reset()
r := float32(1)
if i%3 == 0 {
r = 0.5
}
g := float32(1)
if i%3 == 1 {
g = 0.5
}
b := float32(1)
if i%3 == 2 {
b = 0.5
}
op.ColorScale.Scale(r, g, b, 1)
screen.DrawImage(gl.Image, op)
for i, gl := range g.glyphs {
op.GeoM.Reset()
op.GeoM.Translate(x, y)
op.GeoM.Translate(gl.X, gl.Y)
op.ColorScale.Reset()
r := float32(1)
if i%3 == 0 {
r = 0.5
}
g := float32(1)
if i%3 == 1 {
g = 0.5
}
b := float32(1)
if i%3 == 2 {
b = 0.5
}
op.ColorScale.Scale(r, g, b, 1)
screen.DrawImage(gl.Image, op)
}
}
}

View File

@ -35,12 +35,16 @@ const (
// DrawImageOptions.GeoM is an additional geometry transformation
// after putting the rendering region along with the specified alignments.
// DrawImageOptions.ColorScale scales the text color.
type DrawOptions struct {
ebiten.DrawImageOptions
LayoutOptions
}
// LayoutOptions represents options for layouting texts.
//
// PrimaryAlign and SecondaryAlign determine where to put the text in the given region at Draw.
// Draw might render the text outside of the specified image bounds, so you might have to specify GeoM to make the text visible.
type DrawOptions struct {
ebiten.DrawImageOptions
type LayoutOptions struct {
// LineHeightInPixels is a line height in pixels.
LineHeightInPixels float64
@ -95,12 +99,43 @@ type DrawOptions struct {
// If the vertical alignment is center, the rendering region's middle Y comes to the origin.
// If the vertical alignment is bottom, the rendering region's bottom Y comes to the origin.
func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
if options == nil {
options = &DrawOptions{}
}
geoM := options.GeoM
for _, g := range AppendGlyphs(nil, text, face, &options.LayoutOptions) {
op := &options.DrawImageOptions
op.GeoM.Reset()
op.GeoM.Translate(g.X, g.Y)
op.GeoM.Concat(geoM)
dst.DrawImage(g.Image, op)
}
}
// AppendGlyphs appends glyphs to the given slice and returns a slice.
//
// AppendGlyphs is a low-level API, and you can use AppendGlyphs to have more control than Draw.
// AppendGlyphs is also available to precache glyphs.
//
// For the details of options, see Draw function.
//
// AppendGlyphs is concurrent-safe.
func AppendGlyphs(glyphs []Glyph, text string, face Face, options *LayoutOptions) []Glyph {
return appendGlyphs(glyphs, text, face, 0, 0, options)
}
// appendGlyphs appends glyphs to the given slice and returns a slice.
//
// appendGlyphs assumes the text is rendered with the position (x, y).
// (x, y) might affect the subpixel rendering results.
func appendGlyphs(glyphs []Glyph, text string, face Face, x, y float64, options *LayoutOptions) []Glyph {
if text == "" {
return
return glyphs
}
if options == nil {
options = &DrawOptions{}
options = &LayoutOptions{}
}
// Calculate the advances for each line.
@ -171,7 +206,6 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
var originX, originY float64
var i int
geoM := options.GeoM
for t := text; ; {
line, rest, found := strings.Cut(t, "\n")
@ -197,7 +231,7 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
}
}
drawLine(dst, line, face, options, originX+offsetX, originY+offsetY, geoM)
glyphs = face.appendGlyphs(glyphs, line, originX+offsetX+x, originY+offsetY+y)
if !found {
break
@ -217,17 +251,8 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
originX -= options.LineHeightInPixels
}
}
}
func drawLine(dst *ebiten.Image, line string, face Face, options *DrawOptions, originX, originY float64, geoM ebiten.GeoM) {
op := &options.DrawImageOptions
gs := face.appendGlyphs(nil, line, originX, originY)
for _, g := range gs {
op.GeoM.Reset()
op.GeoM.Translate(g.X, g.Y)
op.GeoM.Concat(geoM)
dst.DrawImage(g.Image, op)
}
return glyphs
}
type horizontalAlign int

View File

@ -115,18 +115,6 @@ type Glyph struct {
GID uint32
}
// AppendGlyphs appends glyphs to the given slice and returns a slice.
//
// AppendGlyphs is a low-level API, and you can use AppendGlyphs to have more control than Draw.
// AppendGlyphs is also available to precache glyphs.
//
// AppendGlyphs doesn't treat multiple lines.
//
// AppendGlyphs is concurrent-safe.
func AppendGlyphs(glyphs []Glyph, text string, face Face, originX, originY float64) []Glyph {
return face.appendGlyphs(glyphs, text, originX, originY)
}
// Advance returns the advanced distance from the origin position when rendering the given text with the given face.
//
// Advance doesn't treat multiple lines.
@ -229,7 +217,7 @@ func CacheGlyphs(text string, face Face) {
var buf []Glyph
// Create all the possible variations (#2528).
for i := 0; i < 4; i++ {
buf = AppendGlyphs(buf, text, face, x, y)
buf = appendGlyphs(buf, text, face, x, y, nil)
buf = buf[:0]
if face.direction().isHorizontal() {