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!
Related
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!
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've never worked with animations on iOS specifically and was hoping to be pointed in the right direction. I have so far animated a bunch of pill shaped objects in a fashion similar to what I have below, however I have yet to implement the correct masking based on the y positioning of the shape.
The pill shapes are just UIViews. I'm looking for a way to get their backgrounds to change based on their positioning; the background itself does not move, just the pills.
EDIT:
Based on Rob's answer below, I am now tying the animation to the device's movement like so:
- (void)setAnimationToDeviceMotion {
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 0.1f;
NSOperationQueue *motionQueue = [[NSOperationQueue alloc] init];
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical toQueue:motionQueue withHandler:^(CMDeviceMotion *motion, NSError *error) {
if (!error) {
double pitch = motion.attitude.pitch;
NSLog(#"%f", pitch);
[self addMaskToView:self.imageView withAdjustment:pitch];
}
}];
}
Assuming this is eight pill shapes that will reveal a single image underneath, you
create a UIBezierPath for the eight pill shapes
create a CAShapeLayer layer that uses that UIBezierPath (or more accurately, its CGPath)
apply this CAShapeLayer as the mask for the layer for your UIImageView
If you want to animate this over time, just replace the mask (e.g. in response to a accelerometer event, a CADisplayLink or NSTimer, or a UIPanGesture), or just update the path of the CAShapeLayer you have applied as the mask to the UIImageView's layer.
Thus, you might have something like:
- (void)addMaskToView:(UIView *)view withAdjustment:(CGFloat)adjustment
{
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = [[self pillsPathWithAdjustment:adjustment forView:view] CGPath];
self.imageView.layer.mask = mask;
}
- (UIBezierPath *)pillsPathWithAdjustment:(CGFloat)adjustment forView:(UIView *)view
{
UIBezierPath *path = [UIBezierPath bezierPath];
for (NSInteger i = 0; i < kNumberOfPills; i++)
[self addPillPathNumber:i forView:view toPath:path adjustment:adjustment];
return path;
}
- (void)addPillPathNumber:(NSInteger)index forView:(UIView *)view toPath:(UIBezierPath *)path adjustment:(CGFloat)adjustment
{
CGFloat const pillSpacing = 5.0f;
CGFloat const pillWidth = view.bounds.size.width / kNumberOfPills - pillSpacing;
CGFloat const pillHeight = view.bounds.size.height * 0.4;
CGFloat const cornerRounding = 10.0f;
CGPoint point = CGPointMake(index * (pillWidth + pillSpacing) + pillSpacing / 2.0, sinf((float) index * M_PI / kNumberOfPills + adjustment * M_PI * 2.0) * 100.0);
// top
point.x += cornerRounding;
[path moveToPoint:point];
point.x += pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// top right corner
[path addArcWithCenter:CGPointMake(point.x, point.y + cornerRounding) radius:cornerRounding startAngle:3 * M_PI_2 endAngle:2.0 * M_PI clockwise:YES];
point.x += cornerRounding;
point.y += cornerRounding;
// right
point.y += pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower right corner
[path addArcWithCenter:CGPointMake(point.x - cornerRounding, point.y) radius:cornerRounding startAngle:0 endAngle:M_PI_2 clockwise:YES];
// bottom
point.y += cornerRounding;
point.x -= cornerRounding;
point.x -= pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower left corner
[path addArcWithCenter:CGPointMake(point.x, point.y - cornerRounding) radius:cornerRounding startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
// left
point.y -= cornerRounding;
point.x -= cornerRounding;
point.y -= pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// upper left corner
[path addArcWithCenter:CGPointMake(point.x + cornerRounding, point.y) radius:cornerRounding startAngle:M_PI endAngle:3.0 * M_PI_2 clockwise:YES];
[path closePath];
}
By way of illustration, this is how I'd animate this using a CADisplayLink:
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CGFloat seconds;
CGFloat fractionsOfSecond = modff(CACurrentMediaTime() - self.startTime, &seconds);
[self addMaskToView:self.imageView withAdjustment:fractionsOfSecond];
}
To do masking, you'll need to work with the view's layers. Instead of moving the views themselves up and down, create views that occupy the whole height of the parent, and then create a CALayer in the pill shape. Set the pill layer as the view's layer's mask, and move it up and down.
Something like this:
UIImageView *myImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"redAndBlueBackground"]];
CALayer *pillLayer = [[CALayer alloc] initWithFrame:CGRectMake(0, 0, myImageView.bounds.size.width, myImageView.bounds.size.height / 2];
pillLayer.cornerRadius = 8;
myImageView.layer.maskLayer = pillLayer;
// move the pill layer to height y:
pillLayer.center = CGPointMake(myImageView.bounds.size.width / 2,
(myImageView.bounds.size.height / 2) + y);
I want to draw shape of small land on view by taking latitude and longitude at the corner of land.
I have wrote following code. For now I took hard core values.
- (void)drawRect:(CGRect)rect {
CGSize screenSize = [UIScreen mainScreen].applicationFrame.size;
SCALE = MIN(screenSize.width, screenSize.height) / (2.0 * EARTH_RADIUS);
OFFSET = MIN(screenSize.width, screenSize.height) / 2.0;
CGPoint latLong1 = {18.626103, 73.805023};
CGPoint latLong2 = {18.626444, 73.804884};
CGPoint latLong3 = {18.626226, 73.804969};
CGPoint latLong4 = {18.626103, 73.805023};
NSMutableArray *points=[NSMutableArray arrayWithObjects:[NSValue valueWithCGPoint:[self convertLatLongCoord:latLong1]],[NSValue valueWithCGPoint:[self convertLatLongCoord:latLong2]], [NSValue valueWithCGPoint:[self convertLatLongCoord:latLong3]],[NSValue valueWithCGPoint:[self convertLatLongCoord:latLong4]],nil];
CGContextRef ctx = UIGraphicsGetCurrentContext();
for(int i=0;i<points.count;i++)
{
// CGPoint newCoord = [self convertLatLongCoord:latLong];
NSValue *val = [points objectAtIndex:i];
CGPoint newCoord = [val CGPointValue];
if(i == 0)
{
// move to the first point
CGContextMoveToPoint(ctx, newCoord.x, newCoord.y);
}
else
{
CGContextAddLineToPoint(ctx, newCoord.x, newCoord.y);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1);
CGContextSetStrokeColorWithColor(ctx, [[UIColor redColor] CGColor]);
}
}
CGContextStrokePath(ctx);
}
Below is method which converts lat long into x,y co-ordinates.
- (CGPoint)convertLatLongCoord:(CGPoint)latLong
{
CGFloat x = EARTH_RADIUS * cos(latLong.x) * cos(latLong.y) * SCALE + OFFSET;
CGFloat y = EARTH_RADIUS * cos(latLong.x) * sin(latLong.y) * SCALE + OFFSET;
return CGPointMake(x, y);
}
My problem is when I took small land(e.g house land) area lat long its shape is not visible on view after draw. How I can show maximise shape of land on view.
Thanks 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.