~eliasnaur/gio-patches

op/clip: implement arc path v3 PROPOSED

Sebastien Binet: 1
 op/clip: implement arc path

 3 files changed, 181 insertions(+), 0 deletions(-)
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Friday, July 10, 2020 1:13 PM, Elias Naur <mail@eliasnaur.com> wrote:
> CC: Anthony Starks ajstarks@gmail.com
> 
> Looking through the change I'm wondering whether Path is too low-level for
> for Arc. Two reasons:
> 
> -   I find the SVG API very confusing, in particular largeArc and sweep flags.
agreed.
as I wrote on slack, I sticked to the SVG API b/c I thought you wanted something that looked like it :)
(from the gonum/plot point of view, we only need circular arcs.
I thought that for 'Arc' to be in op/clip.Path, it had to provide the most general operation, i.e.: elliptical arcs.)

largeArc makes sense (I think), but sweepFlag is not so easy to explain (which is a serious hint).
clockwise would make more sense.
the first API I submitted, modeled after the one from gonum/plot/vg.Path is simpler, I think:

package vg // import "gonum.org/v1/plot/vg"

func (p *Path) Arc(pt Point, rad Length, s, a float64)
    Arc adds an arc to the path defined by the center point of the arc's circle,
    the radius of the circle and the start and sweep angles.

if we want elliptical arcs, adding another radius is simple.
if we also want "tilted" elliptical arcs, pushing a rotation before the arc is simple, too.
now, the only issue is the following: what should be done when one adds an arc whose starting point doesn't coincide with the current op/clip.Path.pen position?
(this circles back -I think- to your issue with my first attempt at Path.Arc)



          
          
          
          
Next
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Sunday, July 12, 2020 1:39 PM, Elias Naur <mail@eliasnaur.com> wrote:
Next
May I suggest that, instead of using radians, we use either rotations
[0, 1] or degrees [0, 360] for angles.  Multiplying by 2*math.Pi only
serves to distract from the intent.

On Sun, Jul 12, 2020 at 4:51 PM Sebastien Binet <s@sbinet.org> wrote:
‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐
On Sunday, July 12, 2020 6:25 PM, Gordon Klaus <gordon.klaus@gmail.com> wrote:
Next
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/11531/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH v3] op/clip: implement arc path Export this patch

---
 internal/rendertest/clip_test.go          |  34 +++++
 internal/rendertest/refs/TestPaintArc.png | Bin 0 -> 2756 bytes
 op/clip/clip.go                           | 147 ++++++++++++++++++++++
 3 files changed, 181 insertions(+)
 create mode 100644 internal/rendertest/refs/TestPaintArc.png

diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go
index 6d205f7..d7fa660 100644
--- a/internal/rendertest/clip_test.go
+++ b/internal/rendertest/clip_test.go
@@ -49,6 +49,40 @@ func TestPaintClippedCirle(t *testing.T) {
	})
}

func TestPaintArc(t *testing.T) {
	run(t, func(o *op.Ops) {
		var (
			largeArc  = true
			sweepFlag = true
		)
		p := new(clip.Path)
		p.Begin(o)
		p.Move(f32.Pt(0, 10))
		p.Line(f32.Pt(10, 0))
		p.Arc(20, 30, f32.Pt(30, 0), largeArc, sweepFlag)
		p.Line(f32.Pt(10, 0))
		p.Arc(20, 30, f32.Pt(30, 0), largeArc, !sweepFlag)
		p.Line(f32.Pt(10, 0))
		p.Line(f32.Pt(0, 10))
		p.Arc(30, 20, f32.Pt(0, 30), !largeArc, !sweepFlag)
		p.Line(f32.Pt(0, 10))
		p.Arc(15, 10, f32.Pt(0, 30), !largeArc, sweepFlag)
		p.Line(f32.Pt(0, 10))
		p.Line(f32.Pt(-10, 0))
		p.Arc(20, 30, f32.Pt(-30, 0), !largeArc, !sweepFlag)
		p.Line(f32.Pt(-10, 0))
		p.Arc(20, 30, f32.Pt(-30, 0), !largeArc, sweepFlag)
		p.Line(f32.Pt(-10, 0))
		p.End().Add(o)

		paint.ColorOp{Color: colornames.Red}.Add(o)
		paint.PaintOp{Rect: f32.Rect(0, 0, 128, 128)}.Add(o)
	}, func(r result) {
		r.expect(0, 0, colornames.White)
		r.expect(90, 40, colornames.Red)
	})
}

func TestPaintTexture(t *testing.T) {
	run(t, func(o *op.Ops) {
		squares.Add(o)
diff --git a/internal/rendertest/refs/TestPaintArc.png b/internal/rendertest/refs/TestPaintArc.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0bbb193ad7bb25140fbddd566d78d542bd836c3
GIT binary patch
literal 2756
zcmV;#3On_QP)<h;3K|Lk000e1NJLTq004jh004jp0ssI2OkDPy000V$Nkl<Zc%1BA
zZEO@p7=F8Fi-^Sv7$~;as^x0bSQ{Wv(U4pkQ2VD5trUaeKS_-KM-4SGA;Cm{)Ifn~
z|5!0b1&sv_mkOG;)}*Ahv|dY_h#dU@A+)x=o_6mw!(Qn1?)GDLXJ@AAKAAMV-JSQD
zo9EtnXLsl04nzbG;-H8wz+#6dfM9q62!<zsV0Z!uh9`hvcmfE9CxBph0tkizKA%y#
zc+nNwix-VZ>+4-1)lgq=M9m*)kQ4>??7>(J*&-GrX7xx{ug0ra88as1>QzFa_kFp;
zOaMi}#zw}Rjg7>t9_f)IjOo&G<j4XMKr0gfwzV-MY-^h{n~^ROVaBD0NJO^)++o23
z0MPHpzCH$QeSO4~fdKpVF<`NRefuQwfGnej(aNnpQan!nKJo~B{WU!Nu>EKg4#SQe
z(ASr+teH(gTN_kV*r%|L_I7yk$<%|20;+11D;|d%H(+Fhq~T$bA`y}<T_Pzf3yO=O
zs0a!Rp`ZZnzaQ4FO{C?^b!}Sm04@3b*xG7S=+@T6Z$G(ZOG%}rI5oxi2d0OqDMDw(
z1LASgWQ9U_@E~s4V$x#krI%j9Pd>rUPMn-fF;63amKqw;tr%f)l4QvkT2j)10}gAI
z4hIgH2yp#6o;roEykgX4piL`R;`{I8ufO8-v_?WN003^=hG);3cTZz6a@n?RnJuRu
zY-w??wlkomMOT2KA^h+|%*{0_t;1AShD}X){kkmyv=j{D@#EOve_PB1rl-ll<HyMb
z2A0zg4i9q`V0bvS06ri7`6uq*Z>hA-lcJ#CZ#f8OgwIE!md^*zK1+fR0EtDXPbUr<
z1hn)Z91KDzMB*4XAVOs&w6r8XzVi;e_#%{-XZXH)T?U7^W%dpRhEx^4|K6#Qty{U)
zJ^-QHx-}s{cQ-UNKv@~r0^EY30P%Rj2{<4S;6#){T2cZ%J&>OdH*dQ2OE-j}0NiRH
zfb^aDqof3yn_=nF#7FM6Zzcdw<mdBdjK^W)M&6`pkmh~BvSqv(g*<#tgERu%c_&x8
zufF0+rw<n0P})-&J9aZOuw;p8aoOOXe~cc^q{Gc_s5Bmslf#dTs`g!mGy-tD2^b&e
zL}U%p2*3@EjiO614~8+owE1Hg1GtTnH*ht*FDont@VpU|0U~b146B%Df&TlC2TLqw
zbXLR<P8tumaf1iT&6`GNrSkxuxOPo+3B*7e0Y*o8u;k_OV4;CD0z@J_Sh(G6TZS|O
zxHJe=dPpO{&p(@VQCQ)lk9e?HhBP-~0MOsh<GtAUxNam^>^EY6{K13eo_lmpN!JbK
zT%aF*;6yYrk@>igJAeD*g0z7&HclCgv>6-YG$`n|-*EYIiUB+>jSG^?0|20-gA>u{
zD2$Dz9$eIqA~byV84uM}l@OJeLpVI|xQG}aSrY_+U0trXKV4l3u8Iom@0Yj<5`xb^
zch^4R97TIOR8>JVD)JG9bRGbJy}d5}{Cj&dFm`w2-o5g9fRHv306zZMWTuL3s46j@
zj<PaXxe~g&1%y2rJwQ<~6msR~9|{>!Ys1s)>iBtp-*3rHcnbi4k3Y`-o?vVY%gT(X
z7uW<1S3Lj#-+GJX9PwC;81(r}JRlivOi^03DltVV8r4Y5GKlg3Egd?9@i>FpkH?8A
ztyES#ASJIhN$-OX@TsS4*89$#`01w^C+SHuG*R#D7hgb4jiX^(i16i?uybdkREWCN
zJ|E1^8ktI-raCeLSFVs0iNIffk-u44P*@0lKNJ+WGfkD72c(>BYC=`D`P)}jl0CUp
z)|((MSO@|r3LZRYXA1Gu6v>!u%@e?;006jS2ln((e6yY&k|o930uw-Z5Kh0it`5Ut
zqZUb3$py)MdFuj5y;g)?)N(rrXT11@7vTBlArOH3?t{DTB1fjD;o3DA7=UlSNu7L=
zoZVp%b^$WI6J~O;adu+?6a{i~Vcj}-@IiRs0VpaWT^g-p<J!}@h3NrQpz?r>WmXi}
zy&I~kV8aF|E@nKHVZj7p+leX&o7TggJ$Uvk&djii0W_uOCV-QRod{sCj#tX5?c0eA
zv$HM>;CX;?J-qoQUb*54AWi_E4+8;gZ+9Yqe6J)I76t~Oq5@8xvTeW<fCZ|m;HRJL
zW%UGLgz9P-8nThk6MzvWCg8pIYz*-B1Yn1zCL1IBJOP-Zp@H_M-Fp^ffk^L>Ov-oE
zvX73!>eUpbGby?dh~j1Pkmu4Rs>{75h&A*kALzgnKrlQ3Sc7sgB2NI;C@!YD+!KH)
zYHMNDDykfw08H`v>$F`sPXKm!`DNI&i8hlb03+n)!v6g>)}oMdf`%wwC(EXtJP8jy
zWFwz<C!7S^cf!rh5DePO>UlsCY<NIP3ADGvbI;l4^gJL5v>x!r8*uI%Y~JjMl;Vvx
zx0muxE_U)#o@!XixpPd0rAV`XKnkyrR>+D6y!INr`YQR=Gg^F0n*9SLhc+~E<3=bd
zVmmZ(feTPs3G3IBe>yuM6k=d7Hx~kdxxu;Gws?hwaPPegjAw*N>nY*Dtm<kU9K;J3
zu&xeQtVkSOv4VWOaDkkvuC^Hiva_AH>9lYb067QY+FDZN+iMEw>9lt*xvaL9S^yC<
zWEM_0w4(z`OG*2C_^?U#!-vUbrKQwk6r@`*OA-$V1TY%KygU;l={EE7NX9@wW)Co{
zec>@+^JWZ(Eg8|X84eR8Rs`^3KxzsEj22X~K8U)%Kj8=Rd4Swgl%mnOUDiyE7~9lL
z8IKFf&W3;g)wSRbBz_<8`|q%34Kr(N))0g8K~@3YeHRWLV!7eY_&726_S?)1vVuuV
zEh-3icbi8D8xv`|x?~H&3TYGdlBz;|y(_ZU*BkK;4MBOiE4)uUL5%D1at{C&nxNgg
zp|X-OXJsWZs|Q4PcbaN!<hNs!6_`6oPmiwK+Gv6l#bnKm!9lC-iS4<3gM$p)5Qnf3
zgdckhPMlyS_=yw5paH)9mKm2o3hS^Wld9seV+=gt*fEo(m;U}6b8^gsze5km$st5?
zz?uMvI6I3+k2(_I=uz``c+JfewdrI!adWftx?=*%0%(5l-FKv~Mk_#eHpytZ>{vXG
zyLNFBVAn1-%K<tDjQ|?LVXUd45TK@pWU+$Fm+5NL;e`6jmu=J+bI=Qbh%+;I`ZNZE
zCISS5<TC3Ei<~*bRe&>R?0k3Du=P%O$r5<x83+cUw--7(;Kv`~<jJ|S)zz?lJ8aqn
zk3LGbP4oHlb_!z~=g+gh#i2(QGPx_6nSp4O{Ihha&E4T+G1#&NIy(~|t&B`@xUSRs
z_0ZZXd}}vG0yxLW2&`S3_}}y~`0dLaF03^?4Eg!AC1nd~o`xoG=E-eTmIOP9N+Uq>
zW}e($j`P_;)DY4Lki3~EHxJ;mgQy{-5ny0ov5?M(Gy?qc%VHs&4{3Tqci#<DQ*$3V
zbVC7P*)o`%q|3q@(s;mPg(rYucmfE9CxBo`DS)U^*&-m#SKY8-?tgAKV#%a}AdLV|
zKfPE;=R+C+)~#DCr1K$-00jk$g>*io5rE-9Xvs<E<Np8v0RR64(n~?Rl^`ns0000<
KMNUMnLSTa1lLW~C

literal 0
HcmV?d00001

diff --git a/op/clip/clip.go b/op/clip/clip.go
index 9f6ced4..bf2a710 100644
--- a/op/clip/clip.go
+++ b/op/clip/clip.go
@@ -5,6 +5,7 @@ package clip
import (
	"encoding/binary"
	"image"
	"math"

	"gioui.org/f32"
	"gioui.org/internal/opconst"
@@ -104,6 +105,152 @@ func (p *Path) quadTo(ctrl, to f32.Point) {
	p.pen = to
}

// Arc records an elliptical arc from the current point to end,
// with radii (rx,ry).
// In general, two ellipses satisfy the constraints:
//  - largeArc selects whether the arc should be greater than π;
//  - sweepFlag selects whether the arc should begin moving at
//    positive angles or negative ones.
func (p *Path) Arc(rx, ry float32, end f32.Point, largeArc, sweepFlag bool) {
	xangle := float32(0) // angle of ellipse's minor axis wrt x-axis' window.
	end = end.Add(p.pen)
	rx, ry, c, theta, delta := arcCenter(rx, ry, xangle, p.pen, end, largeArc, sweepFlag)
	p.arc(c, rx, ry, theta, theta+delta)
}

func absF32(x float32) float32 {
	if x < 0 {
		return -x
	}
	return x
}

func arcCenter(rx, ry, xangle float32, p1, p2 f32.Point, largeArc, sweepFlag bool) (float32, float32, f32.Point, float32, float32) {
	rx = absF32(rx)
	ry = absF32(ry)

	var (
		dx = 0.5 * (p1.X - p2.X)
		dy = 0.5 * (p1.Y - p2.Y)

		sin64, cos64 = math.Sincos(float64(xangle))
		sin          = float32(sin64)
		cos          = float32(cos64)

		x1p = +dx*cos + dy*sin
		y1p = -dx*sin + dy*cos

		rx2  = rx * rx
		ry2  = ry * ry
		x1p2 = x1p * x1p
		y1p2 = y1p * y1p

		cr = x1p2/rx2 + y1p2/ry2
	)

	if cr > 1 {
		// scale up rx,ry equally so cr==1
		s := float32(math.Sqrt(float64(cr)))
		rx = s * rx
		ry = s * ry
		rx2 = rx * rx
		ry2 = ry * ry
	}

	dq := rx2*y1p2 + ry2*x1p2
	pq := (rx2*ry2 - dq) / dq
	q := float32(math.Sqrt(float64(pq)))
	if largeArc == sweepFlag {
		q = -q
	}
	cxp := +q * rx * y1p / ry
	cyp := -q * ry * x1p / rx

	cx := cos*cxp - sin*cyp + 0.5*(p1.X+p2.X)
	cy := sin*cxp + cos*cyp + 0.5*(p1.Y+p2.Y)

	angle := func(ux32, uy32, vx32, vy32 float32) float32 {
		var (
			ux = float64(ux32)
			uy = float64(uy32)
			vx = float64(vx32)
			vy = float64(vy32)
		)
		dot := ux*vx + uy*vy
		len := math.Sqrt((ux*ux + uy*uy) * (vx*vx + vy*vy))
		ang := math.Acos(dot / len)
		cross := ux*vy - uy*vx
		if cross < 0 {
			ang = -ang
		}
		return float32(ang)
	}

	theta := angle(1, 0, (x1p-cxp)/rx, (y1p-cyp)/ry)
	delta := angle(
		(+x1p-cxp)/rx, (+y1p-cyp)/ry,
		(-x1p-cxp)/rx, (-y1p-cyp)/ry,
	)
	delta = float32(math.Mod(float64(delta), 2*math.Pi))
	if !sweepFlag {
		delta -= 2 * math.Pi
	}
	return rx, ry, f32.Point{X: cx, Y: cy}, theta, delta
}

// arc records an elliptical arc centered at c, with radii rx and ry,
// starting at angle beg and stopping at end, in radians.
//
// 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://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
func (p *Path) arc(c f32.Point, rx, ry, beg, end float32) {
	var (
		delta = end - beg
		x     = c.X
		y     = c.Y

		a1       = float64(beg)
		a2       = float64(beg)
		sa1, ca1 = math.Sincos(a1)
		sa2, ca2 = sa1, ca1
	)

	// in the paper, the number of steps is automatically derived from
	// the angle and some maximum allowed error.
	// just hardcode 8 for now.
	const (
		n   = 8
		inv = 1. / n
	)

	for i := 0; i < n; i++ {
		p1 := inv * float32(i+0)
		p2 := inv * float32(i+1)
		a1 = float64(beg + delta*p1)
		a2 = float64(beg + delta*p2)
		sa1, ca1 = sa2, ca2
		sa2, ca2 = math.Sincos(a2)

		sa12, ca12 := math.Sincos(0.5 * (a1 + a2))
		x0 := x + rx*float32(ca1)
		y0 := y + ry*float32(sa1)
		x1 := x + rx*float32(ca12)
		y1 := y + ry*float32(sa12)
		x2 := x + rx*float32(ca2)
		y2 := y + ry*float32(sa2)
		cx := 2*x1 - 0.5*(x0+x2)
		cy := 2*y1 - 0.5*(y0+y2)

		p.quadTo(
			f32.Point{X: cx, Y: cy},
			f32.Point{X: x2, Y: y2},
		)
	}
}

// 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) {
-- 
2.27.0
CC: Anthony Starks <ajstarks@gmail.com>

Looking through the change I'm wondering whether Path is too low-level for
for Arc. Two reasons:

- I find the SVG API very confusing, in particular largeArc and sweep flags.
- I don't find Arc obvious replacements for Gio's existing arcs: Loader and
  clip.RRect's rounded corners.

What are the use cases for Arc? Should we add a much simpler clip.Ellipse, similar
to clip.Border?

CC'ed ajstarks for input.

Elias

On Thu Jul 9, 2020 at 18:18, Sebastien Binet wrote: