From be89f8b9456815c98ed16312087398eca661537c Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Tue, 8 Dec 2020 14:55:26 +0000 Subject: [PATCH] all: introduce Outline and Stroke builders This CL introduces 2 new path builders: - Outline which takes a PathSpec to be outlined - Stroke which takes a PathSpec and a stroke style, to stroke a path. typically, code like this: var p clip.Path ... p.Outline().Add(o) should be replaced with: var p clip.Path ... clip.Outline{Path: p.End()}.Op().Add(o) similarly, stroking should be modified from: var p clip.Path ... p.Stroke(width, clip.StrokeStyle{...}).Add(o) to: var p clip.Path ... clip.Stroke{Path: p.End(), Style: clip.StrokeStyle{Width:...}}.Op().Add(o) here are tentative 'rf' scripts (see rsc.io/rf for more details): ``` ex { import "gioui.org/op"; import "gioui.org/op/clip"; var p clip.Path; var o *op.Ops; p.Outline().Add(o) -> clip.Outline{Path:p.End()}.Op().Add(o); } ex { import "gioui.org/op"; import "gioui.org/op/clip"; var o *op.Ops; var p clip.Path; var sty clip.StrokeStyle; var width float32; p.Stroke(width, sty).Add(o) -> \ clip.Stroke{ \ Path:p.End(), \ Style: clip.StrokeStyle{ \ Width: width, \ }}.Op().Add(o); } ``` Signed-off-by: Sebastien Binet --- font/opentype/opentype.go | 4 +- gpu/gpu.go | 119 +++++--- gpu/stroke.go | 41 +-- internal/opconst/ops.go | 8 +- internal/rendertest/clip_test.go | 264 +++++++++--------- .../refs/TestStrokedPathFlatMiter.png | Bin 2309 -> 2258 bytes .../refs/TestStrokedPathFlatMiterInf.png | Bin 2304 -> 2262 bytes internal/rendertest/render_test.go | 8 +- op/clip/clip.go | 121 ++++---- op/clip/shapes.go | 14 +- op/clip/stroke.go | 23 +- widget/material/loader.go | 12 +- 12 files changed, 355 insertions(+), 259 deletions(-) diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index 3e89cefc..3fa27365 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -323,7 +323,9 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str text. x += str.Advances[rune] rune++ } - builder.Outline().Add(ops) + clip.Outline{ + Path: builder.End(), + }.Op().Add(ops) return m.Stop() } diff --git a/gpu/gpu.go b/gpu/gpu.go index 5eb484bd..60cf3f50 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -109,6 +109,35 @@ type imageOp struct { place placement } +func decodeStrokeOp(data []byte) clip.StrokeStyle { + _ = data[10] + if opconst.OpType(data[0]) != opconst.TypeStroke { + panic("invalid op") + } + bo := binary.LittleEndian + return clip.StrokeStyle{ + Width: math.Float32frombits(bo.Uint32(data[1:])), + Miter: math.Float32frombits(bo.Uint32(data[5:])), + Cap: clip.StrokeCap(data[9]), + Join: clip.StrokeJoin(data[10]), + } +} + +type quadsOp struct { + quads uint32 + key ops.Key + aux []byte +} + +func decodeQuadsOp(data []byte) uint32 { + _ = data[:1+4] + if opconst.OpType(data[0]) != opconst.TypePath { + panic("invalid op") + } + bo := binary.LittleEndian + return bo.Uint32(data[1:]) +} + type material struct { material materialType opaque bool @@ -125,9 +154,8 @@ type material struct { // clipOp is the shadow of clip.Op. type clipOp struct { // TODO: Use image.Rectangle? - bounds f32.Rectangle - width float32 - style clip.StrokeStyle + bounds f32.Rectangle + outline bool } // imageOpData is the shadow of paint.ImageOp. @@ -159,13 +187,8 @@ func (op *clipOp) decode(data []byte) { }, } *op = clipOp{ - bounds: layout.FRect(r), - width: math.Float32frombits(bo.Uint32(data[17:])), - style: clip.StrokeStyle{ - Cap: clip.StrokeCap(data[21]), - Join: clip.StrokeJoin(data[22]), - Miter: math.Float32frombits(bo.Uint32(data[23:])), - }, + bounds: layout.FRect(r), + outline: data[17] == 1, } } @@ -786,8 +809,10 @@ func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) { } func (d *drawOps) collectOps(r *ops.Reader, state drawState) int { - var aux []byte - var auxKey ops.Key + var ( + quads quadsOp + stroke clip.StrokeStyle + ) loop: for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { switch opconst.OpType(encOp.Data[0]) { @@ -796,38 +821,51 @@ loop: case opconst.TypeTransform: dop := ops.DecodeTransform(encOp.Data) state.t = state.t.Mul(dop) - case opconst.TypeAux: - aux = encOp.Data[opconst.TypeAuxLen:] - auxKey = encOp.Key + + case opconst.TypeStroke: + stroke = decodeStrokeOp(encOp.Data) + + case opconst.TypePath: + quads.quads = decodeQuadsOp(encOp.Data) + if quads.quads > 0 { + encOp, ok = r.Decode() + if !ok { + break loop + } + quads.aux = encOp.Data[opconst.TypeAuxLen:] + quads.key = encOp.Key + } + case opconst.TypeClip: var op clipOp op.decode(encOp.Data) bounds := op.bounds trans, off := splitTransform(state.t) - if len(aux) > 0 { + if len(quads.aux) > 0 { // There is a clipping path, build the gpu data and update the // cache key such that it will be equal only if the transform is the // same also. Use cached data if we have it. - auxKey = auxKey.SetTransform(trans) - if v, ok := d.pathCache.get(auxKey); ok { + quads.key = quads.key.SetTransform(trans) + if v, ok := d.pathCache.get(quads.key); ok { // Since the GPU data exists in the cache aux will not be used. // Why is this not used for the offset shapes? op.bounds = v.bounds } else { - aux, op.bounds = d.buildVerts(aux, trans, op.width, op.style) + quads.aux, op.bounds = d.buildVerts(quads.aux, trans, op.outline, stroke) // add it to the cache, without GPU data, so the transform can be // reused. - d.pathCache.put(auxKey, opCacheValue{bounds: op.bounds}) + d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds}) } } else { - aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans) - auxKey = encOp.Key - auxKey.SetTransform(trans) + quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans) + quads.key = encOp.Key + quads.key.SetTransform(trans) } state.clip = state.clip.Intersect(op.bounds.Add(off)) - d.addClipPath(&state, aux, auxKey, op.bounds, off) - aux = nil - auxKey = ops.Key{} + d.addClipPath(&state, quads.aux, quads.key, op.bounds, off) + quads = quadsOp{} + stroke = clip.StrokeStyle{} + case opconst.TypeColor: state.matType = materialColor state.color = decodeColorOp(encOp.Data) @@ -1213,7 +1251,7 @@ func (d *drawOps) writeVertCache(n int) []byte { } // transform, split paths as needed, calculate maxY, bounds and create GPU vertices. -func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { +func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { inf := float32(math.Inf(+1)) d.qs.bounds = f32.Rectangle{ Min: f32.Point{X: inf, Y: inf}, @@ -1224,18 +1262,7 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty cli startLength := len(d.vertCache) switch { - default: - // Outline path. - for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ { - d.qs.contour = bo.Uint32(aux) - quad := ops.DecodeQuad(aux[4:]) - quad = quad.Transform(tr) - - d.qs.splitAndEncode(quad) - - aux = aux[ops.QuadSize+4:] - } - case width > 0: + case stroke.Width > 0: // Stroke path. quads := make(strokeQuads, 0, 2*len(aux)/(ops.QuadSize+4)) for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ { @@ -1246,13 +1273,25 @@ func (d *drawOps) buildVerts(aux []byte, tr f32.Affine2D, width float32, sty cli quads = append(quads, quad) aux = aux[ops.QuadSize+4:] } - quads = quads.stroke(width, sty) + quads = quads.stroke(stroke) for _, quad := range quads { d.qs.contour = quad.contour quad.quad = quad.quad.Transform(tr) d.qs.splitAndEncode(quad.quad) } + + case outline: + // Outline path. + for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ { + d.qs.contour = bo.Uint32(aux) + quad := ops.DecodeQuad(aux[4:]) + quad = quad.Transform(tr) + + d.qs.splitAndEncode(quad) + + aux = aux[ops.QuadSize+4:] + } } fillMaxY(d.vertCache[startLength:]) diff --git a/gpu/stroke.go b/gpu/stroke.go index 9a76c50a..edfb923f 100644 --- a/gpu/stroke.go +++ b/gpu/stroke.go @@ -106,14 +106,14 @@ func (qs strokeQuads) split() []strokeQuads { return o } -func (qs strokeQuads) stroke(width float32, sty clip.StrokeStyle) strokeQuads { +func (qs strokeQuads) stroke(stroke clip.StrokeStyle) strokeQuads { var ( o strokeQuads - hw = 0.5 * width + hw = 0.5 * stroke.Width ) for _, ps := range qs.split() { - rhs, lhs := ps.offset(hw, sty) + rhs, lhs := ps.offset(hw, stroke) switch lhs { case nil: o = o.append(rhs) @@ -132,13 +132,14 @@ func (qs strokeQuads) stroke(width float32, sty clip.StrokeStyle) strokeQuads { } } } + return o } // offset returns the right-hand and left-hand sides of the path, offset by // the half-width hw. -// The stroke style sty handles how segments are joined and ends are capped. -func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQuads) { +// The stroke handles how segments are joined and ends are capped. +func (qs strokeQuads) offset(hw float32, stroke clip.StrokeStyle) (rhs, lhs strokeQuads) { var ( states []strokeState beg = qs[0].quad.From @@ -180,7 +181,7 @@ func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQ next = states[0] } if state.n1 != next.n0 { - strokePathJoin(sty, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0) + strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0) } } } @@ -196,10 +197,10 @@ func (qs strokeQuads) offset(hw float32, sty clip.StrokeStyle) (rhs, lhs strokeQ // Default to counter-clockwise direction. lhs = lhs.reverse() - strokePathCap(sty, &rhs, hw, qend.p1, qend.n1) + strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1) rhs = rhs.append(lhs) - strokePathCap(sty, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1)) + strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1)) rhs.close() @@ -421,13 +422,13 @@ 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) +// stroke operation. +func strokePathJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + if stroke.Miter > 0 { + strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) return } - switch sty.Join { + switch stroke.Join { case clip.BevelJoin: strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) case clip.RoundJoin: @@ -468,14 +469,14 @@ 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) { +func strokePathMiterJoin(stroke 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) + limit := math.Max(float64(stroke.Miter), 1.001) cw := dotPt(rot90CW(n0), n1) >= 0.0 if cw { @@ -489,8 +490,8 @@ func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32 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) + stroke.Miter = 0 // Set miter to zero to disable the miter joint. + strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) return } mid := pivot.Add(normPt(n0.Add(n1), float32(d))) @@ -509,9 +510,9 @@ func strokePathMiterJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32 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 { +// strokePathCap caps the provided path qs, according to the provided stroke operation. +func strokePathCap(stroke clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) { + switch stroke.Cap { case clip.FlatCap: strokePathFlatCap(qs, hw, pivot, n0) case clip.SquareCap: diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 4c607538..e0db436c 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -31,6 +31,8 @@ const ( TypeClip TypeProfile TypeCursor + TypePath + TypeStroke ) const ( @@ -54,9 +56,11 @@ const ( TypePushLen = 1 TypePopLen = 1 TypeAuxLen = 1 - TypeClipLen = 1 + 4*4 + 4 + 2 + 4 + TypeClipLen = 1 + 4*4 + 1 TypeProfileLen = 1 TypeCursorLen = 1 + 1 + TypePathLen = 1 + 4 + TypeStrokeLen = 1 + 4 + 4 + 1 + 1 ) func (t OpType) Size() int { @@ -84,6 +88,8 @@ func (t OpType) Size() int { TypeClipLen, TypeProfileLen, TypeCursorLen, + TypePathLen, + TypeStrokeLen, }[t-firstOpIndex] } diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go index 91eafaef..f8f0f337 100644 --- a/internal/rendertest/clip_test.go +++ b/internal/rendertest/clip_test.go @@ -68,7 +68,9 @@ func TestPaintArc(t *testing.T) { p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi) p.Line(f32.Pt(0, -10)) p.Line(f32.Pt(-50, 0)) - p.Outline().Add(o) + clip.Outline{ + Path: p.End(), + }.Op().Add(o) paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) }, func(r result) { @@ -87,7 +89,9 @@ func TestPaintAbsolute(t *testing.T) { p.MoveTo(f32.Pt(20, 20)) p.LineTo(f32.Pt(80, 20)) p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80)) - p.Outline().Add(o) + clip.Outline{ + Path: p.End(), + }.Op().Add(o) paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) }, func(r result) { @@ -125,23 +129,15 @@ func TestPaintClippedTexture(t *testing.T) { func TestStrokedPathBevelFlat(t *testing.T) { run(t, func(o *op.Ops) { - const width = 2.5 - sty := clip.StrokeStyle{ - Cap: clip.FlatCap, - 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) + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) paint.Fill(o, red) }, func(r result) { @@ -152,23 +148,15 @@ 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, - 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) + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) paint.Fill(o, red) }, func(r result) { @@ -179,23 +167,15 @@ func TestStrokedPathBevelRound(t *testing.T) { func TestStrokedPathBevelSquare(t *testing.T) { run(t, func(o *op.Ops) { - const width = 2.5 - sty := clip.StrokeStyle{ - 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) + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.SquareCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) paint.Fill(o, red) }, func(r result) { @@ -206,23 +186,15 @@ func TestStrokedPathBevelSquare(t *testing.T) { 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) - 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) + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.RoundJoin, + }, + }.Op().Add(o) paint.Fill(o, red) }, func(r result) { @@ -233,40 +205,32 @@ 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) + stk := op.Push(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: 5, + }, + }.Op().Add(o) paint.Fill(o, red) + stk.Pop() } - { - 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) + stk := op.Push(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + }, + }.Op().Add(o) paint.Fill(o, black) + stk.Pop() } }, func(r result) { @@ -278,40 +242,32 @@ func TestStrokedPathFlatMiter(t *testing.T) { 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) + stk := op.Push(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + }.Op().Add(o) paint.Fill(o, red) + stk.Pop() } - { - 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) + stk := op.Push(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + }, + }.Op().Add(o) paint.Fill(o, black) + stk.Pop() } }, func(r result) { @@ -323,26 +279,35 @@ func TestStrokedPathFlatMiterInf(t *testing.T) { func TestStrokedPathZeroWidth(t *testing.T) { run(t, func(o *op.Ops) { - const width = 2 - var sty clip.StrokeStyle { + stk := op.Push(o) p := new(clip.Path) p.Begin(o) p.Move(f32.Pt(10, 50)) p.Line(f32.Pt(50, 0)) - p.Stroke(width, sty).Add(o) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: 2, + }, + }.Op().Add(o) paint.Fill(o, black) + stk.Pop() } { + stk := op.Push(o) p := new(clip.Path) p.Begin(o) p.Move(f32.Pt(10, 50)) p.Line(f32.Pt(30, 0)) - p.Stroke(0, sty).Add(o) // width=0, disable stroke + clip.Stroke{ + Path: p.End(), + }.Op().Add(o) // width=0, disable stroke paint.Fill(o, red) + stk.Pop() } }, func(r result) { @@ -352,3 +317,38 @@ func TestStrokedPathZeroWidth(t *testing.T) { r.expect(65, 50, colornames.White) }) } + +func newStrokedPath(o *op.Ops) clip.PathSpec { + 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)) + return p.End() +} + +func newZigZagPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(40, 10)) + 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)) + return p.End() +} + +func newEllipsePath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 65)) + p.Line(f32.Pt(20, 0)) + p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi) + return p.End() +} diff --git a/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/internal/rendertest/refs/TestStrokedPathFlatMiter.png index 95014b280da83d4b58cf51cbba059e88b1d21570..7ad77e3ac26cbe3742bff59c286895561d5dcecc 100644 GIT binary patch delta 2246 zcmV;%2s!tK64DWnB!8JnL_t(|oa~){h!p1;$GLL4rve)2~CR-G=T)7wKaJDpr-YD7ih~tz#1->kSe4?E+?lI z@ASBOCfeNI@7a0Eyt6YqyLXz4xp|-WotZDg^6u<=yN}<`Gk?!KGqcVXhzPm(WOE0Q zb>#q=Z_4uDyF04z(pMTk4S6fB*c3u{ARVc#yb@;v{sqXSPry|8bbm!Nqo0rztN z^HwtMbCw{B>x1tg&l>{1J9~X%?&n)Tz5X?ORg&&=0w8UcF`!k3)CxRN7iGfkOCz|% z?fY!^$Le0~=zmaQgM!bv%_ryXQ>Fpv;65t=#p>wzYWIau?0#kd%*XuzfLiyn0bri) zE6@EvX$PRU`)LQDz5A&KV1oPU1z?E#DFtAT`)LGVl>4azV4C~s0$`x~DFR@o`)L7S ztox||V6yw>12Eiu!vUD@zR3Vm;l8l|(&E0E08-?>fqwwf<-TbEQs=%=0Mh8bIRH}X zz99h8>%Iv9QtiI>0MhQh-T*S-zQzEu;l8c_GUL9M0J7x1egHD&zGeWj=e|w=GU>iH z0J7@79sn}zz6JoY?LIev%)8GDAa`Gi2|(_CC=LL*`!Oj85V&vKU+up7MF=XD6#JnT zK$QCufPY@@2LSr}l^UWHK)mwUws!6Z08XD)XNZ(40CjcWwzYFV0C4D#B11wc0Z{8c z0iBkPH_QT1+O|!FAu%NnV3zv?4CxBs;yw4!tzpWq2Y>zn{4YM|gAWq)WR?;Ga0mC90E_{D z2Ywu8eJgMdn3+BE=+UG+nM=vswz9Jm&pb2V$wHu9P9%&_CEz{acfh}8@!2aA|9f-Z zI@7i>Gm~gA^-@VYL0!E{FTPkw;o!zsLXZ$8pdI+3%=&Kw2Q6y|Ajheh7W~ztkJ8DL zG=Ds-IB@d;jE&LpVqz1F(HNb$2Unej=F+!1L(r zS$guxY8K07Mu_);wKD6Qfjfm7hllcz4c`T_!Rhs%z6iS z-Li)3cCf+BB}?eQ0l#W#AOit-p5H01T7Tsyaty$Axpx(E`8Dt#+pgO({AJlPYHOn# zH@N=|Rhb6h%P;BGS1V;C1TZbcyTAi7@izizZQBcX!Zp?(JLYdUn#m{teSNfUUB%E4 zKtHfeX8ls&ufSw&7vv9`o9Xaj`uub5=;xLp09@BUvMUy=N$oh34Gl*vt0)uyC4XSJ zuFwYn_U%*OS$-{<0H9Rz+m+g?a;Z@G8Bhd%1at>Arw+i*o$9Zy+=a9UFf>G4wp5)J z!Uf(17R$u{EpW583-XsWYy7=*xy&8hgEFvATao z{<@0*4g{@EtPS=1oc4B_p5_kvZlooE&d&G=16PO>vf{S@r!C8?Z9{46R=RwdJL0>S zegFdZLjY4k?6)idtOhQ`4{ieh2M^N31b4*WNSXoY>WW)DAsc)En}Rk!et&TL&_n)C z!hGoj;KL8&7LPSF>;jsA9l)(v8|n`>ZKA=UEM!%!j$9N`F~3T0EZ4$drBexEWfk$6X0e;LrvXpZ|A*qm5{groIB_H z8v^*)wjJORpytxK5OnGkbDW8Z^~XF%N3m;H^lX^uInQeZ{#f^urbmuo{d$i0REYH# zwcFZo=1ef0A^MHy^#Ug?>jy;DIs5lx^JeJ!UD5>*+qT!&7qva)IDdb!Z2`Ohxbb7% z)>fLDVvA6Tm;ku0-}!_91`376zzSd_ezIZhTDp0YCp?8>0`SpC)YKI9ovpIMyk}X} zo<|7k?d6G0tvGhV#*5x6}f-d>K7G!ElUdAJGb6Mf@mt!v>}CXdtx!&VQeehM&6b2(aCz`%>Az(QY}S*59I_~W<||Q6OrW{hw|IQqYiM|UX68IF zBt$Xze|iA=`*HvMih|Kb3NvnK2$Pe+@H$Zs@N@a?+KN`M#(npx3q~6$1aR$IG`uP| zyOr_!^VCxrtbadj00t$1WpM!6IP)`G3NOga+5%eSVK9Qzb8FW4djcUaJ&mR&jE~#mhyxU?xID0D-x)&5w2V5Sa`c2<;vo*k5RV{|iCB U`oS2KBLDyZ07*qoM6N<$g0L}3aR2}S delta 2297 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 Y|BcZ#geXOE;C?$;u{? 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 O0000 0 { + data := o.Write(opconst.TypePathLen) + data[0] = byte(opconst.TypePath) + bo := binary.LittleEndian + bo.PutUint32(data[1:], p.path.quads) + p.path.spec.Add(o) + } + + if p.stroke.Width > 0 { + data := o.Write(opconst.TypeStrokeLen) + data[0] = byte(opconst.TypeStroke) + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(p.stroke.Width)) + bo.PutUint32(data[5:], math.Float32bits(p.stroke.Miter)) + data[9] = uint8(p.stroke.Cap) + data[10] = uint8(p.stroke.Join) + } + + data := o.Write(opconst.TypeClipLen) + data[0] = byte(opconst.TypeClip) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(p.bounds.Min.X)) + bo.PutUint32(data[5:], uint32(p.bounds.Min.Y)) + bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) + bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) + if p.outline { + data[17] = byte(1) + } +} + +type PathSpec struct { + spec op.CallOp + quads uint32 // quads is the number Bézier segments in that path. +} + // Path constructs a Op clip path described by lines and // Bézier curves, where drawing outside the Path is discarded. // The inside-ness of a pixel is determines by the non-zero winding rule, @@ -26,38 +72,12 @@ type Path struct { pen f32.Point macro op.MacroOp start f32.Point + quads uint32 } // Pos returns the current pen position. func (p *Path) Pos() f32.Point { return p.pen } -// Op sets the current clip to the intersection of -// the existing clip with this clip. -// -// If you need to reset the clip to its previous values after -// applying a Op, use op.StackOp. -type Op struct { - call op.CallOp - bounds image.Rectangle - width float32 // Width of the stroked path, 0 for outline paths. - style StrokeStyle // Style of the stroked path, zero for outline paths. -} - -func (p Op) Add(o *op.Ops) { - p.call.Add(o) - data := o.Write(opconst.TypeClipLen) - data[0] = byte(opconst.TypeClip) - bo := binary.LittleEndian - bo.PutUint32(data[1:], uint32(p.bounds.Min.X)) - bo.PutUint32(data[5:], uint32(p.bounds.Min.Y)) - bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) - 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) - bo.PutUint32(data[23:], math.Float32bits(p.style.Miter)) -} - // Begin the path, storing the path data and final Op into ops. func (p *Path) Begin(ops *op.Ops) { p.ops = ops @@ -67,6 +87,15 @@ func (p *Path) Begin(ops *op.Ops) { data[0] = byte(opconst.TypeAux) } +// End returns a PathSpec ready to use in clipping operations. +func (p *Path) End() PathSpec { + c := p.macro.Stop() + return PathSpec{ + spec: c, + quads: p.quads, + } +} + // Move moves the pen by the amount specified by delta. func (p *Path) Move(delta f32.Point) { to := delta.Add(p.pen) @@ -120,6 +149,7 @@ func (p *Path) QuadTo(ctrl, to f32.Point) { To: to, }) p.pen = to + p.quads++ } // Arc adds an elliptical arc to the path. The implied ellipse is defined @@ -329,32 +359,22 @@ func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Po return splits } -// Outline closes the path and returns a clip operation that represents it. -func (p *Path) Outline() Op { +// Close closes the path. +func (p *Path) Close() { p.end() - c := p.macro.Stop() - return Op{ - call: c, - } } -// Stroke returns a stroked path with the specified width -// and configuration. -// If the provided width is <= 0, the path won't be stroked. -func (p *Path) Stroke(width float32, sty StrokeStyle) Op { - if width <= 0 { - // Explicitly discard the macro to ignore the path. - p.macro.Stop() - return Op{ - call: op.Record(p.ops).Stop(), - } - } +// Outline represents the area inside of a path, according to the +// non-zero winding rule. +type Outline struct { + Path PathSpec +} - c := p.macro.Stop() +// Op returns a clip operation representing the outline. +func (o Outline) Op() Op { return Op{ - call: c, - width: width, - style: sty, + path: o.Path, + outline: true, } } @@ -363,7 +383,10 @@ type Rect image.Rectangle // Op returns the op for the rectangle. func (r Rect) Op() Op { - return Op{bounds: image.Rectangle(r)} + return Op{ + bounds: image.Rectangle(r), + outline: true, + } } // Add the clip operation. diff --git a/op/clip/shapes.go b/op/clip/shapes.go index c5f551d1..3227b0fc 100644 --- a/op/clip/shapes.go +++ b/op/clip/shapes.go @@ -36,7 +36,10 @@ func (rr RRect) Op(ops *op.Ops) Op { p.Begin(ops) p.Move(rr.Rect.Min) roundRect(&p, rr.Rect.Size(), rr.SE, rr.SW, rr.NW, rr.NE) - return p.Outline() + + return Outline{ + Path: p.End(), + }.Op() } // Add the rectangle clip. @@ -49,7 +52,6 @@ type Border struct { // Rect is the bounds of the border. Rect f32.Rectangle Width float32 - Style StrokeStyle // The corner radii. SE, SW, NW, NE float32 } @@ -58,11 +60,15 @@ type Border struct { func (b Border) Op(ops *op.Ops) Op { var p Path p.Begin(ops) - p.Move(b.Rect.Min) roundRect(&p, b.Rect.Size(), b.SE, b.SW, b.NW, b.NE) - return p.Stroke(b.Width, b.Style) + return Stroke{ + Path: p.End(), + Style: StrokeStyle{ + Width: b.Width, + }, + }.Op() } // Add the border clip. diff --git a/op/clip/stroke.go b/op/clip/stroke.go index e22f58ba..8cfb721b 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -2,17 +2,30 @@ package clip -// StrokeStyle describes how a stroked path should be drawn. -// The zero value of StrokeStyle represents bevel-joined and flat-capped -// strokes. +// Stroke represents a stroked path. +type Stroke struct { + Path PathSpec + Style StrokeStyle +} + +// Op returns a clip operation representing the stroke. +func (s Stroke) Op() Op { + return Op{ + path: s.Path, + stroke: s.Style, + } +} + +// StrokeStyle describes how a path should be stroked. type StrokeStyle struct { - Cap StrokeCap - Join StrokeJoin + Width float32 // Width of the stroked path. // 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 + Cap StrokeCap // Cap describes the head or tail of a stroked path. + Join StrokeJoin // Join describes how stroked paths are collated. } // StrokeCap describes the head or tail of a stroked path. diff --git a/widget/material/loader.go b/widget/material/loader.go index 05fe508d..c34d2d37 100644 --- a/widget/material/loader.go +++ b/widget/material/loader.go @@ -67,15 +67,17 @@ func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) { pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius)) center = f32.Pt(0, 0).Sub(pen) - style = clip.StrokeStyle{ - Cap: clip.FlatCap, - } - p clip.Path ) p.Begin(ops) p.Move(pen) p.Arc(center, center, delta) - p.Stroke(width, style).Add(ops) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: width, + Cap: clip.FlatCap, + }, + }.Op().Add(ops) }