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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-10-08 17:56:50 +02:00
parent 391725b9d0
commit bd1ef92dc4
19 changed files with 60 additions and 1159 deletions
+17 -18
View File
@@ -28,7 +28,6 @@ import (
"gioui.org/internal/scene" "gioui.org/internal/scene"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip"
"gioui.org/shader" "gioui.org/shader"
"gioui.org/shader/gio" "gioui.org/shader/gio"
"gioui.org/shader/piet" "gioui.org/shader/piet"
@@ -236,10 +235,10 @@ type encoderState struct {
// clipKey completely describes a clip operation (along with its path) and is appropriate // clipKey completely describes a clip operation (along with its path) and is appropriate
// for hashing and equality checks. // for hashing and equality checks.
type clipKey struct { type clipKey struct {
bounds f32.Rectangle bounds f32.Rectangle
stroke clip.StrokeStyle strokeWidth float32
relTrans f32.Affine2D relTrans f32.Affine2D
pathHash uint64 pathHash uint64
} }
// paintKey completely defines a paint operation. It is suitable for hashing and // 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] 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. // Rectangle clip regions.
if len(path) == 0 && !push { if len(path) == 0 && !push {
// If the rectangular clip region contains a previous path it can be discarded. // 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, intersect: intersect,
push: push, push: push,
clipKey: clipKey{ clipKey: clipKey{
bounds: bounds, bounds: bounds,
relTrans: state.relTrans, relTrans: state.relTrans,
stroke: stroke, strokeWidth: strokeWidth,
pathHash: hash, pathHash: hash,
}, },
}) })
state.clip = &c.clipStates[len(c.clipStates)-1] 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 key ops.Key
hash uint64 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() { for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) { switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile: case ops.TypeProfile:
@@ -1763,7 +1762,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
state.t = st.t state.t = st.t
state.relTrans = st.relTrans state.relTrans = st.relTrans
case ops.TypeStroke: case ops.TypeStroke:
str = decodeStrokeOp(encOp.Data) strWidth = decodeStrokeOp(encOp.Data)
case ops.TypePath: case ops.TypePath:
hash := bo.Uint64(encOp.Data[1:]) hash := bo.Uint64(encOp.Data[1:])
encOp, ok = r.Decode() encOp, ok = r.Decode()
@@ -1776,9 +1775,9 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
case ops.TypeClip: case ops.TypeClip:
var op clipOp var op clipOp
op.decode(encOp.Data) 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 pathData.data = nil
str = clip.StrokeStyle{} strWidth = 0
case ops.TypePopClip: case ops.TypePopClip:
for { for {
push := state.clip.push push := state.clip.push
@@ -1806,7 +1805,7 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
if paintState.matType == materialTexture { if paintState.matType == materialTexture {
// Clip to the bounds of the image, to hide other images in the atlas. // Clip to the bounds of the image, to hide other images in the atlas.
bounds := paintState.image.src.Bounds() 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 intersect := paintState.clip.intersect
if intersect.Empty() { if intersect.Empty() {
@@ -2101,9 +2100,9 @@ func encodeOp(viewport image.Point, absOff image.Point, enc *encoder, texOps []t
enc.transform(inv) enc.transform(inv)
for i := len(op.clipStack) - 1; i >= 0; i-- { for i := len(op.clipStack) - 1; i >= 0; i-- {
cl := op.clipStack[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.fillMode(scene.FillModeStroke)
enc.lineWidth(str.Width) enc.lineWidth(w)
fillMode = scene.FillModeStroke fillMode = scene.FillModeStroke
} else if fillMode != scene.FillModeNonzero { } else if fillMode != scene.FillModeNonzero {
enc.fillMode(scene.FillModeNonzero) enc.fillMode(scene.FillModeNonzero)
+12 -18
View File
@@ -27,7 +27,6 @@ import (
"gioui.org/internal/stroke" "gioui.org/internal/stroke"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip"
"gioui.org/shader" "gioui.org/shader"
"gioui.org/shader/gio" "gioui.org/shader/gio"
@@ -135,15 +134,13 @@ type imageOp struct {
place placement place placement
} }
func decodeStrokeOp(data []byte) clip.StrokeStyle { func decodeStrokeOp(data []byte) float32 {
_ = data[4] _ = data[4]
if ops.OpType(data[0]) != ops.TypeStroke { if ops.OpType(data[0]) != ops.TypeStroke {
panic("invalid op") panic("invalid op")
} }
bo := binary.LittleEndian bo := binary.LittleEndian
return clip.StrokeStyle{ return math.Float32frombits(bo.Uint32(data[1:]))
Width: math.Float32frombits(bo.Uint32(data[1:])),
}
} }
type quadsOp struct { 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) { func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
var ( var (
quads quadsOp quads quadsOp
str clip.StrokeStyle strWidth float32
state drawState state drawState
) )
reset := func() { reset := func() {
state = drawState{ state = drawState{
@@ -934,7 +931,7 @@ loop:
d.transStack = d.transStack[:n-1] d.transStack = d.transStack[:n-1]
case ops.TypeStroke: case ops.TypeStroke:
str = decodeStrokeOp(encOp.Data) strWidth = decodeStrokeOp(encOp.Data)
case ops.TypePath: case ops.TypePath:
encOp, ok = r.Decode() encOp, ok = r.Decode()
@@ -960,7 +957,7 @@ loop:
op.bounds = v.bounds op.bounds = v.bounds
} else { } else {
pathData, bounds := d.buildVerts( pathData, bounds := d.buildVerts(
quads.aux, trans, op.outline, str, quads.aux, trans, op.outline, strWidth,
) )
op.bounds = bounds op.bounds = bounds
quads.aux = pathData quads.aux = pathData
@@ -974,7 +971,7 @@ loop:
} }
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, op.push) d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, op.push)
quads = quadsOp{} quads = quadsOp{}
str = clip.StrokeStyle{} strWidth = 0
case ops.TypePopClip: case ops.TypePopClip:
for { for {
push := state.cpath.push 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. // 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)) inf := float32(math.Inf(+1))
d.qs.bounds = f32.Rectangle{ d.qs.bounds = f32.Rectangle{
Min: f32.Point{X: inf, Y: inf}, 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) startLength := len(d.vertCache)
switch { switch {
case str.Width > 0: case strWidth > 0:
// Stroke path. // Stroke path.
ss := stroke.StrokeStyle{ ss := stroke.StrokeStyle{
Width: str.Width, Width: strWidth,
Miter: str.Miter,
Cap: stroke.StrokeCap(str.Cap),
Join: stroke.StrokeJoin(str.Join),
} }
quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) quads := stroke.StrokePathCommands(ss, pathData)
for _, quad := range quads { for _, quad := range quads {
d.qs.contour = quad.Contour d.qs.contour = quad.Contour
quad.Quad = quad.Quad.Transform(tr) quad.Quad = quad.Quad.Transform(tr)
+4 -385
View File
@@ -125,10 +125,8 @@ func TestTexturedStrokeClipped(t *testing.T) {
smallSquares.Add(o) smallSquares.Add(o)
defer op.Offset(f32.Pt(50, 50)).Push(o).Pop() defer op.Offset(f32.Pt(50, 50)).Push(o).Pop()
defer clip.Stroke{ defer clip.Stroke{
Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o),
Style: clip.StrokeStyle{ Width: 10,
Width: 10,
},
}.Op().Push(o).Pop() }.Op().Push(o).Pop()
defer clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.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() defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop()
@@ -142,10 +140,8 @@ func TestTexturedStroke(t *testing.T) {
smallSquares.Add(o) smallSquares.Add(o)
defer op.Offset(f32.Pt(50, 50)).Push(o).Pop() defer op.Offset(f32.Pt(50, 50)).Push(o).Pop()
defer clip.Stroke{ defer clip.Stroke{
Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o),
Style: clip.StrokeStyle{ Width: 10,
Width: 10,
},
}.Op().Push(o).Pop() }.Op().Push(o).Pop()
defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop() defer op.Offset(f32.Pt(-10, -10)).Push(o).Pop()
paint.PaintOp{}.Add(o) 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) { func TestStrokedPathZeroWidth(t *testing.T) {
run(t, func(o *op.Ops) { 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 := new(clip.Path)
p.Begin(o) p.Begin(o)
@@ -354,213 +183,3 @@ func TestStrokedPathZeroWidth(t *testing.T) {
r.expect(65, 50, transparent) 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()
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

-394
View File
@@ -1,394 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
// The algorithms to compute dashes have been extracted, adapted from
// (and used as a reference implementation):
// - github.com/tdewolff/canvas (Licensed under MIT)
package stroke
import (
"math"
"sort"
"gioui.org/f32"
)
type DashOp struct {
Phase float32
Dashes []float32
}
func IsSolidLine(sty DashOp) bool {
return sty.Phase == 0 && len(sty.Dashes) == 0
}
func (qs StrokeQuads) dash(sty DashOp) StrokeQuads {
sty = dashCanonical(sty)
switch {
case len(sty.Dashes) == 0:
return qs
case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0:
return StrokeQuads{}
}
if len(sty.Dashes)%2 == 1 {
// If the dash pattern is of uneven length, dash and space lengths
// alternate. The following duplicates the pattern so that uneven
// indices are always spaces.
sty.Dashes = append(sty.Dashes, sty.Dashes...)
}
var (
i0, pos0 = dashStart(sty)
out StrokeQuads
contour uint32 = 1
)
for _, ps := range qs.split() {
var (
i = i0
pos = pos0
t []float64
length = ps.len()
)
for pos+sty.Dashes[i] < length {
pos += sty.Dashes[i]
if 0.0 < pos {
t = append(t, float64(pos))
}
i++
if i == len(sty.Dashes) {
i = 0
}
}
j0 := 0
endsInDash := i%2 == 0
if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash {
j0 = 1
}
var (
qd StrokeQuads
pd = ps.splitAt(&contour, t...)
)
for j := j0; j < len(pd)-1; j += 2 {
qd = qd.append(pd[j])
}
if endsInDash {
if ps.closed() {
qd = pd[len(pd)-1].append(qd)
} else {
qd = qd.append(pd[len(pd)-1])
}
}
out = out.append(qd)
contour++
}
return out
}
func dashCanonical(sty DashOp) DashOp {
var (
o = sty
ds = o.Dashes
)
if len(sty.Dashes) == 0 {
return sty
}
// Remove zeros except first and last.
for i := 1; i < len(ds)-1; i++ {
if f32Eq(ds[i], 0.0) {
ds[i-1] += ds[i+1]
ds = append(ds[:i], ds[i+2:]...)
i--
}
}
// Remove first zero, collapse with second and last.
if f32Eq(ds[0], 0.0) {
if len(ds) < 3 {
return DashOp{
Phase: 0.0,
Dashes: []float32{0.0},
}
}
o.Phase -= ds[1]
ds[len(ds)-1] += ds[1]
ds = ds[2:]
}
// Remove last zero, collapse with fist and second to last.
if f32Eq(ds[len(ds)-1], 0.0) {
if len(ds) < 3 {
return DashOp{}
}
o.Phase += ds[len(ds)-2]
ds[0] += ds[len(ds)-2]
ds = ds[:len(ds)-2]
}
// If there are zeros or negatives, don't draw dashes.
for i := 0; i < len(ds); i++ {
if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
return DashOp{
Phase: 0.0,
Dashes: []float32{0.0},
}
}
}
// Remove repeated patterns.
loop:
for len(ds)%2 == 0 {
mid := len(ds) / 2
for i := 0; i < mid; i++ {
if !f32Eq(ds[i], ds[mid+i]) {
break loop
}
}
ds = ds[:mid]
}
return o
}
func dashStart(sty DashOp) (int, float32) {
i0 := 0 // i0 is the index into dashes.
for sty.Dashes[i0] <= sty.Phase {
sty.Phase -= sty.Dashes[i0]
i0++
if i0 == len(sty.Dashes) {
i0 = 0
}
}
// pos0 may be negative if the offset lands halfway into dash.
pos0 := -sty.Phase
if sty.Phase < 0.0 {
var sum float32
for _, d := range sty.Dashes {
sum += d
}
pos0 = -(sum + sty.Phase) // handle negative offsets
}
return i0, pos0
}
func (qs StrokeQuads) len() float32 {
var sum float32
for i := range qs {
q := qs[i].Quad
sum += quadBezierLen(q.From, q.Ctrl, q.To)
}
return sum
}
// splitAt splits the path into separate paths at the specified intervals
// along the path.
// splitAt updates the provided contour counter as it splits the segments.
func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads {
if len(ts) == 0 {
qs.setContour(*contour)
return []StrokeQuads{qs}
}
sort.Float64s(ts)
if ts[0] == 0 {
ts = ts[1:]
}
var (
j int // index into ts
t float64 // current position along curve
)
var oo []StrokeQuads
var oi StrokeQuads
push := func() {
oo = append(oo, oi)
oi = nil
}
for _, ps := range qs.split() {
for _, q := range ps {
if j == len(ts) {
oi = append(oi, q)
continue
}
speed := func(t float64) float64 {
return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, q.Quad.To, float32(t))))
}
invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1)
var (
t0 float64
r0 = q.Quad.From
r1 = q.Quad.Ctrl
r2 = q.Quad.To
// from keeps track of the start of the 'running' segment.
from = r0
)
for j < len(ts) && t < ts[j] && ts[j] <= t+dt {
tj := invL(ts[j] - t)
tsub := (tj - t0) / (1.0 - t0)
t0 = tj
var q1 f32.Point
_, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub))
oi = append(oi, StrokeQuad{
Contour: *contour,
Quad: QuadSegment{
From: from,
Ctrl: q1,
To: r0,
},
})
push()
(*contour)++
from = r0
j++
}
if !f64Eq(t0, 1) {
if len(oi) > 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)
}
+5 -146
View File
@@ -40,26 +40,8 @@ import (
// op/clip, eliminating the duplicate types. // op/clip, eliminating the duplicate types.
type StrokeStyle struct { type StrokeStyle struct {
Width float32 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 // strokeTolerance is used to reconcile rounding errors arising
// when splitting quads into smaller and smaller segments to approximate // when splitting quads into smaller and smaller segments to approximate
// them into straight lines, and when joining back segments. // 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 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) { func (qs *StrokeQuads) lineTo(pt f32.Point) {
end := qs.pen() end := qs.pen()
*qs = append(*qs, StrokeQuad{ *qs = append(*qs, StrokeQuad{
@@ -155,11 +131,7 @@ func (qs StrokeQuads) split() []StrokeQuads {
return o return o
} }
func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads {
if !IsSolidLine(dashes) {
qs = qs.dash(dashes)
}
var ( var (
o StrokeQuads o StrokeQuads
hw = 0.5 * stroke.Width hw = 0.5 * stroke.Width
@@ -441,29 +413,6 @@ func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
return p.Mul(2) 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 { func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads {
// Gio strokes are only quadratic Bézier curves, w/o any inflection point. // Gio strokes are only quadratic Bézier curves, w/o any inflection point.
// So we just have to flatten them. // 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 // strokePathJoin joins the two paths rhs and lhs, according to the provided
// stroke operation. // stroke operation.
func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
if stroke.Miter > 0 { strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
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)
} }
func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { 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. // strokePathCap caps the provided path qs, according to the provided stroke operation.
func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
switch stroke.Cap { strokePathRoundCap(qs, hw, pivot, n0)
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 caps the start or end of a path with a round cap. // 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) return math.Hypot(dx, dy)
} }
func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads { func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads {
quads := decodeToStrokeQuads(scene) quads := decodeToStrokeQuads(scene)
return quads.stroke(style, dashes) return quads.stroke(style)
} }
// decodeToStrokeQuads decodes scene commands to quads ready to stroke. // decodeToStrokeQuads decodes scene commands to quads ready to stroke.
+19 -74
View File
@@ -21,8 +21,7 @@ type Op struct {
path PathSpec path PathSpec
outline bool outline bool
stroke StrokeStyle width float32
dashes DashSpec
} }
// Stack represents an Op pushed on the clip stack. // 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) { func (p Op) add(o *op.Ops, push bool) {
str := p.stroke
path := p.path path := p.path
outline := p.outline 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 bo := binary.LittleEndian
if path.hasSegments { if path.hasSegments {
@@ -75,9 +65,9 @@ func (p Op) add(o *op.Ops, push bool) {
} }
bounds := path.bounds bounds := path.bounds
if str.Width > 0 { if p.width > 0 {
// Expand bounds to cover stroke. // Expand bounds to cover stroke.
half := int(str.Width*.5 + .5) half := int(p.width*.5 + .5)
bounds.Min.X -= half bounds.Min.X -= half
bounds.Min.Y -= half bounds.Min.Y -= half
bounds.Max.X += 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 := o.Internal.Write(ops.TypeStrokeLen)
data[0] = byte(ops.TypeStroke) data[0] = byte(ops.TypeStroke)
bo := binary.LittleEndian 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) data := o.Internal.Write(ops.TypeClipLen)
@@ -108,66 +98,6 @@ func (s Stack) Pop() {
data[0] = byte(ops.TypePopClip) 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 { type PathSpec struct {
spec op.CallOp spec op.CallOp
// open is true if any path contour is not closed. A closed contour starts // open is true if any path contour is not closed. A closed contour starts
@@ -382,6 +312,21 @@ func (p *Path) Close() {
p.end() 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 // Outline represents the area inside of a path, according to the
// non-zero winding rule. // non-zero winding rule.
type Outline struct { type Outline struct {
-118
View File
@@ -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
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.Color, b.Color,
clip.Stroke{ clip.Stroke{
Path: clip.UniformRRect(r, rr).Path(gtx.Ops), Path: clip.UniformRRect(r, rr).Path(gtx.Ops),
Style: clip.StrokeStyle{Width: width}, Width: width,
}.Op(), }.Op(),
) )
+2 -5
View File
@@ -74,10 +74,7 @@ func clipLoader(ops *op.Ops, startAngle, endAngle, radius float32) clip.Op {
p.Move(pen) p.Move(pen)
p.Arc(center, center, delta) p.Arc(center, center, delta)
return clip.Stroke{ return clip.Stroke{
Path: p.End(), Path: p.End(),
Style: clip.StrokeStyle{ Width: width,
Width: width,
Cap: clip.FlatCap,
},
}.Op() }.Op()
} }