text/v2: rename Glyph -> Cluster

This also changes AppendClusters to return cluster info even if a
cluster doesn't have a glyph.
This commit is contained in:
Hajime Hoshi 2023-12-05 17:57:22 +09:00
parent 800101da90
commit f0d23de3d3
8 changed files with 71 additions and 63 deletions

View File

@ -61,15 +61,15 @@ type Game struct {
counter int counter int
kanjiText []rune kanjiText []rune
kanjiTextColor color.RGBA kanjiTextColor color.RGBA
glyphs []text.Glyph clusters []text.Cluster
} }
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.clusters) == 0 {
op := &text.LayoutOptions{} op := &text.LayoutOptions{}
op.LineSpacingInPixels = mplusNormalFace.Size * 1.5 op.LineSpacingInPixels = mplusNormalFace.Size * 1.5
g.glyphs = text.AppendGlyphs(g.glyphs, sampleText, mplusNormalFace, op) g.clusters = text.AppendClusters(g.clusters, sampleText, mplusNormalFace, op)
} }
return nil return nil
} }
@ -121,13 +121,16 @@ func (g *Game) Draw(screen *ebiten.Image) {
{ {
const x, y = 240, 360 const x, y = 240, 360
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
// g.glyphs is initialized by text.AppendGlyphs. // g.glyphs is initialized by text.AppendClusters
// 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 i, c := range g.clusters {
if c.Image == nil {
continue
}
op.GeoM.Reset() op.GeoM.Reset()
op.GeoM.Translate(x, y) op.GeoM.Translate(x, y)
op.GeoM.Translate(gl.X, gl.Y) op.GeoM.Translate(c.X, c.Y)
op.ColorScale.Reset() op.ColorScale.Reset()
r := float32(1) r := float32(1)
if i%3 == 0 { if i%3 == 0 {
@ -142,7 +145,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
b = 0.5 b = 0.5
} }
op.ColorScale.Scale(r, g, b, 1) op.ColorScale.Scale(r, g, b, 1)
screen.DrawImage(gl.Image, op) screen.DrawImage(c.Image, op)
} }
} }
} }

View File

@ -291,8 +291,8 @@ func (g *GoTextFace) hasGlyph(r rune) bool {
return ok return ok
} }
// appendGlyphsForLine implements Face. // appendClustersForLine implements Face.
func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { func (g *GoTextFace) appendClustersForLine(clusters []Cluster, line string, indexOffset int, originX, originY float64) []Cluster {
origin := fixed.Point26_6{ origin := fixed.Point26_6{
X: float64ToFixed26_6(originX), X: float64ToFixed26_6(originX),
Y: float64ToFixed26_6(originY), Y: float64ToFixed26_6(originY),
@ -300,8 +300,9 @@ 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) img, imgX, imgY := g.glyphImage(glyph, origin)
if img != nil { // Append a glyph even if img is nil.
glyphs = append(glyphs, Glyph{ // This is necessary to return index information for control characters.
clusters = append(clusters, Cluster{
StartIndexInBytes: indexOffset + glyph.startIndex, StartIndexInBytes: indexOffset + glyph.startIndex,
EndIndexInBytes: indexOffset + glyph.endIndex, EndIndexInBytes: indexOffset + glyph.endIndex,
GID: uint32(glyph.shapingGlyph.GlyphID), GID: uint32(glyph.shapingGlyph.GlyphID),
@ -309,14 +310,13 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
X: float64(imgX), X: float64(imgX),
Y: float64(imgY), Y: float64(imgY),
}) })
}
origin = origin.Add(fixed.Point26_6{ origin = origin.Add(fixed.Point26_6{
X: glyph.shapingGlyph.XAdvance, X: glyph.shapingGlyph.XAdvance,
Y: -glyph.shapingGlyph.YAdvance, Y: -glyph.shapingGlyph.YAdvance,
}) })
} }
return glyphs return clusters
} }
func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) { func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) {

View File

@ -105,7 +105,10 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
} }
geoM := options.GeoM geoM := options.GeoM
for _, g := range AppendGlyphs(nil, text, face, &options.LayoutOptions) { for _, g := range AppendClusters(nil, text, face, &options.LayoutOptions) {
if g.Image == nil {
continue
}
op := &options.DrawImageOptions op := &options.DrawImageOptions
op.GeoM.Reset() op.GeoM.Reset()
op.GeoM.Translate(g.X, g.Y) op.GeoM.Translate(g.X, g.Y)
@ -114,16 +117,16 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
} }
} }
// AppendGlyphs appends glyphs to the given slice and returns a slice. // AppendClusters 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. // AppendClusters is a low-level API, and you can use AppendClusters to have more control than Draw.
// AppendGlyphs is also available to precache glyphs. // AppendClusters is also available to precache glyphs.
// //
// For the details of options, see Draw function. // For the details of options, see Draw function.
// //
// AppendGlyphs is concurrent-safe. // AppendClusters is concurrent-safe.
func AppendGlyphs(glyphs []Glyph, text string, face Face, options *LayoutOptions) []Glyph { func AppendClusters(glyphs []Cluster, text string, face Face, options *LayoutOptions) []Cluster {
return appendGlyphs(glyphs, text, face, 0, 0, options) return appendClusters(glyphs, text, face, 0, 0, options)
} }
// AppndVectorPath appends a vector path for glyphs to the given path. // AppndVectorPath appends a vector path for glyphs to the given path.
@ -136,13 +139,13 @@ func AppendVectorPath(path *vector.Path, text string, face Face, options *Layout
}) })
} }
// appendGlyphs appends glyphs to the given slice and returns a slice. // appendClusters appends glyphs to the given slice and returns a slice.
// //
// appendGlyphs assumes the text is rendered with the position (x, y). // appendClusters assumes the text is rendered with the position (x, y).
// (x, y) might affect the subpixel rendering results. // (x, y) might affect the subpixel rendering results.
func appendGlyphs(glyphs []Glyph, text string, face Face, x, y float64, options *LayoutOptions) []Glyph { func appendClusters(glyphs []Cluster, text string, face Face, x, y float64, options *LayoutOptions) []Cluster {
forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) { forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
glyphs = face.appendGlyphsForLine(glyphs, line, indexOffset, originX+x, originY+y) glyphs = face.appendClustersForLine(glyphs, line, indexOffset, originX+x, originY+y)
}) })
return glyphs return glyphs
} }

View File

@ -55,9 +55,9 @@ func (l *LimitedFace) hasGlyph(r rune) bool {
return l.unicodeRanges.contains(r) && l.face.hasGlyph(r) return l.unicodeRanges.contains(r) && l.face.hasGlyph(r)
} }
// appendGlyphsForLine implements Face. // appendClustersForLine implements Face.
func (l *LimitedFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { func (l *LimitedFace) appendClustersForLine(clusters []Cluster, line string, indexOffset int, originX, originY float64) []Cluster {
return l.face.appendGlyphsForLine(glyphs, l.unicodeRanges.filter(line), indexOffset, originX, originY) return l.face.appendClustersForLine(clusters, l.unicodeRanges.filter(line), indexOffset, originX, originY)
} }
// appendVectorPathForLine implements Face. // appendVectorPathForLine implements Face.

View File

@ -102,15 +102,15 @@ func (m *MultiFace) hasGlyph(r rune) bool {
return false return false
} }
// appendGlyphsForLine implements Face. // appendClustersForLine implements Face.
func (m *MultiFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { func (m *MultiFace) appendClustersForLine(clusters []Cluster, line string, indexOffset int, originX, originY float64) []Cluster {
for _, c := range m.splitText(line) { for _, c := range m.splitText(line) {
if c.faceIndex == -1 { if c.faceIndex == -1 {
continue continue
} }
f := m.faces[c.faceIndex] f := m.faces[c.faceIndex]
t := line[c.textStartIndex:c.textEndIndex] t := line[c.textStartIndex:c.textEndIndex]
glyphs = f.appendGlyphsForLine(glyphs, t, indexOffset, originX, originY) clusters = f.appendClustersForLine(clusters, t, indexOffset, originX, originY)
if a := f.advance(t); f.direction().isHorizontal() { if a := f.advance(t); f.direction().isHorizontal() {
originX += a originX += a
} else { } else {
@ -118,7 +118,7 @@ func (m *MultiFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset
} }
indexOffset += len(t) indexOffset += len(t)
} }
return glyphs return clusters
} }
// appendVectorPathForLine implements Face. // appendVectorPathForLine implements Face.

View File

@ -93,8 +93,8 @@ func (s *StdFace) hasGlyph(r rune) bool {
return ok return ok
} }
// appendGlyphsForLine implements Face. // appendClustersForLine implements Face.
func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph { func (s *StdFace) appendClustersForLine(clusters []Cluster, line string, indexOffset int, originX, originY float64) []Cluster {
s.copyCheck() s.copyCheck()
origin := fixed.Point26_6{ origin := fixed.Point26_6{
@ -108,23 +108,25 @@ func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i
origin.X += s.f.Kern(prevR, r) origin.X += s.f.Kern(prevR, r)
} }
img, imgX, imgY, a := s.glyphImage(r, origin) img, imgX, imgY, a := s.glyphImage(r, origin)
if img != nil {
// Adjust the position to the integers. // Adjust the position to the integers.
// The current glyph images assume that they are rendered on integer positions so far. // The current glyph images assume that they are rendered on integer positions so far.
_, size := utf8.DecodeRuneInString(line[i:]) _, size := utf8.DecodeRuneInString(line[i:])
glyphs = append(glyphs, Glyph{
// Append a glyph even if img is nil.
// This is necessary to return index information for control characters.
clusters = append(clusters, Cluster{
StartIndexInBytes: indexOffset + i, StartIndexInBytes: indexOffset + i,
EndIndexInBytes: indexOffset + i + size, EndIndexInBytes: indexOffset + i + size,
Image: img, Image: img,
X: float64(imgX), X: float64(imgX),
Y: float64(imgY), Y: float64(imgY),
}) })
}
origin.X += a origin.X += a
prevR = r prevR = r
} }
return glyphs return clusters
} }
func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) { func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) {

View File

@ -36,7 +36,7 @@ type Face interface {
hasGlyph(r rune) bool hasGlyph(r rune) bool
appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph appendClustersForLine(glyphs []Cluster, line string, indexOffset int, originX, originY float64) []Cluster
appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64)
direction() Direction direction() Direction
@ -117,12 +117,12 @@ func adjustGranularity(x fixed.Int26_6, face Face) fixed.Int26_6 {
return x / factor * factor return x / factor * factor
} }
// Glyph represents one glyph to render. // Cluster represents one grapheme cluster.
type Glyph struct { type Cluster struct {
// StartIndexInBytes is the start index in bytes for the given string at AppendGlyphs. // StartIndexInBytes is the start index in bytes for the given string at AppendClusters.
StartIndexInBytes int StartIndexInBytes int
// EndIndexInBytes is the end index in bytes for the given string at AppendGlyphs. // EndIndexInBytes is the end index in bytes for the given string at AppendClusters.
EndIndexInBytes int EndIndexInBytes int
// GID is an ID for a glyph of TrueType or OpenType font. GID is valid when the face is GoTextFace. // GID is an ID for a glyph of TrueType or OpenType font. GID is valid when the face is GoTextFace.
@ -134,12 +134,12 @@ type Glyph struct {
Image *ebiten.Image Image *ebiten.Image
// X is the X position to render this glyph. // X is the X position to render this glyph.
// The position is determined in a sequence of characters given at AppendGlyphs. // The position is determined in a sequence of characters given at AppendClusters.
// The position's origin is the first character's origin position. // The position's origin is the first character's origin position.
X float64 X float64
// Y is the Y position to render this glyph. // Y is the Y position to render this glyph.
// The position is determined in a sequence of characters given at AppendGlyphs. // The position is determined in a sequence of characters given at AppendClusters.
// 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
} }
@ -245,10 +245,10 @@ func CacheGlyphs(text string, face Face) {
c := glyphVariationCount(face) c := glyphVariationCount(face)
var buf []Glyph var buf []Cluster
// Create all the possible variations (#2528). // Create all the possible variations (#2528).
for i := 0; i < c; i++ { for i := 0; i < c; i++ {
buf = appendGlyphs(buf, text, face, x, y, nil) buf = appendClusters(buf, text, face, x, y, nil)
buf = buf[:0] buf = buf[:0]
if face.direction().isHorizontal() { if face.direction().isHorizontal() {

View File

@ -40,7 +40,7 @@ over the lazy dog.`
f := text.NewStdFace(bitmapfont.Face) f := text.NewStdFace(bitmapfont.Face)
got := sampleText got := sampleText
for _, g := range text.AppendGlyphs(nil, sampleText, f, nil) { for _, g := range text.AppendClusters(nil, sampleText, f, nil) {
got = got[:g.StartIndexInBytes] + strings.Repeat(" ", g.EndIndexInBytes-g.StartIndexInBytes) + got[g.EndIndexInBytes:] got = got[:g.StartIndexInBytes] + strings.Repeat(" ", g.EndIndexInBytes-g.StartIndexInBytes) + got[g.EndIndexInBytes:]
} }
want := regexp.MustCompile(`\S`).ReplaceAllString(sampleText, " ") want := regexp.MustCompile(`\S`).ReplaceAllString(sampleText, " ")