Create layer mask with custom-shaped hole - iphone

I've spent too much time trying to figure this out and simply cannot find a workable solution.
Situation:
1. A picture of 'something' is displayed on the phone.
2. A semi-transparent (e.g. blue) layer is placed on top of the image, completely covering it.
3. A 'hole' in this layer exists where that part of the layer is fully transparent and is movable.
An example could be a zoom effect where you move this 'hole' around the image. Inside the hole you can see the image normally, while outside it's covered by the semi-transparent layer. NOTE: I'm implementing this in a cocos2d layer, where the image is represented by a CCSprite. It shouldn't matter though, if no cocos is used.
Problem:
I've tried using CAShapeLayer and bitmaps as masks, but nothing is working (see code snippets below). With the CAShapeLayer, I create a UIBezierPath for the 'hole' and apply it to the colored layer. However, only the hole shows the color, while the rest is transparent. With an image, the mask is simply not working (I have no idea why). I've even tried masking masks to see if that would work. I've also tried swapping colors around...from white to black to clear for fill and background.
A simple solution, if it existed, would be to invert the area of the UIBezierPath. I've tried clipping, as well, using the path...but no luck.
I'm hoping that it's something simple-stupid that I'm simply overlooking. Perhaps one of you will see this. The moving part I'm not, yet, concerned with. I need to get the actual mask working first. The sample code is ignoring the y-axis differences between iPhone SDK and openGL.
CAShapeLayer Example:
CGSize winSize = [[CCDirector sharedDirector] winSize];
UIImage* img = [UIImage imageNamed:#"zebra.png"];
CCSprite* spr = [CCSprite spriteWithCGImage:img.CGImage key:#"img"];
spr.position = ccp( winSize.width / 2, winSize.width / 2 );
[self addSprite:spr];
UIBezierPath* path = [UIBezierPath bezierPathWithRect:rectHole];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.bounds = [spr boundingBox];
maskLayer.position = spr.position;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
maskLayer.backgroundColor = [UIColor clearColor].CGColor;
maskLayer.path = path.CGPath;
CALayer* colorLayer = [CALayer layer];
colorLayer.bounds = [spr boundingBox];
colorLayer.position = maskLayer.position;
[colorLayer setMask:maskLayer];
[[[[CCDirector sharedDirector] openGLView] layer] addSublayer:colorLayer];
Multiple Layer Mask Example:
CGSize winSize = [[CCDirector sharedDirector] winSize];
UIImage* img = [UIImage imageNamed:#"zebra.png"];
CCSprite* spr = [CCSprite spriteWithCGImage:img.CGImage key:#"img"];
spr.position = ccp( winSize.width / 2, winSize.width / 2 );
[self addSprite:spr];
UIBezierPath* path = [UIBezierPath bezierPathWithRect:rectHole];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.bounds = [spr boundingBox];
maskLayer.position = spr.position;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
maskLayer.backgroundColor = [UIColor clearColor].CGColor;
maskLayer.path = path.CGPath;
UIBezierPath* pathOuter = [UIBezierPath bezierPathWithRect:img.frame];
CAShapeLayer* outerLayer = [CAShapeLayer layer];
outerLayer.bounds = [spr boundingBox];
outerLayer.position = spr.position;
outerLayer.fillColor = [UIColor blackColor].CGColor;
outerLayer.backgroundColor = [UIColor whiteColor].CGColor;
outerLayer = pathOuter.CGPath;
[outerLayer setMask:maskLayer];
CALayer* colorLayer = [CALayer layer];
colorLayer.bounds = [spr boundingBox];
colorLayer.position = outerLayer.position;
[colorLayer setMask:outerLayer];
[[[[CCDirector sharedDirector] openGLView] layer] addSublayer:colorLayer];
Image Mask Example:
CGSize winSize = [[CCDirector sharedDirector] winSize];
UIImage* img = [UIImage imageNamed:#"zebra.png"];
CCSprite* spr = [CCSprite spriteWithCGImage:img.CGImage key:#"img"];
spr.position = ccp( winSize.width / 2, winSize.width / 2 );
[self addSprite:spr];
CGRect r = [spr boundingBox];
CGSize sz = CGSizeMake( r.size.width, r.size.height );
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
CGContextRef context = CGBitmapContextCreate( NULL, w, h, 8, 0, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaNone );
CGColorSpaceRelease( colorSpace );
CGContextSetFillColorWithColor( context, [UIColor whiteColor].CGColor );
CGContextFillRect( context, r );
CGContextSetFillColorWithColor( context, [UIColor blackColor].CGColor );
CGContextFillRect( context, rectHole );
CGImageRef ref = CGBitmapContextCreateImage( context );
CGContextRelease( context );
CALayer* maskLayer = [CALayer layer];
maskLayer.bounds = [spr boundingBox];
maskLayer.position = spr.position;
[maskLayer setContents:(id)ref];
CALayer* colorLayer = [CALayer layer];
colorLayer.bounds = [spr boundingBox];
colorLayer.position = maskLayer.position;
[colorLayer setMask:maskLayer];
[[[[CCDirector sharedDirector] openGLView] layer] addSublayer:colorLayer];
CGImageRelease( ref );

I came back to this later after learning other core graphics techniques.
The solution is closest to the Multiple Layer Mask Example above.
However, instead of creating an inner and outer layer, you need to combine two paths into a single UIBezierPath in opposite directions.
So, e.g., create a path of the inner area to be cropped (CW). NOTE: x,y,w,h are referring to the origin and size of the "hole".
[path moveToPoint:ccp(x,y)];
[path addLineToPoint:ccp(x+w,y)];
[path addLineToPoint:ccp(x+w,y+h)];
[path addLineToPoint:ccp(x,y+h)];
[path addLineToPoint:ccp(x,y)];
Then, add to the same path the outer area in the opposite direction (CCW). NOTE: x,y,w,h are referring to the origin and size of the outer rect.
[path moveToPoint:ccp(x,y)];
[path addLineToPoint:ccp(x,y+h)];
[path addLineToPoint:ccp(x+w,y+h)];
[path addLineToPoint:ccp(x+w,y)];
[path addLineToPoint:ccp(x,y)];
This path is then applied to a layer (maskLayer), which is used as the mask on the final layer (colorLayer). The "outerLayer" is not needed.

Related

Optimization of drawing lines, possible alternatives to CAShapeLayer

I need to draw many lines (in the range of 50-75) in a screen and currently use the below function for it which works fine. After drawing 40-50 of these lines with the below code, the application slows down noticeably in my iPhone 4. To optimize I tried removing the line shadow it helped but still app wasn't running as smooth as I wanted. I need to optimize the below code, my first idea is to replace the cashapelayers with .png line images. But the new method should support line rotation, different length line with same width, and animation of drawing (it seems a lot for me to do with cgaffinetransforms). Any ideas that can help me?
+ (CAShapeLayer *) drawLineOnView:(UIView *) view BetweenPoint1:(CGPoint) point1 Point2:(CGPoint) point2 lineWidth:(CGFloat)lineWidth lineColor:(UIColor *) color Animated:(BOOL) animed
{
CAShapeLayer *lineShape = [CAShapeLayer layer];
CGMutablePathRef linePath = nil;
linePath = CGPathCreateMutable();
//lineShape.opacity = 0.6;
lineShape.lineWidth = lineWidth;
lineShape.lineCap = kCALineCapRound;
if(color==nil) color = [UIColor orangeColor]; //Default value
lineShape.shadowColor = [color CGColor];
lineShape.shadowOpacity = 1.0;
lineShape.shadowRadius = 5.0;
lineShape.strokeColor = [color CGColor];
CGPathMoveToPoint(linePath, NULL, point1.x, point1.y);
CGPathAddLineToPoint(linePath, NULL, point2.x, point2.y);
if(animed)
{
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[lineShape addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
lineShape.path = linePath;
CGPathRelease(linePath);
[view.layer addSublayer:lineShape];
return lineShape;
}
PARTLY SOLVED (Optimization never ends)
I broke down my line drawing function into 2 complementary parts and draw multiple lines into the one shape layer instead of creating new layers each time. It works much better if not great. Here is the updated code:
+ (CAShapeLayer *) createNewShapeLayerForDrawingLinesOnView:(UIView *) view lineWidth:(CGFloat)lineWidth lineColor:(UIColor *) color
{
CAShapeLayer *lineShape = [CAShapeLayer layer];
//lineShape.opacity = 0.6;
lineShape.lineWidth = lineWidth;
lineShape.lineCap = kCALineCapRound;
if(color==nil) color = [UIColor orangeColor]; //Default value
lineShape.shadowColor = [color CGColor];
lineShape.shadowOpacity = 1.0;
lineShape.shadowRadius = 5.0;
lineShape.strokeColor = [color CGColor];
[view.layer addSublayer:lineShape];
return lineShape;
}
+ (void) addNewLineToShapeLayer:(CAShapeLayer *) shapeLayer BetweenPoint1:(CGPoint) point1 Point2:(CGPoint) point2
{
CGMutablePathRef combinedPath = CGPathCreateMutableCopy(shapeLayer.path);
CGMutablePathRef linePath = CGPathCreateMutable();
CGPathMoveToPoint(linePath, NULL, point1.x, point1.y);
CGPathAddLineToPoint(linePath, NULL, point2.x, point2.y);
//No paths drawn before
if(combinedPath == NULL)
{
combinedPath = linePath;
}
else
{
CGPathAddPath(combinedPath, NULL, linePath);
}
shapeLayer.path = combinedPath;
CGPathRelease(linePath);
}
While I understand the want to create multiple layers, it will be much more efficient to draw all the lines into one and to manage animations and rotations of a list of lines from there. You can do this in a shape layer with a combined path like(missing code marked with "..."):
CGMutablePathRef combinedPath = CGPathCreateMutableCopy(path.CGPath);
for(...)
CGPathAddPath(combinedPath, NULL, [self makeNewPathFrom:...].CGPath);
myLayer.path = combinedPath;
Even faster, you can draw the list of lines directly onto the graphics context of a CALayer. This example for a view's drawRect: method is untested but should give you an idea:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, lineWidth);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0); //red
for(MyLine *line in lines)
{
CGContextMoveToPoint(context, point1.x, point1.y);
CGContextAddLineToPoint(context, point2.x, point2.y);
}
If you need further optimization, you should look into OpenGL.
You definitely do not want 75 layers, each with their own line. Are you sure you can't draw a single, more complex, path in a single layer?

CAShapeLayer isn't rounding corners and stroke isn't on top

Hi guys I've creates this box, basically the corner radius isn't working & the stroke doesn't show on the top side of the shape either. Is there anyway to get this working?
UIBezierPath* conPath = [UIBezierPath bezierPath];
[conPath moveToPoint:CGPointMake(conX, conY)];
[conPath addLineToPoint:CGPointMake(conX, conY+50)];
[conPath addLineToPoint:CGPointMake(conX+220, conY+50)];
[conPath addLineToPoint:CGPointMake(conX+220, conY)];
CAShapeLayer* conLayer = [CAShapeLayer layer];
conLayer.path = conPath.CGPath;
conLayer.cornerRadius = 5.0;
UIColor *bg = [UIColor colorWithWhite:1 alpha:0.7];
[conLayer setFillColor:bg.CGColor];
[conLayer setStrokeColor:[UIColor grayColor].CGColor];
[[self layer] addSublayer:conLayer];

Get UIBezierpath from masking an UIImage

How can I get UIBezierpath from UIImage like this..
I do not want full square path, Just want that path as shown in red border from UIImage..
I had gone through Link 1 and Link 2 but can't got success..
I had done following code to get masking path..
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = imgView.frame;
UIBezierPath *roundedPath = [UIBezierPath bezierPathWithRoundedRect:maskLayer.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(50.f, 50.f)];
Basically I want to get UIBezierpath that follows the shape of alphabets like A,B.. etc.
Please tell me is this possible or not to get this type of path??
Thanks in advance..
For other users looking for answers, an alternative approach can be found here (Using SVG as a source image file and PocketSVG to convert to paths)
Try this code:
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation
animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 15.0;
pathAnimation.repeatCount = 1;
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, x+15, y);
CGPathAddQuadCurveToPoint(curvedPath, NULL, 20, 10, 100, 330);
pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);
Yourimage.center=CGPointMake(x, y);
[Yourimage.layer addAnimation:pathAnimation forKey:#"moveTheSquare"];

Is it possible to add a background Image to a CAShape? (iPhone)

I am trying to distort an image in an arbitrary way, following for example a bezier shape. I have used CAShapeLayer to create the shape, but then it seems that setting its contents to an Image does not work. How can I have the image distorted following that shape? Is it even possible?
Thank you!
it is not possible to do so, but i succeeded in doing so by putting a CALayer on top a CAShapeLayer.
this is my code:
CALayer *image =[CALayer layer];
image.frame = CGRectMake(0, 0, 75, 75);
image.contents = (id) [UIImage imageNamed:#"number-1"].CGImage;
[self.layer addSublayer:image];
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 75, 75)];
blueCircleLayer = [CAShapeLayer layer];
blueCircleLayer.path = circle.CGPath;
blueCircleLayer.strokeColor = [UIColor blueColor].CGColor;
blueCircleLayer.fillColor = [UIColor clearColor].CGColor;
blueCircleLayer.shadowColor =[UIColor blackColor].CGColor;
blueCircleLayer.shadowOffset = CGSizeMake(0.0f, 5.0f);
blueCircleLayer.shadowOpacity = 0.7f;
blueCircleLayer.lineWidth = 7.0;
[self.layer addSublayer:blueCircleLayer];
This is currently not possible. I would suggest filing a radar (http://bugreport.apple.com) if you think this should be supported.
See also: Can a CGImage be added to the contents property of a CAShapeLayer?

Drawing a transparent circle on top of a UIImage - iPhone SDK

I am having a lot of trouble trying to find out how to draw a transparent circle on top of a UIImage within my UIImageView. Google-ing gives me clues, but I still can't find a working example.
Are there any examples that anyone knows of that demonstrate this?
Easiest way is simply to create a semi-transparent square UIView, then set the cornerRadius of its layer to be half of its width/height. Something like:
UIView *squareView = [[UIView alloc] initWithFrame:CGRectMake(0,0,100,100)];
squareView.alpha = 0.5;
squareView.layer.cornerRadius = 50;
...
[squareView release];
This has got to be the simplest solution:
CGFloat r = 150;
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(0,0,1.5*r,1.5*r)];
lbl.text = #"●";
lbl.transform = CGAffineTransformMakeTranslation(0.0f, -r/6);
lbl.textAlignment = UITextAlignmentCenter;
lbl.backgroundColor = [UIColor clearColor];
lbl.textColor = [UIColor redColor];
lbl.font = [UIFont systemFontOfSize:2*r];
lbl.alpha = 0.5;
lbl.center = self.view.center;
[self.view addSubview:lbl];
One way would be to add a CAShapeLayer with a circular path, either directly to the layer of the UIImageView or as the layer of a new UIView that is added to the UIImageView.
If you actually want to modify the image, then create a mutable copy of it by drawing it into a CGBitmapContext then creating a new image from the modified bitmap.
CGPathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect( circlePath , NULL , CGRectMake( 0,0,20,20 ) );
CAShapeLayer *circle = [[CAShapeLayer alloc] init];
circle.path = circlePath;
circle.opacity = 0.5;
[myImageView.layer addSublayer:circle];
CGPathRelease( circlePath );
[circle release];
You can implement a custom sub-class of UIView that draws your image and then the circle in the drawRect method:
#interface CircleImageView : UIView {
UIImage * m_image;
CGRect m_viewRect;
// anything else you need in this view?
}
Implementation of drawRect:
- (void)drawRect:(CGRect)rect {
// first draw the image
[m_image drawInRect:m_viewRect blendMode:kCGBlendModeNormal alpha:1.0];
// then use quartz to draw the circle
CGContextRef context = UIGraphicsGetCurrentContext ()
// stroke and fill black with a 0.5 alpha
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 0.5);
CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 0.5);
// now draw the circle
CGContextFillEllipseInRect (context, m_viewRect);
}
You will need to set up the m_viewRect and m_image member functions on init.