I've been trying to figure out a way to draw segments as illustrated in the following image:
I'd like to:
draw the segment
include gradients
include shadows
animate the drawing from 0 to n angle
I've been attempting to do this with CGContextAddArc and similar calls but not getting very far.
Can anyone help ?
There are many parts to your question.
Getting the path
Creating the path for such a segment shouldn't be too hard. There are two arcs and two straight lines. I've previously explained how you can break down a path like that so I won't do it here. Instead I'm going to be fancy and create the path by stroking another path. You can of course read the breakdown and construct the path yourself. The arc I'm talking about stroking is the orange arc inside the gray dashed end-result.
To stroke the path we first need it. that is basically as simple as moving to the start point and drawing an arc around the center from the current angle to the angle you want the segment to cover.
CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
YES);
Then when you have that path (the single arc) you can create the new segment by stroking it with a certain width. The resulting path is going to have the two straight lines and the two arcs. The stroke happens from the center an equal distance inwards and outwards.
CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
Drawing
Next up is drawing and there are generally two main choices: Core Graphics in drawRect: or shape layers with Core Animation. Core Graphics is going to give you the more powerful drawing but Core Animation is going to give you the better animation performance. Since paths are involved pure Cora Animation won't work. You will end up with strange artifacts. We can however use a combination of layers and Core Graphics by drawing the the graphics context of the layer.
Filling and stroking the segment
We already have the basic shape but before we add gradients and shadows to it I will do a basic fill and stroke (you have a black stroke in your image).
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);
That will put something like this on screen
Adding shadows
I'm going to change the order and do the shadow before the gradient. To draw the shadow we need to configure a shadow for the context and draw fill the shape to draw it with the shadow. Then we need to restore the context (to before the shadow) and stroke the shape again.
CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c,
CGSizeMake(0, 2), // Offset
3.0, // Radius
shadowColor);
CGContextFillPath(c);
CGContextRestoreGState(c);
// Note that filling the path "consumes it" so we add it again
CGContextAddPath(c, strokedArc);
CGContextStrokePath(c);
At this point the result is something like this
Drawing the gradient
For the gradient we need a gradient layer. I'm doing a very simple two color gradient here but you can customize it all you want. To create the gradient we need to get the colors and the suitable color space. Then we can draw the gradient on top of the fill (but before the stroke). We also need to mask the gradient to the same path as before. To do this we clip the path.
CGFloat colors [] = {
0.75, 1.0, // light gray (fully opaque)
0.90, 1.0 // lighter gray (fully opaque)
};
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;
CGContextSaveGState(c);
CGContextAddPath(c, strokedArc);
CGContextClip(c);
CGRect boundingBox = CGPathGetBoundingBox(strokedArc);
CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox));
CGPoint gradientEnd = CGPointMake(0, CGRectGetMaxY(boundingBox));
CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(c);
This finishes the drawing as we currently have this result
Animation
When it comes to the animation of the shape it has all been written before: Animating Pie Slices Using a Custom CALayer. If you try doing the drawing by simply animating the path property you are going to see some really funky warping of the path during the animation. The shadow and gradient has been left intact for illustrative purposes in the image below.
I suggest that you take the drawing code that I've posted in this answer and adopt it to the animation code from that article. Then you should end up with the what you are asking for.
For reference: the same drawing using Core Animation
Plain shape
CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = [UIColor lightGrayColor].CGColor;
segment.strokeColor = [UIColor blackColor].CGColor;
segment.lineWidth = 1.0;
segment.path = strokedArc;
[self.view.layer addSublayer:segment];
Adding shadows
The layer has some shadow related properties that it's up to you to customize. Howerever you should set the shadowPath property for improved performance.
segment.shadowColor = [UIColor blackColor].CGColor;
segment.shadowOffset = CGSizeMake(0, 2);
segment.shadowOpacity = 0.75;
segment.shadowRadius = 3.0;
segment.shadowPath = segment.path; // Important for performance
Drawing the gradient
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = #[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor, // light gray
(id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray
gradient.frame = CGPathGetBoundingBox(segment.path);
If we drew the gradient now it would be on top of the shape and not inside it. No, we can't have a gradient fill of the shape (I know you were thinking of it). We need to mask the gradient so that it go outside the segment. To do that we create another layer to be the mask of that segment. It has to be another layer, the documentation is clear that the behavior is "undefined" if the mask is part of the layer hierarchy. Since the mask's coordinate system is going to be the same as that of sublayers to the gradient we will have to translate the segment shape before setting it.
CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
-CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
&translation);
gradient.mask = mask;
Everything you need is covered in the Quartz 2D Programming Guide. I suggest you look through it.
However, it can be difficult to put it all together, so I'll walk you through it. We'll write a function that takes a size and returns an image that looks roughly like one of your segments:
We start the function definition like this:
static UIImage *imageWithSize(CGSize size) {
We'll need a constant for the thickness of the segment:
static CGFloat const kThickness = 20;
and a constant for the width of the line outlining the segment:
static CGFloat const kLineWidth = 1;
and a constant for the size of the shadow:
static CGFloat const kShadowWidth = 8;
Next we need to create an image context in which to draw:
UIGraphicsBeginImageContextWithOptions(size, NO, 0); {
I put a left brace on the end of that line because I like an extra level of indentation to remind me to call UIGraphicsEndImageContext later.
Since a lot of the functions we need to call are Core Graphics (aka Quartz 2D) functions, not UIKit functions, we need to get the CGContext:
CGContextRef gc = UIGraphicsGetCurrentContext();
Now we're ready to really get started. First we add an arc to the path. The arc runs along the center of the segment we want to draw:
CGContextAddArc(gc, size.width / 2, size.height / 2,
(size.width - kThickness - kLineWidth) / 2,
-M_PI / 4, -3 * M_PI / 4, YES);
Now we'll ask Core Graphics to replace the path with a “stroked” version that outlines the path. We first set the thickness of the stroke to the thickness we want the segment to have:
CGContextSetLineWidth(gc, kThickness);
and we set the line cap style to “butt” so we'll have squared-off ends:
CGContextSetLineCap(gc, kCGLineCapButt);
Then we can ask Core Graphics to replace the path with a stroked version:
CGContextReplacePathWithStrokedPath(gc);
To fill this path with a linear gradient, we have to tell Core Graphics to clip all operations to the interior of the path. Doing so will make Core Graphics reset the path, but we'll need the path later to draw the black line around the edge. So we'll copy the path here:
CGPathRef path = CGContextCopyPath(gc);
Since we want the segment to cast a shadow, we'll set the shadow parameters before we do any drawing:
CGContextSetShadowWithColor(gc,
CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2,
[UIColor colorWithWhite:0 alpha:0.3].CGColor);
We're going to both fill the segment (with a gradient) and stroke it (to draw the black outline). We want a single shadow for both operations. We tell Core Graphics that by beginning a transparency layer:
CGContextBeginTransparencyLayer(gc, 0); {
I put a left brace on the end of that line because I like to have an extra level of indentation to remind me to call CGContextEndTransparencyLayer later.
Since we're going to change the context's clip region for filling, but we won't want to clip when we stroke the outline later, we need to save the graphics state:
CGContextSaveGState(gc); {
I put a left brace on the end of that line because I like to have an extra level of indentation to remind me to call CGContextRestoreGState later.
To fill the path with a gradient, we need to create a gradient object:
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)#[
(__bridge id)[UIColor grayColor].CGColor,
(__bridge id)[UIColor whiteColor].CGColor
], (CGFloat[]){ 0.0f, 1.0f });
CGColorSpaceRelease(rgb);
We also need to figure out a start point and an end point for the gradient. We'll use the path bounding box:
CGRect bbox = CGContextGetPathBoundingBox(gc);
CGPoint start = bbox.origin;
CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox));
and we'll force the gradient to be drawn either horizontally or vertically, whichever is longer:
if (bbox.size.width > bbox.size.height) {
end.y = start.y;
} else {
end.x = start.x;
}
Now we finally have everything we need to draw the gradient. First we clip to the path:
CGContextClip(gc);
Then we draw the gradient:
CGContextDrawLinearGradient(gc, gradient, start, end, 0);
Then we can release the gradient and restore the saved graphics state:
CGGradientRelease(gradient);
} CGContextRestoreGState(gc);
When we called CGContextClip, Core Graphics reset the context's path. The path isn't part of the saved graphics state; that's why we made a copy earlier. Now it's time to use that copy to set the path in the context again:
CGContextAddPath(gc, path);
CGPathRelease(path);
Now we can stroke the path, to draw the black outline of the segment:
CGContextSetLineWidth(gc, kLineWidth);
CGContextSetLineJoin(gc, kCGLineJoinMiter);
[[UIColor blackColor] setStroke];
CGContextStrokePath(gc);
Next we tell Core Graphics to end the transparency layer. This will make it look at what we've drawn and add the shadow underneath:
} CGContextEndTransparencyLayer(gc);
Now we're all done drawing. We ask UIKit to create a UIImage from the image context, then destroy the context and return the image:
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
You can find the code all together in this gist.
This is a Swift 3 version of Rob Mayoff's answer. Just see how much more efficient this language is! This could be the contents of a MView.swift file:
import UIKit
class MView: UIView {
var size = CGSize.zero
override init(frame: CGRect) {
super.init(frame: frame)
size = frame.size
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var niceImage: UIImage {
let kThickness = CGFloat(20)
let kLineWidth = CGFloat(1)
let kShadowWidth = CGFloat(8)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let gc = UIGraphicsGetCurrentContext()!
gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2),
radius: (size.width - kThickness - kLineWidth)/2,
startAngle: -45°,
endAngle: -135°,
clockwise: true)
gc.setLineWidth(kThickness)
gc.setLineCap(.butt)
gc.replacePathWithStrokedPath()
let path = gc.path!
gc.setShadow(
offset: CGSize(width: 0, height: kShadowWidth/2),
blur: kShadowWidth/2,
color: UIColor.gray.cgColor
)
gc.beginTransparencyLayer(auxiliaryInfo: nil)
gc.saveGState()
let rgb = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(
colorsSpace: rgb,
colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray,
locations: [CGFloat(0), CGFloat(1)])!
let bbox = path.boundingBox
let startP = bbox.origin
var endP = CGPoint(x: bbox.maxX, y: bbox.maxY);
if (bbox.size.width > bbox.size.height) {
endP.y = startP.y
} else {
endP.x = startP.x
}
gc.clip()
gc.drawLinearGradient(gradient, start: startP, end: endP,
options: CGGradientDrawingOptions(rawValue: 0))
gc.restoreGState()
gc.addPath(path)
gc.setLineWidth(kLineWidth)
gc.setLineJoin(.miter)
UIColor.black.setStroke()
gc.strokePath()
gc.endTransparencyLayer()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
override func draw(_ rect: CGRect) {
niceImage.draw(at:.zero)
}
}
Call it from a viewController like this:
let vi = MView(frame: self.view.bounds)
self.view.addSubview(vi)
To do the degrees to radians conversions I have created the ° postfix operator. So you can now use e.g. 45° and this does the conversion from 45 degrees to radians.
This example is for Ints, extend these also for the Float types if you have the need:
postfix operator °
protocol IntegerInitializable: ExpressibleByIntegerLiteral {
init (_: Int)
}
extension Int: IntegerInitializable {
postfix public static func °(lhs: Int) -> CGFloat {
return CGFloat(lhs) * .pi / 180
}
}
Put this code into a utilities swift file.
Related
I managed to implement erase drawings on CGContext
UIImageView *maskImgView = [self.view viewWithTag:K_MASKIMG];
UIGraphicsBeginImageContext(maskImgView.image.size);
[maskImgView.image drawAtPoint:CGPointMake(0,0)];
float alp = 0.5;
UIImage *oriBrush = [UIImage imageNamed:_brushName];
//sets the style for the endpoints of lines drawn in a graphics context
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat eraseSize = oriBrush.size.width*_brushSize/_zoomCurrentFactor;
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineJoin(ctx, kCGLineJoinRound);
CGContextSetLineWidth(ctx,eraseSize);
CGContextSetRGBStrokeColor(ctx, 1, 1, 1, alp);
CGContextSetBlendMode(ctx, kCGBlendModeClear);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, lastPoint.x,lastPoint.y);
CGPoint vector = CGPointMake(currentPoint.x - lastPoint.x, currentPoint.y - lastPoint.y);
CGFloat distance = hypotf(vector.x, vector.y);
vector.x /= distance;
vector.y /= distance;
for (CGFloat i = 0; i < distance; i += 1.0f) {
CGPoint p = CGPointMake(lastPoint.x + i * vector.x, lastPoint.y + i * vector.y);
CGContextAddLineToPoint(ctx, p.x, p.y);
}
CGContextStrokePath(ctx);
maskImgView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
Problem is, this TOTALLY erase anything. The alpha set in this function ( CGContextSetRGBStrokeColor(ctx, 1, 1, 1, alp);) seems to be ignored totally.
I want to erase just lightly and repeated erasing will then totally removes the drawing.
Any ideas?
EDIT: As per request, I add more details about this code:
alp=_brushAlpha is a property delcared in this ViewController class. It ranges from 0.1 to 1.0. At testing I set it to 0.5. This drawing code is triggered by pan gesture recognizer (change state). It is basically following the finger (draw/erase by finger).
You've set the blending mode to clear. That ignores stroke color. You should play with the various modes a bit, but I suspect you want something like sourceAtop or maybe screen. See the CGBlendMode docs for full details.
You have a flag named clearsContextBeforeDrawing in UIView. if you set it to YES it will clear it before every draw.
according to documentation
A Boolean value that determines whether the view’s bounds should be automatically cleared before drawing.
When set to YES, the drawing buffer is automatically cleared to transparent black before the drawRect: method is called. This behavior ensures that there are no visual artifacts left over when the view’s contents are redrawn. If the view’s opaque property is also set to YES, the backgroundColor property of the view must not be nil or drawing errors may occur. The default value of this property is YES.
If you set the value of this property to NO, you are responsible for ensuring the contents of the view are drawn properly in your drawRect: method. If your drawing code is already heavily optimized, setting this property is NO can improve performance, especially during scrolling when only a portion of the view might need to be redrawn.
Introduction
I have a CGPath I create from a SVG file using PocketSVG API. It all works fine.
The Problem
The problem is that the shape stretches for some reason, please take a look on this picture (the blue color is just to make is more visible to you, please ignore it, it should be ClearColor):
The Target
What do I want to achieve? I want to achieve a shape that goes all over the screen's width (I don't care about the height, it should modify itself according to the width), and sticks to the bottom of the screen, please take a look on this picture as well (please ignore the circular button):
The Code
The important part ;)
I have a subclass of UIView that draws this shape from the SVG file, it called CategoriesBarView. Then, on my MainViewController (a subclass of UIViewController) I'm creating an object of CategoriesBarView and setting it programmatically as a subview.
CategoriesBarView:
class CategoriesBarView: UIView {
override func layoutSubviews() {
let myPath = PocketSVG.pathFromSVGFileNamed("CategoriesBar").takeUnretainedValue()
var transform = CGAffineTransformMakeScale(self.frame.size.width / 750.0, self.frame.size.height / 1334.0)
let transformedPath = CGPathCreateCopyByTransformingPath(myPath, &transform)
let myShapeLayer = CAShapeLayer()
myShapeLayer.path = transformedPath
let blur = UIBlurEffect(style: .Light)
let effectView = UIVisualEffectView(effect: blur)
effectView.frame.size = self.frame.size
effectView.frame.origin = CGPointMake(0, 0)
effectView.layer.mask = myShapeLayer
self.addSubview(effectView)
}
}
MainViewController:
override func viewDidLoad() {
super.viewDidLoad()
let testHeight = UIScreen.mainScreen().bounds.size.height / 6 // 1/6 of the screen’s height, that is the height in the target picture approximately, doesn’t it?
let categoriesBarView = CategoriesBarView(frame: CGRect(x: 0, y: UIScreen.mainScreen().bounds.size.height - testHeight , width: UIScreen.mainScreen().bounds.size.width, height: testHeight))
categoriesBarView.backgroundColor = UIColor.blueColor() // AS I said, it should be ClearColor
self.view.addSubview(categoriesBarView)
}
Does anyone of you know what is the problem here and why the shape is stretching like that? I'll really appreciate if someone could help me here.
Thank you very much :)
Consider following code which draws a Square of 100x100 dimension. What i have done here is taken 100x100 as a base dimension(Because its easy to calculate respective ratio or scale dimension), as you can see i have defined scaleWidth and scaleHeight variable which represents your current scale for path. Scale is 1.0 at the moment which means it draws a square of 100x100, if you change it to 0.5 and 0.75 respectively it will draw a rectangle of 50X75 pixels. Refer Images which clearly depicts difference between scale width and height as 1.0 and 0.5 and 0.75 respectively.
CGFloat scaleWidth = 0.50f;
CGFloat scaleHeight = 0.75f;
//// Square Drawing
UIBezierPath* bezierSquarePath = [UIBezierPath bezierPath];
// ***Starting point of path ***
[bezierSquarePath moveToPoint: CGPointMake(1, 1)];
// *** move to x and y position to draw lines, calculate respective x & y position using scaleWidth & scaleHeight ***
[bezierSquarePath addLineToPoint: CGPointMake(100*scaleWidth, 1)];
[bezierSquarePath addLineToPoint: CGPointMake(100*scaleWidth, 100*scaleHeight)];
[bezierSquarePath addLineToPoint: CGPointMake(1, 100*scaleHeight)];
// *** end your path ***
[bezierSquarePath closePath];
[UIColor.blackColor setStroke];
bezierSquarePath.lineWidth = 1;
[bezierSquarePath stroke];
Image 1 : Represents 100x100 square using scaleWidth = 1.0 and scaleHeight = 1.0
Image 2 : Represents 50x75 square using scaleWidth = 0.50 and scaleHeight = 0.75
Note: In given images all the drawing is done in UIView's - (void)drawRect:(CGRect)rect method as only UIView is capable to draw. I have placed a UIView which is highlighted with GrayColor in images.
I believe it gives you a perspective about scaling a path to solve your problem as you can not use the same code but you can generate one using it.
Helpful Tool : If you are not expert in Graphics coding you can recommend to use PaintCode software which generates Objective-C code with UI. Thought there might be other softwares you can opt for.
Happy coding :)
I want to achieve the shape shown in image using UIBezier Path, and too the shape is filled with blocks in image it shows one block is filled, how to achieve this.
I have tried the following code taken from here
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 10)];
[path addQuadCurveToPoint:CGPointMake(200, 10) controlPoint:CGPointMake(100, 5)];
[path addLineToPoint:CGPointMake(200, 0)];
[path addLineToPoint:CGPointMake(0, 0)];
[path closePath];
Thanks.
It looks to me like both the outline and also each block has the same shape. What you would probably do is to make one shape for the outline, and stroke it, and one shape for each cell and fill it.
Creating the shape
Each shape could be created something like this (as I've previously explained in this answer). It's done by stroking one path (the orange arc) which is a simple arc from one angle to another to get another path (the dashed outline)
Before we can stroke the path we to create it. CGPath's work just like UIBezierPath but with a C API. First we move to the start point, then we add an arc around the center from the one angle to another angle.
CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
YES);
Now that we have the centered arc, we can create one shape path by stroking it with a certain width. The resulting path is going to have the two straight lines (because we specify the "butt" line cap style) and the two arcs (inner and outer). As you saw in the image above, the stroke happens from the center an equal distance inwards and outwards.
CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
You would do this a couple of times to create one path for the stroked outline and one path for each cell.
Drawing the shape
Depending on if it's the outline or a cell you would either stroke it or fill it. You can either do this with Core Graphics inside drawRect: or with Core Animation using CAShapeLayers. Choose one and don't between them :)
Core Graphics
When using Core Graphics (inside drawRect:) you get the graphics context, configure the colors on it and then stroke the path. For example, the outline with a gray fill color and a black stroke color would look like this:
I know that your shape is filled white (or maybe it's clear) with a light blue stroke but I already had a gray and black image and I didn't want to create a new one ;)
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc); // the path we created above
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke); // both fill and stroke
That will put something like this on screen
Core Animation
The same drawing could be done with a shape layer like this:
CAShapeLayer *outline = [CAShapeLayer layer];
outline.fillColor = [UIColor lightGrayColor].CGColor;
outline.strokeColor = [UIColor blackColor].CGColor;
outline.lineWidth = 1.0;
outline.path = strokedArc; // the path we created above
[self.view.layer addSublayer: outline];
I'm trying to add a feature in my iPhone app that allows users to record the screen. To do this, I used an open source class called ScreenCaptureView that compiles a series of screenshots of the main view through the renderInContext: method. However, this method ignores masked CALayers, which is key to my app.
EDIT:
I need a way to record the screen so that the masks are included. Although this question specifically asks for a way to create the illusion of an image mask, I'm open to any other modifications I can make to record the screen successfully.
APP DESCRIPTION
In the app, I take a picture and create the effect of a moving mouth by animating the position of the jaw region, as shown below.
Currently, I have the entire face as one CALayer, and the chin region as a separate CALayer. To make the chin layer, I mask the chin region from the complete face using a CGPath. (This path is an irregular shape and must be dynamic).
- (CALayer *)getChinLayerFromPic:(UIImage *)pic frame:(CGRect)frame {
CGMutablePathRef mPath = CGPathCreateMutable();
CGPathMoveToPoint(mPath, NULL, p0.x, p0.y);
CGPoint midpt = CGPointMake( (p2.x + p0.x)/2, (p2.y+ p0.y)/2);
CGPoint c1 = CGPointMake(2*v1.x - midpt.x, 2*v1.y - midpt.y); //control points
CGPoint c2 = CGPointMake(2*v2.x - midpt.x, 2*v2.y - midpt.y);
CGPathAddQuadCurveToPoint(mPath, NULL, c1.x, c1.y, p2.x, p2.y);
CGPathAddQuadCurveToPoint(mPath, NULL, c2.x, c2.y, p0.x, p0.y);
CALayer *chin = [CALayer layer];
CAShapeLayer *chinMask = [CAShapeLayer layer];
chin.frame = frame;
chin.contents = (id)[pic CGImageWithProperOrientation];
chinMask.path = mPath;
chin.mask = chinMask;
CGPathRelease(mPath);
return chin;
}
I then animate the chin layer with a path animation.
As mentioned before, the renderInContext: method ignores the mask, and returns an image of the entire face instead of just the chin. Is there any way I can create an illusion of masking the chin? I would like to use CALayers if possible, since it would be most convenient for animations. However, I'm open to any ideas, including other ways to capture the video. Thanks.
EDIT:
I'm turning the cropped chin into a UIImage, and then setting that new image as the layers contents, instead of directly masking the layer. However, the cropped region is the reverse of the specified path.
CALayer *chin = [CALayer layer];
chin.frame = frame;
CGImageRef imageRef = [pic CGImage];
CGColorSpaceRef colorSpaceInfo = CGImageGetColorSpace(imageRef);
int targetWidth = frame.size.width;
int targetHeight = frame.size.height;
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
CGContextRef bitmap = CGBitmapContextCreate(NULL, targetWidth, targetHeight, CGImageGetBitsPerComponent(imageRef), CGImageGetBytesPerRow(imageRef), colorSpaceInfo, bitmapInfo);
CGContextAddPath(bitmap, mPath);
CGContextClip(bitmap);
CGContextDrawImage(bitmap, CGRectMake(0, 0, targetWidth, targetHeight), imageRef);
CGImageRef ref = CGBitmapContextCreateImage(bitmap);
UIImage *chinPic = [UIImage imageWithCGImage:ref];
chin.contents = (id)[chinPic CGImageWithProperOrientation];
Why don't you draw the CALayer of the chin into a seperate CGImage and make a new UIImage with that?
Then you can add this CGImage to a seperate UIImageView, which you can just move around with a PanGestureRecognizer for example?
I suggest that you draw each part alone(you don't need masks), the face without a chin, and the chin, both with alpha pixels around, then just draw the chin over and move it on your path ..
Please inform me if this wasn't helpful to you, regards
If you just need this in order to capture the screen, it is much easier to use a programme such as
http://www.airsquirrels.com/reflector/
that connects your device to the computer via AirPlay, and then record the stream on the computer. This particular programme has screen recording built in, extremely convenient. I think there is a trial version you can use for recordings of up to 10 minutes.
Have you tried taking a look at layer.renderInContext doesn't take layer.mask into account? ?
In particular (notice the coordinate flip):
//Make the drawing right with coordinate switch
CGContextTranslateCTM(context, 0, cHeight);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextClipToMask(context, CGRectMake(maskLayer.frame.origin.x * mod, maskLayer.frame.origin.y * modTwo, maskLayer.frame.size.width * mod,maskLayer.frame.size.height * modTwo), maskLayer.image.CGImage);
//Reverse the coordinate switch
CGAffineTransform ctm = CGContextGetCTM(context);
ctm = CGAffineTransformInvert(ctm);
CGContextConcatCTM(context, ctm);
I am trying to draw a CGPath that has a stroke for it's stroke.
Basically I want a draw a line using CGPath. I then want to go back and draw lines on both sides of the last CGPath giving it the effect that it is outlines.
This line can bend and turn in any way but I always need the two lines on the outside to follow.
EDIT: I need to be able to make the middle of the line transparent but the outlines solid black.
Use CGPathCreateCopyByStrokingPath to create a new path by stroking your old path at some width. Then draw your new path using kCGPathFillStroke.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 50, 50);
CGPathAddLineToPoint(path, NULL, 200, 200);
CGPathRef thickPath = CGPathCreateCopyByStrokingPath(path, NULL, 10, kCGLineCapButt, kCGLineJoinBevel, 0);
CGContextAddPath(context, thickPath);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetLineWidth(context, 3);
CGContextDrawPath(context, kCGPathFillStroke);
CGPathRelease(thickPath);
CGPathRelease(path);
}
The simplest solution would just be to stroke the path twice. First with black at a bigger stroke width and then stroke the same path again using the blue color with a slightly smaller stroke width.
Edit:
If I remember correctly you can use CGPathCreateCopyByStrokingPath(...) to create a new path that you then can both stroke and fill. Then you could use semi-transparent colors.
From the documentation:
CGPathCreateCopyByStrokingPath
Creates a stroked copy of another path.
CGPathRef CGPathCreateCopyByStrokingPath(
CGPathRef path,
const CGAffineTransform *transform,
CGFloat lineWidth,
CGLineCap lineCap,
CGLineJoin lineJoin,
CGFloat miterLimit
);
Parameters
path
The path to copy.
transform
A pointer to an affine transformation matrix, or NULL if no transformation is needed. If specified, Quartz applies the transformation to elements of the converted path before adding them to the new path.
lineWidth
The line width to use, in user space units. The value must be greater than 0.
lineCap
A line cap style constant—kCGLineCapButt (the default), kCGLineCapRound, or kCGLineCapSquare. See “CGLineCap”.
lineJoin
A line join value—kCGLineJoinMiter (the default), kCGLineJoinRound, or kCGLineJoinBevel. See “CGLineJoin”.
miterLimit
The miter limit to use.