Coloque imágenes a lo largo de un camino bezier

¿Alguien sabe cómo colocar imágenes a lo largo de un camino bezier? Puedo escribir el camino fino y animar un sprite a lo largo del camino, pero quiero hacer del recorrido una serie de flechas en lugar de líneas punteadas. Supuse que debe haber una forma de agregar imágenes de flecha a lo largo de la ruta, pero no puedo encontrarla. Además, las rutas son todas curvas:

UIBezierPath * path = [UIBezierPath bezierPath]; [path moveToPoint:startingPoint]; [path addCurveToPoint:endPoint controlPoint1:controlPoint1 controlPoint2:controlPoint2]; 

    Supongo que quieres algo como esto:

    aplicación de demostración de flechas

    Puede encontrar mi proyecto de aplicación de demostración completo en este repository de github .

    De todos modos, este es un pequeño problema interesante.

    Necesita generar una matriz de puntos a lo largo de la ruta, y supongo que quiere que estén equiespaciados. Generar estos puntos no es trivial.

    Afortunadamente, Core Graphics contiene una función que lo hará por usted, pero no es evidente cuál. La función es CGPathCreateCopyByDashingPath .

    Primero, hagamos una categoría UIBezierPath que cree una copia punteada:

    UIBezierPath+Rob_dash.h

     #import  @interface UIBezierPath (Rob_dash) - (instancetype)Rob_dashedPathWithPattern:(NSArray *)pattern phase:(CGFloat)phase; @end 

    UIBezierPath+Rob_dash.m

     #import "UIBezierPath+Rob_dash.h" @implementation UIBezierPath (Rob_dash) - (instancetype)Rob_dashedPathWithPattern:(NSArray *)pattern phase:(CGFloat)phase { CGFloat lengths[pattern.count]; size_t i = 0; for (NSNumber *number in pattern) { lengths[i++] = number.doubleValue; } CGPathRef dashedCGPath = CGPathCreateCopyByDashingPath(self.CGPath, NULL, phase, lengths, pattern.count); UIBezierPath *dashedPath = [self.class bezierPathWithCGPath:dashedCGPath]; CGPathRelease(dashedCGPath); return dashedPath; } @end 

    Una vez que tenemos una ruta discontinua, debemos enumerar los elementos de la ruta (los comandos individuales como moveToPoint: addLineToPoint: etc.). La única forma de hacerlo es usando otra función de gráficos principales, CGPathApply . Escribamos otra categoría UIBezierPath que use bloques para hacerlo más fácil. Este es un poco más largo:

    UIBezierPath+Rob_forEach.h

     #import  typedef void (^Rob_UIBezierPath_moveBlock)(CGPoint destination); typedef void (^Rob_UIBezierPath_lineBlock)(CGPoint destination); typedef void (^Rob_UIBezierPath_quadBlock)(CGPoint control, CGPoint destination); typedef void (^Rob_UIBezierPath_cubicBlock)(CGPoint control0, CGPoint control1, CGPoint destination); typedef void (^Rob_UIBezierPath_closeBlock)(void); @interface UIBezierPath (Rob_forEach) - (void)Rob_forEachMove:(Rob_UIBezierPath_moveBlock)moveBlock line:(Rob_UIBezierPath_lineBlock)lineBlock quad:(Rob_UIBezierPath_quadBlock)quadBlock cubic:(Rob_UIBezierPath_cubicBlock)cubicBlock close:(Rob_UIBezierPath_closeBlock)closeBlock; @end 

    UIBezierPath+Rob_forEach.m

     #import "UIBezierPath+Rob_forEach.h" struct ForEachBlocks { __unsafe_unretained Rob_UIBezierPath_moveBlock moveBlock; __unsafe_unretained Rob_UIBezierPath_lineBlock lineBlock; __unsafe_unretained Rob_UIBezierPath_quadBlock quadBlock; __unsafe_unretained Rob_UIBezierPath_cubicBlock cubicBlock; __unsafe_unretained Rob_UIBezierPath_closeBlock closeBlock; }; static void applyBlockToPathElement(void *info, const CGPathElement *element) { struct ForEachBlocks *blocks = info; switch (element->type) { case kCGPathElementMoveToPoint: if (blocks->moveBlock != nil) { blocks->moveBlock(element->points[0]); } break; case kCGPathElementAddLineToPoint: if (blocks->lineBlock != nil) { blocks->lineBlock(element->points[0]); } break; case kCGPathElementAddQuadCurveToPoint: if (blocks->quadBlock) { blocks->quadBlock(element->points[0], element->points[1]); } break; case kCGPathElementAddCurveToPoint: if (blocks->cubicBlock) { blocks->cubicBlock(element->points[0], element->points[1], element->points[2]); } break; case kCGPathElementCloseSubpath: if (blocks->closeBlock) { blocks->closeBlock(); } break; } } @implementation UIBezierPath (Rob_forEach) - (void)Rob_forEachMove:(Rob_UIBezierPath_moveBlock)moveBlock line:(Rob_UIBezierPath_lineBlock)lineBlock quad:(Rob_UIBezierPath_quadBlock)quadBlock cubic:(Rob_UIBezierPath_cubicBlock)cubicBlock close:(Rob_UIBezierPath_closeBlock)closeBlock { struct ForEachBlocks blocks = { .moveBlock = moveBlock, .lineBlock = lineBlock, .quadBlock = quadBlock, .cubicBlock = cubicBlock, .closeBlock = closeBlock }; CGPathApply(self.CGPath, &blocks, applyBlockToPathElement); } @end 

    Bien, ahora queremos usar estas dos categorías juntas para marcar la ruta, luego caminar a lo largo de los guiones y emitir el punto al final de cada guión. Tenga en cuenta que un guión puede consistir en múltiples segmentos de línea / curva contiguos. Necesitamos mirar para mover comandos para saber cuándo termina un guion. Además, para dibujar cada flecha en el ángulo correcto, necesitamos saber la tangente de la curva en cada punto, por lo que también lo calcularemos como un vector unitario. En el caso de un segmento de línea recta, el vector de tangente es paralelo al segmento de línea. En el caso de las curvas, el punto de control inmediatamente anterior al punto final de la curva determina la tangente en el punto final.

    UIBezierPath+Rob_points.h

     #import  @interface UIBezierPath (Rob_points) - (void)Rob_forEachPointAtInterval:(CGFloat)interval perform:(void (^)(CGPoint point, CGVector vector))block; @end 

    UIBezierPath+Rob_points.m

     #import "UIBezierPath+Rob_points.h" #import "UIBezierPath+Rob_dash.h" #import "UIBezierPath+Rob_forEach.h" #import  static CGVector vectorFromPointToPoint(CGPoint tail, CGPoint head) { CGFloat length = hypot(head.x - tail.x, head.y - tail.y); return CGVectorMake((head.x - tail.x) / length, (head.y - tail.y) / length); } @implementation UIBezierPath (Rob_points) - (void)Rob_forEachPointAtInterval:(CGFloat)interval perform:(void (^)(CGPoint, CGVector))block { UIBezierPath *dashedPath = [self Rob_dashedPathWithPattern:@[ @(interval * 0.5), @(interval * 0.5) ] phase:0]; __block BOOL hasPendingSegment = NO; __block CGPoint pendingControlPoint; __block CGPoint pendingPoint; [dashedPath Rob_forEachMove:^(CGPoint destination) { if (hasPendingSegment) { block(pendingPoint, vectorFromPointToPoint(pendingControlPoint, pendingPoint)); hasPendingSegment = NO; } pendingPoint = destination; } line:^(CGPoint destination) { pendingControlPoint = pendingPoint; pendingPoint = destination; hasPendingSegment = YES; } quad:^(CGPoint control, CGPoint destination) { pendingControlPoint = control; pendingPoint = destination; hasPendingSegment = YES; } cubic:^(CGPoint control0, CGPoint control1, CGPoint destination) { pendingControlPoint = control1; pendingPoint = destination; hasPendingSegment = YES; } close:nil]; if (hasPendingSegment) { block(pendingPoint, vectorFromPointToPoint(pendingControlPoint, pendingPoint)); } } @end 

    Ahora podemos encontrar puntos a lo largo de una ruta y el vector tangente de la unidad en cada punto. Hagamos una vista personalizada que use esta habilidad en drawRect: :

    ArrowView.h

     #import  @interface ArrowView : UIView @property (nonatomic) CGFloat interval; @end 

    ArrowView.m

     #import "ArrowView.h" #import "UIBezierPath+Rob_figureEight.h" #import "UIBezierPath+Rob_points.h" @implementation ArrowView - (void)setInterval:(CGFloat)interval { _interval = interval; [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { UIImage *arrow = [UIImage imageNamed:@"right233.png"]; UIBezierPath *path = [UIBezierPath Rob_figureEightInRect:CGRectInset(self.bounds, 40, 40)]; // [path stroke]; [path Rob_forEachPointAtInterval:self.interval perform:^(CGPoint point, CGVector vector) { CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextSaveGState(gc); { CGContextTranslateCTM(gc, point.x, point.y); CGContextConcatCTM(gc, CGAffineTransformMake(vector.dx, vector.dy, -vector.dy, vector.dx, 0, 0)); CGContextTranslateCTM(gc, -0.5 * arrow.size.width, -0.5 * arrow.size.height); // UIRectFrame((CGRect){ CGPointZero, arrow.size }); [arrow drawAtPoint:CGPointZero]; } CGContextRestoreGState(gc); }]; } @end 

    Eso es todo, si quieres dibujar imágenes de flechas a lo largo de un camino.

    Hay un pequeño bono en mi repository de aplicaciones de demostración. Si vuelves al primer commit, también implementé una solución diferente: una categoría que toma una ruta y la “flecha”, poniendo una punta de flecha al final de cada subpaso. Si combinas eso con apresuramiento (como lo hice en esa versión del proyecto), obtienes flechas a lo largo del camino. Pero terminó no tan bonito como usar imágenes de flecha.