diff --git a/cpu_evaluators.go b/cpu_evaluators.go index 442d770..d8bdd34 100644 --- a/cpu_evaluators.go +++ b/cpu_evaluators.go @@ -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 } diff --git a/forge/textsdf/font.go b/forge/textsdf/font.go new file mode 100644 index 0000000..73f99b9 --- /dev/null +++ b/forge/textsdf/font.go @@ -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, +}) diff --git a/forge/textsdf/glyph_test.go b/forge/textsdf/glyph_test.go index e67f7e0..0d74110 100644 --- a/forge/textsdf/glyph_test.go +++ b/forge/textsdf/glyph_test.go @@ -5,26 +5,21 @@ import ( _ "embed" - "github.com/golang/freetype/truetype" - "github.com/soypat/glgl/math/ms2" - "github.com/soypat/gsdf" - "github.com/soypat/gsdf/glbuild" "github.com/soypat/gsdf/gleval" "github.com/soypat/gsdf/gsdfaux" - "golang.org/x/image/font" - "golang.org/x/image/math/fixed" ) //go:embed iso-3098.ttf var _isonormTTF []byte func TestABC(t *testing.T) { - ttf, err := truetype.Parse(_isonormTTF) + var f Font + err := f.LoadTTFBytes(_isonormTTF) if err != nil { t.Fatal(err) } - var glyph truetype.GlyphBuf - shape, err := sdf(ttf, &glyph, 'A') + shape, err := f.TextLine("e") + // shape, err := f.Glyph('A') if err != nil { t.Fatal(err) } @@ -37,146 +32,3 @@ func TestABC(t *testing.T) { t.Fatal(err) } } - -func sdf(ttf *truetype.Font, glyph *truetype.GlyphBuf, char rune) (glbuild.Shader2D, error) { - idx := ttf.Index(char) - scale := fixed.Int26_6(ttf.FUnitsPerEm()) - // hm := ttf.HMetric(scale, idx) - - // kern := ttf.Kern(scale, iprev, idx) - // xOfs := float32(kern) - err := glyph.Load(ttf, scale, idx, font.HintingNone) - if err != nil { - return nil, err - } - return glyphSDF(glyph) - -} - -func glyphSDF(g *truetype.GlyphBuf) (glbuild.Shader2D, error) { - var spline []ms2.Vec - var bld gsdf.Builder - shape := bld.NewCircle(0.1) - for n := range g.Ends { - spline = spline[:0] - start := 0 - if n != 0 { - start = g.Ends[n-1] - } - end := g.Ends[n] - 1 - if n != 1 { - continue - } - sdf, cw, err := glyphCurve(g.Points, start, end) - if err != nil { - return shape, err - } - cw = true - if cw { - shape = bld.Union2D(shape, sdf) - } else { - shape = bld.Difference2D(shape, sdf) - } - - } - return shape, nil -} - -// func glyphCurve2(points []truetype.Point, start, end int) (glbuild.Shader2D, bool, error) { -// var ( -// bld gsdf.Builder -// sampler = ms2.Spline3Sampler{Spline: quadraticBezier(), Tolerance: 2} -// sum float32 -// ) -// n := end - start + 1 -// i := 0 -// vprev := p2v(points[end]) -// for i < 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 -// switch bits { -// case 0b001: -// sampler.SetSplinePoints(vprev,) -// } -// off := points[i].Flags&1 == 0 // Point on or off curve. - -// } -// } - -func glyphCurve(points []truetype.Point, start, end int) (glbuild.Shader2D, bool, error) { - var bld gsdf.Builder - var spline []ms2.Vec - sampler := ms2.Spline3Sampler{ - Spline: quadraticBezier(), - Tolerance: 2, - } - var sum float32 - offPrev := points[end].Flags&1 == 0 - vPrev := p2v(points[end]) - for i := start; i <= end; i++ { - p := points[i] - v := p2v(p) - off := p.Flags&1 == 0 // Point on or off curve. - if off && offPrev { - // Implicit point on curve as midpoint of 2 off-points. - spline = append(spline, ms2.Scale(0.5, ms2.Add(v, vPrev))) - } else if !off && !offPrev { - // Add inneffective bezier off point. - spline = append(spline, ms2.Scale(0.5, ms2.Add(v, vPrev))) - } - spline = append(spline, v) - sum += (v.X - vPrev.X) * (v.Y + vPrev.Y) - vPrev = v - offPrev = off - } - cw := sum > 0 - var poly []ms2.Vec - const c = 10 - circle := bld.NewCircle(c / 2) - CIRCLE := bld.NewCircle(c) - shape := bld.NewCircle(0.1) - for i := 0; i+2 < len(spline); i += 2 { - p0, cp, p1 := spline[i], spline[i+1], spline[i+2] - sampler.SetSplinePoints(p0, cp, p1, ms2.Vec{}) - shape = bld.Union2D(shape, bld.Translate2D(CIRCLE, p0.X, p0.Y), bld.Translate2D(circle, cp.X, cp.Y)) - poly = append(poly, - sampler.Evaluate(0), - sampler.Evaluate(0.25), - sampler.Evaluate(0.5), - sampler.Evaluate(0.75), - ) - _ = p1 - _ = cp - } - return shape, cw, nil - sdf := bld.NewPolygon(poly) - return sdf, cw, nil -} - -func p2v(p truetype.Point) ms2.Vec { - return ms2.Vec{ - X: float32(p.X), - Y: float32(p.Y), - } -} - -func quadraticBezier() (s ms2.Spline3) { - matrix := []float32{ - 1, 0, 0, 0, - -2, 2, 0, 0, - 1, -2, 1, 0, - 0, 0, 0, 0, - } - // for i := range matrix { - // matrix[i] *= 20 - // } - return ms2.NewSpline3(matrix) - // return ms2.NewSpline3([]float32{ - // 1, -2, 1, 0, - // 0, 2, -2, 0, - // 0, 0, 1, 0, - // 0, 0, 0, 0, - // }) -} diff --git a/gsdf2d.go b/gsdf2d.go index ade3986..e2fff5e 100644 --- a/gsdf2d.go +++ b/gsdf2d.go @@ -740,9 +740,9 @@ func (s *diff2D) AppendShaderName(b []byte) []byte { } func (s *diff2D) AppendShaderBody(b []byte) []byte { - b = append(b, "return max(-"...) + b = append(b, "return max("...) b = s.s1.AppendShaderName(b) - b = append(b, "(p),"...) + b = append(b, "(p),-"...) b = s.s2.AppendShaderName(b) b = append(b, "(p));"...) return b