mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 11:18: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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
@ -28,7 +27,7 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/examples/keyboard/keyboard"
|
"github.com/hajimehoshi/ebiten/v2/examples/keyboard/keyboard"
|
||||||
rkeyboard "github.com/hajimehoshi/ebiten/v2/examples/resources/images/keyboard"
|
rkeyboard "github.com/hajimehoshi/ebiten/v2/examples/resources/images/keyboard"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
"github.com/hajimehoshi/ebiten/v2/text"
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -36,6 +35,8 @@ const (
|
|||||||
screenHeight = 240
|
screenHeight = 240
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var fontFace = text.NewStdFace(bitmapfont.Face)
|
||||||
|
|
||||||
var keyboardImage *ebiten.Image
|
var keyboardImage *ebiten.Image
|
||||||
|
|
||||||
func init() {
|
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.
|
// 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) {
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
|
@ -26,7 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
"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"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,8 +39,8 @@ const sampleText = ` The quick brown fox jumps
|
|||||||
over the lazy dog.`
|
over the lazy dog.`
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mplusNormalFont font.Face
|
mplusNormalFace *text.StdFace
|
||||||
mplusBigFont font.Face
|
mplusBigFace *text.StdFace
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -50,7 +50,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dpi = 72
|
const dpi = 72
|
||||||
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
|
mplusNormalFont, err := opentype.NewFace(tt, &opentype.FaceOptions{
|
||||||
Size: 24,
|
Size: 24,
|
||||||
DPI: dpi,
|
DPI: dpi,
|
||||||
Hinting: font.HintingVertical,
|
Hinting: font.HintingVertical,
|
||||||
@ -58,7 +58,9 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
|
mplusNormalFace = text.NewStdFace(mplusNormalFont)
|
||||||
|
|
||||||
|
mplusBigFont, err := opentype.NewFace(tt, &opentype.FaceOptions{
|
||||||
Size: 32,
|
Size: 32,
|
||||||
DPI: dpi,
|
DPI: dpi,
|
||||||
Hinting: font.HintingVertical,
|
Hinting: font.HintingVertical,
|
||||||
@ -66,43 +68,24 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
mplusBigFace = text.NewStdFace(mplusBigFont)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
counter int
|
counter int
|
||||||
kanjiText []rune
|
kanjiText []rune
|
||||||
kanjiTextColor color.RGBA
|
kanjiTextColor color.RGBA
|
||||||
glyphs []text.Glyph
|
glyphs [][]text.Glyph
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update() error {
|
func (g *Game) Update() error {
|
||||||
// Initialize the glyphs for special (colorful) rendering.
|
// Initialize the glyphs for special (colorful) rendering.
|
||||||
if len(g.glyphs) == 0 {
|
if len(g.glyphs) == 0 {
|
||||||
g.glyphs = text.AppendGlyphs(g.glyphs, mplusNormalFont, sampleText)
|
for _, line := range strings.Split(sampleText, "\n") {
|
||||||
}
|
g.glyphs = append(g.glyphs, text.AppendGlyphs(nil, line, mplusNormalFace, 0, 0))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
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}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
|
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
|
||||||
@ -114,30 +97,40 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const x, y = 20, 40
|
const x, y = 20, 40
|
||||||
b := boundString(mplusNormalFont, sampleText)
|
w, h := text.Measure(sampleText, mplusNormalFace, mplusNormalFace.Metrics().Height)
|
||||||
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)
|
vector.DrawFilledRect(screen, x, y, float32(w), float32(h), gray, false)
|
||||||
text.Draw(screen, sampleText, mplusNormalFont, x, y, color.White)
|
op := &text.DrawOptions{}
|
||||||
|
op.GeoM.Translate(x, y)
|
||||||
|
op.LineHeightInPixels = mplusNormalFace.Metrics().Height
|
||||||
|
text.Draw(screen, sampleText, mplusNormalFace, op)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const x, y = 20, 140
|
const x, y = 20, 140
|
||||||
b := boundString(mplusBigFont, sampleText)
|
w, h := text.Measure(sampleText, mplusBigFace, mplusBigFace.Metrics().Height)
|
||||||
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)
|
vector.DrawFilledRect(screen, x, y, float32(w), float32(h), gray, false)
|
||||||
text.Draw(screen, sampleText, mplusBigFont, x, y, color.White)
|
op := &text.DrawOptions{}
|
||||||
|
op.GeoM.Translate(x, y)
|
||||||
|
op.LineHeightInPixels = mplusBigFace.Metrics().Height
|
||||||
|
text.Draw(screen, sampleText, mplusBigFace, op)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const x, y = 20, 240
|
const x, y = 20, 240
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &text.DrawOptions{}
|
||||||
op.GeoM.Rotate(math.Pi / 4)
|
op.GeoM.Rotate(math.Pi / 4)
|
||||||
op.GeoM.Translate(x, y)
|
op.GeoM.Translate(x, y)
|
||||||
op.Filter = ebiten.FilterLinear
|
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 x, y = 160, 240
|
||||||
const lineHeight = 80
|
const lineHeight = 80
|
||||||
b := boundString(text.FaceWithLineHeight(mplusBigFont, lineHeight), sampleText)
|
w, h := text.Measure(sampleText, mplusBigFace, lineHeight)
|
||||||
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)
|
vector.DrawFilledRect(screen, x, y, float32(w), float32(h), gray, false)
|
||||||
text.Draw(screen, sampleText, text.FaceWithLineHeight(mplusBigFont, lineHeight), x, y, color.White)
|
op := &text.DrawOptions{}
|
||||||
|
op.GeoM.Translate(x, y)
|
||||||
|
op.LineHeightInPixels = lineHeight
|
||||||
|
text.Draw(screen, sampleText, mplusBigFace, op)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const x, y = 240, 400
|
const x, y = 240, 400
|
||||||
@ -145,25 +138,28 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
// g.glyphs is initialized by text.AppendGlyphs.
|
// g.glyphs is initialized by text.AppendGlyphs.
|
||||||
// You can customize how to render each glyph.
|
// You can customize how to render each glyph.
|
||||||
// In this example, multiple colors are used to render glyphs.
|
// In this example, multiple colors are used to render glyphs.
|
||||||
for i, gl := range g.glyphs {
|
for j, line := range g.glyphs {
|
||||||
op.GeoM.Reset()
|
for i, gl := range line {
|
||||||
op.GeoM.Translate(x, y)
|
op.GeoM.Reset()
|
||||||
op.GeoM.Translate(gl.X, gl.Y)
|
op.GeoM.Translate(x, y)
|
||||||
op.ColorScale.Reset()
|
op.GeoM.Translate(0, float64(j)*mplusNormalFace.Metrics().Height)
|
||||||
r := float32(1)
|
op.GeoM.Translate(gl.X, gl.Y)
|
||||||
if i%3 == 0 {
|
op.ColorScale.Reset()
|
||||||
r = 0.5
|
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"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/hajimehoshi/bitmapfont/v3"
|
"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"
|
||||||
"github.com/hajimehoshi/ebiten/v2/exp/textinput"
|
"github.com/hajimehoshi/ebiten/v2/exp/textinput"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"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"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fontFace = bitmapfont.FaceEA
|
var fontFace = text.NewStdFace(bitmapfont.FaceEA)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
screenWidth = 640
|
screenWidth = 640
|
||||||
@ -93,15 +91,15 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
|
|||||||
y = 0
|
y = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
lineHeight := fontFace.Metrics().Height.Ceil()
|
lineHeight := int(fontFace.Metrics().Height)
|
||||||
var nlCount int
|
var nlCount int
|
||||||
var lineStart int
|
var lineStart int
|
||||||
var prevAdvance fixed.Int26_6
|
var prevAdvance float64
|
||||||
for i, r := range t.text {
|
for i, r := range t.text {
|
||||||
var x0, x1 int
|
var x0, x1 int
|
||||||
currentAdvance := font.MeasureString(fontFace, t.text[lineStart:i])
|
currentAdvance := text.Advance(fontFace, t.text[lineStart:i])
|
||||||
if lineStart < i {
|
if lineStart < i {
|
||||||
x0 = ((prevAdvance + currentAdvance) / 2).Ceil()
|
x0 = int((prevAdvance + currentAdvance) / 2)
|
||||||
}
|
}
|
||||||
if r == '\n' {
|
if r == '\n' {
|
||||||
x1 = int(math.MaxInt32)
|
x1 = int(math.MaxInt32)
|
||||||
@ -110,10 +108,10 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
|
|||||||
for !utf8.ValidString(t.text[i:nextI]) {
|
for !utf8.ValidString(t.text[i:nextI]) {
|
||||||
nextI++
|
nextI++
|
||||||
}
|
}
|
||||||
nextAdvance := font.MeasureString(fontFace, t.text[lineStart:nextI])
|
nextAdvance := text.Advance(fontFace, t.text[lineStart:nextI])
|
||||||
x1 = ((currentAdvance + nextAdvance) / 2).Ceil()
|
x1 = int((currentAdvance + nextAdvance) / 2)
|
||||||
} else {
|
} else {
|
||||||
x1 = currentAdvance.Ceil()
|
x1 = int(currentAdvance)
|
||||||
}
|
}
|
||||||
if x0 <= x && x < x1 && nlCount*lineHeight <= y && y < (nlCount+1)*lineHeight {
|
if x0 <= x && x < x1 && nlCount*lineHeight <= y && y < (nlCount+1)*lineHeight {
|
||||||
return i, true
|
return i, true
|
||||||
@ -159,7 +157,7 @@ func (t *TextField) Update() {
|
|||||||
cx, cy := t.cursorPos()
|
cx, cy := t.cursorPos()
|
||||||
px, py := textFieldPadding()
|
px, py := textFieldPadding()
|
||||||
x += cx + px
|
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)
|
t.ch, t.end = textinput.Start(x, y)
|
||||||
// Start returns nil for non-supported envrionments.
|
// Start returns nil for non-supported envrionments.
|
||||||
if t.ch == nil {
|
if t.ch == nil {
|
||||||
@ -257,8 +255,8 @@ func (t *TextField) cursorPos() (int, int) {
|
|||||||
if t.state.Text != "" {
|
if t.state.Text != "" {
|
||||||
txt += t.state.Text[:t.state.CompositionSelectionStartInBytes]
|
txt += t.state.Text[:t.state.CompositionSelectionStartInBytes]
|
||||||
}
|
}
|
||||||
x := font.MeasureString(fontFace, txt).Ceil()
|
x := int(text.Advance(fontFace, txt))
|
||||||
y := nlCount * fontFace.Metrics().Height.Ceil()
|
y := nlCount * int(fontFace.Metrics().Height)
|
||||||
return x, y
|
return x, y
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +274,7 @@ func (t *TextField) Draw(screen *ebiten.Image) {
|
|||||||
cx, cy := t.cursorPos()
|
cx, cy := t.cursorPos()
|
||||||
x += px + cx
|
x += px + cx
|
||||||
y += py + cy
|
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)
|
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
|
tx := t.bounds.Min.X + px
|
||||||
ty := t.bounds.Min.Y + py + fontFace.Metrics().Ascent.Ceil()
|
ty := t.bounds.Min.Y + py
|
||||||
text.Draw(screen, shownText, fontFace, tx, ty, color.Black)
|
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
|
const textFieldHeight = 24
|
||||||
|
|
||||||
func textFieldPadding() (int, int) {
|
func textFieldPadding() (int, int) {
|
||||||
m := fontFace.Metrics()
|
m := fontFace.Metrics()
|
||||||
return 4, (textFieldHeight - m.Height.Ceil()) / 2
|
return 4, (textFieldHeight - int(m.Height)) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
// Package text offers functions to draw texts on an Ebitengine's image.
|
// 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.
|
// For the example using a TTF font, see font package in the examples.
|
||||||
|
//
|
||||||
|
// Deprecated: as of v2.7. Use text/v2 instead.
|
||||||
package text
|
package text
|
||||||
|
|
||||||
import (
|
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