Created
June 24, 2013 16:54
-
-
Save briandw/5851592 to your computer and use it in GitHub Desktop.
rantlab 1 full arrow view
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// RLArrowView.m | |
// | |
// Created by brian on 6/20/13. | |
// Copyright (c) 2013 RantLab. All rights reserved. | |
// | |
#import "RLArrowView.h" | |
@implementation RLArrowView | |
- (void)awakeFromNib | |
{ | |
[self setLayer:[CALayer new]]; | |
[self setWantsLayer:YES]; | |
} | |
- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent | |
{ | |
return YES; | |
} | |
- (void)mouseDown:(NSEvent *)event | |
{ | |
if (arrow) | |
{ | |
[self removeArrow]; | |
} | |
CGPoint startPos = [self convertPoint:[event locationInWindow] fromView:nil]; | |
[self fireArrow:startPos]; | |
} | |
- (void)removeArrow | |
{ | |
if (arrow) | |
{ | |
[arrow removeFromSuperlayer]; | |
} | |
arrow = nil; | |
} | |
- (void)fireArrow:(CGPoint)target | |
{ | |
arrow = [CAShapeLayer layer]; | |
CGPoint middle = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); | |
CGPoint vector = CGPointMake(target.x-middle.x, target.y-middle.y); | |
CGFloat magnitude = sqrt(vector.x*vector.x+vector.y*vector.y); | |
CGPoint start = CGPointMake(middle.x+vector.x/magnitude*20, middle.y+vector.y/magnitude*20); | |
CGPathRef startPath = [RLArrowView createPathWithArrowFromPoint:middle | |
toPoint:start | |
tailWidth:2.0 | |
headWidth:10.0 | |
headLength:20.0 | |
wiggle:0]; | |
arrow.path = startPath; | |
arrow.fillColor = [NSColor redColor].CGColor; | |
arrow.lineWidth = 1; | |
arrow.fillRule = kCAFillRuleNonZero; | |
[self.layer addSublayer:arrow]; | |
CGPathRef endPath = [RLArrowView createPathWithArrowFromPoint:middle | |
toPoint:target | |
tailWidth:2.0 | |
headWidth:10.0 | |
headLength:20.0 | |
wiggle:0]; | |
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){ | |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"]; | |
animation.duration = 0.15; | |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; | |
animation.repeatCount = 0; | |
animation.autoreverses = NO; | |
animation.fromValue = (__bridge id)startPath; | |
animation.toValue = (__bridge id)endPath; | |
[arrow addAnimation:animation forKey:@"animatePath"]; | |
arrow.path = endPath; | |
} completionHandler:^{ | |
[self wiggleFromPath:endPath target:target amount:1.0]; | |
}]; | |
} | |
- (void)wiggleFromPath:(CGPathRef)path target:(CGPoint)target amount:(CGFloat)amount | |
{ | |
CGPoint middle = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); | |
CGPathRef endPath = [RLArrowView createPathWithArrowFromPoint:middle | |
toPoint:target | |
tailWidth:2.0 | |
headWidth:10.0 | |
headLength:20.0 | |
wiggle:amount]; | |
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){ | |
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"]; | |
animation.duration = 0.1; | |
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; | |
animation.repeatCount = 0; | |
animation.autoreverses = NO; | |
animation.fromValue = (__bridge id)path; | |
animation.toValue = (__bridge id)endPath; | |
[arrow addAnimation:animation forKey:@"animatePath"]; | |
arrow.path = endPath; | |
} completionHandler:^{ | |
if (fabs(amount)>0.02) [self wiggleFromPath:endPath target:target amount:amount*-0.7]; | |
}]; | |
} | |
/************* Arrow drawing stuff ****************/ | |
#define kArrowPointCount 9 | |
+ (CGPathRef )createPathWithArrowFromPoint:(CGPoint)startPoint | |
toPoint:(CGPoint)endPoint | |
tailWidth:(CGFloat)tailWidth | |
headWidth:(CGFloat)headWidth | |
headLength:(CGFloat)headLength | |
wiggle:(CGFloat)wiggle | |
{ | |
CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y); | |
CGPoint points[kArrowPointCount]; | |
CGFloat deflection = wiggle*length/4.0; | |
//CGFloat headRotation = atan(deflection/length); | |
CGFloat tailLength = length - headLength; | |
CGFloat halfTail = tailWidth/2.0; | |
points[0] = CGPointMake(0, halfTail); | |
points[1] = CGPointMake(tailLength/2, deflection + halfTail); | |
points[2] = CGPointMake(tailLength, halfTail); | |
points[3] = CGPointMake(tailLength, headWidth / 2); | |
points[4] = CGPointMake(length, 0); | |
points[5] = CGPointMake(tailLength, -headWidth / 2); | |
points[6] = CGPointMake(tailLength, -halfTail); | |
points[7] = CGPointMake(tailLength/2, -halfTail + deflection); | |
points[8] = CGPointMake(0, -halfTail); | |
CGAffineTransform transform = [self transformForStartPoint:startPoint | |
endPoint:endPoint | |
length:length]; | |
CGMutablePathRef cgPath = CGPathCreateMutable(); | |
CGPathMoveToPoint(cgPath, &transform, points[0].x, points[0].y); | |
CGPathAddQuadCurveToPoint(cgPath, &transform, points[1].x, points[1].y, points[2].x, points[2].y); | |
CGPathAddLineToPoint(cgPath, &transform, points[3].x, points[3].y); | |
CGPathAddLineToPoint(cgPath, &transform, points[4].x, points[4].y); | |
CGPathAddLineToPoint(cgPath, &transform, points[5].x, points[5].y); | |
CGPathAddLineToPoint(cgPath, &transform, points[6].x, points[6].y); | |
CGPathAddQuadCurveToPoint(cgPath, &transform, points[7].x, points[7].y, points[8].x, points[8].y); | |
CGPathCloseSubpath(cgPath); | |
return cgPath; | |
} | |
+ (CGAffineTransform)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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment