From 7825bda8f80c58a3647500d1e8fd1aef5eac8efd Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 23 Mar 2021 15:14:44 +0100 Subject: [PATCH] internal/stroke,op/clip: don't import op/clip from internal/stroke To avoid an import cycle in a future change, internal/stroke can no longer import op/clip. Move required op/clip functionality to internal/stroke and duplicate the remaining types. Signed-off-by: Elias Naur --- gpu/compute.go | 8 +- gpu/gpu.go | 12 ++- internal/stroke/stroke.go | 176 ++++++++++++++++++++++++++++++-------- op/clip/clip.go | 97 +-------------------- 4 files changed, 159 insertions(+), 134 deletions(-) diff --git a/gpu/compute.go b/gpu/compute.go index a8932cd0..3524d45c 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -708,7 +708,13 @@ 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) + 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) 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 93ea0b94..5edf6bbb 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -1349,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 stroke.DashOp) (verts []byte, bounds f32.Rectangle) { +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str 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}, @@ -1359,10 +1359,16 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str startLength := len(d.vertCache) switch { - case stroke.Width > 0: + case str.Width > 0: // Stroke path. quads := decodeToStrokeQuads(pathData) - quads = quads.Stroke(stroke, dashes) + ss := stroke.StrokeStyle{ + Width: str.Width, + Miter: str.Miter, + Cap: stroke.StrokeCap(str.Cap), + Join: stroke.StrokeJoin(str.Join), + } + quads = quads.Stroke(ss, dashes) for _, quad := range quads { d.qs.contour = quad.Contour quad.Quad = quad.Quad.Transform(tr) diff --git a/internal/stroke/stroke.go b/internal/stroke/stroke.go index 24dc7365..e344932e 100644 --- a/internal/stroke/stroke.go +++ b/internal/stroke/stroke.go @@ -29,10 +29,32 @@ import ( "math" "gioui.org/f32" - "gioui.org/internal/ops" - "gioui.org/internal/scene" - "gioui.org/op" - "gioui.org/op/clip" +) + +// The following are copies of types from op/clip to avoid a circular import of +// that package. +// TODO: when the old renderer is gone, this package can be merged with +// op/clip, eliminating the duplicate types. +type StrokeStyle struct { + Width float32 + Miter float32 + Cap StrokeCap + Join StrokeJoin +} + +type StrokeCap uint8 + +const ( + RoundCap StrokeCap = iota + FlatCap + SquareCap +) + +type StrokeJoin uint8 + +const ( + RoundJoin StrokeJoin = iota + BevelJoin ) // strokeTolerance is used to reconcile rounding errors arising @@ -90,22 +112,18 @@ func (qs *StrokeQuads) lineTo(pt f32.Point) { } func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) { - var ( - p clip.Path - o = new(op.Ops) - ) - p.Begin(o) - p.Move(qs.pen()) - beg := len(o.Data()) - p.Arc(f1, f2, angle) - end := len(o.Data()) - raw := o.Data()[beg:end] - - for qi := 0; len(raw) >= (scene.CommandSize + 4); qi++ { - quad := decodeQuad(raw[4:]) - raw = raw[scene.CommandSize+4:] + const segments = 16 + pen := qs.pen() + m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments) + for i := 0; i < segments; i++ { + p0 := qs.pen() + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) *qs = append(*qs, StrokeQuad{ - Quad: quad, + Quad: QuadSegment{ + From: p0, Ctrl: ctl, To: p2, + }, }) } } @@ -134,7 +152,7 @@ func (qs StrokeQuads) split() []StrokeQuads { return o } -func (qs StrokeQuads) Stroke(stroke clip.StrokeStyle, dashes DashOp) StrokeQuads { +func (qs StrokeQuads) Stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { if !IsSolidLine(dashes) { qs = qs.dash(dashes) } @@ -171,7 +189,7 @@ 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 StrokeStyle) (rhs, lhs StrokeQuads) { var ( states []strokeState beg = qs[0].Quad.From @@ -317,12 +335,6 @@ func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment { 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 { @@ -533,15 +545,15 @@ 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 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 } switch stroke.Join { - case clip.BevelJoin: + case BevelJoin: strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) - case clip.RoundJoin: + case RoundJoin: strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) default: panic("impossible") @@ -579,7 +591,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 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 @@ -621,13 +633,13 @@ 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 StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { switch stroke.Cap { - case clip.FlatCap: + case FlatCap: strokePathFlatCap(qs, hw, pivot, n0) - case clip.SquareCap: + case SquareCap: strokePathSquareCap(qs, hw, pivot, n0) - case clip.RoundCap: + case RoundCap: strokePathRoundCap(qs, hw, pivot, n0) default: panic("impossible") @@ -659,3 +671,97 @@ func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { c := pivot.Sub(qs.pen()) qs.arc(c, c, math.Pi) } + +// ArcTransform computes a transformation that can be used for generating quadratic bézier +// curve approximations for an arc. +// +// The math is extracted from the following paper: +// "Drawing an elliptical arc using polylines, quadratic or +// cubic Bezier curves", L. Maisonobe +// An electronic version may be found at: +// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf +func ArcTransform(p, f1, f2 f32.Point, angle float32, segments int) f32.Affine2D { + c := f32.Point{ + X: 0.5 * (f1.X + f2.X), + Y: 0.5 * (f1.Y + f2.Y), + } + + // semi-major axis: 2a = |PF1| + |PF2| + a := 0.5 * (dist(f1, p) + dist(f2, p)) + + // semi-minor axis: c^2 = a^2+b^2 (c: focal distance) + f := dist(f1, c) + b := math.Sqrt(a*a - f*f) + + var rx, ry, alpha, start float64 + switch { + case a > b: + rx = a + ry = b + default: + rx = b + ry = a + } + + var x float64 + switch { + case f1 == c || f2 == c: + // degenerate case of a circle. + alpha = 0 + default: + switch { + case f1.X > c.X: + x = float64(f1.X - c.X) + alpha = math.Acos(x / f) + case f1.X < c.X: + x = float64(f2.X - c.X) + alpha = math.Acos(x / f) + case f1.X == c.X: + // special case of a "vertical" ellipse. + alpha = math.Pi / 2 + if f1.Y < c.Y { + alpha = -alpha + } + } + } + + start = math.Acos(float64(p.X-c.X) / dist(c, p)) + if c.Y > p.Y { + start = -start + } + start -= alpha + + var ( + θ = angle / float32(segments) + ref f32.Affine2D // transform from absolute frame to ellipse-based one + rot f32.Affine2D // rotation matrix for each segment + inv f32.Affine2D // transform from ellipse-based frame to absolute one + ) + ref = ref.Offset(f32.Point{}.Sub(c)) + ref = ref.Rotate(f32.Point{}, float32(-alpha)) + ref = ref.Scale(f32.Point{}, f32.Point{ + X: float32(1 / rx), + Y: float32(1 / ry), + }) + inv = ref.Invert() + rot = rot.Rotate(f32.Point{}, float32(0.5*θ)) + + // Instead of invoking math.Sincos for every segment, compute a rotation + // matrix once and apply for each segment. + // Before applying the rotation matrix rot, transform the coordinates + // to a frame centered to the ellipse (and warped into a unit circle), then rotate. + // Finally, transform back into the original frame. + return inv.Mul(rot).Mul(ref) +} + +func dist(p1, p2 f32.Point) float64 { + var ( + x1 = float64(p1.X) + y1 = float64(p1.Y) + x2 = float64(p2.X) + y2 = float64(p2.Y) + dx = x2 - x1 + dy = y2 - y1 + ) + return math.Hypot(dx, dy) +} diff --git a/op/clip/clip.go b/op/clip/clip.go index b6191fb3..f0400f97 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -11,6 +11,7 @@ import ( "gioui.org/internal/opconst" "gioui.org/internal/ops" "gioui.org/internal/scene" + "gioui.org/internal/stroke" "gioui.org/op" ) @@ -174,7 +175,7 @@ func (p *Path) Arc(f1, f2 f32.Point, angle float32) { f1 = f1.Add(p.pen) f2 = f2.Add(p.pen) const segments = 16 - m := arcTransform(p.pen, f1, f2, angle, segments) + m := stroke.ArcTransform(p.pen, f1, f2, angle, segments) for i := 0; i < segments; i++ { p0 := p.pen @@ -185,100 +186,6 @@ func (p *Path) Arc(f1, f2 f32.Point, angle float32) { } } -func dist(p1, p2 f32.Point) float64 { - var ( - x1 = float64(p1.X) - y1 = float64(p1.Y) - x2 = float64(p2.X) - y2 = float64(p2.Y) - dx = x2 - x1 - dy = y2 - y1 - ) - return math.Hypot(dx, dy) -} - -// arcTransform computes a transformation that can be used for generating quadratic bézier -// curve approximations for an arc. -// -// The math is extracted from the following paper: -// "Drawing an elliptical arc using polylines, quadratic or -// cubic Bezier curves", L. Maisonobe -// An electronic version may be found at: -// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf -func arcTransform(p, f1, f2 f32.Point, angle float32, segments int) f32.Affine2D { - c := f32.Point{ - X: 0.5 * (f1.X + f2.X), - Y: 0.5 * (f1.Y + f2.Y), - } - - // semi-major axis: 2a = |PF1| + |PF2| - a := 0.5 * (dist(f1, p) + dist(f2, p)) - - // semi-minor axis: c^2 = a^2+b^2 (c: focal distance) - f := dist(f1, c) - b := math.Sqrt(a*a - f*f) - - var rx, ry, alpha, start float64 - switch { - case a > b: - rx = a - ry = b - default: - rx = b - ry = a - } - - var x float64 - switch { - case f1 == c || f2 == c: - // degenerate case of a circle. - alpha = 0 - default: - switch { - case f1.X > c.X: - x = float64(f1.X - c.X) - alpha = math.Acos(x / f) - case f1.X < c.X: - x = float64(f2.X - c.X) - alpha = math.Acos(x / f) - case f1.X == c.X: - // special case of a "vertical" ellipse. - alpha = math.Pi / 2 - if f1.Y < c.Y { - alpha = -alpha - } - } - } - - start = math.Acos(float64(p.X-c.X) / dist(c, p)) - if c.Y > p.Y { - start = -start - } - start -= alpha - - var ( - θ = angle / float32(segments) - ref f32.Affine2D // transform from absolute frame to ellipse-based one - rot f32.Affine2D // rotation matrix for each segment - inv f32.Affine2D // transform from ellipse-based frame to absolute one - ) - ref = ref.Offset(f32.Point{}.Sub(c)) - ref = ref.Rotate(f32.Point{}, float32(-alpha)) - ref = ref.Scale(f32.Point{}, f32.Point{ - X: float32(1 / rx), - Y: float32(1 / ry), - }) - inv = ref.Invert() - rot = rot.Rotate(f32.Point{}, float32(0.5*θ)) - - // Instead of invoking math.Sincos for every segment, compute a rotation - // matrix once and apply for each segment. - // Before applying the rotation matrix rot, transform the coordinates - // to a frame centered to the ellipse (and warped into a unit circle), then rotate. - // Finally, transform back into the original frame. - return inv.Mul(rot).Mul(ref) -} - // Cube records a cubic Bézier from the pen through // two control points ending in to. func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {