UIView drawRect() : fill everything except polygon - objective-c

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);

Related

Drawing board/grid with Cocoa

I'm writing a small boardgame for Mac OS X using Cocoa. I the actual grid is drawn as follows:
- (void)drawRect:(NSRect)rect
{
for (int x=0; x < GRIDSIZE; x++) {
for (int y=0; y < GRIDSIZE; y++) {
float ix = x*cellWidth;
float iy = y*cellHeight;
NSColor *color = (x % 2 == y % 2) ? boardColors[0] : boardColors[1];
[color set];
NSRect r = NSMakeRect(ix, iy, cellWidth, cellHeight);
NSBezierPath *path = [NSBezierPath bezierPath];
[path appendBezierPathWithRect:r];
[path fill];
[path stroke];
}
}
}
This works great, except that I see some errors in colors between the tiles. I guess this is due to some antialiasing or similar. See screenshots below (hopefully you can also see the same problems... its some black lines where the tiles overlap):
Therefore I have these questions:
Is there any way I can remove these graphical artefacts while still maintaining a resizable/scalable board?
Should I rather use some other graphical library like Core Graphics or OpenGL?
Update:
const int GRIDSIZE = 16;
cellWidth = (frame.size.width / GRIDSIZE);
cellHeight = (frame.size.height / GRIDSIZE);
If you want crisp rectangles you need to align coordinates so that they match the underlying pixels. NSView has a method for this purpose: - (NSRect)backingAlignedRect:(NSRect)aRect options:(NSAlignmentOptions)options. Here's a complete example for drawing the grid:
const NSInteger GRIDSIZE = 16;
- (void)drawRect:(NSRect)dirtyRect {
for (NSUInteger x = 0; x < GRIDSIZE; x++) {
for (NSUInteger y = 0; y < GRIDSIZE; y++) {
NSColor *color = (x % 2 == y % 2) ? [NSColor greenColor] : [NSColor redColor];
[color set];
[NSBezierPath fillRect:[self rectOfCellAtColumn:x row:y]];
}
}
}
- (NSRect)rectOfCellAtColumn:(NSUInteger)column row:(NSUInteger)row {
NSRect frame = [self frame];
CGFloat cellWidth = frame.size.width / GRIDSIZE;
CGFloat cellHeight = frame.size.height / GRIDSIZE;
CGFloat x = column * cellWidth;
CGFloat y = row * cellHeight;
NSRect rect = NSMakeRect(x, y, cellWidth, cellHeight);
NSAlignmentOptions alignOpts = NSAlignMinXNearest | NSAlignMinYNearest |
NSAlignMaxXNearest | NSAlignMaxYNearest ;
return [self backingAlignedRect:rect options:alignOpts];
}
Note that you don't need stroke to draw a game board. To draw pixel aligned strokes you need to remember that coordinates in Cocoa actually point to lower left corners of pixels. To crisp lines you need to offset coordinates by half a pixel from integral coordinates so that coordinates point to centers of pixels. For example to draw a crisp border for a grid cell you can do this:
NSRect rect = NSInsetRect([self rectOfCellAtColumn:column row:row], 0.5, 0.5);
[NSBezierPath strokeRect:rect];
First, make sure your stroke color is not black or gray. (You're setting color but is that stroke or fill color? I can never remember.)
Second, what happens if you simply fill with green, then draw red squares over it, or vice-versa?
There are other ways to do what you want, too. You can use the CICheckerboardGenerator to make your background instead.
Alternately, you could also use a CGBitmapContext that you filled by hand.
First of all, if you don't actually want your rectangles to have a border, you shouldn't call [path stroke].
Second, creating a bezier path for filling a rectangle is overkill. You can do the same with NSRectFill(r). This function is probably more efficient and I suspect less prone to introduce rounding errors to your floats – I assume you realize that your floats must not have a fractional part if you want pixel-precise rectangles. I believe that if the width and height of your view is a multiple of GRIDSIZE and you use NSRectFill, the artifacts should go away.
Third, there's the obvious question as to how you want your board drawn if the view's width and height are not a multiple of GRIDSIZE. This is of course not an issue if the size of your view is fixed and a multiple of that constant. If it is not, however, you first have to clarify how you want the possible remainder of the width or height handled. Should there be a border? Should the last cell in the row or column take up the remainder? Or should it rather be distributed equally among the cells of the rows or columns? You might have to accept cells of varying width and/or height. What the best solution for your problem is, depends on your exact requirements.
You might also want to look into other ways of drawing a checkerboard, e.g. using CICheckerboardGenerator or creating a pattern color with an image ([NSColor colorWithPatternImage:yourImage]) and then filling the whole view with it.
There's also the possibility of (temporarily) turning off anti-aliasing. To do that, add the following line to the beginning of your drawing method:
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
My last observation is about your general approach. If your game is going to have more complicated graphics and animations, e.g. animated movement of pieces, you might be better off using OpenGL.
As of iOS 6, you can generate a checkerboard pattern using CICheckerboardGenerator.
You'll want to guard against the force unwraps in here, but here's the basic implementation:
var checkerboardImage: UIImage? {
let filter = CIFilter(name: "CICheckerboardGenerator")!
let width = NSNumber(value: Float(viewSize.width/16))
let center = CIVector(cgPoint: .zero)
let darkColor = CIColor.red
let lightColor = CIColor.green
let sharpness = NSNumber(value: 1.0)
filter.setDefaults()
filter.setValue(width, forKey: "inputWidth")
filter.setValue(center, forKey: "inputCenter")
filter.setValue(darkColor, forKey: "inputColor0")
filter.setValue(lightColor, forKey: "inputColor1")
filter.setValue(sharpness, forKey: "inputSharpness")
let context = CIContext(options: nil)
let cgImage = context.createCGImage(filter.outputImage!, from: viewSize)
let uiImage = UIImage(cgImage: cgImage!, scale: UIScreen.main.scale, orientation: UIImage.Orientation.up)
return uiImage
}
Apple Developer Docs
Your squares overlap. ix + CELLWIDTH is the same coordinate as ix in the next iteration of the loop.
You can fix this by setting the stroke color explicitly to transparent, or by not calling stroke.
[color set];
[[NSColor clearColor] setStroke];
or
[path fill];
// not [path stroke];

Drawings in drawRect not being displayed correctly

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);

Objective-C Draw a Circle with a Ring shape

I wrote this class that draws a animated progress with a circle (it draws a circular sector based on a float progress)
#implementation MXMProgressView
#synthesize progress;
- (id)initWithDefaultSize {
int circleOffset = 45.0f;
self = [super initWithFrame:CGRectMake(0.0f,
0.0f,
135.0f + circleOffset,
135.0f + circleOffset)];
self.backgroundColor = [UIColor clearColor];
return self;
}
- (void)drawRect:(CGRect)rect {
CGRect allRect = self.bounds;
CGRect circleRect = CGRectMake(allRect.origin.x + 2, allRect.origin.y + 2, allRect.size.width - 4,
allRect.size.height - 4);
CGContextRef context = UIGraphicsGetCurrentContext();
// background image
//UIImage *image = [UIImage imageNamed:#"loader_disc_hover.png"];
//[image drawInRect:circleRect];
// Orange: E27006
CGContextSetRGBFillColor(context,
((CGFloat)0xE2/(CGFloat)0xFF),
((CGFloat)0x70/(CGFloat)0xFF),
((CGFloat)0x06/(CGFloat)0xFF),
0.01f); // fill
//CGContextSetLineWidth(context, 2.0);
CGContextFillEllipseInRect(context, circleRect);
//CGContextStrokeEllipseInRect(context, circleRect);
// Draw progress
float x = (allRect.size.width / 2);
float y = (allRect.size.height / 2);
// Orange: E27006
CGContextSetRGBFillColor(context,
((CGFloat)0xE2/(CGFloat)0xFF),
((CGFloat)0x70/(CGFloat)0xFF),
((CGFloat)0x06/(CGFloat)0xFF),
1.0f); // progress
CGContextMoveToPoint(context, x, y);
CGContextAddArc(context, x, y, (allRect.size.width - 4) / 2, -M_PI_2, (self.progress * 2 * M_PI) - M_PI_2, 0);
CGContextClosePath(context);
CGContextFillPath(context);
}
#end
Now what I want to do I to draw a ring shape with the same progress animation, instead of filling the full circle, so a circular sector again not starting from the center of the circle.
I tried with CGContextAddEllipseInRect and the CGContextEOFillPath(context);
with no success.
I think you'll need to construct a more complex path, something like:
// Move to start point of outer arc (which might not be required)
CGContextMoveToPoint(context, x+outerRadius*cos(startAngle), y+outerRadius*sin(startAngle));
// Add outer arc to path (counterclockwise)
CGContextAddArc(context, x, y, outerRadius, startAngle, endAngle, 0);
// move *inward* to start point of inner arc
CGContextMoveToPoint(context, x+innerRadius*cos(endAngle), y+innerRadius*sin(endAngle));
// Add inner arc to path (clockwise)
CGContextAddArc(context, x, y, innerRadius, endAngle, StartAngle, 1);
// Close the path from end of inner arc to start of outer arc
CGContextClosePath(context);
Note: I haven't tried the above code myself
Cheap and nasty solution:
Draw a solid circle that is smaller than the original circle by the thickness of the ring you want to draw.
Draw this circle on top of the original circle, all that you will see animating is the ring.

How do I fill color space in a CGrect

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.)

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);
}
}