Alright so this'll be a long answer. Here's what Ive done:
Ive programmed a MEL script that allows you to draw a bezier curve within Maya and then - selecting that curve - run my script which will go through the curve analyzing each bezier section of the curve calculating the length of each section and the positions of the curve points / control points. Once it has all of this data calculated, it exports everything to a .bezier file that is structured like this:
Line 1: Number of individual bezier curves contained in the entire bezier path
Line 2: Length of first bezier curve
...
Line X: Length of last bezier curve
X Position of the first control point of the first curve point
Y Position of the first control point of the first curve point
Z Position of the first control point of the first curve point
X Position of the first curve point
Y Position of the first curve point
Z Position of the first curve point
X Position of the second control point of the first curve point
Y Position of the second control point of the first curve point
Z Position of the second control point of the first curve point
...
X Position of the first control point of the last curve point
Y Position of the first control point of the last curve point
Z Position of the first control point of the last curve point
X Position of the last curve point
Y Position of the last curve point
Z Position of the last curve point
X Position of the second control point of the last curve point
Y Position of the second control point of the last curve point
Z Position of the second control point of the last curve point
So for this set of classes to work you'll need a file structured like that.
Here are the three classes Ive then programmed to handle .bezier files:
AEBezierPath:
.h file:
#import <Foundation/Foundation.h>
#import "AEBezierVertex.h"
#import "AEBezierLine.h"
@interface AEBezierPath : NSObject
{
NSMutableArray *vertices;
NSMutableArray *lines;
UIBezierPath *path;
}
@property (strong) NSMutableArray *vertices;
@property (strong) NSMutableArray *lines;
@property (strong) UIBezierPath *path;
-(id) initFromFile: (NSString*) file;
-(CGPoint) positionFromDistance: (float) fromDistance;
@end
.m file:
#import "AEBezierPath.h"
CGFloat bezierInterpolation(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d) {
// see also below for another way to do this, that follows the 'coefficients'
// idea, and is a little clearer
CGFloat t2 = t * t;
CGFloat t3 = t2 * t;
return a + (-a * 3 + t * (3 * a - a * t)) * t
+ (3 * b + t * (-6 * b + b * 3 * t)) * t
+ (c * 3 - c * 3 * t) * t2
+ d * t3;
}
@implementation AEBezierPath
@synthesize vertices;
@synthesize lines;
@synthesize path;
-(id) initFromFile: (NSString*) file
{
self = [super init];
if (self) {
//Init file objects for reading
NSError *fileError;
NSStringEncoding *encoding;
vertices = [[NSMutableArray alloc] init];
lines = [[NSMutableArray alloc] init];
path = [[UIBezierPath alloc] init];
//Load the specified file's contents into an NSString
NSString *fileData = [[NSString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"testcurve" ofType:@"bezier"] usedEncoding:&encoding error:&fileError];
NSScanner *scanner = [[NSScanner alloc] initWithString:fileData];
if(fileData == nil)
{
NSLog(@"Error reading bezier path file");
}
else
{
float x;
float y;
float cx;
float cy;
float cx2;
float cy2;
float temp;
CGPoint readPoint;
CGPoint readControlIn;
CGPoint readControlOut;
int curRead = 0;
int totalSegments = 0;
float length;
[scanner scanInt:&totalSegments];
for (int s = 0; s < totalSegments; s++) {
[scanner scanFloat:&length];
AEBezierLine *newLine = [[AEBezierLine alloc] initWithLength:length];
[lines addObject:newLine];
}
AEBezierVertex *vertex;
while ([scanner isAtEnd] == 0) {
if (curRead == 0) {
[scanner scanFloat:&x];
[scanner scanFloat:&temp];
[scanner scanFloat:&y];
[scanner scanFloat:&cx2];
[scanner scanFloat:&temp];
[scanner scanFloat:&cy2];
cx = x;
cy = y;
}
else{
[scanner scanFloat:&cx];
[scanner scanFloat:&temp];
[scanner scanFloat:&cy];
[scanner scanFloat:&x];
[scanner scanFloat:&temp];
[scanner scanFloat:&y];
if ([scanner isAtEnd] == 0) {
[scanner scanFloat:&cx2];
[scanner scanFloat:&temp];
[scanner scanFloat:&cy2];
}else
{
cx = x;
cy = y;
}
}
readPoint = CGPointMake(x, y);
readControlIn = CGPointMake(cx, cy);
readControlOut = CGPointMake(cx2, cy2);
vertex = [[AEBezierVertex alloc] initWithControl:readPoint In:readControlIn Out:readControlOut];
[vertices addObject:vertex];
curRead ++;
}
for (int c = 0; c < [vertices count]-1; c++) {
//Init CGPoints for single bezier curve segment
CGPoint p1, p2, p3, p4;
//Store starting bezier point and control point
AEBezierVertex *b1 = [vertices objectAtIndex:c];
p1 = b1.control;
p2 = b1.controlOut;
//Store ending bezier point and control point
AEBezierVertex *b2 = [vertices objectAtIndex:c+1];
p3 = b2.controlIn;
p4 = b2.control;
if (c == 0) {
[path moveToPoint:p1];
}
else
{
[path addCurveToPoint:p4 controlPoint1:p2 controlPoint2:p3];
}
}
}
}
return self;
}
-(CGPoint) positionFromDistance: (float) fromDistance
{
CGPoint position;
AEBezierLine *line;
float runningLength;
int seg = 0;
for (int c = 0; c < [lines count]; c++) {
seg = c;
line = [lines objectAtIndex:c];
runningLength += line.length;
if (runningLength > fromDistance) {
break;
}
}
CGPoint p1, p2, p3, p4;
AEBezierVertex *vert1 = [vertices objectAtIndex:seg];
p1 = vert1.control;
p2 = vert1.controlOut;
//Store ending bezier point and control point
AEBezierVertex *vert2 = [vertices objectAtIndex:seg+1];
p3 = vert2.controlIn;
p4 = vert2.control;
float travelDist;
travelDist = fromDistance;
travelDist = runningLength - travelDist;
travelDist = line.length - travelDist;
float t = travelDist / line.length;
//Create a new point to represent this position
position = CGPointMake(bezierInterpolation(t, p1.x, p2.x, p3.x, p4.x),
bezierInterpolation(t, p1.y, p2.y, p3.y, p4.y));
return position;
}
@end
AEBezierVertex:
.h file:
#import <Foundation/Foundation.h>
@interface AEBezierVertex : NSObject
{
CGPoint controlIn;
CGPoint controlOut;
CGPoint control;
}
@property CGPoint controlIn;
@property CGPoint controlOut;
@property CGPoint control;
-(id) initWithControl: (CGPoint) setControl In: (CGPoint) setIn Out: (CGPoint) setOut;
@end
.m file:
#import "AEBezierVertex.h"
@implementation AEBezierVertex
@synthesize controlIn;
@synthesize controlOut;
@synthesize control;
-(id) initWithControl: (CGPoint) setControl In: (CGPoint) setIn Out: (CGPoint) setOut
{
self = [super init];
if (self) {
//Init
control = setControl;
controlIn = setIn;
controlOut = setOut;
}
return self;
}
@end
AEBezierLine:
.h file:
#import <Foundation/Foundation.h>
@interface AEBezierLine : NSObject
{
float length;
}
@property float length;
-(id) initWithLength: (float) setLength;
@end
.m file:
#import "AEBezierLine.h"
@implementation AEBezierLine
@synthesize length;
-(id) initWithLength: (float) setLength
{
self = [super init];
if (self) {
//Init
length = setLength;
}
return self;
}
@end
How It Works:
Ensure you have created a .bezier file suiting the structure I've shown above and have it in your app's bundle.
Instantiate a new AEBezierPath instance via:
-(id) initFromFile: (NSString*) file;
This will read in all of the data from the .bezier file named *file and construct a UIBezierPath from it, as well as store the necessary length information into the AEBezierPath.
Query the AEBezierPath for an x/y position in the form of a CGPoint, by sending it a distance value to travel from the start of the path, using the method:
-(CGPoint) positionFromDistance: (float) fromDistance;
This method will first determine which bezier segment that distance lies on by using the lengths of each bezier segment previously retrieved from the .bezier file. After this the method will use the bezierInterpolation function mentioned in the previous posts on this SO Question to calculate the x/y position on the bezier path at this distance, and return it as a CGPoint.
Its not perfect, there still is some noticeable differences in the distance traveled over long bezier curves vs short tight corners, but it is certainly far less noticeable than not using this system at all and instead relying on a percentage value to travel along the bezier curve.
I know the code can certainly be optimized, this is just a first run through to get everything working, but I think its good enough to post as an answer for now.