Использование кривой Безье для рисования спирали

Это для приложения для iPad, но по сути это математический вопрос.

Мне нужно нарисовать дугу окружности переменной (монотонно увеличивающейся) ширины линии. В начале кривой он будет иметь начальную толщину (скажем, 2 точки), а затем толщина будет плавно увеличиваться до конца дуги, где она будет максимальной толщиной (скажем, 12 точек).

Я считаю, что лучший способ сделать это - создать UIBezierPath и заполнить форму. Моя первая попытка состояла в том, чтобы использовать две дуги окружности (со смещенными центрами), и они отлично работали до 90 °, но дуга часто будет между 90 ° и 180 °, так что такой подход не срезает ее.

пример дуги 90 градусов с увеличивающейся толщиной

Мой нынешний подход состоит в том, чтобы сделать небольшую спираль (одна слегка вырастает из дуги окружности, а другая слегка сокращается) с использованием четырехугольных или кубических кривых Безье. Вопрос в том, где разместить контрольные точки, чтобы отклонение от дуги окружности (также известное как «толщина» формы) было желаемым значением.

Ограничения:

  • Форма должна иметь возможность начинаться и заканчиваться под произвольным углом (в пределах 180 ° друг от друга).
  • «Толщина» формы (отклонение от круга) должна начинаться и заканчиваться заданными значениями.
  • «Толщина» должна монотонно увеличиваться (не может увеличиваться, а потом снова уменьшаться).
  • Он должен выглядеть гладким, резких изгибов быть не может.

Я открыт и для других решений.


person Jon Hull    schedule 07.07.2012    source источник


Ответы (2)


Мой подход просто строит 2 дуги окружности и заполняет область между ними. Сложнее всего определить центры и радиусы этих дуг. Смотрится неплохо при не слишком большой толщине. (Вырежьте и вставьте и решите для себя, соответствует ли это вашим потребностям.) Возможно, это можно улучшить, используя обтравочный контур.

- (void)drawRect:(CGRect)rect
{
  CGContextRef context = UIGraphicsGetCurrentContext();

  CGMutablePathRef path = CGPathCreateMutable();

  // As appropriate for iOS, the code below assumes a coordinate system with
  // the x-axis pointing to the right and the y-axis pointing down (flipped from the standard Cartesian convention).
  // Therefore, 0 degrees = East, 90 degrees = South, 180 degrees = West,
  // -90 degrees = 270 degrees = North (once again, flipped from the standard Cartesian convention).
  CGFloat startingAngle = 90.0;  // South
  CGFloat endingAngle = -45.0;   // North-East
  BOOL weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection = YES;  // change this to NO if necessary

  CGFloat startingThickness = 2.0;
  CGFloat endingThickness = 12.0;

  CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
  CGFloat meanRadius = 0.9 * fminf(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0);

  // the parameters above should be supplied by the user
  // the parameters below are derived from the parameters supplied above

  CGFloat deltaAngle = fabsf(endingAngle - startingAngle);

  // projectedEndingThickness is the ending thickness we would have if the two arcs
  // subtended an angle of 180 degrees at their respective centers instead of deltaAngle
  CGFloat projectedEndingThickness = startingThickness + (endingThickness - startingThickness) * (180.0 / deltaAngle);

  CGFloat centerOffset = (projectedEndingThickness - startingThickness) / 4.0;
  CGPoint centerForInnerArc = CGPointMake(center.x + centerOffset * cos(startingAngle * M_PI / 180.0),
                                          center.y + centerOffset * sin(startingAngle * M_PI / 180.0));
  CGPoint centerForOuterArc = CGPointMake(center.x - centerOffset * cos(startingAngle * M_PI / 180.0),
                                          center.y - centerOffset * sin(startingAngle * M_PI / 180.0));

  CGFloat radiusForInnerArc = meanRadius - (startingThickness + projectedEndingThickness) / 4.0;
  CGFloat radiusForOuterArc = meanRadius + (startingThickness + projectedEndingThickness) / 4.0;

  CGPathAddArc(path,
               NULL,
               centerForInnerArc.x,
               centerForInnerArc.y,
               radiusForInnerArc,
               endingAngle * (M_PI / 180.0),
               startingAngle * (M_PI / 180.0),
               !weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
               );

  CGPathAddArc(path,
               NULL,
               centerForOuterArc.x,
               centerForOuterArc.y,
               radiusForOuterArc,
               startingAngle * (M_PI / 180.0),
               endingAngle * (M_PI / 180.0),
               weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
               );

  CGContextAddPath(context, path);

  CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
  CGContextFillPath(context);

  CGPathRelease(path);  
}
person inwit    schedule 08.07.2012
comment
Выглядит действительно здорово! Вы сэкономили мне немало работы. Это намного проще, чем подход, над которым я работал (решение полиномиальных уравнений Безье для спирали). Я добился того, чтобы он работал с кратными 90 °, но произвольные углы были проблемой. Это намного лучше ... - person Jon Hull; 08.07.2012
comment
@JonHull Рад, что тебе понравилось. Я только что понял, что неявно предполагал, что endingThickness >= startingThickness, но вы легко сможете организовать свои входные параметры так, чтобы это условие выполнялось. В противном случае могут быть сценарии, в которых projectedEndingThickness отрицательно, и тогда я больше не могу быть уверен в алгебре. Это может все еще работать, но я не тестировал. - person inwit; 08.07.2012
comment
О, отличная работа, брат, ты настоящий спасатель жизни ???? ,,, спасибо ???????? - person Dhiru; 19.06.2017

Одним из решений может быть создание ломаной линии вручную. Это просто, но имеет тот недостаток, что вам придется увеличивать количество точек, которые вы генерируете, если элемент управления отображается с высоким разрешением. Я недостаточно знаю iOS, чтобы дать вам пример кода iOS / ObjC, но вот какой-то псевдокод в стиле Python:

# lower: the starting angle
# upper: the ending angle
# radius: the radius of the circle

# we'll fill these with polar coordinates and transform later
innerSidePoints = []
outerSidePoints = []

widthStep = maxWidth / (upper - lower)
width = 0

# could use a finer step if needed
for angle in range(lower, upper):
    innerSidePoints.append(angle, radius - (width / 2))
    outerSidePoints.append(angle, radius + (width / 2))
    width += widthStep

# now we have to flip one of the arrays and join them to make
# a continuous path.  We could have built one of the arrays backwards
# from the beginning to avoid this.

outerSidePoints.reverse()
allPoints = innerSidePoints + outerSidePoints # array concatenation

xyPoints = polarToRectangular(allPoints) # if needed
person japreiss    schedule 07.07.2012
comment
Спасибо за псевдокод. Это будет моя резервная копия, если я не смогу найти решение, в котором используются кривые Безье или дуги. - person Jon Hull; 08.07.2012