From 0a4b6549da3bbf729a7c650794a5403dc49b0dd4 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 23 Mar 2021 15:42:43 +0100 Subject: [PATCH] internal/stroke,gpu: move stroking of path data to package internal/strokg Pure refactor, preparing for use in op/clip. Signed-off-by: Elias Naur --- gpu/compute.go | 3 +- gpu/gpu.go | 121 +----------------------------------- internal/stroke/stroke.go | 125 +++++++++++++++++++++++++++++++++++++- 3 files changed, 127 insertions(+), 122 deletions(-) diff --git a/gpu/compute.go b/gpu/compute.go index 3524d45c..fee0a1e9 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -707,14 +707,13 @@ func encodePath(p *pathOp) encoder { var enc encoder verts := p.pathVerts if p.stroke.Width > 0 && !supportsStroke(p) { - quads := decodeToStrokeQuads(verts) ss := stroke.StrokeStyle{ Width: p.stroke.Width, Miter: p.stroke.Miter, Cap: stroke.StrokeCap(p.stroke.Cap), Join: stroke.StrokeJoin(p.stroke.Join), } - quads = quads.Stroke(ss, p.dashes) + quads := stroke.StrokePathCommands(ss, p.dashes, verts) for _, quad := range quads { q := quad.Quad enc.quad(q.From, q.Ctrl, q.To) diff --git a/gpu/gpu.go b/gpu/gpu.go index 5edf6bbb..2ae2993d 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -1361,14 +1361,13 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str switch { case str.Width > 0: // Stroke path. - quads := decodeToStrokeQuads(pathData) ss := stroke.StrokeStyle{ Width: str.Width, Miter: str.Miter, Cap: stroke.StrokeCap(str.Cap), Join: stroke.StrokeJoin(str.Join), } - quads = quads.Stroke(ss, dashes) + quads := stroke.StrokePathCommands(ss, dashes, pathData) for _, quad := range quads { d.qs.contour = quad.Contour quad.Quad = quad.Quad.Transform(tr) @@ -1403,7 +1402,7 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { q = q.Transform(tr) qs.splitAndEncode(q) case scene.OpCubic: - for _, q := range splitCubic(scene.DecodeCubic(cmd)) { + for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) { q = q.Transform(tr) qs.splitAndEncode(q) } @@ -1414,47 +1413,6 @@ 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) 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 stroke.QuadSegment - q.From, q.To = scene.DecodeLine(cmd) - q.Ctrl = q.From.Add(q.To).Mul(.5) - quad := stroke.StrokeQuad{ - Contour: contour, - Quad: q, - } - quads = append(quads, quad) - case scene.OpQuad: - var q stroke.QuadSegment - q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) - quad := stroke.StrokeQuad{ - Contour: contour, - Quad: q, - } - quads = append(quads, quad) - case scene.OpCubic: - for _, q := range splitCubic(scene.DecodeCubic(cmd)) { - quad := stroke.StrokeQuad{ - Contour: contour, - Quad: q, - } - quads = append(quads, quad) - } - default: - panic("unsupported scene command") - } - pathData = pathData[scene.CommandSize+4:] - } - return quads -} - // create GPU vertices for transformed r, find the bounds and establish texture transform. func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) { if isPureOffset(tr) { @@ -1534,78 +1492,3 @@ func isPureOffset(t f32.Affine2D) bool { a, b, _, d, e, _ := t.Elems() return a == 1 && b == 0 && d == 0 && e == 1 } - -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{ - Min: from, - Max: ctrl0, - }.Canon().Add(ctrl1).Add(to) - l := hull.Dx() - if h := hull.Dy(); h > l { - l = h - } - approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to) - return quads -} - -// approxCube approximates a cubic Bézier by a series of quadratic -// curves. -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 - // from its polynomial expression anchored at the starting point: - // - // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) - // - // The control point for the new quadratic Q1 that shares starting point, pen, with P is - // - // C1 = (3ctrl0 - pen)/2 - // - // The reverse cubic anchored at the end point has the polynomial - // - // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) - // - // The corresponding quadratic Q2 that shares the end point, to, with P has control - // point - // - // C2 = (3ctrl1 - to)/2 - // - // The combined quadratic Bézier, Q, shares both start and end points with its cubic - // and use the midpoint between the two curves Q1 and Q2 as control point: - // - // C = (3ctrl0 - pen + 3ctrl1 - to)/4 - 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, stroke.QuadSegment{From: from, Ctrl: c, To: to}) - return splits - } - // The maximum distance between the cubic P and its approximation Q given t - // can be shown to be - // - // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| - // - // To save a square root, compare d² with the squared tolerance. - 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, stroke.QuadSegment{From: from, Ctrl: c, To: to}) - return splits - } - // De Casteljau split the curve and approximate the halves. - t := float32(0.5) - c0 := from.Add(ctrl0.Sub(from).Mul(t)) - c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) - c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) - c01 := c0.Add(c1.Sub(c0).Mul(t)) - c12 := c1.Add(c2.Sub(c1).Mul(t)) - c0112 := c01.Add(c12.Sub(c01).Mul(t)) - splits++ - splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112) - splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to) - return splits -} diff --git a/internal/stroke/stroke.go b/internal/stroke/stroke.go index e344932e..07b78347 100644 --- a/internal/stroke/stroke.go +++ b/internal/stroke/stroke.go @@ -26,9 +26,12 @@ package stroke import ( + "encoding/binary" "math" "gioui.org/f32" + "gioui.org/internal/ops" + "gioui.org/internal/scene" ) // The following are copies of types from op/clip to avoid a circular import of @@ -152,7 +155,7 @@ func (qs StrokeQuads) split() []StrokeQuads { return o } -func (qs StrokeQuads) Stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { +func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { if !IsSolidLine(dashes) { qs = qs.dash(dashes) } @@ -765,3 +768,123 @@ func dist(p1, p2 f32.Point) float64 { ) return math.Hypot(dx, dy) } + +func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads { + quads := decodeToStrokeQuads(scene) + return quads.stroke(style, dashes) +} + +// decodeToStrokeQuads decodes scene commands to quads ready to stroke. +func decodeToStrokeQuads(pathData []byte) StrokeQuads { + quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) + for len(pathData) >= scene.CommandSize+4 { + contour := binary.LittleEndian.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpQuad: + var q QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + quad := 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, + } + quads = append(quads, quad) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } + return quads +} + +func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment { + quads := make([]QuadSegment, 0, 10) + // Set the maximum distance proportionally to the longest side + // of the bounding rectangle. + hull := f32.Rectangle{ + Min: from, + Max: ctrl0, + }.Canon().Add(ctrl1).Add(to) + l := hull.Dx() + if h := hull.Dy(); h > l { + l = h + } + approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to) + return quads +} + +// approxCubeTo 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 { + // The idea is from + // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + // where a quadratic approximates a cubic by eliminating its t³ term + // from its polynomial expression anchored at the starting point: + // + // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) + // + // The control point for the new quadratic Q1 that shares starting point, pen, with P is + // + // C1 = (3ctrl0 - pen)/2 + // + // The reverse cubic anchored at the end point has the polynomial + // + // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) + // + // The corresponding quadratic Q2 that shares the end point, to, with P has control + // point + // + // C2 = (3ctrl1 - to)/2 + // + // The combined quadratic Bézier, Q, shares both start and end points with its cubic + // and use the midpoint between the two curves Q1 and Q2 as control point: + // + // C = (3ctrl0 - pen + 3ctrl1 - to)/4 + 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}) + return splits + } + // The maximum distance between the cubic P and its approximation Q given t + // can be shown to be + // + // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| + // + // To save a square root, compare d² with the squared tolerance. + 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}) + return splits + } + // De Casteljau split the curve and approximate the halves. + t := float32(0.5) + c0 := from.Add(ctrl0.Sub(from).Mul(t)) + c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) + c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) + c01 := c0.Add(c1.Sub(c0).Mul(t)) + c12 := c1.Add(c2.Sub(c1).Mul(t)) + c0112 := c01.Add(c12.Sub(c01).Mul(t)) + splits++ + splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112) + splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to) + return splits +}