From 1106d90f1102faeecaaafac2b78c50bbd7741cde Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Tue, 10 Nov 2020 15:46:10 +0000 Subject: [PATCH] gpu,op/clip: implement stroked paths with round caps Signed-off-by: Sebastien Binet --- gpu/stroke.go | 34 ++++++++++++++++++ internal/rendertest/clip_test.go | 26 ++++++++++++++ .../refs/TestStrokedPathBevelRound.png | Bin 0 -> 3407 bytes op/clip/stroke.go | 5 +++ 4 files changed, 65 insertions(+) create mode 100644 internal/rendertest/refs/TestStrokedPathBevelRound.png 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 0000000000000000000000000000000000000000..36ba27a261a4fc6104e03999c2ac4a7be1f50908 GIT binary patch literal 3407 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