text/v2: add AppendVectorPath

Closes #1937
Updates #2454
This commit is contained in:
Hajime Hoshi 2023-11-20 02:50:27 +09:00
parent 03a8aaee5c
commit c0e41de921
6 changed files with 98 additions and 88 deletions

View File

@ -15,17 +15,15 @@
package main package main
import ( import (
"bytes"
"image" "image"
"image/color" "image/color"
"log" "log"
"math" "math"
"golang.org/x/image/font"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/text/v2"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
@ -46,13 +44,8 @@ const (
screenHeight = 480 screenHeight = 480
) )
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
return float32(x>>6) + float32(x&((1<<6)-1))/float32(1<<6)
}
type Game struct { type Game struct {
segments sfnt.Segments path vector.Path
bounds fixed.Rectangle26_6
vertices []ebiten.Vertex vertices []ebiten.Vertex
indices []uint16 indices []uint16
@ -60,83 +53,37 @@ type Game struct {
} }
func (g *Game) Update() error { func (g *Game) Update() error {
if g.tick == 0 {
s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
if err != nil {
return err
}
op := &text.LayoutOptions{}
op.LineSpacingInPixels = 100
text.AppendVectorPath(&g.path, "あいうえお\nかきくけこ", &text.GoTextFace{
Source: s,
Size: 100,
}, op)
}
g.tick++ g.tick++
if g.segments == nil {
ppem := fixed.I(300)
f, err := sfnt.Parse(fonts.MPlus1pRegular_ttf)
if err != nil {
return err
}
var b sfnt.Buffer
idx, err := f.GlyphIndex(&b, 'あ')
if err != nil {
return err
}
segments, err := f.LoadGlyph(&b, idx, ppem, nil)
if err != nil {
return err
}
g.segments = segments
bounds, err := f.Bounds(&b, ppem, font.HintingNone)
if err != nil {
return err
}
g.bounds = bounds
}
return nil return nil
} }
func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Draw(screen *ebiten.Image) {
var path vector.Path
for _, seg := range g.segments {
switch seg.Op {
case sfnt.SegmentOpMoveTo:
path.MoveTo(
fixed26_6ToFloat32(seg.Args[0].X),
fixed26_6ToFloat32(seg.Args[0].Y),
)
case sfnt.SegmentOpLineTo:
path.LineTo(
fixed26_6ToFloat32(seg.Args[0].X),
fixed26_6ToFloat32(seg.Args[0].Y),
)
case sfnt.SegmentOpQuadTo:
path.QuadTo(
fixed26_6ToFloat32(seg.Args[0].X),
fixed26_6ToFloat32(seg.Args[0].Y),
fixed26_6ToFloat32(seg.Args[1].X),
fixed26_6ToFloat32(seg.Args[1].Y),
)
case sfnt.SegmentOpCubeTo:
path.CubicTo(
fixed26_6ToFloat32(seg.Args[0].X),
fixed26_6ToFloat32(seg.Args[0].Y),
fixed26_6ToFloat32(seg.Args[1].X),
fixed26_6ToFloat32(seg.Args[1].Y),
fixed26_6ToFloat32(seg.Args[2].X),
fixed26_6ToFloat32(seg.Args[2].Y),
)
}
}
path.Close()
g.vertices = g.vertices[:0] g.vertices = g.vertices[:0]
g.indices = g.indices[:0] g.indices = g.indices[:0]
op := &vector.StrokeOptions{} op := &vector.StrokeOptions{}
op.Width = 7*(float32(math.Sin(float64(g.tick)*2*math.Pi/180))+1) + 1 op.Width = 2*(float32(math.Sin(float64(g.tick)*2*math.Pi/180))+1) + 1
op.LineJoin = vector.LineJoinRound op.LineJoin = vector.LineJoinRound
op.LineCap = vector.LineCapRound op.LineCap = vector.LineCapRound
g.vertices, g.indices = path.AppendVerticesAndIndicesForStroke(g.vertices, g.indices, op) g.vertices, g.indices = g.path.AppendVerticesAndIndicesForStroke(g.vertices, g.indices, op)
for i := range g.vertices { for i := range g.vertices {
g.vertices[i].DstX += screenWidth/2 - fixed26_6ToFloat32(g.bounds.Max.X+g.bounds.Min.X)/2 g.vertices[i].DstX += 50
g.vertices[i].DstY += screenHeight/2 - fixed26_6ToFloat32(g.bounds.Max.Y+g.bounds.Min.Y)/2 g.vertices[i].DstY += 50
g.vertices[i].SrcX = 1 g.vertices[i].SrcX = 1
g.vertices[i].SrcY = 1 g.vertices[i].SrcY = 1
} }

View File

@ -29,6 +29,7 @@ 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/vector"
) )
var _ Face = (*GoTextFace)(nil) var _ Face = (*GoTextFace)(nil)
@ -286,14 +287,13 @@ func (g *GoTextFace) advance(text string) float64 {
return fixed26_6ToFloat64(output.Advance) return fixed26_6ToFloat64(output.Advance)
} }
// appendGlyphs implements Face. // appendGlyphsForLine implements Face.
func (g *GoTextFace) appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph { func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph {
_, gs := g.Source.shape(text, g)
origin := fixed.Point26_6{ origin := fixed.Point26_6{
X: float64ToFixed26_6(originX), X: float64ToFixed26_6(originX),
Y: float64ToFixed26_6(originY), Y: float64ToFixed26_6(originY),
} }
_, 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 { if img != nil {
@ -344,6 +344,22 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
return img, imgX, imgY return img, imgX, imgY
} }
// appendVectorPathForLine implements Face.
func (g *GoTextFace) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) {
origin := fixed.Point26_6{
X: float64ToFixed26_6(originX),
Y: float64ToFixed26_6(originY),
}
_, gs := g.Source.shape(line, g)
for _, glyph := range gs {
appendVectorPathFromSegments(path, glyph.scaledSegments, fixed26_6ToFloat32(origin.X), fixed26_6ToFloat32(origin.Y))
origin = origin.Add(fixed.Point26_6{
X: glyph.shapingGlyph.XAdvance,
Y: -glyph.shapingGlyph.YAdvance,
})
}
}
// direction implements Face. // direction implements Face.
func (g *GoTextFace) direction() Direction { func (g *GoTextFace) direction() Direction {
return g.Direction return g.Direction

View File

@ -21,9 +21,10 @@ import (
"github.com/go-text/typesetting/opentype/api" "github.com/go-text/typesetting/opentype/api"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"golang.org/x/image/vector" gvector "golang.org/x/image/vector"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
) )
func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 { func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 {
@ -94,7 +95,7 @@ func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBo
biasX := fixed26_6ToFloat32(-glyphBounds.Min.X + subpixelOffset.X) biasX := fixed26_6ToFloat32(-glyphBounds.Min.X + subpixelOffset.X)
biasY := fixed26_6ToFloat32(-glyphBounds.Min.Y + subpixelOffset.Y) biasY := fixed26_6ToFloat32(-glyphBounds.Min.Y + subpixelOffset.Y)
rast := vector.NewRasterizer(w, h) rast := gvector.NewRasterizer(w, h)
rast.DrawOp = draw.Src rast.DrawOp = draw.Src
for _, seg := range segs { for _, seg := range segs {
switch seg.Op { switch seg.Op {
@ -120,3 +121,26 @@ func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBo
rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
return ebiten.NewImageFromImage(dst) return ebiten.NewImageFromImage(dst)
} }
func appendVectorPathFromSegments(path *vector.Path, segs []api.Segment, x, y float32) {
for _, seg := range segs {
switch seg.Op {
case api.SegmentOpMoveTo:
path.MoveTo(seg.Args[0].X+x, seg.Args[0].Y+y)
case api.SegmentOpLineTo:
path.LineTo(seg.Args[0].X+x, seg.Args[0].Y+y)
case api.SegmentOpQuadTo:
path.QuadTo(
seg.Args[0].X+x, seg.Args[0].Y+y,
seg.Args[1].X+x, seg.Args[1].Y+y,
)
case api.SegmentOpCubeTo:
path.CubicTo(
seg.Args[0].X+x, seg.Args[0].Y+y,
seg.Args[1].X+x, seg.Args[1].Y+y,
seg.Args[2].X+x, seg.Args[2].Y+y,
)
}
}
path.Close()
}

View File

@ -18,6 +18,7 @@ import (
"strings" "strings"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
) )
// Align is the alignment that determines how to put a text. // Align is the alignment that determines how to put a text.
@ -125,15 +126,32 @@ func AppendGlyphs(glyphs []Glyph, text string, face Face, options *LayoutOptions
return appendGlyphs(glyphs, text, face, 0, 0, options) return appendGlyphs(glyphs, text, face, 0, 0, options)
} }
// AppndVectorPath appends a vector path for glyphs to the given path.
//
// AppendVectorPath works only when face is *GoTextFace so far. For other types, AppendVectorPath does nothing.
func AppendVectorPath(path *vector.Path, text string, face Face, options *LayoutOptions) {
forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
face.appendVectorPathForLine(path, line, originX, originY)
})
}
// appendGlyphs appends glyphs to the given slice and returns a slice. // appendGlyphs appends glyphs to the given slice and returns a slice.
// //
// appendGlyphs assumes the text is rendered with the position (x, y). // appendGlyphs 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 appendGlyphs(glyphs []Glyph, text string, face Face, x, y float64, options *LayoutOptions) []Glyph {
if text == "" { forEachLine(text, face, options, func(line string, indexOffset int, originX, originY float64) {
glyphs = face.appendGlyphsForLine(glyphs, line, indexOffset, originX+x, originY+y)
})
return glyphs return glyphs
} }
// forEachLine interates lines.
func forEachLine(text string, face Face, options *LayoutOptions, f func(text string, indexOffset int, originX, originY float64)) {
if text == "" {
return
}
if options == nil { if options == nil {
options = &LayoutOptions{} options = &LayoutOptions{}
} }
@ -232,7 +250,7 @@ func appendGlyphs(glyphs []Glyph, text string, face Face, x, y float64, options
} }
} }
glyphs = face.appendGlyphs(glyphs, line, indexOffset, originX+offsetX+x, originY+offsetY+y) f(line, indexOffset, originX+offsetX, originY+offsetY)
if !found { if !found {
break break
@ -253,8 +271,6 @@ func appendGlyphs(glyphs []Glyph, text string, face Face, x, y float64, options
originX -= options.LineSpacingInPixels originX -= options.LineSpacingInPixels
} }
} }
return glyphs
} }
type horizontalAlign int type horizontalAlign int

View File

@ -22,6 +22,7 @@ import (
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
) )
var _ Face = (*StdFace)(nil) var _ Face = (*StdFace)(nil)
@ -84,8 +85,8 @@ func (s *StdFace) advance(text string) float64 {
return fixed26_6ToFloat64(font.MeasureString(s.f, text)) return fixed26_6ToFloat64(font.MeasureString(s.f, text))
} }
// appendGlyphs implements Face. // appendGlyphsForLine implements Face.
func (s *StdFace) appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph { func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph {
s.copyCheck() s.copyCheck()
origin := fixed.Point26_6{ origin := fixed.Point26_6{
@ -94,7 +95,7 @@ func (s *StdFace) appendGlyphs(glyphs []Glyph, text string, indexOffset int, ori
} }
prevR := rune(-1) prevR := rune(-1)
for i, r := range text { for i, r := range line {
if prevR >= 0 { if prevR >= 0 {
origin.X += s.f.Kern(prevR, r) origin.X += s.f.Kern(prevR, r)
} }
@ -102,7 +103,7 @@ func (s *StdFace) appendGlyphs(glyphs []Glyph, text string, indexOffset int, ori
if img != nil { 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(text[i:]) _, size := utf8.DecodeRuneInString(line[i:])
glyphs = append(glyphs, Glyph{ glyphs = append(glyphs, Glyph{
StartIndexInBytes: indexOffset + i, StartIndexInBytes: indexOffset + i,
EndIndexInBytes: indexOffset + i + size, EndIndexInBytes: indexOffset + i + size,
@ -173,6 +174,10 @@ func (s *StdFace) direction() Direction {
return DirectionLeftToRight return DirectionLeftToRight
} }
// appendVectorPathForLine implements Face.
func (s *StdFace) appendVectorPathForLine(path *vector.Path, text string, originX, originY float64) {
}
// Metrics implelements Face. // Metrics implelements Face.
func (s *StdFace) private() { func (s *StdFace) private() {
} }

View File

@ -24,6 +24,7 @@ import (
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
) )
// Face is an interface representing a font face. The implementations are only GoTextFace and StdFace. // Face is an interface representing a font face. The implementations are only GoTextFace and StdFace.
@ -38,7 +39,8 @@ type Face interface {
advance(text string) float64 advance(text string) float64
appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph
appendVectorPathForLine(path *vector.Path, text string, originX, originY float64)
direction() Direction direction() Direction