Drawアプリで、端に矢印のある線を描く必要があります。私は三角法が苦手なので、この問題が解けません。
ユーザーは画面に指を置き、任意の方向に線を引きます。したがって、矢印は行末に表示されるはずです。
Drawアプリで、端に矢印のある線を描く必要があります。私は三角法が苦手なので、この問題が解けません。
ユーザーは画面に指を置き、任意の方向に線を引きます。したがって、矢印は行末に表示されるはずです。
これは楽しい小さな問題です。まず第一に、矢印を描く方法はたくさんあります。曲線または直線の側面があります。非常に簡単な方法を選んで、必要な測定値にラベルを付けましょう。
始点、終点、尾の幅、頭の幅、頭の長さを取り、矢印の形を描くパスを返す関数を書きたいと思います。dqd_arrowhead
このメソッドを に追加するという名前のカテゴリを作成しましょう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
矢印のパスには 7 つのコーナーがあるため、その定数に名前を付けて実装を開始しましょう。
// 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、簡単な部分は完了です。では、パス上のこれら 7 点の座標をどのように見つけるのでしょうか? 矢印が X 軸に沿って配置されていると、ポイントを見つけるのがはるかに簡単になります。
軸に沿った矢印のポイント座標を計算するのは非常に簡単ですが、そのためには矢印の全長が必要になります。hypotf
標準ライブラリの関数を使用します。
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
実際に 7 つのポイントを計算するヘルパー メソッドを呼び出します。
CGPoint points[kArrowPointCount];
[self dqd_getAxisAlignedArrowPoints:points
forLength:length
tailWidth:tailWidth
headWidth:headWidth
headLength:headLength];
しかし、一般に、軸に沿った矢印を作成しようとしていないため、これらのポイントを変換する必要があります。幸いなことに、Core Graphics はアフィン変換と呼ばれる一種の変換をサポートしており、これを使用すると、ポイントを回転および移動 (スライド) できます。別のヘルパー メソッドを呼び出して、軸に沿った矢印を要求された矢印に変換する変換を作成します。
CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint
endPoint:endPoint
length:length];
これで、軸に沿った矢印のポイントと、それを目的の矢印に変換する変換を使用して、Core Graphics パスを作成できます。
CGMutablePathRef cgPath = CGPathCreateMutable();
CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
CGPathCloseSubpath(cgPath);
最後に、 をラップしUIBezierPath
てCGPath
返すことができます:
UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
CGPathRelease(cgPath);
return uiPath;
}
ポイント座標を計算するヘルパー メソッドを次に示します。とても簡単です。必要に応じて、軸に沿った矢印の図を参照してください。
+ (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);
}
アフィン変換の計算はより複雑です。ここで三角法の出番です。atan2
とCGAffineTransformRotate
とCGAffineTransformTranslate
関数を使用して作成することもできますが、十分な三角法を覚えていれば、直接作成することもできます。ここで行っていることの詳細については、 Quartz 2D Programming Guideの「The Math Behind the Matrix」を参照してください。
+ (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
簡単にコピーアンドペーストできるように、すべてのコードを要点にまとめました。
このカテゴリを使用すると、簡単に矢印を描くことができます。
パスを生成しているだけなので、次の例のようにパスを塗りつぶしたり、ストロークしたりしないことを選択できます。
ただし、注意が必要です。このコードは、頭の幅を尾の幅よりも小さくした場合、または頭の長さを矢印の全長よりも大きくした場合に、ファンキーな結果が得られることを妨げません。
これは私の古い Objective-C コードの Swift バージョンです。Swift 3.2 以降のバージョンで動作するはずです。
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)
}
}
これをどのように呼び出すかの例を次に示します。
let arrow = UIBezierPath.arrow(from: CGPoint(x: 50, y: 100), to: CGPoint(x: 200, y: 50),
tailWidth: 10, headWidth: 25, headLength: 40)
//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
Swift 3.0 では、これを実現できます
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)
}
}