Skip to content

Commit

Permalink
add Font type
Browse files Browse the repository at this point in the history
  • Loading branch information
soypat committed Nov 23, 2024
1 parent 9472cba commit 8c376f6
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 155 deletions.
2 changes: 1 addition & 1 deletion cpu_evaluators.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ func (u *diff2D) Evaluate(pos []ms2.Vec, dist []float32, userData any) error {
return err
}
for i := range dist {
dist[i] = maxf(-d1[i], d2[i])
dist[i] = maxf(d1[i], -d2[i])
}
return nil
}
Expand Down
229 changes: 229 additions & 0 deletions forge/textsdf/font.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package textsdf

import (
"errors"
"fmt"

"github.com/golang/freetype/truetype"
"github.com/soypat/glgl/math/ms2"
"github.com/soypat/gsdf"
"github.com/soypat/gsdf/glbuild"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)

const firstBasic = '!'
const lastBasic = '~'

// Font implements font parsing and glyph (character) generation.
type Font struct {
ttf truetype.Font
gb truetype.GlyphBuf
// basicGlyphs optimized array access for common ASCII glyphs.
basicGlyphs [lastBasic - firstBasic]glyph
// Other kinds of glyphs.
otherGlyphs map[rune]glyph
bld gsdf.Builder
}

func (f *Font) LoadTTFBytes(ttf []byte) error {
font, err := truetype.Parse(ttf)
if err != nil {
return err
}
f.Reset()
f.ttf = *font
return nil
}

func (f *Font) Reset() {
for i := range f.basicGlyphs {
f.basicGlyphs[i] = glyph{}
}
for k := range f.otherGlyphs {
delete(f.otherGlyphs, k)
}
}

type glyph struct {
sdf glbuild.Shader2D
}

func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
if len(s) == 0 {
return nil, errors.New("no text provided")
}
var shapes []glbuild.Shader2D
scale := f.scale()
var prevChar rune
for i, c := range s {
charshape, err := f.Glyph(c)
if err != nil {
return nil, fmt.Errorf("char %q: %w", c, err)
}
if i > 0 {
kern := f.ttf.Kern(scale, truetype.Index(prevChar), truetype.Index(c))
charshape = f.bld.Translate2D(charshape, float32(kern), 0)
}
shapes = append(shapes, charshape)
prevChar = c
}
if len(shapes) == 1 {
return shapes[0], nil
}
return f.bld.Union2D(shapes...), nil
}

// Kern returns the horizontal adjustment for the given glyph pair. A positive kern means to move the glyphs further apart.
func (f *Font) Kern(c0, c1 rune) float32 {
return float32(f.ttf.Kern(f.scale(), truetype.Index(c0), truetype.Index(c1)))
}

// Glyph returns a SDF for a character.
func (f *Font) Glyph(c rune) (_ glbuild.Shader2D, err error) {
var g glyph
if c >= firstBasic && c <= lastBasic {
// Basic ASCII glyph case.
g = f.basicGlyphs[c-firstBasic]
if g.sdf == nil {
// Glyph not yet created. create it.
g, err = f.makeGlyph(c)
if err != nil {
return nil, err
}
f.basicGlyphs[c-firstBasic] = g
}
return g.sdf, nil
}
// Unicode or other glyph.
g, ok := f.otherGlyphs[c]
if !ok {
g, err = f.makeGlyph(c)
if err != nil {
return nil, err
}
f.otherGlyphs[c] = g
}
return g.sdf, nil
}

func (f *Font) scale() fixed.Int26_6 {
return fixed.Int26_6(f.ttf.FUnitsPerEm())
}

func (f *Font) makeGlyph(char rune) (glyph, error) {
g := &f.gb
idx := f.ttf.Index(char)
scale := f.scale()
bld := &f.bld
err := g.Load(&f.ttf, scale, idx, font.HintingNone)
if err != nil {
return glyph{}, err
}

// Build Glyph.
shape, fill, err := glyphCurve(bld, g.Points, 0, g.Ends[0])
if err != nil {
return glyph{}, err
} else if !fill {
return glyph{}, errors.New("first glyph shape is negative space")
}
start := g.Ends[0]
g.Ends = g.Ends[1:]
for _, end := range g.Ends {
sdf, fill, err := glyphCurve(bld, g.Points, start, end)
start = end
if err != nil {
return glyph{}, err
}
if fill {
shape = bld.Union2D(shape, sdf)
} else {
shape = bld.Difference2D(shape, sdf)
}
}

return glyph{sdf: shape}, nil
}

func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int) (glbuild.Shader2D, bool, error) {
var (
sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: 0.1}
sum float32
)

n := end - start
i := start
var poly []ms2.Vec
vPrev := p2v(points[end-1])
for i < start+n {
p0, p1, p2 := points[i], points[start+(i+1)%n], points[start+(i+2)%n]
onBits := p0.Flags&1 |
(p1.Flags&1)<<1 |
(p2.Flags&1)<<2
v0, v1, v2 := p2v(p0), p2v(p1), p2v(p2)
implicit0 := ms2.Scale(0.5, ms2.Add(v0, v1))
implicit1 := ms2.Scale(0.5, ms2.Add(v1, v2))
switch onBits {
case 0b010, 0b110:
// sampler.SetSplinePoints(vPrev, v0, v1, ms2.Vec{})
i += 1
println("prohibited")
// not valid off start. If getting this error try replacing with `i++;continue`
// return nil, false, errors.New("invalid start to bezier")
poly = append(poly, v0)
continue
// // if i == start+n-1 {
// // poly = append(poly, v0)
// // }
// vPrev = v0
// i += 1
// return bld.NewCircle(1), sum > 0, nil
// continue
case 0b000:
// implicit-off-implicit.
sampler.SetSplinePoints(implicit0, v1, implicit1, ms2.Vec{})
v0 = implicit0
i += 1
case 0b001:
// on-off-implicit.
sampler.SetSplinePoints(v0, v1, implicit1, ms2.Vec{})
i += 1
case 0b011, 0b111:
// on-on Straight line.
poly = append(poly, v0)
i += 1
sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
vPrev = v0
continue
case 0b100:
// implicit-off-on.
sampler.SetSplinePoints(implicit0, v1, v2, ms2.Vec{})
v0 = implicit0
i += 2
case 0b101:
// On-off-on.
sampler.SetSplinePoints(v0, v1, v2, ms2.Vec{})
i += 2
}
poly = append(poly, v0) // Append start point.
poly = sampler.SampleBisect(poly, 1)
sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
vPrev = v0
}
return bld.NewPolygon(poly), sum > 0, bld.Err()
}

func p2v(p truetype.Point) ms2.Vec {
return ms2.Vec{
X: float32(p.X),
Y: float32(p.Y),
}
}

var quadBezier = ms2.NewSpline3([]float32{
1, 0, 0, 0,
-2, 2, 0, 0,
1, -2, 1, 0,
0, 0, 0, 0,
})
Loading

0 comments on commit 8c376f6

Please sign in to comment.