graphic context of rotated layers ignores any transformations - iphone

I'm composing a view by several layers, some of them rotated. The display of these layers works perfectly, however when I try to make an image, all rotations/transformations are ignored.
For example if I have a CALayer containing an image of an arrow pointing to the left, after a rotation around the z axis (using CATransform3DMakeRotation) it points to the top, and the layer is correctly displayed. However if I get an image of this layer (using renderInContext), the arrow still points to the left, ignoring any transformations.
Anyone any idea why, and what do I have to do to get my wanted result? :-)
Example code:
// ViewController.m
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
// Don't forget to add the QuartzCore framework to your target.
#interface ViewController ()
#end
#implementation ViewController
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view.backgroundColor = [UIColor lightGrayColor];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Get the arrow image (tip to the left, 50 x 50 pixels).
// (Download the image from here: http://www.4shared.com/photo/uQfor7vC/arrow.html )
UIImage *arrowImage = [UIImage imageNamed:#"arrow"];
// The arrow layer in it's starting orientation (tip to the left).
CGRect layerFrame = CGRectMake(50.0, 50.0, arrowImage.size.width, arrowImage.size.height);
CALayer *arrowLayerStart = [CALayer new];
arrowLayerStart.frame = layerFrame;
[arrowLayerStart setContents:(id)[arrowImage CGImage]];
[self.view.layer addSublayer:arrowLayerStart];
// The arrow layer rotated around the z axis by 90 degrees.
layerFrame.origin = CGPointMake(layerFrame.origin.x + layerFrame.size.width + 25.0, layerFrame.origin.y);
CALayer *arrowLayerZ90 = [CALayer new];
arrowLayerZ90.frame = layerFrame;
[arrowLayerZ90 setContents:(id)[arrowImage CGImage]];
arrowLayerZ90.transform = CATransform3DMakeRotation(M_PI/2.0, 0.0, 0.0, 1.0);
[self.view.layer addSublayer:arrowLayerZ90];
// Now make images of each of these layers and display them in a second row.
// The starting layer without any rotation.
UIGraphicsBeginImageContext(arrowLayerStart.frame.size);
[arrowLayerStart renderInContext:UIGraphicsGetCurrentContext()];
UIImage *arrowStartImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
layerFrame.origin = CGPointMake(arrowLayerStart.frame.origin.x,
arrowLayerStart.frame.origin.y + arrowLayerStart.frame.size.height + 25.0);
CALayer *arrowLayerStartCaptured = [CALayer new];
arrowLayerStartCaptured.frame = layerFrame;
[arrowLayerStartCaptured setContents:(id)[arrowStartImage CGImage]];
[self.view.layer addSublayer:arrowLayerStartCaptured];
// The second layer, rotated around the z axis by 90 degrees.
UIGraphicsBeginImageContext(arrowLayerZ90.frame.size);
[arrowLayerZ90 renderInContext:UIGraphicsGetCurrentContext()];
UIImage *arrowZ90Image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
layerFrame.origin = CGPointMake(arrowLayerZ90.frame.origin.x,
arrowLayerZ90.frame.origin.y + arrowLayerZ90.frame.size.height + 25.0);
CALayer *arrowLayerZ90Captured = [CALayer new];
arrowLayerZ90Captured.frame = layerFrame;
[arrowLayerZ90Captured setContents:(id)[arrowZ90Image CGImage]];
[self.view.layer addSublayer:arrowLayerZ90Captured];
}
#end
// ViewController.h
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
// AppDelegate.h
#import <UIKit/UIKit.h>
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#end
// AppDelegate.m
#import "AppDelegate.h"
#import "ViewController.h"
#implementation AppDelegate
#synthesize window = _window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = [[ViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
#end

The documentation for -[CALayer renderInContext:] says:
The Mac OS X v10.5 implementation of this method does not
support the entire Core Animation composition model.
QCCompositionLayer, CAOpenGLLayer, and QTMovieLayer layers are not
rendered. Additionally, layers that use 3D transforms are not
rendered, nor are layers that specify backgroundFilters, filters,
compositingFilter, or a mask values. Future versions of Mac OS X may
add support for rendering these layers and properties.
(This limitation is present on iOS, too.)
The header CALayer.h also says:
* WARNING: currently this method does not implement the full
* CoreAnimation composition model, use with caution. */
Essentially, -renderInContext: is useful in a few simple cases, but will not work as expected in more complicated situations. You will need to find some other way to do your drawing. Quartz (Core Graphics) will work for 2D, but for 3D you're on your own.

You can use Core Graphaic instead of CATransform3DMakeRotation :)
CGAffineTransform flip = CGAffineTransformMakeScale(1.0, -1.0);

Related

How to render UIImage (picked from photo library) into CIContext / GLKView?

After researching and trying many things finally I made myself to ask SO:
basically I would like to pick a photo and then render it to a CIcontext, knowing that many other image rendering techniques available (eg. use UIImageView, etc), I have to use low-level OpenGL ES rendering.
Please check my full code (except the preload of UIImagePickerController in AppDelegate)
#import "ViewController.h"
#import "AppDelegate.h"
#import <GLKit/GLKit.h>
#interface ViewController () <GLKViewDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate> {
GLKView *glkview;
CGRect glkview_bounds;
}
- (IBAction)click:(id)sender;
#property (strong, nonatomic) EAGLContext *eaglcontext;
#property (strong, nonatomic) CIContext *cicontext;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.eaglcontext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
glkview = [[GLKView alloc] initWithFrame:[[UIScreen mainScreen] bounds] context:self.eaglcontext];
glkview.drawableDepthFormat = GLKViewDrawableDepthFormat24;
glkview.enableSetNeedsDisplay = YES;
glkview.delegate = self;
[self.view addSubview:glkview];
[glkview bindDrawable];
glkview_bounds = CGRectZero;
glkview_bounds.size.width = glkview.drawableWidth;
glkview_bounds.size.height = glkview.drawableHeight;
NSLog(#"glkview_bounds:%#", NSStringFromCGRect(glkview_bounds));
self.cicontext = [CIContext contextWithEAGLContext:self.eaglcontext options:#{kCIContextWorkingColorSpace : [NSNull null]} ];
UIAppDelegate.g_mediaUI.delegate = self;
}
- (IBAction)click:(id)sender {
[self presentViewController:UIAppDelegate.g_mediaUI animated:YES completion:nil];
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:NO completion:nil];
UIImage *pre_img = (UIImage *)[info objectForKey:UIImagePickerControllerOriginalImage];
CIImage *outputImage = [CIImage imageWithCGImage:pre_img.CGImage];
NSLog(#"orient:%d, size:%#, scale:%f, extent:%#", (int)pre_img.imageOrientation, NSStringFromCGSize(pre_img.size), pre_img.scale,NSStringFromCGRect(outputImage.extent));
if (outputImage) {
if (self.eaglcontext != [EAGLContext currentContext])
[EAGLContext setCurrentContext:self.eaglcontext];
// clear eagl view to grey
glClearColor(0.5, 0.5, 0.5, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
// set the blend mode to "source over" so that CI will use that
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
[self.cicontext drawImage:outputImage inRect:glkview_bounds fromRect:[outputImage extent]];
[glkview display];
}
}
#end
What works: picked images having sizes eg. size 1390x1390 or 1440x1440 are displayed.
What doesn't work: picked images with size of 2592x1936 are not displayed, basically all large pictures which taken with the camera.
Please help finding out the solution, I'm stucked here...
OpenGL has a texture limit, depending on the iOS device it can be 2048x2048.
You can check the max texture limit for the device using the following code:
static GLint maxTextureSize = 0;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
Older devices (before iPhone 4S) all have a maximum texture size of 2048x2048. A5 and above (after and including iPhone 4S) have a maximum 4096x4096 texture size.
Source: Black texture with OpenGL when image is too big

how to customize MKPolyLineView to draw different style lines

I want to customize the lines drawn on MKMapView to show a route so that the lines have a border color and a fill color. Similar to this where it has a black border and is filled with another color:
I'm currently just returning MKPolyLineView objects from mapView:viewForOverlay: which works fine for plain lines. The docs says the MKPolyLineView is not to be subclassed, so should I subclass MKOverlayView and implement my own drawMapRect? Or should I subclass MKOverlayPathView? Or create a replacement for MKPolylineView?
EDIT - what I'm asking is: where is the place to put your own Quartz drawing code in order to draw your own annotations/overlays? Currently I've created a subclass of MKOverlayView and implement my own drawMapRect:zoomScale:inContext: It's pretty easy to draw the overlay that way but is that the best solution?
You can do this by implementing your own MKOverlayPathView subclass, which draws the path twice in the map rect. Once thicker with black and once thinner on top with another colour.
I have created a simple drop-in replacement of MKPolylineView which lets you do that: ASPolylineView.
If you want to do it yourself, the two main methods that you need to implement could look like this:
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
UIColor *darker = [UIColor blackColor];
CGFloat baseWidth = self.lineWidth / zoomScale;
// draw the dark colour thicker
CGContextAddPath(context, self.path);
CGContextSetStrokeColorWithColor(context, darker.CGColor);
CGContextSetLineWidth(context, baseWidth * 1.5);
CGContextSetLineCap(context, self.lineCap);
CGContextStrokePath(context);
// now draw the stroke color with the regular width
CGContextAddPath(context, self.path);
CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
CGContextSetLineWidth(context, baseWidth);
CGContextSetLineCap(context, self.lineCap);
CGContextStrokePath(context);
[super drawMapRect:mapRect zoomScale:zoomScale inContext:context];
}
- (void)createPath
{
// turn the polyline into a path
CGMutablePathRef path = CGPathCreateMutable();
BOOL pathIsEmpty = YES;
for (int i = 0; i < self.polyline.pointCount; i++) {
CGPoint point = [self pointForMapPoint:self.polyline.points[i]];
if (pathIsEmpty) {
CGPathMoveToPoint(path, nil, point.x, point.y);
pathIsEmpty = NO;
} else {
CGPathAddLineToPoint(path, nil, point.x, point.y);
}
}
self.path = path;
}
You can just add two MKPolyLineView objects with the same coordinates, but different thicknesses.
Add one with a lineWidth of 10 (or whatever) with strokeColor set to black.
Then add another with a lineWidth of 6 with strokeColor set to your other desired color.
You can use the same MKPolyLine for both MKPolyLineView objects.
MKPolylineView can only be used for stroking a designated path. You can use some of the properties in MKOverlayPathView to change their appearance but only some of them would apply, e.g. fillColor, strokeColor.
If you want to draw something more complex, you can use MKOverlayPathView. It is more generic and thus suited for more than just stroking paths. For drawing simple lines, the result would be identical to MKPolylineView (at least, according to the docs).
If you want to do more complex drawing, subclass MKOverlayPathView. What you're trying to do is non-trivial.
I use a subclass NamedOverlay that holds an overlay an a name:
NamedOverlay.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#interface NamedOverlay : NSObject <MKOverlay>
#property (strong, readonly, nonatomic) NSString *name;
#property (strong, readonly, nonatomic) id<MKOverlay> overlay;
-(id)initWithOverlay:(id<MKOverlay>)overlay andName:(NSString *)name;
#end
NamedOverlay.m
#import "NamedOverlay.h"
#implementation NamedOverlay
- (id)initWithOverlay:(id<MKOverlay>)overlay andName:(NSString *)name
{
_name = name;
_overlay = overlay;
return self;
}
- (MKMapRect)boundingMapRect
{
return [_overlay boundingMapRect];
}
- (CLLocationCoordinate2D)coordinate
{
return [_overlay coordinate];
}
-(BOOL)intersectsMapRect:(MKMapRect)mapRect
{
return [_overlay intersectsMapRect:mapRect];
}
#end
and in the map controller I instantiate two overlays with different name, then in the MKMapViewDelegate I can identify which overlay I want to draw and do something like:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id < MKOverlay >)overlay
{
NamedOverlay *namedOverlay = (NamedOverlay *) overlay;
MKPolyline *polyline = namedOverlay.overlay;
if ([namedOverlay.name isEqualToString:#"top"]) {
MKPolylineView *view1 = [[MKPolylineView alloc] initWithOverlay:polyline];
view1.strokeColor = [UIColor whiteColor];
view1.lineWidth = 25.0;
return view1;
} else {
MKPolylineView *view1 = [[MKPolylineView alloc] initWithOverlay:polyline];
view1.strokeColor = [UIColor blueColor];
view1.lineWidth = 15.0;
return view1;
}
}
I know that this may not match the pure approach you want, but why not using MKPolygon instead of a MKPolyLine ?
Create a MKPolygon instance that represents a kind of corridor around your route, and then , when you create the MKPolygonView that corresponds to the MKPolygon/corridor you've created, set the properties of the MKPolygonView to get a different fill color and strokeColor
myPolygonView.lineWidth=3;
myPolygonView.fillColor=[UIColor blueColor];
myPolygonView.strokeColor=[UIColor darkGrayColor];
I didn't try it myself but this should work. Only drawback is that when you zoom in / out, the 'width' of the 'route' will change.... :/

Inheriting UIView - Losing instance variables

Bit of an Objective-C rookie, I've looked around for an answer but haven't been able to find one so forgive me if this is an obvious question.
Basically, I need to draw on the screen segments of a circle (for instance, a 90 degree segment, where a horizontal and vertical line meet at the bottom left and an arc connects the end points). I've managed to achieve this in a custom class called CircleSegment that inherits UIView and overrides drawRect.
My issue is achieving this programatically; I need some way of creating my CircleSegment class and storing in it the desired angle before it draws the segment itself.
Here's what I have so far:
CircleSegment.h
#import <UIKit/UIKit.h>
#interface CircleSegment : UIView {
float angleSize;
UIColor *backColor;
}
-(float)convertDegToRad:(float)degrees;
-(float)convertRadToDeg:(float)radians;
#property (nonatomic) float angleSize;
#property (nonatomic, retain) UIColor *backColor;
#end
CircleSegment.m
#import "CircleSegment.h"
#implementation CircleSegment
#synthesize angleSize;
#synthesize backColor;
// INITIALISATION OVERRIDES
// ------------------------
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)setBackgroundColor:(UIColor *)newBGColor
{
// Ignore.
}
- (void)setOpaque:(BOOL)newIsOpaque
{
// Ignore.
}
// MATH FUNCTIONS
// --------------
// Converts degrees to radians.
-(float)convertDegToRad:(float)degrees {
return degrees * M_PI / 180;
}
// Converts radians to degrees.
-(float)convertRadToDeg:(float)radians {
return radians * 180 / M_PI;
}
// DRAWING CODE
// ------------
- (void)drawRect:(CGRect)rect {
float endAngle = 360 - angleSize;
UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100)
radius:100
startAngle:[self convertDegToRad:270]
endAngle:[self convertDegToRad:endAngle]
clockwise:YES];
[aPath addLineToPoint:CGPointMake(100.0, 100.0)];
[aPath closePath];
CGContextRef aRef = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(aRef, backColor.CGColor);
CGContextSaveGState(aRef);
aPath.lineWidth = 1;
[aPath fill];
[aPath stroke];
//CGContextRestoreGState(aRef);
}
- (void)dealloc {
[super dealloc];
}
#end
Note that the .m file is a bit messy with various bits of test code...
So essentially I want to create an instance of CircleSegment, store an angle in the angleSize property, draw a segment based on that angle and then add that view to the main app view to display it...
To try and achieve that, I've add the following test code to viewDidLoad on my ViewController:
CircleSegment *seg1 = [[CircleSegment alloc] init];
seg1.backColor = [UIColor greenColor];
seg1.angleSize = 10;
[self.view addSubview:seg1];
It seems to store the UIColor and angleSize fine when I breakpoint those places, however if I place a breakpoint in drawRect which I overrode on CircleSegment.m, the values have reverted back to nil values (or whatever the correct term would be, feel free to correct me).
I'd really appreciate it if someone could point me in the right direction!
Thanks
It seems to store the UIColor and
angleSize fine when I breakpoint those
places, however if I place a
breakpoint in drawRect which I
overrode on CircleSegment.m, the
values have reverted back to nil
values (or whatever the correct term
would be, feel free to correct me).
Are you sure you are working with the same instance?
Drop in something like NSLog(#"%# %p", NSStringFromSelector(_cmd), self); in viewDidLoad and in drawRect and make sure you are actually dealing with the same instance.
(I've seen cases where a developer will alloc/initWithFrame: a view and then use the instance that was created during NIB loading, wondering why that instance wasn't properly initialized.)
Try using self.angleSize in your DrawRect method (same with backColor). In your test method, replace 10 with 10.0 (to ensure it's setting it as a float).
Also, don't forget to release backColor in your dealloc.

Effect like Interface Builder connection lines on iPhone

Hi I'd like little bit of help on something. In my app, I have a UITableView which is populated with custom cells that represent images. (i.e. selecting a row displays a picture in an image view).
What I would like to do, is have an icon in the custom cell that I could drag to one of a series of image views. Similar to the way you can drag a line in IB to set connections. Once the user releases their finger I will have it check what part of the screen they released it and if it is one one of these rects that represent the picture frames, it will populate the picture frame with the image and the line will disappear.
I have never drawn lines in my app before so thats not something I know how to do (so im just looking for a link to a tutorial or class definition) and second, what problems will I have since the start point of the line is in a UITableViewCell and the end point is in the main view?
I have actually done this before, so I can give you the exact code :D
This is only the drawing part, you implement the touch events (in a separate class, otherwise remove the self.userInteractionEnabled = NO; in .m file
The BOOL dragged tells the LineView if the line should be drawn.
LineView.h
#import <UIKit/UIKit.h>
#interface LineView : UIView
{
CGPoint start;
CGPoint end;
BOOL dragged;
}
#property CGPoint start, end;
#property BOOL dragged;
#end
LineView.m
#import "LineView.h"
#implementation LineView
#synthesize start, end, dragged;
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame: frame])
{
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
#pragma mark Setters
-(void) setStart: (CGPoint) s
{
start = s;
[self setNeedsDisplay];
}
-(void) setEnd: (CGPoint) e
{
end = e;
[self setNeedsDisplay];
}
#define LINE_COLOR [UIColor redColor]
#define CIRCLE_COLOR [UIColor redColor]
#define LINE_WIDTH 5
#define CIRCLE_RADIUS 10
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
if(dragged) {
[LINE_COLOR setStroke];
CGContextMoveToPoint(c, start.x, start.y);
CGContextAddLineToPoint(c, end.x, end.y);
CGContextSetLineWidth(c, LINE_WIDTH);
CGContextClosePath(c);
CGContextStrokePath(c);
[CIRCLE_COLOR setFill];
CGContextAddArc(c, start.x, start.y, CIRCLE_RADIUS, 0, M_PI*2, YES);
CGContextClosePath(c);
CGContextFillPath(c);
CGContextAddArc(c, end.x, end.y, CIRCLE_RADIUS, 0, M_PI*2, YES);
CGContextClosePath(c);
CGContextFillPath(c);
}
}
- (void)dealloc
{
[super dealloc];
}
#end

How to change iphone CATiledLayer fadeDuration?

I am working on an iphone application that displays tiled maps. I am currently using a CATiledLayer in a UIScrollView :
MyTiledDelegate *delegate=[[MyTiledDelegate alloc] initWithMapLayer:map];
tileLayer = [CATiledLayer layer];
tileLayer.delegate = delegate;
[scrollView.layer addSublayer:tileLayer];
[tileLayer setNeedsDisplay];
I wrote and set my own delegate which implements the draw layer method like so :
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
CGRect rect =CGContextGetClipBoundingBox(ctx);
CGFloat x = fabs(round(rect.origin.x/tileSize));
CGFloat y = fabs(round(rect.origin.y/tileSize));
Tile *tile = [map getTileForMapZoom:z x:x y:y];
CGImageRef img=[tile getRealImage];
CGContextDrawImage(
ctx,
CGRectMake(tile.x*tileSize,tile.y*tileSize, tileSize,tileSize) ,
img);
}//edited for brevity
I am annoyed by the default behavior of the CAtiledLayer to fadein after the tile is drawn. Also, sometimes the fadein is not complete (it stops at 90 or 95% opacity).
How can i change or (preferably) remove the fadein animation ?
I played with the speed and duration properties of my CATiledLayer instance, to no avail. I don't set any animation on the tiledLayer. the [tiledLayer removeAllAnimation] does not change anything either.
Thanks for any pointers.
You should subclass the CATiledLayer and return fadeDuration of 0 to disable fade-in:
#interface FastCATiledLayer : CATiledLayer
#end
#implementation FastCATiledLayer
+(CFTimeInterval)fadeDuration {
return 0.0;
}
#end
I also had the problem with fade in animation not completing, what helped was to set the background color of the view to [UIColor clearColor]
Try subclassing and overriding the +fadeDuration accessor on the layer.
Or in Swift 4:
class CAFastTiledLayer: CATiledLayer {
class func fadeDuration() -> CFTimeInterval {
return 0.0
}
}