diff --git a/gpu/stroke.go b/gpu/stroke.go index 75e9189e..2c6ecea4 100644 --- a/gpu/stroke.go +++ b/gpu/stroke.go @@ -28,6 +28,7 @@ import ( "gioui.org/f32" "gioui.org/internal/ops" + "gioui.org/op" "gioui.org/op/clip" ) @@ -45,6 +46,10 @@ type strokeState struct { type strokeQuads []strokeQuad +func (qs *strokeQuads) pen() f32.Point { + return (*qs)[len(*qs)-1].quad.To +} + func (qs *strokeQuads) lineTo(pt f32.Point) { end := (*qs)[len(*qs)-1].quad.To *qs = append(*qs, strokeQuad{ @@ -56,6 +61,27 @@ func (qs *strokeQuads) lineTo(pt f32.Point) { }) } +func (qs *strokeQuads) arc(f1, f2 f32.Point, angle float32) { + var ( + p clip.Path + o = new(op.Ops) + ) + p.Begin(o) + p.Move(qs.pen()) + beg := len(o.Data()) + p.Arc(f1, f2, angle) + end := len(o.Data()) + raw := o.Data()[beg:end] + + for qi := 0; len(raw) >= (ops.QuadSize + 4); qi++ { + quad := ops.DecodeQuad(raw[4:]) + raw = raw[ops.QuadSize+4:] + *qs = append(*qs, strokeQuad{ + quad: quad, + }) + } +} + // split splits a slice of quads into slices of quads grouped // by contours (ie: splitted at move-to boundaries). func (qs strokeQuads) split() []strokeQuads { @@ -409,6 +435,8 @@ func strokePathCap(sty clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 strokePathFlatCap(qs, hw, pivot, n0) case clip.SquareCap: strokePathSquareCap(qs, hw, pivot, n0) + case clip.RoundCap: + strokePathRoundCap(qs, hw, pivot, n0) default: panic("impossible") } @@ -433,3 +461,9 @@ func strokePathSquareCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) { qs.lineTo(corner2) qs.lineTo(end) } + +// strokePathRoundCap caps the start or end of a path with a round cap. +func strokePathRoundCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) { + c := pivot.Sub(qs.pen()) + qs.arc(c, c, math.Pi) +} diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go index c20c6dc0..9a1aef01 100644 --- a/internal/rendertest/clip_test.go +++ b/internal/rendertest/clip_test.go @@ -129,6 +129,32 @@ func TestStrokedPathBevelFlat(t *testing.T) { }) } +func TestStrokedPathBevelRound(t *testing.T) { + run(t, func(o *op.Ops) { + const width = 2.5 + sty := clip.StrokeStyle{ + Cap: clip.RoundCap, + } + + 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 TestStrokedPathBevelSquare(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 diff --git a/internal/rendertest/refs/TestStrokedPathBevelRound.png b/internal/rendertest/refs/TestStrokedPathBevelRound.png new file mode 100644 index 00000000..36ba27a2 Binary files /dev/null and b/internal/rendertest/refs/TestStrokedPathBevelRound.png differ diff --git a/op/clip/stroke.go b/op/clip/stroke.go index c85fd49b..b693435f 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -20,4 +20,9 @@ const ( // and left-hand sides of a stroked path with a half square of length // the stroked path's width. SquareCap + + // RoundCap caps stroked paths with a round cap, joining the right-hand and + // left-hand sides of a stroked path with a half disc of diameter the + // stroked path's width. + RoundCap )