In the world of programming, particularly in graphics and physics simulations, vector operations are essencial.
Swift’s CGVector
provides a basic framework, and we’re going to enhance it even further. In this article, we'll explore how to extend CGVector
to include essential vector operations, making vector manipulation in Swift more powerful.
Extending CGVector
To extend CGVector
, we will implement Essential Vector Operations. This will allow for cleaner, more efficient code that is easier to read and maintain.
Addition and Subtraction
Addition and subtraction are fundamental. They combine or differentiate two vectors by their corresponding components. The order of vectors matters for subtraction but not for addition.
extension CGVector {
// Add two vectors
static func +(lhs: CGVector, rhs: CGVector) -> CGVector {
return CGVector(dx: lhs.dx + rhs.dx, dy: lhs.dy + rhs.dy)
}
// Add point to the vector
static func +(vector: CGVector, point: CGPoint) -> CGPoint {
return CGPoint(x: point.x + vector.dx, y: point.y + vector.dy)
}
// Subtract two vectors
static func -(lhs: CGVector, rhs: CGVector) -> CGVector {
return CGVector(dx: lhs.dx - rhs.dx, dy: lhs.dy - rhs.dy)
}
// Subtract point from th vector
static func -(vector: CGVector, point: CGPoint) -> CGPoint {
return CGPoint(x: point.x - vector.dx, y: point.y - vector.dy)
}
}
let result = vector1 + vector2 // CGVector + CGVector -> CGVector
let result = vector1 - vector2 // CGVector - CGVector -> CGVector
let result = point + vector // CGPoint + CGVector -> CGPoint
let result = point - vector // CGPoint + CGVector -> CGPoint
Rotation from Origin
In game development, rotating vectors is essential for controlling object. This is achieved mathematically using the 2D rotation matrix, applying trigonometric functions.
In the following Swift implementation, we extend CGVector to rotate a vector by a given angle, showcasing this practical application of trigonometry.
- Positive rotation (a positive angle passed to the function) indicates a counterclockwise rotation.
- Negative rotation (a negative angle passed to the function) indicates a clockwise rotation.
import CoreGraphics
extension CGVector {
// Rotate a vector like a clock hand
func rotated(byDegrees degrees: CGFloat) -> CGVector {
let radians = degrees * .pi / 180
return rotated(byRadians: radians)
}
func rotated(byRadians radians: CGFloat) -> CGVector {
let cosAngle = cos(radians)
let sinAngle = sin(radians)
// Applying the rotation matrix for 2D vectors
return CGVector(dx: dx * cosAngle - dy * sinAngle, dy: dx * sinAngle + dy * cosAngle)
}
}
let originalVector = CGVector(dx: 1.0, dy: 0.0) // Represents a clock hand pointing at 3 o'clock
let rotatedVector = originalVector.rotated(byDegrees: 90) // Rotate by 90 degrees, now pointing at 12 o'clock
Magnitude (Norm) of a Vector
Magnitude or norm represents the vector’s length from the origin. To calculate the magnitude, we use the Pythagorean theorem.
extension CGVector {
// Magnitude of a vector
var magnitude: CGFloat {
return sqrt(dx*dx + dy*dy)
}
}
let mag = vector.magnitude // CGVector -> CGPoint
This operation will become increasingly important in the upcoming topics.
Conditional Normalization
Normalization alters a vector’s magnitude to 1 while maintaining its direction, turning it into a unit vector.
In this operation, there is a mathematical certainty that the resulting vector will never have a magnitude greater than 1 in any direction.
extension CGVector {
// Normalize the vector conditionally
func normalized(to maxLength: CGFloat) -> CGVector {
let length = magnitude
if length > maxLength {
return CGVector(dx: dx / length * maxLength, dy: dy / length * maxLength)
} else {
return self
}
}
}
//Two ways to use
let normVector = vector.normalized(to: 1) // CGVector -> CGVector
let normVector = vector.normalized(to: 20) // CGVector -> CGVector maxLeghnt
A normalized vector is typically used when the direction of the vector is required, as it represents the direction of any vector in the best and most concise way. Therefore, it’s an excellent application for joystick control in managing movement direction.
In the image, we observe two scenarios corresponding to the two options within the implementation of the normalized vector.
The first scenario occurs when the movement keeps the dark circle within the boundaries of the light circle, resulting in the returned vector being unchanged.
In the second scenario, if the vector’s value exceeds the radius of the circle (the value that should be entered in .normalized(to maxLength: CGFloat)
in this case), the vector is then normalized and scaled to match the circle's radius, limiting magnitude.
Multiplication by a Scalar
This operation scales a vector’s magnitude without changing its direction.
extension CGVector {
// Multiply a vector by a scalar
static func *(vector: CGVector, scalar: CGFloat) -> CGVector {
return CGVector(dx: vector.dx * scalar, dy: vector.dy * scalar)
}
}
let scaledVector = vector * scalar
This operation can be used to control the movement speed.
The most appropriate way to do this is by obtaining the direction vector in which the object is moving or will move, normalizing it to have a vector with a maximum magnitude of 1, and then scaling it by the chosen value. This speed value can be controlled as needed.
Dot Product (Scalar Product)
The dot product quantifies the cosine of the angle between two vectors multiplied by their magnitudes.
extension CGVector {
// Dot product of two vectors
static func dotProduct(_ lhs: CGVector, _ rhs: CGVector) -> CGFloat {
return lhs.dx * rhs.dx + lhs.dy * rhs.dy
}
}
let dotProd = CGVector.dotProduct(vector1, vector2)
This operation becomes most valuable when discussing reflection vector and angle between two vectors, which we will soon talk about.
Reflect Vector with the Normal Vector from the Collision
The reflected
method in the CGVector extension is designed for vector reflection, particularly useful with SpriteKit’s collision system.
It calculates the reflected vector by using the dot product of the original vector and a normal vector, adjusting it to reflect off surfaces during collisions.
extension CGVector {
// Reflect Vector in choosed orientations.
func reflected(against normal: CGVector) -> CGVector {
let dotProduct = 2 * (self.dx * normal.dx + self.dy * normal.dy)
return CGVector(dx: self.dx - dotProduct * normal.dx, dy: self.dy - dotProduct * normal.dy)
}
}
let originalVector = CGVector(dx: 3.0, dy: -5.0) //Any vector
let normalVector = CGVector(dx: 0.0, dy: 1.0) // Normal vector, in this case this vector corresponde to the collision on the groud
let reflectedVector = originalVector.reflected(against: normalVector)
Projection of Vectors
Projection quantifies how much of one vector lies in the direction of another. The order is important. It projects the first vector in the second.
extension CGVector {
// Project vector 'a' onto vector 'b'
// The order is important; it projects the first vector in the second.
static func project(_ a: CGVector, onto b: CGVector) -> CGVector {
let scale = dotProduct(a, b) / dotProduct(b, b)
return b * scale
}
}
let projVector = CGVector.project(vectorA, onto: vectorB)
Angle Between Two Vectors
This operation finds the angle in radians between two vectors.
extension CGVector {
// Angle between two vectors
static func angleBetween(_ lhs: CGVector, _ rhs: CGVector) -> CGFloat {
return acos(dotProduct(lhs, rhs) / (lhs.magnitude * rhs.magnitude))
}
}
let angle = CGVector.angleBetween(vector1, vector2)
Conclusion
In summary, we’ve added new features to Swift’s CGVector
to make it better for working with 2D vectors in graphics and simulations.
These improvements make the code easier to use and understand. Although we focused on 2D, these methods can be adapted for 3D by adding one more dimension.
The full list of what we added is at the end of the page.
If you think something is missing or have ideas for more features, please let us know in the comments!
Full Code Extension
extension CGVector {
// Magnitude of a vector
var magnitude: CGFloat {
return sqrt(dx*dx + dy*dy)
}
// Add two vectors
static func +(lhs: CGVector, rhs: CGVector) -> CGVector {
return CGVector(dx: lhs.dx + rhs.dx, dy: lhs.dy + rhs.dy)
}
// Add point to the vector
static func +(vector: CGVector, point: CGPoint) -> CGPoint {
return CGPoint(x: point.x + vector.dx, y: point.y + vector.dy)
}
// Subtract two vectors
static func -(lhs: CGVector, rhs: CGVector) -> CGVector {
return CGVector(dx: lhs.dx - rhs.dx, dy: lhs.dy - rhs.dy)
}
// Subtract point from th vector
static func -(vector: CGVector, point: CGPoint) -> CGPoint {
return CGPoint(x: point.x - vector.dx, y: point.y - vector.dy)
}
// Rotate a vector like a clock hand
func rotated(byDegrees degrees: CGFloat) -> CGVector {
let radians = degrees * .pi / 180
return rotated(byRadians: radians)
}
func rotated(byRadians radians: CGFloat) -> CGVector {
let cosAngle = cos(radians)
let sinAngle = sin(radians)
// Applying the rotation matrix for 2D vectors
return CGVector(dx: dx * cosAngle - dy * sinAngle, dy: dx * sinAngle + dy * cosAngle)
}
// Normalize the vector conditionally
func normalized(to maxLength: CGFloat) -> CGVector {
let length = magnitude
if length > maxLength {
return CGVector(dx: dx / length * maxLength, dy: dy / length * maxLength)
} else {
return self
}
}
// Multiply a vector by a scalar
static func *(vector: CGVector, scalar: CGFloat) -> CGVector {
return CGVector(dx: vector.dx * scalar, dy: vector.dy * scalar)
}
// Dot product of two vectors
static func dotProduct(_ lhs: CGVector, _ rhs: CGVector) -> CGFloat {
return lhs.dx * rhs.dx + lhs.dy * rhs.dy
}
//Reflect vector with reference in the normal surface vector collision
func reflected(against normal: CGVector) -> CGVector {
let dotProduct = 2 * CGVector.dotProduct(self, normal)
return CGVector(dx: self.dx - dotProduct * normal.dx, dy: self.dy - dotProduct * normal.dy)
}
// Project vector 'a' onto vector 'b'
// The order is important; it projects the first vector in the second.
static func project(_ a: CGVector, onto b: CGVector) -> CGVector {
let scale = dotProduct(a, b) / dotProduct(b, b)
return b * scale
}
// Angle between two vectors
static func angleBetween(_ lhs: CGVector, _ rhs: CGVector) -> CGFloat {
return acos(dotProduct(lhs, rhs) / (lhs.magnitude * rhs.magnitude))
}
}