Concatenate got your tongue?

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:

matrix

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:

screen-shot-2016-10-27-at-4-20-14-pm

And multiplying rotation by translation gives you this:

screen-shot-2016-10-27-at-4-21-07-pm

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.