forked from gopxl/pixel
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
1,188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
package pixel | ||
|
||
import ( | ||
"fmt" | ||
"math" | ||
) | ||
|
||
// Circle is a 2D circle. It is defined by two properties: | ||
// - Center vector | ||
// - Radius float64 | ||
type Circle struct { | ||
Center Vec | ||
Radius float64 | ||
} | ||
|
||
// C returns a new Circle with the given radius and center coordinates. | ||
// | ||
// Note that a negative radius is valid. | ||
func C(center Vec, radius float64) Circle { | ||
return Circle{ | ||
Center: center, | ||
Radius: radius, | ||
} | ||
} | ||
|
||
// String returns the string representation of the Circle. | ||
// | ||
// c := pixel.C(10.1234, pixel.ZV) | ||
// c.String() // returns "Circle(10.12, Vec(0, 0))" | ||
// fmt.Println(c) // Circle(10.12, Vec(0, 0)) | ||
func (c Circle) String() string { | ||
return fmt.Sprintf("Circle(%s, %.2f)", c.Center, c.Radius) | ||
} | ||
|
||
// Norm returns the Circle in normalized form - this sets the radius to its absolute value. | ||
// | ||
// c := pixel.C(-10, pixel.ZV) | ||
// c.Norm() // returns pixel.Circle{pixel.Vec{0, 0}, 10} | ||
func (c Circle) Norm() Circle { | ||
return Circle{ | ||
Center: c.Center, | ||
Radius: math.Abs(c.Radius), | ||
} | ||
} | ||
|
||
// Area returns the area of the Circle. | ||
func (c Circle) Area() float64 { | ||
return math.Pi * math.Pow(c.Radius, 2) | ||
} | ||
|
||
// Moved returns the Circle moved by the given vector delta. | ||
func (c Circle) Moved(delta Vec) Circle { | ||
return Circle{ | ||
Center: c.Center.Add(delta), | ||
Radius: c.Radius, | ||
} | ||
} | ||
|
||
// Resized returns the Circle resized by the given delta. The Circles center is use as the anchor. | ||
// | ||
// c := pixel.C(pixel.ZV, 10) | ||
// c.Resized(-5) // returns pixel.Circle{pixel.Vec{0, 0}, 5} | ||
// c.Resized(25) // returns pixel.Circle{pixel.Vec{0, 0}, 35} | ||
func (c Circle) Resized(radiusDelta float64) Circle { | ||
return Circle{ | ||
Center: c.Center, | ||
Radius: c.Radius + radiusDelta, | ||
} | ||
} | ||
|
||
// Contains checks whether a vector `u` is contained within this Circle (including it's perimeter). | ||
func (c Circle) Contains(u Vec) bool { | ||
toCenter := c.Center.To(u) | ||
return c.Radius >= toCenter.Len() | ||
} | ||
|
||
// Formula returns the values of h and k, for the equation of the circle: (x-h)^2 + (y-k)^2 = r^2 | ||
// where r is the radius of the circle. | ||
func (c Circle) Formula() (h, k float64) { | ||
return c.Center.X, c.Center.Y | ||
} | ||
|
||
// maxCircle will return the larger circle based on the radius. | ||
func maxCircle(c, d Circle) Circle { | ||
if c.Radius < d.Radius { | ||
return d | ||
} | ||
return c | ||
} | ||
|
||
// minCircle will return the smaller circle based on the radius. | ||
func minCircle(c, d Circle) Circle { | ||
if c.Radius < d.Radius { | ||
return c | ||
} | ||
return d | ||
} | ||
|
||
// Union returns the minimal Circle which covers both `c` and `d`. | ||
func (c Circle) Union(d Circle) Circle { | ||
biggerC := maxCircle(c.Norm(), d.Norm()) | ||
smallerC := minCircle(c.Norm(), d.Norm()) | ||
|
||
// Get distance between centers | ||
dist := c.Center.To(d.Center).Len() | ||
|
||
// If the bigger Circle encompasses the smaller one, we have the result | ||
if dist+smallerC.Radius <= biggerC.Radius { | ||
return biggerC | ||
} | ||
|
||
// Calculate radius for encompassing Circle | ||
r := (dist + biggerC.Radius + smallerC.Radius) / 2 | ||
|
||
// Calculate center for encompassing Circle | ||
theta := .5 + (biggerC.Radius-smallerC.Radius)/(2*dist) | ||
center := Lerp(smallerC.Center, biggerC.Center, theta) | ||
|
||
return Circle{ | ||
Center: center, | ||
Radius: r, | ||
} | ||
} | ||
|
||
// Intersect returns the maximal Circle which is covered by both `c` and `d`. | ||
// | ||
// If `c` and `d` don't overlap, this function returns a zero-sized circle at the centerpoint between the two Circle's | ||
// centers. | ||
func (c Circle) Intersect(d Circle) Circle { | ||
// Check if one of the circles encompasses the other; if so, return that one | ||
biggerC := maxCircle(c.Norm(), d.Norm()) | ||
smallerC := minCircle(c.Norm(), d.Norm()) | ||
|
||
if biggerC.Radius >= biggerC.Center.To(smallerC.Center).Len()+smallerC.Radius { | ||
return biggerC | ||
} | ||
|
||
// Calculate the midpoint between the two radii | ||
// Distance between centers | ||
dist := c.Center.To(d.Center).Len() | ||
// Difference between radii | ||
diff := dist - (c.Radius + d.Radius) | ||
// Distance from c.Center to the weighted midpoint | ||
distToMidpoint := c.Radius + 0.5*diff | ||
// Weighted midpoint | ||
center := Lerp(c.Center, d.Center, distToMidpoint/dist) | ||
|
||
// No need to calculate radius if the circles do not overlap | ||
if c.Center.To(d.Center).Len() >= c.Radius+d.Radius { | ||
return C(center, 0) | ||
} | ||
|
||
radius := c.Center.To(d.Center).Len() - (c.Radius + d.Radius) | ||
|
||
return Circle{ | ||
Center: center, | ||
Radius: math.Abs(radius), | ||
} | ||
} | ||
|
||
// IntersectLine will return the shortest Vec such that if the Circle is moved by the Vec returned, the Line and Rect no | ||
// longer intersect. | ||
func (c Circle) IntersectLine(l Line) Vec { | ||
return l.IntersectCircle(c).Scaled(-1) | ||
} | ||
|
||
// IntersectRect returns a minimal required Vector, such that moving the circle by that vector would stop the Circle | ||
// and the Rect intersecting. This function returns a zero-vector if the Circle and Rect do not overlap, and if only | ||
// the perimeters touch. | ||
// | ||
// This function will return a non-zero vector if: | ||
// - The Rect contains the Circle, partially or fully | ||
// - The Circle contains the Rect, partially of fully | ||
func (c Circle) IntersectRect(r Rect) Vec { | ||
// Checks if the c.Center is not in the diagonal quadrants of the rectangle | ||
if (r.Min.X <= c.Center.X && c.Center.X <= r.Max.X) || (r.Min.Y <= c.Center.Y && c.Center.Y <= r.Max.Y) { | ||
// 'grow' the Rect by c.Radius in each orthagonal | ||
grown := Rect{Min: r.Min.Sub(V(c.Radius, c.Radius)), Max: r.Max.Add(V(c.Radius, c.Radius))} | ||
if !grown.Contains(c.Center) { | ||
// c.Center not close enough to overlap, return zero-vector | ||
return ZV | ||
} | ||
|
||
// Get minimum distance to travel out of Rect | ||
rToC := r.Center().To(c.Center) | ||
h := c.Radius - math.Abs(rToC.X) + (r.W() / 2) | ||
v := c.Radius - math.Abs(rToC.Y) + (r.H() / 2) | ||
|
||
if rToC.X < 0 { | ||
h = -h | ||
} | ||
if rToC.Y < 0 { | ||
v = -v | ||
} | ||
|
||
// No intersect | ||
if h == 0 && v == 0 { | ||
return ZV | ||
} | ||
|
||
if math.Abs(h) > math.Abs(v) { | ||
// Vertical distance shorter | ||
return V(0, v) | ||
} | ||
return V(h, 0) | ||
} else { | ||
// The center is in the diagonal quadrants | ||
|
||
// Helper points to make code below easy to read. | ||
rectTopLeft := V(r.Min.X, r.Max.Y) | ||
rectBottomRight := V(r.Max.X, r.Min.Y) | ||
|
||
// Check for overlap. | ||
if !(c.Contains(r.Min) || c.Contains(r.Max) || c.Contains(rectTopLeft) || c.Contains(rectBottomRight)) { | ||
// No overlap. | ||
return ZV | ||
} | ||
|
||
var centerToCorner Vec | ||
if c.Center.To(r.Min).Len() <= c.Radius { | ||
// Closest to bottom-left | ||
centerToCorner = c.Center.To(r.Min) | ||
} | ||
if c.Center.To(r.Max).Len() <= c.Radius { | ||
// Closest to top-right | ||
centerToCorner = c.Center.To(r.Max) | ||
} | ||
if c.Center.To(rectTopLeft).Len() <= c.Radius { | ||
// Closest to top-left | ||
centerToCorner = c.Center.To(rectTopLeft) | ||
} | ||
if c.Center.To(rectBottomRight).Len() <= c.Radius { | ||
// Closest to bottom-right | ||
centerToCorner = c.Center.To(rectBottomRight) | ||
} | ||
|
||
cornerToCircumferenceLen := c.Radius - centerToCorner.Len() | ||
|
||
return centerToCorner.Unit().Scaled(cornerToCircumferenceLen) | ||
} | ||
} | ||
|
||
// IntersectionPoints returns all the points where the Circle intersects with the line provided. This can be zero, one or | ||
// two points, depending on the location of the shapes. The points of intersection will be returned in order of | ||
// closest-to-l.A to closest-to-l.B. | ||
func (c Circle) IntersectionPoints(l Line) []Vec { | ||
cContainsA := c.Contains(l.A) | ||
cContainsB := c.Contains(l.B) | ||
|
||
// Special case for both endpoint being contained within the circle | ||
if cContainsA && cContainsB { | ||
return []Vec{} | ||
} | ||
|
||
// Get closest point on the line to this circles' center | ||
closestToCenter := l.Closest(c.Center) | ||
|
||
// If the distance to the closest point is greater than the radius, there are no points of intersection | ||
if closestToCenter.To(c.Center).Len() > c.Radius { | ||
return []Vec{} | ||
} | ||
|
||
// If the distance to the closest point is equal to the radius, the line is tangent and the closest point is the | ||
// point at which it touches the circle. | ||
if closestToCenter.To(c.Center).Len() == c.Radius { | ||
return []Vec{closestToCenter} | ||
} | ||
|
||
// Special case for endpoint being on the circles' center | ||
if c.Center == l.A || c.Center == l.B { | ||
otherEnd := l.B | ||
if c.Center == l.B { | ||
otherEnd = l.A | ||
} | ||
intersect := c.Center.Add(c.Center.To(otherEnd).Unit().Scaled(c.Radius)) | ||
return []Vec{intersect} | ||
} | ||
|
||
// This means the distance to the closest point is less than the radius, so there is at least one intersection, | ||
// possibly two. | ||
|
||
// If one of the end points exists within the circle, there is only one intersection | ||
if cContainsA || cContainsB { | ||
containedPoint := l.A | ||
otherEnd := l.B | ||
if cContainsB { | ||
containedPoint = l.B | ||
otherEnd = l.A | ||
} | ||
|
||
// Use trigonometry to get the length of the line between the contained point and the intersection point. | ||
// The following is used to describe the triangle formed: | ||
// - a is the side between contained point and circle center | ||
// - b is the side between the center and the intersection point (radius) | ||
// - c is the side between the contained point and the intersection point | ||
// The captials of these letters are used as the angles opposite the respective sides. | ||
// a and b are known | ||
a := containedPoint.To(c.Center).Len() | ||
b := c.Radius | ||
// B can be calculated by subtracting the angle of b (to the x-axis) from the angle of c (to the x-axis) | ||
B := containedPoint.To(c.Center).Angle() - containedPoint.To(otherEnd).Angle() | ||
// Using the Sin rule we can get A | ||
A := math.Asin((a * math.Sin(B)) / b) | ||
// Using the rule that there are 180 degrees (or Pi radians) in a triangle, we can now get C | ||
C := math.Pi - A + B | ||
// If C is zero, the line segment is in-line with the center-intersect line. | ||
var c float64 | ||
if C == 0 { | ||
c = b - a | ||
} else { | ||
// Using the Sine rule again, we can now get c | ||
c = (a * math.Sin(C)) / math.Sin(A) | ||
} | ||
// Travelling from the contained point to the other end by length of a will provide the intersection point. | ||
return []Vec{ | ||
containedPoint.Add(containedPoint.To(otherEnd).Unit().Scaled(c)), | ||
} | ||
} | ||
|
||
// Otherwise the endpoints exist outside of the circle, and the line segment intersects in two locations. | ||
// The vector formed by going from the closest point to the center of the circle will be perpendicular to the line; | ||
// this forms a right-angled triangle with the intersection points, with the radius as the hypotenuse. | ||
// Calculate the other triangles' sides' length. | ||
a := math.Sqrt(math.Pow(c.Radius, 2) - math.Pow(closestToCenter.To(c.Center).Len(), 2)) | ||
|
||
// Travelling in both directions from the closest point by length of a will provide the two intersection points. | ||
first := closestToCenter.Add(closestToCenter.To(l.A).Unit().Scaled(a)) | ||
second := closestToCenter.Add(closestToCenter.To(l.B).Unit().Scaled(a)) | ||
|
||
if first.To(l.A).Len() < second.To(l.A).Len() { | ||
return []Vec{first, second} | ||
} | ||
return []Vec{second, first} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package pixel | ||
|
||
// Clamp returns x clamped to the interval [min, max]. | ||
// | ||
// If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is | ||
// returned. | ||
func Clamp(x, min, max float64) float64 { | ||
if x < min { | ||
return min | ||
} | ||
if x > max { | ||
return max | ||
} | ||
return x | ||
} |
Oops, something went wrong.