Perspective and Flipping an UIImageView At The Same Time - iphone

I am trying to flip and give perspective rotate an uiimageview, in order to obtain an effect like reflection. first i give perspective using CATransform3D and then flip using CGAffineTransformMake. However i loose the perspective effect after the second trasnformation. and i couldn't figure out how to perspective and flip both using CATransform3D. img is the first image and img2 will be its reflection.
CALayer *layer = img.layer;
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -600;
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, 30.0f * M_PI / 180.0f, 0.0f, 1.0f, 0.0f);
layer.transform = rotationAndPerspectiveTransform;
img2.transform = CGAffineTransformMake(
1, 0, 0, -1, 0, img2.bounds.size.height
);

I was able to accomplish this effect in 4 (optionally 5) steps:
Create two UIImageViews in your nib. Create a UIImageView for your main image, then another for your reflected image. Make sure their view content modes are both set to Aspect Fit. Also, make the frame size of the reflected image 2x that of your main image (because the perspective reflection will add width to the image).
Load up the main image as normal. Now load up the reflection to be upside-down like so:
reflectionView.image = [UIImage
imageWithCGImage:mainImageView.image.CGImage
scale:1.0f
orientation: UIImageOrientationDownMirrored];
Now create the perspective transform like so:
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m24 = 1.0f/-mainImageView.frame.size.width;
[[reflectionView layer] setTransform:rotationAndPerspectiveTransform];
Finally, move the reflection into place:
reflectionView.center = CGPointMake(self.view.frame.size.width/2, mainImageView.frame.size.height*1.5);
Optionally, add a gradient to make the reflection fade away into the
background:
reflectionView.layer.masksToBounds = YES;
// define gradient
CAGradientLayer *theGradient = [CAGradientLayer layer];
theGradient.frame = reflectionView;
theGradient.colors = [NSArray arrayWithObjects:
(id)[[UIColor clearColor] CGColor],
(id)[[UIColor blackColor] CGColor], nil];
// add gradient to reflection image
[reflectionView insertSublayer:theGradient atIndex:0];
reflectionView = 0.25f;
It looks like this:

Related

How to get context of a CALayer plus a few pixels around it?

I am creating a layer with a shadow effect. Instead of adding it as a sublayer to my view's layer, I would like to draw it. But I am having problems being able to draw it with the shadow effect. The problem is that the context size it based on the layer size. But how do I set the rect for the context?!
CALayer *layer = [CALayer layer];
layer.frame = ...;
layer.backgroundColor = [UIColor redColor].CGColor;
layer.cornerRadius = 2.0;
layer.masksToBounds = NO;
layer.shadowColor = [[UIColor whiteColor] CGColor];
layer.shadowOffset = CGSizeMake(0, 1);
layer.shadowRadius = 0.5;
layer.shadowOpacity = 0.2;
[self.layer addSublayer:layer];
UIGraphicsBeginImageContext(layer.bounds.size);
[layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[viewImage drawAtPoint:CGPointMake(0, 0)];
[layer removeFromSuperlayer];
With the code above, what I am getting is the box, but without a few extra pixels of padding with the shadow (on all four sides). If I increase the size of the ImageContext, all I get is more height and width, but still starting from x=0 and y=0, where I would want it to start from x=-5, y=-5 or something like that.
Thanks a bunch!
Make your context 10 pixels larger in both the x and the y.
Then before you draw your layer do a CGContextTranslateCTM(context,5,5);
Hope this helps!
I'm not sure if the following is your problem, but it just caught my eye.
[self.layer addSublayer:layer];
UIGraphicsBeginImageContext(layer.bounds.size);
[layer renderInContext:UIGraphicsGetCurrentContext()];
It looks like you have a property on this file called layer, which you are adding the shadow layer you've created to. You then get the context based on the current CALayer, as opposed to self.layer.
I typically use CALayer to modify UIImageView, and do not have a need to add sub layers, so perhaps that code is fine. Just wanted to point it out, please do let me know if which way is accurate, as I always like to learn.
~Good Luck

use a CALayer as a UIImageView

I would like to create a CALayer bezier path in a circle shape. And I would like to set it like it was a UIImageView (to be able to move it with animation etc.) Is it possible? If so how can I do it? Thank you.
You could always add a CAShapeLayer to your view. During init, do something like (all untested):
CAShapeLayer *circle = [[CAShapeLayer alloc] init];
[self.layer addSublayer:circle];
Then, in drawRect, layoutSubViews, or some other place that seems appropriate to update the layout:
circle.fillColor = [[UIColor redColor].CGColor;
CGMutablePathRef p = CGPathCreateMutable();
CGPathAddEllipseInRect(p, NULL, self.bounds);
circle.path = p;
circle.bounds = self.bounds;
circle.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
I'm not sure what you mean by a "CALayer bezier path" but you have a couple of options here:
Create a subclass of UIView, where you draw your circle using UIBezierPath methods in the drawRect:. This view can then be added to another view as a subview and animated using standard methods. However, you are warned that this may affect performance, (as it has to keep re-drawing) so if that does turn out to be a problem...
Create a graphics context and draw your circle in there. Take an image from this graphics context and set it as the image for a UIImageView. Animate as in answer (1). This way you aren't redrawing all the time so performance would be better, but try with option 1 first as it is simpler, and if you have simple drawing code it shouldn't affect you too much.
For option 2, you begin a graphics context using UIGraphicsBeginImageContextWithOptions - you then do your drawing, and extract the image using UIGraphicsGetImageFromCurrentImageContext, then end the context using UIGraphicsEndImageContext. Those functions are all documented here
Sample code to do this:
CGFloat radius = 50;
CGSize size = CGSizeMake(radius*2,radius*2);
CGPoint centre = CGPointMake(radius,radius);
UIGraphicsBeginImageContextWithOptions(size,NO, 0.0);
UIBezierPath * solidPath = [UIBezierPath bezierPathWithArcCenter:centre radius:edgeSize/20 startAngle:0 endAngle:2 * M_PI clockwise:YES];
[solidPath closePath];
[[UIColor whiteColor] set];
[solidPath fill];
UIImage *circle = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *imageView = [[UIImageView alloc] initWithImage:circle];

UIView animation to slide down

I'm trying to slide down an image in an UIImageView, but I don't know which combination of UIContentMode and animation property is the right one to make that happen.
The image should always have the same size and should not be streched... all I want is, that nothing is visible first and then the frame extends and reveals the image.
Maybe it's easier if you see what I mean:
So, it sounds rather easy, but what UIContentMode should I use for the UIImageView and what property should I animate? Thank you!
I took your lead and made a screencast as well. Was this what you had in mind?
I put the animation repeating indefinitely so it would be easier to capture with a video, but it can be started at the press of a button, as well as frozen in place, showing the popover and its contents, until reversed to be hidden again.
I used Core Animation for that, instead of animating a UIView, since I wanted to use the mask property of CALayer to hide the popover and reveal it with a sliding animation.
Here is the code I used (same as in the video):
- (void)viewDidLoad
{
// Declaring the popover layer
CALayer *popover = [CALayer layer];
CGFloat popoverHeight = 64.0f;
CGFloat popoverWidth = 200.0f;
popover.frame = CGRectMake(50.0f, 100.0f, popoverWidth, popoverHeight);
popover.contents = (id) [UIImage imageNamed:#"popover.png"].CGImage;
// Declaring the mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = CGRectMake(0, - popoverHeight, popoverWidth, popoverHeight);
maskLayer.backgroundColor = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f].CGColor;
// Setting the animation (animates the mask layer)
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
animation.byValue = [NSNumber numberWithFloat:popoverHeight];
animation.repeatCount = HUGE_VALF;
animation.duration = 2.0f;
[maskLayer addAnimation:animation forKey:#"position.y"];
//Assigning the animated maskLayer to the mask property of your popover
popover.mask = maskLayer;
[self.view.layer addSublayer:popover];
[super viewDidLoad];
}
NOTE: You have to import the QuartzCore framework into your project and write this line in your header file: #import <QuartzCore/QuartzCore.h>.
Tells if this works for you or if you need any more help setting this up.
Try this code.
Consider the UIImageView as imageView.
imageView.contentMode = UIViewContentModeScaleAspectFill;
CGRect imageRect = imageView.frame;
CGRect origImgRect = imageRect;
imageRect.size.height = 5;
imageView.frame = imageRect;
[UIView animateWithDuration:2.0
animations:^{imageView.rect = origImgRect;}
completion:^(BOOL finished){ }];

Failed Attempt to use Perspective Transformations in Core Animation

I'm trying to create a simple iPhone app that displays a picture with a reflection beneath, and I want to rotate the picture around the X axis, using Core Animation.
I've started by creating a new iPhone app using the "View-based Application" template. I added a "Picture.jpg" image file, and then I added this to the view controller:
- (void)awakeFromNib {
CGFloat zDistance = 1500.0f;
// Create perspective transformation
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -zDistance;
// Create perspective transform for reflected layer
CATransform3D reflectedTransform = CATransform3DMakeRotation(M_PI, 1.0f, 0.0f, 0.0f);
reflectedTransform.m34 = 1.0f / -zDistance;
// Create spinning picture
CALayer *spinningLayer = [CALayer layer];
spinningLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
spinningLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
spinningLayer.transform = transform;
// Create reflection of spinning picture
CALayer *reflectionLayer = [CALayer layer];
reflectionLayer.frame = CGRectMake(60.0f, 360.0f, 200.0f, 300.0f);
reflectionLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
reflectionLayer.opacity = 0.4f;
reflectionLayer.transform = reflectedTransform;
// Add layers to the root layer
[self.view.layer addSublayer:spinningLayer];
[self.view.layer insertSublayer:reflectionLayer below:spinningLayer];
// Spin the layers
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
anim.duration = 3.0f;
anim.repeatCount = 1e100f;
[spinningLayer addAnimation:anim forKey:#"anim"];
[reflectionLayer addAnimation:anim forKey:#"anim"];
}
This almost works. The problem is that the spinning of the main picture and of the reflection are not perfectly synchronized. It's very close to perfect, but the edges of the bottom of the picture and of top of the reflection are a few pixels apart. They appear to differ by a few degrees.
On the left side of the screen, the reflection seems to be "ahead" of the picture, and on the right side, the reflection is behind the picture. In other words, it looks like the bottom corners of the top image are pushed toward the screen, while the top corners of the reflection are pulled toward the viewer. This is true both when looking at the front and at the back of the images as they spin.
If I increase zDistance, the effect is less noticeable. If I eliminate the perspective transformations altogether by leaving transform.m34 equal to zero for both layers, then the picture and reflection look to be perfectly synchronized. So I don't think the problem is related to time-synchronization issues.
I suspect my perspective transformations are missing something, but I'm not sure how to determine what's wrong. I think I'm basically doing the same transformation described in this related Stack Overflow question.
Can someone help?
I think I may have found an answer to my question. In my original code, I created the reflection by placing it underneath the picture, and applying the flip and perspective. The right way to do it seems to be to place the reflection at the same position as the picture, apply translation and rotation transforms to get it to the right place, and then apply the perspective transformation.
When animating the spin, it is necessary to spin the reflection in the opposite direction of the picture's animation.
So, here's code that works:
- (void)awakeFromNib {
CGFloat zDistance = 1500.0f;
// Create perspective transformation
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -zDistance;
// Create perspective transform for reflected layer
CATransform3D reflectedTransform = CATransform3DMakeTranslation(0.0f, 300.0f, 0.0f);
reflectedTransform = CATransform3DRotate(reflectedTransform, M_PI, 1.0f, 0.0f, 0.0f);
reflectedTransform.m34 = 1.0f / -zDistance;
// Create spinning picture
CALayer *spinningLayer = [CALayer layer];
spinningLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
spinningLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
spinningLayer.transform = transform;
// Create reflection of spinning picture
CALayer *reflectionLayer = [CALayer layer];
reflectionLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
reflectionLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
reflectionLayer.opacity = 0.4f;
reflectionLayer.transform = reflectedTransform;
// Add layers to the root layer
[self.view.layer addSublayer:spinningLayer];
[self.view.layer insertSublayer:reflectionLayer below:spinningLayer];
// Spin the layers
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
anim.duration = 3.0f;
anim.repeatCount = 1e100f;
[spinningLayer addAnimation:anim forKey:#"anim"];
CABasicAnimation *reflectAnim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
reflectAnim.fromValue = [NSNumber numberWithFloat:0.0f];
reflectAnim.toValue = [NSNumber numberWithFloat:(-2 * M_PI)];
reflectAnim.duration = 3.0f;
reflectAnim.repeatCount = 1e100f;
[reflectionLayer addAnimation:reflectAnim forKey:#"reflectAnim"];
}
I'm not sure exactly why this works. It makes intuitive sense to me that placing the reflection at the same position as the original and then transforming it somehow puts the two images into the same "transformation space", but I'd appreciate it if someone could explain the mathematics behind this.
The full project is available at GitHub: https://github.com/kristopherjohnson/perspectivetest
You can use a CAAnimationGroup object to run two animations on the same time space. That should take care of any synchronization issues.
EDIT: removed silly code.

Gradients on UIView and UILabels On iPhone [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Manually drawing a gradient in iPhone apps?
My application needs to display text in either a UIView or UILabel but the back ground must be a gradient as opposed to a true UIColor. Using a graphics program to create desired look is no good as the text may vary depending on data returned from a server.
Does anyone know the quickest way to tackle this?
Your thoughts are greatly appreciated.
I realize this is an older thread, but for future reference:
As of iPhone SDK 3.0, custom gradients can be implemented very easily, without subclassing or images, by using the new CAGradientLayer:
UIView *view = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 100)] autorelease];
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = view.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor blackColor] CGColor], (id)[[UIColor whiteColor] CGColor], nil];
[view.layer insertSublayer:gradient atIndex:0];
Take a look at the CAGradientLayer docs. You can optionally specify start and end points (in case you don't want a linear gradient that goes straight from the top to the bottom), or even specific locations that map to each of the colors.
You can use Core Graphics to draw the gradient, as pointed to in Mike's response. As a more detailed example, you could create a UIView subclass to use as a background for your UILabel. In that UIView subclass, override the drawRect: method and insert code similar to the following:
- (void)drawRect:(CGRect)rect
{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGGradientRef glossGradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 1.0, 1.0, 0.35, // Start color
1.0, 1.0, 1.0, 0.06 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGRect currentBounds = self.bounds;
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGContextDrawLinearGradient(currentContext, glossGradient, topCenter, midCenter, 0);
CGGradientRelease(glossGradient);
CGColorSpaceRelease(rgbColorspace);
}
This particular example creates a white, glossy-style gradient that is drawn from the top of the UIView to its vertical center. You can set the UIView's backgroundColor to whatever you like and this gloss will be drawn on top of that color. You can also draw a radial gradient using the CGContextDrawRadialGradient function.
You just need to size this UIView appropriately and add your UILabel as a subview of it to get the effect you desire.
EDIT (4/23/2009): Per St3fan's suggestion, I have replaced the view's frame with its bounds in the code. This corrects for the case when the view's origin is not (0,0).
Note: The results below apply to older versions of iOS, but when testing on iOS 13 the stepping doesn't occur. I don't know for which version of iOS the stepping was removed.
When using CAGradientLayer, as opposed to CGGradient, the gradient is not smooth, but has noticeable stepping to it. See :
To get more attractive results it is better to use CGGradient.
You could also use a graphic image one pixel wide as the gradient, and set the view property to expand the graphic to fill the view (assuming you are thinking of a simple linear gradient and not some kind of radial graphic).
Mirko Froehlich's answer worked for me, except when i wanted to use custom colors. The trick is to specify UI color with Hue, saturation and brightness instead of RGB.
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = myView.bounds;
UIColor *startColour = [UIColor colorWithHue:.580555 saturation:0.31 brightness:0.90 alpha:1.0];
UIColor *endColour = [UIColor colorWithHue:.58333 saturation:0.50 brightness:0.62 alpha:1.0];
gradient.colors = [NSArray arrayWithObjects:(id)[startColour CGColor], (id)[endColour CGColor], nil];
[myView.layer insertSublayer:gradient atIndex:0];
To get the Hue, Saturation and Brightness of a color, use the in built xcode color picker and go to the HSB tab. Hue is measured in degrees in this view, so divide the value by 360 to get the value you will want to enter in code.
This is what I got working- set UIButton in xCode's IB to transparent/clear, and no bg image.
UIColor *pinkDarkOp = [UIColor colorWithRed:0.9f green:0.53f blue:0.69f alpha:1.0];
UIColor *pinkLightOp = [UIColor colorWithRed:0.79f green:0.45f blue:0.57f alpha:1.0];
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = [[shareWordButton layer] bounds];
gradient.cornerRadius = 7;
gradient.colors = [NSArray arrayWithObjects:
(id)pinkDarkOp.CGColor,
(id)pinkLightOp.CGColor,
nil];
gradient.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:0.7],
nil];
[[recordButton layer] insertSublayer:gradient atIndex:0];
I achieve this in a view with a subview that is an UIImageView. The image the ImageView is pointing to is a gradient. Then I set a background color in the UIView, and I have a colored gradient view. Next I use the view as I need to and everything I draw will be under this gradient view. By adding a second view on top of the ImageView, you can have some options whether your drawing will be below or above the gradient...