diff --git a/README.md b/README.md index 2c5c8fe..cce200a 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ SetHexColor(x string) SetLineWidth(lineWidth float64) SetLineCap(lineCap LineCap) SetLineJoin(lineJoin LineJoin) +SetDash(dashes ...float64) SetFillRule(fillRule FillRule) ``` @@ -149,9 +150,6 @@ even better, implement it and submit a pull request! - Clipping Regions - Gradients / Patterns -- Dashed Lines\* - -\* *might be implemented soon* ## How Do it Do? diff --git a/context.go b/context.go index f58944b..5642a01 100644 --- a/context.go +++ b/context.go @@ -43,6 +43,7 @@ type Context struct { start Point current Point hasCurrent bool + dashes []float64 lineWidth float64 lineCap LineCap lineJoin LineJoin @@ -91,6 +92,10 @@ func (dc *Context) SavePNG(path string) error { return SavePNG(path, dc.im) } +func (dc *Context) SetDash(dashes ...float64) { + dc.dashes = dashes +} + func (dc *Context) SetLineWidth(lineWidth float64) { dc.lineWidth = lineWidth } @@ -276,11 +281,15 @@ func (dc *Context) joiner() raster.Joiner { } func (dc *Context) StrokePreserve() { + path := dc.strokePath + if len(dc.dashes) > 0 { + path = dashed(path, dc.dashes) + } painter := raster.NewRGBAPainter(dc.im) painter.SetColor(dc.color) r := raster.NewRasterizer(dc.width, dc.height) r.UseNonZeroWinding = true - r.AddStroke(dc.strokePath, fi(dc.lineWidth), dc.capper(), dc.joiner()) + r.AddStroke(path, fi(dc.lineWidth), dc.capper(), dc.joiner()) r.Rasterize(painter) } diff --git a/examples/cubic.go b/examples/cubic.go index 8e81c2b..bedc585 100644 --- a/examples/cubic.go +++ b/examples/cubic.go @@ -19,9 +19,10 @@ func main() { dc.MoveTo(x0, y0) dc.CubicTo(x1, y1, x2, y2, x3, y3) dc.SetRGBA(0, 0, 0, 0.2) - dc.SetLineWidth(10) + dc.SetLineWidth(8) dc.FillPreserve() dc.SetRGB(0, 0, 0) + dc.SetDash(16, 24) dc.Stroke() dc.MoveTo(x0, y0) @@ -30,6 +31,7 @@ func main() { dc.LineTo(x3, y3) dc.SetRGBA(1, 0, 0, 0.4) dc.SetLineWidth(2) + dc.SetDash(4, 8, 1, 8) dc.Stroke() dc.SavePNG("out.png") diff --git a/point.go b/point.go index cdbc2c8..57f3ff7 100644 --- a/point.go +++ b/point.go @@ -1,11 +1,25 @@ package gg -import "golang.org/x/image/math/fixed" +import ( + "math" + + "golang.org/x/image/math/fixed" +) type Point struct { X, Y float64 } -func (p Point) Fixed() fixed.Point26_6 { - return fp(p.X, p.Y) +func (a Point) Fixed() fixed.Point26_6 { + return fp(a.X, a.Y) +} + +func (a Point) Distance(b Point) float64 { + return math.Hypot(a.X-b.X, a.Y-b.Y) +} + +func (a Point) Interpolate(b Point, t float64) Point { + x := a.X + (b.X-a.X)*t + y := a.Y + (b.Y-a.Y)*t + return Point{x, y} } diff --git a/util.go b/util.go index 0f46ac6..cc931ce 100644 --- a/util.go +++ b/util.go @@ -107,14 +107,14 @@ func loadFontFace(path string, points float64) font.Face { func flattenPath(p raster.Path) [][]Point { var result [][]Point - path := make([]Point, 0, 16) + var path []Point var cx, cy float64 for i := 0; i < len(p); { switch p[i] { case 0: if len(path) > 0 { result = append(result, path) - path = make([]Point, 0, 16) + path = nil } x := unfix(p[i+1]) y := unfix(p[i+2]) @@ -156,3 +156,85 @@ func flattenPath(p raster.Path) [][]Point { } return result } + +func dashPath(paths [][]Point, dashes []float64) [][]Point { + var result [][]Point + if len(dashes) == 0 { + return paths + } + if len(dashes) == 1 { + dashes = append(dashes, dashes[0]) + } + for _, path := range paths { + if len(path) < 2 { + continue + } + previous := path[0] + pathIndex := 1 + dashIndex := 0 + segmentLength := 0.0 + var segment []Point + segment = append(segment, previous) + for pathIndex < len(path) { + dashLength := dashes[dashIndex] + point := path[pathIndex] + d := previous.Distance(point) + maxd := dashLength - segmentLength + if d > maxd { + t := maxd / d + p := previous.Interpolate(point, t) + segment = append(segment, p) + if dashIndex%2 == 0 && len(segment) > 1 { + result = append(result, segment) + } + segment = nil + segment = append(segment, p) + segmentLength = 0 + previous = p + dashIndex = (dashIndex + 1) % len(dashes) + } else { + segment = append(segment, point) + previous = point + segmentLength += d + pathIndex++ + } + } + if dashIndex%2 == 0 && len(segment) > 1 { + result = append(result, segment) + } + } + return result +} + +func rasterPath(paths [][]Point) raster.Path { + var result raster.Path + for _, path := range paths { + var previous fixed.Point26_6 + for i, point := range path { + f := point.Fixed() + if i == 0 { + result.Start(f) + } else { + dx := f.X - previous.X + dy := f.Y - previous.Y + if dx < 0 { + dx = -dx + } + if dy < 0 { + dy = -dy + } + if dx+dy > 4 { + // TODO: this is a hack for cases where two points are + // too close - causes rendering issues with joins / caps + result.Add1(f) + } + } + previous = f + } + } + return result +} + +func dashed(path raster.Path, dashes []float64) raster.Path { + return rasterPath(dashPath(flattenPath(path), dashes)) +}