123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- // 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
- }
|