I have a custom view for a loading indicator which is comprised of a number of CAShapeLayers which I perform various animations on to represent various states. In the init method I create a number of shape layers and add them as sublayers to the view's layer. The issue I'm having is the shapes that I'm drawing have very rough edges and don't seem to be antialiased. I've tried a number of things from similar answers on here but I can't seem to antialias the shapes. I've never had this problem before then drawing directly in drawRect.
Is there anything I'm missing or is there a better way to accomplish what I'm trying to do?
Update: Here's a comparison of how the shapes are being drawn:
On the left I'm drawing in layoutSubviews using this code:
CGFloat lineWidth = 2.5f;
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGPoint center = CGPointMake(CGRectGetWidth(self.frame)/2, CGRectGetHeight(self.frame)/2);
CGFloat startAngle = - ((float)M_PI / 2);
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *trackPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
self.trackLayer.path = trackPath.CGPath;
self.trackLayer.lineWidth = lineWidth;
On the right I'm calling a separate method from awakeFromNib:
-(void)awakeFromNib
{
....
self.trackLayer = [self trackShape]
[self.layer addSublayer:self.trackLayer];
}
-(CAShapeLayer*)trackShape
{
float start_angle = 2*M_PI*-M_PI_2;
float end_angle = 2*M_PI*1-M_PI_2;
float minSize = MIN(self.frame.size.width, self.frame.size.height);
float radius = (minSize-_strokeWidth)/2 -4;
CGPoint center = CGPointMake(self.frame.size.width/2,self.frame.size.height/2);
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:start_angle
endAngle:end_angle
clockwise:YES].CGPath;
circle.strokeColor = self.trackColor.CGColor;
circle.fillColor = nil;
circle.lineWidth = (_strokeWidth == -1.0) ? minSize * _strokeWidthRatio
: _strokeWidth;
circle.rasterizationScale = [[UIScreen mainScreen] scale];
return circle;
}
The picture you provided is exactly what happened to me. I got the chunky circle. In my case, the code was inside the drawRect method. The problem was that I was adding a CAShapeLayer, as a subLayer of my view, every time drawRect was called.
So to anyone who stumbles upon something similar, keep track of the CAShapeLayer you are adding or clear everything before drawing again in the drawRect.
Ok, so I'm trying to draw a dashed box that's subdivided into sections where I'm going to put content. Here's my code:
NSBezierPath *path = [NSBezierPath bezierPathWithRect:dirtyRect];
[path setLineWidth:3];
CGFloat pattern[2] = {5, 5};
[path setLineDash:pattern count:2 phase:0];
CGFloat totalHeight = header.frame.origin.y - 10;
CGFloat sectionOffset = 0;
if([game getNumPlayers] == 2) {
sectionOffset = totalHeight / 2;
} else if([game getNumPlayers] == 3) {
sectionOffset = totalHeight / 3;
} else if([game getNumPlayers] == 4) {
sectionOffset = totalHeight / 4;
}
for(int i = 0; i < [[game getPlayers] count]; i++) {
[path moveToPoint:NSMakePoint(0, totalHeight - (sectionOffset * i))];
[path lineToPoint:NSMakePoint(dirtyRect.size.width, totalHeight - (sectionOffset * i))];
}
[path stroke];
This is contained within my custom view's drawRect method, so dirtyRect is an NSRect equivalent to the bounds of the view. The variable header refers to another view in the superview, which I'm basing the location of the lines off of.
Here's a screenshot of what this code actually draws (except the label obviously):
As you can see, unless we're dealing with a very unfortunate optically illusion, which I doubt, the dividers contained in the box appear to be thicker than the outline of the box. I've explicitly set the lineWidth of the path object to be three, so I'm not sure why this is. I would be much appreciative of any suggestions that can be provided.
OK, I think the problem is that your outer box is just getting clipped by the edges of its view. You ask for a line that’s 3 points wide, so if your dirtyRect is the actual bounds of the view, then 1.5 points of the enclosing box will be outside the view, so you’ll only see 1.5 points of the edge lines.
The inner lines are showing the full 3-point thickness.
You can fix this by doing something like:
const CGFloat lineWidth = 3;
NSBezierPath *const path = [NSBezierPath bezierPathWithRect:NSInsetRect(dirtyRect, lineWidth/2, lineWidth/2)];
path.lineWidth = lineWidth;
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];
In my program I'm drawing some shapes inside an NSView using NSBezierPath. Everything works and looks great except when I resize the window.
Example
Initial drawing
Window resize
Does anyone know what I should use to prevent this square from being anchored to the initial position, but make it readjust relative the the initial scale.
Any help is appreciated!
If you are doing your drawing in drawRect: then the answer is NO. You will need to rebuild and reposition your path each time. What you can do is something along the following lines:
- (void) drawRect:(NSRect)dirtyRect
{
// Assuming that _relPos with x and y having values bewteen 0 and 1.
// To keep the square in the middle of the view you would set _relPos to
// CGPointMake(0.5, 0.5).
CGRect bounds = self.bounds;
CGRect rect;
rect.size.width = 100;
rect.size.height = 100;
rect.origin.x = bounds.origin.x + bounds.size.width * _relPos.x - rect.size.width /2;
rect.origin.y = bounds.origin.y + bounds.size.height * _relPos.y - rect.size.height/2;
NSBezierPath *path = [NSBezierPath bezierPathWithRect:rect];
[[NSColor redColor] set];
path.lineWidth = 2;
[path stroke];
}
I am making a finger painting app and I am having a hard time giving my brush a nice brush-like feel and texture.
I have this code:
UIColor * brushTexture = [UIColor colorWithPatternImage:[UIImage imageNamed:#"Brush_Black.png"]];
CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), brushTexture.CGColor);
but my output is like this:
The texture of my brush is tiled. How can I make it fit to the stroke Size and stop the tiling of the image?
Here i found a solution!
UIImage *texture = [UIImage imageNamed:"brush.png"]
CGPoint vector = CGPointMake(currentPoint.x - endPoint.x, currentTPoint.y - endPoint.y);
CGFloat distance = hypotf(vector.x, vector.y);
vector.x /= distance;
vector.y /= distance;
for (CGFloat i = 0; i < distance; i += 1.0f) {
CGPoint p = CGPointMake(endPoint.x + i * vector.x, endPoint.y + i * vector.y);
[texture drawAtPoint:p blendMode:blendMode alpha:0.5f];
}