From 700cec440e00d6391cd14b6afcd18be1ea61de73 Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Wed, 11 Nov 2020 12:57:17 +0000 Subject: [PATCH] gpu,op/clip: implement stroked paths with round joins Signed-off-by: Sebastien Binet --- gpu/gpu.go | 3 +- gpu/stroke.go | 40 +++++++++++++++++- internal/opconst/ops.go | 2 +- internal/rendertest/clip_test.go | 36 ++++++++++++++-- .../refs/TestStrokedPathRoundRound.png | Bin 0 -> 3409 bytes op/clip/clip.go | 1 + op/clip/stroke.go | 17 +++++++- 7 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 internal/rendertest/refs/TestStrokedPathRoundRound.png diff --git a/gpu/gpu.go b/gpu/gpu.go index 33d2c327..ddc1f6c3 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -162,7 +162,8 @@ func (op *clipOp) decode(data []byte) { bounds: layout.FRect(r), width: math.Float32frombits(bo.Uint32(data[17:])), style: clip.StrokeStyle{ - Cap: clip.StrokeCap(data[21]), + Cap: clip.StrokeCap(data[21]), + Join: clip.StrokeJoin(data[22]), }, } } diff --git a/gpu/stroke.go b/gpu/stroke.go index 2c6ecea4..c64cfa12 100644 --- a/gpu/stroke.go +++ b/gpu/stroke.go @@ -51,7 +51,7 @@ func (qs *strokeQuads) pen() f32.Point { } func (qs *strokeQuads) lineTo(pt f32.Point) { - end := (*qs)[len(*qs)-1].quad.To + end := qs.pen() *qs = append(*qs, strokeQuad{ quad: ops.Quad{ From: end, @@ -273,6 +273,13 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) } +// cosPt returns the cosine of the opening angle between p and q. +func cosPt(p, q f32.Point) float32 { + np := math.Hypot(float64(p.X), float64(p.Y)) + nq := math.Hypot(float64(q.X), float64(q.Y)) + return dotPt(p, q) / float32(np*nq) +} + func normPt(p f32.Point, l float32) f32.Point { d := math.Hypot(float64(p.X), float64(p.Y)) l64 := float64(l) @@ -416,7 +423,14 @@ 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 style sty. func strokePathJoin(sty clip.StrokeStyle, rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { - strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + switch sty.Join { + case clip.BevelJoin: + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + case clip.RoundJoin: + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + default: + panic("impossible") + } } func strokePathBevelJoin(rhs, lhs *strokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { @@ -428,6 +442,28 @@ 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) { + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + cw := dotPt(rot90CW(n0), n1) >= 0.0 + switch { + case cw: + // Path bends to the right, ie. CW (or 180 degree turn). + c := pivot.Sub(lhs.pen()) + angle := -math.Acos(float64(cosPt(n0, n1))) + lhs.arc(c, c, float32(angle)) + lhs.lineTo(lp) // Add a line to accomodate for rounding errors. + rhs.lineTo(rp) + default: + // Path bends to the left, ie. CCW. + angle := math.Acos(float64(cosPt(n0, n1))) + c := pivot.Sub(rhs.pen()) + rhs.arc(c, c, float32(angle)) + rhs.lineTo(rp) // Add a line to accomodate for rounding errors. + lhs.lineTo(lp) + } +} + // strokePathCap caps the provided path qs, according to the provided stroke style sty. func strokePathCap(sty clip.StrokeStyle, qs *strokeQuads, hw float32, pivot, n0 f32.Point) { switch sty.Cap { diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 59b31508..efa6cc38 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -47,7 +47,7 @@ const ( TypePushLen = 1 TypePopLen = 1 TypeAuxLen = 1 - TypeClipLen = 1 + 4*4 + 4 + 1 + TypeClipLen = 1 + 4*4 + 4 + 2 TypeProfileLen = 1 ) diff --git a/internal/rendertest/clip_test.go b/internal/rendertest/clip_test.go index 9a1aef01..a41f1c3d 100644 --- a/internal/rendertest/clip_test.go +++ b/internal/rendertest/clip_test.go @@ -107,7 +107,8 @@ func TestStrokedPathBevelFlat(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 sty := clip.StrokeStyle{ - Cap: clip.FlatCap, + Cap: clip.FlatCap, + Join: clip.BevelJoin, } p := new(clip.Path) @@ -133,7 +134,8 @@ func TestStrokedPathBevelRound(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 sty := clip.StrokeStyle{ - Cap: clip.RoundCap, + Cap: clip.RoundCap, + Join: clip.BevelJoin, } p := new(clip.Path) @@ -159,7 +161,35 @@ func TestStrokedPathBevelSquare(t *testing.T) { run(t, func(o *op.Ops) { const width = 2.5 sty := clip.StrokeStyle{ - Cap: clip.SquareCap, + Cap: clip.SquareCap, + Join: clip.BevelJoin, + } + + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi) + p.Line(f32.Pt(10, 0)) + p.Line(f32.Pt(10, 10)) + p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi) + p.Line(f32.Pt(-20, 0)) + p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30)) + p.Stroke(width, sty).Add(o) + + paint.Fill(o, colornames.Red) + }, func(r result) { + r.expect(0, 0, colornames.White) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathRoundRound(t *testing.T) { + run(t, func(o *op.Ops) { + const width = 2.5 + sty := clip.StrokeStyle{ + Cap: clip.RoundCap, + Join: clip.RoundJoin, } p := new(clip.Path) diff --git a/internal/rendertest/refs/TestStrokedPathRoundRound.png b/internal/rendertest/refs/TestStrokedPathRoundRound.png new file mode 100644 index 0000000000000000000000000000000000000000..da3be5f08847d779921ad4e11d8b5177bf2794a2 GIT binary patch literal 3409 zcmb_fX*iS(7k*|jwy|dI9gICok+PO$gc>2s3}YV(LmK-U8p}wbw^DY4MA<33vW5oL z*lFxr=EW#`dDG|n`ThTXocmno-2cvXU-xzH2bLCBxj0U9006*+zGh^7(n_yNR4eer~t9H7G`V_UE1JQ_P*u zv(BMy-g04KTe;dn`axTH%|Tni$-BIar><=Xi#%{Dtj+<~02hxIsIXPdLY{l1R@Z2g9K3lNaS4{A zG2~()f6L1za{E@9l+_tM1f3qRWYW^I7ios>Ty(D}ZB~kx*_$pdNqm#_DNt~#=}C%F zemFq5-91*m66n;1P*R#%ZWab?JY+Y!z7~&_Q!&pJ2mm)LaT2MyPH%AYdUfRc9%NwD znio1spbre8e9@TK8ga~c5Py}fJB89$4ISURT) zae@x#A!Qum%c4Lz`t~?4iB%l=)~aZplF)!z;)AiUi%IZ90kJqT2B;y>#~l35tmlPQ;M&Rja$C}F{AsL0E~DBNrGXufU$J%uo|&;SeBogIYta+!u}7xT&!AUk|zYShlr9CMK`lBVwJ+ z-xr%e;$Z>%Num`M3GK&mv5=W>Xww(%M~3eq1m6T|+QP243T$yn`|h&Z;rDLl1;WM0 zX^^}729Y}7t|VI)m_P0`pywh~vkrBPU_pUr3MX2>AkqG-@XJTaPR(6EX;RAp7e}w0 z`tU4w<2FyqT69g?3MygMlR#Kgr|+$gTIK6;H8i)R)m3S?P{mO)Bdii zNrPcs3MwCV2?rc-#;dMDcnjOa9URs^_uqQaYSun$ZW4Kq_r-bn z#`u8x`X(=)&$$mGb>-x5TJx{2EVM;W4lm~a7|1xUDGVaq{i&vsaYY)X-myS>qlRlk zoM`%On%bD?c*+tFO6hfxkn1v$0!GITQ6S$L z(-mTENB{F(8ogR#=2$}CLLbdPI%+u;657&OG8Q+&_+6kQHkgU(?MsP$BHigjZi{ZF z2=b6$mo#S=+@A3NdU0b;VrkL8JF&I*x9o%C&y*f?8V�RRg-y=PAJm-tI%i*AxJM z4k3Wgjca0L78kesjL=A~_fmzDr_ss|M)yxiu7I=|FH2gWKkX!YZWV;B>@vU^PybWwAu!XESMf+zmwlC%CEtl%{pD;x=w+YT`gOeKLq zI<|b-WGh+4(3a;Jw8s0C^G6eDivHY=d?Ub(=qS9xZh%5n+l)PjbjVFrJ1bq7Y@bW! zv#^GDP}4?}{z?r#2&`tZ;NQBLa)81QoHsT$>Qe+*Iz%d*6jT`O===&esYGsYKpLoS ze33e49XZo07j}e4qD|URaEd!=O{w(JDxtj%;nxijzuRjfsIh7 zH_%U>QQuKIZfJfuX^(1ekKqwgvHc}#4g$V?9;)|KZSsrCY=Imuuo`-3Ecvxlja3kU z)J)EskV_sOvlmz;X8|3Z@5LEx9UD`W@xPiM7F2QfrAhb;V!)Ku2#ijHhH|s%_@gzn}+7qP^1~~x zPSfff0z`FykCW4?x^LQvJX;h{26_t3(X?6o(HH$b_eOr=cA)9<%8wr?N!@*sJ74k- z^sFu;1HkyVC@VhM*}FF=TASGRe+}tVZ-3|f=8fbM1@C;bSzNP{9ozwEQ|$RMrVk=^ z@WV3sp^PrI6<*@VMZPtv`^Ry*$ufK+;i~>q!TB-GYYfT0X=Qb5dIu=7DGF zh90sXdOCN$9M#%+y@Dgpo_$?(-+FLtc-Y13I;;!{j81g7ggnEwi^JgGZ}z1NMH2Dl z=F-MgYR4+GzV5p^0!rFye|0JAFuXZ>*I7AE3c%)FdoqB{*vS<*bD~n*+eH4Yb{++V zFVrCq{k(lm|T+Qx0wjKVa^2eUkWjO-g>Lt0-Q{KMnZ$Qr&&8(8sy zf4f$W%Dirt&{5LR3QHCUe`9BQ&Mq3k*tVClMfdi_ueL8m;b`%jfkCY4su6)zdlHef4HB&M_fIc8VK+3t1R8n}a>Ook%0MZbOG^seC^Xa1Fn`BjA%vtlKM zwBP5Zx*7F7;<9SW9yHT7n?L@|gr8Iu_xn%HGhp9 zdHv^BO<+7oCS6$)hJY3`QR1w3DC1Hl%zersnuYvX zn^F?0^BX;*`0Im_8dH+sS_i?Gm3ME_hkQ%Q@ES!_f$+)xf1|+O8Y3F&&j-iHBXu}e zK+w}oV+Vgi7b3#s?7r^3U!|OXN^d-Ipp)w#|76XzXbNI=zbPpVp~Pf2czZw2g$D!_ zObncV-O5VT4d?w#A!myCYY{FH5z084VAEL8t*{Kg{uDtIZlI(=|I$jXD%bl18Ks7~ zTBzqsE`W}h^J>-nOm`yHuRgM=63hlQ;F;PSJyYLkGb9~ndwn82>fKRR!T|)p<_f^J kATJ#MCw1(#pQNKdf69g8uhfg~Pn-+@+StOV$`Bv-A9}_=ZvX%Q literal 0 HcmV?d00001 diff --git a/op/clip/clip.go b/op/clip/clip.go index a0430ff2..9b2c27a0 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -54,6 +54,7 @@ func (p Op) Add(o *op.Ops) { bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) bo.PutUint32(data[17:], math.Float32bits(p.width)) data[21] = uint8(p.style.Cap) + data[22] = uint8(p.style.Join) } // Begin the path, storing the path data and final Op into ops. diff --git a/op/clip/stroke.go b/op/clip/stroke.go index b693435f..abbcd4a2 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -3,9 +3,11 @@ package clip // StrokeStyle describes how a stroked path should be drawn. -// StrokeStyle zero value draws a Bevel-joined and Flat-capped stroked path. +// The zero value of StrokeStyle represents bevel-joined and flat-capped +// strokes. type StrokeStyle struct { - Cap StrokeCap + Cap StrokeCap + Join StrokeJoin } // StrokeCap describes the head or tail of a stroked path. @@ -26,3 +28,14 @@ const ( // stroked path's width. RoundCap ) + +// StrokeJoin describes how stroked paths are collated. +type StrokeJoin uint8 + +const ( + // BevelJoin joins path segments with sharp bevels. + BevelJoin StrokeJoin = iota + + // RoundJoin joins path segments with a round segment. + RoundJoin +)