This repository was archived by the owner on Apr 2, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathellipse.go
More file actions
210 lines (183 loc) · 5.77 KB
/
Copy pathellipse.go
File metadata and controls
210 lines (183 loc) · 5.77 KB
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
package curve
import (
"iter"
"math"
"slices"
)
type Ellipse struct {
inner Affine
}
var _ ClosedShape = Ellipse{}
// NewEllipse returns a new ellipse with a given center, radii, and rotation.
//
// The returned ellipse will be the result of taking a circle, stretching
// it by the radii along the x and y axes, then rotating it from the
// x axis by xRotation radians, before finally translating the center
// to center.
//
// Rotation is clockwise in a y-down coordinate system. For more on
// rotation, see [Rotate].
func NewEllipse(center Point, radii Vec2, xRotation float64) Ellipse {
rx, ry := radii.Splat()
return newEllipse(Vec2(center), rx, ry, xRotation)
}
// NewEllipseFromRect returns the largest ellipse that can be bounded by the
// provided rectangle.
//
// This uses the absolute width and height of the rectangle.
//
// This ellipse is always axis-aligned; to apply rotation you can call
// [Ellipse.WithRotation] on the result.
func NewEllipseFromRect(rect Rect) Ellipse {
center := Vec2(rect.Center())
width, height := rect.Size().Scale(1.0 / 2.0).Splat()
return newEllipse(center, width, height, 0.0)
}
// NewEllipseFromAffine returns an ellipse from an affine transformation of the unit
// circle.
func NewEllipseFromAffine(aff Affine) Ellipse {
return Ellipse{inner: aff}
}
func NewEllipseFromCircle(c Circle) Ellipse {
return NewEllipse(c.Center, Vec(c.Radius, c.Radius), 0)
}
// WithCenter returns a new ellipse centered on the provided point.
func (e Ellipse) WithCenter(center Point) Ellipse {
return Ellipse{inner: e.inner.WithTranslation(Vec2(center))}
}
// WithRadii returns a new ellipse, with the radii replaced by the argument.
func (e Ellipse) WithRadii(radii Vec2) Ellipse {
rotation := e.inner.svd1()
translation := e.inner.Translation()
return newEllipse(translation, radii.X, radii.Y, rotation)
}
// WithRotation returns a new ellipse, with the rotation replaced by the
// argument.
//
// The rotation is clockwise, for a y-down coordinate system. For more
// on rotation, See [Rotate].
func (e Ellipse) WithRotation(rotation float64) Ellipse {
scale := e.inner.svd0()
translation := e.inner.Translation()
return newEllipse(translation, scale.X, scale.Y, rotation)
}
func newEllipse(center Vec2, scaleX, scaleY, xRotation float64) Ellipse {
// Since the circle is symmetric about the x and y axes, using absolute values for the
// radii results in the same ellipse. For simplicity we make this change here.
return Ellipse{
inner: Translate(center).
Mul(Rotate(xRotation)).
Mul(Scale(math.Abs(scaleX), math.Abs(scaleY))),
}
}
// Contains implements ClosedShape.
func (e Ellipse) Contains(pt Point) bool {
return e.Winding(pt) != 0
}
func (e Ellipse) IsInf() bool {
return e.inner.IsInf()
}
func (e Ellipse) IsNaN() bool {
return e.inner.IsNaN()
}
// Area implements ClosedShape.
func (e Ellipse) Area() float64 {
x, y := e.Radii().Splat()
return math.Pi * x * y
}
// BoundingBox implements ClosedShape.
func (e Ellipse) BoundingBox() Rect {
// Compute a tight bounding box of the ellipse.
//
// See https://www.iquilezles.org/www/articles/ellipses/ellipses.htm. We can get the two
// radius vectors by applying the affine map to the two impulses (1, 0) and (0, 1) which gives
// (a, b) and (c, d) if the affine map is
//
// a | c | e
// -----------
// b | d | f
//
// We can then use the method in the link with the translation to get the bounding box.
aff := e.inner.Coefficients()
a2 := aff[0] * aff[0]
b2 := aff[1] * aff[1]
c2 := aff[2] * aff[2]
d2 := aff[3] * aff[3]
cx := aff[4]
cy := aff[5]
rangeX := math.Sqrt(a2 + c2)
rangeY := math.Sqrt(b2 + d2)
return Rect{
X0: cx - rangeX,
Y0: cy - rangeY,
X1: cx + rangeX,
Y1: cy + rangeY,
}
}
func (e Ellipse) Path(tolerance float64) BezPath { return slices.Collect(e.PathElements(tolerance)) }
// PathElements implements ClosedShape.
func (e Ellipse) PathElements(tolerance float64) iter.Seq[PathElement] {
radii, xRotation := e.inner.svd()
return Arc{
Center: e.Center(),
Radii: radii,
StartAngle: 0.0,
SweepAngle: 2 * math.Pi,
XRotation: xRotation,
}.PathElements(tolerance)
}
// Perimeter implements ClosedShape.
func (e Ellipse) Perimeter(accuracy float64) float64 {
// TODO rather than delegate to the bezier path, it is possible to use various series
// expansions to compute the perimeter to any accuracy. I believe Ramanujan authored the
// quickest to converge. See
// https://www.mathematica-journal.com/2009/11/23/on-the-perimeter-of-an-ellipse/
// and https://en.wikipedia.org/wiki/Ellipse#Circumference
//
return SegmentsPerimeter(Segments(e.PathElements(0.1)), accuracy)
}
// Winding implements ClosedShape.
func (e Ellipse) Winding(pt Point) int {
// Strategy here is to apply the inverse map to the point and see if it is in the unit
// circle.
inv := e.inner.Invert()
if Vec2(pt.Transform(inv)).Hypot2() < 1.0 {
return 1
} else {
return 0
}
}
// Center returns the center of the ellipse.
func (e Ellipse) Center() Point {
return Point(e.inner.Translation())
}
// Radii returns the two radii of the ellipse.
//
// The first number is the horizontal radius and the second is the
// vertical radius, before rotation.
func (e Ellipse) Radii() Vec2 {
radii := e.inner.svd0()
return radii
}
// Rotation returns the ellipse's rotation, in radians.
func (e Ellipse) Rotation() float64 {
rot := e.inner.svd1()
return rot
}
// RadiiRotation returns the radii and the rotation of this ellipse.
//
// This is equivalent to, but more efficiant than, using [Ellipse.Radii] and
// [Ellipse.Rotation].
func (e Ellipse) RadiiRotation() (Vec2, float64) {
return e.inner.svd()
}
func (e Ellipse) Translate(v Vec2) Ellipse {
return Ellipse{
inner: Translate(v).Mul(e.inner),
}
}
func (e Ellipse) Transform(aff Affine) Ellipse {
return Ellipse{
inner: e.inner.Mul(aff),
}
}