From CATransform3D to CGAffineTransform - core-animation

I'm using the following function to apply a pulse effect to a view
- (void)pulse {
CATransform3D trasform = CATransform3DScale(self.layer.transform, 1.15, 1.15, 1);
trasform = CATransform3DRotate(trasform, angle, 0, 0, 0);
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.toValue = [NSValue valueWithCATransform3D:trasform];
animation.autoreverses = YES;
animation.duration = 0.3;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.repeatCount = 2;
[self.layer addAnimation:animation forKey:#"pulseAnimation"];
}
I would like to obtain the same result using the CGAffineTransform self.transform instead of the CATransform3D self.layer.transform. Is this possible?

It's possible convert a CATransform3D to a CGAffineTransform, but you will lose some capabilities. I found it useful to convert the aggregate transform of a layer and its ancestors into a CGAffineTransform so I could render it with Core Graphics. The constraints are:
Your input will be treated as flat in the XY plane
Your output will be treated as flat in the XY plane too
Perspective / foreshortening from .m34 will be neutralized
If that sounds OK for your purposes:
// m13, m23, m33, m43 are not important since the destination is a flat XY plane.
// m31, m32 are not important since they would multiply with z = 0.
// m34 is zeroed here, so that neutralizes foreshortening. We can't avoid that.
// m44 is implicitly 1 as CGAffineTransform's m33.
CATransform3D fullTransform = <your 3D transform>
CGAffineTransform affine = CGAffineTransformMake(fullTransform.m11, fullTransform.m12, fullTransform.m21, fullTransform.m22, fullTransform.m41, fullTransform.m42);
You will want to do all your work in 3D transforms first, say by concatenating from your superlayers, and then finally convert the aggregate CATransform3D to a CGAffineTransform. Given that layers are flat to begin with and render onto a flat target, I found this very suitable since my 3D rotations became 2D shears. I also found it acceptable to sacrifice foreshortening. There's no way around that because affine transforms have to preserve parallel lines.
To render a 3D-transformed layer using Core Graphics, for instance, you might concatenate the transforms (respecting anchor points!), then convert to affine, and finally:
CGContextSaveGState(context);
CGContextConcatCTM(context, affine);
[layer renderInContext:context];
CGContextRestoreGState(context);

Of course. If you do a search on CGAffineTransform in the Xcode docs, you'll find a chapter titled "CGAffineTransform Reference". In that chapter is a section called "Functions". It includes functions that are equivalent to CATransform3DScale (CGAffineTransformScale ) and CATransform3DRotate (CGAffineTransformRotate).
Note that your call to CATransform3DRotate doesn't really make sense. You need to rotate around an axis, and you're passing 0 for all 3 axes. Typcially you want to use CATransform3DRotate(trasform, angle, 0, 0, 1.0) to rotate around the Z axis. To quote the docs:
If the vector has zero length the behavior is undefined.

You can use CATransform3DGetAffineTransform to convert CATransform3d to CGAffineTransform.
let scaleTransform = CATransform3DMakeScale( 0.8, 0.8, 1)
imageview.transform = CATransform3DGetAffineTransform(scaleTransform)

Related

What is wrong with this way of using CGAffineTransform?

I want to make a graph in a UIView that shows numerical data. So I need to draw axis, a few coordinate lines, some tick marks, and then the data as connected straight lines. The data might typically consist of a few hundred x values in the range -500. to +1000. with corresponding y values in the range 300. to 350.
So I thought a good approach would be to transform the coordinates of the UIView so (for the example values given) the left side of the view is -500, and right side is 1000, the top is 400 and the bottom is 300. And y increases upwards. Then in drawRect: I could write a bunch of CGContextMoveToPoint() and CGContextAddLineToPoint() statements with my own coordinate system and not have to mentally translate each call to the UIView coordinates.
I wrote the following function to generate my own CGContextRef but it doesn't do what I expected. I've been trying variations on it for a couple days now and wasting so much time. Can someone say how to fix it? I realize I can't get clear in my mind whether the transform is supposed to specify the UIView coordinates in terms of my coordinates, or vice versa, or something else entirely.
static inline CGContextRef myCTX(CGRect rect, CGFloat xLeft, CGFloat xRight, CGFloat yBottom, CGFloat yTop) {
CGAffineTransform ctxTranslate = CGAffineTransformMakeTranslation(xLeft, rect.size.height - yTop);
CGAffineTransform ctxScale = CGAffineTransformMakeScale( rect.size.width / (xRight - xLeft), -rect.size.height / (yTop - yBottom) ); //minus so y increases toward top
CGAffineTransform combinedTransform = CGAffineTransformConcat(ctxTranslate, ctxScale);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextConcatCTM(c, combinedTransform);
return c;
}
The way I'm using this is that inside drawRect I just have:
CGContextRef ctx = myCTX(rect, self.xLeft, self.xRight, self.yBottom, self.yTop);
and then a series of statements like:
CGContextAddLineToPoint(ctx, [x[i] floatValue], [y[i] floatValue]);
I figured this out by experimenting. The transform requires 3 steps instead of 2 (or, if not required, at least it works this way):
static inline CGContextRef myCTX(CGRect rect, CGFloat xLeft, CGFloat xRight, CGFloat yBottom, CGFloat yTop) {
CGAffineTransform translate1 = CGAffineTransformMakeTranslation(-xLeft, -yBottom);
CGAffineTransform scale = CGAffineTransformMakeScale( rect.size.width / (xRight - xLeft), -rect.size.height / (yTop - yBottom) );
CGAffineTransform transform = CGAffineTransformConcat(translate1, scale);
CGAffineTransform translate2 = CGAffineTransformMakeTranslation(1, rect.size.height);
transform = CGAffineTransformConcat(transform, translate2);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextConcatCTM(c, transform);
return c;
}
You use this function inside drawRect. In my case the xLeft, xRight, etc. values are properties of a UIView subclass and are set by the viewController. So the view's drawRect looks like so:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextRef ctx = myCTX(rect, self.xLeft, self.xRight, self.yBottom, self.yTop);
…
all of the CGContextMoveToPoint(), CGContextAddLineToPoint(), calls to
draw your desired lines, rectangles, curves, etc. but not stroke or fill them
…
CGContextRestoreGState(c);
CGContextSetLineWidth(c, 1);
CGContextStrokePath(c);
}
The CGContextSetLineWidth call isn't needed if you want a line width of 1. If you don't restore the graphics state before strokePath the path width is affected by the scaling.
Now I have to figure out how to draw text onto the view.

Rotation of UIImageView does not conserve center

Just setting a rotation transform to an UIImageView does not keep the center still. The image translate around de center, I'm puzzled. Any idea?
CGAffineTransform
transform = CGAffineTransformIdentity;
transform = CGAffineTransformRotate(transform, self.filter.angle);
self.sourceImageView.transform = transform;
Note: the angle is coupled to a UISlider so that the user changes its value from -90 to 90.
I found that setting the transformation to the layer fixes the issue.
self.sourceImageView.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale, scale, 1.), angle, 0, 0, -1);

Ribbon Graphs (charts) with Core Graphics

I am generating a classic line graph using core graphics, which renders and work very well.
There are several lines stacking one after another using "layer.zPosition"
-(void)drawRect:(CGRect)rect {
float colorChange = (0.1 * [self tag]);
theFillColor = [UIColor colorWithRed:(colorChange) green:(colorChange*0.50) blue:colorChange alpha:0.75f].CGColor;
CGContextRef c = UIGraphicsGetCurrentContext();
CGFloat white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
CGContextSetFillColorWithColor(c, theFillColor);
CGContextSetStrokeColor(c, white);
CGContextSetLineWidth(c, 2.0f);
CGContextBeginPath(c);
//
CGContextMoveToPoint(c, 0.0f, 200-[[array objectAtIndex:0] floatValue]);
CGContextAddLineToPoint(c, 0.0f, 200-[[array objectAtIndex:0] floatValue]);
//
distancePerPoint = (rect.size.width / [array count]);
float lastPointX = 750.0;
for (int i = 0 ; i < [array count] ; i++)
{
CGContextAddLineToPoint(c, (i*distancePerPoint), 200-[[array objectAtIndex:i] floatValue]);
lastPointX = (i*distancePerPoint);
}
//
CGContextAddLineToPoint(c, lastPointX, 200.0);
CGContextAddLineToPoint(c, 0, 200);
CGContextClosePath(c);
//
//CGContextFillPath(c);
CGContextDrawPath(c, kCGPathFillStroke);
//CGContextDrawPath(c, kCGPathStroke);
}
(The above code is generating the following result):
(I can post the code I am using for the 3d effect if needed, but the way I do it is generically by
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;)
Question:
How can I transform my line graph to have depth ?
I would like to have a "depth" to the line(s) graph (thus making them a ribbon) as later I would like to represent them using rotation And Perspective Transform (as stated above)
You can't easily do with with Core Graphics or Core animation because CALayers are "flat" - they work like origami, where you can make 3D structures by connecting rectangles in 3D space, but you can't have arbitrary polygonal 3D shapes.
Actually that's not strictly true, you could look at using CAShapeLayers to do your drawing, and then manipulate them in 3D, but I think this is generally going to be very hard work to calculate where to position each shape and to get the edges to line up correctly.
Really the way to make this kind of 3D structure is to use OpenGL directly.
If you're not too familiar with low-level OpenGL programming, you might want to check out Galaxy Engine or Cocos3D.

Saving 2 UIImages to one while saving rotation, resize info and its quality

I want to save 2 UIImages that are moved, resized and rotated by user. The problem is i dont want to use such function as any 'printscreen one', because it makes both images to lose a lot from their quality (resolution).
Atm i use something like this:
UIGraphicsBeginImageContext(image1.size);
[image1 drawInRect:CGRectMake(0, 0, image1.size.width, image1.size.height)];
[image2 drawInRect:CGRectMake(0, 0, image1.size.width, image1.size.height)];
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
However ofc it just adds two images, their rotation, resizing and moving isn't operated here. Can anybody help with considering these 3 aspects in coding? Any help is appreciated!
My biggest thanks in advance :)
EDIT: images can be rotated and zoomed by user (handling touch events)!
You have to set the transform of the context to match your imageView's transform before you start drawing into it.
i.e.,
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, boundingRect.size.width/2, boundingRect.size.height/2);
transform = CGAffineTransformRotate(transform, angle);
transform = CGAffineTransformScale(transform, 1.0, -1.0);
CGContextConcatCTM(context, transform);
// Draw the image into the context
CGContextDrawImage(context, CGRectMake(-imageView.image.size.width/2, -imageView.image.size.height/2, imageView.image.size.width, imageView.image.size.height), imageView.image.CGImage);
// Get an image from the context
rotatedImage = [UIImage imageWithCGImage: CGBitmapContextCreateImage(context)];
and check out Creating a UIImage from a rotated UIImageView.
EDIT: if you don't know the angle of rotation of the image you can get the transform from the layer property of the UIImageView:
UIGraphicsBeginImageContext(rotatedImageView.image.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = rotatedImageView.transform;
transform = CGAffineTransformScale(transform, 1.0, -1.0);
CGContextConcatCTM(context, transform);
// Draw the image into the context
CGContextDrawImage(context, CGRectMake(0, 0, rotatedImageView.image.size.width, rotatedImageView.image.size.height), rotatedImageView.image.CGImage);
// Get an image from the context
UIImage *newRotatedImage = [UIImage imageWithCGImage: CGBitmapContextCreateImage(context)];
UIGraphicsEndImageContext();
You will have to play about with the transform matrix to centre the image in the context and you will also have to calculate a bounding rectangle for the rotated image or it will be cropped at the corners (i.e., rotatedImageView.image.size is not big enough to encompass a rotated version of itself).
Try this:
UIImage *temp = [[UIImage alloc] initWithCGImage:image1 scale:1.0 orientation: yourOrientation];
[temp drawInRect:CGRectMake(0, 0, image1.size.width, image1.size.height)];
Similarly for image2. Rotation and resizing are handled by orientation and scale respectively. yourOrientation is a UIImageOrientation enum variable and can have a value from 0-7(check this apple documentation on different UIImageOrientation values). Hope it helps...
EDIT: To handle rotations, just write the desired orientation for the rotation you require. You can rotate 90 deg left/right or flip vertically/horizontally. For eg, in the apple documentation, UIImageOrientationUp is 0, UIImageOrientationDown is 1 and so on. Check out my github repo for an example.

Linear gradient aliasing with CoreGraphics

I'm trying to emulate the color tint effect from the UITabBarItem.
When I draw a linear gradient at an angle, I get visible aliasing in the middle part of the gradient where the two colors meet at the same location. Left is UITabBarItem, right is my gradient with visible aliasing (stepping):
Here is the snippet of relevant code:
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextScaleCTM(c, 1.0, -1.0);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat components[16] = {1,1,1,1,
109.0/255.0,175.0/255.0,246.0/255.0,1,
31.0/255.0,133.0/255.0,242.0/255.0,1,
143.0/255.0,194.0/255.0,248.0/255.0,1};
CGFloat locations[4] = {0.0, 0.62, 0.62, 1};
CGGradientRef colorGradient =
CGGradientCreateWithColorComponents(colorSpace, components,
locations, (size_t)4);
CGContextDrawLinearGradient(c, colorGradient, CGPointZero,
CGPointMake(size.width*1.0/3.9, -size.height),0);
CGGradientRelease(colorGradient);
CGColorSpaceRelease(colorSpace);
CGContextRestoreGState(c);
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultImage;
What do I need to change, to get a smooth angled gradient like in UITabBarItem?
What is the interpolation quality of your context set to? CGContextGetInterpolationQuality()/CGContextSetInterpolationQuality(). Try changing that if it's too low.
If that doesn't work, I'm curious what happens if you draw the gradient vertically (0,Ymin)-(0,Ymax) but apply a rotation transformation to your context...
As a current workaround, I draw the gradient at double resolution into an image and then draw the image with original dimensions. The image scaling that occurs, takes care of the aliasing. At the pixel level the result is not as smooth as in the UITabBarItem, but that probably uses an image created in Photoshop or something similar.