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} }