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 00000000..95014b28 Binary files /dev/null and b/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ diff --git a/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png new file mode 100644 index 00000000..d8596379 Binary files /dev/null and b/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ diff --git a/op/clip/clip.go b/op/clip/clip.go index 9b2c27a0..a90e7b37 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -55,6 +55,7 @@ func (p Op) Add(o *op.Ops) { bo.PutUint32(data[17:], math.Float32bits(p.width)) data[21] = uint8(p.style.Cap) data[22] = uint8(p.style.Join) + bo.PutUint32(data[23:], math.Float32bits(p.style.Miter)) } // 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 abbcd4a2..e22f58ba 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -8,6 +8,11 @@ package clip type StrokeStyle struct { Cap StrokeCap Join StrokeJoin + + // Miter is the limit to apply to a miter joint. + // The zero Miter disables the miter joint; setting Miter to +∞ + // unconditionally enables the miter joint. + Miter float32 } // StrokeCap describes the head or tail of a stroked path.