Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add affine transform #48

Merged
merged 1 commit into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions Sources/PlaydateKit/Geometry/AffineTransform.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
19 changes: 9 additions & 10 deletions Sources/PlaydateKit/Geometry/Line.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ public struct Line<T: Numeric>: Equatable {
public var start, end: Point<T>
}

public extension Line {
/// The line whose start and end are both located at (0, 0).
static var zero: Line<T> { 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<T> { Line(start: .zero, end: .zero) }
}
17 changes: 10 additions & 7 deletions Sources/PlaydateKit/Geometry/Point.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ public struct Point<T: Numeric>: Equatable {
public var x, y: T
}

public extension Point {
/// The point with location (0,0).
static var zero: Point<T> { 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<T> { Point(x: 0, y: 0) }
}
20 changes: 20 additions & 0 deletions Sources/PlaydateKit/Geometry/Polygon.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// MARK: - Polygon

/// A structure that contains a two-dimensional open or closed polygon.
public struct Polygon<T: Numeric>: Equatable {
// MARK: Lifecycle
Expand All @@ -21,3 +23,21 @@ public struct Polygon<T: Numeric>: Equatable {
vertices.append(first)
}
}

// MARK: - Array + AffineTransformable

extension [Point<Float>]: 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)
}
}
20 changes: 13 additions & 7 deletions Sources/PlaydateKit/Geometry/Rect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,22 @@ public struct Rect<T: Numeric>: Equatable {
public var x, y, width, height: T
}

public extension Rect {
/// The point with location (0,0).
static var zero: Rect<T> { 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<T> { Rect(x: 0, y: 0, width: 0, height: 0) }
}

extension Rect where T == CInt {
Expand Down
Loading