I am trying to understand the results of chaining two transformations using CGAffineTransform in iOS. Based on Apple's documentation, combining translation and scaling is working as expected, but combining translation and rotation is not.
I think the question in this post was onto the same observation, but I am combining translation with scaling to show the inconsistent behavior. Or is there some consistent way to understand what order the transformations will take place using these methods?
Apple's CGAffineTransform documentation shows transforming a point, represented by a row vector [x y 1]
, by multiplying on the right by a matrix. To use the notation used in the CGAffineTransform header file, this matrix is [a b c d tx ty]
(because the last column is always the transpose of [0 0 1]
). Because the matrix is on the right of the row vector, if we have two CGAffineTransform matrices A
and B
, the product AB
applied to a point will first apply the transformation A
and then apply the operation B
(which is opposite of what typical linear algebra books do).
Using a translation transform t
, a scaling transformation s
, and a rotation transform r
, I have examined the resulting transforms and their effects on views for the following:
s.translatedBy(x: 100, y: 0) // translates first, then scales
s.concatenating(t) // scales first, then translates
t.rotated(by: 45 * .pi/180) // translates first, then rotates
t.concatenating(r) // rotates first, then translates
I understand that concatenating
will perform in the reverse order as you see when performing an operation such as translatedBy
. But, per concatenating: documentation, A.concatenatig(B)
should give the transformation AB
, which as noted above performs transformation A
followed by B
. That indeed happens on s.concatenating(t)
, but not t.concatenating(r)
. Based on the example in Matt's iOS book, here is some code to setup.
let v1 = UIView(frame:CGRect(20, 111, 132, 194))
v1.backgroundColor = .red
view.addSubview(v1)
let v2 = UIView(frame:v1.bounds)
v2.backgroundColor = .green
v1.addSubview(v2)
let v3 = UIView(frame: v1.bounds)
v3.backgroundColor = .blue
v1.addSubview(v3)
let t = CGAffineTransform(translationX:100, y:0)
let r = CGAffineTransform(rotationAngle: 45 * .pi/180)
let s = CGAffineTransform(scaleX: 0.1, y: 0.1)
Then you can add this code to see that translating and scaling works as expected:
// translates first, then scales
v2.transform = s.translatedBy(x: 100, y: 0)
// scales first, then translates
v3.transform = s.concatenating(t)
However, the behavior for translating and rotating is different:
// translates first, then rotates
v2.transform = t.rotated(by: 45 * .pi/180)
// rotates first, then translates
v3.transform = t.concatenating(r)
Furthermore, the header doc information for rotated
shows
Rotate t by angle radians and return the result:
t = [ cos(angle) sin(angle) -sin(angle) cos(angle) 0 0 ] * t
The multiplication should imply the rotation happens first, but the wording makes it seem that the rotation is second. Based on the results above, the wording correct (rotation is second).
The header doc for translatedBy
also has wording for translation being second and the matrix multiplication showing the translation is first. But based on the results above, the matrix multiplication is correct (translation is first).
Am I making a mistake in this analysis? Or is there some inconsistency in the order of transformations based on concatenation and the descriptions in the documentation for these transformation and concatenating methods.
The problem is that you have misinterpreted the first diagram:
You say:
Green v2 translates 100 to the right and then is scaled by .1, where blue v3 is scaled by .1 and then translated 100 to the right
No. Your words are backwards from what the diagram actually shows.
Remember, a transform takes place around a view’s center. Ok, so why is the green view only a tiny bit to the right of its original center?
It’s because first we scaled down to the center and then we moved 10 points right — 10 points because the meaning of a point has first been scaled down to 1/10 of a normal point.
But the blue view is a full 100 points right of its original center, because it translated that 100 points before scaling down.