It turns out that in most cases you can make a decent description about an affine transform since it's quite constrained. Doing the same for 3D transforms is much harder :(
Note that I still won't be able to tell you the different transforms matrices that were concatenated, only the end result. I'm also ignoring shearing since there is no provided function for creating such a transform.
I wrote a function that does a decent job of figuring out what the affine transform does.
You will see in a number of places that I write if (fabs(foo - bar) < FLT_EPSILON)
instead of just if (foo == bar)
. This is to protect myself for floating point (im)precision in the comparison.
The other notable thing to point out is the way I'm figuring out the rotation angle. For a pure rotation I could just have used asin(b)
, but if the transform is also scaled, then that result will be incorrect. Instead I divide b by a and use arctan to calculate the angle.
There is a decent amount of comments in the code so you should be able to follow along, mostly by just reading it.
NSString *affineTransformDescription(CGAffineTransform transform)
{
// check if it's simply the identity matrix
if (CGAffineTransformIsIdentity(transform)) {
return @"Is the identity transform";
}
// the above does't catch things like a 720° rotation so also check it manually
if (fabs(transform.a - 1.0) < FLT_EPSILON &&
fabs(transform.b - 0.0) < FLT_EPSILON &&
fabs(transform.c - 0.0) < FLT_EPSILON &&
fabs(transform.d - 1.0) < FLT_EPSILON &&
fabs(transform.tx - 0.0) < FLT_EPSILON &&
fabs(transform.ty - 0.0) < FLT_EPSILON) {
return @"Is the identity transform";
}
// The affine transforms is built up like this:
// a b tx
// c d ty
// 0 0 1
// An array to hold all the different descirptions, charasteristics of the transform.
NSMutableArray *descriptions = [NSMutableArray array];
// Checking for a translation
if (fabs(transform.tx) > FLT_EPSILON) { // translation along X
[descriptions addObject:[NSString stringWithFormat:@"Will move %.2f along the X axis",
transform.tx]];
}
if (fabs(transform.ty) > FLT_EPSILON) { // translation along Y
[descriptions addObject:[NSString stringWithFormat:@"Will move %.2f along the Y axis",
transform.ty]];
}
// Checking for a rotation
CGFloat angle = atan2(transform.b, transform.a); // get the angle of the rotation. Note this assumes no shearing!
if (fabs(angle) < FLT_EPSILON || fabs(angle - M_PI) < FLT_EPSILON) {
// there is a change that there is a 180° rotation, in that case, A and D will and be negative.
BOOL bothAreNegative = transform.a < 0.0 && transform.d < 0.0;
if (bothAreNegative) {
angle = M_PI;
} else {
angle = 0.0; // this is not considered a rotation, but a negative scale along one axis.
}
}
// add the rotation description if there was an angle
if (fabs(angle) > FLT_EPSILON) {
[descriptions addObject:[NSString stringWithFormat:@"Will rotate %.1f° degrees",
angle*180.0/M_PI]];
}
// Checking for a scale (and account for the possible rotation as well)
CGFloat scaleX = transform.a/cos(angle);
CGFloat scaleY = transform.d/cos(angle);
if (fabs(scaleX - scaleY) < FLT_EPSILON && fabs(scaleX - 1.0) > FLT_EPSILON) {
// if both are the same then we can format things a little bit nicer
[descriptions addObject:[NSString stringWithFormat:@"Will scale by %.2f along both X and Y",
scaleX]];
} else {
// otherwise we look at X and Y scale separately
if (fabs(scaleX - 1.0) > FLT_EPSILON) { // scale along X
[descriptions addObject:[NSString stringWithFormat:@"Will scale by %.2f along the X axis",
scaleX]];
}
if (fabs(scaleY - 1.0) > FLT_EPSILON) { // scale along Y
[descriptions addObject:[NSString stringWithFormat:@"Will scale by %.2f along the Y axis",
scaleY]];
}
}
// Return something else when there is nothing to say about the transform matrix
if (descriptions.count == 0) {
return @"Can't easilly be described.";
}
// join all the descriptions on their own line
return [descriptions componentsJoinedByString:@",\n"];
}
To try it out I tested the output on a number of different transforms. This is the code I used to test it:
// identity
CGAffineTransform t = CGAffineTransformIdentity;
NSLog(@"identity: \n%@", affineTransformDescription(t));
// translation
t = CGAffineTransformMakeTranslation(10, 0);
NSLog(@"translate(10, 0): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeTranslation(0, 20);
NSLog(@"translate(0, 20): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeTranslation(2, -3);
NSLog(@"translate(2, -3): \n%@", affineTransformDescription(t));
// scale
t = CGAffineTransformMakeScale(2, 2);
NSLog(@"scale(2, 2): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeScale(-1, 3);
NSLog(@"scale(-1, 3): \n%@", affineTransformDescription(t));
// rotation
t = CGAffineTransformMakeRotation(M_PI/3.0);
NSLog(@"rotate(60 deg): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeRotation(M_PI);
NSLog(@"rotate(180 deg): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeRotation(4.0*M_PI);
NSLog(@"rotate(720 deg): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeRotation(3.0*M_PI);
NSLog(@"rotate(540 deg): \n%@", affineTransformDescription(t));
// concatenated transforms
// rotate & translate
t = CGAffineTransformMakeRotation(M_PI/3.0);
t = CGAffineTransformTranslate(t, 10, 20);
NSLog(@"rotate(60 deg), translate(10, 20): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeTranslation(10, 20);
t = CGAffineTransformRotate(t, M_PI/3.0);
NSLog(@"translate(10, 20), rotate(60 deg): \n%@", affineTransformDescription(t));
// rotate & scale
t = CGAffineTransformMakeRotation(M_PI/3.0);
t = CGAffineTransformScale(t, 2, 2);
NSLog(@"rotate(60 deg), scale(2, 2): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeScale(2, 2);
t = CGAffineTransformRotate(t, M_PI/3.0);
NSLog(@"scale(2, 2), rotate(60 deg): \n%@", affineTransformDescription(t));
// translate & scale
t = CGAffineTransformMakeTranslation(10, 20);
t = CGAffineTransformScale(t, 2, 2);
NSLog(@"translate(10, 20), scale(2, 2): \n%@", affineTransformDescription(t));
t = CGAffineTransformMakeScale(2, 2);
t = CGAffineTransformTranslate(t, 10, 20);
NSLog(@"scale(2, 2), translate(10, 20): \n%@", affineTransformDescription(t));
and the output from that test:
identity:
Is the identity transform
translate(10, 0):
Will move 10.00 along the X axis
translate(0, 20):
Will move 20.00 along the Y axis
translate(2, -3):
Will move 2.00 along the X axis,
Will move -3.00 along the Y axis
scale(2, 2):
Will scale by 2.00 along both X and Y
scale(-1, 3):
Will scale by -1.00 along the X axis,
Will scale by 3.00 along the Y axis
rotate(60 deg):
Will rotate 60.0° degrees
rotate(180 deg):
Will rotate 180.0° degrees
rotate(720 deg):
Is the identity transform
rotate(540 deg):
Will rotate 180.0° degrees
rotate(60 deg), translate(10, 20):
Will move -12.32 along the X axis,
Will move 18.66 along the Y axis,
Will rotate 60.0° degrees
translate(10, 20), rotate(60 deg):
Will move 10.00 along the X axis,
Will move 20.00 along the Y axis,
Will rotate 60.0° degrees
rotate(60 deg), scale(2, 2):
Will rotate 60.0° degrees,
Will scale by 2.00 along both X and Y
scale(2, 2), rotate(60 deg):
Will rotate 60.0° degrees,
Will scale by 2.00 along both X and Y
translate(10, 20), scale(2, 2):
Will move 10.00 along the X axis,
Will move 20.00 along the Y axis,
Will scale by 2.00 along both X and Y
scale(2, 2), translate(10, 20):
Will move 20.00 along the X axis,
Will move 40.00 along the Y axis,
Will scale by 2.00 along both X and Y