Can we avoid the extra bezier curve line after the last point in Bezier graph? Is there any property for that?
We need just end-end curve graph, no extra curve. Please share the solutions/suggestion. Update: Please check the code below,
- (void)drawRect:(CGRect)rect {
UIBezierPath *line = [UIBezierPath bezierPath];
NSValue *value = points[0];
CGPoint p1 = [value CGPointValue];
[line moveToPoint:p1];if (points.count == 2) {
value = points[1];
CGPoint p2 = [value CGPointValue];
[line addLineToPoint:p2];
return line;
}
for (NSUInteger i = 1; i < points.count; i++) {
value = points[i];
CGPoint p2 = [value CGPointValue];
CGPoint midPoint = midPointForPoints(p1, p2);
[line addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
[line addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
p1 = p2;
}
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.bounds;
pathLayer.path = line.CGPath;
pathLayer.strokeColor = self.color.CGColor;
pathLayer.fillColor = nil;
pathLayer.opacity = self.lineAlpha;
pathLayer.lineWidth = self.lineWidth;
pathLayer.lineJoin = kCALineJoinBevel;
pathLayer.lineCap = kCALineCapRound;
[self.layer addSublayer:pathLayer];}
static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);}
Thanks!
Related
How to draw an arc as shown in the image as suppose i have a UIView as the middle point of the line which is movable.
i had store line's start point in CGPoint name lineStartPoint and end point as lineEndPoint.
the moving object can be accessed through CGPoint named movingPoint.
Thanks in advance for help.
Here is an example how to calculate the required parameters for the bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise: method.
- (void)viewDidLoad {
[super viewDidLoad];
// for example
CGPoint lineStartPoint = {100,100};
CGPoint lineEndPoint = {100,200};
CGPoint movingPoint = {30,150};
CGFloat eps = 1e-5;
CGVector v1 = {movingPoint.x-lineEndPoint.x, movingPoint.y-lineEndPoint.y};
CGFloat dist1 = sqrt(v1.dx*v1.dx + v1.dy*v1.dy);
v1.dx = v1.dx/dist1;
v1.dy = v1.dy/dist1;
CGVector v2 = {movingPoint.x-lineStartPoint.x, movingPoint.y-lineStartPoint.y};
CGFloat dist2 = sqrt(v2.dx*v2.dx + v2.dy*v2.dy);
v2.dx = v2.dx/dist2;
v2.dy = v2.dy/dist2;
CGFloat det = v1.dx*v2.dy - v1.dy*v2.dx;
if (fabs(det) < eps) {
// the three points are collinear
// TODO: draw a line from lineStartPoint to lineEndPoint
return;
}
CGPoint mid1 = {(movingPoint.x+lineEndPoint.x)/2, (movingPoint.y+lineEndPoint.y)/2};
CGPoint mid2 = {(movingPoint.x+lineStartPoint.x)/2, (movingPoint.y+lineStartPoint.y)/2};
CGFloat b1 = v1.dx*mid1.x + v1.dy*mid1.y;
CGFloat b2 = v2.dx*mid2.x + v2.dy*mid2.y;
CGFloat centerX = v2.dy/det*b1 - v1.dy/det*b2;
CGFloat centerY = -v2.dx/det*b1 + v1.dx/det*b2;
CGPoint center = {centerX, centerY};
CGFloat radius = sqrtf((movingPoint.x-center.x)*(movingPoint.x-center.x) + (movingPoint.y-center.y)*(movingPoint.y-center.y));
CGFloat startAngle = atan2f(lineStartPoint.y-center.y, lineStartPoint.x-center.x);
CGFloat movingAngle = atan2f(movingPoint.y-center.y, movingPoint.x-center.x);
CGFloat endAngle = atan2f(lineEndPoint.y-center.y, lineEndPoint.x-center.x);
BOOL isClockwise;
if ((endAngle>startAngle && startAngle<movingAngle && movingAngle<endAngle) ||
(endAngle<startAngle && !(endAngle<movingAngle && movingAngle<startAngle))) {
isClockwise = YES;
} else {
isClockwise = NO;
}
//Show results
CAShapeLayer* startPointLayer = [[CAShapeLayer alloc] init];
startPointLayer.path = [UIBezierPath bezierPathWithArcCenter:lineStartPoint radius:2 startAngle:0 endAngle:2*M_PI clockwise:YES].CGPath;
[self.view.layer addSublayer:startPointLayer];
CAShapeLayer* endPointLayer = [[CAShapeLayer alloc] init];
endPointLayer.path = [UIBezierPath bezierPathWithArcCenter:lineEndPoint radius:2 startAngle:0 endAngle:2*M_PI clockwise:YES].CGPath;
[self.view.layer addSublayer:endPointLayer];
CAShapeLayer* movingPointLayer = [[CAShapeLayer alloc] init];
movingPointLayer.path = [UIBezierPath bezierPathWithArcCenter:movingPoint radius:2 startAngle:0 endAngle:2*M_PI clockwise:YES].CGPath;
[self.view.layer addSublayer:movingPointLayer];
CAShapeLayer* arcLayer = [[CAShapeLayer alloc] init];
[arcLayer setFillColor:[UIColor clearColor].CGColor];
[arcLayer setStrokeColor:[UIColor blueColor].CGColor];
arcLayer.path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:isClockwise].CGPath;
[self.view.layer addSublayer:arcLayer];
}
I have some points like
(x1,y1), (x2,y2), (x3,y3)...
Now I want to draw a chart with smooth curve?
I'm trying to draw as below
-(void)drawPrices
{
NSInteger count = self.prices.count;
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineCapStyle = kCGLineCapRound;
for(int i=0; i<count-1; i++)
{
CGPoint controlPoint[2];
CGPoint p = [self pointWithIndex:i inData:self.prices];
if(i==0)
{
[path moveToPoint:p];
}
CGPoint nextPoint, previousPoint, m;
nextPoint = [self pointWithIndex:i+1 inData:self.prices];
previousPoint = [self pointWithIndex:i-1 inData:self.prices];
if(i > 0) {
m.x = (nextPoint.x - previousPoint.x) / 2;
m.y = (nextPoint.y - previousPoint.y) / 2;
} else {
m.x = (nextPoint.x - p.x) / 2;
m.y = (nextPoint.y - p.y) / 2;
}
controlPoint[0].x = p.x + m.x * 0.2;
controlPoint[0].y = p.y + m.y * 0.2;
// Second control point
nextPoint = [self pointWithIndex:i+2 inData:self.prices];
previousPoint = [self pointWithIndex:i inData:self.prices];
p = [self pointWithIndex:i + 1 inData:self.prices];
m = zeroPoint;
if(i < self.prices.count - 2) {
m.x = (nextPoint.x - previousPoint.x) / 2;
m.y = (nextPoint.y - previousPoint.y) / 2;
} else {
m.x = (p.x - previousPoint.x) / 2;
m.y = (p.y - previousPoint.y) / 2;
}
controlPoint[1].x = p.x - m.x * 0.2;
controlPoint[1].y = p.y - m.y * 0.2;
[path addCurveToPoint:p controlPoint1:controlPoint[0] controlPoint2:controlPoint[1]];
}
CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.path = path.CGPath;
lineLayer.lineWidth = LINE_WIDTH;
lineLayer.strokeColor = _priceColor.CGColor;
lineLayer.fillColor = [UIColor clearColor].CGColor;
[self.layer addSublayer:lineLayer];
}
but in some situation, the line will "go back" like
Is there any better way to do that?
I've got you another solution which I used to success.
It requires SpriteKit, which has a fantastic tool called SKKeyFrameSequence which is capable of spline interpolation as shown in this tutorial provided by Apple.
So the idea is that you'll create the correct SKKeyFrameSequence object like this (assuming your data is in an array of (CGFloat, CGFloat) (x, y) tuples):
let xValues = data.map { $0.0 }
let yValues = data.map { $0.1 }
let sequence = SKKeyFrameSequence(keyFrameValues: yValues,
times: xValues.map { NSNumber($0) })
sequence.interpolationMode = .spline
let xMin = xValues.min()!
let xMax = xValues.max()!
Then, if you divide the interpolated spline into 200 pieces (change this value if you want, but for me this resulted in smooth waves to human eyes), you can draw a path consisting of small lines.
var splinedValues = [(CGFloat, CGFloat)]()
stride(from: xMin, to: xMax, by: (xMax - xMin) / 200).forEach {
splinedValues.append((CGFloat($0),
sequence.sample(atTime: CGFloat($0)) as! CGFloat))
}
Then finally the path (I will use SwiftUI, but you can use UIKit the same way too.):
Path { path in
path.move(to: CGPoint(x: splinedValues[0].0, y: splinedValues[0].1))
for splineValue in splinedValues.dropFirst() {
path.addLine(to: CGPoint(x: splineValue.0, y: splineValue.1))
}
}
For the values
[(1, 4), (2, 6), (3, 7), (4, 5), (5, 3), (6, -1), (7, -2), (8, -2.5), (9, -2), (10, 0), (11, 4)]
I've gotten a graph like this with the method described above: (I added the points too to evaluate the result better)
I find the answer at Draw Graph curves with UIBezierPath
And try implement with the code
+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points
{
UIBezierPath *path = [UIBezierPath bezierPath];
NSValue *value = points[0];
CGPoint p1 = [value CGPointValue];
[path moveToPoint:p1];
if (points.count == 2) {
value = points[1];
CGPoint p2 = [value CGPointValue];
[path addLineToPoint:p2];
return path;
}
for (NSUInteger i = 1; i < points.count; i++) {
value = points[i];
CGPoint p2 = [value CGPointValue];
CGPoint midPoint = midPointForPoints(p1, p2);
[path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
[path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
p1 = p2;
}
return path;
}
static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}
static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
CGPoint controlPoint = midPointForPoints(p1, p2);
CGFloat diffY = abs(p2.y - controlPoint.y);
if (p1.y < p2.y)
controlPoint.y += diffY;
else if (p1.y > p2.y)
controlPoint.y -= diffY;
return controlPoint;
}
Thanks user1244109 ^_^.
I am drawing a rect using UIBezierPath. The following code creates the rect in my view controller:
#interface ViewController ()
{
TargetAreaView *targetView; //TargetAreaView is a subclass of UIView
UIView *detectionView;
}
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CGRect frame = self.view.frame;
frame.size.height = frame.size.height - 200;
detectionView = [[UIView alloc]initWithFrame:frame];
targetView = [[TargetAreaView alloc]initWithFrame:detectionView.frame];
targetView.backgroundColor = [UIColor clearColor];
[self.view addSubview:targetView];
// Do any additional setup after loading the view, typically from a nib.
}
I am using the bezier path to draw the rect in TargetAreaView class.
I have used the following code in the UIView subclass:(TargetAreaView)
#interface TargetAreaView : UIView
{
NSMutableArray *points;
CGRect superViewFrame;
CGPoint selectedPoint;
}
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
superViewFrame = frame;
points = [[NSMutableArray alloc]init];
CGPoint startPoint = CGPointMake(superViewFrame.size.width / 2 - 200/2, superViewFrame.size.height / 2 - 200/2);
CGPoint heightPoint1 = CGPointMake(startPoint.x, startPoint.y + 200);
CGPoint widthPoint1 = CGPointMake(startPoint.x + 200, startPoint.y + 200);
CGPoint heightPoint2 = CGPointMake(startPoint.x + 200, startPoint.y);
CGPoint widthPoint2 = CGPointMake(startPoint.x, startPoint.y);
[points addObject:[NSValue valueWithCGPoint:startPoint]];
[points addObject:[NSValue valueWithCGPoint:heightPoint1]];
[points addObject:[NSValue valueWithCGPoint:widthPoint1]];
[points addObject:[NSValue valueWithCGPoint:heightPoint2]];
[points addObject:[NSValue valueWithCGPoint:widthPoint2]];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
if (points && points.count > 0)
{
[self drawTargetArea:points];
[self setNeedsDisplay];
}
}
-(void)drawTargetArea:(NSMutableArray *)savedPoints
{
UIBezierPath *aPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
for (int idx = 0; idx < savedPoints.count; idx++)
{
if (idx == 0)
{
NSValue *startPoint = [savedPoints objectAtIndex:idx];
[aPath moveToPoint:startPoint.CGPointValue]; // Starting Point
}
else
{
NSValue *drawPoints = [savedPoints objectAtIndex:idx];
[aPath addLineToPoint:drawPoints.CGPointValue]; // Add lines from previous to current point
}
}
[aPath closePath]; // close the final path
aPath.lineWidth = 10.0f;
[[UIColor redColor] setStroke];
[aPath stroke];
}
The above UIView subclass code generates the rect as shown below:
I have written the logic to move the corners of rect to change its shape as shown below:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint startTouchPoint = [touch locationInView:self]; //get the user touch position and check it is nearer in 10px range to any of the position in rect
for (NSValue *aPoint in points)
{
CGPoint currentPoint = aPoint.CGPointValue;
CGFloat distance = [self touchPositionDifference:startTouchPoint WithCornerPosition:currentPoint]; // Calculate the distance between user touch position and rect points
if (distance < 10)
{
selectedPoint = currentPoint; // get the nearest point in a global variable
break;
}
}
}
-(CGFloat)touchPositionDifference:(CGPoint)touchPosition WithCornerPosition:(CGPoint)cornnerPosition
{
CGFloat dx= touchPosition.x - cornnerPosition.x;
CGFloat dy= touchPosition.y - cornnerPosition.y;
CGFloat distance= sqrt(dx*dx + dy*dy);
return distance; // calculate and return distance between two points
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchEndPoint = [touch locationInView:self];
for (int idx = 0; idx < points.count; idx++)
{
NSValue *aPoint = [points objectAtIndex:idx];
CGPoint currentPoint = aPoint.CGPointValue;
if (CGPointEqualToPoint(currentPoint, selectedPoint))
{
[points replaceObjectAtIndex:idx withObject:[NSValue valueWithCGPoint:touchEndPoint]]; // get the global point value and touch ended value and replace the new value with old value
}
}
[self setNeedsDisplay];// and draw the shape with new points
}
I can able to get the output like following
I have return code to get the bezier path as follows:
-(UIBezierPath *)getPath
{
if (points.count <= 0) return nil;
NSArray *oldPoints = points;
UIBezierPath *aPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
CGPoint p1 = [[oldPoints objectAtIndex:0] CGPointValue];
[aPath moveToPoint:CGPointMake(p1.x, p1.y)];
for (uint i=1; i<oldPoints.count; i++)
{
CGPoint p = [[oldPoints objectAtIndex:i] CGPointValue];
[aPath addLineToPoint:CGPointMake(p.x, p.y)];
}
[aPath closePath];
return aPath; //This returns me a bezier path of the already drawn shape
}
I am getting the entire view(within that view i am having the irregular shape View is represented in green color and the shape is represented in red border color) as shown below
I want to get that particular shape alone(the irregular shape) As shown below
How can I achieve this? Am I following the right path?
You can use the following code to draw the above mentioned shape.
- (void)drawCanvas1WithFrame: (CGRect)frame
{
//// Color Declarations
UIColor* color = [UIColor colorWithRed: 1 green: 0 blue: 0 alpha: 1];
//// Bezier Drawing
UIBezierPath* bezierPath = UIBezierPath.bezierPath;
[bezierPath moveToPoint: CGPointMake(CGRectGetMinX(frame) + 77.5, CGRectGetMinY(frame) + 39.5)];
[bezierPath addCurveToPoint: CGPointMake(CGRectGetMinX(frame) + 130.5, CGRectGetMinY(frame) + 14.5) controlPoint1: CGPointMake(CGRectGetMinX(frame) + 130.5, CGRectGetMinY(frame) + 14.5) controlPoint2: CGPointMake(CGRectGetMinX(frame) + 130.5, CGRectGetMinY(frame) + 14.5)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetMinX(frame) + 122.5, CGRectGetMinY(frame) + 75.5)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetMinX(frame) + 61.5, CGRectGetMinY(frame) + 110.5)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetMinX(frame) + 77.5, CGRectGetMinY(frame) + 39.5)];
[UIColor.greenColor setFill];
[bezierPath fill];
[color setStroke];
bezierPath.lineWidth = 1;
[bezierPath stroke];
}
In my app I have a MKOverlayPathView where I draw a QuadCurve between two points which works just fine.
Here is the code I use:
MKMapPoint mkDepPoint = MKMapPointForCoordinate([(RoutePath *)self.overlay startCoordinate]);
MKMapPoint mkArrPoint = MKMapPointForCoordinate([(RoutePath *)self.overlay endCoordinate]);
CGPoint dePoint = [self pointForMapPoint:mkDepPoint];
CGPoint arrPoint = [self pointForMapPoint:mkArrPoint];
CGPoint s = depPoint;
CGPoint e = arrPoint;
... some awesome code ...
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:s];
[path addQuadCurveToPoint:e controlPoint:cp1];
[path closePath];
Now I want to get a CGPoint for a specific percentage on that curve. I have been trying out a lot but I'm just not getting anwyhere useful. I have been trying this out but it's not really working:
CGPoint point =
CGPointMake(
[self quadBezierForPercent:percent StartPoint:s.x ControlPoint:cp1.x EndPoint:e.x],
[self quadBezierForPercent:percent StartPoint:s.y ControlPoint:cp1.y EndPoint:e.y]
);
Using this:
-(float)quadBezierForPercent:(float)t StartPoint:(float)start ControlPoint:(float)c1 EndPoint:(float)end {
CGFloat t_ = (1.0 - t);
CGFloat tt_ = t_ * t_;
CGFloat tt = t * t;
return start * tt_
+ 2.0 * c1 * t_ * t
+ end * tt;
}
Some points in the right direction would be really helpful.
Thanks a lot in advance!
I am trying to smoothen my hand drawn Bezier curves, but not able to achieve, it, I got the code of smoothing the curve from the book written by erica sudan , which I tried to implement , but dont know why I am not able to produce a smooth curve, below is my code , I request all of you to please go through it ..
- (void)drawRect:(CGRect)rect
{
myPath.pathColor = [UIColor redColor];
for (BezeirPath *_path in m_pathArray)
{
[_path.pathColor setStroke];
[_path.bezeirPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
m_previousPoint1 = [mytouch previousLocationInView:self];
m_previousPoint2 = [mytouch previousLocationInView:self];
m_currentPoint = [mytouch locationInView:self];
CGPoint controlPoint = CGPointMake(m_previousPoint1.x+(m_previousPoint1.x - m_previousPoint2.x), m_previousPoint1.y +(m_previousPoint1.y - m_previousPoint2.y));
[myPath.bezeirPath addQuadCurveToPoint:m_currentPoint controlPoint:controlPoint];
self.previousPoint3 = controlPoint;
[myPath.bezeirPath smoothedPath:myPath.bezeirPath :40];//Called the smoothing function.
[self setNeedsDisplay];
}
//Smoothing function
-(UIBezierPath*)smoothedPath:(UIBezierPath*)bpath: (NSInteger) granularity
{
NSArray *points = pointsFromBezierPath(bpath);
if (points.count < 4) return bpath;
// This only copies lineWidth. You may want to copy more
UIBezierPath *smoothedPath = [UIBezierPath bezierPath];
smoothedPath.lineWidth = bpath.lineWidth;
// Draw out the first 3 points (0..2)
[smoothedPath moveToPoint:POINT(0)];
for (int index = 1; index < 3; index++)
[smoothedPath addLineToPoint:POINT(index)];
// Points are interpolated between p1 and p2,
// starting with 2..3, and moving from there
for (int index = 4; index < points.count; index++)
{
CGPoint p0 = POINT(index - 3);
CGPoint p1 = POINT(index - 2);
CGPoint p2 = POINT(index - 1);
CGPoint p3 = POINT(index);
// 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];
}
[smoothedPath addLineToPoint:p2];
}
// finish by adding the last point
[smoothedPath addLineToPoint:POINT(points.count - 1)];
return smoothedPath;
}
I suggest watching "Building Advanced Gesture Recognizers" from WWDC 2012. You can skip the beginning - the part you are most interested in is at 38:23.