internal/stroke,gpu: create internal package for stroke to path conversion

Complex strokes are not yet supported in either of the current renderers,
so they are converted to filled outlines in package gpu.

We're about to move that complexity up to the op/clip package, so we're
going to need the converter available from outside package gpu. This
change extracts the conversion code and related types to the separate,
internal package stroke.

No functional changes; a follow-up moves the stroke conversion.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-03-23 12:35:41 +01:00
parent 8750828c69
commit 8c8d1dc16f
5 changed files with 185 additions and 179 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ package gpu
import ( import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/stroke"
) )
type quadSplitter struct { type quadSplitter struct {
@@ -46,7 +47,7 @@ func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) {
encodeQuadTo(data, qs.contour, from, ctrl, to) encodeQuadTo(data, qs.contour, from, ctrl, to)
} }
func (qs *quadSplitter) splitAndEncode(quad quadSegment) { func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) {
cbnd := f32.Rectangle{ cbnd := f32.Rectangle{
Min: quad.From, Min: quad.From,
Max: quad.To, Max: quad.To,
+4 -3
View File
@@ -18,6 +18,7 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/internal/scene" "gioui.org/internal/scene"
"gioui.org/internal/stroke"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -695,7 +696,7 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b
} }
func supportsStroke(p *pathOp) bool { func supportsStroke(p *pathOp) bool {
return isSolidLine(p.dashes) && p.stroke.Miter == 0 && p.stroke.Join == clip.RoundJoin && p.stroke.Cap == clip.RoundCap return stroke.IsSolidLine(p.dashes) && p.stroke.Miter == 0 && p.stroke.Join == clip.RoundJoin && p.stroke.Cap == clip.RoundCap
} }
func isStroke(p *pathOp) bool { func isStroke(p *pathOp) bool {
@@ -707,9 +708,9 @@ func encodePath(p *pathOp) encoder {
verts := p.pathVerts verts := p.pathVerts
if p.stroke.Width > 0 && !supportsStroke(p) { if p.stroke.Width > 0 && !supportsStroke(p) {
quads := decodeToStrokeQuads(verts) quads := decodeToStrokeQuads(verts)
quads = quads.stroke(p.stroke, p.dashes) quads = quads.Stroke(p.stroke, p.dashes)
for _, quad := range quads { for _, quad := range quads {
q := quad.quad q := quad.Quad
enc.quad(q.From, q.Ctrl, q.To) enc.quad(q.From, q.Ctrl, q.To)
} }
return enc return enc
+43 -64
View File
@@ -26,6 +26,7 @@ import (
"gioui.org/internal/opconst" "gioui.org/internal/opconst"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/internal/scene" "gioui.org/internal/scene"
"gioui.org/internal/stroke"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -130,7 +131,7 @@ type pathOp struct {
// For compute // For compute
trans f32.Affine2D trans f32.Affine2D
stroke clip.StrokeStyle stroke clip.StrokeStyle
dashes dashOp dashes stroke.DashOp
} }
type imageOp struct { type imageOp struct {
@@ -142,24 +143,15 @@ type imageOp struct {
place placement place placement
} }
type dashOp struct { func decodeDashOp(data []byte) stroke.DashOp {
phase float32
dashes []float32
}
type quadSegment struct {
From, Ctrl, To f32.Point
}
func decodeDashOp(data []byte) dashOp {
_ = data[5] _ = data[5]
if opconst.OpType(data[0]) != opconst.TypeDash { if opconst.OpType(data[0]) != opconst.TypeDash {
panic("invalid op") panic("invalid op")
} }
bo := binary.LittleEndian bo := binary.LittleEndian
return dashOp{ return stroke.DashOp{
phase: math.Float32frombits(bo.Uint32(data[1:])), Phase: math.Float32frombits(bo.Uint32(data[1:])),
dashes: make([]float32, data[5]), Dashes: make([]float32, data[5]),
} }
} }
@@ -852,7 +844,7 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1] return &d.pathOpCache[len(d.pathOpCache)-1]
} }
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle, dashes dashOp) { func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle, dashes stroke.DashOp) {
npath := d.newPathOp() npath := d.newPathOp()
*npath = pathOp{ *npath = pathOp{
parent: state.cpath, parent: state.cpath,
@@ -891,8 +883,8 @@ func (d *drawOps) save(id int, state drawState) {
func (d *drawOps) collectOps(r *ops.Reader, state drawState) { func (d *drawOps) collectOps(r *ops.Reader, state drawState) {
var ( var (
quads quadsOp quads quadsOp
stroke clip.StrokeStyle str clip.StrokeStyle
dashes dashOp dashes stroke.DashOp
z int z int
) )
d.save(opconst.InitialStateID, state) d.save(opconst.InitialStateID, state)
@@ -907,22 +899,22 @@ loop:
case opconst.TypeDash: case opconst.TypeDash:
dashes = decodeDashOp(encOp.Data) dashes = decodeDashOp(encOp.Data)
if len(dashes.dashes) > 0 { if len(dashes.Dashes) > 0 {
encOp, ok = r.Decode() encOp, ok = r.Decode()
if !ok { if !ok {
panic("gpu: could not decode dashes pattern") panic("gpu: could not decode dashes pattern")
} }
data := encOp.Data[1:] data := encOp.Data[1:]
bo := binary.LittleEndian bo := binary.LittleEndian
for i := range dashes.dashes { for i := range dashes.Dashes {
dashes.dashes[i] = math.Float32frombits(bo.Uint32( dashes.Dashes[i] = math.Float32frombits(bo.Uint32(
data[i*4:], data[i*4:],
)) ))
} }
} }
case opconst.TypeStroke: case opconst.TypeStroke:
stroke = decodeStrokeOp(encOp.Data) str = decodeStrokeOp(encOp.Data)
case opconst.TypePath: case opconst.TypePath:
encOp, ok = r.Decode() encOp, ok = r.Decode()
@@ -948,7 +940,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, stroke, dashes, quads.aux, trans, op.outline, str, dashes,
) )
op.bounds = bounds op.bounds = bounds
if !d.compute { if !d.compute {
@@ -964,10 +956,10 @@ loop:
quads.key.SetTransform(trans) quads.key.SetTransform(trans)
} }
state.clip = state.clip.Intersect(op.bounds.Add(off)) state.clip = state.clip.Intersect(op.bounds.Add(off))
d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, stroke, dashes) d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, str, dashes)
quads = quadsOp{} quads = quadsOp{}
stroke = clip.StrokeStyle{} str = clip.StrokeStyle{}
dashes = dashOp{} dashes = stroke.DashOp{}
case opconst.TypeColor: case opconst.TypeColor:
state.matType = materialColor state.matType = materialColor
@@ -1005,7 +997,7 @@ loop:
// The paint operation is sheared or rotated, add a clip path representing // The paint operation is sheared or rotated, add a clip path representing
// this transformed rectangle. // this transformed rectangle.
encOp.Key.SetTransform(trans) encOp.Key.SetTransform(trans)
d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}, dashOp{}) d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}, stroke.DashOp{})
} }
bounds := boundRectF(cl) bounds := boundRectF(cl)
@@ -1357,7 +1349,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, stroke clip.StrokeStyle, dashes dashOp) (verts []byte, bounds f32.Rectangle) { func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, stroke clip.StrokeStyle, dashes stroke.DashOp) (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},
@@ -1370,12 +1362,12 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str
case stroke.Width > 0: case stroke.Width > 0:
// Stroke path. // Stroke path.
quads := decodeToStrokeQuads(pathData) quads := decodeToStrokeQuads(pathData)
quads = quads.stroke(stroke, dashes) quads = quads.Stroke(stroke, dashes)
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)
d.qs.splitAndEncode(quad.quad) d.qs.splitAndEncode(quad.Quad)
} }
case outline: case outline:
@@ -1394,13 +1386,13 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
cmd := ops.DecodeCommand(pathData[4:]) cmd := ops.DecodeCommand(pathData[4:])
switch cmd.Op() { switch cmd.Op() {
case scene.OpLine: case scene.OpLine:
var q quadSegment var q stroke.QuadSegment
q.From, q.To = scene.DecodeLine(cmd) q.From, q.To = scene.DecodeLine(cmd)
q.Ctrl = q.From.Add(q.To).Mul(.5) q.Ctrl = q.From.Add(q.To).Mul(.5)
q = q.Transform(tr) q = q.Transform(tr)
qs.splitAndEncode(q) qs.splitAndEncode(q)
case scene.OpQuad: case scene.OpQuad:
var q quadSegment var q stroke.QuadSegment
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
q = q.Transform(tr) q = q.Transform(tr)
qs.splitAndEncode(q) qs.splitAndEncode(q)
@@ -1418,34 +1410,34 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
// decodeToStrokeQuads is like decodeOutlineQuads, except it returns a list of stroke // decodeToStrokeQuads is like decodeOutlineQuads, except it returns a list of stroke
// quads ready to stroke. // quads ready to stroke.
func decodeToStrokeQuads(pathData []byte) strokeQuads { func decodeToStrokeQuads(pathData []byte) stroke.StrokeQuads {
quads := make(strokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) quads := make(stroke.StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
for len(pathData) >= scene.CommandSize+4 { for len(pathData) >= scene.CommandSize+4 {
contour := bo.Uint32(pathData) contour := bo.Uint32(pathData)
cmd := ops.DecodeCommand(pathData[4:]) cmd := ops.DecodeCommand(pathData[4:])
switch cmd.Op() { switch cmd.Op() {
case scene.OpLine: case scene.OpLine:
var q quadSegment var q stroke.QuadSegment
q.From, q.To = scene.DecodeLine(cmd) q.From, q.To = scene.DecodeLine(cmd)
q.Ctrl = q.From.Add(q.To).Mul(.5) q.Ctrl = q.From.Add(q.To).Mul(.5)
quad := strokeQuad{ quad := stroke.StrokeQuad{
contour: contour, Contour: contour,
quad: q, Quad: q,
} }
quads = append(quads, quad) quads = append(quads, quad)
case scene.OpQuad: case scene.OpQuad:
var q quadSegment var q stroke.QuadSegment
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
quad := strokeQuad{ quad := stroke.StrokeQuad{
contour: contour, Contour: contour,
quad: q, Quad: q,
} }
quads = append(quads, quad) quads = append(quads, quad)
case scene.OpCubic: case scene.OpCubic:
for _, q := range splitCubic(scene.DecodeCubic(cmd)) { for _, q := range splitCubic(scene.DecodeCubic(cmd)) {
quad := strokeQuad{ quad := stroke.StrokeQuad{
contour: contour, Contour: contour,
quad: q, Quad: q,
} }
quads = append(quads, quad) quads = append(quads, quad)
} }
@@ -1537,21 +1529,8 @@ func isPureOffset(t f32.Affine2D) bool {
return a == 1 && b == 0 && d == 0 && e == 1 return a == 1 && b == 0 && d == 0 && e == 1
} }
func (q quadSegment) Transform(t f32.Affine2D) quadSegment { func splitCubic(from, ctrl0, ctrl1, to f32.Point) []stroke.QuadSegment {
q.From = t.Transform(q.From) quads := make([]stroke.QuadSegment, 0, 10)
q.Ctrl = t.Transform(q.Ctrl)
q.To = t.Transform(q.To)
return q
}
func decodeQuad(d []byte) (q quadSegment) {
cmd := ops.DecodeCommand(d)
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
return
}
func splitCubic(from, ctrl0, ctrl1, to f32.Point) []quadSegment {
quads := make([]quadSegment, 0, 10)
// Set the maximum distance proportionally to the longest side // Set the maximum distance proportionally to the longest side
// of the bounding rectangle. // of the bounding rectangle.
hull := f32.Rectangle{ hull := f32.Rectangle{
@@ -1568,7 +1547,7 @@ func splitCubic(from, ctrl0, ctrl1, to f32.Point) []quadSegment {
// approxCube approximates a cubic Bézier by a series of quadratic // approxCube approximates a cubic Bézier by a series of quadratic
// curves. // curves.
func approxCubeTo(quads *[]quadSegment, splits int, maxDist float32, from, ctrl0, ctrl1, to f32.Point) int { func approxCubeTo(quads *[]stroke.QuadSegment, splits int, maxDist float32, from, ctrl0, ctrl1, to f32.Point) int {
// The idea is from // The idea is from
// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
// where a quadratic approximates a cubic by eliminating its t³ term // where a quadratic approximates a cubic by eliminating its t³ term
@@ -1596,7 +1575,7 @@ func approxCubeTo(quads *[]quadSegment, splits int, maxDist float32, from, ctrl0
c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
const maxSplits = 32 const maxSplits = 32
if splits >= maxSplits { if splits >= maxSplits {
*quads = append(*quads, quadSegment{From: from, Ctrl: c, To: to}) *quads = append(*quads, stroke.QuadSegment{From: from, Ctrl: c, To: to})
return splits return splits
} }
// The maximum distance between the cubic P and its approximation Q given t // The maximum distance between the cubic P and its approximation Q given t
@@ -1608,7 +1587,7 @@ func approxCubeTo(quads *[]quadSegment, splits int, maxDist float32, from, ctrl0
v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from) v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from)
d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
if d2 <= maxDist*maxDist { if d2 <= maxDist*maxDist {
*quads = append(*quads, quadSegment{From: from, Ctrl: c, To: to}) *quads = append(*quads, stroke.QuadSegment{From: from, Ctrl: c, To: to})
return splits return splits
} }
// De Casteljau split the curve and approximate the halves. // De Casteljau split the curve and approximate the halves.
+55 -50
View File
@@ -4,7 +4,7 @@
// (and used as a reference implementation): // (and used as a reference implementation):
// - github.com/tdewolff/canvas (Licensed under MIT) // - github.com/tdewolff/canvas (Licensed under MIT)
package gpu package stroke
import ( import (
"math" "math"
@@ -13,30 +13,35 @@ import (
"gioui.org/f32" "gioui.org/f32"
) )
func isSolidLine(sty dashOp) bool { type DashOp struct {
return sty.phase == 0 && len(sty.dashes) == 0 Phase float32
Dashes []float32
} }
func (qs strokeQuads) dash(sty dashOp) strokeQuads { func IsSolidLine(sty DashOp) bool {
return sty.Phase == 0 && len(sty.Dashes) == 0
}
func (qs StrokeQuads) dash(sty DashOp) StrokeQuads {
sty = dashCanonical(sty) sty = dashCanonical(sty)
switch { switch {
case len(sty.dashes) == 0: case len(sty.Dashes) == 0:
return qs return qs
case len(sty.dashes) == 1 && sty.dashes[0] == 0.0: case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0:
return strokeQuads{} return StrokeQuads{}
} }
if len(sty.dashes)%2 == 1 { if len(sty.Dashes)%2 == 1 {
// If the dash pattern is of uneven length, dash and space lengths // If the dash pattern is of uneven length, dash and space lengths
// alternate. The following duplicates the pattern so that uneven // alternate. The following duplicates the pattern so that uneven
// indices are always spaces. // indices are always spaces.
sty.dashes = append(sty.dashes, sty.dashes...) sty.Dashes = append(sty.Dashes, sty.Dashes...)
} }
var ( var (
i0, pos0 = dashStart(sty) i0, pos0 = dashStart(sty)
out strokeQuads out StrokeQuads
contour uint32 = 1 contour uint32 = 1
) )
@@ -48,13 +53,13 @@ func (qs strokeQuads) dash(sty dashOp) strokeQuads {
t []float64 t []float64
length = ps.len() length = ps.len()
) )
for pos+sty.dashes[i] < length { for pos+sty.Dashes[i] < length {
pos += sty.dashes[i] pos += sty.Dashes[i]
if 0.0 < pos { if 0.0 < pos {
t = append(t, float64(pos)) t = append(t, float64(pos))
} }
i++ i++
if i == len(sty.dashes) { if i == len(sty.Dashes) {
i = 0 i = 0
} }
} }
@@ -66,7 +71,7 @@ func (qs strokeQuads) dash(sty dashOp) strokeQuads {
} }
var ( var (
qd strokeQuads qd StrokeQuads
pd = ps.splitAt(&contour, t...) pd = ps.splitAt(&contour, t...)
) )
for j := j0; j < len(pd)-1; j += 2 { for j := j0; j < len(pd)-1; j += 2 {
@@ -85,13 +90,13 @@ func (qs strokeQuads) dash(sty dashOp) strokeQuads {
return out return out
} }
func dashCanonical(sty dashOp) dashOp { func dashCanonical(sty DashOp) DashOp {
var ( var (
o = sty o = sty
ds = o.dashes ds = o.Dashes
) )
if len(sty.dashes) == 0 { if len(sty.Dashes) == 0 {
return sty return sty
} }
@@ -107,12 +112,12 @@ func dashCanonical(sty dashOp) dashOp {
// Remove first zero, collapse with second and last. // Remove first zero, collapse with second and last.
if f32Eq(ds[0], 0.0) { if f32Eq(ds[0], 0.0) {
if len(ds) < 3 { if len(ds) < 3 {
return dashOp{ return DashOp{
phase: 0.0, Phase: 0.0,
dashes: []float32{0.0}, Dashes: []float32{0.0},
} }
} }
o.phase -= ds[1] o.Phase -= ds[1]
ds[len(ds)-1] += ds[1] ds[len(ds)-1] += ds[1]
ds = ds[2:] ds = ds[2:]
} }
@@ -120,9 +125,9 @@ func dashCanonical(sty dashOp) dashOp {
// Remove last zero, collapse with fist and second to last. // Remove last zero, collapse with fist and second to last.
if f32Eq(ds[len(ds)-1], 0.0) { if f32Eq(ds[len(ds)-1], 0.0) {
if len(ds) < 3 { if len(ds) < 3 {
return dashOp{} return DashOp{}
} }
o.phase += ds[len(ds)-2] o.Phase += ds[len(ds)-2]
ds[0] += ds[len(ds)-2] ds[0] += ds[len(ds)-2]
ds = ds[:len(ds)-2] ds = ds[:len(ds)-2]
} }
@@ -130,9 +135,9 @@ func dashCanonical(sty dashOp) dashOp {
// If there are zeros or negatives, don't draw dashes. // If there are zeros or negatives, don't draw dashes.
for i := 0; i < len(ds); i++ { for i := 0; i < len(ds); i++ {
if ds[i] < 0.0 || f32Eq(ds[i], 0.0) { if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
return dashOp{ return DashOp{
phase: 0.0, Phase: 0.0,
dashes: []float32{0.0}, Dashes: []float32{0.0},
} }
} }
} }
@@ -151,31 +156,31 @@ loop:
return o return o
} }
func dashStart(sty dashOp) (int, float32) { func dashStart(sty DashOp) (int, float32) {
i0 := 0 // i0 is the index into dashes. i0 := 0 // i0 is the index into dashes.
for sty.dashes[i0] <= sty.phase { for sty.Dashes[i0] <= sty.Phase {
sty.phase -= sty.dashes[i0] sty.Phase -= sty.Dashes[i0]
i0++ i0++
if i0 == len(sty.dashes) { if i0 == len(sty.Dashes) {
i0 = 0 i0 = 0
} }
} }
// pos0 may be negative if the offset lands halfway into dash. // pos0 may be negative if the offset lands halfway into dash.
pos0 := -sty.phase pos0 := -sty.Phase
if sty.phase < 0.0 { if sty.Phase < 0.0 {
var sum float32 var sum float32
for _, d := range sty.dashes { for _, d := range sty.Dashes {
sum += d sum += d
} }
pos0 = -(sum + sty.phase) // handle negative offsets pos0 = -(sum + sty.Phase) // handle negative offsets
} }
return i0, pos0 return i0, pos0
} }
func (qs strokeQuads) len() float32 { func (qs StrokeQuads) len() float32 {
var sum float32 var sum float32
for i := range qs { for i := range qs {
q := qs[i].quad q := qs[i].Quad
sum += quadBezierLen(q.From, q.Ctrl, q.To) sum += quadBezierLen(q.From, q.Ctrl, q.To)
} }
return sum return sum
@@ -184,10 +189,10 @@ func (qs strokeQuads) len() float32 {
// splitAt splits the path into separate paths at the specified intervals // splitAt splits the path into separate paths at the specified intervals
// along the path. // along the path.
// splitAt updates the provided contour counter as it splits the segments. // splitAt updates the provided contour counter as it splits the segments.
func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads { func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads {
if len(ts) == 0 { if len(ts) == 0 {
qs.setContour(*contour) qs.setContour(*contour)
return []strokeQuads{qs} return []StrokeQuads{qs}
} }
sort.Float64s(ts) sort.Float64s(ts)
@@ -200,8 +205,8 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads {
t float64 // current position along curve t float64 // current position along curve
) )
var oo []strokeQuads var oo []StrokeQuads
var oi strokeQuads var oi StrokeQuads
push := func() { push := func() {
oo = append(oo, oi) oo = append(oo, oi)
oi = nil oi = nil
@@ -214,15 +219,15 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads {
continue continue
} }
speed := func(t float64) float64 { speed := func(t float64) float64 {
return float64(lenPt(quadBezierD1(q.quad.From, q.quad.Ctrl, q.quad.To, float32(t)))) return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, q.Quad.To, float32(t))))
} }
invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1) invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1)
var ( var (
t0 float64 t0 float64
r0 = q.quad.From r0 = q.Quad.From
r1 = q.quad.Ctrl r1 = q.Quad.Ctrl
r2 = q.quad.To r2 = q.Quad.To
// from keeps track of the start of the 'running' segment. // from keeps track of the start of the 'running' segment.
from = r0 from = r0
@@ -235,9 +240,9 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads {
var q1 f32.Point var q1 f32.Point
_, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub)) _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub))
oi = append(oi, strokeQuad{ oi = append(oi, StrokeQuad{
contour: *contour, Contour: *contour,
quad: quadSegment{ Quad: QuadSegment{
From: from, From: from,
Ctrl: q1, Ctrl: q1,
To: r0, To: r0,
@@ -253,9 +258,9 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads {
if len(oi) > 0 { if len(oi) > 0 {
r0 = oi.pen() r0 = oi.pen()
} }
oi = append(oi, strokeQuad{ oi = append(oi, StrokeQuad{
contour: *contour, Contour: *contour,
quad: quadSegment{ Quad: QuadSegment{
From: r0, From: r0,
Ctrl: r1, Ctrl: r1,
To: r2, To: r2,
+81 -61
View File
@@ -21,12 +21,15 @@
// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html // - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
// R. Levien // R. Levien
package gpu // Package stroke implements conversion of strokes to filled outlines. It is used as a
// fallback for stroke configurations not natively supported by the renderer.
package stroke
import ( import (
"math" "math"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/ops"
"gioui.org/internal/scene" "gioui.org/internal/scene"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -41,9 +44,13 @@ import (
// and speed. // and speed.
const strokeTolerance = 0.01 const strokeTolerance = 0.01
type strokeQuad struct { type QuadSegment struct {
contour uint32 From, Ctrl, To f32.Point
quad quadSegment }
type StrokeQuad struct {
Contour uint32
Quad QuadSegment
} }
type strokeState struct { type strokeState struct {
@@ -53,28 +60,28 @@ type strokeState struct {
ctl f32.Point // ctl is the control point of the quadratic Bézier segment. ctl f32.Point // ctl is the control point of the quadratic Bézier segment.
} }
type strokeQuads []strokeQuad type StrokeQuads []StrokeQuad
func (qs *strokeQuads) setContour(n uint32) { func (qs *StrokeQuads) setContour(n uint32) {
for i := range *qs { for i := range *qs {
(*qs)[i].contour = n (*qs)[i].Contour = n
} }
} }
func (qs *strokeQuads) pen() f32.Point { 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 { func (qs *StrokeQuads) closed() bool {
beg := (*qs)[0].quad.From beg := (*qs)[0].Quad.From
end := (*qs)[len(*qs)-1].quad.To end := (*qs)[len(*qs)-1].Quad.To
return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y) 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{
quad: quadSegment{ Quad: QuadSegment{
From: end, From: end,
Ctrl: end.Add(pt).Mul(0.5), Ctrl: end.Add(pt).Mul(0.5),
To: pt, To: pt,
@@ -82,7 +89,7 @@ func (qs *strokeQuads) lineTo(pt f32.Point) {
}) })
} }
func (qs *strokeQuads) arc(f1, f2 f32.Point, angle float32) { func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) {
var ( var (
p clip.Path p clip.Path
o = new(op.Ops) o = new(op.Ops)
@@ -97,29 +104,29 @@ func (qs *strokeQuads) arc(f1, f2 f32.Point, angle float32) {
for qi := 0; len(raw) >= (scene.CommandSize + 4); qi++ { for qi := 0; len(raw) >= (scene.CommandSize + 4); qi++ {
quad := decodeQuad(raw[4:]) quad := decodeQuad(raw[4:])
raw = raw[scene.CommandSize+4:] raw = raw[scene.CommandSize+4:]
*qs = append(*qs, strokeQuad{ *qs = append(*qs, StrokeQuad{
quad: quad, Quad: quad,
}) })
} }
} }
// split splits a slice of quads into slices of quads grouped // split splits a slice of quads into slices of quads grouped
// by contours (ie: splitted at move-to boundaries). // by contours (ie: splitted at move-to boundaries).
func (qs strokeQuads) split() []strokeQuads { func (qs StrokeQuads) split() []StrokeQuads {
if len(qs) == 0 { if len(qs) == 0 {
return nil return nil
} }
var ( var (
c uint32 c uint32
o []strokeQuads o []StrokeQuads
i = len(o) i = len(o)
) )
for _, q := range qs { for _, q := range qs {
if q.contour != c { if q.Contour != c {
c = q.contour c = q.Contour
i = len(o) i = len(o)
o = append(o, strokeQuads{}) o = append(o, StrokeQuads{})
} }
o[i] = append(o[i], q) o[i] = append(o[i], q)
} }
@@ -127,13 +134,13 @@ func (qs strokeQuads) split() []strokeQuads {
return o return o
} }
func (qs strokeQuads) stroke(stroke clip.StrokeStyle, dashes dashOp) strokeQuads { func (qs StrokeQuads) Stroke(stroke clip.StrokeStyle, dashes DashOp) StrokeQuads {
if !isSolidLine(dashes) { if !IsSolidLine(dashes) {
qs = qs.dash(dashes) qs = qs.dash(dashes)
} }
var ( var (
o strokeQuads o StrokeQuads
hw = 0.5 * stroke.Width hw = 0.5 * stroke.Width
) )
@@ -164,15 +171,15 @@ func (qs strokeQuads) stroke(stroke clip.StrokeStyle, dashes dashOp) strokeQuads
// offset returns the right-hand and left-hand sides of the path, offset by // offset returns the right-hand and left-hand sides of the path, offset by
// the half-width hw. // the half-width hw.
// The stroke handles how segments are joined and ends are capped. // The stroke handles how segments are joined and ends are capped.
func (qs strokeQuads) offset(hw float32, stroke clip.StrokeStyle) (rhs, lhs strokeQuads) { func (qs StrokeQuads) offset(hw float32, stroke clip.StrokeStyle) (rhs, lhs StrokeQuads) {
var ( var (
states []strokeState states []strokeState
beg = qs[0].quad.From beg = qs[0].Quad.From
end = qs[len(qs)-1].quad.To end = qs[len(qs)-1].Quad.To
closed = beg == end closed = beg == end
) )
for i := range qs { for i := range qs {
q := qs[i].quad q := qs[i].Quad
var ( var (
n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw) n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
@@ -231,16 +238,16 @@ func (qs strokeQuads) offset(hw float32, stroke clip.StrokeStyle) (rhs, lhs stro
return rhs, nil return rhs, nil
} }
func (qs *strokeQuads) close() { func (qs *StrokeQuads) close() {
p0 := (*qs)[len(*qs)-1].quad.To p0 := (*qs)[len(*qs)-1].Quad.To
p1 := (*qs)[0].quad.From p1 := (*qs)[0].Quad.From
if p1 == p0 { if p1 == p0 {
return return
} }
*qs = append(*qs, strokeQuad{ *qs = append(*qs, StrokeQuad{
quad: quadSegment{ Quad: QuadSegment{
From: p0, From: p0,
Ctrl: p0.Add(p1).Mul(0.5), Ctrl: p0.Add(p1).Mul(0.5),
To: p1, To: p1,
@@ -249,36 +256,36 @@ func (qs *strokeQuads) close() {
} }
// ccw returns whether the path is counter-clockwise. // ccw returns whether the path is counter-clockwise.
func (qs strokeQuads) ccw() bool { func (qs StrokeQuads) ccw() bool {
// Use the Shoelace formula: // Use the Shoelace formula:
// https://en.wikipedia.org/wiki/Shoelace_formula // https://en.wikipedia.org/wiki/Shoelace_formula
var area float32 var area float32
for _, ps := range qs.split() { for _, ps := range qs.split() {
for i := 1; i < len(ps); i++ { for i := 1; i < len(ps); i++ {
pi := ps[i].quad.To pi := ps[i].Quad.To
pj := ps[i-1].quad.To pj := ps[i-1].Quad.To
area += (pi.X - pj.X) * (pi.Y + pj.Y) area += (pi.X - pj.X) * (pi.Y + pj.Y)
} }
} }
return area <= 0.0 return area <= 0.0
} }
func (qs strokeQuads) reverse() strokeQuads { func (qs StrokeQuads) reverse() StrokeQuads {
if len(qs) == 0 { if len(qs) == 0 {
return nil return nil
} }
ps := make(strokeQuads, 0, len(qs)) ps := make(StrokeQuads, 0, len(qs))
for i := range qs { for i := range qs {
q := qs[len(qs)-1-i] q := qs[len(qs)-1-i]
q.quad.To, q.quad.From = q.quad.From, q.quad.To q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To
ps = append(ps, q) ps = append(ps, q)
} }
return ps return ps
} }
func (qs strokeQuads) append(ps strokeQuads) strokeQuads { func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads {
switch { switch {
case len(ps) == 0: case len(ps) == 0:
return qs return qs
@@ -289,11 +296,11 @@ func (qs strokeQuads) append(ps strokeQuads) strokeQuads {
// Consolidate quads and smooth out rounding errors. // Consolidate quads and smooth out rounding errors.
// We need to also check for the strokeTolerance to correctly handle // We need to also check for the strokeTolerance to correctly handle
// join/cap points or on-purpose disjoint quads. // join/cap points or on-purpose disjoint quads.
p0 := qs[len(qs)-1].quad.To p0 := qs[len(qs)-1].Quad.To
p1 := ps[0].quad.From p1 := ps[0].Quad.From
if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance { if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
qs = append(qs, strokeQuad{ qs = append(qs, StrokeQuad{
quad: quadSegment{ Quad: QuadSegment{
From: p0, From: p0,
Ctrl: p0.Add(p1).Mul(0.5), Ctrl: p0.Add(p1).Mul(0.5),
To: p1, To: p1,
@@ -303,6 +310,19 @@ func (qs strokeQuads) append(ps strokeQuads) strokeQuads {
return append(qs, ps...) return append(qs, ps...)
} }
func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment {
q.From = t.Transform(q.From)
q.Ctrl = t.Transform(q.Ctrl)
q.To = t.Transform(q.To)
return q
}
func decodeQuad(d []byte) (q QuadSegment) {
cmd := ops.DecodeCommand(d)
q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
return
}
// strokePathNorm returns the normal vector at t. // strokePathNorm returns the normal vector at t.
func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
switch t { switch t {
@@ -429,16 +449,16 @@ func quadBezierLen(p0, p1, p2 f32.Point) float32 {
return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32)) 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.
var qs strokeQuads var qs StrokeQuads
return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
} }
// flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments, // flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments,
// themselves also encoded as Bézier (degenerate, flat) quadratic curves. // themselves also encoded as Bézier (degenerate, flat) quadratic curves.
func flattenQuadBezier(qs strokeQuads, p0, p1, p2 f32.Point, d, flatness float32) strokeQuads { func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads {
var ( var (
t float32 t float32
flat64 = float64(flatness) flat64 = float64(flatness)
@@ -463,21 +483,21 @@ func flattenQuadBezier(qs strokeQuads, p0, p1, p2 f32.Point, d, flatness float32
return qs return qs
} }
func (qs *strokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) { func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
switch i := len(*qs); i { switch i := len(*qs); i {
case 0: case 0:
p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
default: default:
// Address possible rounding errors and use previous point. // Address possible rounding errors and use previous point.
p0 = (*qs)[i-1].quad.To p0 = (*qs)[i-1].Quad.To
} }
p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d)) p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
*qs = append(*qs, *qs = append(*qs,
strokeQuad{ StrokeQuad{
quad: quadSegment{ Quad: QuadSegment{
From: p0, From: p0,
Ctrl: p0.Add(p1).Mul(0.5), Ctrl: p0.Add(p1).Mul(0.5),
To: p1, To: p1,
@@ -513,7 +533,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 clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { func strokePathJoin(stroke clip.StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
if stroke.Miter > 0 { if stroke.Miter > 0 {
strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
return return
@@ -528,7 +548,7 @@ func strokePathJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32,
} }
} }
func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
rp := pivot.Add(n1) rp := pivot.Add(n1)
lp := pivot.Sub(n1) lp := pivot.Sub(n1)
@@ -537,7 +557,7 @@ func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po
lhs.lineTo(lp) 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) {
rp := pivot.Add(n1) rp := pivot.Add(n1)
lp := pivot.Sub(n1) lp := pivot.Sub(n1)
cw := dotPt(rot90CW(n0), n1) >= 0.0 cw := dotPt(rot90CW(n0), n1) >= 0.0
@@ -559,7 +579,7 @@ func strokePathRoundJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po
} }
} }
func strokePathMiterJoin(stroke 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) { if n0 == n1.Mul(-1) {
strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
return return
@@ -601,7 +621,7 @@ func strokePathMiterJoin(stroke clip.StrokeStyle, rhs, lhs *strokeQuads, hw floa
} }
// 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 clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) { func strokePathCap(stroke clip.StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
switch stroke.Cap { switch stroke.Cap {
case clip.FlatCap: case clip.FlatCap:
strokePathFlatCap(qs, hw, pivot, n0) strokePathFlatCap(qs, hw, pivot, n0)
@@ -615,13 +635,13 @@ func strokePathCap(stroke clip.StrokeStyle, qs *strokeQuads, hw float32, pivot,
} }
// strokePathFlatCap caps the start or end of a path with a flat cap. // strokePathFlatCap caps the start or end of a path with a flat cap.
func strokePathFlatCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) { func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
end := pivot.Sub(n0) end := pivot.Sub(n0)
qs.lineTo(end) qs.lineTo(end)
} }
// strokePathSquareCap caps the start or end of a path with a square cap. // strokePathSquareCap caps the start or end of a path with a square cap.
func strokePathSquareCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) { func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
var ( var (
e = pivot.Add(rot90CCW(n0)) e = pivot.Add(rot90CCW(n0))
corner1 = e.Add(n0) corner1 = e.Add(n0)
@@ -635,7 +655,7 @@ func strokePathSquareCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) {
} }
// 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.
func strokePathRoundCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) { func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
c := pivot.Sub(qs.pen()) c := pivot.Sub(qs.pen())
qs.arc(c, c, math.Pi) qs.arc(c, c, math.Pi)
} }