Search code examples
iosswiftmatrixcgaffinetransform

How to correctly combine CGAffineTransform matrices?


An image should be scaled and transformed.

Depending on how the transform matrix is composed, I get different results:

// A) This produces the desired result, scales the image and translates the image independently from each other
let transform = CGAffineTransform(translationX: translation.x, y: translation.y).scaledBy(x: scale.width, y: scale.height)

// B) This also produces the desired result
let scaleTransform = CGAffineTransform(scaleX: scale.width, y: scale.height)
let translateTransform = CGAffineTransform(translationX: translation.x, y: translation.y)
let transform = scaleTransform.concatenating(translateTransform)

// C) This does not produce the desired result, it also scales the translation, so a 10x scale results in a 10x translation
let transform = CGAffineTransform(scaleX: scale.width, y: scale.height).translatedBy(x: translation.x, y: translation.y)

// Transform image
image = image.transformed(by: transform)

If .concatenating means multiplying and .scaledBy or .translatedBy means adding the two matrices, why does A and C not produce the same results given that matrix order should not matter when adding them together?


Solution

  • It is a coincidence that the multiplication and the addition of a scaling matrix and a translation matrix have the same result.

    In the general case, scaledBy and translatedBy don't mean adding, they are shorthand for concatenating two transforms, which is a matrix multiplication. Matrix multiplication is only commutative for diagonal matrices (matrices that only have non-zero values in the diagonal line), so S * T generally isn't the same as T * S.

    Look up $(xcrun --show-sdk-path)/System/Library/Frameworks/CoreGraphics.framework/Headers/CGAffineTransform.h for what each function does:

    • CGAffineTransformTranslate: t' = [ 1 0 0 1 tx ty ] * t
    • CGAffineTransformScale: t' = [ sx 0 0 sy 0 0 ] * t
    • CGAffineTransformRotate: t' = [ cos(angle) sin(angle) -sin(angle) cos(angle) 0 0 ] * t
    • CGAffineTransformConcat: t' = t1 * t2

    This means that when you use CGAffineTransformConcat, t1 must be the transformation that you're applying and t2 must be the matrix that you're transforming. In other words, scale.translatedBy is equivalent to concat(translation, scale), not concat(scale, translation). When using concatenate as a method, this makes the operation look backwards because of its mathematical definition.