From 7eb32360e56449e0cf623c75ebef56a822d806a0 Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Wed, 11 Nov 2020 15:05:47 +0000 Subject: [PATCH] gpu,op/clip: implement stroked paths with miter joins Signed-off-by: Sebastien Binet --- gpu/gpu.go | 5 +- gpu/stroke.go | 45 +++++++++ internal/opconst/ops.go | 2 +- internal/rendertest/clip_test.go | 90 ++++++++++++++++++ .../refs/TestStrokedPathFlatMiter.png | Bin 0 -> 2309 bytes .../refs/TestStrokedPathFlatMiterInf.png | Bin 0 -> 2304 bytes op/clip/clip.go | 1 + op/clip/stroke.go | 5 + 8 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 internal/rendertest/refs/TestStrokedPathFlatMiter.png create mode 100644 internal/rendertest/refs/TestStrokedPathFlatMiterInf.png diff --git a/gpu/gpu.go b/gpu/gpu.go index 125e0872..6637bd64 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -162,8 +162,9 @@ 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]), - Join: clip.StrokeJoin(data[22]), + Cap: clip.StrokeCap(data[21]), + Join: clip.StrokeJoin(data[22]), + Miter: math.Float32frombits(bo.Uint32(data[23:])), }, } } diff --git a/gpu/stroke.go b/gpu/stroke.go index c64cfa12..9a76c50a 100644 --- a/gpu/stroke.go +++ b/gpu/stroke.go @@ -423,6 +423,10 @@ 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) { + if sty.Miter > 0 { + strokePathMiterJoin(sty, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } switch sty.Join { case clip.BevelJoin: strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) @@ -464,6 +468,47 @@ func strokePathRoundJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po } } +func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + if n0 == n1.Mul(-1) { + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + + // This is to handle nearly linear joints that would be clipped otherwise. + limit := math.Max(float64(sty.Miter), 1.001) + + cw := dotPt(rot90CW(n0), n1) >= 0.0 + if cw { + // hw is used to calculate |R|. + // When running CW, n0 and n1 point the other way, + // so the sign of r0 and r1 is negated. + hw = -hw + } + hw64 := float64(hw) + + cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1)))) + d := hw64 / cos + if math.Abs(limit*hw64) < math.Abs(d) { + sty.Miter = 0 // Set miter to zero to disable the miter joint. + strokePathJoin(sty, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + mid := pivot.Add(normPt(n0.Add(n1), float32(d))) + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + switch { + case cw: + // Path bends to the right, ie. CW. + lhs.lineTo(mid) + default: + // Path bends to the left, ie. CCW. + rhs.lineTo(mid) + } + rhs.lineTo(rp) + 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 efa6cc38..80a3bc62 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 + 2 + TypeClipLen = 1 + 4*4 + 4 + 2 + 4 TypeProfileLen = 1 ) diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go index a41f1c3d..f7805347 100644 --- a/internal/rendertest/clip_test.go +++ b/internal/rendertest/clip_test.go @@ -211,6 +211,96 @@ func TestStrokedPathRoundRound(t *testing.T) { }) } +func TestStrokedPathFlatMiter(t *testing.T) { + run(t, func(o *op.Ops) { + const width = 10 + sty := clip.StrokeStyle{ + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: 5, + } + + beg := f32.Pt(40, 10) + { + p := new(clip.Path) + p.Begin(o) + p.Move(beg) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + + p.Stroke(width, sty).Add(o) + paint.Fill(o, colornames.Red) + } + + { + p := new(clip.Path) + p.Begin(o) + p.Move(beg) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + + p.Stroke(2, clip.StrokeStyle{}).Add(o) + paint.Fill(o, colornames.Black) + } + + }, func(r result) { + r.expect(0, 0, colornames.White) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathFlatMiterInf(t *testing.T) { + run(t, func(o *op.Ops) { + const width = 10 + sty := clip.StrokeStyle{ + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + } + + beg := f32.Pt(40, 10) + { + p := new(clip.Path) + p.Begin(o) + p.Move(beg) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + + p.Stroke(width, sty).Add(o) + paint.Fill(o, colornames.Red) + } + + { + p := new(clip.Path) + p.Begin(o) + p.Move(beg) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + + p.Stroke(2, clip.StrokeStyle{}).Add(o) + paint.Fill(o, colornames.Black) + } + + }, func(r result) { + r.expect(0, 0, colornames.White) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + func TestStrokedPathZeroWidth(t *testing.T) { run(t, func(o *op.Ops) { const width = 2 diff --git a/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/internal/rendertest/refs/TestStrokedPathFlatMiter.png new file mode 100644 index 0000000000000000000000000000000000000000..95014b280da83d4b58cf51cbba059e88b1d21570 GIT binary patch literal 2309 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 a0RR6%f&P`cn;=&J0000