Finding the center of a CGPath - iphone

I have an arbitrary CGPath and I'd like to find it's geographic center. I can get the path bounding box with CGPathGetPathBoundingBox and then find the center of that box. But is there a better way to find the center of a path?
Update for those who like to see code: here is code for using the average-of-points method suggested by Adam in the answers (don't miss the even better technique in the answers below)...
BOOL moved = NO; // the first coord should be a move, the rest add lines
CGPoint total = CGPointZero;
for (NSDictionary *coord in [polygon objectForKey:#"coordinates"]) {
CGPoint point = CGPointMake([(NSNumber *)[coord objectForKey:#"x"] floatValue],
[(NSNumber *)[coord objectForKey:#"y"] floatValue]);
if (moved) {
CGContextAddLineToPoint(context, point.x, point.y);
// calculate totals of x and y to help find the center later
// skip the first "move" point since it is repeated at the end in this data
total.x = total.x + point.x;
total.y = total.y + point.y;
} else {
CGContextMoveToPoint(context, point.x, point.y);
moved = YES; // we only move once, then we add lines
}
}
// the center is the average of the total points
CGPoint center = CGPointMake(total.x / ([[polygon objectForKey:#"coordinates"] count]-1), total.y / ([[polygon objectForKey:#"coordinates"] count]-1));
If you have a better idea, please share!

The technique works, but the code you put in the question doesn't. AFAICS, that only works for the few situations where you are doing straight-line polygons ONLY, and you have a list of points, and you haven't made the CGPath object yet.
I needed to do it for arbitrary CGPath objects. Using Adam's (other Adam) suggestion, and Apple's CGPathApply, I came up with this, which works very well:
{
float dataArray[3] = { 0, 0, 0 };
CGPathApply( (CGPathRef) YOUR_PATH, dataArray, pathApplierSumCoordinatesOfAllPoints);
float averageX = dataArray[0] / dataArray[2];
float averageY = dataArray[1] / dataArray[2];
CGPoint centerOfPath = CGPointMake(averageX, averageY);
}
static void pathApplierSumCoordinatesOfAllPoints(void* info, const CGPathElement* element)
{
float* dataArray = (float*) info;
float xTotal = dataArray[0];
float yTotal = dataArray[1];
float numPoints = dataArray[2];
switch (element->type)
{
case kCGPathElementMoveToPoint:
{
/** for a move to, add the single target point only */
CGPoint p = element->points[0];
xTotal += p.x;
yTotal += p.y;
numPoints += 1.0;
}
break;
case kCGPathElementAddLineToPoint:
{
/** for a line to, add the single target point only */
CGPoint p = element->points[0];
xTotal += p.x;
yTotal += p.y;
numPoints += 1.0;
}
break;
case kCGPathElementAddQuadCurveToPoint:
for( int i=0; i<2; i++ ) // note: quad has TWO not THREE
{
/** for a curve, we add all ppints, including the control poitns */
CGPoint p = element->points[i];
xTotal += p.x;
yTotal += p.y;
numPoints += 1.0;
}
break;
case kCGPathElementAddCurveToPoint:
for( int i=0; i<3; i++ ) // note: cubic has THREE not TWO
{
/** for a curve, we add all ppints, including the control poitns */
CGPoint p = element->points[i];
xTotal += p.x;
yTotal += p.y;
numPoints += 1.0;
}
break;
case kCGPathElementCloseSubpath:
/** for a close path, do nothing */
break;
}
//NSLog(#"new x=%2.2f, new y=%2.2f, new num=%2.2f", xTotal, yTotal, numPoints);
dataArray[0] = xTotal;
dataArray[1] = yTotal;
dataArray[2] = numPoints;
}

For me, the simple average of all points in the path did not suffice for some of the polygons I was dealing with.
I implemented it using the area (see wikipedia, Centroid of polygon and Paul Bourke's page). It might not be the most efficient implementation but it works for me.
Note that it only works for closed, non-intersecting polygons. The vertices are assumed to be numbered in order of their occurrence along the polygon's perimeter, and the last point is assumed to be the same as the first point.
CGPoint GetCenterPointOfCGPath (CGPathRef aPath)
{
// Convert path to an array
NSMutableArray* a = [NSMutableArray new];
CGPathApply(aPath, (__bridge void *)(a), convertToListOfPoints);
return centroid(a);
}
static void convertToListOfPoints(void* info, const CGPathElement* element)
{
NSMutableArray* a = (__bridge NSMutableArray*) info;
switch (element->type)
{
case kCGPathElementMoveToPoint:
{
[a addObject:[NSValue valueWithCGPoint:element->points[0]]];
}
break;
case kCGPathElementAddLineToPoint:
{
[a addObject:[NSValue valueWithCGPoint:element->points[0]]];
}
break;
case kCGPathElementAddQuadCurveToPoint:
{
for (int i=0; i<2; i++)
[a addObject:[NSValue valueWithCGPoint:element->points[i]]];
}
break;
case kCGPathElementAddCurveToPoint:
{
for (int i=0; i<3; i++)
[a addObject:[NSValue valueWithCGPoint:element->points[i]]];
}
break;
case kCGPathElementCloseSubpath:
break;
}
}
double polygonArea(NSMutableArray* points) {
int i,j;
double area = 0;
int N = [points count];
for (i=0;i<N;i++) {
j = (i + 1) % N;
CGPoint pi = [(NSValue*)[points objectAtIndex:i] CGPointValue];
CGPoint pj = [(NSValue*)[points objectAtIndex:j] CGPointValue];
area += pi.x * pj.y;
area -= pi.y * pj.x;
}
area /= 2;
return area;
}
CGPoint centroid(NSMutableArray* points) {
double cx = 0, cy = 0;
double area = polygonArea(points);
int i, j, n = [points count];
double factor = 0;
for (i = 0; i < n; i++) {
j = (i + 1) % n;
CGPoint pi = [(NSValue*)[points objectAtIndex:i] CGPointValue];
CGPoint pj = [(NSValue*)[points objectAtIndex:j] CGPointValue];
factor = (pi.x * pj.y - pj.x * pi.y);
cx += (pi.x + pj.x) * factor;
cy += (pi.y + pj.y) * factor;
}
cx *= 1 / (6.0f * area);
cy *= 1 / (6.0f * area);
return CGPointMake(cx, cy);
}

Does the simple average of all x and all y for the points in the path give the point you want? Calculate one value for x and one for y. I made a quick sketch and this method gave a believable answer.
See wikipedia, finding the centroid of a finite set of points.
If not you may need to first find the area - see Paul Bourke's page.

Updated Adam's answer to swift4 version:
extension CGPath {
func findCenter() -> CGPoint {
class Context {
var sumX: CGFloat = 0
var sumY: CGFloat = 0
var points = 0
}
var context = Context()
apply(info: &context) { (context, element) in
guard let context = context?.assumingMemoryBound(to: Context.self).pointee else {
return
}
switch element.pointee.type {
case .moveToPoint, .addLineToPoint:
let point = element.pointee.points[0]
context.sumX += point.x
context.sumY += point.y
context.points += 1
case .addQuadCurveToPoint:
let controlPoint = element.pointee.points[0]
let point = element.pointee.points[1]
context.sumX += point.x + controlPoint.x
context.sumY += point.y + controlPoint.y
context.points += 2
case .addCurveToPoint:
let controlPoint1 = element.pointee.points[0]
let controlPoint2 = element.pointee.points[1]
let point = element.pointee.points[2]
context.sumX += point.x + controlPoint1.x + controlPoint2.x
context.sumY += point.y + controlPoint1.y + controlPoint2.y
context.points += 3
case .closeSubpath:
break
}
}
return CGPoint(x: context.sumX / CGFloat(context.points),
y: context.sumY / CGFloat(context.points))
}
}
But be careful, CGPath may have extra move commands that will break this logic because of point count

Here is the centroid, come get it:
-(CLLocationCoordinate2D)getCentroidFor:(GMSMutablePath *)rect
{
CLLocationCoordinate2D coord = [rect coordinateAtIndex:0];
double minX = coord.longitude;
double maxX = coord.longitude;
double minY = coord.latitude;
double maxY = coord.latitude;
for (int i = 1; i < rect.count; i++)
{
CLLocationCoordinate2D coord = [rect coordinateAtIndex:i];
if (minX > coord.longitude)
minX = coord.longitude;
if (maxX < coord.longitude)
maxX = coord.longitude;
if (minY > coord.latitude)
minY = coord.latitude;
if (maxY < coord.latitude)
maxY = coord.latitude;
}
CLLocationDegrees centerX = minX + ((maxX - minX) / 2);
CLLocationDegrees centerY = minY + ((maxY - minY) / 2);
return CLLocationCoordinate2DMake(centerY, centerX);
}

Find the bounding box of the CGPath and take the center of it.
CGRect boundingBox = CGPathGetBoundingBox(my_path);
my_center_point = ccp(boundingBox.origin.x+boundingBox.size.width/2, boundingBox.origin.y+boundingBox.size.height/2);

Related

Collision detection (tutorial: How to make a platform game like Super Mario Brothers)

guys!
I am following this tutorial How to make a platform game like Super Mario Brothers to create a simple platform game. In the tutorial, the character sprite's size is a about a size of a single tile, and collision detection is calculated for 8 surrounding tiles. I modified the sprite's size to be equal about 4 tiles (2x2) and calculate collisions with 12 surrounding tiles. It works fine for the bottom and right sprite's edges, but the left and upper edges a little bit overlap with an obstacle before collision occurs, that is obviously wrong.
I feel that I have some mistakes in there, but as I am quite new in Spritekit I cannot spot them. I would really appreciate if someone could help me with it. Thanks in advance.
Here is the update function for the character:
- (void)update:(NSTimeInterval)delta
{
CGPoint gravity = CGPointMake(0.0, -450.0);
CGPoint gravityStep = CGPointMultiplyScalar(gravity, delta);
CGPoint forwardMove = CGPointMake(800.0, 0.0);
CGPoint forwardMoveStep = CGPointMultiplyScalar(forwardMove, delta);
CGPoint backwardMove = CGPointMake(-800.0, 0.0);
CGPoint backwardMoveStep = CGPointMultiplyScalar(backwardMove, delta);
if (self.forwardMarch)
{
self.velocity = CGPointAdd(self.velocity, forwardMoveStep);
}
if (self.backwardMarch)
{
self.velocity = CGPointAdd(self.velocity, backwardMoveStep);
}
self.velocity = CGPointAdd(self.velocity, gravityStep);
self.velocity = CGPointMake(self.velocity.x * 0.9, self.velocity.y);
// setup minimum and maximum limits for the motion speed
CGPoint minMovement = CGPointMake(0.0, -450);
CGPoint maxMovement = CGPointMake(0.0, 0.0);
// forward motion
if (self.velocity.x >= 0)
{
minMovement = CGPointMake(0.0, -450);
maxMovement = CGPointMake(120.0, 250.0);
}
// backward motion
if (self.velocity.x < 0)
{
minMovement = CGPointMake(-120.0, -450);
maxMovement = CGPointMake(0.0, 250.0);
}
self.velocity = CGPointMake(Clamp(self.velocity.x, minMovement.x, maxMovement.x), Clamp(self.velocity.y, minMovement.y, maxMovement.y));
CGPoint velocityStep = CGPointMultiplyScalar(self.velocity, delta);
self.newPosition = CGPointAdd(self.position, velocityStep);
}
This function find the bounding box of the sprite
- (CGRect)collisionBoundingBox
{
CGPoint diff = CGPointSubtract(self.newPosition, self.position);
return CGRectOffset(self.frame, diff.x, diff.y);
}
And the function where I handle collisions
- (void)handleObstacleCollisionsForPlayer:(Player *)player forLayer:(TMXLayer *)layer
{
NSInteger indices[12] = {13, 14, 1, 2, 4, 8, 7, 11, 0, 3, 12, 15};
player.onGround = NO;
for (NSUInteger i = 0; i < 12; i++)
{
NSInteger tileIndex = indices[i];
CGRect playerRect = [player collisionBoundingBox];
CGPoint playerCoord = [layer coordForPoint:player.newPosition];
NSInteger tileColumn = tileIndex % 4;
NSInteger tileRow = tileIndex / 4;
CGPoint tileCoord = CGPointMake(playerCoord.x + (tileColumn - 1), playerCoord.y + (tileRow - 1));
NSInteger gid = [self tileGIDAtTileCoord:tileCoord forLayer:layer];
if (gid)
{
CGRect tileRect = [self tileRectFromTileCoords:tileCoord];
if (CGRectIntersectsRect(playerRect, tileRect))
{
CGRect intersection = CGRectIntersection(playerRect, tileRect);
if (tileIndex == 13 || tileIndex == 14)
{
//tile is below
player.newPosition = CGPointMake(player.newPosition.x, player.newPosition.y + intersection.size.height);
player.velocity = CGPointMake(player.velocity.x, 0.0);
player.onGround = YES;
}
else if (tileIndex == 1 || tileIndex == 2)
{
//tile is directly above
player.newPosition = CGPointMake(player.newPosition.x, player.newPosition.y - intersection.size.height);
}
else if (tileIndex == 4 || tileIndex == 8)
{
//tile is left
player.newPosition = CGPointMake(player.newPosition.x + intersection.size.width, player.newPosition.y);
}
else if (tileIndex == 7 || tileIndex == 11)
{
//tile is right
player.newPosition = CGPointMake(player.newPosition.x - intersection.size.width, player.newPosition.y);
}
}
}
}
player.position = player.newPosition;
}
inside your GameViewController.m enable debug physics mode
that will draw a thick outline around every box and give u exact information about collision tiles may i think either your hero or tile sizes are differ than the tutorial you following
skView.showsPhysics= YES;

Objective-C random position on screen not working properly ( SpriteKit )

Well I made a randomNumber Class, (for practice and) for calculating a random position on my screen for an object and it works properly, except for it sometimes puts the sprite out of the screen.The x and y coordinates are smaller the screen height and width. but it doesn't show it on the screen.
The whole program is just basically randomly placing instances of an object inside the screen.
randomNumber.h
#import <Foundation/Foundation.h>
#import <SpriteKit/SpriteKit.h>
#interface randomNumber : NSObject
-(int)randNumX:(int) max :(SKSpriteNode *) sprite;
-(int)randNumY:(int) max :(SKSpriteNode *) sprite;
#end
randomNumber.m
#import "randomNumber.h"
#implementation randomNumber
-(int)randNumX:(int) max :(SKSpriteNode *) sprite {
int _spriteW = sprite.frame.size.width;
int _random = (arc4random() % (max - _spriteW));
NSLog(#"The x value is %d", _random);
return _random;
}
-(int)randNumY:(int) max :(SKSpriteNode *) sprite {
int _spriteH = sprite.frame.size.height;
int _random = (arc4random() % (max - _spriteH));
NSLog(#"The y value is %d", _random);
return _random;
}
#end
MyScene.m ( only the initilazeMole method )
-(void) initilazeMole {
int x = [self.rndNum randNumX:(self.scene.size.width):(self.mole)];
int y = [self.rndNum randNumY:(self.scene.size.height):(self.mole)]
self.mole = [SKSpriteNode spriteNodeWithImageNamed:#"spaceship"];
self.mole.anchorPoint = CGPointMake(0,0);
self.mole.position = CGPointMake(x,y);
SKAction *pulseRed = [SKAction sequence:#[
[SKAction colorizeWithColor:[SKColor redColor] colorBlendFactor:1.0 duration:0.5],
[SKAction waitForDuration:0.1],
[SKAction colorizeWithColorBlendFactor:0.0 duration:1.0]]];
[self.mole runAction: pulseRed];
NSLog(#"mole x position: %f", self.mole.position.x);
NSLog(#"mole y position: %f", self.mole.position.y);
[self addChild:self.mole];
}
I don't really understand why does it place it off the screen, hence I generate a random number that can maximally be ( the screen width - sprite width ) and ( the screen height - sprite height )
My project settings are set up for an iphone 3.5 inch in landscape mode.
Any idea where did my code go wrong ?
Try this:
- (CGPoint) randomPointWithinContainerSize:(CGSize)containerSize forViewSize:(CGSize)size {
NSLog(#"move");
CGFloat xRange = containerSize.width - size.width;
CGFloat yRange = containerSize.height - size.height;
CGFloat minX = (containerSize.width - xRange) / 2;
CGFloat minY = (containerSize.height - yRange) / 2;
int randomX = (arc4random() % (int)floorf(xRange)) + minX;
int randomY = (arc4random() % (int)floorf(yRange)) + minY;
return CGPointMake(randomX, randomY);
}
Then replace:
int x = [self.rndNum randNumX:(self.scene.size.width):(self.mole)];
int y = [self.rndNum randNumY:(self.scene.size.height):(self.mole)]
self.mole = [SKSpriteNode spriteNodeWithImageNamed:#"spaceship"];
self.mole.anchorPoint = CGPointMake(0,0);
self.mole.position = CGPointMake(x,y);
With:
self.mole.position = [self randomPointWithinContainerSize:self.scene.size forViewSize:self.mole.bounds.size];

Having a path that follows another one

I have an array of coordinates (geographic coordinates, but that shouldn't matter) and I need to have a path that "follows" the path that we already have.
We need something like on the following image. You can see that the path is not exactly the same (not a simple offset) and we don't want it to scale either.
Is there some library that we could use to do that or some pointers on how to implement this?
After spending way too much time trying to find a working solution I ended up coding my own:
CGContextBeginPath(context);
CGMutablePathRef path = CGPathCreateMutable();
MKMapPoint *mapPoints = itineraryPath.points;
CGPoint previousEdgeNormal = CGPointZero;
CGPoint previousDrawnPoint = CGPointZero;
float offsetDistance = self.pathWidth*2.5;
for(int i = 0; i < itineraryPath.pointCount; i++) {
if(i < itineraryPath.pointCount-1) {
MKMapPoint mapPoint = mapPoints[i];
CGPoint point = [self pointForMapPoint:mapPoint];
MKMapPoint secondMapPoint = mapPoints[i+1];
CGPoint secondPoint = [self pointForMapPoint:secondMapPoint];
float xDelta = point.x-secondPoint.x;
float yDelta = point.y-secondPoint.y;
float factor = xDelta > 0 ? -1 : 1;
float segmentLength = sqrt(pow(xDelta, 2.0)+pow(yDelta, 2.0));
float yDeltaAngle = asin(sin(M_PI/2*factor)*yDelta/segmentLength);
float opposedAngle = M_PI/2-yDeltaAngle;
float remainingAngle = M_PI/2-opposedAngle;
float yOffset = sin(opposedAngle)*offsetDistance/sin(M_PI/2)*factor;
float xOffset = sin(remainingAngle)*offsetDistance/sin(M_PI/2)*factor;
CGPoint offsetFirstPoint = CGPointMake(point.x+xOffset, point.y+yOffset);
CGPoint offsetSecondPoint = CGPointMake(secondPoint.x+xOffset, secondPoint.y+yOffset);
if(i == mapPointIndex) {
CGPathMoveToPoint(path, NULL, offsetFirstPoint.x, offsetFirstPoint.y);
previousDrawnPoint = offsetFirstPoint;
}
else {
float xNormalDifference = previousEdgeNormal.x-offsetFirstPoint.x;
float yNormalDifference = previousEdgeNormal.y-offsetFirstPoint.y;
float xAverage = (xNormalDifference)/2;
float yAverage = (yNormalDifference)/2;
CGPoint normalAveragePoint = CGPointMake(offsetFirstPoint.x+xAverage, offsetFirstPoint.y+yAverage);
CGPathAddLineToPoint(path, NULL, normalAveragePoint.x, normalAveragePoint.y);
previousDrawnPoint = normalAveragePoint;
}
previousEdgeNormal = offsetSecondPoint;
}
else
CGPathAddLineToPoint(path, NULL, previousEdgeNormal.x, previousEdgeNormal.y);
}
Only caveat is that it doesn't handle acute angles very well yet.
But otherwise gives something pretty neat (right is original path, left is offset)
What you want is called a parallel curve: http://en.wikipedia.org/wiki/Parallel_curve
One way to generate that is to compute the normal of your original curve at each point, and then offset those points using that normal. This is quite simple if you only have straight line segments. For arcs and bezier curves, you also need to figure out how to modify the control points.

Drawing Smooth Curves - Methods Needed

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 points 1,2,3,4 - 2,3,4,5. I have heard of spline curves and all the other types. I am quite new to iPhone programming and do not understand how to program it in my quartz drawing app. A solid example would be greatly appreciated, I have spent weeks running in circles and I can never seem to find any iOS code for this task. Most of the posts just link to a java simulation or pages on wikipedia about curve fitting which does nothing for me. Also I do not want to switch to openGL ES. I hope someone can finally provide code to answer this circulating question.
This was my code for the UIBezierPath which left edges at intersection///
UPDATED TO AN ANSWER BELOW
#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]]
#define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue]
- (UIBezierPath*)smoothedPathWithGranularity:(NSInteger)granularity
{
NSMutableArray *points = [(NSMutableArray*)[self pointsOrdered] mutableCopy];
if (points.count < 4) return [self bezierPath];
// Add control points to make the math make sense
[points insertObject:[points objectAtIndex:0] atIndex:0];
[points addObject:[points lastObject]];
UIBezierPath *smoothedPath = [self bezierPath];
[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;
}
- (PVPoint *)pointAppendingCGPoint:(CGPoint)CGPoint
{
PVPoint *newPoint = [[PVPoint alloc] initInsertingIntoManagedObjectContext:[self managedObjectContext]];
[newPoint setCGPoint:CGPoint];
[newPoint setOrder:[NSNumber numberWithUnsignedInteger:[[self points] count]]];
[[self mutableSetValueForKey:#"points"] addObject:newPoint];
[(NSMutableArray *)[self pointsOrdered] addObject:newPoint];
[[self bezierPath] addLineToPoint:CGPoint];
return [newPoint autorelease];
if ([self bezierPath] && [pointsOrdered count] > 3)
{
PVPoint *control1 = [pointsOrdered objectAtIndex:[pointsOrdered count] - 2];
PVPoint *control2 = [pointsOrdered objectAtIndex:[pointsOrdered count] - 1];
[bezierPath moveToPoint:[[pointsOrdered objectAtIndex:[pointsOrdered count] - 3] CGPoint]];
[[self bezierPath] addCurveToPoint:CGPoint controlPoint1:[control1 CGPoint] controlPoint2:[control2 CGPoint]];
}
}
- (BOOL)isComplete { return [[self points] count] > 1; }
- (UIBezierPath *)bezierPath
{
if (!bezierPath)
{
bezierPath = [UIBezierPath bezierPath];
for (NSUInteger p = 0; p < [[self points] count]; p++)
{
if (!p) [bezierPath moveToPoint:[(PVPoint *)[[self pointsOrdered] objectAtIndex:p] CGPoint]];
else [bezierPath addLineToPoint:[(PVPoint *)[[self pointsOrdered] objectAtIndex:p] CGPoint]];
}
[bezierPath retain];
}
return bezierPath;
}
- (CGPathRef)CGPath
{
return [[self bezierPath] CGPath];
}
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.
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:
The "touch down" point is the start of the path, and store location in prevPoint.
For every dragged location, calculate midPoint, the point between currentPoint and prevPoint.
If this is the first dragged location, add currentPoint as a line segment.
For all points in the future, add a quad curve that terminates at the 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.
Store currentPoint in prevPoint, and repeat #2 until dragging ends.
Add the final point as another straight segment, to finish up the path.
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:
#Rakesh is absolutely right - you dont need to use Catmull-Rom algorithm if you just want a curved line. And the link he suggested does exacly that. So here's an addition to his answer.
The code bellow does NOT use Catmull-Rom algorithm & granularity, but draws a quad-curved line (control points are calculated for you). This is essentially what's done in the ios freehand drawing tutorial suggested by Rakesh, but in a standalone method that you can drop anywhere (or in a UIBezierPath category) and get a quad-curved spline out of the box.
You do need to have an array of CGPoint's wrapped in NSValue's
+ (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;
}
Here's the result:
The key to getting two bezier curves to join smoothly is that the relevant control points and the start/end points on the curves must be collinear. Think of the control point and the endpoint as forming a line that's tangent to the curve at the endpoint. If one curve starts at the same point where another ends, and if they both have the same tangent line at that point, the curve will be smooth. Here's a bit of code to illustrate:
- (void)drawRect:(CGRect)rect
{
#define commonY 117
CGPoint point1 = CGPointMake(20, 20);
CGPoint point2 = CGPointMake(100, commonY);
CGPoint point3 = CGPointMake(200, 50);
CGPoint controlPoint1 = CGPointMake(50, 60);
CGPoint controlPoint2 = CGPointMake(20, commonY);
CGPoint controlPoint3 = CGPointMake(200, commonY);
CGPoint controlPoint4 = CGPointMake(250, 75);
UIBezierPath *path1 = [UIBezierPath bezierPath];
UIBezierPath *path2 = [UIBezierPath bezierPath];
[path1 setLineWidth:3.0];
[path1 moveToPoint:point1];
[path1 addCurveToPoint:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2];
[[UIColor blueColor] set];
[path1 stroke];
[path2 setLineWidth:3.0];
[path2 moveToPoint:point2];
[path2 addCurveToPoint:point3 controlPoint1:controlPoint3 controlPoint2:controlPoint4];
[[UIColor orangeColor] set];
[path2 stroke];
}
Notice that path1 ends at point2, path2 starts at point2, and control points 2 and 3 share the same Y-value, commonY, with point2. You can change any of the values in the code as you like; as long as those three points all fall on the same line, the two paths will join smoothly. (In the code above, the line is y = commonY. The line doesn't have to be parallel to the X axis; it's just easier to see that the points are collinear that way.)
Here's the image that the code above draws:
After looking at your code, the reason that your curve is jagged is that you're thinking of control points as points on the curve. In a bezier curve, the control points are usually not on the curve. Since you're taking the control points from the curve, the control points and the point of intersection are not collinear, and the paths therefore don't join smoothly.
We need to observe some thing before applying any algorithm on captured points.
Generally UIKit does not give the points at equal distance.
We need to calculate the intermediate points in between two CGPoints[ Which has captured with Touch moved method]
Now to get smooth line, there are so many ways.
Some times we can achieve the by applying second degree polynomial or third degree polynomial or catmullRomSpline algorithms
- (float)findDistance:(CGPoint)point lineA:(CGPoint)lineA lineB:(CGPoint)lineB
{
CGPoint v1 = CGPointMake(lineB.x - lineA.x, lineB.y - lineA.y);
CGPoint v2 = CGPointMake(point.x - lineA.x, point.y - lineA.y);
float lenV1 = sqrt(v1.x * v1.x + v1.y * v1.y);
float lenV2 = sqrt(v2.x * v2.x + v2.y * v2.y);
float angle = acos((v1.x * v2.x + v1.y * v2.y) / (lenV1 * lenV2));
return sin(angle) * lenV2;
}
- (NSArray *)douglasPeucker:(NSArray *)points epsilon:(float)epsilon
{
int count = [points count];
if(count < 3) {
return points;
}
//Find the point with the maximum distance
float dmax = 0;
int index = 0;
for(int i = 1; i < count - 1; i++) {
CGPoint point = [[points objectAtIndex:i] CGPointValue];
CGPoint lineA = [[points objectAtIndex:0] CGPointValue];
CGPoint lineB = [[points objectAtIndex:count - 1] CGPointValue];
float d = [self findDistance:point lineA:lineA lineB:lineB];
if(d > dmax) {
index = i;
dmax = d;
}
}
//If max distance is greater than epsilon, recursively simplify
NSArray *resultList;
if(dmax > epsilon) {
NSArray *recResults1 = [self douglasPeucker:[points subarrayWithRange:NSMakeRange(0, index + 1)] epsilon:epsilon];
NSArray *recResults2 = [self douglasPeucker:[points subarrayWithRange:NSMakeRange(index, count - index)] epsilon:epsilon];
NSMutableArray *tmpList = [NSMutableArray arrayWithArray:recResults1];
[tmpList removeLastObject];
[tmpList addObjectsFromArray:recResults2];
resultList = tmpList;
} else {
resultList = [NSArray arrayWithObjects:[points objectAtIndex:0], [points objectAtIndex:count - 1],nil];
}
return resultList;
}
- (NSArray *)catmullRomSplineAlgorithmOnPoints:(NSArray *)points segments:(int)segments
{
int count = [points count];
if(count < 4) {
return points;
}
float b[segments][4];
{
// precompute interpolation parameters
float t = 0.0f;
float dt = 1.0f/(float)segments;
for (int i = 0; i < segments; i++, t+=dt) {
float tt = t*t;
float ttt = tt * t;
b[i][0] = 0.5f * (-ttt + 2.0f*tt - t);
b[i][1] = 0.5f * (3.0f*ttt -5.0f*tt +2.0f);
b[i][2] = 0.5f * (-3.0f*ttt + 4.0f*tt + t);
b[i][3] = 0.5f * (ttt - tt);
}
}
NSMutableArray *resultArray = [NSMutableArray array];
{
int i = 0; // first control point
[resultArray addObject:[points objectAtIndex:0]];
for (int j = 1; j < segments; j++) {
CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
CGPoint pointIp2 = [[points objectAtIndex:(i + 2)] CGPointValue];
float px = (b[j][0]+b[j][1])*pointI.x + b[j][2]*pointIp1.x + b[j][3]*pointIp2.x;
float py = (b[j][0]+b[j][1])*pointI.y + b[j][2]*pointIp1.y + b[j][3]*pointIp2.y;
[resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
}
}
for (int i = 1; i < count-2; i++) {
// the first interpolated point is always the original control point
[resultArray addObject:[points objectAtIndex:i]];
for (int j = 1; j < segments; j++) {
CGPoint pointIm1 = [[points objectAtIndex:(i - 1)] CGPointValue];
CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
CGPoint pointIp2 = [[points objectAtIndex:(i + 2)] CGPointValue];
float px = b[j][0]*pointIm1.x + b[j][1]*pointI.x + b[j][2]*pointIp1.x + b[j][3]*pointIp2.x;
float py = b[j][0]*pointIm1.y + b[j][1]*pointI.y + b[j][2]*pointIp1.y + b[j][3]*pointIp2.y;
[resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
}
}
{
int i = count-2; // second to last control point
[resultArray addObject:[points objectAtIndex:i]];
for (int j = 1; j < segments; j++) {
CGPoint pointIm1 = [[points objectAtIndex:(i - 1)] CGPointValue];
CGPoint pointI = [[points objectAtIndex:i] CGPointValue];
CGPoint pointIp1 = [[points objectAtIndex:(i + 1)] CGPointValue];
float px = b[j][0]*pointIm1.x + b[j][1]*pointI.x + (b[j][2]+b[j][3])*pointIp1.x;
float py = b[j][0]*pointIm1.y + b[j][1]*pointI.y + (b[j][2]+b[j][3])*pointIp1.y;
[resultArray addObject:[NSValue valueWithCGPoint:CGPointMake(px, py)]];
}
}
// the very last interpolated point is the last control point
[resultArray addObject:[points objectAtIndex:(count - 1)]];
return resultArray;
}
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.
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.
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;
}
Here is the code in Swift 4/5
func quadCurvedPathWithPoint(points: [CGPoint] ) -> UIBezierPath {
let path = UIBezierPath()
if points.count > 1 {
var prevPoint:CGPoint?
for (index, point) in points.enumerated() {
if index == 0 {
path.move(to: point)
} else {
if index == 1 {
path.addLine(to: point)
}
if prevPoint != nil {
let midPoint = self.midPointForPoints(from: prevPoint!, to: point)
path.addQuadCurve(to: midPoint, controlPoint: controlPointForPoints(from: midPoint, to: prevPoint!))
path.addQuadCurve(to: point, controlPoint: controlPointForPoints(from: midPoint, to: point))
}
}
prevPoint = point
}
}
return path
}
func midPointForPoints(from p1:CGPoint, to p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
}
func controlPointForPoints(from p1:CGPoint,to p2:CGPoint) -> CGPoint {
var controlPoint = midPointForPoints(from:p1, to: p2)
let diffY = abs(p2.y - controlPoint.y)
if p1.y < p2.y {
controlPoint.y = controlPoint.y + diffY
} else if ( p1.y > p2.y ) {
controlPoint.y = controlPoint.y - diffY
}
return controlPoint
}
I found a pretty nice tutorial that describes a slight modification to Bezier curve drawing that does tend to smooth out the edges pretty nicely. It's essentially what Caleb is referring to above about putting the joining end points on the same line as the control points. It's one of the best tutorials (on anything) that I've read in a while. And it comes with a fully working Xcode project.
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 was inspired by the answer of u/User1244109 ... but it only works if the points are constantly fluctuating up and then down each time, so that every point should be joined by an S curve.
I built off of his answer to include custom logic to check if the point is going to be a local minima or not, and then use the S-curve if so, otherwise determine if it should curve up or down based on the points before and after it or if it should curve tangentially and if so I use the intersection of tangents as the control point.
#define AVG(__a, __b) (((__a)+(__b))/2.0)
-(UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points {
if (points.count < 2) {
return [UIBezierPath new];
}
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint p0 = [points[0] CGPointValue];
CGPoint p1 = [points[1] CGPointValue];
[path moveToPoint:p0];
if (points.count == 2) {
[path addLineToPoint:p1];
return path;
}
for (int i = 1; i <= points.count-1; i++) {
CGPoint p1 = [points[i-1] CGPointValue];
CGPoint p2 = [points[i] CGPointValue];//current point
CGPoint p0 = p1;
CGPoint p3 = p2;
if (i-2 >= 0) {
p0 = [points[i-2] CGPointValue];
}
if (i+1 <= points.count-1) {
p3 = [points[i+1] CGPointValue];
}
if (p2.y == p1.y) {
[path addLineToPoint:p2];
continue;
}
float previousSlope = p1.y-p0.y;
float currentSlope = p2.y-p1.y;
float nextSlope = p3.y-p2.y;
BOOL shouldCurveUp = NO;
BOOL shouldCurveDown = NO;
BOOL shouldCurveS = NO;
BOOL shouldCurveTangental = NO;
if (previousSlope < 0) {//up hill
if (currentSlope < 0) {//up hill
if (nextSlope < 0) {//up hill
shouldCurveTangental = YES;
} else {//down hill
shouldCurveUp = YES;
}
} else {//down hill
if (nextSlope > 0) {//down hill
shouldCurveUp = YES;
} else {//up hill
shouldCurveS = YES;
}
}
} else {//down hill
if (currentSlope > 0) {//down hill
if (nextSlope > 0) {//down hill
shouldCurveTangental = YES;
} else {//up hill
shouldCurveDown = YES;
}
} else {//up hill
if (nextSlope < 0) {//up hill
shouldCurveDown = YES;
} else {//down hill
shouldCurveS = YES;
}
}
}
if (shouldCurveUp) {
[path addQuadCurveToPoint:p2 controlPoint:CGPointMake(AVG(p1.x, p2.x), MIN(p1.y, p2.y))];
}
if (shouldCurveDown) {
[path addQuadCurveToPoint:p2 controlPoint:CGPointMake(AVG(p1.x, p2.x), MAX(p1.y, p2.y))];
}
if (shouldCurveS) {
CGPoint midPoint = midPointForPoints(p1, p2);
[path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
[path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
}
if (shouldCurveTangental) {
float nextTangent_dy = p3.y-p2.y;
float nextTangent_dx = p3.x-p2.x;
float previousTangent_dy = p1.y-p0.y;
float previousTangent_dx = p1.x-p0.x;
float nextTangent_m = 0;
if (nextTangent_dx != 0) {
nextTangent_m = nextTangent_dy/nextTangent_dx;
}
float previousTangent_m = 0;
if (nextTangent_dx != 0) {
previousTangent_m = previousTangent_dy/previousTangent_dx;
}
if (isnan(previousTangent_m) ||
isnan(nextTangent_m) ||
nextTangent_dx == 0 ||
previousTangent_dx == 0) {//division by zero would have occured, etc
[path addLineToPoint:p2];
} else {
CGPoint nextTangent_start = CGPointMake(p1.x, (nextTangent_m*p1.x) - (nextTangent_m*p2.x) + p2.y);
CGPoint nextTangent_end = CGPointMake(p2.x, (nextTangent_m*p2.x) - (nextTangent_m*p2.x) + p2.y);
CGPoint previousTangent_start = CGPointMake(p1.x, (previousTangent_m*p1.x) - (previousTangent_m*p1.x) + p1.y);
CGPoint previousTangent_end = CGPointMake(p2.x, (previousTangent_m*p2.x) - (previousTangent_m*p1.x) + p1.y);
NSValue *tangentIntersection_pointValue = [self intersectionOfLineFrom:nextTangent_start to:nextTangent_end withLineFrom:previousTangent_start to:previousTangent_end];
if (tangentIntersection_pointValue) {
[path addQuadCurveToPoint:p2 controlPoint:[tangentIntersection_pointValue CGPointValue]];
} else {
[path addLineToPoint:p2];
}
}
}
}
return path;
}
-(NSValue *)intersectionOfLineFrom:(CGPoint)p1 to:(CGPoint)p2 withLineFrom:(CGPoint)p3 to:(CGPoint)p4 {//from https://stackoverflow.com/a/15692290/2057171
CGFloat d = (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x);
if (d == 0)
return nil; // parallel lines
CGFloat u = ((p3.x - p1.x)*(p4.y - p3.y) - (p3.y - p1.y)*(p4.x - p3.x))/d;
CGFloat v = ((p3.x - p1.x)*(p2.y - p1.y) - (p3.y - p1.y)*(p2.x - p1.x))/d;
if (u < 0.0 || u > 1.0)
return nil; // intersection point not between p1 and p2
if (v < 0.0 || v > 1.0)
return nil; // intersection point not between p3 and p4
CGPoint intersection;
intersection.x = p1.x + u * (p2.x - p1.x);
intersection.y = p1.y + u * (p2.y - p1.y);
return [NSValue valueWithCGPoint:intersection];
}
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 = fabs(p2.y - controlPoint.y);
if (p1.y < p2.y)
controlPoint.y += diffY;
else if (p1.y > p2.y)
controlPoint.y -= diffY;
return controlPoint;
}

cocos2d fruit ninja parabola math

i am making a game similar to fruit ninja. birds flying down the water and then up (just like fruits up side down)
but some of the birds fly too far and the others too near.
can someone check my code? vy should quite close to each other.(vx is not a problem)
static float tuna = 10.0f;
-(void) reset
{
float vy = 0.0f;
float vx = 0.0f;
int sign = 1;
if (CCRANDOM_0_1() >= 0.5) {
sign = -1;
}
float hurry = 0.0f;
if (CCRANDOM_0_1() <= 0.1) {
hurry = 1.0f;
}
switch (birdType) {
case BirdType1:
vx = 1.0f * sign + (CCRANDOM_0_1() - 0.5f) * 0.08f;
vy = -6.5f;
break;
case BirdType2:
vx = 1.5f * sign + (CCRANDOM_0_1() - 0.5f) * 0.08f;
vy = -6.2f + (CCRANDOM_0_1() - 0.5f) * 0.1f;
break;
case BirdType3:
vx = 1.0f * sign + (CCRANDOM_0_1() - 0.5f) * 0.1f;
vy = -5.8f - hurry;
break;
default:
[NSException exceptionWithName:#"BirdMoveComponent exception" reason:#"unhandled bird type" userInfo:nil];
break;
}
velocity = CGPointMake(vx * 5, vy * 5);
if ((int)([[GameManager sharedManager] score] / 100) >= prevLevel) {
if (tuna <= 12.0f) {
tuna += 0.01f;
}
prevLevel = (int)[[GameManager sharedManager] score] / 100;
}
}
-(void) update:(ccTime) delta
{
if (self.parent.visible) {
NSAssert([self.parent isKindOfClass:[BirdEntity class]], #"node is not an entity");
BirdEntity* bird = (BirdEntity*) self.parent;
bird.position = ccpAdd(bird.position, ccpMult(velocity, delta * tuna));
velocity = ccpAdd(velocity, ccpMult(acceleration, delta * tuna));
acceleration = ccp(0, 0.3f);
float birdHeight = CGRectGetHeight([bird boundingBox]);
//20 is the bottom trap
if (bird.position.y <= (birdHeight / 2) + 20) {
[bird dieAccidently];
}
if (CGRectIntersectsRect([GameScene screenRect], [bird boundingBox]) == NO)
{
bird.visible = NO;
[bird stopAllActions];
[bird unscheduleAllSelectors];
[bird unscheduleUpdate];
[self reset];
}
}
}
thoght your question not programmatical but physical (mechanical).
position of object can be calculated from the system of equation:
x = Vx * t + x0
y = (-g*t*t)/2 + Vy * t + y0
, where g - Gravitational acceleration, V - initial speed, Vx and Vy - its projections on axes X and Y, respectively.
Question is what's the highest point, i.e. we need to found MAX(y(t)).
derivative: y'(t) = -g*t + Vy.
y'(t) should equals zero, -g*t + Vy = 0; t = Vy/g; MAX(y) = y(Vy/g) = Vy*Vy/2g.
MAX(y) = Vy*Vy/2g + y0 // ballistic trajectory
MIN(y) = y0 - Vy*Vy/2g // your case
End you should calculate velocity accroding to this, if you want your bird Y to be in certain range.
Addition:
btw is there a sample cocos2d code for
parabola?
Here is my working code.
- (void) update: (ccTime)dt
{
t += dt*20;
...
[self getVertices];
}
- (void) getVertices
{
//for every index: {
...
//getting initial position (x0, y0)
...
vertices[index] = ccpAdd(vertices[index], ccpMult(velocity[index/3], t * screenFactor)); //+velocity*t
vertices[index] = ccpAdd(vertices[index], ccpMult(gravity, (t*t/2) * screenFactor)); //+acceleration*t^2 /2
//}
}
1) As you can see, there's no need to calculate Velocity every time: use initial speed.
2) Vertices is CGPoint array of current Sprite positions.
3) t (current time), vertices, gravity, velocity are instance variables of common class.