Quick quiz time! Given these two transforms:
let translation = CGAffineTransform(translationX: 5, y: 10) let rotation = CGAffineTransform(rotationAngle: CGFloat(Double.pi) / 6)
Consider the following assignments.
let a = rotation.concatenating(translation) let b = translation.concatenating(rotation) let c = rotation.translatedBy(x: 5, y: 10) let d = translation.rotated(byDegrees: CGFloat(Double.pi) / 6)
Can you tell me which of these outcomes match each other before scrolling down to the answer? (No cheating!) To provide a little buffer between here and there, let me remind you about what a basic affine transform looks like:
For translation, the tx and ty entries specify the offsets for x and y:
translation: ┌ ┐ │ 1.000 0.000 0.000│ translation: (5.0, 10.0) │ │ scale: (1.00, 1.00) │ 0.000 1.000 0.000│ rotation: 0.00° │ │ rotation: 0.00 π │ 5.000 10.000 1.000│ rotation: 0.00 radians └ ┘
When rotating, the abcd slots are filled by cos(????), sin(????), -sin(????), and cos(????):
rotation: ┌ ┐ │ 0.866 0.500 0.000│ translation: (0.0, 0.0) │ │ scale: (1.00, 1.00) │ -0.500 0.866 0.000│ rotation: 30.00° │ │ rotation: 0.16 π │ 0.000 0.000 1.000│ rotation: 0.52 radians └ ┘
Multiplying translation by rotation gives you this:
And multiplying rotation by translation gives you this:
The results are identical except in the (tx, ty) offset slots.
Okay, ready with your answers? If you guessed a/d and b/c, you’re right. As a basic rule of thumb, x.concatenating(y)
is going to be the same as y.performing(x)
, where the performing call is rotated(by:), translatedBy(x:,y:), or scaledBy(x, y).
Concatenating a transform simply multiplies one transform by another: T1 x T2. However, performing a transformation (rotated, translated, scaled) gives you T2 x T1. If you hop into the module declarations, the answers are there to see in the hipster retro documentation:
/* Translate `t' by `(tx, ty)' and return the result: t' = [ 1 0 0 1 tx ty ] * t */ @available(iOS 2.0, *) public func translatedBy(x tx: CGFloat, y ty: CGFloat) -> CGAffineTransform /* Scale `t' by `(sx, sy)' and return the result: t' = [ sx 0 0 sy 0 0 ] * t */ @available(iOS 2.0, *) public func scaledBy(x sx: CGFloat, y sy: CGFloat) -> CGAffineTransform /* Rotate `t' by `angle' radians and return the result: t' = [ cos(angle) sin(angle) -sin(angle) cos(angle) 0 0 ] * t */ @available(iOS 2.0, *) public func rotated(by angle: CGFloat) -> CGAffineTransform
Here are the results, printed from a Swift playground, just to confirm that the behavior is, in fact, exactly as documented:
a CGAffineTransform(a: 0.866025403784439, b: 0.5, c: -0.5, d: 0.866025403784439, tx: 5.0, ty: 10.0) b CGAffineTransform(a: 0.866025403784439, b: 0.5, c: -0.5, d: 0.866025403784439, tx: -0.669872981077805, ty: 11.1602540378444) c CGAffineTransform(a: 0.866025403784439, b: 0.5, c: -0.5, d: 0.866025403784439, tx: -0.669872981077805, ty: 11.1602540378444) d CGAffineTransform(a: 0.866025403784439, b: 0.5, c: -0.5, d: 0.866025403784439, tx: 5.0, ty: 10.0)
I will leave my rants about the absurd and inconsistent naming, caps, argument labels, and initializers for another day.
Comments are closed.