Usando una curva de Bézier para dibujar una espiral

Esto es para una aplicación de iPad, pero es esencialmente una pregunta de matemáticas.

Necesito dibujar un arco circular de ancho de línea variable (monotónicamente creciente). Al principio de la curva, tendría un grosor inicial (digamos 2pts) y luego el grosor boostía suavemente hasta el final del arco donde estaría en su mayor espesor (digamos 12pts).

Me imagino que la mejor manera de hacerlo es creando un UIBezierPath y completando la forma. Mi primer bash fue usar dos arcos circulares (con centros de compensación), y eso funcionó bien hasta 90 °, pero el arco estará a menudo entre 90 ° y 180 °, por lo que ese enfoque no lo cortará.

Ejemplo de arco de 90 grados con grosor creciente

Mi enfoque actual es hacer una ligera espiral (una ligeramente creciente a partir del arco circular y una leve contracción) usando curvas bezier cuádruples o cúbicas. La pregunta es dónde coloco los puntos de control para que la desviación del arco circular (también conocido como "grosor" de forma) sea el valor que deseo.

Restricciones:

  • La forma debe poder comenzar y terminar en un ángulo arbitrario (dentro de 180 ° el uno del otro)
  • El "grosor" de la forma (desviación del círculo) debe comenzar y finalizar con los valores dados
  • El "grosor" debe boost monótonamente (no puede volverse más grande y luego más pequeño)
  • Tiene que verse suavemente a la vista, no puede haber ninguna curva cerrada.

Estoy abierto a otras soluciones también.

Mi enfoque solo construye 2 arcos circulares y llena la región en el medio. Lo difícil es averiguar los centros y los radios de estos arcos. Se ve bastante bien siempre que los grosores no sean demasiado grandes. (Corte y pegue y decida por usted mismo si satisface sus necesidades). Posiblemente podría mejorarse mediante el uso de una ruta de recorte.

- (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 networkingColor].CGColor); CGContextFillPath(context); CGPathRelease(path); } 

Una solución podría ser generar una polilínea manualmente. Esto es simple pero tiene la desventaja de que tendría que boost la cantidad de puntos que genera si el control se muestra en alta resolución. No sé lo suficiente acerca de iOS para darte el código de ejemplo de iOS / ObjC, pero aquí hay un pseudocódigo de Python-Ish:

 # 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