diff --git a/gpu/gpu.go b/gpu/gpu.go index 33d2c327..ddc1f6c3 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -162,7 +162,8 @@ func (op *clipOp) decode(data []byte) { bounds: layout.FRect(r), width: math.Float32frombits(bo.Uint32(data[17:])), style: clip.StrokeStyle{ - Cap: clip.StrokeCap(data[21]), + Cap: clip.StrokeCap(data[21]), + Join: clip.StrokeJoin(data[22]), }, } } diff --git a/gpu/stroke.go b/gpu/stroke.go index 2c6ecea4..c64cfa12 100644 --- a/gpu/stroke.go +++ b/gpu/stroke.go @@ -51,7 +51,7 @@ func (qs *strokeQuads) pen() f32.Point { } func (qs *strokeQuads) lineTo(pt f32.Point) { - end := (*qs)[len(*qs)-1].quad.To + end := qs.pen() *qs = append(*qs, strokeQuad{ quad: ops.Quad{ From: end, @@ -273,6 +273,13 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) } +// cosPt returns the cosine of the opening angle between p and q. +func cosPt(p, q f32.Point) float32 { + np := math.Hypot(float64(p.X), float64(p.Y)) + nq := math.Hypot(float64(q.X), float64(q.Y)) + return dotPt(p, q) / float32(np*nq) +} + func normPt(p f32.Point, l float32) f32.Point { d := math.Hypot(float64(p.X), float64(p.Y)) l64 := float64(l) @@ -416,7 +423,14 @@ func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32 // strokePathJoin joins the two paths rhs and lhs, according to the provided // stroke style sty. func strokePathJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { - strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + switch sty.Join { + case clip.BevelJoin: + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + case clip.RoundJoin: + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + default: + panic("impossible") + } } func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { @@ -428,6 +442,28 @@ func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po lhs.lineTo(lp) } +func strokePathRoundJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + cw := dotPt(rot90CW(n0), n1) >= 0.0 + switch { + case cw: + // Path bends to the right, ie. CW (or 180 degree turn). + c := pivot.Sub(lhs.pen()) + angle := -math.Acos(float64(cosPt(n0, n1))) + lhs.arc(c, c, float32(angle)) + lhs.lineTo(lp) // Add a line to accomodate for rounding errors. + rhs.lineTo(rp) + default: + // Path bends to the left, ie. CCW. + angle := math.Acos(float64(cosPt(n0, n1))) + c := pivot.Sub(rhs.pen()) + rhs.arc(c, c, float32(angle)) + rhs.lineTo(rp) // Add a line to accomodate for rounding errors. + lhs.lineTo(lp) + } +} + // strokePathCap caps the provided path qs, according to the provided stroke style sty. func strokePathCap(sty clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) { switch sty.Cap { diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 59b31508..efa6cc38 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -47,7 +47,7 @@ const ( TypePushLen = 1 TypePopLen = 1 TypeAuxLen = 1 - TypeClipLen = 1 + 4*4 + 4 + 1 + TypeClipLen = 1 + 4*4 + 4 + 2 TypeProfileLen = 1 ) diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go index 9a1aef01..a41f1c3d 100644 --- a/internal/rendertest/clip_test.go +++ b/internal/rendertest/clip_test.go @@ -107,7 +107,8 @@ func TestStrokedPathBevelFlat(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 sty := clip.StrokeStyle{ - Cap: clip.FlatCap, + Cap: clip.FlatCap, + Join: clip.BevelJoin, } p := new(clip.Path) @@ -133,7 +134,8 @@ func TestStrokedPathBevelRound(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 sty := clip.StrokeStyle{ - Cap: clip.RoundCap, + Cap: clip.RoundCap, + Join: clip.BevelJoin, } p := new(clip.Path) @@ -159,7 +161,35 @@ func TestStrokedPathBevelSquare(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 sty := clip.StrokeStyle{ - Cap: clip.SquareCap, + Cap: clip.SquareCap, + Join: clip.BevelJoin, + } + + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi) + p.Line(f32.Pt(10, 0)) + p.Line(f32.Pt(10, 10)) + p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi) + p.Line(f32.Pt(-20, 0)) + p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30)) + p.Stroke(width, sty).Add(o) + + paint.Fill(o, colornames.Red) + }, func(r result) { + r.expect(0, 0, colornames.White) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathRoundRound(t *testing.T) { + run(t, func(o *op.Ops) { + const width = 2.5 + sty := clip.StrokeStyle{ + Cap: clip.RoundCap, + Join: clip.RoundJoin, } p := new(clip.Path) diff --git a/internal/rendertest/refs/TestStrokedPathRoundRound.png b/internal/rendertest/refs/TestStrokedPathRoundRound.png new file mode 100644 index 00000000..da3be5f0 Binary files /dev/null and b/internal/rendertest/refs/TestStrokedPathRoundRound.png differ diff --git a/op/clip/clip.go b/op/clip/clip.go index a0430ff2..9b2c27a0 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -54,6 +54,7 @@ func (p Op) Add(o *op.Ops) { bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) bo.PutUint32(data[17:], math.Float32bits(p.width)) data[21] = uint8(p.style.Cap) + data[22] = uint8(p.style.Join) } // Begin the path, storing the path data and final Op into ops. diff --git a/op/clip/stroke.go b/op/clip/stroke.go index b693435f..abbcd4a2 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -3,9 +3,11 @@ package clip // StrokeStyle describes how a stroked path should be drawn. -// StrokeStyle zero value draws a Bevel-joined and Flat-capped stroked path. +// The zero value of StrokeStyle represents bevel-joined and flat-capped +// strokes. type StrokeStyle struct { - Cap StrokeCap + Cap StrokeCap + Join StrokeJoin } // StrokeCap describes the head or tail of a stroked path. @@ -26,3 +28,14 @@ const ( // stroked path's width. RoundCap ) + +// StrokeJoin describes how stroked paths are collated. +type StrokeJoin uint8 + +const ( + // BevelJoin joins path segments with sharp bevels. + BevelJoin StrokeJoin = iota + + // RoundJoin joins path segments with a round segment. + RoundJoin +)