objective-c - cómo - como dibujar una flecha de minecraft
¿Cómo puedo dibujar una flecha usando Core Graphics? (4)
ACTUALIZAR
He publicado una versión Swift de esta respuesta por separado.
ORIGINAL
Este es un pequeño problema divertido. En primer lugar, hay muchas formas de dibujar flechas, con lados curvos o rectos. Vamos a elegir una manera muy simple y etiquetar las medidas que necesitaremos:
Queremos escribir una función que tome el punto de inicio, el punto final, el ancho de la cola, el ancho de la cabeza y la longitud de la cabeza, y devuelve una ruta que delinee la forma de la flecha. Vamos a crear una categoría llamada dqd_arrowhead
para agregar este método a UIBezierPath
:
// UIBezierPath+dqd_arrowhead.h
@interface UIBezierPath (dqd_arrowhead)
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength;
@end
Como hay siete esquinas en el camino de la flecha, comencemos nuestra implementación nombrando esa constante:
// UIBezierPath+dqd_arrowhead.m
#import "UIBezierPath+dqd_arrowhead.h"
#define kArrowPointCount 7
@implementation UIBezierPath (dqd_arrowhead)
+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength {
OK, la parte fácil ya está hecha. Ahora, ¿cómo encontramos las coordenadas de esos siete puntos en el camino? Es mucho más fácil encontrar los puntos si la flecha está alineada a lo largo del eje X:
Es bastante fácil calcular las coordenadas de puntos en una flecha alineada con el eje, pero necesitaremos la longitud total de la flecha para hacerlo. Usaremos la función hypotf
de la biblioteca estándar:
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
Llamaremos a un método de ayuda para calcular realmente los siete puntos:
CGPoint points[kArrowPointCount];
[self dqd_getAxisAlignedArrowPoints:points
forLength:length
tailWidth:tailWidth
headWidth:headWidth
headLength:headLength];
Pero tenemos que transformar esos puntos, porque en general no estamos tratando de crear una flecha alineada con el eje. Afortunadamente, Core Graphics admite un tipo de transformación llamada transformación afín , que nos permite rotar y traducir (deslizar) puntos. Llamaremos a otro método auxiliar para crear la transformación que convierte nuestra flecha alineada con el eje en la flecha que se nos pidió:
CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint
endPoint:endPoint
length:length];
Ahora podemos crear una ruta de gráficos centrales usando los puntos de la flecha alineada al eje y la transformación que la convierte en la flecha que queremos:
CGMutablePathRef cgPath = CGPathCreateMutable();
CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
CGPathCloseSubpath(cgPath);
Finalmente, podemos envolver un UIBezierPath
alrededor de CGPath
y devolverlo:
UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
CGPathRelease(cgPath);
return uiPath;
}
Aquí está el método de ayuda que calcula las coordenadas del punto. Es bastante simple. Consulte de nuevo el diagrama de la flecha alineada al eje si es necesario.
+ (void)dqd_getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points
forLength:(CGFloat)length
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength {
CGFloat tailLength = length - headLength;
points[0] = CGPointMake(0, tailWidth / 2);
points[1] = CGPointMake(tailLength, tailWidth / 2);
points[2] = CGPointMake(tailLength, headWidth / 2);
points[3] = CGPointMake(length, 0);
points[4] = CGPointMake(tailLength, -headWidth / 2);
points[5] = CGPointMake(tailLength, -tailWidth / 2);
points[6] = CGPointMake(0, -tailWidth / 2);
}
Computar la transformación afín es más complicado. Aquí es donde entra la trigonometría. Puede usar atan2
y las funciones CGAffineTransformRotate
y CGAffineTransformTranslate
para crearlo, pero si recuerda suficiente trigonometría, puede crearla directamente. Consulte "Las matemáticas detrás de las matrices" en la Guía de programación 2D de Quartz para obtener más información sobre lo que estoy haciendo aquí:
+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint
endPoint:(CGPoint)endPoint
length:(CGFloat)length {
CGFloat cosine = (endPoint.x - startPoint.x) / length;
CGFloat sine = (endPoint.y - startPoint.y) / length;
return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
}
@end
He puesto todo el código en una esencia para copiar y pegar fácilmente .
Con esta categoría, puedes dibujar flechas fácilmente:
Como solo está generando una ruta, puede elegir no llenarla, o no acariciarla como en este ejemplo:
Sin embargo, tienes que ser cuidadoso. Este código no le impide obtener resultados originales si hace que el ancho de la cabeza sea inferior al ancho de la cola, o si hace que la longitud de la cabeza sea mayor que la longitud total de la flecha:
Necesito dibujar una línea con una flecha en su extremo en mi aplicación Draw. No soy bueno en trigonometría, así que no puedo resolver este problema.
El usuario coloca su dedo en la pantalla y dibuja la línea en cualquier dirección. Entonces, la flecha debería aparecer en el extremo de la línea.
Aquí hay una versión Swift de mi antiguo código Objective-C. Debería funcionar en Swift 3.2 y versiones posteriores.
extension UIBezierPath {
static func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> UIBezierPath {
let length = hypot(end.x - start.x, end.y - start.y)
let tailLength = length - headLength
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
let points: [CGPoint] = [
p(0, tailWidth / 2),
p(tailLength, tailWidth / 2),
p(tailLength, headWidth / 2),
p(length, 0),
p(tailLength, -headWidth / 2),
p(tailLength, -tailWidth / 2),
p(0, -tailWidth / 2)
]
let cosine = (end.x - start.x) / length
let sine = (end.y - start.y) / length
let transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
let path = CGMutablePath()
path.addLines(between: points, transform: transform)
path.closeSubpath()
return self.init(cgPath: path)
}
}
Aquí hay un ejemplo de cómo lo llamarías:
let arrow = UIBezierPath.arrow(from: CGPoint(x: 50, y: 100), to: CGPoint(x: 200, y: 50),
tailWidth: 10, headWidth: 25, headLength: 40)
En Swift 3.0 puedes lograr esto con
extension UIBezierPath {
class func arrow(from start: CGPoint, to end: CGPoint, tailWidth: CGFloat, headWidth: CGFloat, headLength: CGFloat) -> Self {
let length = hypot(end.x - start.x, end.y - start.y)
let tailLength = length - headLength
func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) }
var points: [CGPoint] = [
p(0, tailWidth / 2),
p(tailLength, tailWidth / 2),
p(tailLength, headWidth / 2),
p(length, 0),
p(tailLength, -headWidth / 2),
p(tailLength, -tailWidth / 2),
p(0, -tailWidth / 2)
]
let cosine = (end.x - start.x) / length
let sine = (end.y - start.y) / length
var transform = CGAffineTransform(a: cosine, b: sine, c: -sine, d: cosine, tx: start.x, ty: start.y)
let path = CGMutablePath()
path.addLines(between: points, transform: transform)
path.closeSubpath()
return self.init(cgPath: path)
}
}
//This is the integration into the view of the previous exemple
//Attach the following class to your view in the xib file
#import <UIKit/UIKit.h>
@interface Arrow : UIView
@end
#import "Arrow.h"
#import "UIBezierPath+dqd_arrowhead.h"
@implementation Arrow
{
CGPoint startPoint;
CGPoint endPoint;
CGFloat tailWidth;
CGFloat headWidth;
CGFloat headLength;
UIBezierPath *path;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
[self setMultipleTouchEnabled:NO];
[self setBackgroundColor:[UIColor whiteColor]];
}
return self;
}
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setStroke];
tailWidth = 4;
headWidth = 8;
headLength = 8;
path = [UIBezierPath dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint
toPoint:(CGPoint)endPoint
tailWidth:(CGFloat)tailWidth
headWidth:(CGFloat)headWidth
headLength:(CGFloat)headLength];
[path setLineWidth:2.0];
[path stroke];
}
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
UITouch* touchPoint = [touches anyObject];
startPoint = [touchPoint locationInView:self];
endPoint = [touchPoint locationInView:self];
[self setNeedsDisplay];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
endPoint=[touch locationInView:self];
[self setNeedsDisplay];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
endPoint = [touch locationInView:self];
[self setNeedsDisplay];
}
@end