mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 11:18:54 +01:00
text/v2: bug fix: correct rendering vertical texts in Mongolian
Closes #2849 Updates go-text/typesetting#111
This commit is contained in:
parent
ef1fea890f
commit
6878bd79fc
@ -16,6 +16,12 @@ Open Font License 1.1
|
|||||||
|
|
||||||
https://fonts.google.com/noto/specimen/Noto+Sans+Myanmar/about
|
https://fonts.google.com/noto/specimen/Noto+Sans+Myanmar/about
|
||||||
|
|
||||||
|
# `NotoSansMongolian-Regular.ttf`
|
||||||
|
|
||||||
|
Open Font License 1.1
|
||||||
|
|
||||||
|
https://fonts.google.com/noto/specimen/Noto+Sans+Mongolian/about
|
||||||
|
|
||||||
# `NotoSansThai-Regular.ttf`
|
# `NotoSansThai-Regular.ttf`
|
||||||
|
|
||||||
Open Font License 1.1
|
Open Font License 1.1
|
||||||
|
BIN
examples/texti18n/NotoSansMongolian-Regular.ttf
Normal file
BIN
examples/texti18n/NotoSansMongolian-Regular.ttf
Normal file
Binary file not shown.
@ -83,6 +83,19 @@ func init() {
|
|||||||
myanmarFaceSource = s
|
myanmarFaceSource = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:embed NotoSansMongolian-Regular.ttf
|
||||||
|
var mongolianTTF []byte
|
||||||
|
|
||||||
|
var mongolianFaceSource *text.GoTextFaceSource
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
s, err := text.NewGoTextFaceSource(bytes.NewReader(mongolianTTF))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
mongolianFaceSource = s
|
||||||
|
}
|
||||||
|
|
||||||
var japaneseFaceSource *text.GoTextFaceSource
|
var japaneseFaceSource *text.GoTextFaceSource
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -167,7 +180,26 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
text.Draw(screen, thaiText, f, op)
|
text.Draw(screen, thaiText, f, op)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const japaneseText = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…"
|
const mongolianText = "ᠬᠦᠮᠦᠨ ᠪᠦᠷ ᠲᠥᠷᠥᠵᠦ ᠮᠡᠨᠳᠡᠯᠡᠬᠦ\nᠡᠷᠬᠡ ᠴᠢᠯᠥᠭᠡ ᠲᠡᠢ᠂ ᠠᠳᠠᠯᠢᠬᠠᠨ"
|
||||||
|
f := &text.GoTextFace{
|
||||||
|
Source: mongolianFaceSource,
|
||||||
|
Direction: text.DirectionTopToBottomAndLeftToRight,
|
||||||
|
Size: 24,
|
||||||
|
Language: language.Mongolian,
|
||||||
|
// language.Mongolian.Script() returns "Cyrl" (Cyrillic), but we want Mongolian script here.
|
||||||
|
Script: language.MustParseScript("Mong"),
|
||||||
|
}
|
||||||
|
const lineSpacing = 48
|
||||||
|
x, y := 20, 280
|
||||||
|
w, h := text.Measure(mongolianText, f, lineSpacing)
|
||||||
|
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false)
|
||||||
|
op := &text.DrawOptions{}
|
||||||
|
op.GeoM.Translate(float64(x), float64(y))
|
||||||
|
op.LineSpacingInPixels = lineSpacing
|
||||||
|
text.Draw(screen, mongolianText, f, op)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const japaneseText = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…\nあHello World.あ"
|
||||||
f := &text.GoTextFace{
|
f := &text.GoTextFace{
|
||||||
Source: japaneseFaceSource,
|
Source: japaneseFaceSource,
|
||||||
Direction: text.DirectionTopToBottomAndRightToLeft,
|
Direction: text.DirectionTopToBottomAndRightToLeft,
|
||||||
|
2
go.mod
2
go.mod
@ -5,7 +5,7 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b
|
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b
|
||||||
github.com/ebitengine/purego v0.6.0-alpha.2
|
github.com/ebitengine/purego v0.6.0-alpha.2
|
||||||
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d
|
github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658
|
||||||
github.com/hajimehoshi/bitmapfont/v3 v3.0.0
|
github.com/hajimehoshi/bitmapfont/v3 v3.0.0
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.4
|
github.com/hajimehoshi/go-mp3 v0.3.4
|
||||||
github.com/jakecoffman/cp v1.2.1
|
github.com/jakecoffman/cp v1.2.1
|
||||||
|
6
go.sum
6
go.sum
@ -2,9 +2,9 @@ github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b h1:gi7
|
|||||||
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b/go.mod h1:JtMbxJHZBDXfS8BmVYwzWk9Z6r7jsjwsHzOuZrEkfs4=
|
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b/go.mod h1:JtMbxJHZBDXfS8BmVYwzWk9Z6r7jsjwsHzOuZrEkfs4=
|
||||||
github.com/ebitengine/purego v0.6.0-alpha.2 h1:lYSvMtNBEjNGAzqPC5WP7bHUOxkFU3L+JZMdxK7krkw=
|
github.com/ebitengine/purego v0.6.0-alpha.2 h1:lYSvMtNBEjNGAzqPC5WP7bHUOxkFU3L+JZMdxK7krkw=
|
||||||
github.com/ebitengine/purego v0.6.0-alpha.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
github.com/ebitengine/purego v0.6.0-alpha.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d h1:AFVBrIZZMhmnB8NbkKayNVz714G048XqZGmNhXjvSag=
|
github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658 h1:KeDKnC99J3l5qJK4zV13Au2UwPn4N20TnIlM0YvILj8=
|
||||||
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d/go.mod h1:MrLApvxyzSW0MhQqLc484jkUWYX4wsEvEqDosB5Io80=
|
github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79 h1:3yBOzx29wog0i7TnUBMcp90EwIb+A5kqmr5vny1UOm8=
|
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
|
||||||
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
|
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
|
||||||
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
|
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
|
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
|
||||||
|
@ -278,11 +278,17 @@ func (g *GoTextFace) gScript() glanguage.Script {
|
|||||||
|
|
||||||
// advance implements Face.
|
// advance implements Face.
|
||||||
func (g *GoTextFace) advance(text string) float64 {
|
func (g *GoTextFace) advance(text string) float64 {
|
||||||
output, _ := g.Source.shape(text, g)
|
outputs, _ := g.Source.shape(text, g)
|
||||||
if g.direction().isHorizontal() {
|
|
||||||
return fixed26_6ToFloat64(output.Advance)
|
var a fixed.Int26_6
|
||||||
|
for _, output := range outputs {
|
||||||
|
a += output.Advance
|
||||||
}
|
}
|
||||||
return -fixed26_6ToFloat64(output.Advance)
|
|
||||||
|
if g.direction().isHorizontal() {
|
||||||
|
return fixed26_6ToFloat64(a)
|
||||||
|
}
|
||||||
|
return -fixed26_6ToFloat64(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasGlyph implements Face.
|
// hasGlyph implements Face.
|
||||||
|
@ -49,9 +49,9 @@ type glyph struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type goTextOutputCacheValue struct {
|
type goTextOutputCacheValue struct {
|
||||||
output shaping.Output
|
outputs []shaping.Output
|
||||||
glyphs []glyph
|
glyphs []glyph
|
||||||
atime int64
|
atime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type goTextGlyphImageCacheKey struct {
|
type goTextGlyphImageCacheKey struct {
|
||||||
@ -164,7 +164,7 @@ func (g *GoTextFaceSource) UnsafeInternal() font.Face {
|
|||||||
return g.f
|
return g.f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output, []glyph) {
|
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Output, []glyph) {
|
||||||
g.copyCheck()
|
g.copyCheck()
|
||||||
|
|
||||||
g.m.Lock()
|
g.m.Lock()
|
||||||
@ -173,7 +173,7 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
|
|||||||
key := face.outputCacheKey(text)
|
key := face.outputCacheKey(text)
|
||||||
if out, ok := g.outputCache[key]; ok {
|
if out, ok := g.outputCache[key]; ok {
|
||||||
out.atime = now()
|
out.atime = now()
|
||||||
return out.output, out.glyphs
|
return out.outputs, out.glyphs
|
||||||
}
|
}
|
||||||
|
|
||||||
g.f.SetVariations(face.variations)
|
g.f.SetVariations(face.variations)
|
||||||
@ -189,54 +189,81 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
|
|||||||
Script: face.gScript(),
|
Script: face.gScript(),
|
||||||
Language: language.Language(face.Language.String()),
|
Language: language.Language(face.Language.String()),
|
||||||
}
|
}
|
||||||
out := (&shaping.HarfbuzzShaper{}).Shape(input)
|
|
||||||
|
var inputs []shaping.Input
|
||||||
|
if face.Direction.isHorizontal() {
|
||||||
|
// shaping.Segmenter is not used for horizontal texts so far due to a bug (go-text/typesetting#127).
|
||||||
|
inputs = []shaping.Input{input}
|
||||||
|
} else {
|
||||||
|
var seg shaping.Segmenter
|
||||||
|
inputs = seg.Split(input, &singleFontmap{face: face.Source.f})
|
||||||
|
}
|
||||||
|
|
||||||
|
if face.Direction == DirectionRightToLeft {
|
||||||
|
// Reverse the input for RTL texts.
|
||||||
|
for i, j := 0, len(inputs)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
inputs[i], inputs[j] = inputs[j], inputs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs := make([]shaping.Output, len(inputs))
|
||||||
|
var gs []glyph
|
||||||
|
for i, input := range inputs {
|
||||||
|
out := (&shaping.HarfbuzzShaper{}).Shape(input)
|
||||||
|
outputs[i] = out
|
||||||
|
|
||||||
|
(shaping.Line{out}).AdjustBaselines()
|
||||||
|
|
||||||
|
var indices []int
|
||||||
|
for i := range text {
|
||||||
|
indices = append(indices, i)
|
||||||
|
}
|
||||||
|
indices = append(indices, len(text))
|
||||||
|
|
||||||
|
for _, gl := range out.Glyphs {
|
||||||
|
gl := gl
|
||||||
|
var segs []api.Segment
|
||||||
|
switch data := g.f.GlyphData(gl.GlyphID).(type) {
|
||||||
|
case api.GlyphOutline:
|
||||||
|
if out.Direction.IsSideways() {
|
||||||
|
data.Sideways(fixed26_6ToFloat32(-gl.YOffset) / fixed26_6ToFloat32(out.Size) * float32(face.Source.f.Upem()))
|
||||||
|
}
|
||||||
|
segs = data.Segments
|
||||||
|
case api.GlyphSVG:
|
||||||
|
segs = data.Outline.Segments
|
||||||
|
case api.GlyphBitmap:
|
||||||
|
if data.Outline != nil {
|
||||||
|
segs = data.Outline.Segments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scaledSegs := make([]api.Segment, len(segs))
|
||||||
|
scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
|
||||||
|
for i, seg := range segs {
|
||||||
|
scaledSegs[i] = seg
|
||||||
|
for j := range seg.Args {
|
||||||
|
scaledSegs[i].Args[j].X *= scale
|
||||||
|
scaledSegs[i].Args[j].Y *= -scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gs = append(gs, glyph{
|
||||||
|
shapingGlyph: &gl,
|
||||||
|
startIndex: indices[gl.ClusterIndex],
|
||||||
|
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
|
||||||
|
scaledSegments: scaledSegs,
|
||||||
|
bounds: segmentsToBounds(scaledSegs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if g.outputCache == nil {
|
if g.outputCache == nil {
|
||||||
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
|
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var indices []int
|
|
||||||
for i := range text {
|
|
||||||
indices = append(indices, i)
|
|
||||||
}
|
|
||||||
indices = append(indices, len(text))
|
|
||||||
|
|
||||||
gs := make([]glyph, len(out.Glyphs))
|
|
||||||
for i, gl := range out.Glyphs {
|
|
||||||
gl := gl
|
|
||||||
var segs []api.Segment
|
|
||||||
switch data := g.f.GlyphData(gl.GlyphID).(type) {
|
|
||||||
case api.GlyphOutline:
|
|
||||||
segs = data.Segments
|
|
||||||
case api.GlyphSVG:
|
|
||||||
segs = data.Outline.Segments
|
|
||||||
case api.GlyphBitmap:
|
|
||||||
if data.Outline != nil {
|
|
||||||
segs = data.Outline.Segments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scaledSegs := make([]api.Segment, len(segs))
|
|
||||||
scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
|
|
||||||
for i, seg := range segs {
|
|
||||||
scaledSegs[i] = seg
|
|
||||||
for j := range seg.Args {
|
|
||||||
scaledSegs[i].Args[j].X *= scale
|
|
||||||
scaledSegs[i].Args[j].Y *= -scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gs[i] = glyph{
|
|
||||||
shapingGlyph: &gl,
|
|
||||||
startIndex: indices[gl.ClusterIndex],
|
|
||||||
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
|
|
||||||
scaledSegments: scaledSegs,
|
|
||||||
bounds: segmentsToBounds(scaledSegs),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.outputCache[key] = &goTextOutputCacheValue{
|
g.outputCache[key] = &goTextOutputCacheValue{
|
||||||
output: out,
|
outputs: outputs,
|
||||||
glyphs: gs,
|
glyphs: gs,
|
||||||
atime: now(),
|
atime: now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheSoftLimit = 512
|
const cacheSoftLimit = 512
|
||||||
@ -250,7 +277,7 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, gs
|
return outputs, gs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GoTextFaceSource) scale(size float64) float64 {
|
func (g *GoTextFaceSource) scale(size float64) float64 {
|
||||||
@ -266,3 +293,11 @@ func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goT
|
|||||||
}
|
}
|
||||||
return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create)
|
return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type singleFontmap struct {
|
||||||
|
face font.Face
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singleFontmap) ResolveFace(r rune) font.Face {
|
||||||
|
return s.face
|
||||||
|
}
|
||||||
|
@ -186,22 +186,18 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
|
|||||||
boundaryWidth = longestAdvance
|
boundaryWidth = longestAdvance
|
||||||
boundaryHeight = float64(lineCount-1)*options.LineSpacingInPixels + m.HAscent + m.HDescent
|
boundaryHeight = float64(lineCount-1)*options.LineSpacingInPixels + m.HAscent + m.HDescent
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: Perhaps HAscent and HDescent should be used for sideways glyphs.
|
||||||
boundaryWidth = float64(lineCount-1)*options.LineSpacingInPixels + m.VAscent + m.VDescent
|
boundaryWidth = float64(lineCount-1)*options.LineSpacingInPixels + m.VAscent + m.VDescent
|
||||||
boundaryHeight = longestAdvance
|
boundaryHeight = longestAdvance
|
||||||
}
|
}
|
||||||
|
|
||||||
var offsetX, offsetY float64
|
var offsetX, offsetY float64
|
||||||
|
|
||||||
// The Y position has an offset by an ascent for horizontal texts.
|
|
||||||
if d.isHorizontal() {
|
|
||||||
offsetY += m.HAscent
|
|
||||||
}
|
|
||||||
// TODO: Adjust offsets for vertical texts.
|
|
||||||
|
|
||||||
// Adjust the offset based on the secondary alignments.
|
// Adjust the offset based on the secondary alignments.
|
||||||
h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
|
h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
|
||||||
switch d {
|
switch d {
|
||||||
case DirectionLeftToRight, DirectionRightToLeft:
|
case DirectionLeftToRight, DirectionRightToLeft:
|
||||||
|
offsetY += m.HAscent
|
||||||
switch v {
|
switch v {
|
||||||
case verticalAlignTop:
|
case verticalAlignTop:
|
||||||
case verticalAlignCenter:
|
case verticalAlignCenter:
|
||||||
@ -210,7 +206,8 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
|
|||||||
offsetY -= boundaryHeight
|
offsetY -= boundaryHeight
|
||||||
}
|
}
|
||||||
case DirectionTopToBottomAndLeftToRight:
|
case DirectionTopToBottomAndLeftToRight:
|
||||||
offsetX -= m.VAscent
|
// TODO: Perhaps HDescent should be used for sideways glyphs.
|
||||||
|
offsetX += m.VDescent
|
||||||
switch h {
|
switch h {
|
||||||
case horizontalAlignLeft:
|
case horizontalAlignLeft:
|
||||||
case horizontalAlignCenter:
|
case horizontalAlignCenter:
|
||||||
@ -219,6 +216,7 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
|
|||||||
offsetX -= boundaryWidth
|
offsetX -= boundaryWidth
|
||||||
}
|
}
|
||||||
case DirectionTopToBottomAndRightToLeft:
|
case DirectionTopToBottomAndRightToLeft:
|
||||||
|
// TODO: Perhaps HAscent should be used for sideways glyphs.
|
||||||
offsetX -= m.VAscent
|
offsetX -= m.VAscent
|
||||||
switch h {
|
switch h {
|
||||||
case horizontalAlignLeft:
|
case horizontalAlignLeft:
|
||||||
|
Loading…
Reference in New Issue
Block a user