How do you smooth a set of points in an iOS drawing app WHILE MOVING? I have tried UIBezierpaths but all I get are jagged ends where they intersect, when I just shift the po
Swift:
let point1 = CGPoint(x: 50, y: 100)
let point2 = CGPoint(x: 50 + 1 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 200)
let point3 = CGPoint(x: 50 + 2 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 250)
let point4 = CGPoint(x: 50 + 3 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 50)
let point5 = CGPoint(x: 50 + 4 * CGFloat(60) * UIScreen.main.bounds.width / 375, y: 100)
let points = [point1, point2, point3, point4, point5]
let bezier = UIBezierPath()
let count = points.count
var prevDx = CGFloat(0)
var prevDy = CGFloat(0)
var prevX = CGFloat(0)
var prevY = CGFloat(0)
let div = CGFloat(7)
for i in 0..<count {
let x = points[i].x
let y = points[i].y
var dx = CGFloat(0)
var dy = CGFloat(0)
if (i == 0) {
bezier.move(to: points[0])
let nextX = points[i + 1].x
let nextY = points[i + 1].y
prevDx = (nextX - x) / div
prevDy = (nextY - y) / div
prevX = x
prevY = y
} else if (i == count - 1) {
dx = (x - prevX) / div
dy = (y - prevY) / div
} else {
let nextX = points[i + 1].x
let nextY = points[i + 1].y
dx = (nextX - prevX) / div;
dy = (nextY - prevY) / div;
}
bezier.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: prevX + prevDx, y: prevY + prevDy), controlPoint2: CGPoint(x: x - dx, y: y - dy))
prevDx = dx;
prevDy = dy;
prevX = x;
prevY = y;
}
Dont need to write this much of code.
Just refer to the ios freehand drawing tutorial; it really smoothen the drawing, also cache mechanism is there so that performance does not go down even when you keep drawing continuously.
I tried all of the above, but can't make it work. One of the answer yield a broken result for me even. Upon searching more I found this: https://github.com/sam-keene/uiBezierPath-hermite-curve. I did not write this code, but I implemented it and it works really really well. Just copy the UIBezierPath+Interpolation.m/h and CGPointExtension.m/h. Then you use it like this:
UIBezierPath *path = [UIBezierPath interpolateCGPointsWithHermite:arrayPoints closed:YES];
It is really a robust and neat solution overall.
I just implemented something similar in a project I am working on. My solution was to use a Catmull-Rom spline instead of using Bezier splines. These provide a very smooth curve THROUGH a set a points rather then a bezier spline 'around' points.
// Based on code from Erica Sadun
#import "UIBezierPath+Smoothing.h"
void getPointsFromBezier(void *info, const CGPathElement *element);
NSArray *pointsFromBezierPath(UIBezierPath *bpath);
#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]]
#define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue]
@implementation UIBezierPath (Smoothing)
// Get points from Bezier Curve
void getPointsFromBezier(void *info, const CGPathElement *element)
{
NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;
// Retrieve the path element type and its points
CGPathElementType type = element->type;
CGPoint *points = element->points;
// Add the points if they're available (per type)
if (type != kCGPathElementCloseSubpath)
{
[bezierPoints addObject:VALUE(0)];
if ((type != kCGPathElementAddLineToPoint) &&
(type != kCGPathElementMoveToPoint))
[bezierPoints addObject:VALUE(1)];
}
if (type == kCGPathElementAddCurveToPoint)
[bezierPoints addObject:VALUE(2)];
}
NSArray *pointsFromBezierPath(UIBezierPath *bpath)
{
NSMutableArray *points = [NSMutableArray array];
CGPathApply(bpath.CGPath, (__bridge void *)points, getPointsFromBezier);
return points;
}
- (UIBezierPath*)smoothedPathWithGranularity:(NSInteger)granularity;
{
NSMutableArray *points = [pointsFromBezierPath(self) mutableCopy];
if (points.count < 4) return [self copy];
// Add control points to make the math make sense
[points insertObject:[points objectAtIndex:0] atIndex:0];
[points addObject:[points lastObject]];
UIBezierPath *smoothedPath = [self copy];
[smoothedPath removeAllPoints];
[smoothedPath moveToPoint:POINT(0)];
for (NSUInteger index = 1; index < points.count - 2; index++)
{
CGPoint p0 = POINT(index - 1);
CGPoint p1 = POINT(index);
CGPoint p2 = POINT(index + 1);
CGPoint p3 = POINT(index + 2);
// now add n points starting at p1 + dx/dy up until p2 using Catmull-Rom splines
for (int i = 1; i < granularity; i++)
{
float t = (float) i * (1.0f / (float) granularity);
float tt = t * t;
float ttt = tt * t;
CGPoint pi; // intermediate point
pi.x = 0.5 * (2*p1.x+(p2.x-p0.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*tt + (3*p1.x-p0.x-3*p2.x+p3.x)*ttt);
pi.y = 0.5 * (2*p1.y+(p2.y-p0.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*tt + (3*p1.y-p0.y-3*p2.y+p3.y)*ttt);
[smoothedPath addLineToPoint:pi];
}
// Now add p2
[smoothedPath addLineToPoint:p2];
}
// finish by adding the last point
[smoothedPath addLineToPoint:POINT(points.count - 1)];
return smoothedPath;
}
@end
The original Catmull-Rom implementation is based on some code from Erica Sadun in one of her books, I modified it slightly to allow for a full smoothed curve. This is implemented as a category on UIBezierPath and worked out very well for me.
For achieving this we need to use this method. BezierSpline the code is in C# to generate arrays of control points for a bezier spline. I converted this code to Objective C and it works brilliantly for me.
To convert the code from C# to Objective C. understand the C# code line by line, even if you dont know C#, u must be knowing C++/Java ?
While converting:
Replace Point struct used here with CGPoint.
Replace Point array with NSMutableArray and store NSvalues wrapping CGPoints in it.
Replace all double arrays with NSMutableArrays and store NSNumber wrapping double in it.
use objectAtIndex: method in case of subscript for accessing array elements.
use replaceObjectAtIndex:withObject: to store objects at specific index.
Remember that NSMutableArray is a linkedList and what C# uses are dynamic arrays so they already have existing indices. In your case, in a NSMutableArray if it is empty, you cant store objects at random indices as the C# code does. they at times in this C# code, populate index 1 before index 0 and they can do so as index 1 exists. in NSMutabelArrays here, index 1 should be there if u want to call replaceObject on it. so before storing anything make a method that will add n NSNull objects in the NSMutableArray.
ALSO :
well this logic has a static method that will accept an array of points and give you two arrays:-
array of first control points.
array of second control points.
These arrays will hold first and second control point for each curve between two points you pass in the first array.
In my case, I already had all the points and I could draw curve through them.
In you case while drawing, you will need to some how supply a set of points through which you want a smooth curve to pass.
and refresh by calling setNeedsDisplay and draw the spline which is nothing but UIBezierPath between two adjacent points in the first array. and taking control points from both the control point arrays index wise.
Problem in your case is that, its difficult to understand while moving what all critical points to take.
What you can do is: Simply while moving the finger keep drawing straight lines between previous and current point. Lines will be so small that it wont be visible to naked eye that they are small small straight lines unless you zoom in.
UPDATE
Anyone interested in an Objective C implementation of the link above can refer to
this GitHub repo.
I wrote it sometime back and it doesn't support ARC, but you can easily edit it and remove few release and autorelease calls and get it working with ARC.
This one just generates two arrays of control points for a set of points which one wants to join using bezier spline.
Some good answers here, though I think they are either way off (user1244109's answer only supports horizontal tangents, not useful for generic curves), or overly complicated (sorry Catmull-Rom fans).
I implemented this in a much simpler way, using Quad bezier curves. These need a start point, an end point, and a control point. The natural thing to do might be to use the touch points as the start & end points. Don't do this! There are no appropriate control points to use. Instead, try this idea: use the touch points as control points, and the midpoints as the start/end points. You're guaranteed to have proper tangents this way, and the code is stupid simple. Here's the algorithm:
location
in prevPoint
.midPoint
, the point between currentPoint
and prevPoint
.
currentPoint
as a line segment.midPoint
, and use the prevPoint
as the control point. This will create a segment that gently curves from the previous point to the current point.currentPoint
in prevPoint
, and repeat #2 until dragging ends.This results in very good looking curves, because using the midPoints guarantees that the curve is a smooth tangent at the end points (see attached photo).
Swift code looks like this:
var bezierPath = UIBezierPath()
var prevPoint: CGPoint?
var isFirst = true
override func touchesBegan(touchesSet: Set<UITouch>, withEvent event: UIEvent?) {
let location = touchesSet.first!.locationInView(self)
bezierPath.removeAllPoints()
bezierPath.moveToPoint(location)
prevPoint = location
}
override func touchesMoved(touchesSet: Set<UITouch>, withEvent event: UIEvent?) {
let location = touchesSet.first!.locationInView(self)
if let prevPoint = prevPoint {
let midPoint = CGPoint(
x: (location.x + prevPoint.x) / 2,
y: (location.y + prevPoint.y) / 2,
)
if isFirst {
bezierPath.addLineToPoint(midPoint)
else {
bezierPath.addQuadCurveToPoint(midPoint, controlPoint: prevPoint)
}
isFirst = false
}
prevPoint = location
}
override func touchesEnded(touchesSet: Set<UITouch>, withEvent event: UIEvent?) {
let location = touchesSet.first!.locationInView(self)
bezierPath.addLineToPoint(location)
}
Or, if you have an array of points and want to construct the UIBezierPath
in one shot:
var points: [CGPoint] = [...]
var bezierPath = UIBezierPath()
var prevPoint: CGPoint?
var isFirst = true
// obv, there are lots of ways of doing this. let's
// please refrain from yak shaving in the comments
for point in points {
if let prevPoint = prevPoint {
let midPoint = CGPoint(
x: (point.x + prevPoint.x) / 2,
y: (point.y + prevPoint.y) / 2,
)
if isFirst {
bezierPath.addLineToPoint(midPoint)
}
else {
bezierPath.addQuadCurveToPoint(midPoint, controlPoint: prevPoint)
}
isFirst = false
}
else {
bezierPath.moveToPoint(point)
}
prevPoint = point
}
if let prevPoint = prevPoint {
bezierPath.addLineToPoint(prevPoint)
}
Here are my notes: