Рисование гладких кривых - необходимые методы
Как сгладить набор точек в приложении для рисования iOS во время движения? Я пробовал UIBezierpaths, но все, что я получаю, это зазубренные концы, где они пересекаются, когда я просто сдвигаю точки 1,2,3,4 - 2,3,4,5. Я слышал о кривых сплайнов и всех других типах. Я довольно новичок в программировании iPhone и не понимаю, как программировать его в своем приложении для рисования кварца. Хороший пример был бы очень признателен, я провел несколько недель в кругах, и мне никогда не удается найти какой-либо код iOS для этой задачи. Большинство постов просто ссылаются на симуляцию java или страницы в википедии о подгонке кривой, которая ничего не делает для меня. Также я не хочу переходить на openGL ES. Я надеюсь, что кто-то может наконец предоставить код, чтобы ответить на этот циркулирующий вопрос.
Это был мой код для UIBezierPath, который оставил ребра на пересечении ///
ОБНОВЛЕНО ДЛЯ ОТВЕТА НИЖЕ
#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];
}
10 ответов
Я только что реализовал нечто подобное в проекте, над которым я работаю. Мое решение состояло в том, чтобы использовать сплайн Catmull-Rom вместо сплайнов Безье. Они обеспечивают очень плавную кривую ЧЕРЕЗ набор точек, а не безлинейный сплайн вокруг точек.
// 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
Первоначальная реализация Catmull-Rom основана на некотором коде Эрики Садун в одной из ее книг, я немного изменил его, чтобы получить полную сглаженную кривую. Это реализовано как категория на UIBezierPath и отлично сработало для меня.
Здесь есть несколько хороших ответов, хотя я думаю, что они либо далеки (ответ user1244109 поддерживает только горизонтальные касательные, бесполезные для общих кривых), либо слишком сложны (извините, поклонники Catmull-Rom).
Я реализовал это гораздо более простым способом, используя кривые Безье. Им нужны начальная точка, конечная точка и контрольная точка. Естественно, можно использовать точки касания в качестве начальной и конечной точек. Не делай этого! Нет подходящих контрольных точек для использования. Вместо этого попробуйте эту идею: используйте точки касания в качестве контрольных точек, а средние точки в качестве начальной / конечной точек. Таким образом, вы гарантированно получите правильные касательные, а код просто глуп. Вот алгоритм:
- Точка "приземления" - это начало пути, и магазин
location
вprevPoint
, - Для каждого перетаскиваемого местоположения рассчитайте
midPoint
, точка междуcurrentPoint
а такжеprevPoint
,- Если это первое перетаскиваемое место, добавьте
currentPoint
как отрезок. - Для всех точек в будущем добавьте четырехугольную кривую, которая заканчивается на
midPoint
и использоватьprevPoint
в качестве контрольной точки. Это создаст сегмент, который плавно изгибается от предыдущей точки к текущей точке.
- Если это первое перетаскиваемое место, добавьте
- хранить
currentPoint
вprevPoint
и повторяйте #2, пока не закончится перетаскивание. - Добавьте конечную точку как еще один прямой отрезок, чтобы закончить путь.
Это приводит к очень хорошо выглядящим кривым, потому что использование средних точек гарантирует, что кривая является гладкой касательной в конечных точках (см. Прилагаемое фото).
Код Swift выглядит так:
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)
}
Или, если у вас есть массив точек и вы хотите построить UIBezierPath
в одном кадре:
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)
}
Вот мои заметки:
@Rakesh абсолютно прав - вам не нужно использовать алгоритм Catmull-Rom, если вы просто хотите изогнутую линию. И ссылка, которую он предложил, делает именно это. Итак, вот дополнение к его ответу.
Приведенный ниже код НЕ использует алгоритм и гранулярность Catmull-Rom, но рисует изогнутую квадратом линию (контрольные точки рассчитываются для вас). По сути, это то, что сделано в учебнике рисования от руки ios, предложенном Ракешем, но в автономном методе, который вы можете отбросить куда угодно (или в категорию UIBezierPath) и получить изогнутый квадратом сплайн из коробки.
Вам нужно иметь массив CGPoint
завернут в 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;
}
Вот результат:
Ключом к плавному объединению двух кривых Безье является то, что соответствующие контрольные точки и начальные / конечные точки на кривых должны быть коллинеарными. Представьте, что контрольная точка и конечная точка образуют линию, которая касается кривой в конечной точке. Если одна кривая начинается в той же точке, где заканчивается другая, и если они обе имеют одинаковую касательную в этой точке, кривая будет гладкой. Вот немного кода для иллюстрации:
- (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];
}
Заметить, что path1
заканчивается в point2
, path2
начинается в point2
и контрольные точки 2 и 3 имеют одинаковое значение Y, commonY
, с point2
, Вы можете изменить любое значение в коде так, как вам нравится; до тех пор, пока все эти три точки находятся на одной линии, два пути будут плавно соединяться. (В приведенном выше коде строка y = commonY
, Линия не должна быть параллельной оси X; просто легче понять, что точки коллинеарны таким образом.)
Вот изображение, которое рисует код выше:
Посмотрев на ваш код, причина, по которой ваша кривая имеет неровный угол, заключается в том, что вы рассматриваете контрольные точки как точки на кривой. На кривой Безье контрольные точки обычно не находятся на кривой. Поскольку вы берете контрольные точки из кривой, контрольные точки и точки пересечения не коллинеарны, и поэтому пути не соединяются плавно.
Нам нужно кое-что заметить, прежде чем применять какой-либо алгоритм к зафиксированным точкам.
- Обычно UIKit не дает очки на равном расстоянии.
- Нам нужно вычислить промежуточные точки между двумя CGPoints[которые были получены методом Touch Move]
Теперь, чтобы получить плавную линию, есть так много способов.
В некоторых случаях мы можем добиться применения алгоритмов полиномов второй степени или полиномов третьей степени или catmullRomSpline
- (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;
}
Для достижения этого нам необходимо использовать этот метод. BezierSpline код находится в C#, чтобы генерировать массивы контрольных точек для более сплайна Безье. Я преобразовал этот код в Objective C, и он прекрасно работает для меня.
Чтобы преобразовать код из C# в Objective C. понимать код C# построчно, даже если вы не знаете C#, вы должны знать C++/Java?
При конвертации:
Замените структуру Point, используемую здесь, на CGPoint.
Замените массив Point на NSMutableArray и сохраните значения NSvalues, заключающие в себе CGPoints.
Замените все двойные массивы на NSMutableArrays и сохраните обертку NSNumber в ней.
используйте objectAtIndex: метод в случае индекса для доступа к элементам массива.
используйте replaceObjectAtIndex:withObject: для хранения объектов по определенному индексу.
Помните, что NSMutableArray является связным списком, а C# использует динамические массивы, поэтому у них уже есть существующие индексы. В вашем случае, в NSMutableArray, если он пуст, вы не можете хранить объекты со случайными индексами, как это делает код C#. они иногда в этом коде C# заполняют индекс 1 до индекса 0, и они могут сделать это, так как индекс 1 существует. в NSMutabelArrays здесь, индекс 1 должен быть там, если вы хотите вызвать replaceObject для него. поэтому перед сохранением чего-либо создайте метод, который добавит n объектов NSNull в NSMutableArray.
ТАКЖЕ:
у этой логики есть статический метод, который будет принимать массив точек и давать вам два массива:
массив первых контрольных точек.
массив вторых контрольных точек.
Эти массивы будут содержать первую и вторую контрольную точку для каждой кривой между двумя точками, которые вы проходите в первом массиве.
В моем случае у меня уже были все точки, и я мог нарисовать кривую через них.
В случае, когда вы рисуете, вам понадобится несколько наборов точек, через которые вы хотите, чтобы плавная кривая проходила.
и обновить, вызвав setNeedsDisplay и нарисовать сплайн, который является ничем иным, как UIBezierPath между двумя смежными точками в первом массиве. и взятие контрольных точек из обоих массивов контрольных точек по индексу.
Проблема в вашем случае заключается в том, что трудно понять, перемещая все критические точки, чтобы взять.
Что вы можете сделать: просто перемещая палец, продолжайте рисовать прямые линии между предыдущей и текущей точкой. Линии будут настолько маленькими, что их не будет видно невооруженным глазом, так что они будут маленькими маленькими прямыми линиями, если вы не увеличите масштаб.
ОБНОВИТЬ
Любой, кто заинтересован в реализации Objective C по ссылке выше, может обратиться к
это GitHub репо.
Я написал это некоторое время назад, и он не поддерживает ARC, но вы можете легко отредактировать его, удалить несколько вызовов релиза и автоматического выпуска и заставить его работать с ARC.
Этот просто генерирует два массива контрольных точек для набора точек, к которым каждый хочет присоединиться, используя более сплайновое число.
Не нужно писать так много кода.
Просто обратитесь к руководству по рисованию от ios; это действительно сглаживает рисунок, также есть механизм кэширования, так что производительность не снижается, даже если вы продолжаете рисовать непрерывно.
Вот код в 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
}
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;
}
Я попробовал все вышеперечисленное, но не могу заставить его работать. Один из ответов даже для меня дает неверный результат. После поиска больше я нашел это: https://github.com/sam-keene/uiBezierPath-hermite-curve. Я не писал этот код, но я реализовал его, и он работает очень хорошо. Просто скопируйте UIBezierPath+Interpolation.m/h и CGPointExtension.m/h. Тогда вы используете это так:
UIBezierPath *path = [UIBezierPath interpolateCGPointsWithHermite:arrayPoints closed:YES];
Это действительно надежное и аккуратное решение в целом.
Я нашел довольно хороший учебник, в котором описывается небольшая модификация рисования кривой Безье, которая действительно очень хорошо сглаживает края. Это, по сути, то, на что ссылается Калеб выше, о размещении конечных точек соединения на одной линии с контрольными точками. Это один из лучших уроков (о чем угодно), которые я читал за последнее время. И это идет с полностью работающим проектом XCode.
Меня вдохновил ответ u/User1244109 ... но он работает, только если точки постоянно колеблются вверх, а затем вниз каждый раз, поэтому каждая точка должна быть соединена S-образной кривой.
Я построил его ответ, чтобы включить пользовательскую логику, чтобы проверить, будет ли точка быть локальным минимумом или нет, а затем использовать S-образную кривую, если это так, иначе определить, должна ли она изгибаться вверх или вниз на основе точек до и после него или если он должен изгибаться по касательной, и если да, то я использую пересечение касательных в качестве контрольной точки.
#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://stackru.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;
}