Confusing lag situation - UIBezierPath re-draw going too slaw - ios

I've got a "SLATE2" tablet that allows me to write on a tablet with a special pen and interact with my own app. I'm having some trouble though, and I don't think it's a problem with hardware.
- (void)touchesBegan: (CGPoint)point
{
[path moveToPoint:point];
}
- (void)touchesMoved: (CGPoint)point
{
[path addLineToPoint: point]; // (4)
[Newpath appendPath: path];
pathTwo = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPathWithCGPath:Newpath.CGPath];
pathTwo = [pathTwo fitInto: self.bounds];
[self setNeedsDisplay];
}
- (void)touchesEnded: (CGPoint)point
{
pathTwo = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPathWithCGPath:Newpath.CGPath];
pathTwo = [pathTwo fitInto: self.bounds];
[path removeAllPoints];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] setStroke];
[pathTwo stroke];
return;
}
The drawing to the UIView screen is far too slow. I'm looking at the console and the events being raised by the actual tablet are at almost lightspeed. If there is a better way to draw on this UIView faster please show me.
I've dscovered the issue. fitInto is called so frequently and pathTwo is re-drawn every time. I need fitInto to be called everytime theres a new part of the pad that's drawn on to scale how much is shown.
I tried turning off anti-aliasing but that didn't work.
fitInto works like so...
imagine the screen of the iphone
and if you were to draw on a tablet, a square tablet, at the bottom, say a circle, if you were to draw a circle, it would just zoom in on the iphone screen as its the only drawing on the tablet. if you draw at the top of the tablet, the circle at the bottom is now shown to scale of the entire tablet because of the two opposing ends. i don't think this function could be made any faster... but let's try!
func fit(into:CGRect) -> Self {
let bounds = self.cgPath.boundingBox
let sw = into.size.width/bounds.width
let sh = into.size.height/bounds.height
let factor = min (5, min(sw, max(sh, 0.0)))
return scale(x: factor, y: factor, into: into)
}
and here is scale
func scale(x:CGFloat, y:CGFloat, into: CGRect? = nil) -> Self{
var transform = CGAffineTransform(scaleX: x, y: y)
if into != nil {
transform = transform.concatenating(CGAffineTransform(translationX: into!.midX - self.cgPath.boundingBox.midX, y: into!.midY - self.cgPath.boundingBox.midY))
}
let _ = applyCentered(transform: transform)
return self
}

Have you checked how many points are actually in your path? I don't think you want to draw your CGPath pixel by pixel. Instead you should only add a segment when the distance exceeds a certain threshold. You can also simplify the path by drawing curves through the points instead of drawing line segments. See this primer on bezier curves.
EDIT: The Ray Wenderlich sample app looks like its actually doing the same thing, adding points in TouchedMoved. However in touchesEnded they rasterize the curve into a bitmap. I'm not so sure that that will help your case though because it looks like you are deleting all of the points in touchesEnded. I would still look at their code and see if you have any other points that could be a bottle neck.

What effect are you after? Are you trying to let the user trace lines and curves with the pen, or dots if they tap?
The simple fact is that iOS is not fast enough to track the user's pen/finger strokes point-by-point. Instead, you want to collect a series of points and create a UIBezierPath that connects those points with line segments. That will give you something pretty close to what you want. However, if the user traces fast, that will lag behind as well.

Related

UIBezierPath & CAShapeLayer initial animation jump

I built this for my company: https://github.com/busycm/BZYStrokeTimer and during the course of building, I noticed an interesting "bug" that I can't seem to mitigate when using UIBezierPath. Right when the animation starts, the path jumps a certain number of pixels forward (or backwards depending if it's counterclockwise) instead of starting up with a smooth, incremental animation. And what I found that's really interesting is how much the path jumps forward is actually the value of the line width for the CAShaperLayer.
So for example, if my bezier path starts off at CGRectGetMidX(self.bounds) and the line with is 35, the animation actually starts from CGRectGetMidX(self.bounds)+35 and the larger the line width, the more noticeable the jump is. Is there any way to get rid of that so that path will smoothly animate out from the start point?
Here's a picture of the first frame. This is what it looks like immediately after the animation starts.
Then when I resume the animation and pause again, the distance moved is about 1/100th of the distance you see in the picture.
Here's my bezier path code:
- (UIBezierPath *)generatePathWithXInset:(CGFloat)dx withYInset:(CGFloat)dy clockWise:(BOOL)clockwise{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, dy/2)];
[path closePath];
return clockwise ? path : [path bezierPathByReversingPath];
}
Here's the animation code:
CABasicAnimation *wind = [self generateAnimationWithDuration:self.duration == 0 ? kDefaultDuration : self.duration fromValue:#(self.shapeLayer.strokeStart) toValue:#(self.shapeLayer.strokeEnd) withKeypath:keypath withFillMode:kCAFillModeForwards];
wind.timingFunction = [CAMediaTimingFunction functionWithName:self.timingFunction];
wind.removedOnCompletion = NO;
self.shapeLayer.path = [self generatePathWithXInset:self.lineWidth withYInset:self.lineWidth clockWise:self.clockwise].CGPath;
[self.shapeLayer addAnimation:wind forKey:#"strokeEndAnimation"];
And here's how I construct the CAShapeLayer.
- (CAShapeLayer *)shapeLayer {
return !_shapeLayer ? _shapeLayer = ({
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = kDefaultLineWidth;
layer.fillColor = UIColor.clearColor.CGColor;
layer.strokeColor = [UIColor blackColor].CGColor;
layer.lineCap = kCALineCapSquare;
layer.frame = self.bounds;
layer.strokeStart = 0;
layer.strokeEnd = 1;
layer;
}) : _shapeLayer;
}
I think what's happening here is that, in this frame of the animation, you are drawing a line that consists of a single point. Since the line has a thickness associated with it, and the line cap type is kCALineCapSquare, that'll get rendered as a square with height and width equal to the line width.
You can think of it as if you are drawing a line with a square marker, and you are going to drag the midpoint of the marker so that it goes through every point in the curve you specified. For the first point in the line, it's as if the marker touches down at that point, leaving a square behind.
Here's a visual representation the different line cap types that will hopefully make it more intuitive. You should probably change the line cap style to kCALineCapButt.
Sidenote:
After you make that change, in this line of code
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
you probably don't have to offset the x coordinate by dx/2 anymore.

iOS Game - Vector Collision

I am making a game for iOS. I inputed two images, one will be used as the player, and the other which is an object. I want to code that if the two objects intersect, then it runs the method EndGame.
-(void)Collision{
if (CGRectIntersectsRect(Ball.frame, Platform1.frame)) {
[self EndGame];
}
}
Although, The images are not a square shape, but the UIImage is a square. Therefore when the two objects intersect it Ends the game, even if the two images look like they came close it still ends the game because of the incorrect collision detection. Do you have suggestions?
Can I change the image shape on XCODE or can I make it so if the image collides with a certain point on the players image?
Thanks.
create a custom view and in draw rect create a bezier path. Something like this
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:ball.center radius:ballRadius startAngle:0.0 endAngle:M_PI*2.0 clockwise:YES];
This will draw a circle view
EDIT: heres a sample - I'm not sure exactly what all the details are, so this is pretty generic
#interface ballView : UIView//create subclass of UIView
...
#implementation ...
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];//create bezier path
[path addArcWithCenter:self.center radius:5.0 startAngle:0.0 endAngle:M_PI*2.0 clockwise:YES];//draw circle. You can change around the parameters to the way you like
[[UIColor orangeColor]setStroke];
[path stroke];//draw line

Can I add a custom line cap to a UIBezierPath?

I'm drawing an arc by creating a CAShapeLayer and giving it a Bezier path like so:
self.arcLayer = [CAShapeLayer layer];
UIBezierPath *remainingLayerPath = [UIBezierPath bezierPathWithArcCenter:self.center
radius:100
startAngle:DEGREES_TO_RADIANS(135)
endAngle:DEGREES_TO_RADIANS(45)
clockwise:YES];
self.arcLayer.path = remainingLayerPath.CGPath;
self.arcLayer.position = CGPointMake(0,0);
self.arcLayer.fillColor = [UIColor clearColor].CGColor;
self.arcLayer.strokeColor = [UIColor blueColor].CGColor;
self.arcLayer.lineWidth = 15;
This all works well, and I can easily animate the arc from one side to the other. As it stands, this gives a very squared edge to the ends of my lines. Can I round the edges of these line caps with a custom radius, like 3 (one third the line width)? I have played with the lineCap property, but the only real options seem to be completely squared or rounded with a larger corner radius than I want. I also tried the cornerRadius property on the layer, but it didn't seem to have any effect (I assume because the line caps are not treated as actual layer corners).
I can only think of two real options and I'm not excited about either of them. I can come up with a completely custom Bezier path tracing the outside of the arc, complete with my custom rounded edges. I'm concerned however about being able to animate the arc in the same fashion (right now I'm just animating the stroke from 0 to 1). The other option is to leave the end caps square and mask the corners, but my understanding is that masking is relatively expensive, and I'm planning on doing some fairly intensive animations with this view.
Any suggestions would be helpful. Thanks in advance.
I ended up solving this by creating two completely separate layers, one for the left end cap and one for the right end cap. Here's the right end cap example:
self.rightEndCapLayer = [CAShapeLayer layer];
CGRect rightCapRect = CGRectMake(remainingLayerPath.currentPoint.x, remainingLayerPath.currentPoint.y, 0, 0);
rightCapRect = CGRectInset(rightCapRect, self.arcWidth / -2, -1 * endCapRadius);
self.rightEndCapLayer.frame = rightCapRect;
self.rightEndCapLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.rightEndCapLayer.bounds
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
cornerRadii:CGSizeMake(endCapRadius, endCapRadius)].CGPath;
self.rightEndCapLayer.fillColor = self.remainingColor.CGColor;
// Rotate the end cap
self.rightEndCapLayer.anchorPoint = CGPointMake(.5, 0);
self.rightEndCapLayer.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(45), 0.0, 0.0, 1.0);
[self.layer addSublayer:self.rightEndCapLayer];
Using the bezier path's current point saves from doing a lot of math to calculate where the end point should appear. Moving the anchoring point also allows the layers to not overlap, which is important if your arc is at all transparent.
This still isn't entirely ideal, as animations have to be chained through multiple layers. It's better than the alternatives I could come up with though.

iOS Draw the same shape multiple times

need some help with quartz 2d, it is completely new for me.
Basically my app needs to follow the touch, draw that line starting from center multiple times. The issue is that it has to be dynamic and the lines have to be on equally spread( kind of like octopus starting from center). The way I have it on android is that I remember the shape paths in array, than draw it multiple times with rotating the coordinate system, but I cannot figure out how to do it on iOS.
My rotate function
- (void) rotateContext:(int)angle
{
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), self.center.x, self.center.y);
CGContextRotateCTM(UIGraphicsGetCurrentContext(), radians(angle));
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -self.center.x, -self.center.y);
}
It only works if I try do do it in drawRect(), and it rotates all the paths with it.
Can you please suggest me a good way to solve the problem?
Thanks
This can lead you to a solution:
(maybe it even compiles)
/* setup the context */
UIBezierPath *bpath = [UIBezierPath bezierPath];
UIBezierPath *subpath =
[UIBezierPath bezierPathWithOvalInRect:<#some rect#>];
[bpath appendPath:subpath];
/* add more stuff to the path as you wish */
bezierPath.lineWidth = 2;
/* draw the same path rotated multiple times */
for(NSInteger i = 0; i < 4; i++) {
[bpath applyTransform:CGAffineTransformMakeRotation(M_PI_2 * i)];
[bpath stroke];
}
/* teardown the context */
Rotating a bezier is tricky, you'll need to apply a more complex transformation depending on the results you expect.
Those bezier paths objects can be stored in an array or whatever you need.

Create the indented look found in UINavigationBarButton - programmatically

I'm trying to programmatically recreate the indented button look that can be seen on a UINavigationBarButton. Not the shiny two tone look or the gradient, just the perimeter shading:
It looks like an internal dark shadowing around the entire view perimeter, slightly darker at the top? And then an external highlighting shadow around the lower view perimeter.
I've played a bit with Core Graphics, and experimented with QuartzCore and shadowing with view.layer.shadowRadius and .shadowOffset, but can't even get the lower highlighting to look right. I'm also not sure where to start to achieve both a dark shadowing with internal offset and a light shadowing with external offset.
It seems as though you want a border that looks looks like a shadow. Since the shadow appears to some sort of gradient, setting a border as a gradient won't be possible at first glance. However, it is possible to create a path that represents the border and then fill that with a gradient. Apple provides what seems to be a little known function called CGPathCreateCopyByStrokingPath. This takes a path (say, a rounded rect, for example) and creates a new path that would be the stroke of the old path given the settings you pass into the function (like line width, join/cap setting, miter limit, etc). So lets say you define a path (this isn't exactly what Apple provides, but's it's similar):
+ (UIBezierPath *) bezierPathForBackButtonInRect:(CGRect)rect withRoundingRadius:(CGFloat)radius{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint mPoint = CGPointMake(CGRectGetMaxX(rect) - radius, rect.origin.y);
CGPoint ctrlPoint = mPoint;
[path moveToPoint:mPoint];
ctrlPoint.y += radius;
mPoint.x += radius;
mPoint.y += radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:M_PI + M_PI_2 endAngle:0 clockwise:YES];
mPoint.y = CGRectGetMaxY(rect) - radius;
[path addLineToPoint:mPoint];
ctrlPoint = mPoint;
mPoint.y += radius;
mPoint.x -= radius;
ctrlPoint.x -= radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:0 endAngle:M_PI_2 clockwise:YES];
mPoint.x = rect.origin.x + (10.0f);
[path addLineToPoint:mPoint];
[path addLineToPoint:CGPointMake(rect.origin.x, CGRectGetMidY(rect))];
mPoint.y = rect.origin.y;
[path addLineToPoint:mPoint];
[path closePath];
return path;
}
This returns a path similar to Apple's back button (I use this in my app). I have added this method (along with dozens more) as a category to UIBezierPath.
Now lets add that inner shadow in a drawing routine:
- (void) drawRect:(CGRect)rect{
UIBezierPath *path = [UIBezierPath bezierPathForBackButtonInRect:rect withRoundingRadius:5.0f];
//Just fill with blue color, do what you want here for the button
[[UIColor blueColor] setFill];
[path fill];
[path addClip]; //Not completely necessary, but borders are actually drawn 'around' the path edge, so that half is inside your path, half is outside adding this will ensure the shadow only fills inside the path
//This strokes the standard path, however you might want to might want to inset the rect, create a new 'back button path' off the inset rect and create the inner shadow path off that.
//The line width of 2.0f will actually show up as 1.0f with the above clip: [path addClip];, due to the fact that borders are drawn around the edge
UIBezierPath *innerShadow = [UIBezierPath bezierPathWithCGPath: CGPathCreateCopyByStrokingPath(path.CGPath, NULL, 2.0f, path.lineCapStyle, path.lineJoinStyle, path.miterLimit)];
//You need this, otherwise the center (inside your path) will also be filled with the gradient, which you don't want
innerShadow.usesEvenOddFillRule = YES;
[innerShadow addClip];
//Now lets fill it with a vertical gradient
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint start = CGPointMake(0, 0);
CGPoint end = CGPointMake(0, CGRectGetMaxY(rect));
CGFloat locations[2] = { 0.0f, 1.0f};
NSArray *colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:.7f alpha:.5f].CGColor, (id)[UIColor colorWithWhite:.3f alpha:.5f].CGColor, nil];
CGGradientRef gradRef = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), (__bridge CFArrayRef)colors, locations);
CGContextDrawLinearGradient(context, gradRef, start, end, 0);
CGGradientRelease(gradRef);
}
Now this is just a simple example. I don't save/restore contexts or anything, which you'll probably want to do. There are things you might still want to do to make it better, like maybe inset the 'shadow' path if you want to use a normal border. You might want to use more/different colors and locations. But this should get you started.
UPDATE
There is another method you can use to create this effect. I wrote an algorithm to bevel arbitrary bezier paths in core graphics. This can be used to create the effect you're looking for. This is an example of how I use it in my app:
You pass to the routine the CGContextRef, CGPathRef, size of the bevel and what colors you want it to use for the highlight/shadow.
The code I used for this can be found here:Github - Beveling Algorithm.
I also explain the code and my methodology here: Beveling-Shapes in Core Graphics
Using the layer's shadow won't do it. You need both a light outer shadow and a dark inner shadow to get that effect. A layer can only have one (outer) shadow. (Also, layer shadows are redrawn dynamically, and force CPU-based rendering which kills performance.)
You'll need to do your own drawing with CoreGraphics, either in a view's drawRect: method or a layer's drawInContext: method. (Or you draw into an image context and then reuse the image.) Said drawing will mostly use CGContext functions. (I'll name some below, but this link has documentation for them all.)
For a round rect button, you might find it tedious to create the appropriate CGPath -- instead, you can use +[UIBezierPath bezierPathWithRoundedRect:cornerRadius:] and then the path's CGPath property to set the context's current path with CGContextAddPath.
You can create an inner shadow by setting a clipping path (see CGContextClip and related functions) to the shape of the button, setting up a shadow (see CGContextSetShadowWithColor and related functions), and then drawing around the outside of the shape you want shadowed. For the inner shadow, stroke (CGContextStrokePath) a round-rect that's a bit larger than your button, using a thick stroke width (CGContextSetLineWidth) so there's plenty of "ink" to generate a shadow (remember, this stroke won't be visible due to the clipping path).
You can create an outer shadow in much the same way -- don't use a clipping path this time, because you want the shadow to be outside the shape, and fill (CGContextFillPath) the shape of your button instead of stroking it. Note that drawing a shadow is sort of a "mode": you save the graphics state (CGContextSaveGState), setup a shadow, then draw the shape you want to see a shadow of (the shape itself isn't drawn when you're in this mode), and finally restore state (CGContextRestoreGState) to get out of "shadow mode". Since that mode doesn't draw the shape, only the shadow, you'll need to draw the shape itself separately.
There's an order to do this all in, too. It should be obvious if you think about the order in which you'd paint these things with physical media: First draw the outer shadow, then the button's fill, then the inner shadow. You might add a stroke after that if the inner shadow doesn't give you a pronounced enough outline.
There are a few drawing tools which can output source code for CoreGraphics: Opacity is one that I use. Be careful with these, though, as they code they generate may not be efficient.

Resources