NSBezierPath line thins out - objective-c

I'm trying to draw a power function curve in a 300x300 pixel rectangle using NSBezierPath as follows:
-(void)drawPowerCurve:(float)power points:(int)numbPoints{
NSBezierPath * path = [NSBezierPath bezierPath];
[path setLineWidth: 1.0];
[[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:1.0 alpha:1.0] set];
NSPoint borderOrigin = {35.5,15.5};
NSPoint endPoint;
[path moveToPoint:borderOrigin];
for(int i = 0; i < numbPoints; i++){
endPoint.x = borderOrigin.x + (300.0/numbPoints)*(i+1);
endPoint.y = borderOrigin.y + 300.0*pow(((i+1)/(float)numbPoints), power);
[path lineToPoint:endPoint];
[path stroke];
[path moveToPoint:endPoint];
}
}
However, the curve thins out at the top end compared to the bottom end.
For example power = 1.8 and numbPoints = 50.
Also the curve doesn't look as smooth as for example curves shown in Apple's ColorSync Utility. Of course I don't know how they are drawing the curves in ColorSync. Any ideas on how to improve the look of these curves (particularly getting rid of the thining out).
Edit -- Here is a screenshot:

Move the stroke out of the loop so you only draw the curve once instead of numbPoints times as it grows. Also remove the redundant moveToPoint in the loop, lineToPoint leaves the current point at the end of the added segment.

Related

SceneKit – Extrude profile along Circle

I'am struggling to find any information about bending or extruding along path some bezier profile.
Let's say I have some path:
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointZero];
[path addLineToPoint:CGPointMake(5, 0)];
[path addLineToPoint:CGPointMake(5, 0.1)];
[path addLineToPoint:CGPointMake(0, 0.1)];
[path closePath];
(for simplicity its a box, but that profile can be something more complicated, like 3 rounded corners and some curve)
and I can extrude it to 3d object with SCNShape, like this:
SCNShape *ringShape = [SCNShape shapeWithPath: path extrusionDepth: 1];
ringShape.firstMaterial.doubleSided = YES;
SCNNode *node = [SCNNode nodeWithGeometry: pieShape];
but now I would love to bend this "box" shape to 360 degrees to form a ring shape. Is this even possible without loading external geometry?
Maybe there are some other option to make that geometry programmatically, maybe some SCNTube and then chamfer its edges with path profile ? Or some shader ?
For visual helper, this is what I mean, profile ~ 360 extrude = ring
Any pointers would be lovely, as I can't find much information about SceneKit.

CAShapeLayer keeps closing path, I want the path open

I'm using CA to draw line segments from an array of points. For some reason, although I did not close the NSBezierPath, CAShapeLayer results in a closed shape. The following are my codes. Do anyone else have this problem?
// mappedPoints is an array of CGPoints
NSBezierPath *path = [NSBezierPath bezierPath];
[path appendBezierPathWithPoints:mappedPoints count:numPoints];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [color CGColor];
shapeLayer.lineWidth = lineWidth;
shapeLayer.fillColor = nil;
[self.layer addSublayer:shapeLayer];
I'm allowing myself to guess here that the [path CGPath] call is what's closing that path for you, as that's exactly what happened to me.
To give a bit more context - Apple's Creating a CGPathRef From an NSBezierPath Object sample code, which creates an NSBezierPath category method like CGPath above, has this in it:
// Be sure the path is closed or Quartz may not do valid hit detection.
if (!didClosePath)
CGPathCloseSubpath(path);
The reasoning behind that (from the same source):
Quartz requires paths to be closed in order to do hit detection on the
path’s fill area
If you're not interested in fill-area hit detection, it's safe to skip over that path-closing part and you'd get the expected behavior.

Translating a view with a path (ObjectiveC, Cocoa)

The short story is that I would like the bounds (I think I mean bounds instead of frame) of a NSBezierPath to fill a view. Something like this:
To generate the above image I scaled/translated each point in my created path using the information from Covert latitude/longitude point to a pixels (x,y) on mercator projection. The problem is that this isn't scalable (I will be adding many more paths) and I want to easily add pan/zoom functionality to my view. Additionally, I want the stroke to remain the same regardless of scale (i.e. no fat boundaries when I zoom).
I think I want to generate a reusable path in some arbitrary reference frame (e.g. longitude and modified latitude) instead of generating a new path every time the window changes. Then I can translate/scale my view's coordinate system to fill the view with the path.
So I used Apple's geometry guide to to modify the view's frame. I got the translation right but scaling failed.
[self setBoundsOrigin:self.path.bounds.origin];
[self scaleUnitSquareToSize:NSMakeSize(1.5, 1.5)];
Then I tried a coordinate system transformation in my drawRect: method only to end up with a similar result.
NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:(-self.path.bounds.origin.x) yBy:(-self.path.bounds.origin.y)];
[xform scaleXBy:1.5 yBy:1.5];
[xform concat];
Finally I tried manually setting the view bounds in drawRect: but the result was ugly and very slow!
I know I can also transform the NSBezierPath object and I think that would work, but I'd rather transform the view once instead of looping through and transforming each path every update. I think there's about three lines of code I'm missing that will do exactly what I'm looking for.
Edit:
Here's the drawRect: method I'm using:
- (void)drawRect:(NSRect)dirtyRect
{
// NSAffineTransform* xform = [NSAffineTransform transform];
// [xform translateXBy:-self.path.bounds.origin.x yBy:-self.path.bounds.origin.y];
// [xform scaleXBy:1.5 yBy:1.5];
// [xform concat];
[self drawBoundaries];
NSRect bounds = [self bounds];
[[NSColor blackColor] set];
[NSBezierPath fillRect:bounds];
// Draw the path in white
[[NSColor whiteColor] set];
[self.path stroke];
[[NSColor redColor] set];
[NSBezierPath strokeRect:self.path.bounds];
NSLog(#"path origin %f x %f",self.path.bounds.origin.x, self.path.bounds.origin.y);
NSLog(#"path bounds %f x %f",self.path.bounds.size.width, self.path.bounds.size.height);
}
I was able to get it to work using two transformations. I was trying to avoid this to reduce complexity and computation when I have many paths to transform and a window that zooms/pans.
- (void)transformPath:(NSBezierPath *)path
{
NSAffineTransform *translateTransform = [NSAffineTransform transform];
NSAffineTransform *scaleTransform = [NSAffineTransform transform];
[translateTransform translateXBy:(-self.path.bounds.origin.x)
yBy:(-self.path.bounds.origin.y)];
float scale = MIN(self.bounds.size.width / self.path.bounds.size.width,
self.bounds.size.height / self.path.bounds.size.height);
[scaleTransform scaleBy:scale];
[path transformUsingAffineTransform: translateTransform];
[path transformUsingAffineTransform: scaleTransform];
}

Custom NSView draws over controls on top of it

I have an NSView with a custom subclass that draws a grid of rounded rectangles inside it. This NSView was placed with interface builder and on top of it I have some NSButtons.
The problem is that sometimes when the view is re-drawn (ie, when i click a button on top of it) then it re-draws over some of the buttons that are meant to stay on top. When this happens only the smaller rounded rects appear over the buttons though, not the background one that is drawn before the loop.
Here is the code form drawRect:
[NSGraphicsContext saveGraphicsState];
NSBezierPath *path = [NSBezierPath bezierPathWithRect:self.bounds];
[[NSColor grayColor] set];
[path fill];
[NSGraphicsContext restoreGraphicsState];
for( int r = 0; r < 15; r++ ){
for( int c = 0; c < 15; c++ ) {
[NSGraphicsContext saveGraphicsState];
// Draw shape
NSRect rect = NSMakeRect(20 * c, 20 * r, 15, 15);
NSBezierPath *roundedRect = [NSBezierPath bezierPathWithRoundedRect: rect xRadius:1 yRadius:1];
[roundedRect setClip];
// Fill
[[NSColor colorWithCalibratedHue:0 saturation:0 brightness:0.3 alpha:1] set];
[roundedRect fill];
// Stroke
[[NSColor colorWithCalibratedHue:0 saturation:0 brightness:0.5 alpha:1] set];
[roundedRect setLineWidth:2.0];
[roundedRect stroke];
[NSGraphicsContext restoreGraphicsState];
}
}
Here's a screenshot:
Update: Simplified the code, added a screenshot.
the mac has issues with overlapping sibling views. it didn't work before.... 10.6 and it still doesnt work quite often.
use a proper superview / subview hierachy
OK I just managed to solve this by removing the setClip and finding a different way to draw the inner stroke.
I'm sure it's possible to solve this while still using setClip but this solution worked fine for me this time.

Making a Grid in an NSView

I currently have an NSView that draws a grid pattern (essentially a guide of horizontal and vertical lines) with the idea being that a user can change the spacing of the grid and the color of the grid.
The purpose of the grid is to act as a guideline for the user when lining up objects. Everything works just fine with one exception. When I resize the NSWindow by dragging the resize handle, if my grid spacing is particularly small (say 10 pixels). the drag resize becomes lethargic in nature.
My drawRect code for the grid is as follows:
-(void)drawRect:(NSRect)dirtyRect {
NSRect thisViewSize = [self bounds];
// Set the line color
[[NSColor colorWithDeviceRed:0
green:(255/255.0)
blue:(255/255.0)
alpha:1] set];
// Draw the vertical lines first
NSBezierPath * verticalLinePath = [NSBezierPath bezierPath];
int gridWidth = thisViewSize.size.width;
int gridHeight = thisViewSize.size.height;
int i;
while (i < gridWidth)
{
i = i + [self currentSpacing];
NSPoint startPoint = {i,0};
NSPoint endPoint = {i, gridHeight};
[verticalLinePath setLineWidth:1];
[verticalLinePath moveToPoint:startPoint];
[verticalLinePath lineToPoint:endPoint];
[verticalLinePath stroke];
}
// Draw the horizontal lines
NSBezierPath * horizontalLinePath = [NSBezierPath bezierPath];
i = 0;
while (i < gridHeight)
{
i = i + [self currentSpacing];
NSPoint startPoint = {0,i};
NSPoint endPoint = {gridWidth, i};
[horizontalLinePath setLineWidth:1];
[horizontalLinePath moveToPoint:startPoint];
[horizontalLinePath lineToPoint:endPoint];
[horizontalLinePath stroke];
}
}
I suspect this is entirely to do with the way that I am drawing the grid and am open to suggestions on how I might better go about it.
I can see where the inefficiency is coming in, drag-resizing the NSWindow is constantly calling the drawRect in this view as it resizes, and the closer the grid, the more calculations per pixel drag of the parent window.
I was thinking of hiding the view on the resize of the window, but it doesn't feel as dynamic. I want the user experience to be very smooth without any perceived delay or flickering.
Does anyone have any ideas on a better or more efficient method to drawing the grid?
All help, as always, very much appreciated.
You've inadvertently introduced a Schlemiel into your algorithm. Every time you call moveToPoint and lineToPoint in your loops, you are actually adding more lines to the same path, all of which will be drawn every time you call stroke on that path.
This means that you are drawing one line the first time through, two lines the second time through, three lines the third time, etc...
A quick fix would be to use a new path each time through the loop simply perform the stroke after the loop (with thanks to Jason Coco for the idea):
path = [NSBezierPath path];
while (...)
{
...
[path setLineWidth:1];
[path moveToPoint:startPoint];
[path lineToPoint:endPoint];
}
[path stroke];
Update: Another approach would be to avoid creating that NSBezierPath altogether, and just use the strokeLineFromPoint:toPoint: class method:
[NSBezierPath setDefaultLineWidth:1];
while (...)
{
...
[NSBezierPath strokeLineFromPoint:startPoint toPoint:endPoint];
}
Update #2: I did some basic benchmarking on the approaches so far. I'm using a window sized 800x600 pixels, a grid spacing of ten pixels, and I'm having cocoa redraw the window a thousand times, scaling from 800x600 to 900x700 and back again. Running on my 2GHz Core Duo Intel MacBook, I see the following times:
Original method posted in question: 206.53 seconds
Calling stroke after the loops: 16.68 seconds
New path each time through the loop: 16.68 seconds
Using strokeLineFromPoint:toPoint: 16.68 seconds
This means that the slowdown was entirely caused by the repetition, and that any of the several micro-improvements do very little to actually speed things up. This shouldn't be much of a surprise, since the actual drawing of pixels on-screen is (almost always) far more processor-intensive than simple loops and mathematical operations.
Lessons to be learned:
Hidden Schlemiels can really slow things down.
Always profile your code before doing unnecessary optimization
You should run Instruments Cpu Sampler to determine where most of the time is being spent and then optimized based on that info. If it's the stroke, put it outside the loop. If it's drawing the path, try offloading the rendering to the gpu. See if CALayer can help.
Maybe to late for the party, however someone could find this helpful. Recently, I needed a custom components for a customer, in order to recreate a grid resizable overlay UIView. The following should to the work, without issues even with very little dimensions.
The code is for iPhone (UIView), but it can be ported to NSView very quickly.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
//corners
CGContextSetLineWidth(context, 5.0);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, 15, 0);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, 0, 15);
CGContextMoveToPoint(context, rect.size.width, 0);
CGContextAddLineToPoint(context, rect.size.width-15, 0);
CGContextMoveToPoint(context, rect.size.width, 0);
CGContextAddLineToPoint(context, rect.size.width, 15);
CGContextMoveToPoint(context, 0, rect.size.height);
CGContextAddLineToPoint(context, 15, rect.size.height);
CGContextMoveToPoint(context, 0, rect.size.height);
CGContextAddLineToPoint(context, 0, rect.size.height-15);
CGContextMoveToPoint(context, rect.size.width, rect.size.height);
CGContextAddLineToPoint(context, rect.size.width-15, rect.size.height);
CGContextMoveToPoint(context, rect.size.width, rect.size.height);
CGContextAddLineToPoint(context, rect.size.width, rect.size.height-15);
CGContextStrokePath(context);
//border
CGFloat correctRatio = 2.0;
CGContextSetLineWidth(context, correctRatio);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
//grid
CGContextSetLineWidth(context, 0.5);
for (int i=0; i<4; i++) {
//vertical
CGPoint aPoint = CGPointMake(i*(rect.size.width/4), 0.0);
CGContextMoveToPoint(context, aPoint.x, aPoint.y);
CGContextAddLineToPoint(context,aPoint.x, rect.size.height);
CGContextStrokePath(context);
//horizontal
aPoint = CGPointMake(0.0, i*(rect.size.height/4));
CGContextMoveToPoint(context, aPoint.x, aPoint.y);
CGContextAddLineToPoint(context,rect.size.width, aPoint.y);
CGContextStrokePath(context);
}
}