From b925f28104ec33f9ef8445a60d17179fb6792372 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 26 Nov 2023 02:36:08 +0900 Subject: [PATCH] text/v2: add MultiFace Closes #2845 --- examples/mixedfont/main.go | 90 ++++++++++++++++++++ text/v2/gotext.go | 6 ++ text/v2/multi.go | 167 +++++++++++++++++++++++++++++++++++++ text/v2/std.go | 8 +- text/v2/text.go | 4 +- 5 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 examples/mixedfont/main.go create mode 100644 text/v2/multi.go diff --git a/examples/mixedfont/main.go b/examples/mixedfont/main.go new file mode 100644 index 000000000..fc87fa15e --- /dev/null +++ b/examples/mixedfont/main.go @@ -0,0 +1,90 @@ +// 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 main + +import ( + "bytes" + "log" + + "golang.org/x/image/font/gofont/goregular" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" + "github.com/hajimehoshi/ebiten/v2/text/v2" +) + +const ( + screenWidth = 640 + screenHeight = 480 +) + +var ( + goRegularFaceSource *text.GoTextFaceSource + mplusFaceSource *text.GoTextFaceSource +) + +func init() { + s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf)) + if err != nil { + log.Fatal(err) + } + mplusFaceSource = s +} + +func init() { + s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF)) + if err != nil { + log.Fatal(err) + } + goRegularFaceSource = s +} + +type Game struct{} + +func (g *Game) Update() error { + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + f := text.MultiFace([]text.Face{ + // goregular.TTF is used primarily. If a glyph is not found in this font, the second font is used. + &text.GoTextFace{ + Source: goRegularFaceSource, + Size: 24, + }, + // M+ Font is the second font. + // Use a relatively big size to see different-sized faces are well mixed. + &text.GoTextFace{ + Source: mplusFaceSource, + Size: 32, + }, + }) + op := &text.DrawOptions{} + op.GeoM.Translate(20, 20) + op.LineSpacingInPixels = 48 + text.Draw(screen, "HelloこんにちはWorld世界\n日本語とEnglish", f, op) +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return screenWidth, screenHeight +} + +func main() { + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("Mixed Font Faces (Ebitengine Demo)") + if err := ebiten.RunGame(&Game{}); err != nil { + log.Fatal(err) + } +} diff --git a/text/v2/gotext.go b/text/v2/gotext.go index 331996dc1..3cfa8d083 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -285,6 +285,12 @@ func (g *GoTextFace) advance(text string) float64 { return -fixed26_6ToFloat64(output.Advance) } +// hasGlyph implements Face. +func (g *GoTextFace) hasGlyph(r rune) bool { + _, ok := g.Source.f.Cmap.Lookup(r) + return ok +} + // appendGlyphsForLine implements Face. func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { origin := fixed.Point26_6{ diff --git a/text/v2/multi.go b/text/v2/multi.go new file mode 100644 index 000000000..944f00272 --- /dev/null +++ b/text/v2/multi.go @@ -0,0 +1,167 @@ +// 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 ( + "unicode/utf8" + + "github.com/hajimehoshi/ebiten/v2/vector" +) + +var _ Face = (MultiFace)(nil) + +// MultiFace is a Face that consists of multiple Face objects. +// The face in the first index is used in the highest priority, and the last the lowest priority. +// +// There is a known issue: if the writing directions of the faces don't agree, the rendering result might be messed up. +type MultiFace []Face + +// Metrics implements Face. +func (m MultiFace) Metrics() Metrics { + var mt Metrics + for _, f := range m { + mt1 := f.Metrics() + if mt1.Height > mt.Height { + mt.Height = mt1.Height + } + if mt1.HAscent > mt.HAscent { + mt.HAscent = mt1.HAscent + } + if mt1.HDescent > mt.HDescent { + mt.HDescent = mt1.HDescent + } + if mt1.Width > mt.Width { + mt.Width = mt1.Width + } + if mt1.VAscent > mt.VAscent { + mt.VAscent = mt1.VAscent + } + if mt1.VDescent > mt.VDescent { + mt.VDescent = mt1.VDescent + } + } + return mt +} + +// advance implements Face. +func (m MultiFace) advance(text string) float64 { + var a float64 + for _, c := range m.splitText(text) { + if c.faceIndex == -1 { + continue + } + f := m[c.faceIndex] + a += f.advance(text[c.textStartIndex:c.textEndIndex]) + } + return a +} + +// hasGlyph implements Face. +func (m MultiFace) hasGlyph(r rune) bool { + for _, f := range m { + if f.hasGlyph(r) { + return true + } + } + return false +} + +// appendGlyphsForLine implements Face. +func (m MultiFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { + for _, c := range m.splitText(line) { + if c.faceIndex == -1 { + continue + } + f := m[c.faceIndex] + t := line[c.textStartIndex:c.textEndIndex] + glyphs = f.appendGlyphsForLine(glyphs, t, indexOffset, originX, originY) + if a := f.advance(t); f.direction().isHorizontal() { + originX += a + } else { + originY += a + } + indexOffset += len(t) + } + return glyphs +} + +// appendVectorPathForLine implements Face. +func (m MultiFace) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) { + for _, c := range m.splitText(line) { + if c.faceIndex == -1 { + continue + } + f := m[c.faceIndex] + t := line[c.textStartIndex:c.textEndIndex] + f.appendVectorPathForLine(path, t, originX, originY) + if a := f.advance(t); f.direction().isHorizontal() { + originX += a + } else { + originY += a + } + } +} + +// direction implements Face. +func (m MultiFace) direction() Direction { + if len(m) == 0 { + return DirectionLeftToRight + } + return m[0].direction() +} + +// private implements Face. +func (m MultiFace) private() { +} + +type textChunk struct { + textStartIndex int + textEndIndex int + faceIndex int +} + +func (m MultiFace) splitText(text string) []textChunk { + var chunks []textChunk + + for ri, r := range text { + // -1 indicates the default face index. -1 is used when no face is found for the glyph. + fi := -1 + + _, l := utf8.DecodeRuneInString(text[ri:]) + for i, f := range m { + if !f.hasGlyph(r) { + continue + } + fi = i + break + } + + var s int + if len(chunks) > 0 { + if chunks[len(chunks)-1].faceIndex == fi { + chunks[len(chunks)-1].textEndIndex += l + continue + } + s = chunks[len(chunks)-1].textEndIndex + } + chunks = append(chunks, textChunk{ + textStartIndex: s, + textEndIndex: s + l, + faceIndex: fi, + }) + } + + return chunks +} diff --git a/text/v2/std.go b/text/v2/std.go index 0921a18b6..9c6b195d8 100644 --- a/text/v2/std.go +++ b/text/v2/std.go @@ -87,6 +87,12 @@ func (s *StdFace) advance(text string) float64 { return fixed26_6ToFloat64(font.MeasureString(s.f, text)) } +// hasGlyph implements Face. +func (s *StdFace) hasGlyph(r rune) bool { + _, ok := s.f.GlyphAdvance(r) + return ok +} + // appendGlyphsForLine implements Face. func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { s.copyCheck() @@ -176,7 +182,7 @@ func (s *StdFace) direction() Direction { } // appendVectorPathForLine implements Face. -func (s *StdFace) appendVectorPathForLine(path *vector.Path, text string, originX, originY float64) { +func (s *StdFace) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) { } // Metrics implelements Face. diff --git a/text/v2/text.go b/text/v2/text.go index d0bee7cbd..28bc54156 100644 --- a/text/v2/text.go +++ b/text/v2/text.go @@ -34,8 +34,10 @@ type Face interface { advance(text string) float64 + hasGlyph(r rune) bool + appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph - appendVectorPathForLine(path *vector.Path, text string, originX, originY float64) + appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) direction() Direction