gpu,op/clip: implement stroked paths with round joins

Signed-off-by: Sebastien Binet <s@sbinet.org>
This commit is contained in:
Sebastien Binet
2020-11-11 12:57:17 +00:00
committed by Elias Naur
parent f3f32ed7aa
commit 700cec440e
7 changed files with 90 additions and 9 deletions
+2 -1
View File
@@ -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]),
},
}
}
+38 -2
View File
@@ -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 {
+1 -1
View File
@@ -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
)
+33 -3
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+1
View File
@@ -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.
+15 -2
View File
@@ -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
)