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