iPhone smooth sketch drawing algorithm - ios

I am working on a sketching app on the iPhone.
I got it working but not pretty as seen here
And I am looking for any suggestion to smooth the drawing
Basically, what I did is when user places a finger on the screen I called
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
then I collect a single touch in an array with
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
and when the user lefts a finger from the screen, I called
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
then I draw all the points in the array using
NSMutableArray *points = [collectedArray points];
CGPoint firstPoint;
[[points objectAtIndex:0] getValue:&firstPoint];
CGContextMoveToPoint(context, firstPoint.x, firstPoint.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
for (int i=1; i < [points count]; i++) {
NSValue *value = [points objectAtIndex:i];
CGPoint point;
[value getValue:&point];
CGContextAddLineToPoint(context, point.x, point.y);
}
CGContextStrokePath(context);
UIGraphicsPushContext(context);
And now I want to improve the drawing tobe more like "Sketch Book" App
I think there is something to do with signal processing algorithm to rearrange all the points in the array but I am not sure. Any Help would be much appreciated.
Thankz in advance :)

CGPoint midPoint(CGPoint p1, CGPoint p2)
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
previousPoint1 = [touch previousLocationInView:self];
previousPoint2 = [touch previousLocationInView:self];
currentPoint = [touch locationInView:self];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
previousPoint2 = previousPoint1;
previousPoint1 = [touch previousLocationInView:self];
currentPoint = [touch locationInView:self];
// calculate mid point
CGPoint mid1 = midPoint(previousPoint1, previousPoint2);
CGPoint mid2 = midPoint(currentPoint, previousPoint1);
UIGraphicsBeginImageContext(self.imageView.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.imageView.image drawInRect:CGRectMake(0, 0, self.imageView.frame.size.width, self.imageView.frame.size.height)];
CGContextMoveToPoint(context, mid1.x, mid1.y);
// Use QuadCurve is the key
CGContextAddQuadCurveToPoint(context, previousPoint1.x, previousPoint1.y, mid2.x, mid2.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, 2.0);
CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextStrokePath(context);
self.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}

The easiest way to smooth a curve like this is to use a Bezier curve instead of straight line segments. For the math behind this, see this article (pointed to in this answer), which describes how to calculate the curves required to smooth a curve that passes through multiple points.
I believe that the Core Plot framework now has the ability to smooth the curves of plots, so you could look at the code used there to implement this kind of smoothing.
There's no magic to any of this, as these smoothing routines are fast and relatively easy to implement.

I really love the topic. Thanks for all the implementations, espesially Krzysztof Zabłocki and Yu-Sen Han.
I have modified the version of Yu-Sen Han in order to change line thickness depending on the speed of panning (in fact the distance between last touches). Also I've implemented dot drawing (for touchBegan and touchEnded locations being close to each other)
Here is the result:
To define the line thickness I've chosen such a function of distance:
(Don't ask me why... I just though it suits well, but I'm sure you can find a better one)
CGFloat dist = distance(previousPoint1, currentPoint);
CGFloat newWidth = 4*(atan(-dist/15+1) + M_PI/2)+2;
One more hint. To be sure the thickness is changing smoothly, I've bounded it depending on the thickness of the previous segment and a custom coef:
self.lineWidth = MAX(MIN(newWidth,lastWidth*WIDTH_RANGE_COEF),lastWidth/WIDTH_RANGE_COEF);

I translated kyoji's answer into Swift, as a reusable subclass of UIImageView. The subclass TouchDrawImageView allows the user to draw on an image view with her finger.
Once you've added this TouchDrawImageView class to your project, make sure to open your storyboard and
select TouchDrawImageView as the "Custom Class" of your image view
check "User Interaction Enabled" property of your image view
Here's the code of TouchDrawImageView.swift:
import UIKit
class TouchDrawImageView: UIImageView {
var previousPoint1 = CGPoint()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
previousPoint1 = touch.previousLocation(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocation(in: self)
let currentPoint = touch.location(in: self)
// calculate mid point
let mid1 = midPoint(p1: previousPoint1, p2: previousPoint2)
let mid2 = midPoint(p1: currentPoint, p2: previousPoint1)
UIGraphicsBeginImageContext(self.frame.size)
guard let context = UIGraphicsGetCurrentContext() else { return }
if let image = self.image {
image.draw(in: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
}
context.move(to: mid1)
context.addQuadCurve(to: mid2, control: previousPoint1)
context.setLineCap(.round)
context.setLineWidth(2.0)
context.setStrokeColor(red: 1.0, green: 0, blue: 0, alpha: 1.0)
context.strokePath()
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func midPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2.0, y: (p1.y + p2.y) / 2.0)
}
}

Thankz for the input.I update my quest here because I need the space for it.
I look up both corePlot and Bezier curve solutions that you suggested with little success.
For the corePlot I am able to get the graph plot from an array of int but can't find anything related to curve smoothing.BTW Here I am using CPScatterPlot with some random number.
as for Bezier curve, My quest lead me to here It is something to do with Spline implementation in iOS
CatmullRomSpline *myC = [[CatmullRomSpline alloc] initAtPoint:CGPointMake(1.0, 1.0)];
[myC addPoint:CGPointMake(1.0, 1.5)];
[myC addPoint:CGPointMake(1.0, 1.15)];
[myC addPoint:CGPointMake(1.0, 1.25)];
[myC addPoint:CGPointMake(1.0, 1.23)];
[myC addPoint:CGPointMake(1.0, 1.24)];
[myC addPoint:CGPointMake(1.0, 1.26)];
NSLog(#"xxppxx %#",[myC asPointArray]);
NSLog(#"xxppxx2 %#",myC.curves);
and the result I get is:
2011-02-24 14:45:53.915 DVA[10041:40b] xxppxx (
"NSPoint: {1, 1}",
"NSPoint: {1, 1.26}"
)
2011-02-24 14:45:53.942 DVA[10041:40b] xxppxx2 (
"QuadraticBezierCurve: 0x59eea70"
)
I am not really sure how to go from there. So I am stuck on that front as well :(
I did look up GLPaint, as a last resource. It uses OpenGLES and use a "soft dot" sprite to plot the points in the array. I know it's more like avoiding the problem rather than fixing it. But I guess I'l share my findings here anyway.
The Black is GLPaint and the white one is the old method. And the last one is the drawing from "Sketch Book" app just to compare
I am still trying to get this done right, any further suggestion are most welcome.

To get rid of the silly dot in the GLPaint code.
Change in
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
this function
//Ändrat av OLLE
/*
// Convert touch point from UIView referential to OpenGL one (upside-down flip)
if (firstTouch) {
firstTouch = NO;
previousLocation = [touch previousLocationInView:self];
previousLocation.y = bounds.size.height - previousLocation.y;
} else {
location = [touch locationInView:self];
location.y = bounds.size.height - location.y;
previousLocation = [touch previousLocationInView:self];
previousLocation.y = bounds.size.height - previousLocation.y;
}
*/
location = [touch locationInView:self];
location.y = bounds.size.height - location.y;
previousLocation = [touch previousLocationInView:self];
previousLocation.y = bounds.size.height - previousLocation.y;
//Ändrat av OLLE//
I know that this isn't the solution for our problem, but it's something.

Related

UIButton - move and scale

I have an UIButton that I've creates programmatically. Actually it should'n be UIButton, I just need to have possibility to mark some area above the image.
So the features I need it - move object and resize it. For this i have 2 methods:
- (void) objMove:(id) sender withEvent:(UIEvent *) event
{
UIControl *control = sender;
UITouch *t = [[event allTouches] anyObject];
CGPoint pPrev = [t previousLocationInView:control];
CGPoint p = [t locationInView:control];
CGPoint center = control.center;
center.x += p.x - pPrev.x;
center.y += p.y - pPrev.y;
control.center = center;
}
- (void)objScale:(UIPinchGestureRecognizer *)recognizer
{
UIView *pinchView = recognizer.view;
CGRect bounds = pinchView.bounds;
CGPoint pinchCenter = [recognizer locationInView:pinchView];
pinchCenter.x -= CGRectGetMidX(bounds);
pinchCenter.y -= CGRectGetMidY(bounds);
CGAffineTransform transform = pinchView.transform;
transform = CGAffineTransformTranslate(transform, pinchCenter.x, pinchCenter.y);
CGFloat scale = recognizer.scale;
transform = CGAffineTransformScale(transform, scale, scale);
transform = CGAffineTransformTranslate(transform, -pinchCenter.x, -pinchCenter.y);
pinchView.transform = transform;
recognizer.scale = 1.0;
}
Scale works ok. Moving looks ok until I change the size of object - when i increase object it become moves slower than finger, and vice versa - if object smaller than original it moves faster than finger. why it works like this?
I think you should get startPoint and startCenter in
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// get startPoint and startCenter here
}
- (void) objMove:(id) sender withEvent:(UIEvent *) event
{
UIControl *control = sender;
UITouch *t = [[event allTouches] anyObject];
CGPoint p = [t locationInView:control];
startCenter.x += p.x - startPoint.x;
startCenter.y += p.y - startPoint.y;
control.center = startCenter;
}
Change your code like this, maybe it works.
Your center is current center, p is current point, pPrev is previous point.
current center adds previous point moved size is wrong.
You should get relative distance, not dynamic distance.

Measure distance of line drawn on iPhone screen

I am trying to creating an application that allows the user to draw a line on the screen and it measures the distance of the drawn line. I have been able to successfully draw the line but I don't know how to measure it. The line does not have to be perfectly straight either. It is basically a squiggle. If someone could please point me in the right direction or help guide me that would be awesome. I am using Xcode 5.1.1 and objective-c. I only just started dabbling in the language this summer.
EDIT: I am looking to measure the distance in either inches or cm. I would like the measurement to be the entire line, to follow the curve of the line. The distance not the displacement.
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
mouseSwipe = YES; //swipe declared in header
UITouch *touch = [touches anyObject];
currentPoint = [touch locationInView:self.view]; //tracking finger movement on screen
UIGraphicsBeginImageContext(CGSizeMake(320, 568)); // 568 iphone 5, 480 is iphone 4 (320,525)
[drawImage.image drawInRect:CGRectMake(0, 0, 320, 568)]; // 0,0 centered in corner
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound); //round line end
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 5.0); // width of line
CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [[UIColor redColor] CGColor]); //sets color to red (change red to any color for that color)
//CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0,1,0,1); //green color
CGContextBeginPath(UIGraphicsGetCurrentContext()); //start of when drawn path
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextStrokePath(UIGraphicsGetCurrentContext());
[drawImage setFrame:CGRectMake(0, 0, 320, 568)]; //(320, 568)
drawImage.image = UIGraphicsGetImageFromCurrentImageContext(); //importnant
UIGraphicsEndImageContext(); //finished drawing for time period
lastPoint = currentPoint;
[self.view addSubview:drawImage]; //adds to page
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject]; //touch fires of touch
location = [touch locationInView:touch.view];
lastClick = [NSDate date];
lastPoint = [touch locationInView:self.view]; //stops connecting to previous line
lastPoint.y -= 0;
[super touchesEnded:touches withEvent: event];
}
First add a property that cumulates the length of the path.
#property(nonatomic, assign) CGFloat pathLength;
Initialize it to 0.0 when user begins drawing the path. Maybe do this in touchesBegan, or the same place elsewhere in your code where you realize you're beginning to draw. Add a method that computes the cartesian distance between points:
- (CGFloat)distanceFrom:(CGPoint)p1 to:(CGPoint)p2 {
CGFloat x = (p2.x - p1.x);
CGFloat y = (p2.y - p1.y);
return sqrt(x*x + y*y);
}
As you get touches moved, you are already handling the current and last touch positions. All you must do now is cumulate the distance between successive points:
// in touches moved, after you have lastPoint and currentPoint
self.pathLength += [self distanceFrom:currentPoint to:lastPoint];
There are quite a few refs here and elsewhere for converting these points to inches or cm. As far as I can see all are fraught with the inability to get the device resolution at runtime from the SDK. If you're willing to add a (dangerous) constant to the code, you can get PPI here, and divide that into the pathLength computed above.

Define custom touch area in custom UIControl object

I am creating a custom UIControl object as detailed here. It is all working well except for the touch area.
I want to find a way to limit the touch area to only part of the control, in the example above I want it to be restricted to the black circumference only rather than the whole control area.
Any idea?
Cheers
You can override UIView's pointInside:withEvent: to reject unwanted touches.
Here's a method that checks if the touch occurred in a ring around the center of the view:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
UITouch *touch = [[event touchesForView:self] anyObject];
if (touch == nil)
return NO;
CGPoint touchPoint = [touch locationInView:self];
CGRect bounds = self.bounds;
CGPoint center = { CGRectGetMidX(bounds), CGRectGetMidY(bounds) };
CGVector delta = { touchPoint.x - center.x, touchPoint.y - center.y };
CGFloat squareDistance = delta.dx * delta.dx + delta.dy * delta.dy;
CGFloat outerRadius = bounds.size.width * 0.5;
if (squareDistance > outerRadius * outerRadius)
return NO;
CGFloat innerRadius = outerRadius * 0.5;
if (squareDistance < innerRadius * innerRadius)
return NO;
return YES;
}
To detect other hits on more complex shapes you can use a CGPath to describe the shape and test using CGPathContainsPoint. Another way is to use an image of the control and test the pixel's alpha value.
All that depends on how you build your control.

Center moving drawRect?

In my app, a circle is drawn based on a users drag. e.g. a user taps, that is the center of a circle that will be drawn, and as they drag their finger, the circle will grow to that point. This works, except for some reason the center moves down and to the right as the radius of the circle grows. Why is this happening? Here is what I am trying:
#implementation CircleView{
CGPoint center;
CGPoint endPoint;
CGFloat distance;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
center = [touch locationInView:self];
[self setNeedsDisplay];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
endPoint = [touch locationInView:self];
CGFloat xDist = (endPoint.x - center.x);
CGFloat yDist = (endPoint.y - center.y);
distance = sqrt((xDist * xDist) + (yDist * yDist));
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGRect rectangle = CGRectMake(center.x,center.y,distance, distance);
CGContextAddEllipseInRect(context, rectangle);
CGContextStrokePath(context);
}
What should be happening is that center point never ever moves, and the circle should just grow. Any ideas?
CGRect rectangle = CGRectMake(center.x,center.y,distance, distance);
should be:
CGRect rectangle = CGRectMake(center.x - distance, center.y - distance, distance * 2, distance * 2);
Because in CGRectMake you have to specify the origin (and the size) of the rectangle, not the center.
CGRect rectangle = CGRectMake(center.x - distance, center.y - distance, distance * 2, distance * 2);

Using TouchesMoved to draw a shape

I am trying to draw an arrow using my finger on the screen. the idea was that by touching the screen I set the initial coordinates of my arrow, and as I dragged on the screen the arrow would extend and follow my finger. the height and width of the arrow will be the same, its the size of the arrow that matters. The arrow will be longer as I drag it away from the starting point. I tried dong it with something like this:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UIGraphicsBeginImageContext(CGSizeMake(1536, 2048));
UITouch *touch = [touches anyObject];
CGPoint p1 = [touch locationInView:self.view];
CGSize size;
size.width = 50;
size.height = 400;
CGContextRef context = UIGraphicsGetCurrentContext();
[self drawArrowWithContext:context atPoint:p1 withSize:size lineWidth:4 arrowHeight:20 andColor:[UIColor whiteColor]];
// converts your context into a UIImage
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// Adds that image into an imageView and sticks it on the screen.
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[self.view addSubview:imageView];
}
and
- (void) drawArrowWithContext:(CGContextRef)context atPoint:(CGPoint)startPoint withSize: (CGSize)size lineWidth:(float)width arrowHeight:(float)aheight andColor:(UIColor *)color
{
float width_wing = (size.width - width) / 2;
float main = size.height-aheight;
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextSetStrokeColorWithColor(context, [color CGColor]);
CGPoint rectangle_points[] = {
CGPointMake(startPoint.x + width_wing, startPoint.y + 0.0),
CGPointMake(startPoint.x + width_wing, startPoint.y + main),
CGPointMake(startPoint.x + 0.0, startPoint.y + main), // left point
CGPointMake(startPoint.x + size.width / 2, startPoint.y + size.height),
CGPointMake(startPoint.x + size.width, startPoint.y + main), // right point
CGPointMake(startPoint.x + size.width-width_wing, startPoint.y + main),
CGPointMake(startPoint.x + size.width-width_wing, startPoint.y + 0.0),
CGPointMake(startPoint.x + width_wing, startPoint.y + 0.0),
};
CGContextAddLines(context, rectangle_points, 8);
CGContextFillPath(context);
}
The arrow does appear on the screen if I run the code from the touches moved in a normal IBOutlet, but that was not the idea. I haven't managed to get this code to work yet, but I think that even if it worked, it would cause a crash, since I am deleting and redrawing the shape each time. Is this the right approach? What should I do?
Basically, there are some different options.
Stick to the UIImageView approach and stop recreating the image view all the time. Keep a reference to that UIImageView in the class detecting the touch events and just replace the image if something changed. Might be worth the extra effort for off-screen-drawing if you need the image for something else.
Implement the arrow drawing dynamically in an extra view.
The second one I will describe here briefly:
The class detecting the event needs a member variable/property, ArrowView *arrowView and something to remember the start point, CGPoint startPoint maybe.
ArrowView needs properties for the arrow parameters, CGPoint arrowStart, CGSize arrowSize.
In the touch event handler, do
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint position = [touch locationInView:self.view];
[self.startPoint startFrom:position];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint next = [touch locationInView:self.view];
CGSize size;
size.width = next.x - self.startPoint.x;
size.height = next.y - self.startPoint.y;
self.arrowView.arrowPoint = self.startPoint;
self.arrowView.arrowSize = size;
[self.arrowView setNeedsDisplay];
}
In ArrowView:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
// Now do the drawing stuff here using that context!
// self.arrowSize / self.arrowPoint do contain your drawing parameters.
...
}
I hope that gives you an idea.
For further reading, start here.
Yo can try to use UIPanGestureRecognizer to follow your finger and draw above of it.

Resources