diff --git a/examples/texti18n/LICENSE.md b/examples/texti18n/LICENSE.md index 975c21361..34f1e5cfb 100644 --- a/examples/texti18n/LICENSE.md +++ b/examples/texti18n/LICENSE.md @@ -16,6 +16,12 @@ Open Font License 1.1 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` Open Font License 1.1 diff --git a/examples/texti18n/NotoSansMongolian-Regular.ttf b/examples/texti18n/NotoSansMongolian-Regular.ttf new file mode 100644 index 000000000..9612379ac Binary files /dev/null and b/examples/texti18n/NotoSansMongolian-Regular.ttf differ diff --git a/examples/texti18n/main.go b/examples/texti18n/main.go index 24b8e83b6..0021957c2 100644 --- a/examples/texti18n/main.go +++ b/examples/texti18n/main.go @@ -83,6 +83,19 @@ func init() { 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 func init() { @@ -167,7 +180,26 @@ func (g *Game) Draw(screen *ebiten.Image) { 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{ Source: japaneseFaceSource, Direction: text.DirectionTopToBottomAndRightToLeft, diff --git a/go.mod b/go.mod index 012f66e26..1a4daa30a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b 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/go-mp3 v0.3.4 github.com/jakecoffman/cp v1.2.1 diff --git a/go.sum b/go.sum index 7556b53b9..a56c39a85 100644 --- a/go.sum +++ b/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/purego v0.6.0-alpha.2 h1:lYSvMtNBEjNGAzqPC5WP7bHUOxkFU3L+JZMdxK7krkw= 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-20231211160022-6295f3c76f4d/go.mod h1:MrLApvxyzSW0MhQqLc484jkUWYX4wsEvEqDosB5Io80= -github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79 h1:3yBOzx29wog0i7TnUBMcp90EwIb+A5kqmr5vny1UOm8= +github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658 h1:KeDKnC99J3l5qJK4zV13Au2UwPn4N20TnIlM0YvILj8= +github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= +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/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA= github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= diff --git a/text/v2/gotext.go b/text/v2/gotext.go index 150331b4a..3373693ec 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -278,11 +278,17 @@ func (g *GoTextFace) gScript() glanguage.Script { // advance implements Face. func (g *GoTextFace) advance(text string) float64 { - output, _ := g.Source.shape(text, g) - if g.direction().isHorizontal() { - return fixed26_6ToFloat64(output.Advance) + outputs, _ := g.Source.shape(text, g) + + 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. diff --git a/text/v2/gotextfacesource.go b/text/v2/gotextfacesource.go index 7360a5b11..46c0cffc8 100644 --- a/text/v2/gotextfacesource.go +++ b/text/v2/gotextfacesource.go @@ -49,9 +49,9 @@ type glyph struct { } type goTextOutputCacheValue struct { - output shaping.Output - glyphs []glyph - atime int64 + outputs []shaping.Output + glyphs []glyph + atime int64 } type goTextGlyphImageCacheKey struct { @@ -164,7 +164,7 @@ func (g *GoTextFaceSource) UnsafeInternal() font.Face { 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.m.Lock() @@ -173,7 +173,7 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output, key := face.outputCacheKey(text) if out, ok := g.outputCache[key]; ok { out.atime = now() - return out.output, out.glyphs + return out.outputs, out.glyphs } g.f.SetVariations(face.variations) @@ -189,54 +189,81 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output, Script: face.gScript(), 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 { 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{ - output: out, - glyphs: gs, - atime: now(), + outputs: outputs, + glyphs: gs, + atime: now(), } 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 { @@ -266,3 +293,11 @@ func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goT } 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 +} diff --git a/text/v2/layout.go b/text/v2/layout.go index 85a4879c7..3dc720216 100644 --- a/text/v2/layout.go +++ b/text/v2/layout.go @@ -186,22 +186,18 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str boundaryWidth = longestAdvance boundaryHeight = float64(lineCount-1)*options.LineSpacingInPixels + m.HAscent + m.HDescent } else { + // TODO: Perhaps HAscent and HDescent should be used for sideways glyphs. boundaryWidth = float64(lineCount-1)*options.LineSpacingInPixels + m.VAscent + m.VDescent boundaryHeight = longestAdvance } 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. h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign) switch d { case DirectionLeftToRight, DirectionRightToLeft: + offsetY += m.HAscent switch v { case verticalAlignTop: case verticalAlignCenter: @@ -210,7 +206,8 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str offsetY -= boundaryHeight } case DirectionTopToBottomAndLeftToRight: - offsetX -= m.VAscent + // TODO: Perhaps HDescent should be used for sideways glyphs. + offsetX += m.VDescent switch h { case horizontalAlignLeft: case horizontalAlignCenter: @@ -219,6 +216,7 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str offsetX -= boundaryWidth } case DirectionTopToBottomAndRightToLeft: + // TODO: Perhaps HAscent should be used for sideways glyphs. offsetX -= m.VAscent switch h { case horizontalAlignLeft: