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 95014b28..7ad77e3a 100644 Binary files a/internal/rendertest/refs/TestStrokedPathFlatMiter.png and b/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ diff --git a/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png index d8596379..23a13c3b 100644 Binary files a/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png and b/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ diff --git a/internal/rendertest/render_test.go b/internal/rendertest/render_test.go index 75576150..eecac8fa 100644 --- a/internal/rendertest/render_test.go +++ b/internal/rendertest/render_test.go @@ -71,7 +71,10 @@ func TestRepeatedPaintsZ(t *testing.T) { builder.Line(f32.Pt(0, 10)) builder.Line(f32.Pt(-10, 0)) builder.Line(f32.Pt(0, -10)) - builder.Outline().Add(o) + p := builder.End() + clip.Outline{ + Path: p, + }.Op().Add(o) paint.Fill(o, red) }, func(r result) { r.expect(5, 5, colornames.Red) @@ -109,7 +112,8 @@ func constSqPath() op.CallOp { builder.Line(f32.Pt(0, 10)) builder.Line(f32.Pt(-10, 0)) builder.Line(f32.Pt(0, -10)) - builder.Outline().Add(innerOps) + p := builder.End() + clip.Outline{Path: p}.Op().Add(innerOps) return m.Stop() } diff --git a/op/clip/clip.go b/op/clip/clip.go index 10608178..825cba05 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -13,6 +13,52 @@ import ( "gioui.org/op" ) +// Op represents a clip area. Op intersects the current clip area with +// itself. +type Op struct { + bounds image.Rectangle + path PathSpec + + outline bool + stroke StrokeStyle +} + +func (p Op) Add(o *op.Ops) { + if p.path.quads > 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) }