mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 03:08:54 +01:00
all: add text/v2
This change adds some basic APIs incuding StdFace. GoTextFace will be added later. Updates #2454
This commit is contained in:
parent
1d4c210ff2
commit
b8b8b16098
@ -17,7 +17,6 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/png"
|
||||
"log"
|
||||
"strings"
|
||||
@ -28,7 +27,7 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2/examples/keyboard/keyboard"
|
||||
rkeyboard "github.com/hajimehoshi/ebiten/v2/examples/resources/images/keyboard"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -36,6 +35,8 @@ const (
|
||||
screenHeight = 240
|
||||
)
|
||||
|
||||
var fontFace = text.NewStdFace(bitmapfont.Face)
|
||||
|
||||
var keyboardImage *ebiten.Image
|
||||
|
||||
func init() {
|
||||
@ -91,7 +92,9 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
||||
}
|
||||
|
||||
// Use bitmapfont.Face instead of ebitenutil.DebugPrint, since some key names might not be printed with DebugPrint.
|
||||
text.Draw(screen, strings.Join(keyStrs, ", ")+"\n"+strings.Join(keyNames, ", "), bitmapfont.Face, 4, 12, color.White)
|
||||
textOp := &text.DrawOptions{}
|
||||
textOp.LineHeightInPixels = fontFace.Metrics().Height
|
||||
text.Draw(screen, strings.Join(keyStrs, ", ")+"\n"+strings.Join(keyNames, ", "), fontFace, textOp)
|
||||
}
|
||||
|
||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
||||
"github.com/hajimehoshi/ebiten/v2/text"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
)
|
||||
|
||||
@ -39,8 +39,8 @@ const sampleText = ` The quick brown fox jumps
|
||||
over the lazy dog.`
|
||||
|
||||
var (
|
||||
mplusNormalFont font.Face
|
||||
mplusBigFont font.Face
|
||||
mplusNormalFace *text.StdFace
|
||||
mplusBigFace *text.StdFace
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -50,7 +50,7 @@ func init() {
|
||||
}
|
||||
|
||||
const dpi = 72
|
||||
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
|
||||
mplusNormalFont, err := opentype.NewFace(tt, &opentype.FaceOptions{
|
||||
Size: 24,
|
||||
DPI: dpi,
|
||||
Hinting: font.HintingVertical,
|
||||
@ -58,7 +58,9 @@ func init() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
|
||||
mplusNormalFace = text.NewStdFace(mplusNormalFont)
|
||||
|
||||
mplusBigFont, err := opentype.NewFace(tt, &opentype.FaceOptions{
|
||||
Size: 32,
|
||||
DPI: dpi,
|
||||
Hinting: font.HintingVertical,
|
||||
@ -66,43 +68,24 @@ func init() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mplusBigFace = text.NewStdFace(mplusBigFont)
|
||||
}
|
||||
|
||||
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 {
|
||||
g.glyphs = text.AppendGlyphs(g.glyphs, mplusNormalFont, sampleText)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func boundString(face font.Face, str string) fixed.Rectangle26_6 {
|
||||
str = strings.TrimRight(str, "\n")
|
||||
lines := strings.Split(str, "\n")
|
||||
if len(lines) == 0 {
|
||||
return fixed.Rectangle26_6{}
|
||||
}
|
||||
|
||||
minX := fixed.I(0)
|
||||
maxX := fixed.I(0)
|
||||
for _, line := range lines {
|
||||
a := font.MeasureString(face, line)
|
||||
if maxX < a {
|
||||
maxX = a
|
||||
for _, line := range strings.Split(sampleText, "\n") {
|
||||
g.glyphs = append(g.glyphs, text.AppendGlyphs(nil, line, mplusNormalFace, 0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
m := face.Metrics()
|
||||
minY := -m.Ascent
|
||||
maxY := fixed.Int26_6(len(lines)-1)*m.Height + m.Descent
|
||||
return fixed.Rectangle26_6{Min: fixed.Point26_6{X: minX, Y: minY}, Max: fixed.Point26_6{X: maxX, Y: maxY}}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
|
||||
@ -114,30 +97,40 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
||||
|
||||
{
|
||||
const x, y = 20, 40
|
||||
b := boundString(mplusNormalFont, sampleText)
|
||||
vector.DrawFilledRect(screen, fixed26_6ToFloat32(b.Min.X)+x, fixed26_6ToFloat32(b.Min.Y)+y, fixed26_6ToFloat32(b.Max.X-b.Min.X), fixed26_6ToFloat32(b.Max.Y-b.Min.Y), gray, false)
|
||||
text.Draw(screen, sampleText, mplusNormalFont, x, y, color.White)
|
||||
w, h := text.Measure(sampleText, mplusNormalFace, mplusNormalFace.Metrics().Height)
|
||||
vector.DrawFilledRect(screen, x, y, float32(w), float32(h), gray, false)
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(x, y)
|
||||
op.LineHeightInPixels = mplusNormalFace.Metrics().Height
|
||||
text.Draw(screen, sampleText, mplusNormalFace, op)
|
||||
}
|
||||
{
|
||||
const x, y = 20, 140
|
||||
b := boundString(mplusBigFont, sampleText)
|
||||
vector.DrawFilledRect(screen, fixed26_6ToFloat32(b.Min.X)+x, fixed26_6ToFloat32(b.Min.Y)+y, fixed26_6ToFloat32(b.Max.X-b.Min.X), fixed26_6ToFloat32(b.Max.Y-b.Min.Y), gray, false)
|
||||
text.Draw(screen, sampleText, mplusBigFont, x, y, color.White)
|
||||
w, h := text.Measure(sampleText, mplusBigFace, mplusBigFace.Metrics().Height)
|
||||
vector.DrawFilledRect(screen, x, y, float32(w), float32(h), gray, false)
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(x, y)
|
||||
op.LineHeightInPixels = mplusBigFace.Metrics().Height
|
||||
text.Draw(screen, sampleText, mplusBigFace, op)
|
||||
}
|
||||
{
|
||||
const x, y = 20, 240
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Rotate(math.Pi / 4)
|
||||
op.GeoM.Translate(x, y)
|
||||
op.Filter = ebiten.FilterLinear
|
||||
text.DrawWithOptions(screen, sampleText, mplusNormalFont, op)
|
||||
op.LineHeightInPixels = mplusNormalFace.Metrics().Height
|
||||
text.Draw(screen, sampleText, mplusNormalFace, op)
|
||||
}
|
||||
{
|
||||
const x, y = 160, 240
|
||||
const lineHeight = 80
|
||||
b := boundString(text.FaceWithLineHeight(mplusBigFont, lineHeight), sampleText)
|
||||
vector.DrawFilledRect(screen, fixed26_6ToFloat32(b.Min.X)+x, fixed26_6ToFloat32(b.Min.Y)+y, fixed26_6ToFloat32(b.Max.X-b.Min.X), fixed26_6ToFloat32(b.Max.Y-b.Min.Y), gray, false)
|
||||
text.Draw(screen, sampleText, text.FaceWithLineHeight(mplusBigFont, lineHeight), x, y, color.White)
|
||||
w, h := text.Measure(sampleText, mplusBigFace, lineHeight)
|
||||
vector.DrawFilledRect(screen, x, y, float32(w), float32(h), gray, false)
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(x, y)
|
||||
op.LineHeightInPixels = lineHeight
|
||||
text.Draw(screen, sampleText, mplusBigFace, op)
|
||||
}
|
||||
{
|
||||
const x, y = 240, 400
|
||||
@ -145,25 +138,28 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
||||
// 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 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
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,17 +24,15 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/hajimehoshi/bitmapfont/v3"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/exp/textinput"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text"
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
)
|
||||
|
||||
var fontFace = bitmapfont.FaceEA
|
||||
var fontFace = text.NewStdFace(bitmapfont.FaceEA)
|
||||
|
||||
const (
|
||||
screenWidth = 640
|
||||
@ -93,15 +91,15 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
|
||||
y = 0
|
||||
}
|
||||
|
||||
lineHeight := fontFace.Metrics().Height.Ceil()
|
||||
lineHeight := int(fontFace.Metrics().Height)
|
||||
var nlCount int
|
||||
var lineStart int
|
||||
var prevAdvance fixed.Int26_6
|
||||
var prevAdvance float64
|
||||
for i, r := range t.text {
|
||||
var x0, x1 int
|
||||
currentAdvance := font.MeasureString(fontFace, t.text[lineStart:i])
|
||||
currentAdvance := text.Advance(fontFace, t.text[lineStart:i])
|
||||
if lineStart < i {
|
||||
x0 = ((prevAdvance + currentAdvance) / 2).Ceil()
|
||||
x0 = int((prevAdvance + currentAdvance) / 2)
|
||||
}
|
||||
if r == '\n' {
|
||||
x1 = int(math.MaxInt32)
|
||||
@ -110,10 +108,10 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
|
||||
for !utf8.ValidString(t.text[i:nextI]) {
|
||||
nextI++
|
||||
}
|
||||
nextAdvance := font.MeasureString(fontFace, t.text[lineStart:nextI])
|
||||
x1 = ((currentAdvance + nextAdvance) / 2).Ceil()
|
||||
nextAdvance := text.Advance(fontFace, t.text[lineStart:nextI])
|
||||
x1 = int((currentAdvance + nextAdvance) / 2)
|
||||
} else {
|
||||
x1 = currentAdvance.Ceil()
|
||||
x1 = int(currentAdvance)
|
||||
}
|
||||
if x0 <= x && x < x1 && nlCount*lineHeight <= y && y < (nlCount+1)*lineHeight {
|
||||
return i, true
|
||||
@ -159,7 +157,7 @@ func (t *TextField) Update() {
|
||||
cx, cy := t.cursorPos()
|
||||
px, py := textFieldPadding()
|
||||
x += cx + px
|
||||
y += cy + py + fontFace.Metrics().Ascent.Ceil()
|
||||
y += cy + py + int(fontFace.Metrics().HAscent)
|
||||
t.ch, t.end = textinput.Start(x, y)
|
||||
// Start returns nil for non-supported envrionments.
|
||||
if t.ch == nil {
|
||||
@ -257,8 +255,8 @@ func (t *TextField) cursorPos() (int, int) {
|
||||
if t.state.Text != "" {
|
||||
txt += t.state.Text[:t.state.CompositionSelectionStartInBytes]
|
||||
}
|
||||
x := font.MeasureString(fontFace, txt).Ceil()
|
||||
y := nlCount * fontFace.Metrics().Height.Ceil()
|
||||
x := int(text.Advance(fontFace, txt))
|
||||
y := nlCount * int(fontFace.Metrics().Height)
|
||||
return x, y
|
||||
}
|
||||
|
||||
@ -276,7 +274,7 @@ func (t *TextField) Draw(screen *ebiten.Image) {
|
||||
cx, cy := t.cursorPos()
|
||||
x += px + cx
|
||||
y += py + cy
|
||||
h := fontFace.Metrics().Height.Ceil()
|
||||
h := int(fontFace.Metrics().Height)
|
||||
vector.StrokeLine(screen, float32(x), float32(y), float32(x), float32(y+h), 1, color.Black, false)
|
||||
}
|
||||
|
||||
@ -286,15 +284,19 @@ func (t *TextField) Draw(screen *ebiten.Image) {
|
||||
}
|
||||
|
||||
tx := t.bounds.Min.X + px
|
||||
ty := t.bounds.Min.Y + py + fontFace.Metrics().Ascent.Ceil()
|
||||
text.Draw(screen, shownText, fontFace, tx, ty, color.Black)
|
||||
ty := t.bounds.Min.Y + py
|
||||
op := &text.DrawOptions{}
|
||||
op.GeoM.Translate(float64(tx), float64(ty))
|
||||
op.ColorScale.ScaleWithColor(color.Black)
|
||||
op.LineHeightInPixels = fontFace.Metrics().Height
|
||||
text.Draw(screen, shownText, fontFace, op)
|
||||
}
|
||||
|
||||
const textFieldHeight = 24
|
||||
|
||||
func textFieldPadding() (int, int) {
|
||||
m := fontFace.Metrics()
|
||||
return 4, (textFieldHeight - m.Height.Ceil()) / 2
|
||||
return 4, (textFieldHeight - int(m.Height)) / 2
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
|
@ -15,6 +15,8 @@
|
||||
// Package text offers functions to draw texts on an Ebitengine's image.
|
||||
//
|
||||
// For the example using a TTF font, see font package in the examples.
|
||||
//
|
||||
// Deprecated: as of v2.7. Use text/v2 instead.
|
||||
package text
|
||||
|
||||
import (
|
||||
|
325
text/v2/draw.go
Normal file
325
text/v2/draw.go
Normal file
@ -0,0 +1,325 @@
|
||||
// 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
|
||||
}
|
117
text/v2/glyph.go
Normal file
117
text/v2/glyph.go
Normal file
@ -0,0 +1,117 @@
|
||||
// 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 (
|
||||
"math"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/hook"
|
||||
)
|
||||
|
||||
var monotonicClock int64
|
||||
|
||||
const infTime = math.MaxInt64
|
||||
|
||||
func now() int64 {
|
||||
return monotonicClock
|
||||
}
|
||||
|
||||
func init() {
|
||||
hook.AppendHookOnBeforeUpdate(func() error {
|
||||
monotonicClock++
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type glyphImageCacheKey struct {
|
||||
// For StdFace
|
||||
rune rune
|
||||
xoffset fixed.Int26_6
|
||||
}
|
||||
|
||||
type glyphImageCacheEntry struct {
|
||||
image *ebiten.Image
|
||||
atime int64
|
||||
}
|
||||
|
||||
type glyphImageCache struct {
|
||||
cache map[Face]map[glyphImageCacheKey]*glyphImageCacheEntry
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
var theGlyphImageCache glyphImageCache
|
||||
|
||||
func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
|
||||
g.m.Lock()
|
||||
defer g.m.Unlock()
|
||||
|
||||
e, ok := g.cache[face][key]
|
||||
if ok {
|
||||
e.atime = now()
|
||||
return e.image
|
||||
}
|
||||
|
||||
if g.cache == nil {
|
||||
g.cache = map[Face]map[glyphImageCacheKey]*glyphImageCacheEntry{}
|
||||
}
|
||||
if g.cache[face] == nil {
|
||||
g.cache[face] = map[glyphImageCacheKey]*glyphImageCacheEntry{}
|
||||
}
|
||||
|
||||
img := create()
|
||||
e = &glyphImageCacheEntry{
|
||||
image: img,
|
||||
}
|
||||
if img != nil {
|
||||
e.atime = now()
|
||||
} else {
|
||||
// If the glyph image is nil, the entry doesn't have to be removed.
|
||||
// Keep this until the face is GCed.
|
||||
e.atime = infTime
|
||||
}
|
||||
g.cache[face][key] = e
|
||||
|
||||
// Clean up old entries.
|
||||
|
||||
// cacheSoftLimit indicates the soft limit of the number of glyphs in the cache.
|
||||
// If the number of glyphs exceeds this soft limits, old glyphs are removed.
|
||||
// Even after cleaning up the cache, the number of glyphs might still exceed the soft limit, but
|
||||
// this is fine.
|
||||
const cacheSoftLimit = 512
|
||||
if len(g.cache[face]) > cacheSoftLimit {
|
||||
for key, e := range g.cache[face] {
|
||||
// 60 is an arbitrary number.
|
||||
if e.atime >= now()-60 {
|
||||
continue
|
||||
}
|
||||
delete(g.cache[face], key)
|
||||
}
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
func (g *glyphImageCache) clear(face Face) {
|
||||
runtime.SetFinalizer(face, nil)
|
||||
|
||||
g.m.Lock()
|
||||
defer g.m.Unlock()
|
||||
delete(g.cache, face)
|
||||
}
|
166
text/v2/std.go
Normal file
166
text/v2/std.go
Normal file
@ -0,0 +1,166 @@
|
||||
// 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 (
|
||||
"image"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
var _ Face = (*StdFace)(nil)
|
||||
|
||||
// StdFace is a Face implementation for a semi-standard font.Face (golang.org/x/image/font).
|
||||
// StdFace is useful to transit from existing codebase with text v1, or to use some bitmap fonts defined as font.Face.
|
||||
// StdFace must not be copied by value.
|
||||
type StdFace struct {
|
||||
f *faceWithCache
|
||||
|
||||
addr *StdFace
|
||||
}
|
||||
|
||||
// NewStdFace creates a new StdFace from a semi-standard font.Face.
|
||||
func NewStdFace(face font.Face) *StdFace {
|
||||
s := &StdFace{
|
||||
f: &faceWithCache{
|
||||
f: face,
|
||||
},
|
||||
}
|
||||
s.addr = s
|
||||
runtime.SetFinalizer(s, theGlyphImageCache.clear)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StdFace) copyCheck() {
|
||||
if s.addr != s {
|
||||
panic("text: illegal use of non-zero StdFace copied by value")
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics implelements Face.
|
||||
func (s *StdFace) Metrics() Metrics {
|
||||
s.copyCheck()
|
||||
|
||||
m := s.f.Metrics()
|
||||
return Metrics{
|
||||
Height: fixed26_6ToFloat64(m.Height),
|
||||
HAscent: fixed26_6ToFloat64(m.Ascent),
|
||||
HDescent: fixed26_6ToFloat64(m.Descent),
|
||||
}
|
||||
}
|
||||
|
||||
// UnsafeInternal implements Face.
|
||||
func (s *StdFace) UnsafeInternal() any {
|
||||
s.copyCheck()
|
||||
return s.f.f
|
||||
}
|
||||
|
||||
// advance implements Face.
|
||||
func (s *StdFace) advance(text string) float64 {
|
||||
return fixed26_6ToFloat64(font.MeasureString(s.f, text))
|
||||
}
|
||||
|
||||
// appendGlyphs implements Face.
|
||||
func (s *StdFace) appendGlyphs(glyphs []Glyph, text string, originX, originY float64) []Glyph {
|
||||
s.copyCheck()
|
||||
|
||||
x := float64ToFixed26_6(originX)
|
||||
y := float64ToFixed26_6(originY)
|
||||
prevR := rune(-1)
|
||||
|
||||
for i, r := range text {
|
||||
if prevR >= 0 {
|
||||
x += s.f.Kern(prevR, r)
|
||||
}
|
||||
img, imgX, imgY, a := s.glyphImage(r, x, y)
|
||||
if img != nil {
|
||||
// Adjust the position to the integers.
|
||||
// The current glyph images assume that they are rendered on integer positions so far.
|
||||
glyphs = append(glyphs, Glyph{
|
||||
Rune: r,
|
||||
IndexInBytes: i,
|
||||
Image: img,
|
||||
X: imgX,
|
||||
Y: imgY,
|
||||
})
|
||||
}
|
||||
x += a
|
||||
prevR = r
|
||||
}
|
||||
|
||||
return glyphs
|
||||
}
|
||||
|
||||
func (s *StdFace) glyphImage(r rune, x, y fixed.Int26_6) (*ebiten.Image, float64, float64, fixed.Int26_6) {
|
||||
b, a, _ := s.f.GlyphBounds(r)
|
||||
offset := fixed.Point26_6{
|
||||
X: (adjustOffsetGranularity(x) + b.Min.X) & ((1 << 6) - 1),
|
||||
Y: (fixed.I(y.Floor()) + b.Min.Y) & ((1 << 6) - 1),
|
||||
}
|
||||
key := glyphImageCacheKey{
|
||||
rune: r,
|
||||
xoffset: offset.X,
|
||||
// yoffset is always an integer, so this doesn't have to be a key.
|
||||
}
|
||||
img := theGlyphImageCache.getOrCreate(s, key, func() *ebiten.Image {
|
||||
return s.glyphImageImpl(r, offset)
|
||||
})
|
||||
imgX := fixed26_6ToFloat64(x + b.Min.X - offset.X)
|
||||
imgY := fixed26_6ToFloat64(y + b.Min.Y - offset.Y)
|
||||
return img, imgX, imgY, a
|
||||
}
|
||||
|
||||
func (s *StdFace) glyphImageImpl(r rune, offset fixed.Point26_6) *ebiten.Image {
|
||||
b, _, _ := s.f.GlyphBounds(r)
|
||||
w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil()
|
||||
if w == 0 || h == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.Min.X&((1<<6)-1) != 0 {
|
||||
w++
|
||||
}
|
||||
if b.Min.Y&((1<<6)-1) != 0 {
|
||||
h++
|
||||
}
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
|
||||
d := font.Drawer{
|
||||
Dst: rgba,
|
||||
Src: image.White,
|
||||
Face: s.f,
|
||||
}
|
||||
|
||||
x, y := -b.Min.X, -b.Min.Y
|
||||
x += offset.X
|
||||
y += offset.Y
|
||||
d.Dot = fixed.Point26_6{X: x, Y: y}
|
||||
d.DrawString(string(r))
|
||||
|
||||
return ebiten.NewImageFromImage(rgba)
|
||||
}
|
||||
|
||||
// direction implelements Face.
|
||||
func (s *StdFace) direction() Direction {
|
||||
return DirectionLeftToRight
|
||||
}
|
||||
|
||||
// Metrics implelements Face.
|
||||
func (s *StdFace) private() {
|
||||
}
|
127
text/v2/stdcache.go
Normal file
127
text/v2/stdcache.go
Normal file
@ -0,0 +1,127 @@
|
||||
// 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 (
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type glyphBoundsCacheValue struct {
|
||||
bounds fixed.Rectangle26_6
|
||||
advance fixed.Int26_6
|
||||
ok bool
|
||||
}
|
||||
|
||||
type glyphAdvanceCacheValue struct {
|
||||
advance fixed.Int26_6
|
||||
ok bool
|
||||
}
|
||||
|
||||
type kernCacheKey struct {
|
||||
r0 rune
|
||||
r1 rune
|
||||
}
|
||||
|
||||
type faceWithCache struct {
|
||||
f font.Face
|
||||
|
||||
glyphBoundsCache map[rune]glyphBoundsCacheValue
|
||||
glyphAdvanceCache map[rune]glyphAdvanceCacheValue
|
||||
kernCache map[kernCacheKey]fixed.Int26_6
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (f *faceWithCache) Close() error {
|
||||
if err := f.f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.m.Lock()
|
||||
defer f.m.Unlock()
|
||||
|
||||
f.glyphBoundsCache = nil
|
||||
f.glyphAdvanceCache = nil
|
||||
f.kernCache = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *faceWithCache) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
|
||||
return f.f.Glyph(dot, r)
|
||||
}
|
||||
|
||||
func (f *faceWithCache) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
|
||||
f.m.Lock()
|
||||
defer f.m.Unlock()
|
||||
|
||||
if v, ok := f.glyphBoundsCache[r]; ok {
|
||||
return v.bounds, v.advance, v.ok
|
||||
}
|
||||
|
||||
bounds, advance, ok = f.f.GlyphBounds(r)
|
||||
if f.glyphBoundsCache == nil {
|
||||
f.glyphBoundsCache = map[rune]glyphBoundsCacheValue{}
|
||||
}
|
||||
f.glyphBoundsCache[r] = glyphBoundsCacheValue{
|
||||
bounds: bounds,
|
||||
advance: advance,
|
||||
ok: ok,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *faceWithCache) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
|
||||
f.m.Lock()
|
||||
defer f.m.Unlock()
|
||||
|
||||
if v, ok := f.glyphAdvanceCache[r]; ok {
|
||||
return v.advance, v.ok
|
||||
}
|
||||
|
||||
advance, ok = f.f.GlyphAdvance(r)
|
||||
if f.glyphAdvanceCache == nil {
|
||||
f.glyphAdvanceCache = map[rune]glyphAdvanceCacheValue{}
|
||||
}
|
||||
f.glyphAdvanceCache[r] = glyphAdvanceCacheValue{
|
||||
advance: advance,
|
||||
ok: ok,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *faceWithCache) Kern(r0, r1 rune) fixed.Int26_6 {
|
||||
f.m.Lock()
|
||||
defer f.m.Unlock()
|
||||
|
||||
key := kernCacheKey{r0: r0, r1: r1}
|
||||
if v, ok := f.kernCache[key]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
v := f.f.Kern(r0, r1)
|
||||
if f.kernCache == nil {
|
||||
f.kernCache = map[kernCacheKey]fixed.Int26_6{}
|
||||
}
|
||||
f.kernCache[key] = v
|
||||
return v
|
||||
}
|
||||
|
||||
func (f *faceWithCache) Metrics() font.Metrics {
|
||||
return f.f.Metrics()
|
||||
}
|
199
text/v2/text.go
Normal file
199
text/v2/text.go
Normal file
@ -0,0 +1,199 @@
|
||||
// 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 offers functions to draw texts on an Ebitengine's image.
|
||||
//
|
||||
// For the example using a TrueType font, see examples in the examples directory.
|
||||
package text
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
// Face is an interface representing a font face. The implementations are only GoTextFace and StdFace.
|
||||
type Face interface {
|
||||
// Metrics returns the metrics for this Face.
|
||||
Metrics() Metrics
|
||||
|
||||
// UnsafeInternal returns the internal object for this face.
|
||||
// The returned value is either a semi-standard font.Face or go-text's font.Face.
|
||||
// This is unsafe since this might make internal cache states out of sync.
|
||||
UnsafeInternal() any
|
||||
|
||||
advance(text string) float64
|
||||
|
||||
appendGlyphs(glyphs []Glyph, text string, originX, originY float64) []Glyph
|
||||
|
||||
direction() Direction
|
||||
|
||||
// private is an unexported function preventing being implemented by other packages.
|
||||
private()
|
||||
}
|
||||
|
||||
// Metrics holds the metrics for a Face.
|
||||
// A visual depiction is at https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
|
||||
type Metrics struct {
|
||||
// Height is the recommended amount of vertical space between two lines of text in pixels.
|
||||
Height float64
|
||||
|
||||
// HAscent is the distance in pixels from the top of a line to its baseline for horizontal lines.
|
||||
HAscent float64
|
||||
|
||||
// HDescent is the distance in pixels from the bottom of a line to its baseline for horizontal lines.
|
||||
// The value is typically positive, even though a descender goes below the baseline.
|
||||
HDescent float64
|
||||
|
||||
// VAscent is the distance in pixels from the top of a line to its baseline for vertical lines.
|
||||
// If the face is StdFace or the font dosen't support a vertical direction, VAscent is 0.
|
||||
VAscent float64
|
||||
|
||||
// VDescent is the distance in pixels from the top of a line to its baseline for vertical lines.
|
||||
// If the face is StdFace or the font dosen't support a vertical direction, VDescent is 0.
|
||||
VDescent float64
|
||||
}
|
||||
|
||||
func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
|
||||
return float64(x>>6) + float64(x&((1<<6)-1))/float64(1<<6)
|
||||
}
|
||||
|
||||
func float64ToFixed26_6(x float64) fixed.Int26_6 {
|
||||
i := math.Floor(x)
|
||||
frac := x - i
|
||||
return fixed.Int26_6(i)<<6 + fixed.Int26_6(frac*(1<<6))
|
||||
}
|
||||
|
||||
func adjustOffsetGranularity(x fixed.Int26_6) fixed.Int26_6 {
|
||||
return x / (1 << 4) * (1 << 4)
|
||||
}
|
||||
|
||||
// Glyph represents one glyph to render.
|
||||
type Glyph struct {
|
||||
// Rune is a character for this glyph.
|
||||
Rune rune
|
||||
|
||||
// IndexInBytes is an index in bytes for the given string at AppendGlyphs.
|
||||
IndexInBytes int
|
||||
|
||||
// Image is a rasterized glyph image.
|
||||
// Image is a grayscale image i.e. RGBA values are the same.
|
||||
// Image should be used as a render source and should not be modified.
|
||||
Image *ebiten.Image
|
||||
|
||||
// X is the X position to render this glyph.
|
||||
// The position is determined in a sequence of characters given at AppendGlyphs.
|
||||
// The position's origin is the first character's origin position.
|
||||
X float64
|
||||
|
||||
// Y is the Y position to render this glyph.
|
||||
// The position is determined in a sequence of characters given at AppendGlyphs.
|
||||
// The position's origin is the first character's origin position.
|
||||
Y float64
|
||||
|
||||
// GID is an ID for a glyph of TrueType or OpenType font. GID is valid when the font is GoTextFont.
|
||||
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.
|
||||
//
|
||||
// Advance is concurrent-safe.
|
||||
func Advance(face Face, text string) float64 {
|
||||
return face.advance(text)
|
||||
}
|
||||
|
||||
// Direction represents a direction of text rendering.
|
||||
// Direction indicates both the primary direction, in which a text in one line is rendered,
|
||||
// and the secondary direction, in which multiple lines are rendered.
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
// DirectionLeftToRight indicates that the primary direction is from left to right,
|
||||
// and the secondary direction is from top to bottom.
|
||||
DirectionLeftToRight Direction = iota
|
||||
|
||||
// DirectionRightToLeft indicates that the primary direction is from right to left,
|
||||
// and the secondary direction is from top to bottom.
|
||||
DirectionRightToLeft
|
||||
|
||||
// DirectionTopToBottomAndLeftToRight indicates that the primary direction is from top to bottom,
|
||||
// and the secondary direction is from left to right.
|
||||
// This is used e.g. for Mongolian.
|
||||
DirectionTopToBottomAndLeftToRight
|
||||
|
||||
// DirectionTopToBottomAndRightToLeft indicates that the primary direction is from top to bottom,
|
||||
// and the secondary direction is from right to left.
|
||||
// This is used e.g. for Japanese.
|
||||
DirectionTopToBottomAndRightToLeft
|
||||
)
|
||||
|
||||
func (d Direction) isHorizontal() bool {
|
||||
switch d {
|
||||
case DirectionLeftToRight, DirectionRightToLeft:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Measure measures the boundary size of the text.
|
||||
// With a horizontal direction face, the width is the longest line's advance, and the height is the total of line heights.
|
||||
// With a vertical direction face, the width and the height are calculated in an opposite manner.
|
||||
//
|
||||
// Measure is concurrent-safe.
|
||||
func Measure(text string, face Face, lineHeightInPixels float64) (width, height float64) {
|
||||
if text == "" {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
var primary float64
|
||||
var lineCount int
|
||||
for t := text; ; {
|
||||
lineCount++
|
||||
line, rest, found := strings.Cut(t, "\n")
|
||||
a := face.advance(line)
|
||||
if primary < a {
|
||||
primary = a
|
||||
}
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
t = rest
|
||||
}
|
||||
|
||||
m := face.Metrics()
|
||||
|
||||
if face.direction().isHorizontal() {
|
||||
secondary := float64(lineCount-1)*lineHeightInPixels + m.HAscent + m.HDescent
|
||||
return primary, secondary
|
||||
}
|
||||
secondary := float64(lineCount-1)*lineHeightInPixels + m.VAscent + m.VDescent
|
||||
return secondary, primary
|
||||
}
|
Loading…
Reference in New Issue
Block a user