Reputation: 5732
I'd like to create an NSBezierPath that represents a "curved" or "bloated" square like this:
I'm struggling to come up with the correct math to get this shape exactly right. I've looked all over the internet but Googling for this topic turns up mostly "here's how you draw rounded corners", which is not what I need.
Can anyone point me to a formula I can use to place the control points for these curves? Thanks!
Upvotes: 1
Views: 1734
Reputation: 5732
Okay; after much trial and error I've got something that works. The code below will draw a shape like this one. (This image uses the following values for the #define statements in the code):
#define SIDE_DEFLECTION 18.0f
#define CORNER_RAD 18.0f
#define KAPPA 0.55238f
#define CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION .10
#define SIDE_CENTER_CP_OFFSET 0.60
These values, given a 500x500 pixel view, will draw this shape:
In my case, I'm drawing this shape inside an NSButtonCell subclass. Because I know that this cell's frame will never resize in my app, I can do some optimizations. Specifically, I store the NSBezierPath as an iVar so I don't have to recreate it each time through -drawImage... Additionally, I store an NSShadow and NSColor as iVars so those don't have to be recreated either.
If you're going to draw this shape in a view that resizes, you'll need to tweak the code a bit. The values of the #define statements need to be manually adjusted right now if the square changes sizes drastically (otherwise, the corners don't look quiiiiite right. There's little indentations where the side curves transition to the rounded-corner curves, etc.)
The code below is configured to draw the same shape in a 52x52 pixel square. The general approach is to set the "side deflection" and "corner radius" you want, then adjust the two "offset" define statements (which are percentages) until the corners look perfect. The value of "kappa" must never change --- that one is taken from a mathematical paper I found.
Here's the optimized drawing code:
#import "LPProjectIconButtonCell.h"
#define SIDE_DEFLECTION 1.0f
#define CORNER_RAD 4.0f
// Distance (%) control points should be from curve start/end to form perfectly circular rounded corners
#define KAPPA 0.55238f
// Percentage offset (from perfectly circular rounded corner location) that the corner control points use to
// compensate for the fact that our sides are rounded. Without this, we get a rough transition between the
// curve of the side and the start of the corner curve
#define CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION .85
// As the curve approaches each side-center point, this is the percentage of the distance between the side endpoints
// and the side centerpoint where the control point for approaching the centerpoint is located. You are not expected
// to understand the preceeding sentence.
#define SIDE_CENTER_CP_OFFSET 0.60
@implementation LPProjectIconButtonCell
- (void) awakeFromNib
{
_shadow = [[NSShadow alloc] init];
[_shadow setShadowBlurRadius:1.0f];
[_shadow setShadowColor:[NSColor colorWithCalibratedWhite:1.0f alpha:0.2f]];
[_shadow setShadowOffset:NSMakeSize(0.0f, -1.0f)];
_borderColor = [[NSColor colorWithCalibratedWhite:0.13 alpha:1.0f] retain];
}
- (void) dealloc
{
[_path release];
_path = nil;
[_shadow release];
_shadow = nil;
[_borderColor release];
_borderColor = nil;
[super dealloc];
}
- (void) drawImage:(NSImage *)image withFrame:(NSRect)frame inView:(NSView *)controlView
{
NSGraphicsContext *currentContext = [NSGraphicsContext currentContext];
// The path never changes because this view never resizes. So we'll save it to be efficient
if (!_path)
{
NSRect rect = NSInsetRect(frame, 2.0f, 2.0f);
// Create the primary points -- 3 per side
NSPoint TCenter = NSMakePoint(rect.size.width/2.0f, rect.origin.y);
NSPoint TLeft = NSMakePoint(rect.origin.x + CORNER_RAD + SIDE_DEFLECTION, rect.origin.y + SIDE_DEFLECTION);
NSPoint TRight = NSMakePoint(rect.origin.x + rect.size.width - (CORNER_RAD + SIDE_DEFLECTION), rect.origin.y + SIDE_DEFLECTION);
NSPoint LTop = NSMakePoint(rect.origin.x + SIDE_DEFLECTION, rect.origin.y + CORNER_RAD + SIDE_DEFLECTION);
NSPoint LCenter = NSMakePoint(rect.origin.x, rect.size.height/2.0f);
NSPoint LBottom = NSMakePoint(rect.origin.x + SIDE_DEFLECTION, rect.origin.y + rect.size.height - CORNER_RAD - SIDE_DEFLECTION);
NSPoint BLeft = NSMakePoint(TLeft.x, rect.origin.y + rect.size.height - SIDE_DEFLECTION);
NSPoint BCenter = NSMakePoint(TCenter.x, rect.origin.y + rect.size.height);
NSPoint BRight = NSMakePoint(TRight.x, BLeft.y);
NSPoint RTop = NSMakePoint(rect.origin.x + rect.size.width - SIDE_DEFLECTION, LTop.y);
NSPoint RCenter = NSMakePoint(rect.origin.x + rect.size.width, LCenter.y);
NSPoint RBottom = NSMakePoint(RTop.x, LBottom.y);
// Create corner control points for rounded corners
// We don't want them to be perfectly circular, because our sides are curved. So we adjust them slightly to compensate for that.
NSPoint CP_TLeft = NSMakePoint(TLeft.x - (TLeft.x - LTop.x) * KAPPA, TLeft.y + SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION);
NSPoint CP_LTop = NSMakePoint(LTop.x + SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION, LTop.y - (LTop.y - TLeft.y) * KAPPA);
NSPoint CP_LBottom = NSMakePoint(LBottom.x + SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION, LBottom.y + (BLeft.y - LBottom.y) * KAPPA);
NSPoint CP_BLeft = NSMakePoint(BLeft.x - (BLeft.x - LBottom.x) * KAPPA, BLeft.y - SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION);
NSPoint CP_BRight = NSMakePoint(BRight.x + (RBottom.x - BRight.x) * KAPPA, BRight.y - SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION);
NSPoint CP_RBottom = NSMakePoint(RBottom.x - SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION, RBottom.y + (BRight.y - RBottom.y) * KAPPA);
NSPoint CP_RTop = NSMakePoint(RTop.x - SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION, RTop.y - (RTop.y - TRight.y) * KAPPA);
NSPoint CP_TRight = NSMakePoint(TRight.x + (RTop.x - TRight.x) * KAPPA, TRight.y + SIDE_DEFLECTION * CORNER_CP_OFFSET_FOR_SIDE_DEFLECTION);
// Create control points for the rounded sides. (The "duplicate" control points are here in case I ever tweak this in the future.)
NSPoint CP_DepartingTCenterForTLeft = NSMakePoint(TCenter.x - (TCenter.x - TLeft.x) * SIDE_CENTER_CP_OFFSET, TCenter.y);
NSPoint CP_ApproachingTLeft = TLeft;
NSPoint CP_DepartingLTopForLCenter = LTop;
NSPoint CP_ApproachingLCenterFromLTop = NSMakePoint(LCenter.x, LCenter.y - (LCenter.y - LTop.y) * SIDE_CENTER_CP_OFFSET);
NSPoint CP_DepartingLCenterForLBottom = NSMakePoint(LCenter.x, LCenter.y + (LBottom.y - LCenter.y) * SIDE_CENTER_CP_OFFSET);
NSPoint CP_ApproachingLBottom = LBottom;
NSPoint CP_DepartingBLeftForBCenter = BLeft;
NSPoint CP_ApproachingBCenter = NSMakePoint(BCenter.x - (BCenter.x - BLeft.x) * SIDE_CENTER_CP_OFFSET, BCenter.y);
NSPoint CP_DepartingBCenterForBRight = NSMakePoint(BCenter.x + (BRight.x - BCenter.x) * SIDE_CENTER_CP_OFFSET, BCenter.y);
NSPoint CP_ApproachingBRight = BRight;
NSPoint CP_DepartingRBottomForRCenter = RBottom;
NSPoint CP_ApproachingRCenterFromRBottom = NSMakePoint(RCenter.x, RCenter.y + (RBottom.y - RCenter.y) * SIDE_CENTER_CP_OFFSET);
NSPoint CP_DepartingRCenterForRTop = NSMakePoint(RCenter.x, RCenter.y - (RCenter.y - RTop.y) * SIDE_CENTER_CP_OFFSET);
NSPoint CP_ApproachingRTopFromRCenter = RTop;
NSPoint CP_DepartingTRightForTCenter = TRight;
NSPoint CP_ApproachingTCenterFromTRight = NSMakePoint(TCenter.x + (TRight.x - TCenter.x) * SIDE_CENTER_CP_OFFSET, TCenter.y);
// Draw the bloody square
NSBezierPath *p = [[NSBezierPath alloc] init];
[p moveToPoint:TCenter];
[p curveToPoint:TLeft controlPoint1:CP_DepartingTCenterForTLeft controlPoint2:CP_ApproachingTLeft];
[p curveToPoint:LTop controlPoint1:CP_TLeft controlPoint2:CP_LTop];
[p curveToPoint:LCenter controlPoint1:CP_DepartingLTopForLCenter controlPoint2:CP_ApproachingLCenterFromLTop];
[p curveToPoint:LBottom controlPoint1:CP_DepartingLCenterForLBottom controlPoint2:CP_ApproachingLBottom];
[p curveToPoint:BLeft controlPoint1:CP_LBottom controlPoint2:CP_BLeft];
[p curveToPoint:BCenter controlPoint1:CP_DepartingBLeftForBCenter controlPoint2:CP_ApproachingBCenter];
[p curveToPoint:BRight controlPoint1:CP_DepartingBCenterForBRight controlPoint2:CP_ApproachingBRight];
[p curveToPoint:RBottom controlPoint1:CP_BRight controlPoint2:CP_RBottom];
[p curveToPoint:RCenter controlPoint1:CP_DepartingRBottomForRCenter controlPoint2:CP_ApproachingRCenterFromRBottom];
[p curveToPoint:RTop controlPoint1:CP_DepartingRCenterForRTop controlPoint2:CP_ApproachingRTopFromRCenter];
[p curveToPoint:TRight controlPoint1:CP_RTop controlPoint2:CP_TRight];
[p curveToPoint:TCenter controlPoint1:CP_DepartingTRightForTCenter controlPoint2:CP_ApproachingTCenterFromTRight];
[p closePath];
_path = p;
}
// We want a slightly white drop shadow on the stroke and fill, giving our square some sense of depth.
[_shadow set];
[[NSColor blackColor] set];
[_path fill];
// Clip to the bezier path and draw a fill image inside of it.
[currentContext saveGraphicsState];
[_path addClip];
[image drawInRect:frame fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0f respectFlipped:YES hints:nil];
if (self.isHighlighted)
{
// If we're clicked, draw a 50% black overlay to show that
NSColor *overColor = [NSColor colorWithCalibratedWhite:0.0 alpha:0.5];
[overColor set];
[_path fill];
}
[currentContext restoreGraphicsState];
// Stroke the square to create a nice border with a drop shadow at top and bottom.
[_borderColor set];
[_path stroke];
}
@end
Upvotes: 4