From 1b6005d50a48c8bb5b0113cc3d05faa915713907 Mon Sep 17 00:00:00 2001 From: soypat Date: Sun, 24 Nov 2024 12:14:15 -0300 Subject: [PATCH] add Text example --- examples/ui-text/uitext.go | 55 ++++++++++++++++ forge/textsdf/embed.go | 16 +++++ forge/textsdf/font.go | 125 +++++++++++++++++++++++------------- forge/textsdf/glyph_test.go | 30 +++++++-- gsdfaux/gsdfaux.go | 2 + gsdfaux/ui.go | 22 ++++--- 6 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 examples/ui-text/uitext.go create mode 100644 forge/textsdf/embed.go diff --git a/examples/ui-text/uitext.go b/examples/ui-text/uitext.go new file mode 100644 index 0000000..1c3ba44 --- /dev/null +++ b/examples/ui-text/uitext.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "log" + "runtime" + "unicode/utf8" + + "github.com/soypat/gsdf" + "github.com/soypat/gsdf/forge/textsdf" + "github.com/soypat/gsdf/glbuild" + "github.com/soypat/gsdf/gsdfaux" +) + +func init() { + runtime.LockOSThread() +} + +// scene generates the 3D object for rendering. +func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) { + var f textsdf.Font + err := f.LoadTTFBytes(textsdf.ISO3098TTF()) + if err != nil { + return nil, err + } + const text = "Hello world!" + line, err := f.TextLine(text) + if err != nil { + return nil, err + } + // Find characteristic size of characters(glyphs/letters). + len := utf8.RuneCountInString(text) + sz := line.Bounds().Size() + charWidth := sz.X / float32(len) // We then extrude based on the letter width. + + line = bld.Translate2D(line, -sz.X/2, 0) // Center text. + return bld.Extrude(line, charWidth/3), bld.Err() +} + +func main() { + var bld gsdf.Builder + shape, err := scene(&bld) + shape = bld.Scale(shape, 0.3) + if err != nil { + log.Fatal("creating scene:", err) + } + fmt.Println("Running UI... compiling text shaders may take a while...") + err = gsdfaux.UI(shape, gsdfaux.UIConfig{ + Width: 800, + Height: 600, + }) + if err != nil { + log.Fatal("UI:", err) + } +} diff --git a/forge/textsdf/embed.go b/forge/textsdf/embed.go new file mode 100644 index 0000000..43c6bb0 --- /dev/null +++ b/forge/textsdf/embed.go @@ -0,0 +1,16 @@ +package textsdf + +import ( + _ "embed" +) + +// Embedded fonts. +var ( + //go:embed iso-3098.ttf + _iso3098TTF []byte +) + +// ISO3098TTF returns the ISO-3098 true type font file. +func ISO3098TTF() []byte { + return append([]byte{}, _iso3098TTF...) // copy contents. +} diff --git a/forge/textsdf/font.go b/forge/textsdf/font.go index 73f99b9..309c5ff 100644 --- a/forge/textsdf/font.go +++ b/forge/textsdf/font.go @@ -3,6 +3,7 @@ package textsdf import ( "errors" "fmt" + "unicode" "github.com/golang/freetype/truetype" "github.com/soypat/glgl/math/ms2" @@ -20,28 +21,34 @@ type Font struct { ttf truetype.Font gb truetype.GlyphBuf // basicGlyphs optimized array access for common ASCII glyphs. - basicGlyphs [lastBasic - firstBasic]glyph + basicGlyphs [lastBasic - firstBasic + 1]glyph // Other kinds of glyphs. otherGlyphs map[rune]glyph bld gsdf.Builder } +// LoadTTFBytes loads a TTF file blob into f. After calling Load the Font is ready to generate text SDFs. func (f *Font) LoadTTFBytes(ttf []byte) error { font, err := truetype.Parse(ttf) if err != nil { return err } - f.Reset() + f.reset() f.ttf = *font return nil } -func (f *Font) Reset() { +// reset resets most internal state of Font without removing underlying assigned font. +func (f *Font) reset() { for i := range f.basicGlyphs { f.basicGlyphs[i] = glyph{} } - for k := range f.otherGlyphs { - delete(f.otherGlyphs, k) + if f.otherGlyphs == nil { + f.otherGlyphs = make(map[rune]glyph) + } else { + for k := range f.otherGlyphs { + delete(f.otherGlyphs, k) + } } } @@ -49,27 +56,48 @@ type glyph struct { sdf glbuild.Shader2D } +// TextLine returns a single line of text with the set font. +// TextLine takes kerning and advance width into account for letter spacing. +// Glyph locations are set starting at x=0 and appended in positive x direction. 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 { + var idxPrev truetype.Index + var xOfs float32 + for ic, c := range s { + if !unicode.IsGraphic(c) { + return nil, fmt.Errorf("char %q not graphic", c) + } + + idx := truetype.Index(c) + hm := f.ttf.HMetric(scale, idx) + if unicode.IsSpace(c) { + if c == '\t' { + hm.AdvanceWidth *= 4 + } + xOfs += float32(hm.AdvanceWidth) + continue + } 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) + + kern := f.ttf.Kern(scale, idxPrev, idx) + xOfs += float32(kern) + idxPrev = idx + if ic == 0 { + xOfs += float32(hm.LeftSideBearing) } + charshape = f.bld.Translate2D(charshape, xOfs, 0) shapes = append(shapes, charshape) - prevChar = c + xOfs += float32(hm.AdvanceWidth) } if len(shapes) == 1 { return shapes[0], nil + } else if len(shapes) == 0 { + // Only whitespace. + return nil, errors.New("no text provided") } return f.bld.Union2D(shapes...), nil } @@ -79,7 +107,12 @@ 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. +// Kern returns the horizontal adjustment for the given glyph pair. A positive kern means to move the glyphs further apart. +func (f *Font) AdvanceWidth(c rune) float32 { + return float32(f.ttf.HMetric(f.scale(), truetype.Index(c)).AdvanceWidth) +} + +// Glyph returns a SDF for a character defined by the argument rune. func (f *Font) Glyph(c rune) (_ glbuild.Shader2D, err error) { var g glyph if c >= firstBasic && c <= lastBasic { @@ -113,9 +146,11 @@ func (f *Font) scale() fixed.Int26_6 { func (f *Font) makeGlyph(char rune) (glyph, error) { g := &f.gb + bld := &f.bld + idx := f.ttf.Index(char) scale := f.scale() - bld := &f.bld + // hm := f.ttf.HMetric(scale, idx) err := g.Load(&f.ttf, scale, idx, font.HintingNone) if err != nil { return glyph{}, err @@ -126,7 +161,8 @@ func (f *Font) makeGlyph(char rune) (glyph, error) { if err != nil { return glyph{}, err } else if !fill { - return glyph{}, errors.New("first glyph shape is negative space") + _ = fill // This is not an error... + // return glyph{}, errors.New("first glyph shape is negative space") } start := g.Ends[0] g.Ends = g.Ends[1:] @@ -142,7 +178,6 @@ func (f *Font) makeGlyph(char rune) (glyph, error) { shape = bld.Difference2D(shape, sdf) } } - return glyph{sdf: shape}, nil } @@ -151,56 +186,46 @@ func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int) (glb sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: 0.1} sum float32 ) - - n := end - start - i := start + points = points[start:end] + n := len(points) + i := 0 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 + vPrev := p2v(points[n-1]) + for i < n { + p0, p1, p2 := points[i], points[(i+1)%n], points[(i+2)%n] + onBits := onbits3(points, 0, n, i) 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") + // implicit off start case? + fallthrough + 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 - // // 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{}) @@ -227,3 +252,11 @@ var quadBezier = ms2.NewSpline3([]float32{ 1, -2, 1, 0, 0, 0, 0, 0, }) + +func onbits3(points []truetype.Point, start, end, i int) uint32 { + n := end - start + p0, p1, p2 := points[i], points[start+(i+1)%n], points[start+(i+2)%n] + return p0.Flags&1 | + (p1.Flags&1)<<1 | + (p2.Flags&1)<<2 +} diff --git a/forge/textsdf/glyph_test.go b/forge/textsdf/glyph_test.go index 0d74110..046e7a8 100644 --- a/forge/textsdf/glyph_test.go +++ b/forge/textsdf/glyph_test.go @@ -1,25 +1,26 @@ package textsdf import ( + "fmt" "testing" _ "embed" + "github.com/golang/freetype/truetype" "github.com/soypat/gsdf/gleval" "github.com/soypat/gsdf/gsdfaux" + "golang.org/x/image/math/fixed" ) -//go:embed iso-3098.ttf -var _isonormTTF []byte - func TestABC(t *testing.T) { + const okchar = "BCDEFGHIJK" + const badchar = "iB~" var f Font - err := f.LoadTTFBytes(_isonormTTF) + err := f.LoadTTFBytes(ISO3098TTF()) if err != nil { t.Fatal(err) } - shape, err := f.TextLine("e") - // shape, err := f.Glyph('A') + shape, err := f.TextLine(badchar) if err != nil { t.Fatal(err) } @@ -32,3 +33,20 @@ func TestABC(t *testing.T) { t.Fatal(err) } } + +func Test(t *testing.T) { + ttf, err := truetype.Parse(_iso3098TTF) + if err != nil { + panic(err) + } + scale := fixed.Int26_6(ttf.FUnitsPerEm()) + hm := ttf.HMetric(scale, 'E') + fmt.Println(hm.AdvanceWidth, int(hm.AdvanceWidth)) + t.Error(hm) + // var g truetype.GlyphBuf + // err = g.Load(ttf, , 'B', font.HintingFull) + // if err != nil { + // panic(err) + // } + +} diff --git a/gsdfaux/gsdfaux.go b/gsdfaux/gsdfaux.go index f89303d..6070350 100644 --- a/gsdfaux/gsdfaux.go +++ b/gsdfaux/gsdfaux.go @@ -2,6 +2,7 @@ package gsdfaux import ( "bytes" + "context" "errors" "fmt" "image" @@ -38,6 +39,7 @@ type RenderConfig struct { type UIConfig struct { Width, Height int + Ctx context.Context } func UI(s glbuild.Shader3D, cfg UIConfig) error { diff --git a/gsdfaux/ui.go b/gsdfaux/ui.go index 6503ca0..5cff5c7 100644 --- a/gsdfaux/ui.go +++ b/gsdfaux/ui.go @@ -16,6 +16,7 @@ import ( func ui(s glbuild.Shader3D, cfg UIConfig) error { bb := s.Bounds() + diag := bb.Diagonal() // Initialize GLFW window, term, err := startGLFW(cfg.Width, cfg.Height) if err != nil { @@ -66,6 +67,10 @@ void main() { 1.0, 1.0, } gl.BufferData(gl.ARRAY_BUFFER, 4*len(vertices), gl.Ptr(vertices), gl.STATIC_DRAW) + charDistUniform, err := prog.UniformLocation("uCharDist\x00") + if err != nil { + return err + } camDistUniform, err := prog.UniformLocation("uCamDist\x00") if err != nil { return err @@ -94,7 +99,7 @@ void main() { gl.Enable(gl.DEPTH_TEST) // Set up mouse input tracking - diag := bb.Diagonal() + minZoom := float64(diag * 0.00001) maxZoom := float64(diag * 10) var ( @@ -187,7 +192,7 @@ void main() { gl.Uniform2f(resUniform, float32(width), float32(height)) gl.Uniform1f(yawUniform, float32(yaw)) gl.Uniform1f(pitchUniform, float32(pitch)) - + gl.Uniform1f(charDistUniform, float32(camDist)+diag) // Draw the quad gl.BindVertexArray(vao) gl.DrawArrays(gl.TRIANGLES, 0, 6) @@ -217,7 +222,7 @@ func makeFragSource(rootSDFName, sdfDecl string) string { buf.WriteString(`in vec2 vTexCoord; out vec4 fragColor; - +uniform float uCharDist; uniform vec2 uResolution; uniform float uYaw; uniform float uPitch; @@ -275,31 +280,32 @@ void main() { vec3 rd = normalize(p.x * uu + p.y * vv + 1.5 * ww); // Ray marching - const float tmax = 100.0; + const float tol = 0.0001; float t = 0.0; bool hit = false; for (int i = 0; i < 256; i++) { vec3 pos = ro + t * rd; float h = sdf(pos); - if (h < 0.0001 || t > tmax) { - hit = true; + if (h < tol || t > 1.3*uCharDist) { + hit = h