package pixel import ( "fmt" "math" ) // Vec is a 2D vector type with X and Y coordinates. // // Create vectors with the V constructor: // // u := pixel.V(1, 2) // v := pixel.V(8, -3) // // Use various methods to manipulate them: // // w := u.Add(v) // fmt.Println(w) // Vec(9, -1) // fmt.Println(u.Sub(v)) // Vec(-7, 5) // u = pixel.V(2, 3) // v = pixel.V(8, 1) // if u.X < 0 { // fmt.Println("this won't happen") // } // x := u.Unit().Dot(v.Unit()) type Vec struct { X, Y float64 } // ZV is a zero vector. var ZV = Vec{0, 0} // V returns a new 2D vector with the given coordinates. func V(x, y float64) Vec { return Vec{x, y} } // nearlyEqual compares two float64s and returns whether they are equal, accounting for rounding errors.At worst, the // result is correct to 7 significant digits. func nearlyEqual(a, b float64) bool { epsilon := 0.000001 if a == b { return true } diff := math.Abs(a - b) if a == 0.0 || b == 0.0 || diff < math.SmallestNonzeroFloat64 { return diff < (epsilon * math.SmallestNonzeroFloat64) } absA := math.Abs(a) absB := math.Abs(b) return diff/math.Min(absA+absB, math.MaxFloat64) < epsilon } // Eq will compare two vectors and return whether they are equal accounting for rounding errors. At worst, the result // is correct to 7 significant digits. func (u Vec) Eq(v Vec) bool { return nearlyEqual(u.X, v.X) && nearlyEqual(u.Y, v.Y) } // Unit returns a vector of length 1 facing the given angle. func Unit(angle float64) Vec { return Vec{1, 0}.Rotated(angle) } // String returns the string representation of the vector u. // // u := pixel.V(4.5, -1.3) // u.String() // returns "Vec(4.5, -1.3)" // fmt.Println(u) // Vec(4.5, -1.3) func (u Vec) String() string { return fmt.Sprintf("Vec(%v, %v)", u.X, u.Y) } // XY returns the components of the vector in two return values. func (u Vec) XY() (x, y float64) { return u.X, u.Y } // Add returns the sum of vectors u and v. func (u Vec) Add(v Vec) Vec { return Vec{ u.X + v.X, u.Y + v.Y, } } // Sub returns the difference betweeen vectors u and v. func (u Vec) Sub(v Vec) Vec { return Vec{ u.X - v.X, u.Y - v.Y, } } // Floor converts x and y to their integer equivalents. func (u Vec) Floor() Vec { return Vec{ math.Floor(u.X), math.Floor(u.Y), } } // To returns the vector from u to v. Equivalent to v.Sub(u). func (u Vec) To(v Vec) Vec { return Vec{ v.X - u.X, v.Y - u.Y, } } // Scaled returns the vector u multiplied by c. func (u Vec) Scaled(c float64) Vec { return Vec{u.X * c, u.Y * c} } // ScaledXY returns the vector u multiplied by the vector v component-wise. func (u Vec) ScaledXY(v Vec) Vec { return Vec{u.X * v.X, u.Y * v.Y} } // Len returns the length of the vector u. func (u Vec) Len() float64 { return math.Hypot(u.X, u.Y) } // Angle returns the angle between the vector u and the x-axis. The result is in range [-Pi, Pi]. func (u Vec) Angle() float64 { return math.Atan2(u.Y, u.X) } // Unit returns a vector of length 1 facing the direction of u (has the same angle). func (u Vec) Unit() Vec { if u.X == 0 && u.Y == 0 { return Vec{1, 0} } return u.Scaled(1 / u.Len()) } // Rotated returns the vector u rotated by the given angle in radians. func (u Vec) Rotated(angle float64) Vec { sin, cos := math.Sincos(angle) return Vec{ u.X*cos - u.Y*sin, u.X*sin + u.Y*cos, } } // Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster. func (u Vec) Normal() Vec { return Vec{-u.Y, u.X} } // Dot returns the dot product of vectors u and v. func (u Vec) Dot(v Vec) float64 { return u.X*v.X + u.Y*v.Y } // Cross return the cross product of vectors u and v. func (u Vec) Cross(v Vec) float64 { return u.X*v.Y - v.X*u.Y } // Project returns a projection (or component) of vector u in the direction of vector v. // // Behaviour is undefined if v is a zero vector. func (u Vec) Project(v Vec) Vec { len := u.Dot(v) / v.Len() return v.Unit().Scaled(len) } // Map applies the function f to both x and y components of the vector u and returns the modified // vector. // // u := pixel.V(10.5, -1.5) // v := u.Map(math.Floor) // v is Vec(10, -2), both components of u floored func (u Vec) Map(f func(float64) float64) Vec { return Vec{ f(u.X), f(u.Y), } } // Lerp returns a linear interpolation between vectors a and b. // // This function basically returns a point along the line between a and b and t chooses which one. // If t is 0, then a will be returned, if t is 1, b will be returned. Anything between 0 and 1 will // return the appropriate point between a and b and so on. func Lerp(a, b Vec, t float64) Vec { return a.Scaled(1 - t).Add(b.Scaled(t)) } // Line is a 2D line segment, between points A and B. type Line struct { A, B Vec } // L creates and returns a new Line. func L(from, to Vec) Line { return Line{ A: from, B: to, } } // Bounds returns the lines bounding box. This is in the form of a normalized Rect. func (l Line) Bounds() Rect { return R(l.A.X, l.A.Y, l.B.X, l.B.Y).Norm() } // Center will return the point at center of the line; that is, the point equidistant from either end. func (l Line) Center() Vec { return l.A.Add(l.A.To(l.B).Scaled(0.5)) } // Closest will return the point on the line which is closest to the Vec provided. func (l Line) Closest(v Vec) Vec { // between is a helper function which determines whether x is greater than min(a, b) and less than max(a, b) between := func(a, b, x float64) bool { min := math.Min(a, b) max := math.Max(a, b) return min < x && x < max } // Closest point will be on a line which perpendicular to this line. // If and only if the infinite perpendicular line intersects the segment. m, b := l.Formula() // Account for horizontal lines if m == 0 { x := v.X y := l.A.Y // check if the X coordinate of v is on the line if between(l.A.X, l.B.X, v.X) { return V(x, y) } // Otherwise get the closest endpoint if l.A.To(v).Len() < l.B.To(v).Len() { return l.A } return l.B } // Account for vertical lines if math.IsInf(math.Abs(m), 1) { x := l.A.X y := v.Y // check if the Y coordinate of v is on the line if between(l.A.Y, l.B.Y, v.Y) { return V(x, y) } // Otherwise get the closest endpoint if l.A.To(v).Len() < l.B.To(v).Len() { return l.A } return l.B } perpendicularM := -1 / m perpendicularB := v.Y - (perpendicularM * v.X) // Coordinates of intersect (of infinite lines) x := (perpendicularB - b) / (m - perpendicularM) y := m*x + b // Check if the point lies between the x and y bounds of the segment if !between(l.A.X, l.B.X, x) && !between(l.A.Y, l.B.Y, y) { // Not within bounding box toStart := v.To(l.A) toEnd := v.To(l.B) if toStart.Len() < toEnd.Len() { return l.A } return l.B } return V(x, y) } // Contains returns whether the provided Vec lies on the line. func (l Line) Contains(v Vec) bool { return l.Closest(v).Eq(v) } // Formula will return the values that represent the line in the formula: y = mx + b // This function will return math.Inf+, math.Inf- for a vertical line. func (l Line) Formula() (m, b float64) { // Account for horizontal lines if l.B.Y == l.A.Y { return 0, l.A.Y } m = (l.B.Y - l.A.Y) / (l.B.X - l.A.X) b = l.A.Y - (m * l.A.X) return m, b } // Intersect will return the point of intersection for the two line segments. If the line segments do not intersect, // this function will return the zero-vector and false. func (l Line) Intersect(k Line) (Vec, bool) { // Check if the lines are parallel lDir := l.A.To(l.B) kDir := k.A.To(k.B) if lDir.X == kDir.X && lDir.Y == kDir.Y { return ZV, false } // The lines intersect - but potentially not within the line segments. // Get the intersection point for the lines if they were infinitely long, check if the point exists on both of the // segments lm, lb := l.Formula() km, kb := k.Formula() // Account for vertical lines if math.IsInf(math.Abs(lm), 1) && math.IsInf(math.Abs(km), 1) { // Both vertical, therefore parallel return ZV, false } var x, y float64 if math.IsInf(math.Abs(lm), 1) || math.IsInf(math.Abs(km), 1) { // One line is vertical intersectM := lm intersectB := lb verticalLine := k if math.IsInf(math.Abs(lm), 1) { intersectM = km intersectB = kb verticalLine = l } y = intersectM*verticalLine.A.X + intersectB x = verticalLine.A.X } else { // Coordinates of intersect x = (kb - lb) / (lm - km) y = lm*x + lb } if l.Contains(V(x, y)) && k.Contains(V(x, y)) { // The intersect point is on both line segments, they intersect. return V(x, y), true } return ZV, false } // IntersectCircle will return the shortest Vec such that moving the Line by that Vec will cause the Line and Circle // to no longer intesect. If they do not intersect at all, this function will return a zero-vector. func (l Line) IntersectCircle(c Circle) Vec { // Get the point on the line closest to the center of the circle. closest := l.Closest(c.Center) cirToClosest := c.Center.To(closest) if cirToClosest.Len() >= c.Radius { return ZV } return cirToClosest.Scaled(cirToClosest.Len() - c.Radius) } // IntersectRect will return the shortest Vec such that moving the Line by that Vec will cause the Line and Rect to // no longer intesect. If they do not intersect at all, this function will return a zero-vector. func (l Line) IntersectRect(r Rect) Vec { // Check if either end of the line segment are within the rectangle if r.Contains(l.A) || r.Contains(l.B) { // Use the Rect.Intersect to get minimal return value rIntersect := l.Bounds().Intersect(r) if rIntersect.H() > rIntersect.W() { // Go vertical return V(0, rIntersect.H()) } return V(rIntersect.W(), 0) } // Check if any of the rectangles' edges intersect with this line. for _, edge := range r.Edges() { if _, ok := l.Intersect(edge); ok { // Get the closest points on the line to each corner, where: // - the point is contained by the rectangle // - the point is not the corner itself corners := r.Vertices() var closest *Vec closestCorner := corners[0] for _, c := range corners { cc := l.Closest(c) if closest == nil || (closest.Len() > cc.Len() && r.Contains(cc)) { closest = &cc closestCorner = c } } return closest.To(closestCorner) } } // No intersect return ZV } // Len returns the length of the line segment. func (l Line) Len() float64 { return l.A.To(l.B).Len() } // Moved will return a line moved by the delta Vec provided. func (l Line) Moved(delta Vec) Line { return Line{ A: l.A.Add(delta), B: l.B.Add(delta), } } // Rotated will rotate the line around the provided Vec. func (l Line) Rotated(around Vec, angle float64) Line { // Move the line so we can use `Vec.Rotated` lineShifted := l.Moved(around.Scaled(-1)) lineRotated := Line{ A: lineShifted.A.Rotated(angle), B: lineShifted.B.Rotated(angle), } return lineRotated.Moved(around) } // Scaled will return the line scaled around the center point. func (l Line) Scaled(scale float64) Line { return l.ScaledXY(l.Center(), scale) } // ScaledXY will return the line scaled around the Vec provided. func (l Line) ScaledXY(around Vec, scale float64) Line { toA := around.To(l.A).Scaled(scale) toB := around.To(l.B).Scaled(scale) return Line{ A: around.Add(toA), B: around.Add(toB), } } func (l Line) String() string { return fmt.Sprintf("Line(%v, %v)", l.A, l.B) }