ioscore-graphics

CGAffineTransform how to determine it only contains scaling/translation and not skew/rotation


I am using CGRectApplyAffineTransform, and I need to efficiently tell if the transformed rect is still a rect, meaning that I don't want to compute the actual values of rotation/skew, but just need to check if it contains rotation/skew.

From this API doc:

https://developer.apple.com/documentation/coregraphics/1455875-cgrectapplyaffinetransform

If the affine transform t consists solely of scaling and translation operations, then the returned rectangle coincides with the rectangle constructed from the four transformed corners.

So how can I efficiently tell if the affine transform t consists solely of scaling and translation operations?


Solution

  • The documentation is correct: If you only apply scaling and translating operations, the resulting CGRect is equivalent to transforming the corners. This is equivalent to saying if b and c are zero, the resulting CGRect is equivalent to transforming the corners.

    Consider the formulae in the CGAffineTransform documentation:

    x′ = ax + cy + tx
    y′ = bx + dy + ty

    If b and c are zero, that is simplified to:

    x′ = ax + tx
    y′ = dy + ty

    Therefore, x′ is x scaled by a and translated by tx. Likewise, y′ is y scaled by d and translated by ty.

    Thus, there is no skewing taking place as both x and y are scaled by their respective scalar constant (and translated by another similarly respective scalar constant). You can only get skewing if either of the calculations of x′ and y′ had more than one variable.


    Some hair-splitting:

    1. I might hesitate to say, though, that b and c of zero means no rotation is taking place. E.g., CGAffineTransform(a: -1, b: 0, c: 0, d: -1, tx: 0, ty: 0) represents a 180º rotation. (It also represents a scaling of both x and y by -1; it’s the same thing.) This is somewhat academic, as my earlier observations regarding b and c still hold. Still, I would hesitate to say b and c values of zero means “no rotation.”

    2. The documentation only says that if you only scale and translate, then “the returned rectangle coincides with the rectangle constructed from the four transformed corners.” But this is not the only case.

      Consider CGAffineTransform(a: 0, b: 1, c: -1, d: 0, tx: 0, ty: 0). That is a rotation by 90º. That, too, will result in a rectangle constructed solely from the four transformed corners.

      That is because if a and d are zero, again the formulae are simplified, but in this case to:

      x′ = cy + tx
      y′ = bx + ty

      Again, both x′ and y′ are dependent upon a single variable.


    So, the long and short of it is that the resulting CGRect of applying(_:) will be equivalent to the bounding box of the transform of the four corners if, either:

    E.g.,

    extension CGAffineTransform {
        var isEquivalentToTransformedCorners: Bool {
            (b == 0 && c == 0) || (a == 0 && d == 0)
        }
    }