From bd1ef92dc4e7bbcaa494922d91574482b451d3fc Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 8 Oct 2021 17:56:50 +0200 Subject: [PATCH] op/clip: remove complex stroke support In a discussion with Raph Levien, the author of our compute renderer implementation, it became clear to me that it's not at all certain that complex strokes will ever be efficiently supported by a GPU renderer. At the same time, the machinery for converting a complex stroke to a GPU-friendly outline has a significant maintenance cost. Further, it is surprising to users that complex strokes are significantly slower and allocate memory. This change removes support for complex strokes, leaving only round-capped, round-joined strokes supported by the compute renderer. The default renderer still converts all strokes to outline, but it also caches the result. This is an API change. The complex stroke conversion code has been moved to the external gioui.org/x/stroke package, with a similar API. Updats gio#282 (Inkeliz brought up the allocation issue) Signed-off-by: Elias Naur --- gpu/compute.go | 35 +- gpu/gpu.go | 30 +- gpu/internal/rendertest/clip_test.go | 389 +---------------- .../refs/TestDashedPathFlatCapEllipse.png | Bin 5159 -> 0 bytes .../refs/TestDashedPathFlatCapZ.png | Bin 2891 -> 0 bytes .../refs/TestDashedPathFlatCapZNoDash.png | Bin 1573 -> 0 bytes .../refs/TestDashedPathFlatCapZNoPath.png | Bin 765 -> 0 bytes .../refs/TestStrokedPathBevelFlat.png | Bin 2758 -> 0 bytes .../refs/TestStrokedPathBevelRound.png | Bin 2769 -> 0 bytes .../refs/TestStrokedPathBevelSquare.png | Bin 2756 -> 0 bytes .../refs/TestStrokedPathFlatMiter.png | Bin 1585 -> 0 bytes .../refs/TestStrokedPathFlatMiterInf.png | Bin 1573 -> 0 bytes .../refs/TestStrokedPathRoundRound.png | Bin 2782 -> 0 bytes internal/stroke/dash.go | 394 ------------------ internal/stroke/stroke.go | 151 +------ op/clip/clip.go | 93 +---- op/clip/stroke.go | 118 ------ widget/border.go | 2 +- widget/material/loader.go | 7 +- 19 files changed, 60 insertions(+), 1159 deletions(-) delete mode 100644 gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png delete mode 100644 gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png delete mode 100644 gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png delete mode 100644 gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png delete mode 100644 gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png delete mode 100644 gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png delete mode 100644 gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png delete mode 100644 gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png delete mode 100644 gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png delete mode 100644 gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png delete mode 100644 internal/stroke/dash.go delete mode 100644 op/clip/stroke.go diff --git a/gpu/compute.go b/gpu/compute.go index ee4eccc3..1645e061 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -28,7 +28,6 @@ import ( "gioui.org/internal/scene" "gioui.org/layout" "gioui.org/op" - "gioui.org/op/clip" "gioui.org/shader" "gioui.org/shader/gio" "gioui.org/shader/piet" @@ -236,10 +235,10 @@ type encoderState struct { // clipKey completely describes a clip operation (along with its path) and is appropriate // for hashing and equality checks. type clipKey struct { - bounds f32.Rectangle - stroke clip.StrokeStyle - relTrans f32.Affine2D - pathHash uint64 + bounds f32.Rectangle + strokeWidth float32 + relTrans f32.Affine2D + pathHash uint64 } // paintKey completely defines a paint operation. It is suitable for hashing and @@ -1683,7 +1682,7 @@ func (c *opsCollector) reset() { c.layers = c.layers[:0] } -func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, hash uint64, stroke clip.StrokeStyle, push bool) { +func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, path []byte, key ops.Key, hash uint64, strokeWidth float32, push bool) { // Rectangle clip regions. if len(path) == 0 && !push { // If the rectangular clip region contains a previous path it can be discarded. @@ -1713,10 +1712,10 @@ func (c *collector) addClip(state *encoderState, viewport, bounds f32.Rectangle, intersect: intersect, push: push, clipKey: clipKey{ - bounds: bounds, - relTrans: state.relTrans, - stroke: stroke, - pathHash: hash, + bounds: bounds, + relTrans: state.relTrans, + strokeWidth: strokeWidth, + pathHash: hash, }, }) state.clip = &c.clipStates[len(c.clipStates)-1] @@ -1742,9 +1741,9 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur key ops.Key hash uint64 } - str clip.StrokeStyle + strWidth float32 ) - c.addClip(&state, fview, fview, nil, ops.Key{}, 0, clip.StrokeStyle{}, false) + c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false) for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { switch ops.OpType(encOp.Data[0]) { case ops.TypeProfile: @@ -1763,7 +1762,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur state.t = st.t state.relTrans = st.relTrans case ops.TypeStroke: - str = decodeStrokeOp(encOp.Data) + strWidth = decodeStrokeOp(encOp.Data) case ops.TypePath: hash := bo.Uint64(encOp.Data[1:]) encOp, ok = r.Decode() @@ -1776,9 +1775,9 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur case ops.TypeClip: var op clipOp op.decode(encOp.Data) - c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, pathData.hash, str, op.push) + c.addClip(&state, fview, op.bounds, pathData.data, pathData.key, pathData.hash, strWidth, op.push) pathData.data = nil - str = clip.StrokeStyle{} + strWidth = 0 case ops.TypePopClip: for { push := state.clip.push @@ -1806,7 +1805,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur if paintState.matType == materialTexture { // Clip to the bounds of the image, to hide other images in the atlas. bounds := paintState.image.src.Bounds() - c.addClip(&paintState, fview, layout.FRect(bounds), nil, ops.Key{}, 0, clip.StrokeStyle{}, false) + c.addClip(&paintState, fview, layout.FRect(bounds), nil, ops.Key{}, 0, 0, false) } intersect := paintState.clip.intersect if intersect.Empty() { @@ -2101,9 +2100,9 @@ func encodeOp(viewport image.Point, absOff image.Point, enc *encoder, texOps []t enc.transform(inv) for i := len(op.clipStack) - 1; i >= 0; i-- { cl := op.clipStack[i] - if str := cl.state.stroke; str.Width > 0 { + if w := cl.state.strokeWidth; w > 0 { enc.fillMode(scene.FillModeStroke) - enc.lineWidth(str.Width) + enc.lineWidth(w) fillMode = scene.FillModeStroke } else if fillMode != scene.FillModeNonzero { enc.fillMode(scene.FillModeNonzero) diff --git a/gpu/gpu.go b/gpu/gpu.go index 7cd7df40..0ca6b689 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -27,7 +27,6 @@ import ( "gioui.org/internal/stroke" "gioui.org/layout" "gioui.org/op" - "gioui.org/op/clip" "gioui.org/shader" "gioui.org/shader/gio" @@ -135,15 +134,13 @@ type imageOp struct { place placement } -func decodeStrokeOp(data []byte) clip.StrokeStyle { +func decodeStrokeOp(data []byte) float32 { _ = data[4] if ops.OpType(data[0]) != ops.TypeStroke { panic("invalid op") } bo := binary.LittleEndian - return clip.StrokeStyle{ - Width: math.Float32frombits(bo.Uint32(data[1:])), - } + return math.Float32frombits(bo.Uint32(data[1:])) } type quadsOp struct { @@ -907,9 +904,9 @@ func (k opKey) SetTransform(t f32.Affine2D) opKey { func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) { var ( - quads quadsOp - str clip.StrokeStyle - state drawState + quads quadsOp + strWidth float32 + state drawState ) reset := func() { state = drawState{ @@ -934,7 +931,7 @@ loop: d.transStack = d.transStack[:n-1] case ops.TypeStroke: - str = decodeStrokeOp(encOp.Data) + strWidth = decodeStrokeOp(encOp.Data) case ops.TypePath: encOp, ok = r.Decode() @@ -960,7 +957,7 @@ loop: op.bounds = v.bounds } else { pathData, bounds := d.buildVerts( - quads.aux, trans, op.outline, str, + quads.aux, trans, op.outline, strWidth, ) op.bounds = bounds quads.aux = pathData @@ -974,7 +971,7 @@ loop: } d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, op.push) quads = quadsOp{} - str = clip.StrokeStyle{} + strWidth = 0 case ops.TypePopClip: for { push := state.cpath.push @@ -1343,7 +1340,7 @@ func (d *drawOps) writeVertCache(n int) []byte { } // transform, split paths as needed, calculate maxY, bounds and create GPU vertices. -func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, strWidth float32) (verts []byte, bounds f32.Rectangle) { inf := float32(math.Inf(+1)) d.qs.bounds = f32.Rectangle{ Min: f32.Point{X: inf, Y: inf}, @@ -1353,15 +1350,12 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str startLength := len(d.vertCache) switch { - case str.Width > 0: + case strWidth > 0: // Stroke path. ss := stroke.StrokeStyle{ - Width: str.Width, - Miter: str.Miter, - Cap: stroke.StrokeCap(str.Cap), - Join: stroke.StrokeJoin(str.Join), + Width: strWidth, } - quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) + quads := stroke.StrokePathCommands(ss, pathData) for _, quad := range quads { d.qs.contour = quad.Contour quad.Quad = quad.Quad.Transform(tr) diff --git a/gpu/internal/rendertest/clip_test.go b/gpu/internal/rendertest/clip_test.go index 895adddb..dbee98fc 100644 --- a/gpu/internal/rendertest/clip_test.go +++ b/gpu/internal/rendertest/clip_test.go @@ -125,10 +125,8 @@ func TestTexturedStrokeClipped(t *testing.T) { smallSquares.Add(o) defer op.Offset(f32.Pt(50, 50)).Push(o).Pop() defer clip.Stroke{ - Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), - Style: clip.StrokeStyle{ - Width: 10, - }, + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Width: 10, }.Op().Push(o).Pop() defer clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Push(o).Pop() defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop() @@ -142,10 +140,8 @@ func TestTexturedStroke(t *testing.T) { smallSquares.Add(o) defer op.Offset(f32.Pt(50, 50)).Push(o).Pop() defer clip.Stroke{ - Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), - Style: clip.StrokeStyle{ - Width: 10, - }, + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Width: 10, }.Op().Push(o).Pop() defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop() paint.PaintOp{}.Add(o) @@ -165,175 +161,8 @@ func TestPaintClippedTexture(t *testing.T) { }) } -func TestStrokedPathBevelFlat(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathBevelRound(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.RoundCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathBevelSquare(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.SquareCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathRoundRound(t *testing.T) { - run(t, func(o *op.Ops) { - p := newStrokedPath(o) - defer clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2.5, - Cap: clip.RoundCap, - Join: clip.RoundJoin, - }, - }.Op().Push(o).Pop() - - paint.Fill(o, red) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Red) - }) -} - -func TestStrokedPathFlatMiter(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: 5, - }, - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - }) -} - -func TestStrokedPathFlatMiterInf(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - }) -} - func TestStrokedPathZeroWidth(t *testing.T) { run(t, func(o *op.Ops) { - { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(10, 50)) - p.Line(f32.Pt(50, 0)) - cl := clip.Stroke{ - Path: p.End(), - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - - paint.Fill(o, black) - cl.Pop() - } - { p := new(clip.Path) p.Begin(o) @@ -354,213 +183,3 @@ func TestStrokedPathZeroWidth(t *testing.T) { r.expect(65, 50, transparent) }) } - -func TestDashedPathFlatCapEllipse(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newEllipsePath(o) - - var dash clip.Dash - dash.Begin(o) - dash.Dash(5) - dash.Dash(3) - - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - - paint.Fill( - o, - red, - ) - cl.Pop() - } - { - p := newEllipsePath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - }, - }.Op().Push(o) - - paint.Fill( - o, - black, - ) - cl.Pop() - } - - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(0, 62, colornames.Red) - r.expect(0, 65, colornames.Black) - }) -} - -func TestDashedPathFlatCapZ(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - var dash clip.Dash - dash.Begin(o) - dash.Dash(5) - dash.Dash(3) - - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - r.expect(46, 12, transparent) - }) -} - -func TestDashedPathFlatCapZNoDash(t *testing.T) { - run(t, func(o *op.Ops) { - { - p := newZigZagPath(o) - var dash clip.Dash - dash.Begin(o) - dash.Phase(1) - - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - cl := clip.Stroke{ - Path: newZigZagPath(o), - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, colornames.Red) - r.expect(46, 12, colornames.Red) - }) -} - -func TestDashedPathFlatCapZNoPath(t *testing.T) { - run(t, func(o *op.Ops) { - { - var dash clip.Dash - dash.Begin(o) - dash.Dash(0) - cl := clip.Stroke{ - Path: newZigZagPath(o), - Style: clip.StrokeStyle{ - Width: 10, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - Miter: float32(math.Inf(+1)), - }, - Dashes: dash.End(), - }.Op().Push(o) - paint.Fill(o, red) - cl.Pop() - } - { - p := newZigZagPath(o) - cl := clip.Stroke{ - Path: p, - Style: clip.StrokeStyle{ - Width: 2, - Cap: clip.FlatCap, - Join: clip.BevelJoin, - }, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(40, 10, colornames.Black) - r.expect(40, 12, transparent) - r.expect(46, 12, transparent) - }) -} - -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/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png deleted file mode 100644 index 79bae3842d74b1eac858919856ac82870bab4aae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5159 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 diff --git a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png deleted file mode 100644 index d315f0f8161e098aeb2979d663358de4bd5054ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1573 zcma)+|2xwO0LMSmObcnE%~UF_Z>lX+D%)@^hPIXOVVg=6-Q0Cov}r^7;!d>aiHMfNg}pmA~uZ7#=czFJ^g;q^ZxR@-+#dCGKJ)Cy9l`m003KJz}|yj zn)9z0LcZjmxHuUASgMG7eM3`w)OXmI9O4l#@XY9hsPf2LnFW+^aMZqK_6L?YI))xb z`8qg3koQvqjPVqR@6jRx^nRve=HGGgFv6|h3d-3CtHN~;ZWM-_yr&y`)8>wp)Xsi9 zHajjoJbY)u>cS7&{;00uY43?fkLmHv3s;|C;!m(JjC=#@n)Gm(SQ+$H zc0Q)vt?0)r0(mN1Y26CvqSw*_#QCh%dcXgF=(*@`a@pQ{f9!D@N8ElD@>$()Z@x!m|U&*3YLcm?MPyMdsPz`5QVCuk7%I<>xgkgB<%+t`1( z2miSkQB^5*%sGK;Dml(?ao?v~FAdMMovAF5n}0CXf**&3PJ47X54uj_+}Re)rXez< zH&P;EcsvP$y^0tjyC;%mD(w+t3U3bnY;l()X3q;oH zQy+MrTao@6zPG)V3GK5yombL{hiZe2kItOgz}0(yM7JPS>6Sa1-OSZUSC&g3ny82& zxR(@4Fk`9ayDnq?8R=0XnLH`wb}8bjfyB?I&g{%GQbaP^5zN5@Z9D^9Fy8-a_GL-< z{0DI-y>87-hMt`8>dh^y9X843c?J*VgvS#itgYU_v~B1t#J7u#PlXk6jIj$fIdrlA ze1!9IqP*?XC+%>T>CK3L`NPPkJj35mRWSJlRXMElNQy8C1{+G@1dA$?ELL4;rmJNQ zK{kOoRMQZdEbAL6#6N6Swt=Y;$^ox&T5^h4!Mt0y5H}#dx`my+34eFgjKPlF9(pS; z>Ql2zMX8z=33`hz9pA_7LhH*-rdW7!MGHX+O(f;b&FYK>==)RCcF34IpIEl%TXU!` zJ2!Arda1oOZ{7~r02;{0H{HVqa~Le-yDHUVc3QhQkmF@f-6eTT~6q3`i)r{%=s7Q1!3~s zvKFE(q8aCFO4gsU-1#fFQ%7l0{!|xchru+TcV@D%e!>Gc!Cv?ey7%vj+GxdK#Pvep z)zoL&6%q1;4Mp?7^PXqX=n&4}$qyF#R!G9jcTd+|8x4CtXzHOT5y`|paJRh<&}GAP@(PYtKN$f{U*&kM^j&bN;yc<>KSv>*S}zT)?q zP8}QB#+>P`d$7`H1L(ZH&wrTx%G$4?!E5ZuGpt(%#2@^vr(%%=;XO diff --git a/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png deleted file mode 100644 index 94c160ef51d703bb49f52a3c54a99a07a05fe041..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 765 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 K=E{w%6aNL52FM)% diff --git a/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png deleted file mode 100644 index a37235cca098039b66c670ff24e171a9f408a173..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2769 zcmbVOdpy&98~*-&+iaH2`84N>Ii$oybDHE-2KU`rG?#mzn$n;euF4)M%yCS%P9MwP|N=V ziZ7Y+7j-tN)5#Loq>5~DZKU}HOJi&U&fZ9pMRTS-e1c!df2G}jbbHQ9SMp=z9@VW1 z)8fqeY);-=?02)+Fqsl2aKhUgR1fk8f~@*Ys|9?S<226d>IGw7X=c06Ez<+_7y%o5SS1U)k7scw+G*kls(8*qgYn$#sF#;J&B6qFuS}ln7;m$W<37(Dn zUZXu#eq_)AcD3%9!o+JeRr&#y)I*#h*^8GaV;V*hMSrb?IA2}vaI4x4vj;^)&Xd9M zng&*wvq~dbxC8jpxFK%s_Si9#QuNokfWn2a?;*9FY+E`fRl2QkDCUW z_;%~dM%U_VLuAQOlTjPMJUOx6jM#na*ll?>t9>bm+mHcf#~iY07GVmMCRqi#VRV#i zr6%Xjc_?}_iEu#K(Ium24uqv~bMb`U(;%z=6+bAAdl|C8$$>NkSm69+*|ZLRK8zwx zlF~G@Aw=Ip7EovA>yY5hq|?--lwe^3Y@>C#iiNyWK!3#)pQqA$)PkZ2u#ro;FxC}2r1DyJHUOq^ znjWQ-C1tSsn8u8C40+YJ$7*sPt_N@bRX9$Zlg+EB<~i4`O3IO=D25Wi^{ax_LKOyh@V;#JEJ!cnaY1Rr530;BW| z0KB^{aB8=pWQcRXAE3j=-V`OCjQnK4;YgFRVc+2JGwTxYpTfDA7*?L8nH$g6+r~m*%QpJVS`a z*n6^X!7FiHklC7&`Cv}<`2*2g8QRY-9$Dkvs{T^&25`k)=LI>YQCzn$V8s7>0*xeW z`lou0 z*E2(&4X~=8W!Y4#0VQi4SB&rq@;zSJrDZQ*JB|}b6E9q<8_FOB>DkW7sj6?pN*Q*V zH~-@BF>KGe*>k11UT~Ju|u*;6`+R<(b9Hhn1U2_DR*9_8^s96L%3UM z!$@HDleNPzFkZe}dc(~SYz$iZ6?kgq)kZw&eYP5yWtHRHup+B3nyHQJ5PbC>OH1!91sKwe6-Nu0iw>T^ zn13__?XM*LleVW1^2N<9W>gr`v-X2j-!m92%R0*q$42%Dq;c4WV;C@SUUXdX?%Rz3 zzB|qQLA(<|A;`07cPkYoTV)bzU^X#Q9=lhVlnc{ZUgooov8MWqfNihR?2^+BYk-zX zRpj6Bf`kP>&nCC8dSp>jNethCmsN*1YuQ@?anXA%Jn~OF;7)$pP5rJ#y8L$58fGKf zfphayjVdp7I#j@d-Cdkob^BX1a2BhPt2dt@QRfcp3G`9f81CLBpbF@!6qJ^~JR9+b zSEtRGMpd_+`p2xS{{j6wZb z3dCLk>^Dj*)NhMq0q$`SnoRw9Pf_#Flr%%o2qgsy$Kl9W2}+#O051lv}__z z6UedVEF`%BcBw&VB&z5MjxuhI zdzcrNWdY36KK!%3I%g+?pl_lIGtxxLGxB4Ltx! z&vY$~T_Vv2o?ouhH1-OM)Hv52dhIlUNxWp3VHsgf8zJeF-~Ty{;p{N6yY2F{Y|+Dk z-mD7-bdz1n+CFU1?A5I_EBP_}^(y3x0oKxn#H*j~+tFUsGk7h;6Wk^Xu-;`JzvOD5 z;Z0g!tQ^7C-+ARjw2c*k-3q-vVb9kAty!mlh8JZF}H#4eh;!fB;;K6bUUgi)iTd5;B?)8$S_no&d7eJBBiT<__^~T${*u1lCTcG^jxJ1^Ub_S%2pPep zI`HFAaSlJsZT1xc46|%%Vll#L#Y={!3_7#Uf8;3M8b1C7>sE&X4h!E-c7D=Qj&1yH zZKyY(@ftQ=^qfs`Z>>`T4whj&*3_6)o;rOSU9=Q};(7ux@AyNOx!z7=lkmgCU#1hb zed#owxRQxYf4BDh%dfAwS_g2I2f6Nz*I|WSq-!U7c3#cU>IwOBK&40Y77-VWu6i-} z9wD_H8Cn^@brn0GlC$;H8w0SQ z#9%6O+*)f#;nf$!T1!CHW}D96x*thgh?yQ`J2?)H9(o$VdkOv*1xMenf)iceY+Nk7 RYu2JF@O1UrS-FFe`X3;H+vorQ diff --git a/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png deleted file mode 100644 index 8d2919d30a0426249eea651f77d793f7f924b8e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2756 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 UqJJjn^(z44ViKZnMzMu|0zY;X?*IS* diff --git a/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png deleted file mode 100644 index d315f0f8161e098aeb2979d663358de4bd5054ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1573 zcma)+|2xwO0LMSmObcnE%~UF_Z>lX+D%)@^hPIXOVVg=6-Q0Cov}r^7;!d>aiHMfNg}pmA~uZ7#=czFJ^g;q^ZxR@-+#dCGKJ)Cy9l`m003KJz}|yj zn)9z0LcZjmxHuUASgMG7eM3`w)OXmI9O4l#@XY9hsPf2LnFW+^aMZqK_6L?YI))xb z`8qg3koQvqjPVqR@6jRx^nRve=HGGgFv6|h3d-3CtHN~;ZWM-_yr&y`)8>wp)Xsi9 zHajjoJbY)u>cS7&{;00uY43?fkLmHv3s;|C;!m(JjC=#@n)Gm(SQ+$H zc0Q)vt?0)r0(mN1Y26CvqSw*_#QCh%dcXgF=(*@`a@pQ{f9!D@N8ElD@>$()Z@x!m|U&*3YLcm?MPyMdsPz`5QVCuk7%I<>xgkgB<%+t`1( z2miSkQB^5*%sGK;Dml(?ao?v~FAdMMovAF5n}0CXf**&3PJ47X54uj_+}Re)rXez< zH&P;EcsvP$y^0tjyC;%mD(w+t3U3bnY;l()X3q;oH zQy+MrTao@6zPG)V3GK5yombL{hiZe2kItOgz}0(yM7JPS>6Sa1-OSZUSC&g3ny82& zxR(@4Fk`9ayDnq?8R=0XnLH`wb}8bjfyB?I&g{%GQbaP^5zN5@Z9D^9Fy8-a_GL-< z{0DI-y>87-hMt`8>dh^y9X843c?J*VgvS#itgYU_v~B1t#J7u#PlXk6jIj$fIdrlA ze1!9IqP*?XC+%>T>CK3L`NPPkJj35mRWSJlRXMElNQy8C1{+G@1dA$?ELL4;rmJNQ zK{kOoRMQZdEbAL6#6N6Swt=Y;$^ox&T5^h4!Mt0y5H}#dx`my+34eFgjKPlF9(pS; z>Ql2zMX8z=33`hz9pA_7LhH*-rdW7!MGHX+O(f;b&FYK>==)RCcF34IpIEl%TXU!` zJ2!Arda1oOZ{7~r02;{0H{HVqa~Le-yDHUVc3QhQkmF@f-6eTT~6q3`i)r{%=s7Q1!3~s zvKFE(q8aCFO4gsU-1#fFQ%7l0{!|xchru+TcV@D%e!>Gc!Cv?ey7%vj+GxdK#Pvep z)zoL&6%q1;4Mp?7^PXqX=n&4}$qyF#R!G9jcTd+|8x4CtXzHOT5y`|paJRh<&}GAP@(PYtKN$f{U*&kM^j&bN;yc<>KSv>*S}zT)?q zP8}QB#+>P`d$7`H1L(ZH&wrTx%G$4?!E5ZuGpt(%#2@^vr(%%=;XO diff --git a/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png deleted file mode 100644 index 8ef5a942f2c0f5ffe9ea7552135cd3abe085d95d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2782 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 g5c0oF7_wJ@<4(=k?JM 0 { - r0 = oi.pen() - } - oi = append(oi, StrokeQuad{ - Contour: *contour, - Quad: QuadSegment{ - From: r0, - Ctrl: r1, - To: r2, - }, - }) - } - t += dt - } - } - if len(oi) > 0 { - push() - (*contour)++ - } - - return oo -} - -func f32Eq(a, b float32) bool { - const epsilon = 1e-10 - return math.Abs(float64(a-b)) < epsilon -} - -func f64Eq(a, b float64) bool { - const epsilon = 1e-10 - return math.Abs(a-b) < epsilon -} - -func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, fp func(float64) float64, tmin, tmax float64) (func(float64) float64, float64) { - // The TODOs below are copied verbatim from tdewolff/canvas: - // - // TODO: find better way to determine N. For Arc 10 seems fine, for some - // Quads 10 is too low, for Cube depending on inflection points is - // maybe not the best indicator - // - // TODO: track efficiency, how many times is fp called? - // Does a look-up table make more sense? - fLength := func(t float64) float64 { - return math.Abs(gaussLegendre(fp, tmin, t)) - } - totalLength := fLength(tmax) - t := func(L float64) float64 { - return bisectionMethod(fLength, L, tmin, tmax) - } - return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, tmax), totalLength -} - -func polynomialChebyshevApprox(N int, f func(float64) float64, xmin, xmax, ymin, ymax float64) func(float64) float64 { - var ( - invN = 1.0 / float64(N) - fs = make([]float64, N) - ) - for k := 0; k < N; k++ { - u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN) - fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1)) - } - - c := make([]float64, N) - for j := 0; j < N; j++ { - var a float64 - for k := 0; k < N; k++ { - a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N)) - } - c[j] = 2 * invN * a - } - - if ymax < ymin { - ymin, ymax = ymax, ymin - } - return func(x float64) float64 { - x = math.Min(xmax, math.Max(xmin, x)) - u := (x-xmin)/(xmax-xmin)*2 - 1 - var a float64 - for j := 0; j < N; j++ { - a += c[j] * math.Cos(float64(j)*math.Acos(u)) - } - y := -0.5*c[0] + a - if !math.IsNaN(ymin) && !math.IsNaN(ymax) { - y = math.Min(ymax, math.Max(ymin, y)) - } - return y - } -} - -// bisectionMethod finds the value x for which f(x) = y in the interval x -// in [xmin, xmax] using the bisection method. -func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 { - const ( - maxIter = 100 - tolerance = 0.001 // 0.1% - ) - - var ( - n = 0 - x float64 - tolX = math.Abs(xmax-xmin) * tolerance - tolY = math.Abs(f(xmax)-f(xmin)) * tolerance - ) - for { - x = 0.5 * (xmin + xmax) - if n >= maxIter { - return x - } - - dy := f(x) - y - switch { - case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX: - return x - case dy > 0: - xmax = x - default: - xmin = x - } - n++ - } -} - -type gaussLegendreFunc func(func(float64) float64, float64, float64) float64 - -// Gauss-Legendre quadrature integration from a to b with n=7 -func gaussLegendre7(f func(float64) float64, a, b float64) float64 { - c := 0.5 * (b - a) - d := 0.5 * (a + b) - Qd1 := f(-0.949108*c + d) - Qd2 := f(-0.741531*c + d) - Qd3 := f(-0.405845*c + d) - Qd4 := f(d) - Qd5 := f(0.405845*c + d) - Qd6 := f(0.741531*c + d) - Qd7 := f(0.949108*c + d) - return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4) -} diff --git a/internal/stroke/stroke.go b/internal/stroke/stroke.go index fda42dbd..95025eb8 100644 --- a/internal/stroke/stroke.go +++ b/internal/stroke/stroke.go @@ -40,26 +40,8 @@ import ( // op/clip, eliminating the duplicate types. type StrokeStyle struct { Width float32 - Miter float32 - Cap StrokeCap - Join StrokeJoin } -type StrokeCap uint8 - -const ( - RoundCap StrokeCap = iota - FlatCap - SquareCap -) - -type StrokeJoin uint8 - -const ( - RoundJoin StrokeJoin = iota - BevelJoin -) - // strokeTolerance is used to reconcile rounding errors arising // when splitting quads into smaller and smaller segments to approximate // them into straight lines, and when joining back segments. @@ -97,12 +79,6 @@ func (qs *StrokeQuads) pen() f32.Point { return (*qs)[len(*qs)-1].Quad.To } -func (qs *StrokeQuads) closed() bool { - beg := (*qs)[0].Quad.From - end := (*qs)[len(*qs)-1].Quad.To - return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y) -} - func (qs *StrokeQuads) lineTo(pt f32.Point) { end := qs.pen() *qs = append(*qs, StrokeQuad{ @@ -155,11 +131,7 @@ func (qs StrokeQuads) split() []StrokeQuads { return o } -func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { - if !IsSolidLine(dashes) { - qs = qs.dash(dashes) - } - +func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads { var ( o StrokeQuads hw = 0.5 * stroke.Width @@ -441,29 +413,6 @@ func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { return p.Mul(2) } -// quadBezierLen returns the length of the Bézier curve. -// See: -// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ -func quadBezierLen(p0, p1, p2 f32.Point) float32 { - a := p0.Sub(p1.Mul(2)).Add(p2) - b := p1.Mul(2).Sub(p0.Mul(2)) - A := float64(4 * dotPt(a, a)) - B := float64(4 * dotPt(a, b)) - C := float64(dotPt(b, b)) - if f64Eq(A, 0.0) { - // p1 is in the middle between p0 and p2, - // so it is a straight line from p0 to p2. - return lenPt(p2.Sub(p0)) - } - - Sabc := 2 * math.Sqrt(A+B+C) - A2 := math.Sqrt(A) - A32 := 2 * A * A2 - C2 := 2 * math.Sqrt(C) - BA := B / A2 - return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32)) -} - func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { // Gio strokes are only quadratic Bézier curves, w/o any inflection point. // So we just have to flatten them. @@ -549,27 +498,7 @@ 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 operation. func strokePathJoin(stroke 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 stroke.Join { - case BevelJoin: - strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) - case 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) { - - rp := pivot.Add(n1) - lp := pivot.Sub(n1) - - rhs.lineTo(rp) - lhs.lineTo(lp) + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) } func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { @@ -594,79 +523,9 @@ func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Po } } -func strokePathMiterJoin(stroke 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(stroke.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) { - 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))) - - 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 operation. func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - switch stroke.Cap { - case FlatCap: - strokePathFlatCap(qs, hw, pivot, n0) - case SquareCap: - strokePathSquareCap(qs, hw, pivot, n0) - case RoundCap: - strokePathRoundCap(qs, hw, pivot, n0) - default: - panic("impossible") - } -} - -// strokePathFlatCap caps the start or end of a path with a flat cap. -func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - end := pivot.Sub(n0) - qs.lineTo(end) -} - -// strokePathSquareCap caps the start or end of a path with a square cap. -func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - var ( - e = pivot.Add(rot90CCW(n0)) - corner1 = e.Add(n0) - corner2 = e.Sub(n0) - end = pivot.Sub(n0) - ) - - qs.lineTo(corner1) - qs.lineTo(corner2) - qs.lineTo(end) + strokePathRoundCap(qs, hw, pivot, n0) } // strokePathRoundCap caps the start or end of a path with a round cap. @@ -763,9 +622,9 @@ func dist(p1, p2 f32.Point) float64 { return math.Hypot(dx, dy) } -func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads { +func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads { quads := decodeToStrokeQuads(scene) - return quads.stroke(style, dashes) + return quads.stroke(style) } // decodeToStrokeQuads decodes scene commands to quads ready to stroke. diff --git a/op/clip/clip.go b/op/clip/clip.go index 7a0a2f26..ad79e2db 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -21,8 +21,7 @@ type Op struct { path PathSpec outline bool - stroke StrokeStyle - dashes DashSpec + width float32 } // Stack represents an Op pushed on the clip stack. @@ -54,17 +53,8 @@ func (p Op) Add(o *op.Ops) { } func (p Op) add(o *op.Ops, push bool) { - str := p.stroke path := p.path outline := p.outline - approx := str.Width > 0 && !(p.dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap) - if approx { - // If the stroke is not natively supported by the compute renderer, construct a filled path - // that approximates it. - path = p.approximateStroke(o) - str = StrokeStyle{} - outline = true - } bo := binary.LittleEndian if path.hasSegments { @@ -75,9 +65,9 @@ func (p Op) add(o *op.Ops, push bool) { } bounds := path.bounds - if str.Width > 0 { + if p.width > 0 { // Expand bounds to cover stroke. - half := int(str.Width*.5 + .5) + half := int(p.width*.5 + .5) bounds.Min.X -= half bounds.Min.Y -= half bounds.Max.X += half @@ -85,7 +75,7 @@ func (p Op) add(o *op.Ops, push bool) { data := o.Internal.Write(ops.TypeStrokeLen) data[0] = byte(ops.TypeStroke) bo := binary.LittleEndian - bo.PutUint32(data[1:], math.Float32bits(str.Width)) + bo.PutUint32(data[1:], math.Float32bits(p.width)) } data := o.Internal.Write(ops.TypeClipLen) @@ -108,66 +98,6 @@ func (s Stack) Pop() { data[0] = byte(ops.TypePopClip) } -func (p Op) approximateStroke(o *op.Ops) PathSpec { - if !p.path.hasSegments { - return PathSpec{} - } - - var r ops.Reader - // Add path op for us to decode. Use a macro to omit it from later decodes. - ignore := op.Record(o) - r.ResetAt(&o.Internal, o.Internal.PC()) - p.path.spec.Add(o) - ignore.Stop() - encOp, ok := r.Decode() - if !ok || ops.OpType(encOp.Data[0]) != ops.TypeAux { - panic("corrupt path data") - } - pathData := encOp.Data[ops.TypeAuxLen:] - - // Decode dashes in a similar way. - var dashes stroke.DashOp - if p.dashes.phase != 0 || p.dashes.size > 0 { - ignore := op.Record(o) - r.ResetAt(&o.Internal, o.Internal.PC()) - p.dashes.spec.Add(o) - ignore.Stop() - encOp, ok := r.Decode() - if !ok || ops.OpType(encOp.Data[0]) != ops.TypeAux { - panic("corrupt dash data") - } - dashes.Dashes = make([]float32, p.dashes.size) - dashData := encOp.Data[ops.TypeAuxLen:] - bo := binary.LittleEndian - for i := range dashes.Dashes { - dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:])) - } - dashes.Phase = p.dashes.phase - } - - // Approximate and output path data. - var outline Path - outline.Begin(o) - ss := stroke.StrokeStyle{ - Width: p.stroke.Width, - Miter: p.stroke.Miter, - Cap: stroke.StrokeCap(p.stroke.Cap), - Join: stroke.StrokeJoin(p.stroke.Join), - } - quads := stroke.StrokePathCommands(ss, dashes, pathData) - pen := f32.Pt(0, 0) - for _, quad := range quads { - q := quad.Quad - if q.From != pen { - pen = q.From - outline.MoveTo(pen) - } - outline.contour = int(quad.Contour) - outline.QuadTo(q.Ctrl, q.To) - } - return outline.End() -} - type PathSpec struct { spec op.CallOp // open is true if any path contour is not closed. A closed contour starts @@ -382,6 +312,21 @@ func (p *Path) Close() { p.end() } +// Stroke represents a stroked path. +type Stroke struct { + Path PathSpec + // Width of the stroked path. + Width float32 +} + +// Op returns a clip operation representing the stroke. +func (s Stroke) Op() Op { + return Op{ + path: s.Path, + width: s.Width, + } +} + // Outline represents the area inside of a path, according to the // non-zero winding rule. type Outline struct { diff --git a/op/clip/stroke.go b/op/clip/stroke.go deleted file mode 100644 index 623d57a9..00000000 --- a/op/clip/stroke.go +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clip - -import ( - "encoding/binary" - "math" - - "gioui.org/internal/ops" - "gioui.org/op" -) - -// Stroke represents a stroked path. -type Stroke struct { - Path PathSpec - Style StrokeStyle - - // Dashes specify the dashes of the stroke. - // The empty value denotes no dashes. - Dashes DashSpec -} - -// Op returns a clip operation representing the stroke. -func (s Stroke) Op() Op { - return Op{ - path: s.Path, - stroke: s.Style, - dashes: s.Dashes, - } -} - -// StrokeStyle describes how a path should be stroked. -type StrokeStyle struct { - 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. -type StrokeCap uint8 - -const ( - // RoundCap caps stroked paths with a round cap, joining the right-hand and - // left-hand sides of a stroked path with a half disc of diameter the - // stroked path's width. - RoundCap StrokeCap = iota - - // FlatCap caps stroked paths with a flat cap, joining the right-hand - // and left-hand sides of a stroked path with a straight line. - FlatCap - - // SquareCap caps stroked paths with a square cap, joining the right-hand - // and left-hand sides of a stroked path with a half square of length - // the stroked path's width. - SquareCap -) - -// StrokeJoin describes how stroked paths are collated. -type StrokeJoin uint8 - -const ( - // RoundJoin joins path segments with a round segment. - RoundJoin StrokeJoin = iota - - // BevelJoin joins path segments with sharp bevels. - BevelJoin -) - -// Dash records dashes' lengths and phase for a stroked path. -type Dash struct { - ops *ops.Ops - macro op.MacroOp - phase float32 - size uint8 // size of the pattern -} - -func (d *Dash) Begin(o *op.Ops) { - d.ops = &o.Internal - d.macro = op.Record(o) - // Write the TypeAux opcode - data := d.ops.Write(ops.TypeAuxLen) - data[0] = byte(ops.TypeAux) -} - -func (d *Dash) Phase(v float32) { - d.phase = v -} - -func (d *Dash) Dash(length float32) { - if d.size == math.MaxUint8 { - panic("clip: dash pattern too large") - } - data := d.ops.Write(4) - bo := binary.LittleEndian - bo.PutUint32(data[0:], math.Float32bits(length)) - d.size++ -} - -func (d *Dash) End() DashSpec { - c := d.macro.Stop() - return DashSpec{ - spec: c, - phase: d.phase, - size: d.size, - } -} - -// DashSpec describes a dashed pattern. -type DashSpec struct { - spec op.CallOp - phase float32 - size uint8 // size of the pattern -} diff --git a/widget/border.go b/widget/border.go index 694c9281..8d1bcd10 100644 --- a/widget/border.go +++ b/widget/border.go @@ -35,7 +35,7 @@ func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { b.Color, clip.Stroke{ Path: clip.UniformRRect(r, rr).Path(gtx.Ops), - Style: clip.StrokeStyle{Width: width}, + Width: width, }.Op(), ) diff --git a/widget/material/loader.go b/widget/material/loader.go index 7a57bbe0..904a214d 100644 --- a/widget/material/loader.go +++ b/widget/material/loader.go @@ -74,10 +74,7 @@ func clipLoader(ops *op.Ops, startAngle, endAngle, radius float32) clip.Op { p.Move(pen) p.Arc(center, center, delta) return clip.Stroke{ - Path: p.End(), - Style: clip.StrokeStyle{ - Width: width, - Cap: clip.FlatCap, - }, + Path: p.End(), + Width: width, }.Op() }