Compare commits

...

3 Commits

Author SHA1 Message Date
Hajime Hoshi
df266e8acf text/v2: add Glyph.OriginOffset{X,Y}
Closes #3070
2024-08-23 03:59:16 +09:00
Hajime Hoshi
6056fc59eb text/v2: update comments about CacheGlyphs
The example was not clear.
2024-08-23 00:13:20 +09:00
Hajime Hoshi
a3d084e2de text/v2: add Glyph.Origin{X,Y}
Closes #3070
2024-08-23 00:00:30 +09:00
5 changed files with 105 additions and 16 deletions

View File

@ -21,7 +21,9 @@ import (
"math" "math"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/hajimehoshi/ebiten/v2/text/v2"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
@ -58,10 +60,8 @@ func init() {
} }
type Game struct { type Game struct {
counter int glyphs []text.Glyph
kanjiText []rune showOrigins bool
kanjiTextColor color.RGBA
glyphs []text.Glyph
} }
func (g *Game) Update() error { func (g *Game) Update() error {
@ -71,10 +71,15 @@ func (g *Game) Update() error {
op.LineSpacing = mplusNormalFace.Size * 1.5 op.LineSpacing = mplusNormalFace.Size * 1.5
g.glyphs = text.AppendGlyphs(g.glyphs, sampleText, mplusNormalFace, op) g.glyphs = text.AppendGlyphs(g.glyphs, sampleText, mplusNormalFace, op)
} }
if inpututil.IsKeyJustPressed(ebiten.KeyO) {
g.showOrigins = !g.showOrigins
}
return nil return nil
} }
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Press O to show/hide origins")
gray := color.RGBA{0x80, 0x80, 0x80, 0xff} gray := color.RGBA{0x80, 0x80, 0x80, 0xff}
{ {
@ -147,6 +152,12 @@ func (g *Game) Draw(screen *ebiten.Image) {
op.ColorScale.Scale(r, g, b, 1) op.ColorScale.Scale(r, g, b, 1)
screen.DrawImage(gl.Image, op) screen.DrawImage(gl.Image, op)
} }
if g.showOrigins {
for _, gl := range g.glyphs {
vector.DrawFilledCircle(screen, x+float32(gl.OriginX), y+float32(gl.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
}
}
} }
} }

View File

@ -23,7 +23,9 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/hajimehoshi/ebiten/v2/text/v2"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
@ -109,13 +111,19 @@ const (
) )
type Game struct { type Game struct {
showOrigins bool
} }
func (g *Game) Update() error { func (g *Game) Update() error {
if inpututil.IsKeyJustPressed(ebiten.KeyO) {
g.showOrigins = !g.showOrigins
}
return nil return nil
} }
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Press O to show/hide origins.\nRed points are the original origin positions.\nThe green points are the origin positions after applying the offset.")
gray := color.RGBA{0x80, 0x80, 0x80, 0xff} gray := color.RGBA{0x80, 0x80, 0x80, 0xff}
{ {
@ -126,13 +134,21 @@ func (g *Game) Draw(screen *ebiten.Image) {
Size: 24, Size: 24,
Language: language.Arabic, Language: language.Arabic,
} }
x, y := screenWidth-20, 40 x, y := screenWidth-20, 50
w, h := text.Measure(arabicText, f, 0) w, h := text.Measure(arabicText, f, 0)
// The left upper point is not x but x-w, since the text runs in the rigth-to-left direction. // The left upper point is not x but x-w, since the text runs in the rigth-to-left direction.
vector.DrawFilledRect(screen, float32(x)-float32(w), float32(y), float32(w), float32(h), gray, false) vector.DrawFilledRect(screen, float32(x)-float32(w), float32(y), float32(w), float32(h), gray, false)
op := &text.DrawOptions{} op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
text.Draw(screen, arabicText, f, op) text.Draw(screen, arabicText, f, op)
if g.showOrigins {
op := &text.LayoutOptions{}
for _, g := range text.AppendGlyphs(nil, arabicText, f, op) {
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX+g.OriginOffsetX), float32(y)+float32(g.OriginY+g.OriginOffsetY), 2, color.RGBA{0, 0xff, 0, 0xff}, true)
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX), float32(y)+float32(g.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
}
}
} }
{ {
const hindiText = "चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान" const hindiText = "चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान"
@ -141,12 +157,20 @@ func (g *Game) Draw(screen *ebiten.Image) {
Size: 24, Size: 24,
Language: language.Hindi, Language: language.Hindi,
} }
x, y := 20, 100 x, y := 20, 110
w, h := text.Measure(hindiText, f, 0) w, h := text.Measure(hindiText, f, 0)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false) vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false)
op := &text.DrawOptions{} op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
text.Draw(screen, hindiText, f, op) text.Draw(screen, hindiText, f, op)
if g.showOrigins {
op := &text.LayoutOptions{}
for _, g := range text.AppendGlyphs(nil, hindiText, f, op) {
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX+g.OriginOffsetX), float32(y)+float32(g.OriginY+g.OriginOffsetY), 2, color.RGBA{0, 0xff, 0, 0xff}, true)
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX), float32(y)+float32(g.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
}
}
} }
{ {
const myanmarText = "လူခပ်သိမ်း၏ မျိုးရိုးဂုဏ်သိက္ခာနှင့်တကွ" const myanmarText = "လူခပ်သိမ်း၏ မျိုးရိုးဂုဏ်သိက္ခာနှင့်တကွ"
@ -155,12 +179,20 @@ func (g *Game) Draw(screen *ebiten.Image) {
Size: 24, Size: 24,
Language: language.Burmese, Language: language.Burmese,
} }
x, y := 20, 160 x, y := 20, 170
w, h := text.Measure(myanmarText, f, 0) w, h := text.Measure(myanmarText, f, 0)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false) vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false)
op := &text.DrawOptions{} op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
text.Draw(screen, myanmarText, f, op) text.Draw(screen, myanmarText, f, op)
if g.showOrigins {
op := &text.LayoutOptions{}
for _, g := range text.AppendGlyphs(nil, myanmarText, f, op) {
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX+g.OriginOffsetX), float32(y)+float32(g.OriginY+g.OriginOffsetY), 2, color.RGBA{0, 0xff, 0, 0xff}, true)
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX), float32(y)+float32(g.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
}
}
} }
{ {
const thaiText = "โดยที่การยอมรับนับถือเกียรติศักดิ์ประจำตัว" const thaiText = "โดยที่การยอมรับนับถือเกียรติศักดิ์ประจำตัว"
@ -169,12 +201,20 @@ func (g *Game) Draw(screen *ebiten.Image) {
Size: 24, Size: 24,
Language: language.Thai, Language: language.Thai,
} }
x, y := 20, 220 x, y := 20, 230
w, h := text.Measure(thaiText, f, 0) w, h := text.Measure(thaiText, f, 0)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false) vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false)
op := &text.DrawOptions{} op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
text.Draw(screen, thaiText, f, op) text.Draw(screen, thaiText, f, op)
if g.showOrigins {
op := &text.LayoutOptions{}
for _, g := range text.AppendGlyphs(nil, thaiText, f, op) {
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX+g.OriginOffsetX), float32(y)+float32(g.OriginY+g.OriginOffsetY), 2, color.RGBA{0, 0xff, 0, 0xff}, true)
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX), float32(y)+float32(g.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
}
}
} }
{ {
const mongolianText = "ᠬᠦᠮᠦᠨ ᠪᠦᠷ ᠲᠥᠷᠥᠵᠦ ᠮᠡᠨᠳᠡᠯᠡᠬᠦ\nᠡᠷᠬᠡ ᠴᠢᠯᠥᠭᠡ ᠲᠡᠢ᠂ ᠠᠳᠠᠯᠢᠬᠠᠨ" const mongolianText = "ᠬᠦᠮᠦᠨ ᠪᠦᠷ ᠲᠥᠷᠥᠵᠦ ᠮᠡᠨᠳᠡᠯᠡᠬᠦ\nᠡᠷᠬᠡ ᠴᠢᠯᠥᠭᠡ ᠲᠡᠢ᠂ ᠠᠳᠠᠯᠢᠬᠠᠨ"
@ -187,13 +227,22 @@ func (g *Game) Draw(screen *ebiten.Image) {
Script: language.MustParseScript("Mong"), Script: language.MustParseScript("Mong"),
} }
const lineSpacing = 48 const lineSpacing = 48
x, y := 20, 280 x, y := 20, 290
w, h := text.Measure(mongolianText, f, lineSpacing) w, h := text.Measure(mongolianText, f, lineSpacing)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false) vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false)
op := &text.DrawOptions{} op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
op.LineSpacing = lineSpacing op.LineSpacing = lineSpacing
text.Draw(screen, mongolianText, f, op) text.Draw(screen, mongolianText, f, op)
if g.showOrigins {
op := &text.LayoutOptions{}
op.LineSpacing = lineSpacing
for _, g := range text.AppendGlyphs(nil, mongolianText, f, op) {
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX+g.OriginOffsetX), float32(y)+float32(g.OriginY+g.OriginOffsetY), 2, color.RGBA{0, 0xff, 0, 0xff}, true)
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX), float32(y)+float32(g.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
}
}
} }
{ {
const japaneseText = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…\nあHello World.あ" const japaneseText = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…\nあHello World.あ"
@ -204,7 +253,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
Language: language.Japanese, Language: language.Japanese,
} }
const lineSpacing = 48 const lineSpacing = 48
x, y := screenWidth-20, 280 x, y := screenWidth-20, 290
w, h := text.Measure(japaneseText, f, lineSpacing) w, h := text.Measure(japaneseText, f, lineSpacing)
// The left upper point is not x but x-w, since the text runs in the rigth-to-left direction as the secondary direction. // The left upper point is not x but x-w, since the text runs in the rigth-to-left direction as the secondary direction.
vector.DrawFilledRect(screen, float32(x)-float32(w), float32(y), float32(w), float32(h), gray, false) vector.DrawFilledRect(screen, float32(x)-float32(w), float32(y), float32(w), float32(h), gray, false)
@ -212,6 +261,15 @@ func (g *Game) Draw(screen *ebiten.Image) {
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
op.LineSpacing = lineSpacing op.LineSpacing = lineSpacing
text.Draw(screen, japaneseText, f, op) text.Draw(screen, japaneseText, f, op)
if g.showOrigins {
op := &text.LayoutOptions{}
op.LineSpacing = lineSpacing
for _, g := range text.AppendGlyphs(nil, japaneseText, f, op) {
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX), float32(y)+float32(g.OriginY), 2, color.RGBA{0xff, 0, 0, 0xff}, true)
vector.DrawFilledCircle(screen, float32(x)+float32(g.OriginX+g.OriginOffsetX), float32(y)+float32(g.OriginY+g.OriginOffsetY), 2, color.RGBA{0, 0xff, 0, 0xff}, true)
}
}
} }
} }

View File

@ -304,10 +304,11 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
} }
_, gs := g.Source.shape(line, g) _, gs := g.Source.shape(line, g)
for _, glyph := range gs { for _, glyph := range gs {
img, imgX, imgY := g.glyphImage(glyph, origin.Add(fixed.Point26_6{ o := origin.Add(fixed.Point26_6{
X: glyph.shapingGlyph.XOffset, X: glyph.shapingGlyph.XOffset,
Y: -glyph.shapingGlyph.YOffset, Y: -glyph.shapingGlyph.YOffset,
})) })
img, imgX, imgY := g.glyphImage(glyph, o)
// Append a glyph even if img is nil. // Append a glyph even if img is nil.
// This is necessary to return index information for control characters. // This is necessary to return index information for control characters.
glyphs = append(glyphs, Glyph{ glyphs = append(glyphs, Glyph{
@ -317,6 +318,10 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
Image: img, Image: img,
X: float64(imgX), X: float64(imgX),
Y: float64(imgY), Y: float64(imgY),
OriginX: fixed26_6ToFloat64(origin.X),
OriginY: fixed26_6ToFloat64(origin.Y),
OriginOffsetX: fixed26_6ToFloat64(glyph.shapingGlyph.XOffset),
OriginOffsetY: fixed26_6ToFloat64(-glyph.shapingGlyph.YOffset),
}) })
origin = origin.Add(fixed.Point26_6{ origin = origin.Add(fixed.Point26_6{
X: glyph.shapingGlyph.XAdvance, X: glyph.shapingGlyph.XAdvance,

View File

@ -124,6 +124,10 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i
Image: img, Image: img,
X: float64(imgX), X: float64(imgX),
Y: float64(imgY), Y: float64(imgY),
OriginX: fixed26_6ToFloat64(origin.X),
OriginY: fixed26_6ToFloat64(origin.Y),
OriginOffsetX: 0,
OriginOffsetY: 0,
}) })
origin.X += a origin.X += a
prevR = r prevR = r

View File

@ -141,6 +141,20 @@ type Glyph struct {
// The position is determined in a sequence of characters given at AppendGlyphs. // The position is determined in a sequence of characters given at AppendGlyphs.
// The position's origin is the first character's origin position. // The position's origin is the first character's origin position.
Y float64 Y float64
// OriginX is the X position of the origin of this glyph.
OriginX float64
// OriginY is the Y position of the origin of this glyph.
OriginY float64
// OriginOffsetX is the adjustment value to the X position of the origin of this glyph.
// OriginOffsetX is usually 0, but can be non-zero for some special glyphs or glyphs in the vertical text layout.
OriginOffsetX float64
// OriginOffsetY is the adjustment value to the Y position of the origin of this glyph.
// OriginOffsetY is usually 0, but can be non-zero for some special glyphs or glyphs in the vertical text layout.
OriginOffsetY float64
} }
// Advance returns the advanced distance from the origin position when rendering the given text with the given face. // Advance returns the advanced distance from the origin position when rendering the given text with the given face.
@ -233,10 +247,7 @@ func Measure(text string, face Face, lineSpacingInPixels float64) (width, height
// CacheGlyphs creates all such variations for one rune, while Draw and AppendGlyphs create only necessary glyphs. // CacheGlyphs creates all such variations for one rune, while Draw and AppendGlyphs create only necessary glyphs.
// //
// Draw and AppendGlyphs automatically create and cache necessary glyphs, so usually you don't have to call CacheGlyphs explicitly. // Draw and AppendGlyphs automatically create and cache necessary glyphs, so usually you don't have to call CacheGlyphs explicitly.
// However, for example, when you call Draw for each rune of one big text, Draw tries to create the glyph cache and render it for each rune. // If you really care about the performance, CacheGlyphs might be useful.
// This is very inefficient because creating a glyph image and rendering it are different operations
// (`(*ebiten.Image).WritePixels` and `(*ebiten.Image).DrawImage`) and can never be merged as one draw call.
// CacheGlyphs creates necessary glyphs without rendering them so that these operations are likely merged into one draw call regardless of the size of the text.
// //
// CacheGlyphs is concurrent-safe. // CacheGlyphs is concurrent-safe.
func CacheGlyphs(text string, face Face) { func CacheGlyphs(text string, face Face) {