diff --git a/Sources/PlaydateKit/Geometry/AffineTransform.swift b/Sources/PlaydateKit/Geometry/AffineTransform.swift new file mode 100644 index 00000000..50232757 --- /dev/null +++ b/Sources/PlaydateKit/Geometry/AffineTransform.swift @@ -0,0 +1,198 @@ +// MARK: - AffineTransformable + +public protocol AffineTransformable { + mutating func transform(by transform: AffineTransform) +} + +public extension AffineTransformable { + func transformed(by transform: AffineTransform) -> Self { + var result = self + result.transform(by: transform) + return result + } + + mutating func translateBy(dx: Float, dy: Float) { + transform(by: .init(translationX: dx, y: dy)) + } + + func translatedBy(dx: Float, dy: Float) -> Self { + transformed(by: .init(translationX: dx, y: dy)) + } + + /// - Parameter angle: The rotation angle in degrees. + mutating func rotateBy(angle: Float) { + transform(by: .init(rotationAngle: angle)) + } + + /// - Parameter angle: The rotation angle in degrees. + func rotatedBy(angle: Float) -> Self { + transformed(by: .init(rotationAngle: angle)) + } + + mutating func scaleBy(x: Float, y: Float) { + transform(by: .init(scaleX: x, y: y)) + } + + func scaledBy(x: Float, y: Float) -> Self { + transformed(by: .init(scaleX: x, y: y)) + } +} + +// MARK: - AffineTransform + +public struct AffineTransform: Equatable { + // MARK: Lifecycle + + /// Returns an affine transformation matrix constructed from translation values you provide. + public init(translationX x: Float, y: Float) { + m11 = 1 + m12 = 0 + m21 = 0 + m22 = 1 + tx = x + ty = y + } + + /// Returns an affine transformation matrix constructed from a rotation value you provide. + /// - Parameter rotationAngle: The rotation angle in degrees. + public init(rotationAngle: Float) { + let rotationAngleRadians = rotationAngle * Float.pi / 180 + m11 = cosf(rotationAngleRadians) + m12 = -sinf(rotationAngleRadians) + m21 = sinf(rotationAngleRadians) + m22 = cosf(rotationAngleRadians) + tx = 0 + ty = 0 + } + + /// Returns an affine transformation matrix constructed from scaling values you provide. + public init(scaleX x: Float, y: Float) { + m11 = x + m12 = 0 + m21 = 0 + m22 = y + tx = 0 + ty = 0 + } + + public init(m11: Float, m12: Float, m21: Float, m22: Float, tx: Float, ty: Float) { + self.m11 = m11 + self.m12 = m12 + self.m21 = m21 + self.m22 = m22 + self.tx = tx + self.ty = ty + } + + // MARK: Public + + /// The identity transform. + public nonisolated(unsafe) static let identity = AffineTransform(m11: 1, m12: 0, m21: 0, m22: 1, tx: 0, ty: 0) + + /// The entry at position [1,1] in the matrix. + public var m11: Float + /// The entry at position [1,2] in the matrix. + public var m12: Float + /// The entry at position [2,1] in the matrix. + public var m21: Float + /// The entry at position [2,2] in the matrix. + public var m22: Float + /// The entry at position [3,1] in the matrix. + public var tx: Float + /// The entry at position [3,2] in the matrix. + public var ty: Float + + /// Returns an affine transformation matrix constructed by combining two existing affine transforms. + public func concatenating(_ transform: AffineTransform) -> AffineTransform { + AffineTransform( + m11: m11 * transform.m11 + m12 * transform.m21, + m12: m11 * transform.m12 + m12 * transform.m22, + m21: m21 * transform.m11 + m22 * transform.m21, + m22: m21 * transform.m12 + m22 * transform.m22, + tx: tx + transform.tx, + ty: ty + transform.ty + ) + } + + /// Inverts the affine transform. + /// + /// If the affine transform cannot be inverted, the affine transform is unchanged. + public mutating func invert() { + let determinant = m11 * m22 - m12 * m21 + if determinant != 0 { + let inverseDet = 1 / determinant + let tmp11 = m22 * inverseDet + let tmp12 = -m12 * inverseDet + let tmp21 = -m21 * inverseDet + let tmp22 = m11 * inverseDet + let tmpTx = (m21 * ty - m22 * tx) * inverseDet + let tmpTy = (m12 * tx - m11 * ty) * inverseDet + m11 = tmp11 + m12 = tmp12 + m21 = tmp21 + m22 = tmp22 + tx = tmpTx + ty = tmpTy + } + } + + /// Returns an affine transformation matrix constructed by inverting the affine transform. + /// + /// If the affine transform cannot be inverted, the affine transform is returned unchanged. + public func inverted() -> AffineTransform { + var result = self + result.invert() + return result + } + + /// Translates the affine transform. + public mutating func translateBy(dx: Float, dy: Float) { + tx += dx + ty += dy + } + + /// Returns an affine transformation matrix constructed by translating the affine transform. + public func translatedBy(dx: Float, dy: Float) -> AffineTransform { + var result = self + result.translateBy(dx: dx, dy: dy) + return result + } + + /// Rotates the affine transform. + /// - Parameter angle: The rotation angle in degrees. + public mutating func rotateBy(angle: Float) { + let cosAngle = cosf(angle) + let sinAngle = sinf(angle) + let new11 = m11 * cosAngle + m12 * sinAngle + let new12 = m12 * cosAngle - m11 * sinAngle + let new21 = m21 * cosAngle + m22 * sinAngle + let new22 = m22 * cosAngle - m21 * sinAngle + m11 = new11 + m12 = new12 + m21 = new21 + m22 = new22 + } + + /// Returns an affine transformation matrix constructed by rotating the affine transform. + /// - Parameter angle: The rotation angle in degrees. + public func rotatedBy(angle: Float) -> AffineTransform { + var result = self + result.rotateBy(angle: angle) + return result + } + + /// Scales the affine transform. + public mutating func scaleBy(x: Float, y: Float) { + m11 *= x + m12 *= y + m21 *= x + m22 *= y + } + + /// Returns an affine transformation matrix constructed by scaling the affine transform. + public func scaledBy(x: Float, y: Float) -> AffineTransform { + var result = self + result.scaleBy(x: x, y: y) + return result + } +} diff --git a/Sources/PlaydateKit/Geometry/Line.swift b/Sources/PlaydateKit/Geometry/Line.swift index 9b7aba47..c37848b7 100644 --- a/Sources/PlaydateKit/Geometry/Line.swift +++ b/Sources/PlaydateKit/Geometry/Line.swift @@ -14,17 +14,16 @@ public struct Line: Equatable { public var start, end: Point } -public extension Line { - /// The line whose start and end are both located at (0, 0). - static var zero: Line { Line(start: .zero, end: .zero) } +// MARK: AffineTransformable + +extension Line: AffineTransformable where T == Float { + public mutating func transform(by transform: AffineTransform) { + start.transform(by: transform) + end.transform(by: transform) + } } public extension Line { - /// Returns a line with a start and end that is offset from that of the source line. - func offsetBy(dx: T, dy: T) -> Line { - Line( - start: start.offsetBy(dx: dx, dy: dy), - end: end.offsetBy(dx: dx, dy: dy) - ) - } + /// The line whose start and end are both located at (0, 0). + static var zero: Line { Line(start: .zero, end: .zero) } } diff --git a/Sources/PlaydateKit/Geometry/Point.swift b/Sources/PlaydateKit/Geometry/Point.swift index 55720f0f..09398545 100644 --- a/Sources/PlaydateKit/Geometry/Point.swift +++ b/Sources/PlaydateKit/Geometry/Point.swift @@ -14,14 +14,17 @@ public struct Point: Equatable { public var x, y: T } -public extension Point { - /// The point with location (0,0). - static var zero: Point { Point(x: 0, y: 0) } +// MARK: AffineTransformable + +extension Point: AffineTransformable where T == Float { + public mutating func transform(by transform: AffineTransform) { + let newX = transform.m11 * x + transform.m12 * y + transform.tx + let newY = transform.m21 * x + transform.m22 * y + transform.ty + self = Point(x: newX, y: newY) + } } public extension Point { - /// Returns a point that is offset from that of the source point. - func offsetBy(dx: T, dy: T) -> Point { - Point(x: x + dx, y: y + dy) - } + /// The point with location (0,0). + static var zero: Point { Point(x: 0, y: 0) } } diff --git a/Sources/PlaydateKit/Geometry/Polygon.swift b/Sources/PlaydateKit/Geometry/Polygon.swift index 852ce812..f69ca655 100644 --- a/Sources/PlaydateKit/Geometry/Polygon.swift +++ b/Sources/PlaydateKit/Geometry/Polygon.swift @@ -1,3 +1,5 @@ +// MARK: - Polygon + /// A structure that contains a two-dimensional open or closed polygon. public struct Polygon: Equatable { // MARK: Lifecycle @@ -21,3 +23,21 @@ public struct Polygon: Equatable { vertices.append(first) } } + +// MARK: - Array + AffineTransformable + +extension [Point]: AffineTransformable { + public mutating func transform(by transform: AffineTransform) { + for i in indices { + self[i].transform(by: transform) + } + } +} + +// MARK: - Polygon + AffineTransformable + +extension Polygon: AffineTransformable where T == Float { + public mutating func transform(by transform: AffineTransform) { + vertices.transform(by: transform) + } +} diff --git a/Sources/PlaydateKit/Geometry/Rect.swift b/Sources/PlaydateKit/Geometry/Rect.swift index e7041246..5a3dc09a 100644 --- a/Sources/PlaydateKit/Geometry/Rect.swift +++ b/Sources/PlaydateKit/Geometry/Rect.swift @@ -23,16 +23,22 @@ public struct Rect: Equatable { public var x, y, width, height: T } -public extension Rect { - /// The point with location (0,0). - static var zero: Rect { Rect(x: 0, y: 0, width: 0, height: 0) } +// MARK: AffineTransformable + +extension Rect: AffineTransformable where T == Float { + public mutating func transform(by transform: AffineTransform) { + let transformedOrigin = Point(x: x, y: y).transformed(by: transform) + let transformedTopRight = Point(x: x + width, y: y + height).transformed(by: transform) + x = transformedOrigin.x + y = transformedOrigin.y + width = transformedTopRight.x - transformedOrigin.x + height = transformedTopRight.y - transformedOrigin.y + } } public extension Rect { - /// Returns a rectangle with an origin that is offset from that of the source rectangle. - func offsetBy(dx: T, dy: T) -> Rect { - Rect(x: x + dx, y: y + dy, width: width, height: height) - } + /// The point with location (0,0). + static var zero: Rect { Rect(x: 0, y: 0, width: 0, height: 0) } } extension Rect where T == CInt {