diff --git a/README.md b/README.md index bf43298..19f5e49 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ These features *should* work just like their HTML5 counterparts, but there are l - measureText - textAlign - fillStyle +- strokeText - strokeStyle - linear gradients - radial gradients @@ -126,4 +127,3 @@ These features *should* work just like their HTML5 counterparts, but there are l - textBaseline - isPointInPath - isPointInStroke -- strokeText diff --git a/canvas_test.go b/canvas_test.go index 1913f69..45352aa 100644 --- a/canvas_test.go +++ b/canvas_test.go @@ -42,7 +42,7 @@ func run(t *testing.T, fn func(cv *canvas.Canvas)) { callerFuncName := callerFunc.Name() callerFuncName = callerFuncName[strings.Index(callerFuncName, prefix)+len(prefix):] - fileName := fmt.Sprintf("testimages/%s.png", callerFuncName) + fileName := fmt.Sprintf("testdata/%s.png", callerFuncName) _, err = os.Stat(fileName) if err != nil && !os.IsNotExist(err) { @@ -352,3 +352,13 @@ func TestLineDash2(t *testing.T) { cv.Stroke() }) } +func TestText(t *testing.T) { + run(t, func(cv *canvas.Canvas) { + cv.SetFont("testdata/Roboto-Light.ttf", 48) + cv.SetFillStyle("#F00") + cv.FillText("A BC", 0, 46) + cv.SetStrokeStyle("#0F0") + cv.SetLineWidth(1) + cv.StrokeText("D EF", 0, 90) + }) +} diff --git a/testimages/Alpha.png b/testdata/Alpha.png similarity index 100% rename from testimages/Alpha.png rename to testdata/Alpha.png diff --git a/testimages/ClosePath.png b/testdata/ClosePath.png similarity index 100% rename from testimages/ClosePath.png rename to testdata/ClosePath.png diff --git a/testimages/Curves.png b/testdata/Curves.png similarity index 100% rename from testimages/Curves.png rename to testdata/Curves.png diff --git a/testimages/DrawPath.png b/testdata/DrawPath.png similarity index 100% rename from testimages/DrawPath.png rename to testdata/DrawPath.png diff --git a/testimages/FillConcavePath.png b/testdata/FillConcavePath.png similarity index 100% rename from testimages/FillConcavePath.png rename to testdata/FillConcavePath.png diff --git a/testimages/FillConvexPath.png b/testdata/FillConvexPath.png similarity index 100% rename from testimages/FillConvexPath.png rename to testdata/FillConvexPath.png diff --git a/testimages/FillRect.png b/testdata/FillRect.png similarity index 100% rename from testimages/FillRect.png rename to testdata/FillRect.png diff --git a/testimages/LineDash.png b/testdata/LineDash.png similarity index 100% rename from testimages/LineDash.png rename to testdata/LineDash.png diff --git a/testimages/LineDash2.png b/testdata/LineDash2.png similarity index 100% rename from testimages/LineDash2.png rename to testdata/LineDash2.png diff --git a/testimages/MiterLimit.png b/testdata/MiterLimit.png similarity index 100% rename from testimages/MiterLimit.png rename to testdata/MiterLimit.png diff --git a/testdata/Roboto-Light.ttf b/testdata/Roboto-Light.ttf new file mode 100755 index 0000000..13bf13a Binary files /dev/null and b/testdata/Roboto-Light.ttf differ diff --git a/testdata/Text.png b/testdata/Text.png new file mode 100755 index 0000000..e5ba6b3 Binary files /dev/null and b/testdata/Text.png differ diff --git a/text.go b/text.go index 52d5ac4..d5982d5 100644 --- a/text.go +++ b/text.go @@ -232,6 +232,110 @@ func (cv *Canvas) FillText(str string, x, y float64) { gli.ActiveTexture(gl_TEXTURE0) } +// StrokeText draws the given string at the given coordinates +// using the currently set font and font height and using the +// current stroke style +func (cv *Canvas) StrokeText(str string, x, y float64) { + cv.activate() + + if cv.state.font == nil { + return + } + + frc := fontRenderingContext + frc.setFont(cv.state.font.font) + frc.setFontSize(float64(cv.state.fontSize)) + fnt := cv.state.font.font + + for _, rn := range str { + idx := fnt.Index(rn) + if idx == 0 { + idx = fnt.Index(' ') + } + advance, _, err := frc.glyphMeasure(idx, fixed.Point26_6{}) + if err != nil { + continue + } + + cv.strokeRune(rn, vec{x, y}) + + x += float64(advance) / 64 + } +} + +func (cv *Canvas) strokeRune(rn rune, pos vec) { + gb := &truetype.GlyphBuf{} + gb.Load(cv.state.font.font, fixed.Int26_6(cv.state.fontSize*64), cv.state.font.font.Index(rn), font.HintingNone) + + prevPath := cv.path + defer func() { + cv.path = prevPath + }() + + from := 0 + for _, to := range gb.Ends { + ps := gb.Points[from:to] + + start := fixed.Point26_6{ + X: ps[0].X, + Y: ps[0].Y, + } + others := []truetype.Point(nil) + if ps[0].Flags&0x01 != 0 { + others = ps[1:] + } else { + last := fixed.Point26_6{ + X: ps[len(ps)-1].X, + Y: ps[len(ps)-1].Y, + } + if ps[len(ps)-1].Flags&0x01 != 0 { + start = last + others = ps[:len(ps)-1] + } else { + start = fixed.Point26_6{ + X: (start.X + last.X) / 2, + Y: (start.Y + last.Y) / 2, + } + others = ps + } + } + + p0, on0 := gb.Points[from], false + cv.MoveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64) + for _, p := range others { + on := p.Flags&0x01 != 0 + if on { + if on0 { + cv.LineTo(float64(p.X)/64+pos[0], pos[1]-float64(p.Y)/64) + } else { + cv.QuadraticCurveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64, float64(p.X)/64+pos[0], pos[1]-float64(p.Y)/64) + } + } else { + if on0 { + // No-op. + } else { + mid := fixed.Point26_6{ + X: (p0.X + p.X) / 2, + Y: (p0.Y + p.Y) / 2, + } + cv.QuadraticCurveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64, float64(mid.X)/64+pos[0], pos[1]-float64(mid.Y)/64) + } + } + p0, on0 = p, on + } + + if on0 { + cv.LineTo(float64(start.X)/64+pos[0], pos[1]-float64(start.Y)/64) + } else { + cv.QuadraticCurveTo(float64(p0.X)/64+pos[0], pos[1]-float64(p0.Y)/64, float64(start.X)/64+pos[0], pos[1]-float64(start.Y)/64) + } + cv.ClosePath() + cv.Stroke() + + from = to + } +} + // TextMetrics is the result of a MeasureText call type TextMetrics struct { Width float64