In this thread I asked how to create animations of blurred-edged filled paths, to create a "circle wipe" animation with feathered edges.
The solution is to install the path in the shadowPath property of a layer an animate that. Core Graphics applies a variable amount of blur to the shadow path, and the shadowPath property is animatable. However, it fills whatever path you install into it. You can't use it to draw feathered/blurred stroked paths, since what gets rendered as the shadow is a closed, filled version of whatever path you install.
There is a method copy(strokingWithWidth:lineCap:lineJoin:miterLimit:transform:) that will convert a stroked path into a closed path that traces the outline of the stroked path. If you fill the output of that method, you get what looks like a stroked version of the original path. You can install that converted path into the shadowPath of a layer and get a nice, blurred stroked path.
However, under the covers, the copy(strokingWithWidth:lineCap:lineJoin:miterLimit:transform:) method does some things that make the resulting path unsuitable for path animation. If you look at Rob Mayoff's excellent answer in this thread, you see that for inside corners, Core Graphics creates odd triangles that overlap with the interior of the stroked shape. It also seems to sometimes vary the number of vertices in the output of the path depending on it's shape.
For paths stroked with a fairly thin line width, they animate well. Here is an example:
But when you increase the line width and/or blur radius, things go "all pear-shaped" (meaning they go very wrong.) Here is an animation of the same curve with different settings:
You can see the project that generates these images on Github, and try it yourself.
Question:
Does anybody know of a way to convert a stroked path to a filled path that does not have the artifacts described in Rob Mayoff's post?
If I have to I can write my own version of copy(strokingWithWidth:lineCap:lineJoin:miterLimit:transform:) that creates a filled path with a fixed number of vertexes and without the artifacts that method creates, but it would be pretty involved. I'm hoping there is an existing solution.
Related
I have been searching for a way to to take my paths (in blue below) and get a CGPathRef from them, there would be 3/6 paths created per tile. I heard something about using an Alpha but I was unsure because all references were about creating a polygon and not a path.
In the end you will have sprites that followPath per tile point they land on.
I'm wondering, is there an alternative to CGPathAddElipseInRect for drawing an ellipse? It works well and all, but I need a custom ellipse that has less steeper curves. Any thing else that can be done? I am using this path with CTFramesetterCreateFrame in CoreText so I can draw text within different shapes.
No...
...But it can be done with Core Graphics
You would have to use something like CGPathAddQuadCurveToPoint, CGPathAddCurveToPoint or CGPathAddArcToPoint to create a custom path for you ellipse. If your not familiar with how control points for bezier paths work you can starts by reading the Wikipedia article. You don't need to read the math part about it, just look at this image (from Wikipedia).
For the ...AddCurveTo... method:
Your current point is p0 and the end point is p3. The two control points are p1 and p2.
For the ...AddQuadCurveTo.. method:
Same as above but with only one control point.
For the ...AddArcTo.. method:
Doen't use control points. Instead uses a fixed radius.
...You can also create the path with UIBezierPath if you prefer
There is also similar methods on UIBezier path like addCurveToPoint:controlPoint1:controlPoint2: or addCurveToPoint:controlPoint1:controlPoint2:. The bezier path can then be converted to a CGPath but that shouldn't be necessary if your familiar with Core Graphics.
You can create a UIBezierPath and then get its CGPath property.
How you create this path will depend on your needs. The method +(UIBezierPath *) bezierPathWithOvalInRect: looks like it most closely resembles CGPathAddElipseInRect.
You can also use its fill and stroke methods to draw into the current context.
How would I efficiently draw a CGPath on a CATiledLayer? I'm currently checking if the bounding box of the tile intersects the bounding box of the path like this:
-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
CGRect boundingBox = CGPathGetPathBoundingBox(drawPath);
CGRect rect = CGContextGetClipBoundingBox(context);
if( !CGRectIntersectsRect(boundingBox, rect) )
return;
// Draw path...
}
This is not very efficient as the drawLayer:inContext: is called multiple times from multiple threads and results in drawing the path many times.
Is there a better, more efficient way to do this?
The simplest option is to draw your curve into a large image and then tile the image. But if you're tiling, it probably means the image would be too large, or you would have just drawn the path in the first place, right?
So you probably need to split your path up. The simplest approach is to split it up element by element using CGPathApply. For each element, you can check its bounding box and determine if that element falls in your bounds. If not, just keep track of the last end point. If so, then move to the last end point you saw and add the element to a new path for this tile. When you're done, each tile will draw its own path.
Technically you will "draw" things that go outside your bounds here (such as a line that extends beyond the tile), but this is much cheaper than it sounds. Core Graphics is going to clip single elements very easily. The goal is to avoid calculating elements that are not in your bounding box at all.
Be sure to cache the resulting path. You don't need to calculate the path for every tile; just the ones you're drawing. But avoid recalculating it every time the tile draws. Whenever the data changes, dump your cache. If there are a very large number of tiles, you can also use NSCache to optimize this even better.
You don't show where the path gets created. If possible, you might try building the path up in the -drawLayer:inContext: method, only creating the portion of it needed for the tile being drawn.
As with all performance problems, you should use Instruments to profile your code and find out exactly where the bottlenecks are. Have you tried that already, and if so, what did you find?
As a side note, is there a reason you're using CGPath instead of UIBezierPath? From Apple's documentation:
For creating paths in iOS, it is recommended that you use UIBezierPath
instead of CGPath functions unless you need some of the capabilities
that only Core Graphics provides, such as adding ellipses to paths.
For more on creating and rendering paths in UIKit, see “Drawing Shapes
Using Bezier Paths.”
I have a rectangular CGMutablePathRef and I want to subtract a circle which lays exactly on centered on one edge of that rectangle, so the edge does not cross the circle anymore.
There seem to be no functions to intersect or subtract paths from another. How can I do it?
You need to look at the CGContext you are drawing into and use the clipping on the context rather than the path.
Apple's documentation is here.
If I understand your question, you can draw your rectangle into the context and then "clip out" the circle path. If you are filling the paths, you'll need to pay attention to the winding rules.
Alternatively, you can make your path with a series of commands such as CGPathAddLineToPoint, CGPathAddArcToPoint, etc and then stoke the path in your context. If you use this approach, you can then apply transforms to the final path for scaling and rotating as needed. Depending on what you are trying to accomplish, this may be the better approach.
Is there a way to move a path on screen by (x,y) pixels using directly a CGPathRef instead of walking through its points and lines again in drawRect method? I want to be reusing my old CGPathRef when I want to move it on screen instead of recreating it with new pixels.
Depending on your needs, you could just translate the drawing context via CGContextTranslateCTM before drawing your path (then restore the old context, either with push/popping contexts, or inverting the translation). You might also like CGPathApply, which will call a function for every path element in the path (so you can translate the points by hand).