1// Copyright 2018 by the rasterx Authors. All rights reserved.
2//_
3// created: 2/06/2018 by S.R.Wiley
4// Functions that rasterize common shapes easily.
5
6package rasterx
7
8import (
9 "math"
10
11 "golang.org/x/image/math/fixed"
12)
13
14// MaxDx is the Maximum radians a cubic splice is allowed to span
15// in ellipse parametric when approximating an off-axis ellipse.
16const MaxDx float64 = math.Pi / 8
17
18// ToFixedP converts two floats to a fixed point.
19func ToFixedP(x, y float64) (p fixed.Point26_6) {
20 p.X = fixed.Int26_6(x * 64)
21 p.Y = fixed.Int26_6(y * 64)
22 return
23}
24
25// AddCircle adds a circle to the Adder p
26func AddCircle(cx, cy, r float64, p Adder) {
27 AddEllipse(cx, cy, r, r, 0, p)
28}
29
30// AddEllipse adds an elipse with center at cx,cy, with the indicated
31// x and y radius, (rx, ry), rotated around the center by rot degrees.
32func AddEllipse(cx, cy, rx, ry, rot float64, p Adder) {
33 rotRads := rot * math.Pi / 180
34 px, py := Identity.
35 Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).Transform(cx+rx, cy)
36 points := []float64{rx, ry, rot, 1.0, 0.0, px, py}
37 p.Start(ToFixedP(px, py))
38 AddArc(points, cx, cy, px, py, p)
39 p.Stop(true)
40}
41
42// AddRect adds a rectangle of the indicated size, rotated
43// around the center by rot degrees.
44func AddRect(minX, minY, maxX, maxY, rot float64, p Adder) {
45 rot *= math.Pi / 180
46 cx, cy := (minX+maxX)/2, (minY+maxY)/2
47 m := Identity.Translate(cx, cy).Rotate(rot).Translate(-cx, -cy)
48 q := &MatrixAdder{M: m, Adder: p}
49 q.Start(ToFixedP(minX, minY))
50 q.Line(ToFixedP(maxX, minY))
51 q.Line(ToFixedP(maxX, maxY))
52 q.Line(ToFixedP(minX, maxY))
53 q.Stop(true)
54}
55
56// AddRoundRect adds a rectangle of the indicated size, rotated
57// around the center by rot degrees with rounded corners of radius
58// rx in the x axis and ry in the y axis. gf specifes the shape of the
59// filleting function. Valid values are RoundGap, QuadraticGap, CubicGap,
60// FlatGap, or nil which defaults to a flat gap.
61func AddRoundRect(minX, minY, maxX, maxY, rx, ry, rot float64, gf GapFunc, p Adder) {
62 if rx <= 0 || ry <= 0 {
63 AddRect(minX, minY, maxX, maxY, rot, p)
64 return
65 }
66 rot *= math.Pi / 180
67 if gf == nil {
68 gf = FlatGap
69 }
70 w := maxX - minX
71 if w < rx*2 {
72 rx = w / 2
73 }
74 h := maxY - minY
75 if h < ry*2 {
76 ry = h / 2
77 }
78 stretch := rx / ry
79 midY := minY + h/2
80 m := Identity.Translate(minX+w/2, midY).Rotate(rot).Scale(1, 1/stretch).Translate(-minX-w/2, -minY-h/2)
81 maxY = midY + h/2*stretch
82 minY = midY - h/2*stretch
83
84 q := &MatrixAdder{M: m, Adder: p}
85
86 q.Start(ToFixedP(minX+rx, minY))
87 q.Line(ToFixedP(maxX-rx, minY))
88 gf(q, ToFixedP(maxX-rx, minY+rx), ToFixedP(0, -rx), ToFixedP(rx, 0))
89 q.Line(ToFixedP(maxX, maxY-rx))
90 gf(q, ToFixedP(maxX-rx, maxY-rx), ToFixedP(rx, 0), ToFixedP(0, rx))
91 q.Line(ToFixedP(minX+rx, maxY))
92 gf(q, ToFixedP(minX+rx, maxY-rx), ToFixedP(0, rx), ToFixedP(-rx, 0))
93 q.Line(ToFixedP(minX, minY+rx))
94 gf(q, ToFixedP(minX+rx, minY+rx), ToFixedP(-rx, 0), ToFixedP(0, -rx))
95 q.Stop(true)
96}
97
98//AddArc adds an arc to the adder p
99func AddArc(points []float64, cx, cy, px, py float64, p Adder) (lx, ly float64) {
100 rotX := points[2] * math.Pi / 180 // Convert degress to radians
101 largeArc := points[3] != 0
102 sweep := points[4] != 0
103 startAngle := math.Atan2(py-cy, px-cx) - rotX
104 endAngle := math.Atan2(points[6]-cy, points[5]-cx) - rotX
105 deltaTheta := endAngle - startAngle
106 arcBig := math.Abs(deltaTheta) > math.Pi
107
108 // Approximate ellipse using cubic bezeir splines
109 etaStart := math.Atan2(math.Sin(startAngle)/points[1], math.Cos(startAngle)/points[0])
110 etaEnd := math.Atan2(math.Sin(endAngle)/points[1], math.Cos(endAngle)/points[0])
111 deltaEta := etaEnd - etaStart
112 if (arcBig && !largeArc) || (!arcBig && largeArc) { // Go has no boolean XOR
113 if deltaEta < 0 {
114 deltaEta += math.Pi * 2
115 } else {
116 deltaEta -= math.Pi * 2
117 }
118 }
119 // This check might be needed if the center point of the elipse is
120 // at the midpoint of the start and end lines.
121 if deltaEta < 0 && sweep {
122 deltaEta += math.Pi * 2
123 } else if deltaEta >= 0 && !sweep {
124 deltaEta -= math.Pi * 2
125 }
126
127 // Round up to determine number of cubic splines to approximate bezier curve
128 segs := int(math.Abs(deltaEta)/MaxDx) + 1
129 dEta := deltaEta / float64(segs) // span of each segment
130 // Approximate the ellipse using a set of cubic bezier curves by the method of
131 // L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
132 // or cubic Bezier curves", 2003
133 // https://www.spaceroots.org/documents/elllipse/elliptical-arc.pdf
134 tde := math.Tan(dEta / 2)
135 alpha := math.Sin(dEta) * (math.Sqrt(4+3*tde*tde) - 1) / 3 // Math is fun!
136 lx, ly = px, py
137 sinTheta, cosTheta := math.Sin(rotX), math.Cos(rotX)
138 ldx, ldy := ellipsePrime(points[0], points[1], sinTheta, cosTheta, etaStart, cx, cy)
139 for i := 1; i <= segs; i++ {
140 eta := etaStart + dEta*float64(i)
141 var px, py float64
142 if i == segs {
143 px, py = points[5], points[6] // Just makes the end point exact; no roundoff error
144 } else {
145 px, py = ellipsePointAt(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
146 }
147 dx, dy := ellipsePrime(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
148 p.CubeBezier(ToFixedP(lx+alpha*ldx, ly+alpha*ldy),
149 ToFixedP(px-alpha*dx, py-alpha*dy), ToFixedP(px, py))
150 lx, ly, ldx, ldy = px, py, dx, dy
151 }
152 return lx, ly
153}
154
155// ellipsePrime gives tangent vectors for parameterized elipse; a, b, radii, eta parameter, center cx, cy
156func ellipsePrime(a, b, sinTheta, cosTheta, eta, cx, cy float64) (px, py float64) {
157 bCosEta := b * math.Cos(eta)
158 aSinEta := a * math.Sin(eta)
159 px = -aSinEta*cosTheta - bCosEta*sinTheta
160 py = -aSinEta*sinTheta + bCosEta*cosTheta
161 return
162}
163
164// ellipsePointAt gives points for parameterized elipse; a, b, radii, eta parameter, center cx, cy
165func ellipsePointAt(a, b, sinTheta, cosTheta, eta, cx, cy float64) (px, py float64) {
166 aCosEta := a * math.Cos(eta)
167 bSinEta := b * math.Sin(eta)
168 px = cx + aCosEta*cosTheta - bSinEta*sinTheta
169 py = cy + aCosEta*sinTheta + bSinEta*cosTheta
170 return
171}
172
173// FindEllipseCenter locates the center of the Ellipse if it exists. If it does not exist,
174// the radius values will be increased minimally for a solution to be possible
175// while preserving the ra to rb ratio. ra and rb arguments are pointers that can be
176// checked after the call to see if the values changed. This method uses coordinate transformations
177// to reduce the problem to finding the center of a circle that includes the origin
178// and an arbitrary point. The center of the circle is then transformed
179// back to the original coordinates and returned.
180func FindEllipseCenter(ra, rb *float64, rotX, startX, startY, endX, endY float64, sweep, smallArc bool) (cx, cy float64) {
181 cos, sin := math.Cos(rotX), math.Sin(rotX)
182
183 // Move origin to start point
184 nx, ny := endX-startX, endY-startY
185
186 // Rotate ellipse x-axis to coordinate x-axis
187 nx, ny = nx*cos+ny*sin, -nx*sin+ny*cos
188 // Scale X dimension so that ra = rb
189 nx *= *rb / *ra // Now the ellipse is a circle radius rb; therefore foci and center coincide
190
191 midX, midY := nx/2, ny/2
192 midlenSq := midX*midX + midY*midY
193
194 var hr float64
195 if *rb**rb < midlenSq {
196 // Requested ellipse does not exist; scale ra, rb to fit. Length of
197 // span is greater than max width of ellipse, must scale *ra, *rb
198 nrb := math.Sqrt(midlenSq)
199 if *ra == *rb {
200 *ra = nrb // prevents roundoff
201 } else {
202 *ra = *ra * nrb / *rb
203 }
204 *rb = nrb
205 } else {
206 hr = math.Sqrt(*rb**rb-midlenSq) / math.Sqrt(midlenSq)
207 }
208 // Notice that if hr is zero, both answers are the same.
209 if (sweep && smallArc) || (!sweep && !smallArc) {
210 cx = midX + midY*hr
211 cy = midY - midX*hr
212 } else {
213 cx = midX - midY*hr
214 cy = midY + midX*hr
215 }
216
217 // reverse scale
218 cx *= *ra / *rb
219 //Reverse rotate and translate back to original coordinates
220 return cx*cos - cy*sin + startX, cx*sin + cy*cos + startY
221}