From 8c8d1dc16fb5ee82a4f62411bfbcc0ed3ef5f7e9 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 23 Mar 2021 12:35:41 +0100 Subject: [PATCH] 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 --- gpu/clip.go | 3 +- gpu/compute.go | 7 +- gpu/gpu.go | 107 +++++++++------------- {gpu => internal/stroke}/dash.go | 105 +++++++++++---------- {gpu => internal/stroke}/stroke.go | 142 ++++++++++++++++------------- 5 files changed, 185 insertions(+), 179 deletions(-) rename {gpu => internal/stroke}/dash.go (80%) rename {gpu => internal/stroke}/stroke.go (82%) diff --git a/gpu/clip.go b/gpu/clip.go index 3c4e5ca6..57c03a4f 100644 --- a/gpu/clip.go +++ b/gpu/clip.go @@ -2,6 +2,7 @@ package gpu import ( "gioui.org/f32" + "gioui.org/internal/stroke" ) type quadSplitter struct { @@ -46,7 +47,7 @@ func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) { encodeQuadTo(data, qs.contour, from, ctrl, to) } -func (qs *quadSplitter) splitAndEncode(quad quadSegment) { +func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) { cbnd := f32.Rectangle{ Min: quad.From, Max: quad.To, diff --git a/gpu/compute.go b/gpu/compute.go index f802d901..a8932cd0 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -18,6 +18,7 @@ import ( "gioui.org/internal/f32color" "gioui.org/internal/ops" "gioui.org/internal/scene" + "gioui.org/internal/stroke" "gioui.org/layout" "gioui.org/op" "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 { - 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 { @@ -707,9 +708,9 @@ func encodePath(p *pathOp) encoder { verts := p.pathVerts if p.stroke.Width > 0 && !supportsStroke(p) { quads := decodeToStrokeQuads(verts) - quads = quads.stroke(p.stroke, p.dashes) + quads = quads.Stroke(p.stroke, p.dashes) for _, quad := range quads { - q := quad.quad + q := quad.Quad enc.quad(q.From, q.Ctrl, q.To) } return enc diff --git a/gpu/gpu.go b/gpu/gpu.go index b5dbb448..93ea0b94 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -26,6 +26,7 @@ import ( "gioui.org/internal/opconst" "gioui.org/internal/ops" "gioui.org/internal/scene" + "gioui.org/internal/stroke" "gioui.org/layout" "gioui.org/op" "gioui.org/op/clip" @@ -130,7 +131,7 @@ type pathOp struct { // For compute trans f32.Affine2D stroke clip.StrokeStyle - dashes dashOp + dashes stroke.DashOp } type imageOp struct { @@ -142,24 +143,15 @@ type imageOp struct { place placement } -type dashOp struct { - phase float32 - dashes []float32 -} - -type quadSegment struct { - From, Ctrl, To f32.Point -} - -func decodeDashOp(data []byte) dashOp { +func decodeDashOp(data []byte) stroke.DashOp { _ = data[5] if opconst.OpType(data[0]) != opconst.TypeDash { panic("invalid op") } bo := binary.LittleEndian - return dashOp{ - phase: math.Float32frombits(bo.Uint32(data[1:])), - dashes: make([]float32, data[5]), + return stroke.DashOp{ + Phase: math.Float32frombits(bo.Uint32(data[1:])), + Dashes: make([]float32, data[5]), } } @@ -852,7 +844,7 @@ func (d *drawOps) newPathOp() *pathOp { 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 = pathOp{ parent: state.cpath, @@ -891,8 +883,8 @@ func (d *drawOps) save(id int, state drawState) { func (d *drawOps) collectOps(r *ops.Reader, state drawState) { var ( quads quadsOp - stroke clip.StrokeStyle - dashes dashOp + str clip.StrokeStyle + dashes stroke.DashOp z int ) d.save(opconst.InitialStateID, state) @@ -907,22 +899,22 @@ loop: case opconst.TypeDash: dashes = decodeDashOp(encOp.Data) - if len(dashes.dashes) > 0 { + if len(dashes.Dashes) > 0 { encOp, ok = r.Decode() if !ok { panic("gpu: could not decode dashes pattern") } data := encOp.Data[1:] bo := binary.LittleEndian - for i := range dashes.dashes { - dashes.dashes[i] = math.Float32frombits(bo.Uint32( + for i := range dashes.Dashes { + dashes.Dashes[i] = math.Float32frombits(bo.Uint32( data[i*4:], )) } } case opconst.TypeStroke: - stroke = decodeStrokeOp(encOp.Data) + str = decodeStrokeOp(encOp.Data) case opconst.TypePath: encOp, ok = r.Decode() @@ -948,7 +940,7 @@ loop: op.bounds = v.bounds } else { pathData, bounds := d.buildVerts( - quads.aux, trans, op.outline, stroke, dashes, + quads.aux, trans, op.outline, str, dashes, ) op.bounds = bounds if !d.compute { @@ -964,10 +956,10 @@ loop: quads.key.SetTransform(trans) } 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{} - stroke = clip.StrokeStyle{} - dashes = dashOp{} + str = clip.StrokeStyle{} + dashes = stroke.DashOp{} case opconst.TypeColor: state.matType = materialColor @@ -1005,7 +997,7 @@ loop: // The paint operation is sheared or rotated, add a clip path representing // this transformed rectangle. 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) @@ -1357,7 +1349,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, 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)) d.qs.bounds = f32.Rectangle{ 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: // Stroke path. quads := decodeToStrokeQuads(pathData) - quads = quads.stroke(stroke, dashes) + quads = quads.Stroke(stroke, dashes) for _, quad := range quads { - d.qs.contour = quad.contour - quad.quad = quad.quad.Transform(tr) + d.qs.contour = quad.Contour + quad.Quad = quad.Quad.Transform(tr) - d.qs.splitAndEncode(quad.quad) + d.qs.splitAndEncode(quad.Quad) } case outline: @@ -1394,13 +1386,13 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { cmd := ops.DecodeCommand(pathData[4:]) switch cmd.Op() { case scene.OpLine: - var q quadSegment + var q stroke.QuadSegment q.From, q.To = scene.DecodeLine(cmd) q.Ctrl = q.From.Add(q.To).Mul(.5) q = q.Transform(tr) qs.splitAndEncode(q) case scene.OpQuad: - var q quadSegment + var q stroke.QuadSegment q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) q = q.Transform(tr) 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 // quads ready to stroke. -func decodeToStrokeQuads(pathData []byte) strokeQuads { - quads := make(strokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) +func decodeToStrokeQuads(pathData []byte) stroke.StrokeQuads { + quads := make(stroke.StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) for len(pathData) >= scene.CommandSize+4 { contour := bo.Uint32(pathData) cmd := ops.DecodeCommand(pathData[4:]) switch cmd.Op() { case scene.OpLine: - var q quadSegment + var q stroke.QuadSegment q.From, q.To = scene.DecodeLine(cmd) q.Ctrl = q.From.Add(q.To).Mul(.5) - quad := strokeQuad{ - contour: contour, - quad: q, + quad := stroke.StrokeQuad{ + Contour: contour, + Quad: q, } quads = append(quads, quad) case scene.OpQuad: - var q quadSegment + var q stroke.QuadSegment q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) - quad := strokeQuad{ - contour: contour, - quad: q, + quad := stroke.StrokeQuad{ + Contour: contour, + Quad: q, } quads = append(quads, quad) case scene.OpCubic: for _, q := range splitCubic(scene.DecodeCubic(cmd)) { - quad := strokeQuad{ - contour: contour, - quad: q, + quad := stroke.StrokeQuad{ + Contour: contour, + Quad: q, } quads = append(quads, quad) } @@ -1537,21 +1529,8 @@ func isPureOffset(t f32.Affine2D) bool { return a == 1 && b == 0 && d == 0 && e == 1 } -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 -} - -func splitCubic(from, ctrl0, ctrl1, to f32.Point) []quadSegment { - quads := make([]quadSegment, 0, 10) +func splitCubic(from, ctrl0, ctrl1, to f32.Point) []stroke.QuadSegment { + quads := make([]stroke.QuadSegment, 0, 10) // Set the maximum distance proportionally to the longest side // of the bounding 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 // 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 // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html // 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) const maxSplits = 32 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 } // 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) d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) 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 } // De Casteljau split the curve and approximate the halves. diff --git a/gpu/dash.go b/internal/stroke/dash.go similarity index 80% rename from gpu/dash.go rename to internal/stroke/dash.go index 76706026..58745379 100644 --- a/gpu/dash.go +++ b/internal/stroke/dash.go @@ -4,7 +4,7 @@ // (and used as a reference implementation): // - github.com/tdewolff/canvas (Licensed under MIT) -package gpu +package stroke import ( "math" @@ -13,30 +13,35 @@ import ( "gioui.org/f32" ) -func isSolidLine(sty dashOp) bool { - return sty.phase == 0 && len(sty.dashes) == 0 +type DashOp struct { + 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) switch { - case len(sty.dashes) == 0: + case len(sty.Dashes) == 0: return qs - case len(sty.dashes) == 1 && sty.dashes[0] == 0.0: - return strokeQuads{} + case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0: + 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 // alternate. The following duplicates the pattern so that uneven // indices are always spaces. - sty.dashes = append(sty.dashes, sty.dashes...) + sty.Dashes = append(sty.Dashes, sty.Dashes...) } var ( i0, pos0 = dashStart(sty) - out strokeQuads + out StrokeQuads contour uint32 = 1 ) @@ -48,13 +53,13 @@ func (qs strokeQuads) dash(sty dashOp) strokeQuads { t []float64 length = ps.len() ) - for pos+sty.dashes[i] < length { - pos += sty.dashes[i] + 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) { + if i == len(sty.Dashes) { i = 0 } } @@ -66,7 +71,7 @@ func (qs strokeQuads) dash(sty dashOp) strokeQuads { } var ( - qd strokeQuads + qd StrokeQuads pd = ps.splitAt(&contour, t...) ) for j := j0; j < len(pd)-1; j += 2 { @@ -85,13 +90,13 @@ func (qs strokeQuads) dash(sty dashOp) strokeQuads { return out } -func dashCanonical(sty dashOp) dashOp { +func dashCanonical(sty DashOp) DashOp { var ( o = sty - ds = o.dashes + ds = o.Dashes ) - if len(sty.dashes) == 0 { + if len(sty.Dashes) == 0 { return sty } @@ -107,12 +112,12 @@ func dashCanonical(sty dashOp) dashOp { // 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}, + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, } } - o.phase -= ds[1] + o.Phase -= ds[1] ds[len(ds)-1] += ds[1] ds = ds[2:] } @@ -120,9 +125,9 @@ func dashCanonical(sty dashOp) dashOp { // Remove last zero, collapse with fist and second to last. if f32Eq(ds[len(ds)-1], 0.0) { 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 = ds[:len(ds)-2] } @@ -130,9 +135,9 @@ func dashCanonical(sty dashOp) dashOp { // 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}, + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, } } } @@ -151,31 +156,31 @@ loop: return o } -func dashStart(sty dashOp) (int, float32) { +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] + for sty.Dashes[i0] <= sty.Phase { + sty.Phase -= sty.Dashes[i0] i0++ - if i0 == len(sty.dashes) { + 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 { + pos0 := -sty.Phase + if sty.Phase < 0.0 { var sum float32 - for _, d := range sty.dashes { + for _, d := range sty.Dashes { sum += d } - pos0 = -(sum + sty.phase) // handle negative offsets + pos0 = -(sum + sty.Phase) // handle negative offsets } return i0, pos0 } -func (qs strokeQuads) len() float32 { +func (qs StrokeQuads) len() float32 { var sum float32 for i := range qs { - q := qs[i].quad + q := qs[i].Quad sum += quadBezierLen(q.From, q.Ctrl, q.To) } return sum @@ -184,10 +189,10 @@ func (qs strokeQuads) len() float32 { // 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 { +func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads { if len(ts) == 0 { qs.setContour(*contour) - return []strokeQuads{qs} + return []StrokeQuads{qs} } sort.Float64s(ts) @@ -200,8 +205,8 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads { t float64 // current position along curve ) - var oo []strokeQuads - var oi strokeQuads + var oo []StrokeQuads + var oi StrokeQuads push := func() { oo = append(oo, oi) oi = nil @@ -214,15 +219,15 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads { continue } 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) var ( t0 float64 - r0 = q.quad.From - r1 = q.quad.Ctrl - r2 = q.quad.To + r0 = q.Quad.From + r1 = q.Quad.Ctrl + r2 = q.Quad.To // from keeps track of the start of the 'running' segment. from = r0 @@ -235,9 +240,9 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads { var q1 f32.Point _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub)) - oi = append(oi, strokeQuad{ - contour: *contour, - quad: quadSegment{ + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ From: from, Ctrl: q1, To: r0, @@ -253,9 +258,9 @@ func (qs strokeQuads) splitAt(contour *uint32, ts ...float64) []strokeQuads { if len(oi) > 0 { r0 = oi.pen() } - oi = append(oi, strokeQuad{ - contour: *contour, - quad: quadSegment{ + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ From: r0, Ctrl: r1, To: r2, diff --git a/gpu/stroke.go b/internal/stroke/stroke.go similarity index 82% rename from gpu/stroke.go rename to internal/stroke/stroke.go index c0bd8f18..24dc7365 100644 --- a/gpu/stroke.go +++ b/internal/stroke/stroke.go @@ -21,12 +21,15 @@ // - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html // 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 ( "math" "gioui.org/f32" + "gioui.org/internal/ops" "gioui.org/internal/scene" "gioui.org/op" "gioui.org/op/clip" @@ -41,9 +44,13 @@ import ( // and speed. const strokeTolerance = 0.01 -type strokeQuad struct { - contour uint32 - quad quadSegment +type QuadSegment struct { + From, Ctrl, To f32.Point +} + +type StrokeQuad struct { + Contour uint32 + Quad QuadSegment } type strokeState struct { @@ -53,28 +60,28 @@ type strokeState struct { 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 { - (*qs)[i].contour = n + (*qs)[i].Contour = n } } -func (qs *strokeQuads) pen() f32.Point { - return (*qs)[len(*qs)-1].quad.To +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 +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() - *qs = append(*qs, strokeQuad{ - quad: quadSegment{ + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ From: end, Ctrl: end.Add(pt).Mul(0.5), 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 ( p clip.Path 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++ { quad := decodeQuad(raw[4:]) raw = raw[scene.CommandSize+4:] - *qs = append(*qs, strokeQuad{ - quad: quad, + *qs = append(*qs, StrokeQuad{ + Quad: quad, }) } } // split splits a slice of quads into slices of quads grouped // by contours (ie: splitted at move-to boundaries). -func (qs strokeQuads) split() []strokeQuads { +func (qs StrokeQuads) split() []StrokeQuads { if len(qs) == 0 { return nil } var ( c uint32 - o []strokeQuads + o []StrokeQuads i = len(o) ) for _, q := range qs { - if q.contour != c { - c = q.contour + if q.Contour != c { + c = q.Contour i = len(o) - o = append(o, strokeQuads{}) + o = append(o, StrokeQuads{}) } o[i] = append(o[i], q) } @@ -127,13 +134,13 @@ func (qs strokeQuads) split() []strokeQuads { return o } -func (qs strokeQuads) stroke(stroke clip.StrokeStyle, dashes dashOp) strokeQuads { - if !isSolidLine(dashes) { +func (qs StrokeQuads) Stroke(stroke clip.StrokeStyle, dashes DashOp) StrokeQuads { + if !IsSolidLine(dashes) { qs = qs.dash(dashes) } var ( - o strokeQuads + o StrokeQuads 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 // the half-width hw. // 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 ( states []strokeState - beg = qs[0].quad.From - end = qs[len(qs)-1].quad.To + beg = qs[0].Quad.From + end = qs[len(qs)-1].Quad.To closed = beg == end ) for i := range qs { - q := qs[i].quad + q := qs[i].Quad var ( 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 } -func (qs *strokeQuads) close() { - p0 := (*qs)[len(*qs)-1].quad.To - p1 := (*qs)[0].quad.From +func (qs *StrokeQuads) close() { + p0 := (*qs)[len(*qs)-1].Quad.To + p1 := (*qs)[0].Quad.From if p1 == p0 { return } - *qs = append(*qs, strokeQuad{ - quad: quadSegment{ + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ From: p0, Ctrl: p0.Add(p1).Mul(0.5), To: p1, @@ -249,36 +256,36 @@ func (qs *strokeQuads) close() { } // ccw returns whether the path is counter-clockwise. -func (qs strokeQuads) ccw() bool { +func (qs StrokeQuads) ccw() bool { // Use the Shoelace formula: // https://en.wikipedia.org/wiki/Shoelace_formula var area float32 for _, ps := range qs.split() { for i := 1; i < len(ps); i++ { - pi := ps[i].quad.To - pj := ps[i-1].quad.To + pi := ps[i].Quad.To + pj := ps[i-1].Quad.To area += (pi.X - pj.X) * (pi.Y + pj.Y) } } return area <= 0.0 } -func (qs strokeQuads) reverse() strokeQuads { +func (qs StrokeQuads) reverse() StrokeQuads { if len(qs) == 0 { return nil } - ps := make(strokeQuads, 0, len(qs)) + ps := make(StrokeQuads, 0, len(qs)) for i := range qs { 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) } return ps } -func (qs strokeQuads) append(ps strokeQuads) strokeQuads { +func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads { switch { case len(ps) == 0: return qs @@ -289,11 +296,11 @@ func (qs strokeQuads) append(ps strokeQuads) strokeQuads { // Consolidate quads and smooth out rounding errors. // We need to also check for the strokeTolerance to correctly handle // join/cap points or on-purpose disjoint quads. - p0 := qs[len(qs)-1].quad.To - p1 := ps[0].quad.From + p0 := qs[len(qs)-1].Quad.To + p1 := ps[0].Quad.From if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance { - qs = append(qs, strokeQuad{ - quad: quadSegment{ + qs = append(qs, StrokeQuad{ + Quad: QuadSegment{ From: p0, Ctrl: p0.Add(p1).Mul(0.5), To: p1, @@ -303,6 +310,19 @@ func (qs strokeQuads) append(ps strokeQuads) strokeQuads { 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. func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { 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)) } -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. // So we just have to flatten them. - var qs strokeQuads + var qs StrokeQuads return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) } // flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments, // 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 ( t float32 flat64 = float64(flatness) @@ -463,21 +483,21 @@ func flattenQuadBezier(qs strokeQuads, p0, p1, p2 f32.Point, d, flatness float32 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 { case 0: p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) default: // 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)) *qs = append(*qs, - strokeQuad{ - quad: quadSegment{ + StrokeQuad{ + Quad: QuadSegment{ From: p0, Ctrl: p0.Add(p1).Mul(0.5), 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 // 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 { strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) 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) lp := pivot.Sub(n1) @@ -537,7 +557,7 @@ func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Po 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) lp := pivot.Sub(n1) 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) { strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) 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. -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 { case clip.FlatCap: 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. -func strokePathFlatCap(qs *strokeQuads, hw float32, pivot, n0 f32.Point) { +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) { +func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { var ( e = pivot.Add(rot90CCW(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. -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()) qs.arc(c, c, math.Pi) }