ebiten/text/v2/layout.go

379 lines
11 KiB
Go

// Copyright 2023 The Ebitengine 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
import (
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
// Align is the alignment that determines how to put a text.
type Align int
const (
AlignStart Align = iota
AlignCenter
AlignEnd
)
// DrawOptions represents options for the Draw function.
//
// DrawOption embeds ebiten.DrawImageOptions.
// 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 LayoutOptions struct {
// LineSpacing is a distance between two adjacent lines's baselines.
// The unit is in pixels.
LineSpacing float64
// PrimaryAlign is an alignment of the primary direction, in which a text in one line is rendered.
// The primary direction is the horizontal direction for a horizontal-direction face,
// and the vertical direction for a vertical-direction face.
// The meaning of the start and the end depends on the face direction.
PrimaryAlign Align
// SecondaryAlign is an alignment of the secondary direction, in which multiple lines are rendered.
// The secondary direction is the vertical direction for a horizontal-direction face,
// and the horizontal direction for a vertical-direction face.
// The meaning of the start and the end depends on the face direction.
SecondaryAlign Align
}
// Draw draws a given text on a given destination image dst.
// face is the font for text rendering.
//
// The '\n' newline character puts the following text on the next line.
//
// Glyphs used for rendering are cached in least-recently-used way.
// Then old glyphs might be evicted from the cache.
// As the cache capacity has limit, it is not guaranteed that all the glyphs for runes given at Draw are cached.
//
// It is OK to call Draw with a same text and a same face at every frame in terms of performance.
//
// Draw is concurrent-safe.
//
// # Rendering region
//
// A rectangle region where a text is put is called a 'rendering region'.
// The position of the text in the rendering region is determined by the specified primary and secondary alignments.
//
// The actual rendering position of the rendering region depends on the alignments in DrawOptions.
// By default, if the face's primary direction is left-to-right, the rendering region's upper-left position is (0, 0).
// Note that this is different from text v1. In text v1, (0, 0) is always the origin position.
//
// # Alignments
//
// For horizontal directions, the start and end depends on the face.
// If the face is GoTextFace, the start and the end depend on the Direction property.
// If the face is GoXFace, the start and the end are always left and right respectively.
//
// For vertical directions, the start and end are top and bottom respectively.
//
// If the horizontal alignment is left, the rendering region's left X comes to the destination image's origin (0, 0).
// If the horizontal alignment is center, the rendering region's middle X comes to the origin.
// If the horizontal alignment is right, the rendering region's right X comes to the origin.
//
// If the vertical alignment is top, the rendering region's top Y comes to the destination image's origin (0, 0).
// 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) {
var layoutOp LayoutOptions
var drawOp ebiten.DrawImageOptions
if options != nil {
layoutOp = options.LayoutOptions
drawOp = options.DrawImageOptions
}
geoM := drawOp.GeoM
for _, g := range AppendGlyphs(nil, text, face, &layoutOp) {
if g.Image == nil {
continue
}
drawOp.GeoM.Reset()
drawOp.GeoM.Translate(g.X, g.Y)
drawOp.GeoM.Concat(geoM)
dst.DrawImage(g.Image, &drawOp)
}
}
// 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)
}
// AppndVectorPath appends a vector path for glyphs to the given path.
//
// AppendVectorPath works only when the face is *GoTextFace or a composite face using *GoTextFace so far.
// For other types, AppendVectorPath does nothing.
func AppendVectorPath(path *vector.Path, text string, face Face, options *LayoutOptions) {
forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
face.appendVectorPathForLine(path, line, originX, originY)
})
}
// 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 {
forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
glyphs = face.appendGlyphsForLine(glyphs, line, indexOffset, originX+x, originY+y)
})
return glyphs
}
// forEachLine interates lines.
func forEachLine(text string, face Face, options *LayoutOptions, f func(text string, indexOffset int, originX, originY float64)) {
if text == "" {
return
}
if options == nil {
options = &LayoutOptions{}
}
// Calculate the advances for each line.
var advances []float64
var longestAdvance float64
var lineCount int
for t := text; ; {
lineCount++
line, rest, found := strings.Cut(t, "\n")
a := face.advance(line)
advances = append(advances, a)
if longestAdvance < a {
longestAdvance = a
}
if !found {
break
}
t = rest
}
d := face.direction()
m := face.Metrics()
var boundaryWidth, boundaryHeight float64
if d.isHorizontal() {
boundaryWidth = longestAdvance
boundaryHeight = float64(lineCount-1)*options.LineSpacing + m.HAscent + m.HDescent
} else {
// TODO: Perhaps HAscent and HDescent should be used for sideways glyphs.
boundaryWidth = float64(lineCount-1)*options.LineSpacing + m.VAscent + m.VDescent
boundaryHeight = longestAdvance
}
var offsetX, offsetY float64
// Adjust the offset based on the secondary alignments.
h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
switch d {
case DirectionLeftToRight, DirectionRightToLeft:
offsetY += m.HAscent
switch v {
case verticalAlignTop:
case verticalAlignCenter:
offsetY -= boundaryHeight / 2
case verticalAlignBottom:
offsetY -= boundaryHeight
}
case DirectionTopToBottomAndLeftToRight:
// TODO: Perhaps HDescent should be used for sideways glyphs.
offsetX += m.VDescent
switch h {
case horizontalAlignLeft:
case horizontalAlignCenter:
offsetX -= boundaryWidth / 2
case horizontalAlignRight:
offsetX -= boundaryWidth
}
case DirectionTopToBottomAndRightToLeft:
// TODO: Perhaps HAscent should be used for sideways glyphs.
offsetX -= m.VAscent
switch h {
case horizontalAlignLeft:
offsetX += boundaryWidth
case horizontalAlignCenter:
offsetX += boundaryWidth / 2
case horizontalAlignRight:
}
}
var indexOffset int
var originX, originY float64
var i int
for t := text; ; {
line, rest, found := strings.Cut(t, "\n")
// Adjust the origin position based on the primary alignments.
switch d {
case DirectionLeftToRight, DirectionRightToLeft:
switch h {
case horizontalAlignLeft:
originX = 0
case horizontalAlignCenter:
originX = -advances[i] / 2
case horizontalAlignRight:
originX = -advances[i]
}
case DirectionTopToBottomAndLeftToRight, DirectionTopToBottomAndRightToLeft:
switch v {
case verticalAlignTop:
originY = 0
case verticalAlignCenter:
originY = -advances[i] / 2
case verticalAlignBottom:
originY = -advances[i]
}
}
f(line, indexOffset, originX+offsetX, originY+offsetY)
if !found {
break
}
t = rest
indexOffset += len(line) + 1
i++
// Advance the origin position in the secondary direction.
switch face.direction() {
case DirectionLeftToRight:
originY += options.LineSpacing
case DirectionRightToLeft:
originY += options.LineSpacing
case DirectionTopToBottomAndLeftToRight:
originX += options.LineSpacing
case DirectionTopToBottomAndRightToLeft:
originX -= options.LineSpacing
}
}
}
type horizontalAlign int
const (
horizontalAlignLeft horizontalAlign = iota
horizontalAlignCenter
horizontalAlignRight
)
type verticalAlign int
const (
verticalAlignTop verticalAlign = iota
verticalAlignCenter
verticalAlignBottom
)
func calcAligns(direction Direction, primaryAlign, secondaryAlign Align) (horizontalAlign, verticalAlign) {
var h horizontalAlign
var v verticalAlign
switch direction {
case DirectionLeftToRight:
switch primaryAlign {
case AlignStart:
h = horizontalAlignLeft
case AlignCenter:
h = horizontalAlignCenter
case AlignEnd:
h = horizontalAlignRight
}
switch secondaryAlign {
case AlignStart:
v = verticalAlignTop
case AlignCenter:
v = verticalAlignCenter
case AlignEnd:
v = verticalAlignBottom
}
case DirectionRightToLeft:
switch primaryAlign {
case AlignStart:
h = horizontalAlignRight
case AlignCenter:
h = horizontalAlignCenter
case AlignEnd:
h = horizontalAlignLeft
}
switch secondaryAlign {
case AlignStart:
v = verticalAlignTop
case AlignCenter:
v = verticalAlignCenter
case AlignEnd:
v = verticalAlignBottom
}
case DirectionTopToBottomAndLeftToRight:
switch primaryAlign {
case AlignStart:
v = verticalAlignTop
case AlignCenter:
v = verticalAlignCenter
case AlignEnd:
v = verticalAlignBottom
}
switch secondaryAlign {
case AlignStart:
h = horizontalAlignLeft
case AlignCenter:
h = horizontalAlignCenter
case AlignEnd:
h = horizontalAlignRight
}
case DirectionTopToBottomAndRightToLeft:
switch primaryAlign {
case AlignStart:
v = verticalAlignTop
case AlignCenter:
v = verticalAlignCenter
case AlignEnd:
v = verticalAlignBottom
}
switch secondaryAlign {
case AlignStart:
h = horizontalAlignRight
case AlignCenter:
h = horizontalAlignCenter
case AlignEnd:
h = horizontalAlignLeft
}
}
return h, v
}