~eliasnaur/gio-patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
5 4

[PATCH gio-x] stroke: add ArcTo

Sebastien Binet <binet@cern.ch>
Details
Message ID
<20220513123321.2300212-1-binet@cern.ch>
DKIM signature
missing
Download raw message
Patch: +127 -0
This CL adds ArcTo and a special segment operation to easily create
arc segments.

Signed-off-by: Sebastien Binet <binet@cern.ch>
---
 stroke/arc.go    | 106 +++++++++++++++++++++++++++++++++++++++++++++++
 stroke/stroke.go |  21 ++++++++++
 2 files changed, 127 insertions(+)
 create mode 100644 stroke/arc.go

diff --git a/stroke/arc.go b/stroke/arc.go
new file mode 100644
index 0000000..64bef5d
--- /dev/null
+++ b/stroke/arc.go
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Unlicense OR MIT

package stroke

import (
	"math"

	"gioui.org/f32"
)

// 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) (transform f32.Affine2D, segments int) {
	const segmentsPerCircle = 16
	const anglePerSegment = 2 * math.Pi / segmentsPerCircle

	s := angle / anglePerSegment
	if s < 0 {
		s = -s
	}
	segments = int(math.Ceil(float64(s)))
	if segments <= 0 {
		segments = 1
	}

	var rx, ry, alpha float64
	if f1 == f2 {
		// degenerate case of a circle.
		rx = dist(f1, p)
		ry = rx
	} else {
		// 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)
		c := dist(f1, f2) * 0.5
		b := math.Sqrt(a*a - c*c)
		switch {
		case a > b:
			rx = a
			ry = b
		default:
			rx = b
			ry = a
		}
		if f1.X == f2.X {
			// special case of a "vertical" ellipse.
			alpha = math.Pi / 2
			if f1.Y < f2.Y {
				alpha = -alpha
			}
		} else {
			x := float64(f1.X-f2.X) * 0.5
			if x < 0 {
				x = -x
			}
			alpha = math.Acos(x / c)
		}
	}

	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
	)
	center := f32.Point{
		X: 0.5 * (f1.X + f2.X),
		Y: 0.5 * (f1.Y + f2.Y),
	}
	ref = ref.Offset(f32.Point{}.Sub(center))
	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{}, 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), segments
}

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/stroke/stroke.go b/stroke/stroke.go
index f1c3756..3ab2362 100644
--- a/stroke/stroke.go
+++ b/stroke/stroke.go
@@ -48,6 +48,7 @@ const (
	segOpLineTo
	segOpQuadTo
	segOpCubeTo
	segOpArcTo
)

// StrokeCap describes the head or tail of a stroked path.
@@ -119,6 +120,16 @@ func CubeTo(ctrl0, ctrl1, end f32.Point) Segment {
	return s
}

func ArcTo(f1, f2 f32.Point, angle float32) Segment {
	s := Segment{
		op: segOpArcTo,
	}
	s.args[0] = f1
	s.args[1] = f2
	s.args[2].X = angle
	return s
}

// Op returns a clip operation that approximates stroke.
func (s Stroke) Op(ops *op.Ops) clip.Op {
	if len(s.Path.Segments) == 0 {
@@ -147,6 +158,16 @@ func (s Stroke) Op(ops *op.Ops) clip.Op {
		case segOpCubeTo:
			contour = append(contour, stroke.Segment{stroke.Point(pen), stroke.Point(seg.args[0]), stroke.Point(seg.args[1]), stroke.Point(seg.args[2])})
			pen = seg.args[2]
		case segOpArcTo:
			m, nseg := arcTransform(pen, seg.args[0], seg.args[1], seg.args[2].X)
			for i := 0; i < nseg; i++ {
				p0 := pen
				p1 := m.Transform(p0)
				p2 := m.Transform(p1)
				ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
				contour = append(contour, stroke.QuadraticSegment(stroke.Point(pen), stroke.Point(ctl), stroke.Point(p2)))
				pen = p2
			}
		}
	}
	if len(contour) > 0 {
-- 
2.36.1
Details
Message ID
<CJYN5FY5D078.2KTG7KGIX8X0D@clrinfopc42>
In-Reply-To
<20220513123321.2300212-1-binet@cern.ch> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
hi,

upgrading gonum/plot to the latest Gio version, I had the need to be
able to build stroked paths along an arc.
this doesn't seem to have been possible since the migration to the
x/stroke + andybalholm/stroke setup.

-s

On Fri May 13, 2022 at 14:33 CET, Sebastien Binet wrote:
> This CL adds ArcTo and a special segment operation to easily create
> arc segments.
>
> Signed-off-by: Sebastien Binet <binet@cern.ch>
> ---
> stroke/arc.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++
> stroke/stroke.go | 21 ++++++++++
> 2 files changed, 127 insertions(+)
> create mode 100644 stroke/arc.go
>
> diff --git a/stroke/arc.go b/stroke/arc.go
> new file mode 100644
> index 0000000..64bef5d
> --- /dev/null
> +++ b/stroke/arc.go
> @@ -0,0 +1,106 @@
> +// SPDX-License-Identifier: Unlicense OR MIT
> +
> +package stroke
> +
> +import (
> + "math"
> +
> + "gioui.org/f32"
> +)
> +
> +// 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) (transform
> f32.Affine2D, segments int) {
> + const segmentsPerCircle = 16
> + const anglePerSegment = 2 * math.Pi / segmentsPerCircle
> +
> + s := angle / anglePerSegment
> + if s < 0 {
> + s = -s
> + }
> + segments = int(math.Ceil(float64(s)))
> + if segments <= 0 {
> + segments = 1
> + }
> +
> + var rx, ry, alpha float64
> + if f1 == f2 {
> + // degenerate case of a circle.
> + rx = dist(f1, p)
> + ry = rx
> + } else {
> + // 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)
> + c := dist(f1, f2) * 0.5
> + b := math.Sqrt(a*a - c*c)
> + switch {
> + case a > b:
> + rx = a
> + ry = b
> + default:
> + rx = b
> + ry = a
> + }
> + if f1.X == f2.X {
> + // special case of a "vertical" ellipse.
> + alpha = math.Pi / 2
> + if f1.Y < f2.Y {
> + alpha = -alpha
> + }
> + } else {
> + x := float64(f1.X-f2.X) * 0.5
> + if x < 0 {
> + x = -x
> + }
> + alpha = math.Acos(x / c)
> + }
> + }
> +
> + 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
> + )
> + center := f32.Point{
> + X: 0.5 * (f1.X + f2.X),
> + Y: 0.5 * (f1.Y + f2.Y),
> + }
> + ref = ref.Offset(f32.Point{}.Sub(center))
> + 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{}, 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), segments
> +}
> +
> +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/stroke/stroke.go b/stroke/stroke.go
> index f1c3756..3ab2362 100644
> --- a/stroke/stroke.go
> +++ b/stroke/stroke.go
> @@ -48,6 +48,7 @@ const (
> segOpLineTo
> segOpQuadTo
> segOpCubeTo
> + segOpArcTo
> )
>  
> // StrokeCap describes the head or tail of a stroked path.
> @@ -119,6 +120,16 @@ func CubeTo(ctrl0, ctrl1, end f32.Point) Segment {
> return s
> }
>  
> +func ArcTo(f1, f2 f32.Point, angle float32) Segment {
> + s := Segment{
> + op: segOpArcTo,
> + }
> + s.args[0] = f1
> + s.args[1] = f2
> + s.args[2].X = angle
> + return s
> +}
> +
> // Op returns a clip operation that approximates stroke.
> func (s Stroke) Op(ops *op.Ops) clip.Op {
> if len(s.Path.Segments) == 0 {
> @@ -147,6 +158,16 @@ func (s Stroke) Op(ops *op.Ops) clip.Op {
> case segOpCubeTo:
> contour = append(contour, stroke.Segment{stroke.Point(pen),
> stroke.Point(seg.args[0]), stroke.Point(seg.args[1]),
> stroke.Point(seg.args[2])})
> pen = seg.args[2]
> + case segOpArcTo:
> + m, nseg := arcTransform(pen, seg.args[0], seg.args[1], seg.args[2].X)
> + for i := 0; i < nseg; i++ {
> + p0 := pen
> + p1 := m.Transform(p0)
> + p2 := m.Transform(p1)
> + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
> + contour = append(contour, stroke.QuadraticSegment(stroke.Point(pen),
> stroke.Point(ctl), stroke.Point(p2)))
> + pen = p2
> + }
> }
> }
> if len(contour) > 0 {
> --
> 2.36.1
Details
Message ID
<CAMAFT9WpUN=vC8oj5Fh+=Q9-62SSsfk3O5sYygOD3SVDfOYoPw@mail.gmail.com>
In-Reply-To
<20220513123321.2300212-1-binet@cern.ch> (view parent)
DKIM signature
pass
Download raw message
Andy (CC'ed), do you have bandwidth to review this? Does ArcTo belong
in https://github.com/andybalholm/stroke?

Elias

On Fri, 13 May 2022 at 14:33, Sebastien Binet <binet@cern.ch> wrote:
>
> This CL adds ArcTo and a special segment operation to easily create
> arc segments.
>
> Signed-off-by: Sebastien Binet <binet@cern.ch>
> ---
>  stroke/arc.go    | 106 +++++++++++++++++++++++++++++++++++++++++++++++
>  stroke/stroke.go |  21 ++++++++++
>  2 files changed, 127 insertions(+)
>  create mode 100644 stroke/arc.go
>
> diff --git a/stroke/arc.go b/stroke/arc.go
> new file mode 100644
> index 0000000..64bef5d
> --- /dev/null
> +++ b/stroke/arc.go
> @@ -0,0 +1,106 @@
> +// SPDX-License-Identifier: Unlicense OR MIT
> +
> +package stroke
> +
> +import (
> +       "math"
> +
> +       "gioui.org/f32"
> +)
> +
> +// 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) (transform f32.Affine2D, segments int) {
> +       const segmentsPerCircle = 16
> +       const anglePerSegment = 2 * math.Pi / segmentsPerCircle
> +
> +       s := angle / anglePerSegment
> +       if s < 0 {
> +               s = -s
> +       }
> +       segments = int(math.Ceil(float64(s)))
> +       if segments <= 0 {
> +               segments = 1
> +       }
> +
> +       var rx, ry, alpha float64
> +       if f1 == f2 {
> +               // degenerate case of a circle.
> +               rx = dist(f1, p)
> +               ry = rx
> +       } else {
> +               // 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)
> +               c := dist(f1, f2) * 0.5
> +               b := math.Sqrt(a*a - c*c)
> +               switch {
> +               case a > b:
> +                       rx = a
> +                       ry = b
> +               default:
> +                       rx = b
> +                       ry = a
> +               }
> +               if f1.X == f2.X {
> +                       // special case of a "vertical" ellipse.
> +                       alpha = math.Pi / 2
> +                       if f1.Y < f2.Y {
> +                               alpha = -alpha
> +                       }
> +               } else {
> +                       x := float64(f1.X-f2.X) * 0.5
> +                       if x < 0 {
> +                               x = -x
> +                       }
> +                       alpha = math.Acos(x / c)
> +               }
> +       }
> +
> +       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
> +       )
> +       center := f32.Point{
> +               X: 0.5 * (f1.X + f2.X),
> +               Y: 0.5 * (f1.Y + f2.Y),
> +       }
> +       ref = ref.Offset(f32.Point{}.Sub(center))
> +       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{}, 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), segments
> +}
> +
> +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/stroke/stroke.go b/stroke/stroke.go
> index f1c3756..3ab2362 100644
> --- a/stroke/stroke.go
> +++ b/stroke/stroke.go
> @@ -48,6 +48,7 @@ const (
>         segOpLineTo
>         segOpQuadTo
>         segOpCubeTo
> +       segOpArcTo
>  )
>
>  // StrokeCap describes the head or tail of a stroked path.
> @@ -119,6 +120,16 @@ func CubeTo(ctrl0, ctrl1, end f32.Point) Segment {
>         return s
>  }
>
> +func ArcTo(f1, f2 f32.Point, angle float32) Segment {
> +       s := Segment{
> +               op: segOpArcTo,
> +       }
> +       s.args[0] = f1
> +       s.args[1] = f2
> +       s.args[2].X = angle
> +       return s
> +}
> +
>  // Op returns a clip operation that approximates stroke.
>  func (s Stroke) Op(ops *op.Ops) clip.Op {
>         if len(s.Path.Segments) == 0 {
> @@ -147,6 +158,16 @@ func (s Stroke) Op(ops *op.Ops) clip.Op {
>                 case segOpCubeTo:
>                         contour = append(contour, stroke.Segment{stroke.Point(pen), stroke.Point(seg.args[0]), stroke.Point(seg.args[1]), stroke.Point(seg.args[2])})
>                         pen = seg.args[2]
> +               case segOpArcTo:
> +                       m, nseg := arcTransform(pen, seg.args[0], seg.args[1], seg.args[2].X)
> +                       for i := 0; i < nseg; i++ {
> +                               p0 := pen
> +                               p1 := m.Transform(p0)
> +                               p2 := m.Transform(p1)
> +                               ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
> +                               contour = append(contour, stroke.QuadraticSegment(stroke.Point(pen), stroke.Point(ctl), stroke.Point(p2)))
> +                               pen = p2
> +                       }
>                 }
>         }
>         if len(contour) > 0 {
> --
> 2.36.1
>
Details
Message ID
<d439055f-e143-3330-7573-9a63992892a3@balholm.com>
In-Reply-To
<CAMAFT9WpUN=vC8oj5Fh+=Q9-62SSsfk3O5sYygOD3SVDfOYoPw@mail.gmail.com> (view parent)
DKIM signature
pass
Download raw message
I would be fine with adding something like this to 
github.com/andybalholm/stroke.

It would make more sense to use a cubic approximation of the arc 
directly, instead of using a quadratic approximation (which then gets 
converted to cubic segments). With a cubic approximation, you could 
split the circle into 6 or 8 segments instead of 16.

Andy

On 5/17/22 04:47, Elias Naur wrote:
> Andy (CC'ed), do you have bandwidth to review this? Does ArcTo belong
> in https://github.com/andybalholm/stroke?
>
> Elias
>
> On Fri, 13 May 2022 at 14:33, Sebastien Binet <binet@cern.ch> wrote:
>> This CL adds ArcTo and a special segment operation to easily create
>> arc segments.
>>
>> Signed-off-by: Sebastien Binet <binet@cern.ch>
>> ---
>>   stroke/arc.go    | 106 +++++++++++++++++++++++++++++++++++++++++++++++
>>   stroke/stroke.go |  21 ++++++++++
>>   2 files changed, 127 insertions(+)
>>   create mode 100644 stroke/arc.go
>>
>> diff --git a/stroke/arc.go b/stroke/arc.go
>> new file mode 100644
>> index 0000000..64bef5d
>> --- /dev/null
>> +++ b/stroke/arc.go
>> @@ -0,0 +1,106 @@
>> +// SPDX-License-Identifier: Unlicense OR MIT
>> +
>> +package stroke
>> +
>> +import (
>> +       "math"
>> +
>> +       "gioui.org/f32"
>> +)
>> +
>> +// 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) (transform f32.Affine2D, segments int) {
>> +       const segmentsPerCircle = 16
>> +       const anglePerSegment = 2 * math.Pi / segmentsPerCircle
>> +
>> +       s := angle / anglePerSegment
>> +       if s < 0 {
>> +               s = -s
>> +       }
>> +       segments = int(math.Ceil(float64(s)))
>> +       if segments <= 0 {
>> +               segments = 1
>> +       }
>> +
>> +       var rx, ry, alpha float64
>> +       if f1 == f2 {
>> +               // degenerate case of a circle.
>> +               rx = dist(f1, p)
>> +               ry = rx
>> +       } else {
>> +               // 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)
>> +               c := dist(f1, f2) * 0.5
>> +               b := math.Sqrt(a*a - c*c)
>> +               switch {
>> +               case a > b:
>> +                       rx = a
>> +                       ry = b
>> +               default:
>> +                       rx = b
>> +                       ry = a
>> +               }
>> +               if f1.X == f2.X {
>> +                       // special case of a "vertical" ellipse.
>> +                       alpha = math.Pi / 2
>> +                       if f1.Y < f2.Y {
>> +                               alpha = -alpha
>> +                       }
>> +               } else {
>> +                       x := float64(f1.X-f2.X) * 0.5
>> +                       if x < 0 {
>> +                               x = -x
>> +                       }
>> +                       alpha = math.Acos(x / c)
>> +               }
>> +       }
>> +
>> +       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
>> +       )
>> +       center := f32.Point{
>> +               X: 0.5 * (f1.X + f2.X),
>> +               Y: 0.5 * (f1.Y + f2.Y),
>> +       }
>> +       ref = ref.Offset(f32.Point{}.Sub(center))
>> +       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{}, 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), segments
>> +}
>> +
>> +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/stroke/stroke.go b/stroke/stroke.go
>> index f1c3756..3ab2362 100644
>> --- a/stroke/stroke.go
>> +++ b/stroke/stroke.go
>> @@ -48,6 +48,7 @@ const (
>>          segOpLineTo
>>          segOpQuadTo
>>          segOpCubeTo
>> +       segOpArcTo
>>   )
>>
>>   // StrokeCap describes the head or tail of a stroked path.
>> @@ -119,6 +120,16 @@ func CubeTo(ctrl0, ctrl1, end f32.Point) Segment {
>>          return s
>>   }
>>
>> +func ArcTo(f1, f2 f32.Point, angle float32) Segment {
>> +       s := Segment{
>> +               op: segOpArcTo,
>> +       }
>> +       s.args[0] = f1
>> +       s.args[1] = f2
>> +       s.args[2].X = angle
>> +       return s
>> +}
>> +
>>   // Op returns a clip operation that approximates stroke.
>>   func (s Stroke) Op(ops *op.Ops) clip.Op {
>>          if len(s.Path.Segments) == 0 {
>> @@ -147,6 +158,16 @@ func (s Stroke) Op(ops *op.Ops) clip.Op {
>>                  case segOpCubeTo:
>>                          contour = append(contour, stroke.Segment{stroke.Point(pen), stroke.Point(seg.args[0]), stroke.Point(seg.args[1]), stroke.Point(seg.args[2])})
>>                          pen = seg.args[2]
>> +               case segOpArcTo:
>> +                       m, nseg := arcTransform(pen, seg.args[0], seg.args[1], seg.args[2].X)
>> +                       for i := 0; i < nseg; i++ {
>> +                               p0 := pen
>> +                               p1 := m.Transform(p0)
>> +                               p2 := m.Transform(p1)
>> +                               ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
>> +                               contour = append(contour, stroke.QuadraticSegment(stroke.Point(pen), stroke.Point(ctl), stroke.Point(p2)))
>> +                               pen = p2
>> +                       }
>>                  }
>>          }
>>          if len(contour) > 0 {
>> --
>> 2.36.1
>>
Sebastien Binet <binet@cern.ch>
Details
Message ID
<CK2ZKN3D0BTQ.3CJL87V0ZPZ4A@clrinfopc42>
In-Reply-To
<d439055f-e143-3330-7573-9a63992892a3@balholm.com> (view parent)
DKIM signature
missing
Download raw message
On Tue May 17, 2022 at 19:39 CET, Andy Balholm wrote:
> I would be fine with adding something like this to
> github.com/andybalholm/stroke.
>
> It would make more sense to use a cubic approximation of the arc
> directly, instead of using a quadratic approximation (which then gets
> converted to cubic segments). With a cubic approximation, you could
> split the circle into 6 or 8 segments instead of 16.

ok.
I'll try to send something along these lines against your repo.
I hope it's fine LICENSE-wise as the arcTransform code originates
from gio (I wrote the first version but Egon improved upon it.)
always unsure about these things.

-s

>
> Andy
>
> On 5/17/22 04:47, Elias Naur wrote:
> > Andy (CC'ed), do you have bandwidth to review this? Does ArcTo belong
> > in https://github.com/andybalholm/stroke?
> >
> > Elias
> >
> > On Fri, 13 May 2022 at 14:33, Sebastien Binet <binet@cern.ch> wrote:
> >> This CL adds ArcTo and a special segment operation to easily create
> >> arc segments.
> >>
> >> Signed-off-by: Sebastien Binet <binet@cern.ch>
> >> ---
> >>   stroke/arc.go    | 106 +++++++++++++++++++++++++++++++++++++++++++++++
> >>   stroke/stroke.go |  21 ++++++++++
> >>   2 files changed, 127 insertions(+)
> >>   create mode 100644 stroke/arc.go
> >>
> >> diff --git a/stroke/arc.go b/stroke/arc.go
> >> new file mode 100644
> >> index 0000000..64bef5d
> >> --- /dev/null
> >> +++ b/stroke/arc.go
> >> @@ -0,0 +1,106 @@
> >> +// SPDX-License-Identifier: Unlicense OR MIT
> >> +
> >> +package stroke
> >> +
> >> +import (
> >> +       "math"
> >> +
> >> +       "gioui.org/f32"
> >> +)
> >> +
> >> +// 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) (transform f32.Affine2D, segments int) {
> >> +       const segmentsPerCircle = 16
> >> +       const anglePerSegment = 2 * math.Pi / segmentsPerCircle
> >> +
> >> +       s := angle / anglePerSegment
> >> +       if s < 0 {
> >> +               s = -s
> >> +       }
> >> +       segments = int(math.Ceil(float64(s)))
> >> +       if segments <= 0 {
> >> +               segments = 1
> >> +       }
> >> +
> >> +       var rx, ry, alpha float64
> >> +       if f1 == f2 {
> >> +               // degenerate case of a circle.
> >> +               rx = dist(f1, p)
> >> +               ry = rx
> >> +       } else {
> >> +               // 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)
> >> +               c := dist(f1, f2) * 0.5
> >> +               b := math.Sqrt(a*a - c*c)
> >> +               switch {
> >> +               case a > b:
> >> +                       rx = a
> >> +                       ry = b
> >> +               default:
> >> +                       rx = b
> >> +                       ry = a
> >> +               }
> >> +               if f1.X == f2.X {
> >> +                       // special case of a "vertical" ellipse.
> >> +                       alpha = math.Pi / 2
> >> +                       if f1.Y < f2.Y {
> >> +                               alpha = -alpha
> >> +                       }
> >> +               } else {
> >> +                       x := float64(f1.X-f2.X) * 0.5
> >> +                       if x < 0 {
> >> +                               x = -x
> >> +                       }
> >> +                       alpha = math.Acos(x / c)
> >> +               }
> >> +       }
> >> +
> >> +       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
> >> +       )
> >> +       center := f32.Point{
> >> +               X: 0.5 * (f1.X + f2.X),
> >> +               Y: 0.5 * (f1.Y + f2.Y),
> >> +       }
> >> +       ref = ref.Offset(f32.Point{}.Sub(center))
> >> +       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{}, 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), segments
> >> +}
> >> +
> >> +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/stroke/stroke.go b/stroke/stroke.go
> >> index f1c3756..3ab2362 100644
> >> --- a/stroke/stroke.go
> >> +++ b/stroke/stroke.go
> >> @@ -48,6 +48,7 @@ const (
> >>          segOpLineTo
> >>          segOpQuadTo
> >>          segOpCubeTo
> >> +       segOpArcTo
> >>   )
> >>
> >>   // StrokeCap describes the head or tail of a stroked path.
> >> @@ -119,6 +120,16 @@ func CubeTo(ctrl0, ctrl1, end f32.Point) Segment {
> >>          return s
> >>   }
> >>
> >> +func ArcTo(f1, f2 f32.Point, angle float32) Segment {
> >> +       s := Segment{
> >> +               op: segOpArcTo,
> >> +       }
> >> +       s.args[0] = f1
> >> +       s.args[1] = f2
> >> +       s.args[2].X = angle
> >> +       return s
> >> +}
> >> +
> >>   // Op returns a clip operation that approximates stroke.
> >>   func (s Stroke) Op(ops *op.Ops) clip.Op {
> >>          if len(s.Path.Segments) == 0 {
> >> @@ -147,6 +158,16 @@ func (s Stroke) Op(ops *op.Ops) clip.Op {
> >>                  case segOpCubeTo:
> >>                          contour = append(contour, stroke.Segment{stroke.Point(pen), stroke.Point(seg.args[0]), stroke.Point(seg.args[1]), stroke.Point(seg.args[2])})
> >>                          pen = seg.args[2]
> >> +               case segOpArcTo:
> >> +                       m, nseg := arcTransform(pen, seg.args[0], seg.args[1], seg.args[2].X)
> >> +                       for i := 0; i < nseg; i++ {
> >> +                               p0 := pen
> >> +                               p1 := m.Transform(p0)
> >> +                               p2 := m.Transform(p1)
> >> +                               ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
> >> +                               contour = append(contour, stroke.QuadraticSegment(stroke.Point(pen), stroke.Point(ctl), stroke.Point(p2)))
> >> +                               pen = p2
> >> +                       }
> >>                  }
> >>          }
> >>          if len(contour) > 0 {
> >> --
> >> 2.36.1
> >>
Details
Message ID
<d4062fe7-bb6a-434e-3004-62fb61a34fa4@balholm.com>
In-Reply-To
<CK2ZKN3D0BTQ.3CJL87V0ZPZ4A@clrinfopc42> (view parent)
DKIM signature
pass
Download raw message
On 5/18/22 08:18, Sebastien Binet wrote:
> ok.
> I'll try to send something along these lines against your repo.
> I hope it's fine LICENSE-wise as the arcTransform code originates
> from gio (I wrote the first version but Egon improved upon it.)
> always unsure about these things.
>
> -s

If it comes from Gio, it should be under the Unlicense, which is 
compatible with anything.

Andy
Reply to thread Export thread (mbox)