I am drawing a shape in a UIView drawRect function that involves clipping a path, and then adding coloured blocks behind so that the colors have the shape of the clipped path. However, for some reason the lines of the path are not coming out smoothly; its if the antialiasing isn't working properly.
CGContextSetStrokeColorWithColor(context, [UIColor clearColor].CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextSetShouldAntialias(context, true);
CGContextSetAllowsAntialiasing(context, true);
CGContextBeginPath (context);
CGContextMoveToPoint(context, xStart, yStart);
for (int i=0; i<points.count; i++) {
CGContextAddLineToPoint(context, xPoint, yPoint);
}
}
CGContextAddLineToPoint(context, xStart, yStart );
CGContextClip(context);
CGRect colorRect = CGRectMake(0, 0 , rectWidth, rectHeight);
CGContextSetFillColorWithColor(context, blockColor.CGColor);
CGContextFillRect(context, colorRect);
CGContextRestoreGState(context);
The result should have smooth lines, but it comes out jagged with visible pixels as in this image:
Any idea what the problem is and how to fix it?
Thanks in advance
Okey. So, there is no problem in your code. The problem is that antialiasing works a little different. You drawing a vertical slopes expecting that edge of resulting histogram will be smoothed. But actually antialiasing doesn't smooth the resulting figure. It works with path elements (curves, lines) one by one. So, for example if you draw a circle, its edges will be smoothed.
There is a simple solution of your problem: just create a curve, enveloping the histogram. Add it to your context. It will look more smooth.
Sorry for bad english.
Related
First off, here's an image of my current situation.
To accomplish this I am using a subclassed UIView with the following method:
-(void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint (ctx, CGRectGetMinX(rect), CGRectGetMinY(rect)); // top left
CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMidY(rect)); // mid right
CGContextAddLineToPoint(ctx, CGRectGetMinX(rect), CGRectGetMaxY(rect)); // bottom left
CGContextClosePath(ctx);
CGContextSetRGBFillColor(ctx, 0, 1, 0, 1);
CGContextFillPath(ctx);
}
My ultimate goal is to clip the colored regions so that only the neon green remains. (I'll flip the colors later). I want it to look like a pie chart. I'm guessing there's a way to clip the colored corners out but I have no clue. CGContextClip(ctx) doesn't work.
I am using the Pixate framework as well. If anyone knows a way to accomplish a triangle shape with Pixate that would be even better.
The easiest way to clip a UIView to a path is to create a CAShapeLayer, add a path to the layer, and add the shape layer as a mask of your view's layer.
Or with a CGPathRef, essentially the same thing but at core foundation level
eg..
{
// before existing drawing code..
CGMutablePathRef clipTriangle = CGPathCreateMutable();
CGPathMoveToPoint (clipTriangle, nil, startX, startY);
CGPathAddLineToPoint(clipTriangle, nil, secondPointX, secondPointY);
CGPathAddLineToPoint(clipTriangle, nil, thirdPointX, thirdPointY);
CGPathCloseSubpath(clipTriangle);
CGContextSaveGstate(ctx);
CGContextAddPath(ctx, clipTriangle); /// this is the essential line I left out, we drew the triangle and forgot to use it, clipping to nothing instead, sorry mate
CGContextClip(ctx);
//draw stuff that you wanted clipped here...
CGContextRestoreGstate(ctx);
CGPathRelease(clippingTriangle); //CFstuff still needs manual memory management regardless of ARC
}
I draw line like this:
- (void)drawRect:(CGRect)rect {
_lineColor = [UIColor blackColor];
[_lineColor setStroke];
[_lineColor setFill];
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextClearRect(c, rect);
[[UIColor clearColor] setFill];
CGContextAddRect(c, rect);
CGContextSetLineWidth(c, 1);
CGContextDrawPath(c, kCGPathFill);
CGContextBeginPath(c);
CGContextMoveToPoint(c, 0, 0);
CGContextAddLineToPoint(c, x1, y1);
CGContextStrokePath(c);
}
but, my line is have different width which angle is 90 or 45 degrees. How I can draw line with same width
I whipped up something that may make the effect here more visible. Here's the code:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
// Get us a gray background
CGContextSetFillColorWithColor(c, [[UIColor grayColor] CGColor]);
CGContextFillRect(c, CGRectInfinite);
CGContextSetStrokeColorWithColor(c, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(c, 1.0);
CGContextBeginPath(c);
// Draw some lines at various angles
for (CGFloat i = -10.; i <= 10.; i += 1.0)
{
CGContextMoveToPoint(c, CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGContextAddLineToPoint(c, CGRectGetMidX(self.bounds) + i * 10., CGRectGetMidY(self.bounds) + 100);
}
CGContextStrokePath(c);
}
Here's the output from that code on a retina device: (This should display at 100% in the standard StackOverflow format, but you can look at it full size to be sure)
Now here's a piece of that blown up:
What you're seeing here is anti-aliasing at work. For starters the vertical line is 2.0 pixels across, all the time, with no anti-aliasing (assuming you draw it on a pixel boundary). Now think about a 45deg line drawn using the same pixel grid, and employ the Pythagorean theorem. Here's another diagram:
At it's narrowest (i.e. in the dimension perpendicular to the line itself), a 45 deg line will appear 1.414px wide, and at it's widest opaque section (not counting the mostly transparent pixel that's bridging the space in the jaggy gaps) it's going to appear 2.828px across. When blown up, you can see how the work that's being done to anti-alias these lines is effecting the optical appearance of the lines.
Someone is probably going to come along and suggest that you turn off anti-aliasing, but for reference, that makes the optical effect even worse (because then the rasterizer is going to make every pixel with coverage completely opaque):
In short, this is expected behavior, and if you need to adjust how you draw each line to achieve some desired optical appearance that the default anti-aliasing code doesn't provide, then you just have to do that work. There's not a "make all my lines optically similar" setting in CoreGraphics -- they've done the best they can for the general case, and if you need something more specific that's up to you. FWIW, many many applications simply use the default, and people seem pretty satisfied with the results, so you might ask yourself if this is really the place in your app where you want to put in a bunch of extra work.
It occurred to me: I'm not sure you'll like the effect, but one possible approach for achieving optical similarity might be to draw your vertical lines not on pixel boundaries. This will cause them to appear anti-aliased too, which (depending on how you look at it) may make them appear more consistent with the angled, anti-aliased lines. Here's what that looks like:
What you lose doing this is a certain "crispness" to the lines. Drawing off pixel-boundaries can most easily be achieved by translating the context by 0.25pt in the X direction (for retina -- for non-retina, 0.5pt will have a similar effect) like this:
CGContextTranslateCTM(c, 0.25, 0);
Hope this helps.
I'm making a custom control and it was fine til my client says "I want it with gradients".
So here I am.
I need to draw an angle and fill it with gradient color. My previous code was this:
- (void)drawRect:(CGRect)rect{
CGContextBeginPath(context);
CGContextMoveToPoint(context, self.center.x, self.center.y);
CGContextAddArc(context, self.center.x, self.center.y, radius, radians(180), radians(minValue - 180), 0);
CGContextClosePath(context);
CGContextSetFillColorWithColor(context, gray);
CGContextFillPath(context);
}
With the CGContextAddArc() function I can draw my angle and paint it with CGContextSetFillColorWithColor() function, then close the path and voilá. But now I need to painted with gradients.
I know that maybe I should use CGContextDrawRadialGradient, but don't know how to use it with an arc.
I've been tried to figured out this with other question in StackOVerFlow and reading the doc, but I just can't get it.
In my custom control I have defined a few CGMutablePathRefs with the needed lines and arcs to draw my control; one draws the overall fill shape and others provide specular highlights. I have also defined two CGMutablePathRefs which contain the paths needed as clipping masks for the active and inactive state of the control.
What I'm struggling with is applying the clipping paths. I have previously used clipping paths for applying gradients to an image, but those drawing commands were of the CGContext... variety, not the CGPath... variety.
For testing purposes I have removed the specular highlight drawing aspects, just trying to get a large path clipped to a smaller path. This is what I had been testing with:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextAddPath(ctx, inactiveClip);
CGContextClosePath(ctx);
CGContextClip(ctx);
CGContextAddPath(ctx, frontFace);
CGContextSetLineWidth(ctx, 1.0);
CGContextSetFillColorWithColor(ctx, [[UIColor blackColor] CGColor]);
CGContextFillPath(ctx);
}
By putting the clipping command before any drawing, I thought I was saying to CoreGraphics, "Here's the region you should actually draw into."
Alas, nothing is drawn.
So assuming I had that ordering backwards I tried:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextAddPath(ctx, frontFace);
CGContextSetLineWidth(ctx, 1.0);
CGContextSetFillColorWithColor(ctx, [[UIColor blackColor] CGColor]);
CGContextFillPath(ctx);
CGContextBeginPath(ctx);
CGContextAddPath(ctx, inactiveClip);
CGContextClosePath(ctx);
CGContextClip(ctx);
}
This was to say to CoreGraphics, "Okay before you actually color bits, check them against this clipping region."
Alas, nothing is clipped.
Since it is the case that my clipping path uses some of the same points and control points, in the same order, as the fill path, I have also replaced the call to CGContextClip with a call to CGContextEOClip to see if I was really struggling with the even-odd rule, but that doesn't seem to have had any visual affect.
I don't really know if I needed to bracket the CGContextAddPath call with CGContextBeginPath/CGContextClosePath calls, but what I was trying to do was minimize the differences between Apple's example code and my code. In theirs they do their CGContext... drawing calls between begin/close calls so I was too.
What am I misunderstanding here?
I'm able to use CGContextDrawRadialGradient to make a sphere that does an alpha fade to UIColor.clearColor and it works.
However, I'm trying to do this type of thing:
While placing some strategic spheres around makes for an interesting effect (similar to LED backlights in strategic places), I would love to get a true glow. How can I draw a glow around a rounded rectangle in drawRect?
You can create a glow effect around any path using CGContextSetShadowWithColor, but you don't get precise control over the appearance. In particular, the default shadow is fairly light:
And the only way I know of to make it darker is to draw it again over itself:
Not optimal, but it approximates what you want pretty well.
Those images were generated by the following drawRect:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
int padding = 20;
CGPathMoveToPoint(path, NULL, padding, padding);
CGPathAddLineToPoint(path, NULL, rect.size.width - padding, rect.size.height / 2);
CGPathAddLineToPoint(path, NULL, padding, rect.size.height - padding);
CGContextSetShadowWithColor(context, CGSizeZero, 20, UIColor.redColor.CGColor);
CGContextSetFillColorWithColor(context, UIColor.blueColor.CGColor);
CGContextAddPath(context, path);
CGContextFillPath(context);
// CGContextAddPath(context, path);
// CGContextFillPath(context);
CGPathRelease(path);
}
One thing to bear in mind is that rendering fuzzy shadows is fairly expensive, which may or may not be a problem depending on how often your views are redrawn. If the shadows don't need to animate, consider rendering them to a UIImage once and just displaying the result in a UIImageView.