ebiten/text/v2/draw.go
Hajime Hoshi b8b8b16098 all: add text/v2
This change adds some basic APIs incuding StdFace.

GoTextFace will be added later.

Updates #2454
2023-11-12 17:04:25 +09:00

326 lines
9.3 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"
)
// 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.
//
// 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
// LineHeightInPixels is a line height in pixels.
LineHeightInPixels 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 StdFace, 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) {
if text == "" {
return
}
if options == nil {
options = &DrawOptions{}
}
// 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.LineHeightInPixels + m.HAscent + m.HDescent
} else {
boundaryWidth = float64(lineCount-1)*options.LineHeightInPixels + m.VAscent + m.VDescent
boundaryHeight = longestAdvance
}
var offsetX, offsetY float64
// Whichever the direction and the alignments are, the Y position has an offset by an ascent for horizontal texts.
offsetY += m.HAscent
// Adjust the offset based on the secondary alignments.
h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
switch d {
case DirectionLeftToRight, DirectionRightToLeft:
switch v {
case verticalAlignTop:
case verticalAlignCenter:
offsetY -= boundaryHeight / 2
case verticalAlignBottom:
offsetY -= boundaryHeight
}
case DirectionTopToBottomAndLeftToRight:
offsetX -= m.VAscent
switch h {
case horizontalAlignLeft:
case horizontalAlignCenter:
offsetX -= boundaryWidth / 2
case horizontalAlignRight:
offsetX -= boundaryWidth
}
case DirectionTopToBottomAndRightToLeft:
offsetX -= m.VAscent
switch h {
case horizontalAlignLeft:
offsetX += boundaryWidth
case horizontalAlignCenter:
offsetX += boundaryWidth / 2
case horizontalAlignRight:
}
}
var originX, originY float64
var i int
geoM := options.GeoM
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]
}
}
drawLine(dst, line, face, options, originX+offsetX, originY+offsetY, geoM)
if !found {
break
}
t = rest
i++
// Advance the origin position in the secondary direction.
switch face.direction() {
case DirectionLeftToRight:
originY += options.LineHeightInPixels
case DirectionRightToLeft:
originY += options.LineHeightInPixels
case DirectionTopToBottomAndLeftToRight:
originX += options.LineHeightInPixels
case DirectionTopToBottomAndRightToLeft:
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)
}
}
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
}