2012-07-07 16 views
6

Questo è per un'applicazione iPad, ma è essenzialmente una domanda di matematica.Uso di una curva di Bezier per disegnare una spirale

Ho bisogno di disegnare un arco circolare di larghezza della linea variabile (che aumenta in modo monotono). All'inizio della curva, avrebbe uno spessore iniziale (diciamo 2pts) e quindi lo spessore aumenterebbe gradualmente fino alla fine dell'arco dove sarebbe al suo massimo spessore (diciamo 12pts).

Credo che il modo migliore per farlo è creare un UIBezierPath e riempire la forma. Il mio primo tentativo è stato quello di utilizzare due archi circolari (con centri di offset), che funzionavano bene fino a 90 °, ma l'arco sarebbe spesso compreso tra 90 ° e 180 °, in modo che l'approccio non lo tagli.

example of 90 degree arc with increasing thickness

mio approccio attuale è quella di fare una leggera spirale (uno leggermente crescente dall'arco circolare ed uno leggermente restringimento) utilizzando quad bezier o curve cubiche. La domanda è dove inserisco i punti di controllo in modo che la deviazione dall'arco circolare (ovvero la forma "spessore") sia il valore desiderato.

Vincoli:

  • La forma deve essere in grado di iniziare e terminare in un angolo arbitrario (entro 180 ° l'uno dall'altro)
  • Il "spessore" della forma (deviazione dal cerchio) deve iniziare e terminare con i valori indicati
  • il "spessore" deve aumentare in modo monotono (non si può ottenere più grande e quindi più piccolo di nuovo)
  • ha a guardare liscia per l'occhio, non ci può essere alcun curve strette

Sono aperto anche ad altre soluzioni.

risposta

5

Il mio approccio solo costruisce 2 archi di cerchio e riempie la regione in mezzo. Il difficile è capire i centri e i raggi di questi archi. Sembra abbastanza buono a condizione che gli spessori non siano troppo grandi. (Taglia e incolla e decidi tu stesso se soddisfa le tue esigenze.) Potrebbe essere migliorato utilizzando un tracciato di ritaglio.

- (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); 
} 
+0

Questo sembra davvero fantastico! Mi hai salvato un bel po 'di lavoro. Questo è molto più semplice dell'approccio su cui stavo lavorando (risolvendo le equazioni polinomiali di Bezier per la spirale). Ho funzionato per multipli di 90 °, ma gli angoli arbitrari sarebbero stati un dolore. Questo è molto meglio ... –

+0

@JonHull Sono contento che ti piaccia. Mi sono appena reso conto che ho assunto implicitamente che 'endingThickness> = startingThickness', ma dovresti facilmente essere in grado di organizzare i tuoi parametri di input in modo che questa condizione sia soddisfatta. In caso contrario, potrebbero verificarsi scenari in cui "projectedEndingThickness" è negativo e quindi non posso più essere certo dell'algebra. Potrebbe ancora funzionare, ma non l'ho provato. – inwit

+0

Oh ottimo lavoro fratello ,,, sei un vero risparmiatore di vita ,,, grazie – Dhiru

1

Una soluzione potrebbe essere quella di generare una polilinea manualmente. Questo è semplice ma ha lo svantaggio di dover aumentare la quantità di punti che si generano se il controllo viene visualizzato ad alta risoluzione. Non so abbastanza di iOS per darvi iOS/codice di esempio objC, ma ecco alcuni pseudocodice 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 
+0

Grazie per lo pseudocodice. Questo sarà il mio backup se non riesco a trovare una soluzione che utilizza curve o archi di bezier. –