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) }