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);
}
}
Related
In my app, I draw a grid in a custom view controller. This gets embedded in a scroll view, so the grid gets redrawn from time to time after the user zooms in or out (not during zooming).
The problem is, I'm trying to optimize this method for low-memory devices like the iPad Mini, and it's still crashing. If I take this drawing away entirely, the app works fine, and it doesn't give me a reason for the crash; it just tells me the connection was lost. I can see in my instruments that memory is spiking just before it crashes, so I'm pretty certain this is the issue.
The view is 2.5 times the screen size horizontally and vertically, set programmatically when it's created. It has multiple CALayers and this grid drawing happens on several of them. The crash happens either immediately when I segue to this view, or soon after when I do some zooming.
Here are my drawing methods (somewhat simplified for readability and because they're pretty redundant):
#define MARKER_FADE_NONE 0
#define MARKER_FADE_OUT 1 // fades out at widest zoom
#define MARKER_FADE_IN 2 // fades in at widest zoom
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if (layer == gridSubdividers) [self drawGridInContext:ctx spacing:1 lineWidth:1 markers:NO fade:MARKER_FADE_NONE];
if (layer == gridDividers) [self drawGridInContext:ctx spacing:5 lineWidth:1 markers:YES fade:MARKER_FADE_OUT];
if (layer == gridSuperdividers) [self drawGridInContext:ctx spacing:10 lineWidth:2 markers:YES fade:MARKER_FADE_IN];
}
// DRAW GRID LINES
- (void)drawGridInContext:(CGContextRef)context
spacing:(float)spacing
lineWidth:(NSInteger)lineWidth
markers:(BOOL)markers
fade:(int)fade
{
spacing = _gridUnits * spacing;
CGContextSetStrokeColorWithColor(context, [UIColor gridLinesColor]);
CGContextSetLineWidth(context, lineWidth);
CGContextSetShouldAntialias(context, NO);
float top = 0;
float bottom = _gridSize.height;
float left = 0;
float right = _gridSize.width;
// vertical lines (right of origin)
CGMutablePathRef path = CGPathCreateMutable();
for (float i = _origin.x + spacing; i <= _gridSize.width; i = i + spacing) {
CGPathMoveToPoint(path, NULL, i, top);
CGPathAddLineToPoint(path, NULL, i, bottom);
}
CGContextAddPath(context, path);
CGContextStrokePath(context);
CGPathRelease(path);
}
...then I repeat this for() loop three more times to draw the other lines of the grid. I also tried this slightly different version of the loop, where I don't create an individual path, but instead just add lines to the context and then stroke only at the very end of all four of these loops:
for (float i = _origin.x + spacing; i <= _gridSize.width; i = i + spacing) {
CGContextMoveToPoint(context, i, top);
CGContextAddLineToPoint(context, i, bottom);
}
CGContextStrokePath(context);
I also tried a combination of the two, in which I began and stroked within each cycle of the loop:
for (float i = _origin.x + spacing; i <= _gridSize.width; i = i + spacing) {
CGContextBeginPath(context);
CGContextMoveToPoint(context, i, top);
CGContextAddLineToPoint(context, i, bottom);
CGContextStrokePath(context);
}
So how can I streamline this whole drawing method so that it doesn't use as much memory? If it can't draw one path at a time, releasing each one afterward, like the first loop I showed above...I'm not really sure what to do other than read the available memory on the device and turn off the grid drawing function if it's low.
I'm also totally open to alternative methods of grid drawing.
I need to fill my UIView in drawRect() method with 'reverted polygon' - everything in view is filled with some color except polygon itself.
I've got this code to draw a simple polygon:
CGContextBeginPath(context);
for(int i = 0; i < corners.count; ++i)
{
CGPoint cur = [self cornerAt:i], next = [self cornerAt:(i + 1) % corners.count];
if(i == 0)
CGContextMoveToPoint(context, cur.x, cur.y);
CGContextAddLineToPoint(context, next.x, next.y);
}
CGContextClosePath(context);
CGContextFillPath(context);
I found a similar question, but in C#, not Obj-C : c# fill everything but GraphicsPath
Probably the fastest way is to set a clip:
// create your path as posted
// but don't fill it (remove the last line)
CGContextAddRect(context, self.bounds);
CGContextEOClip(context);
CGContextSetRGBFillColor(context, 1, 1, 0, 1);
CGContextFillRect(context, self.bounds);
Both the other answers suggest to first fill a rect, then draw the shape with clear color on top. Both omit the necessary blend mode. Here's a working version:
CGContextSetRGBFillColor(context, 1, 1, 0, 1);
CGContextFillRect(context, self.bounds);
CGContextSetBlendMode(context, kCGBlendModeClear);
// create and fill your path as posted
Edit: Both approaches require the backgroundColor to be the clearColor and opaque to be set to NO.
Second Edit: The original question was about Core Graphics. Of course there are other ways of masking part of a view. Most notably CALayer's mask property.
You can set this property to an instance of a CAPathLayer that contains your clip path to create a stencil effect.
in drawRect u can set the background color of ur view with the color u want
self.backgroundColor = [UIcolor redColor]; //set ur color
and then draw a polygon the way u r doing.
CGContextBeginPath(context);
for(int i = 0; i < corners.count; ++i)
{
CGPoint cur = [self cornerAt:i], next = [self cornerAt:(i + 1) % corners.count];
if(i == 0)
CGContextMoveToPoint(context, cur.x, cur.y);
CGContextAddLineToPoint(context, next.x, next.y);
}
CGContextClosePath(context);
CGContextFillPath(context);
hope it helps.. happy coding :)
Create a new CGLayer, fill it with your outside color, then draw your polygon using a clear color.
layer1 = CGLayerCreateWithContext(context, self.bounds.size, NULL);
context1 = CGLayerGetContext(layer1);
[... fill entire layer ...]
CGContextSetFillColorWithColor(self.context1, [[UIColor clearColor] CGColor]);
[... draw your polygon ...]
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawLayerAtPoint(context, CGPointZero, layer1);
I want to implement freeform drawing in my app. First, I tried the code inside drawLayer:inContext: and it gave me the result I wanted.
Drawing in CALayer:
But when I decided to implement the code inside drawRect:, this happened:
Even if I draw inside the white space, the drawing is rendered outside as shown above. The code I used is exactly the same. I copy-pasted it from drawLayer:inContext: to drawRect:. I didn't change a thing, so why is this happening?
The Code:
CGContextSaveGState(ctx);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, 1.0);
CGContextSetRGBStrokeColor(ctx, 1, 0, 0, 1);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, prevPoint.x, prevPoint.y);
for (NSValue *r in drawnPoints){
CGPoint pt = [r CGPointValue];
CGContextAddLineToPoint(ctx, pt.x, pt.y);
}
CGContextStrokePath(ctx);
CGContextRestoreGState(ctx);
I see you are using app in full screen mode where the view is centered and does not take full width of the screen.
It may be that CALayer has transform applied to it that translates the drawing from the left side of the screen to the center. This may not be the case with drawRect:. Try setting CGContext's transform matrix:
CGContextSaveGState(ctx);
CGFloat xOffset = CGRectGetMidX(screenFrame) - CGRectGetMidX(viewFrame);
CGContextTranslateCTM(ctx, xOffset, 0.0f);
// rest of drawing code
// ...
CGContextRestoreGState(ctx);
I am using the code below and am trying everything to color the resulting circle that it draws, but cannot for the life of me figure it out! Can anyone help? How do I color the circle in that my method draws?
What I have so far is this:
-(void)drawCircleAtPoint:(CGPoint)p withRadius:(CGFloat)radius inContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
CGContextBeginPath(context);
CGContextAddArc(context, p.x, p.y, radius, 0, 2*M_PI, YES);
CGContextStrokePath(context);
UIGraphicsPopContext();
}
- (void)drawRect:(CGRect)rect
{
CGPoint midPoint;
midPoint.x = self.bounds.origin.x + self.bounds.size.width/2;
midPoint.y = self.bounds.origin.y + self.bounds.size.width/2;
CGFloat size = self.bounds.size.width/2;
if (self.bounds.size.height < self.bounds.size.width) size = self.bounds.size.height/2;
size *= 0.90;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 10.0);
[[UIColor blackColor] setStroke];
CGColorRef color = [[UIColor redColor] CGColor];
CGContextSetFillColorWithColor(context, color);
CGContextFillPath(context);
[self drawCircleAtPoint:midPoint withRadius:size inContext:context];
}
You're trying to fill the current path, but you haven't added anything to it yet. The only point at which you add anything to the current path is in drawCircleAtPoint:withRadius:inContext:, after which you only stroke it.
Move your CGContextFillPath call into that method. You'll want to fill before you stroke, since the stroke will reset the current path. (If you want to fill on top of the inside of the stroke, you'll need to save the gstate before stroking and restore it after, so that the current path is still set when you fill.)
Searching the web for about 4 hours not getting an answer so:
How to draw a shadow on a path which has transparency?
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.8] CGColor]);
// Sample Path
CGContextMoveToPoint(c, 20.0, 10.0);
CGContextAddLineToPoint(c, 100.0, 40.0);
CGContextAddLineToPoint(c, 40.0, 70.0);
CGContextClosePath(c);
CGContextDrawPath(c, kCGPathFillStroke);
}
The first thing I notice, the shadow is only around the stroke. But that isn't the problem so far. The shadow behind the path/rect is still visible, which means: the shadow color is effecting the fill color of my path. The fill color should be white but instead its grey. How to solve this issue?
You will have to clip the context and draw twice.
First you create a reference to your path since you will have to use it a few times and save your graphics context so you can come back to it.
Then you clip the graphics context to a only draw outside of your path. This is done by adding your path to the path that covers the entire view. Once you have clipped you draw your path with the shadow so that it's draw on the outside.
Next you restore the graphics context to how it was before you clipped and draw your path again without the shadow.
It's going to look like this on an orange background (white background wasn't very visible)
The code to do the above drawing is this:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.5] CGColor]);
// Sample Path
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 20.0, 10.0);
CGPathAddLineToPoint(path, NULL, 40.0, 70.0);
CGPathAddLineToPoint(path, NULL, 100.0, 40.0);
CGPathCloseSubpath(path);
// Save the state so we can undo the shadow and clipping later
CGContextSaveGState(c);
{ // Only for readability (so we know what are inside the save/restore scope
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGFloat width = CGRectGetWidth(self.frame);
CGFloat height = CGRectGetHeight(self.frame);
// Create a mask that covers the entire frame
CGContextMoveToPoint(c, 0, 0);
CGContextAddLineToPoint(c, width, 0);
CGContextAddLineToPoint(c, width, height);
CGContextAddLineToPoint(c, 0, height);
CGContextClosePath(c);
// Add the path (which by even-odd rule will remove it)
CGContextAddPath(c, path);
// Clip to that path (drawing will only happen outside our path)
CGContextClip(c);
// Now draw the path in the clipped context
CGContextAddPath(c, path);
CGContextDrawPath(c, kCGPathFillStroke);
}
CGContextRestoreGState(c); // Go back to before the clipping and before the shadow
// Draw the path without the shadow to get the transparent fill
CGContextAddPath(c, path);
CGContextDrawPath(c, kCGPathFillStroke);
}
If you want the entire shadow to be as strong and don't want the transparency of the fill color to make the shadow weaker then you can use a fully opaque color when filling the first time. It's going to get clipped so it won't be visible inside the path anyway. It will only affect the shadow.
Per your request in comments, here's a more in-depth exploration. Consider the following screenshot (StackOverflow shrinks it for me -- it helps to look at it full size.):
What you're seeing here is 5 different drawing approaches (top to bottom) over three different backgrounds (left to right). (I've also dropped the fill alpha from 0.8 to 0.5 to make the effects easier to see.) The three different drawing approaches are (top to bottom):
Just the stroke, not the fill, with a shadow
The way you posted in the code in your original question
The stroke and fill, with no shadow applied
Just the shadow, by itself
The way #DavidRönnqvist proposed in his answer.
The three different backgrounds should be self explanatory.
You said in your original question:
The first thing I notice, the shadow is only around the stroke.
This is why I included the first drawing approach. That's what it really looks like when there is just the stroke, with no fill, and (therefore) only the stroke is being shadowed.
Then, you said:
But that isn't the problem so far. The shadow behind the path/rect is
still visible, which means: the shadow color is effecting the fill
color of my path. The fill color should be white but instead its grey.
Your original code is the next version (#2). What you're seeing there is that the shadow for the stroke is darker than the shadow for the fill. This is because the stroke color's alpha is 1.0 and the fill's alpha is less than 1.0. This might be easier to see in version #4 which is just the shadow -- it's darker around the edge where the stroke is. Version #3 shows the drawing without a shadow. See you you can see the red and the image semi-obsurced in the fill of the shape? So in your original drawing you're seeing the object's own shadow through the object itself.
If that's not making sense, try thinking of a piece of glass that's got a tint to it (if you're into photography, think of a Neutral Density Filter). If you hold that glass between a light source and another surface, and then peek from the side and look just at the lower surface, you know that the semi-transparent glass is going to cast some shadow, but not as dark a shadow as something completely opaque (like a piece of cardboard). This is what you're seeing -- you're looking through the object at it's shadow.
Version #5 is #DavidRönnqvist's approach. The eye-fooling effect I was talking about in my comment is easiest to appreciate (for me, anyway) by looking at the shapes drawn over the image background. What it ends up looking like (in version #5) is that the shape is a bordered, copied, portion of the image that's been overlaid with a semi-transparent white mask of some sort. If you look back at version #3, it's clear, in the absence of the shadow, what's going on: you're looking through the semi-transparent shape at the image beneath. Then if you look at version #4, it's also clear that you have a shadow being cast by an object that's behind your eye/camera. From there, I would argue that that's also clear when looking at version #2 over the image what's going on (even if it's less clear over a solid color). At first glance, my eye/brain doesn't know what it's looking at in version #5 -- there's a moment of "visual dissonance" before I establish the mental model of "copied, masked, portion of the image floating above the original image."
So if that effect (#5) was what you were going for, then David's solution will work great. I just wanted to point out that it's sort of a non-intuitive effect.
Hope this is helpful. I've put the complete sample project I used to generate this screenshot on GitHub.
CGFloat lineWidth = 2.0f;
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextSetLineWidth(c, lineWidth);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathStroke);
CGContextRestoreGState(c);
someRect.origin.x += lineWidth/2;
someRect.origin.y += lineWidth/2;
someRect.size.width -= lineWidth;
someRect.size.height -= lineWidth;
CGContextClearRect(c, someRect);
CGContextSetFillColorWithColor(c, [[[UIColor whiteColor] colorWithAlphaComponent:0.8] CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathFill);
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowColor: [NSColor blackColor]];
[shadow setShadowOffset: NSMakeSize(2.1, -3.1)];
[shadow setShadowBlurRadius: 5];
NSBezierPath* bezierPath = [NSBezierPath bezierPath];
[bezierPath moveToPoint: NSMakePoint(12.5, 6.5)];
[bezierPath curveToPoint: NSMakePoint(52.5, 8.5) controlPoint1: NSMakePoint(40.5, 13.5) controlPoint2: NSMakePoint(52.5, 8.5)];
[bezierPath lineToPoint: NSMakePoint(115.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(150.5, 6.5)];
[bezierPath lineToPoint: NSMakePoint(201.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(222.5, 8.5)];
[NSGraphicsContext saveGraphicsState];
[shadow set];
[[NSColor blackColor] setStroke];
[bezierPath setLineWidth: 1];
[bezierPath stroke];
[NSGraphicsContext restoreGraphicsState];