iOS drawing imitate flat tip pen - ios

I'm creating a drawing app and I want to simulate a flat tip pen. Surprisingly it turned to pretty difficult task. This is my solution:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.drawingImageView];
currentPoint = CGPointMake(roundf(currentPoint.x), roundf(currentPoint.y));
UIGraphicsBeginImageContext(self.drawingImageView.frame.size);
[self.drawingImageView.image drawInRect:CGRectMake(0, 0, self.drawingImageView.frame.size.width, self.drawingImageView.frame.size.height)];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextMoveToPoint (ctx, lastPoint.x + 8.f, lastPoint.y - 8.f);
CGContextAddLineToPoint(ctx, currentPoint.x + 8.f, currentPoint.y - 8.f);
CGContextAddLineToPoint(ctx, currentPoint.x - 8.f, currentPoint.y + 8.f);
CGContextAddLineToPoint(ctx, lastPoint.x - 8.f, lastPoint.y + 8.f);
CGContextAddLineToPoint(ctx, lastPoint.x + 8.f, lastPoint.y - 8.f);
CGContextClosePath(ctx);
CGContextSetRGBFillColor(ctx, 0, 0, 0, 1);
CGContextDrawPath(ctx, kCGPathFillStroke);
[self.drawingImageView performSelectorInBackground:#selector(setImage:) withObject:UIGraphicsGetImageFromCurrentImageContext()];
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
This is how it looks:
As you can see, the edges are not smooth. I'd like to use UIBezierPath instead but I didn't find a way to achieve flat tip pen effect using UIBezierPath. Maybe there are some ways to improve my code or any other approaches (maybe UIBezierPath)?
The only app with such drawing style that I could find is the app called "Khattat". It's on arabic, you should tap second button on the main screen to open painting. It's a perfect example of what I want.

This is a pretty common problem.
The problem is with the line
UIGraphicsBeginImageContext(self.drawingImageView.frame.size);
What this does is create a new transparent image context with a size of your image view, but a scale of 1.0. Because of this scale - any drawing done in it will have to be upsampled to be displayed on a 2x or 3x display - hence your drawing coming out blurry.
The fix is amazingly simple. You can just use
UIGraphicsBeginImageContextWithOptions(drawingImageView.frame.size, NO, 0.0);
By passing 0.0 into the scale argument, Core Graphics will automatically detect the scale of the screen, and set the context's scale to match. Therefore any drawing you do in it will be done at the correct resolution.
Also, I just noticed the line
[self.drawingImageView performSelectorInBackground:#selector(setImage:) withObject:UIGraphicsGetImageFromCurrentImageContext()];
This doesn't sound like a great idea. If you look at the UIView documentation, you'll see Apple says:
Manipulations to your application’s user interface must occur on the main thread. Thus, you should always call the methods of the UIView class from code running in the main thread of your application.
Therefore you should be doing
self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext()
instead.
In regards to your comments about using a UIBezierPath - I don't see any obvious advantages that this change will bring you - drawing the path into the context yourself is just as direct as using the UIBezierPath methods for drawing.
However if you still want to, you can of course use a UIBezierPath instead - you'll want to do something like the following:
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
currentPoint = CGPointMake(roundf(currentPoint.x), roundf(currentPoint.y));
UIGraphicsBeginImageContextWithOptions(drawingImageView.frame.size, NO, 0.0);
[drawingImageView.image drawInRect:CGRectMake(0, 0, drawingImageView.frame.size.width, drawingImageView.frame.size.height)];
UIBezierPath* path = [UIBezierPath bezierPath];
[path moveToPoint:(CGPoint){lastPoint.x + 8.f, lastPoint.y - 8.f}];
[path addLineToPoint:(CGPoint){currentPoint.x + 8.f, currentPoint.y - 8.f}];
[path addLineToPoint:(CGPoint){currentPoint.x - 8.f, currentPoint.y + 8.f}];
[path addLineToPoint:(CGPoint){lastPoint.x - 8.f, lastPoint.y + 8.f}];
[path addLineToPoint:(CGPoint){lastPoint.x + 8.f, lastPoint.y - 8.f}];
[[UIColor blackColor] set];
[path fill];
drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;

Related

move Arc drawn using CGContextAddArc iOS Objective c

I have drawn an arc using below code,
CGPoint circleCenter = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 20.0);
CGContextSetStrokeColorWithColor(context,[UIColor redColor].CGColor);
CGContextAddArc(context, circleCenter.x , circleCenter.y, 100, [self radians:315], [self radians:135], 1);
CGContextStrokePath(context);
now i need to know can i move this arc in circular path using below method.?
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
If not how can i program an arc shape which can be moved using above method.?

3D-Touch changing line-width of CGContext - crossing lines is buggy

I want to change the width of the line with 3D-touch while drawing. I'm using the library ACEDrawingView which uses this code to draw:
- (void)draw
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextSetAlpha(context, self.lineAlpha);
CGContextStrokePath(context);
}
I use this code to change the lineWidth based on the pressure in touchesMoved:
// save all the touches in the path
UITouch *touch = [touches anyObject];
//3d touch
CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;
CGFloat lineWidth = normalizedForce * 10;
self.currentTool.lineWidth = lineWidth;
It all works good until I draw another line which crosses the previous one. Then it looks like the image above.
Any help would be greatly appreciated! Please ask for more code if needed, or take a quick look at ACEDrawingView. Thank you!

Objective C - Sketch Application will not draw long lines

I have an iPhone app where I provide a sketch pad for the user to save a signature. An UIImageView gets added to the main view and that holds the strokes. For some reason you can only draw short lines on the pad like the following image.
I have another application for the iPad that uses the same code and it works fine. I'm not sure what could be causing it. I'm not using any touch or gesture code that would interfere with it. The following is some of the code I use.
UPDATE: If I create a UIViewController with the same class and make it the root view controller then it works fine. Something in my navigation hierarchy is doing something weird.
-(void)SetUpSignaturePad{
//create a frame for our signature capture
imageFrame = CGRectMake(self.view.frame.origin.x,
self.view.frame.origin.y,
self.view.frame.size.width + 23,
self.view.frame.size.height + 7 );
//allocate an image view and add to the main view
mySignatureImage = [[UIImageView alloc] initWithImage:nil];
mySignatureImage.frame = imageFrame;
mySignatureImage.backgroundColor = [UIColor whiteColor];
[self.view addSubview:mySignatureImage];
}
//when one or more fingers touch down in a view or window
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//did our finger moved yet?
fingerMoved = NO;
UITouch *touch = [touches anyObject];
//we need 3 points of contact to make our signature smooth using quadratic bezier curve
currentPoint = [touch locationInView:mySignatureImage];
lastContactPoint1 = [touch previousLocationInView:mySignatureImage];
lastContactPoint2 = [touch previousLocationInView:mySignatureImage];
//when one or more fingers associated with an event move within a view or window
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//well its obvious that our finger moved on the screen
fingerMoved = YES;
UITouch *touch = [touches anyObject];
//save previous contact locations
lastContactPoint2 = lastContactPoint1;
lastContactPoint1 = [touch previousLocationInView:mySignatureImage];
//save current location
currentPoint = [touch locationInView:mySignatureImage];
//find mid points to be used for quadratic bezier curve
CGPoint midPoint1 = [self midPoint:lastContactPoint1 withPoint:lastContactPoint2];
CGPoint midPoint2 = [self midPoint:currentPoint withPoint:lastContactPoint1];
//create a bitmap-based graphics context and makes it the current context
UIGraphicsBeginImageContext(imageFrame.size);
//draw the entire image in the specified rectangle frame
[mySignatureImage.image drawInRect:CGRectMake(0, 0, imageFrame.size.width, imageFrame.size.height)];
//set line cap, width, stroke color and begin path
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 3.0f);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextBeginPath(UIGraphicsGetCurrentContext());
//begin a new new subpath at this point
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), midPoint1.x, midPoint1.y);
//create quadratic BĂ©zier curve from the current point using a control point and an end point
CGContextAddQuadCurveToPoint(UIGraphicsGetCurrentContext(),
lastContactPoint1.x, lastContactPoint1.y, midPoint2.x, midPoint2.y);
//set the miter limit for the joins of connected lines in a graphics context
CGContextSetMiterLimit(UIGraphicsGetCurrentContext(), 2.0);
//paint a line along the current path
CGContextStrokePath(UIGraphicsGetCurrentContext());
//set the image based on the contents of the current bitmap-based graphics context
mySignatureImage.image = UIGraphicsGetImageFromCurrentImageContext();
//remove the current bitmap-based graphics context from the top of the stack
UIGraphicsEndImageContext();
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
//if the finger never moved draw a point
if(!fingerMoved) {
UIGraphicsBeginImageContext(imageFrame.size);
[mySignatureImage.image drawInRect:CGRectMake(0, 0, imageFrame.size.width, imageFrame.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 3.0f);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
CGContextFlush(UIGraphicsGetCurrentContext());
mySignatureImage.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
}
//calculate midpoint between two points
- (CGPoint) midPoint:(CGPoint )p0 withPoint: (CGPoint) p1 {
return (CGPoint) {
(p0.x + p1.x) / 2.0,
(p0.y + p1.y) / 2.0
};
}
I'm sorry to tell you that I haven't a real solution, but your problem most probably is due to performance issues. Why? Because you are creating an image each time a gesture is detected. Creating images requires of screen renderings that takes time and resources.
You should base your code on same project that has drawing functionalities, usually that use a view that updates their draw in the drawRect method, for you maybe a CAShapeLaywr is also fine.
Run Time Profiler in instruments and search wich method is tanking time.

iOS adding points to quadratic curves and manipulating

I'm starting out with a straight line, as shown below.
I need the user to be able to drag and manipulate this line with their finger in real time. So they might end up with a series or curves as below:
It is simple to do this with one curve, as in the code below.
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
controlPoint = [[touches anyObject] locationInView:self];
CGRect controlRect = CGRectMake(controlPoint.x - 50.0, controlPoint.y - 50.0, 100.0, 100.0);
[self setNeedsDisplayInRect:controlRect];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, 0 , screenHeight/2 + gapDistance);
CGContextAddQuadCurveToPoint(context, controlPoint.x + gapDistance, controlPoint.y + gapDistance, 100, screenHeight/2 + gapDistance);
CGContextStrokePath(context);
}
My question is, how do I do it with multiple curves? I will also need to be able to add smaller curves to existing curves if the user so desires.

UIBezierPath Array rendering issue (How to use CGLayerRef for optimization)

friends, I am working with UIBezierPaths, for free hand drawing , and everything works fine, for this I am storing my paths in a path array, everything works fine, while rendering, I am looping the whole array and rendering the paths,but soon as the array count increases, I see a lag while drawing, below is my drawRect code. please help me out in finding where I am going wrong
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
m_previousPoint2 = m_previousPoint1;
m_previousPoint1 = [mytouch previousLocationInView:self];
m_currentPoint = [mytouch locationInView:self];
CGPoint mid1 = midPoint(m_previousPoint1, m_previousPoint2);
CGPoint mid2 = midPoint(m_currentPoint, m_previousPoint1);
testpath = CGPathCreateMutable();
CGPathMoveToPoint(testpath, NULL, mid1.x, mid1.y);
CGPathAddQuadCurveToPoint(testpath, NULL, m_previousPoint1.x, m_previousPoint1.y, mid2.x, mid2.y);
CGRect bounds = CGPathGetBoundingBox(testpath);
CGPathRelease(testpath);
CGRect drawBox = bounds;
//Pad our values so the bounding box respects our line width
drawBox.origin.x -= self.lineWidth * 2;
drawBox.origin.y -= self.lineWidth * 2;
drawBox.size.width += self.lineWidth * 4;
drawBox.size.height += self.lineWidth * 4;
CGContextRef context = UIGraphicsGetCurrentContext();
context = CGLayerGetContext(myLayerRef);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
[self setNeedsDisplayInRect:drawBox];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
if(myLayerRef == nil)
{
myLayerRef = CGLayerCreateWithContext(context, self.bounds.size, NULL);
}
[self.layer renderInContext:context];
CGPoint mid1 = midPoint(m_previousPoint1, m_previousPoint2);
CGPoint mid2 = midPoint(m_currentPoint, m_previousPoint1);
CGContextMoveToPoint(context, mid1.x, mid1.y);
CGContextAddQuadCurveToPoint(context, m_previousPoint1.x, m_previousPoint1.y, mid2.x, mid2.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetFlatness(context, 0.1);
CGContextSetAllowsAntialiasing(context, true);
CGContextStrokePath(context);
[super drawRect:rect];
}
Code updated according to the below discussion by #borrrden
Regards
Ranjit
There is an excellent session in the WWDC 2012 sessions about this exact problem. I think it is called iOS performance: Graphics or something similar. You should definitely watch it.
The summary is, DON'T store it in a path. There is no need to keep this path information around. Store a separate context (CGLayer is best) and draw into that (just like you add points to the path, add points to the context intead). Then in your drawRect, simply draw that layer into the current graphics context. Also be sure to only call setNeedsDisplay on the rectangle that changed, or you will most likely run into problems on a high resolution device.

Resources