Is there a better alternative to CGContext? - ios

I'm creating a funnel shape using CGContext, by first drawing a triangle, followed by a line. I'm implementing this in drawRect of my UIView subclass, but wondering if there is an alternative way of drawing this, because the lines are a bit blurry.
override func drawRect(rect: CGRect) {
let context: CGContextRef = UIGraphicsGetCurrentContext()!
CGContextClearRect(context, rect);
let rectWidth: CGFloat = 15
// Line
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1)
CGContextMoveToPoint(context, rectWidth / 2, 5)
CGContextAddLineToPoint(context, rectWidth / 2, 10)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, 3.0)
CGContextStrokePath(context)
// Triangle
CGContextBeginPath(context);
CGContextMoveToPoint (context, CGRectGetMinX(rect), CGRectGetMinY(rect));
CGContextAddLineToPoint(context, CGRectGetMidX(rect), 8);
CGContextAddLineToPoint(context, CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGContextClosePath(context);
UIColor.blueColor().setFill()
CGContextFillPath(context);
}
Produces this:
But when I import an image that I designed, the image is crisper (you can tell on an iPhone):
So, is there a better way of achieving this output? The CGContext lines are a bit blurry, so I'd like to know if there is a better alternative and what the pros and cons to those methods are.

Core Graphics is drawing things blurry probably because you told it to. The location of each pair of integer coordinates in the CG coordinate system is exactly on the grid lines — if you draw a one point wide stroke between such coordinates, you get a stroke that straddles the grid line by half a point on either side. Depending on the scale of the bitmap context you're drawing into, that could result in two columns of half-shaded pixels instead of one column of fully-shaded pixels — that is, a blurry line.
In your case, some of the points you're drawing lines between fall on whole-pixel boundaries and some on half-pixel boundaries, so on 1x displays some of the lines will be blurry and on 2x or 3x displays others will be. Read about Points versus Pixels in Apple's docs on drawing, and you'll probably find a way to offset your coordinates that makes the lines all fall where you want them to.

Related

Elliptical radial gradient in CGContext?

As far as I can tell you can use two methods to draw gradients in a CGContext, that's drawLinearGradient and drawRadialGradient. What I'm looking for is a way to define an elliptical gradient where I can define x and y radii.
An example of this capability in another environment (SVG).
<RadialGradient id="gradient" cx="50" cy="50" rx="20" ry="40" fx="150" fy="75">
The existing declaration for drawRadialGradient is as follows.
func drawRadialGradient(_ gradient: CGGradient,
startCenter: CGPoint,
startRadius: CGFloat,
endCenter: CGPoint,
endRadius: CGFloat,
options: CGGradientDrawingOptions)
Both start and end radii are scalar values, so all you can do is circles. How can I draw elliptical gradients in a CGContext?
You should be able to scale the context and use CGContextDrawRadialGradient(). If you scale down, there should be no artifacts. Does the following work?
CGContextRef context;
CGGradientRef gradient;
CGGradientDrawingOptions options;
CGPoint center;
CGFloat radiusX;
CGFloat radiusY;
CGFloat radius = MAX(radiusX, radiusY);
CGContextSaveGState(context);
// scale down by the smaller dimension, and translate so the center stays in place
if (radiusX < radiusY) {
CGContextTranslateCTM(context, center.x - (center.x * (radiusX / radiusY)), 0);
CGContextScaleCTM(context, radiusX / radiusY, 1.0);
}
else {
CGContextTranslateCTM(context, 0, center.y - (center.y * (radiusY / radiusX)));
CGContextScaleCTM(context, 1.0, radiusY / radiusX);
}
CGContextDrawRadialGradient(context, gradient, center, 0, center, radius, options);
CGContextRestoreGState(context);
Incidentally, I think this is roughly the behavior if you set the type property of CAGradientLayer to the undocumented and private value of #"radial". It uses the startPoint as the center, and the difference of the startPoint and endPoint to determine the radiusX and radiusY values (i.e. the endPoint defines a corner of the bounding box of the gradient, and the startPoint is the center). It does have odd behavior when you make the start and end point nearly the same, so there is probably more going on there than I have figured out (and probably why Apple never bothered to make it public).
The only thing I can think of would be to apply a scale transform with unequal x and y scale factors to your context before drawing the gradient. That would stretch it out of round, and should make it oval.
There's no "royal road". This facility is not built-in so you'll have to draw every pixel yourself. (There are probably third-party libraries that will do that for you.)

MKOverlayRenderer draw line using CGPath

I'm trying to draw a line using MKOverlayRenderer. My overlay renderer's drawMapRect is roughly:
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, x, y) // x, y = starting point
let remainingPoints: [CGPoint] = ... // remaining points
CGPathAddLines(path, nil, remainingPoints, remainingPoints.count)
CGContextAddPath(context, path)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
CGContextSetLineWidth(context, 2.0)
CGContextStrokePath(context)
This however doesn't work, nothing appears. I also tried stroking with:
CGContextDrawPath(context, .Stroke)
I know my path is defined correctly because if I use .FillStroke, it fills the polygons.
Closing the path using CGPathCloseSubpath(path) doesn't help.
My line width (2.0) was too small the for the zoom level.

2D transforms in ellipse drawing algorithm

I'm trying to draw a 3D ellipse with a 2.5D graphics engine (Core Animation layers) which allow me to only compose my ellipse with line segments that must be moved into place using rotations and translations. I'm having trouble with the order of operations and can't get it to draw properly. Any graphics gurus or game programmers out there who can help me?
Here's an image describing my current approach:
For each segment in the ellipse polygon, I'm first creating a line segment with the correct length, then translating it to the point P1, then rotating the point by the PI/2+theta, but this is clearly not working.
It's been 10 years since I took a graphics class in university, can someone please jog my memory as to what I'm doing wrong?
I accidentally found the solution while cleaning up my code to post here!
The correct procedure is:
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, -size.width/2.0, 0, 0);
transform = CATransform3DTranslate(transform, point.x, 0, point.y);
transform = CATransform3DRotate(transform, M_PI_2+angle, 0, 1, 0);
transform = CATransform3DTranslate(transform, -size.width/2.0, 0, 0);

In iOS, arcs are malformed for certain start angles

I use the following code to draw an arc
double radius = 358.40001058578491;
startAngle = 0.13541347644783652;
double center_x= 684;
double center_y = 440;
std::complex<double> start1( std::polar(radius,startAngle) );
CGPoint targetStart1 = CGPointMake(start1.real() + center_x, start1.imag() +center_y);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, targetStart1.x, targetStart1.y);
CGPathAddArc(path, NULL, center_x, center_y, radius, startAngle, 0.785, 0 );
CGContextAddPath(context, path);
CGContextSetLineWidth( context, 30 );
CGContextSetStrokeColorWithColor( context, targetColor.CGColor);
CGContextStrokePath(context);
CGPathRelease(path);
If u check it in retina, it looks like this:
My arc is the green arc. I have shown the place that the start angle is with a orange line. As I have shown in the red rectangle, there is an extra thing drawn in the very beginning of the arc. This happens not for all start angles, but only for certain start angles.
Do you have any idea why it happens?
Thanks.
In your original question, you specified a literal starting point that was not quite right and, as a result, Core Graphics will draw a line from that point to the start of the arc. And because that starting point was just a few pixels away from the actual start of the arc, it results in that curious rendering you illustrate in your question.
In your revised question, you're calculating the starting point, but I might suggest calculating it programmatically like so:
CGFloat centerX = 684.0;
CGFloat centerY = 440.0;
CGFloat radius = 360.0;
CGFloat startAngle = 0.135;
CGFloat endAngle = 0.785;
CGFloat startingX = centerX + radius * cosf(startAngle);
CGFloat startingY = centerY + radius * sinf(startAngle);
CGContextMoveToPoint(context, startingX, startingY);
CGContextAddArc(context, centerX, centerY, radius, startAngle, endAngle, 0);
CGContextSetLineWidth(context, 30);
CGContextSetStrokeColorWithColor(context, targetColor.CGColor);
CGContextStrokePath(context);
When I calculated it this way, there was no rounding errors that resulted in the artifact illustrated in your original question.
Note, if you're not drawing anything before the arc, you can just omit the CGContextMoveToPoint call altogether. You only need that "move to point" call if you've drawn something before the arc and don't want the path connecting from that CGContextGetPathCurrentPoint to the start of the arc.

Changing the pattern in setLineDash:count:phase: to an arc in ios core graphics

I am drawing an ellipse
CGRect paperRect = self.bounds;
CGRect strokeRect = CGRectInset(paperRect, 5.0, 5.0);
CGContextAddEllipseInRect(context, strokeRect);
CGContextStrokePath(context);
I was hoping to change this to a jarred ellipse (see pic). In search for the best way to do this, I was wondering if there is a way to use
setLineDash:count:phase:
and pass an arc/ curve as the pattern instead of line? Or is there a better way to do this?
UPDATE
I tried the following, as suggested by Wain:
float a = strokeRect.size.width/2;
float b = strokeRect.size.height/2;
float x1,y1,x2,y2,k;
x1=CGRectGetMinX(strokeRect);
y1=CGRectGetMinY(strokeRect);
int maxAngle = 360;
CGContextBeginPath(context);
CGContextMoveToPoint(context, x1, y1);
for(int i=0;i<maxAngle;i+=30){
float cX=cos(DEGREES_TO_RADIANS(i));
float sX=sin(DEGREES_TO_RADIANS(i));
k=1/(sqrt(pow((b*cX),2) + pow(a*sX,2)));
x2=k*a*b*cos(DEGREES_TO_RADIANS(i));
y2=k*a*b*sin(DEGREES_TO_RADIANS(i));
CGContextAddArcToPoint(context, x1, y1, x2, y2, 20);
x1=x2; // make x2 the new x1
y1=y2;
}
CGContextClosePath(context);
CGContextStrokePath(context);
I can see some irregular drawings, but nothing to the extend that makes sense visually.
You don't 'pass a line' as the dash. The dash is a description of how the line is drawn.
You need to create a path (instead of the ellipse) which traces the 'jarred ellipse' shape (using CGContextAddArcToPoint) and then draw that.
The points are all on the edge of the ellipse, you can find them with the calculations described here. I haven't tried it but I guess the first tangent point could be the center point of the ellipse...

Resources