Here are some tidbits about affine transforms on Apple platforms that I have collected over the years. I hope they are helpful for you. This might be overkill for your particular question (as it appears to have received some pretty good answers already), but I'm putting it on the forums in case it's useful to anyone else.
Documentation
Core Graphics affine transform is defined here:
CGAffineTransform
There is also a detailed discussion here:
Quartz 2D Programming Guide: Transforms
Core Animation includes a unit based coordinate system discussed here:
Core Animation Programming Guide: Core Animation Basics
Foundation also has an affine transform:
Foundation: AffineTransform
Accelerate also includes simd based affine transforms used with 3D coordinaes:
Accelerate: Working with Matrices
A technote about debugging coordinate space issues:
TN3124: Debugging coordinate space issues
Understanding affine transforms
There are many interesting discussions of affine transforms and how to use them available elsewhere. I'm not going to talk about them here, but if you're interested please consider taking a look around in books about computer graphics and on the Internet.
The key thing you need to know about using affine transforms
All of the transformations are relative to the origin. If you rotate, the rotation operation will be centered at the origin. If you are scaling, the scaling operation is centered at the origin. if you need to rotate a square at its center, for example, you will need to perform three operations - translate the square so it is centered at the origin, rotate the square at the origin, and then translate the square back to wherever it was.
Implementation details
Mathematically speaking, there are two approaches to implementing affine transformations with matrices. When reviewing documentation about them, you'll probably find different sources that use one or the other approach. Both approaches are equivalent and are the transpose of one another (you can translate from one format to the other by applying the matrix rule (AB)' = (B')(A') - where I'm using ' to indicate the transpose operation). For all practical purposes, either method will produce the same results. However, when using affine transformation matrices together with our APIs may sometimes need to be aware of the convention being employed by the API that you are using. For example, Foundation and Core Graphics use the row-major organization for matrices, but the Accelerate article mentioned above uses the column-major format.
Column-major format
Affine transformation by multiplying a column vector on the right:
|a c tx| |x| |x'|
|b d ty|x|y|=|y'|
|0 0 1 | |1| |1 |
Row-major format
Affine transformation by multiplying a row vector on the left:
|a b 0|
[x y 1]x|c d 0|=[x' y' 1]
|tx ty 1|
note, I'm calling these column-major and row-major, but this is only to do with how the values are organized in the matrices and not to the underlying organization of the arrays in computer memory.
Choice of matrix format determines the order of operations
The matrix format used will determine the order in which you multiply your transformation matrices together to construct a combined transformation matrix:
- With the column-major format, operations are added by multiplying in new matrices on the left hand side:
( last_operation * ... * second_operation * first_operation * input_column_vector) → output_column_vector
- With the row-major format, operations are added by multiplying in matrices on the right hand side:
( input_row_vector * first_operation * second_operation * ... * last_operation) → output_row_vector
This is important because it will completely determine how you write your matrix handling code. However, though it's useful to be aware of these details in some situations, they are mostly hidden by the APIs.
On Apple platforms
The Core Graphics CGAffineTransform API uses row-major format, and adding new transformation operations by multiplying transformation matrices in on the right is accomplished using the concatenating(_:) method or one of the modifying operations such as rotated(by:), translatedBy(x:, y:), etc.
The Foundation AffineTransform uses row-major format also API provides prepend(:) and append(:) for multiplying in new transform matrices on either the left or the right respectively. For adding new operations, use the append(_:) method.