Skip to content

Commit 1b6005d

Browse files
committed
add Text example
1 parent 8c376f6 commit 1b6005d

File tree

6 files changed

+190
-60
lines changed

6 files changed

+190
-60
lines changed

examples/ui-text/uitext.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"runtime"
7+
"unicode/utf8"
8+
9+
"github.com/soypat/gsdf"
10+
"github.com/soypat/gsdf/forge/textsdf"
11+
"github.com/soypat/gsdf/glbuild"
12+
"github.com/soypat/gsdf/gsdfaux"
13+
)
14+
15+
func init() {
16+
runtime.LockOSThread()
17+
}
18+
19+
// scene generates the 3D object for rendering.
20+
func scene(bld *gsdf.Builder) (glbuild.Shader3D, error) {
21+
var f textsdf.Font
22+
err := f.LoadTTFBytes(textsdf.ISO3098TTF())
23+
if err != nil {
24+
return nil, err
25+
}
26+
const text = "Hello world!"
27+
line, err := f.TextLine(text)
28+
if err != nil {
29+
return nil, err
30+
}
31+
// Find characteristic size of characters(glyphs/letters).
32+
len := utf8.RuneCountInString(text)
33+
sz := line.Bounds().Size()
34+
charWidth := sz.X / float32(len) // We then extrude based on the letter width.
35+
36+
line = bld.Translate2D(line, -sz.X/2, 0) // Center text.
37+
return bld.Extrude(line, charWidth/3), bld.Err()
38+
}
39+
40+
func main() {
41+
var bld gsdf.Builder
42+
shape, err := scene(&bld)
43+
shape = bld.Scale(shape, 0.3)
44+
if err != nil {
45+
log.Fatal("creating scene:", err)
46+
}
47+
fmt.Println("Running UI... compiling text shaders may take a while...")
48+
err = gsdfaux.UI(shape, gsdfaux.UIConfig{
49+
Width: 800,
50+
Height: 600,
51+
})
52+
if err != nil {
53+
log.Fatal("UI:", err)
54+
}
55+
}

forge/textsdf/embed.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package textsdf
2+
3+
import (
4+
_ "embed"
5+
)
6+
7+
// Embedded fonts.
8+
var (
9+
//go:embed iso-3098.ttf
10+
_iso3098TTF []byte
11+
)
12+
13+
// ISO3098TTF returns the ISO-3098 true type font file.
14+
func ISO3098TTF() []byte {
15+
return append([]byte{}, _iso3098TTF...) // copy contents.
16+
}

forge/textsdf/font.go

+79-46
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package textsdf
33
import (
44
"errors"
55
"fmt"
6+
"unicode"
67

78
"github.com/golang/freetype/truetype"
89
"github.com/soypat/glgl/math/ms2"
@@ -20,56 +21,83 @@ type Font struct {
2021
ttf truetype.Font
2122
gb truetype.GlyphBuf
2223
// basicGlyphs optimized array access for common ASCII glyphs.
23-
basicGlyphs [lastBasic - firstBasic]glyph
24+
basicGlyphs [lastBasic - firstBasic + 1]glyph
2425
// Other kinds of glyphs.
2526
otherGlyphs map[rune]glyph
2627
bld gsdf.Builder
2728
}
2829

30+
// LoadTTFBytes loads a TTF file blob into f. After calling Load the Font is ready to generate text SDFs.
2931
func (f *Font) LoadTTFBytes(ttf []byte) error {
3032
font, err := truetype.Parse(ttf)
3133
if err != nil {
3234
return err
3335
}
34-
f.Reset()
36+
f.reset()
3537
f.ttf = *font
3638
return nil
3739
}
3840

39-
func (f *Font) Reset() {
41+
// reset resets most internal state of Font without removing underlying assigned font.
42+
func (f *Font) reset() {
4043
for i := range f.basicGlyphs {
4144
f.basicGlyphs[i] = glyph{}
4245
}
43-
for k := range f.otherGlyphs {
44-
delete(f.otherGlyphs, k)
46+
if f.otherGlyphs == nil {
47+
f.otherGlyphs = make(map[rune]glyph)
48+
} else {
49+
for k := range f.otherGlyphs {
50+
delete(f.otherGlyphs, k)
51+
}
4552
}
4653
}
4754

4855
type glyph struct {
4956
sdf glbuild.Shader2D
5057
}
5158

59+
// TextLine returns a single line of text with the set font.
60+
// TextLine takes kerning and advance width into account for letter spacing.
61+
// Glyph locations are set starting at x=0 and appended in positive x direction.
5262
func (f *Font) TextLine(s string) (glbuild.Shader2D, error) {
53-
if len(s) == 0 {
54-
return nil, errors.New("no text provided")
55-
}
5663
var shapes []glbuild.Shader2D
5764
scale := f.scale()
58-
var prevChar rune
59-
for i, c := range s {
65+
var idxPrev truetype.Index
66+
var xOfs float32
67+
for ic, c := range s {
68+
if !unicode.IsGraphic(c) {
69+
return nil, fmt.Errorf("char %q not graphic", c)
70+
}
71+
72+
idx := truetype.Index(c)
73+
hm := f.ttf.HMetric(scale, idx)
74+
if unicode.IsSpace(c) {
75+
if c == '\t' {
76+
hm.AdvanceWidth *= 4
77+
}
78+
xOfs += float32(hm.AdvanceWidth)
79+
continue
80+
}
6081
charshape, err := f.Glyph(c)
6182
if err != nil {
6283
return nil, fmt.Errorf("char %q: %w", c, err)
6384
}
64-
if i > 0 {
65-
kern := f.ttf.Kern(scale, truetype.Index(prevChar), truetype.Index(c))
66-
charshape = f.bld.Translate2D(charshape, float32(kern), 0)
85+
86+
kern := f.ttf.Kern(scale, idxPrev, idx)
87+
xOfs += float32(kern)
88+
idxPrev = idx
89+
if ic == 0 {
90+
xOfs += float32(hm.LeftSideBearing)
6791
}
92+
charshape = f.bld.Translate2D(charshape, xOfs, 0)
6893
shapes = append(shapes, charshape)
69-
prevChar = c
94+
xOfs += float32(hm.AdvanceWidth)
7095
}
7196
if len(shapes) == 1 {
7297
return shapes[0], nil
98+
} else if len(shapes) == 0 {
99+
// Only whitespace.
100+
return nil, errors.New("no text provided")
73101
}
74102
return f.bld.Union2D(shapes...), nil
75103
}
@@ -79,7 +107,12 @@ func (f *Font) Kern(c0, c1 rune) float32 {
79107
return float32(f.ttf.Kern(f.scale(), truetype.Index(c0), truetype.Index(c1)))
80108
}
81109

82-
// Glyph returns a SDF for a character.
110+
// Kern returns the horizontal adjustment for the given glyph pair. A positive kern means to move the glyphs further apart.
111+
func (f *Font) AdvanceWidth(c rune) float32 {
112+
return float32(f.ttf.HMetric(f.scale(), truetype.Index(c)).AdvanceWidth)
113+
}
114+
115+
// Glyph returns a SDF for a character defined by the argument rune.
83116
func (f *Font) Glyph(c rune) (_ glbuild.Shader2D, err error) {
84117
var g glyph
85118
if c >= firstBasic && c <= lastBasic {
@@ -113,9 +146,11 @@ func (f *Font) scale() fixed.Int26_6 {
113146

114147
func (f *Font) makeGlyph(char rune) (glyph, error) {
115148
g := &f.gb
149+
bld := &f.bld
150+
116151
idx := f.ttf.Index(char)
117152
scale := f.scale()
118-
bld := &f.bld
153+
// hm := f.ttf.HMetric(scale, idx)
119154
err := g.Load(&f.ttf, scale, idx, font.HintingNone)
120155
if err != nil {
121156
return glyph{}, err
@@ -126,7 +161,8 @@ func (f *Font) makeGlyph(char rune) (glyph, error) {
126161
if err != nil {
127162
return glyph{}, err
128163
} else if !fill {
129-
return glyph{}, errors.New("first glyph shape is negative space")
164+
_ = fill // This is not an error...
165+
// return glyph{}, errors.New("first glyph shape is negative space")
130166
}
131167
start := g.Ends[0]
132168
g.Ends = g.Ends[1:]
@@ -142,7 +178,6 @@ func (f *Font) makeGlyph(char rune) (glyph, error) {
142178
shape = bld.Difference2D(shape, sdf)
143179
}
144180
}
145-
146181
return glyph{sdf: shape}, nil
147182
}
148183

@@ -151,56 +186,46 @@ func glyphCurve(bld *gsdf.Builder, points []truetype.Point, start, end int) (glb
151186
sampler = ms2.Spline3Sampler{Spline: quadBezier, Tolerance: 0.1}
152187
sum float32
153188
)
154-
155-
n := end - start
156-
i := start
189+
points = points[start:end]
190+
n := len(points)
191+
i := 0
157192
var poly []ms2.Vec
158-
vPrev := p2v(points[end-1])
159-
for i < start+n {
160-
p0, p1, p2 := points[i], points[start+(i+1)%n], points[start+(i+2)%n]
161-
onBits := p0.Flags&1 |
162-
(p1.Flags&1)<<1 |
163-
(p2.Flags&1)<<2
193+
vPrev := p2v(points[n-1])
194+
for i < n {
195+
p0, p1, p2 := points[i], points[(i+1)%n], points[(i+2)%n]
196+
onBits := onbits3(points, 0, n, i)
164197
v0, v1, v2 := p2v(p0), p2v(p1), p2v(p2)
165198
implicit0 := ms2.Scale(0.5, ms2.Add(v0, v1))
166199
implicit1 := ms2.Scale(0.5, ms2.Add(v1, v2))
167200
switch onBits {
168201
case 0b010, 0b110:
169-
// sampler.SetSplinePoints(vPrev, v0, v1, ms2.Vec{})
170-
i += 1
171-
println("prohibited")
172-
// not valid off start. If getting this error try replacing with `i++;continue`
173-
// return nil, false, errors.New("invalid start to bezier")
202+
// implicit off start case?
203+
fallthrough
204+
case 0b011, 0b111:
205+
// on-on Straight line.
174206
poly = append(poly, v0)
207+
i += 1
208+
sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
209+
vPrev = v0
175210
continue
176-
// // if i == start+n-1 {
177-
// // poly = append(poly, v0)
178-
// // }
179-
// vPrev = v0
180-
// i += 1
181-
// return bld.NewCircle(1), sum > 0, nil
182-
// continue
211+
183212
case 0b000:
184213
// implicit-off-implicit.
185214
sampler.SetSplinePoints(implicit0, v1, implicit1, ms2.Vec{})
186215
v0 = implicit0
187216
i += 1
217+
188218
case 0b001:
189219
// on-off-implicit.
190220
sampler.SetSplinePoints(v0, v1, implicit1, ms2.Vec{})
191221
i += 1
192-
case 0b011, 0b111:
193-
// on-on Straight line.
194-
poly = append(poly, v0)
195-
i += 1
196-
sum += (v0.X - vPrev.X) * (v0.Y + vPrev.Y)
197-
vPrev = v0
198-
continue
222+
199223
case 0b100:
200224
// implicit-off-on.
201225
sampler.SetSplinePoints(implicit0, v1, v2, ms2.Vec{})
202226
v0 = implicit0
203227
i += 2
228+
204229
case 0b101:
205230
// On-off-on.
206231
sampler.SetSplinePoints(v0, v1, v2, ms2.Vec{})
@@ -227,3 +252,11 @@ var quadBezier = ms2.NewSpline3([]float32{
227252
1, -2, 1, 0,
228253
0, 0, 0, 0,
229254
})
255+
256+
func onbits3(points []truetype.Point, start, end, i int) uint32 {
257+
n := end - start
258+
p0, p1, p2 := points[i], points[start+(i+1)%n], points[start+(i+2)%n]
259+
return p0.Flags&1 |
260+
(p1.Flags&1)<<1 |
261+
(p2.Flags&1)<<2
262+
}

forge/textsdf/glyph_test.go

+24-6
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
package textsdf
22

33
import (
4+
"fmt"
45
"testing"
56

67
_ "embed"
78

9+
"github.com/golang/freetype/truetype"
810
"github.com/soypat/gsdf/gleval"
911
"github.com/soypat/gsdf/gsdfaux"
12+
"golang.org/x/image/math/fixed"
1013
)
1114

12-
//go:embed iso-3098.ttf
13-
var _isonormTTF []byte
14-
1515
func TestABC(t *testing.T) {
16+
const okchar = "BCDEFGHIJK"
17+
const badchar = "iB~"
1618
var f Font
17-
err := f.LoadTTFBytes(_isonormTTF)
19+
err := f.LoadTTFBytes(ISO3098TTF())
1820
if err != nil {
1921
t.Fatal(err)
2022
}
21-
shape, err := f.TextLine("e")
22-
// shape, err := f.Glyph('A')
23+
shape, err := f.TextLine(badchar)
2324
if err != nil {
2425
t.Fatal(err)
2526
}
@@ -32,3 +33,20 @@ func TestABC(t *testing.T) {
3233
t.Fatal(err)
3334
}
3435
}
36+
37+
func Test(t *testing.T) {
38+
ttf, err := truetype.Parse(_iso3098TTF)
39+
if err != nil {
40+
panic(err)
41+
}
42+
scale := fixed.Int26_6(ttf.FUnitsPerEm())
43+
hm := ttf.HMetric(scale, 'E')
44+
fmt.Println(hm.AdvanceWidth, int(hm.AdvanceWidth))
45+
t.Error(hm)
46+
// var g truetype.GlyphBuf
47+
// err = g.Load(ttf, , 'B', font.HintingFull)
48+
// if err != nil {
49+
// panic(err)
50+
// }
51+
52+
}

gsdfaux/gsdfaux.go

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gsdfaux
22

33
import (
44
"bytes"
5+
"context"
56
"errors"
67
"fmt"
78
"image"
@@ -38,6 +39,7 @@ type RenderConfig struct {
3839

3940
type UIConfig struct {
4041
Width, Height int
42+
Ctx context.Context
4143
}
4244

4345
func UI(s glbuild.Shader3D, cfg UIConfig) error {

0 commit comments

Comments
 (0)