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.
Related
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.)
I have been searching for a quite while but couldn't find any answer to this, anyways, I am working on Google Maps for iOS using Obj C and have drawn routes (polyline) using multiple coordinates provided to me by the server in the form of an array. But the problem is that I want to show arrow heads on that line so that the direction can be seen on the map. Please help.
Here's a function which draws a nice little arrow line. It has some parameters you can tweak:
void TRDrawLineWithArrow(CGContextRef CXT, CGPoint FROMPOINT, CGPoint TOPOINT, CGFloat WIDTH, CGFloat ARROWSIZEMULTIPLE)
{
CGFloat rise = TOPOINT.y - FROMPOINT.y;
CGFloat run = TOPOINT.x - FROMPOINT.x;
// trig
CGFloat length = sqrt(rise*rise + run+run);
CGFloat angle = atan2(rise, run);
// the length of our arrowhead
CGFloat arrowLen = WIDTH*ARROWSIZEMULTIPLE;
// push graphics context
CGContextSaveGState(CXT);
// transform context according to line's origin and angle
CGContextTranslateCTM(CXT, FROMPOINT.x, FROMPOINT.y);
CGContextRotateCTM(CXT, angle);
// draw straight line
CGContextMoveToPoint(CXT, 0, -WIDTH/2.);
CGContextAddLineToPoint(CXT, 0, WIDTH/2.);
CGContextAddLineToPoint(CXT, length-arrowLen, WIDTH/2.);
// draw arrowhead
CGContextAddLineToPoint(CXT, length-arrowLen, (WIDTH*ARROWSIZEMULTIPLE)/2.);
CGContextAddLineToPoint(CXT, length, 0);
CGContextAddLineToPoint(CXT, length-arrowLen, -(WIDTH*ARROWSIZEMULTIPLE)/2.);
CGContextAddLineToPoint(CXT, length-arrowLen, -WIDTH/2.);
CGContextAddLineToPoint(CXT, 0, -WIDTH/2.);
// fill the path
CGContextFillPath(CXT);
// pop graphics context
CGContextRestoreGState(CXT);
}
You would call it from a UIView like this:
CGContextRef cxt = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(cxt, [UIColor blackColor].CGColor);
TRDrawLineWithArrow(cxt, CGPointMake(10,10), CGPointMake(300,100), 5, 3);
i am drawing Custom shape using Core Graphics and i want to make Rounded Corners for this shape
this is my code of Drawing my custom Shape
CGPoint p1=[self getPointFromAngleQuarter:start_angle2 andRaduis:card.small_Raduis andCenter:center];
CGContextMoveToPoint(context, p1.x, p1.y);
CGPoint p2=[self getPointFromAngleQuarter:start_angle2 andCenter:center andRaduis:self.large_Raduis];
CGContextAddLineToPoint(context, p2.x, p2.y);
CGContextAddArc(context,center.x, center.y, selectedLargeRaduis, start, end,0);
CGPoint p5=[self getPointFromAngle:end_Angle andCenter:center andRaduis:self.small_Raduis];
CGContextAddLineToPoint(context, p5.x, p5.y);
CGContextAddArc(context,center.x, center.y,selectedSmallRaduis, end, start,1);
CGContextDrawPath(context, kCGPathFill);
and here is the final Result of my custom Shape
Custom Shape:
If this shape is a solid color, the easy solution is to use a very wide line width, plus a round line cap and round line join. I presume, though, that you want this rounded shape to lay entirely inside the shape you included in your picture. Then the trick is to offset the arcs you draw by an amount equal to corner radius of the path (and stroke the line with twice the width of the corner radius).
For example, considering this diagram (which is not the desired shape, but shows us how to get there):
The black shape in the background is your original shape. The white path is the path I'm going to draw to achieve the rounded corners. The light gray is that path stroked with a large line width, a rounded line join, and a rounded line cap. The dark gray is that path filled in with another color.
So hopefully this illustrates the idea. Create a new path, offset by the corner radius, and drawn with a line width twice the corner radius. If you simply draw the new path with a solid back stroke (replacing the light gray in the above image) and solid black fill (replacing the dark gray in the above image), you get your desired shape:
Here is routine to get the path (the white line in my first image) in Objective-C:
- (UIBezierPath *)arcWithRoundedCornerAt:(CGPoint)center
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
innerRadius:(CGFloat)innerRadius
outerRadius:(CGFloat)outerRadius
cornerRadius:(CGFloat)cornerRadius {
CGFloat innerTheta = asin(cornerRadius / 2.0 / (innerRadius + cornerRadius)) * 2.0;
CGFloat outerTheta = asin(cornerRadius / 2.0 / (outerRadius - cornerRadius)) * 2.0;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:center
radius:innerRadius + cornerRadius
startAngle:endAngle - innerTheta
endAngle:startAngle + innerTheta
clockwise:false];
[path addArcWithCenter:center
radius:outerRadius - cornerRadius
startAngle:startAngle + outerTheta
endAngle:endAngle - outerTheta
clockwise:true];
[path closePath];
return path;
}
Or in Swift 3:
private func arcWithRoundedCorners(at center: CGPoint, startAngle: CGFloat, endAngle: CGFloat, innerRadius: CGFloat, outerRadius: CGFloat, cornerRadius: CGFloat) -> UIBezierPath {
let innerTheta = asin(cornerRadius / 2 / (innerRadius + cornerRadius)) * 2
let outerTheta = asin(cornerRadius / 2 / (outerRadius - cornerRadius)) * 2
let path = UIBezierPath()
path.addArc(withCenter: center, radius: innerRadius + cornerRadius, startAngle: endAngle - innerTheta, endAngle: startAngle + innerTheta, clockwise: false)
path.addArc(withCenter: center, radius: outerRadius - cornerRadius, startAngle: startAngle + outerTheta, endAngle: endAngle - outerTheta, clockwise: true)
path.close()
return path
}
(You can do the above with Core Graphics calls if you want, but I generally use UIBezierPath.)
If, though, you needed the fill to be a different color than the stroke, then the process is more complicated, because you can't just use this technique. Instead, you actually have to define a path that is an outline of the above shape, but consists of drawing not only the two big arcs, but four little arcs for each of the corners. It's tedious, but simple, trigonometry to construct that path, but I wouldn't go through that effort unless you had to.
i refered from this example
i will like to draw the line from 6 o'clock instead of 3 o'clock
//Create the path
CGContextAddArc(ctx, self.frame.size.width/2, self.frame.size.height/2, radius, 0, M_PI *2, 0);
//Set the stroke color to black
[[UIColor colorWithRed:241.0/255.0 green:90.0/255.0 blue:36.0/255.0 alpha:1.0]setStroke];
//Define line width and cap
CGContextSetLineWidth(ctx, TB_BACKGROUND_WIDTH);
CGContextSetLineCap(ctx, kCGLineCapButt);
//draw it!
//CGContextDrawPath(ctx, kCGPathFill);
CGContextDrawPath(ctx, kCGPathStroke);
currently
required to be like this drawing from 6 o'clock instead of 3 0'clock
For CGContextAddArc(), the method signature is as follow:
void CGContextAddArc (
CGContextRef c,
CGFloat x,
CGFloat y,
CGFloat radius,
CGFloat startAngle,
CGFloat endAngle,
int clockwise
);
To modify the start position from 6 o'clock to 3 o'clock , that is to set the startAngle to -90 degree, which is :
angle in degree = angle in radian x 180 / M_PI
i.e.
-90 = radian x 180 / M_PI
radian = -90 x M_PI / 180
radian = -M_PI / 2
modify the 1st line as follow :
CGContextAddArc(ctx, self.frame.size.width/2, self.frame.size.height/2, radius, -M_PI/2, ToRad(270), 0);
Note: the actual direction of the final path is dependent on the current transformation matrix of the graphics context.
This is for an iPad application, but it is essentially a math question.
I need to draw a circular arc of varying (monotonically increasing) line width. At the beginning of the curve, it would have a starting thickness (let's say 2pts) and then the thickness would smoothly increase until the end of the arc where it would be at its greatest thickness (let's say 12pts).
I figure the best way to make this is by creating a UIBezierPath and filling the shape. My first attempt was to use two circular arcs (with offset centers), and that worked fine up to 90°, but the arc will often be between 90° and 180°, so that approach won't cut it.
My current approach is to make a slight spiral (one slightly growing from the circular arc and one slightly shrinking) using bezier quad or cubic curves. The question is where do I put the control points so that the deviation from the circular arc (aka the shape "thickness") is the value I want.
Constraints:
The shape must be able to start and end at an arbitrary angle (within 180° of each other)
The "thickness" of the shape (deviation from the circle) must start and end with the given values
The "thickness" must increase monotonically (it can't get bigger and then smaller again)
It has to look smooth to the eye, there can't be any sharp bends
I am open to other solutions as well.
My approach just constructs 2 circular arcs and fills the region in between. The tricky bit is figuring out the centers and radii of these arcs. Looks quite good provided the thicknesses are not too large. (Cut and paste and decide for yourself if it meet your needs.) Could possibly be improved by use of a clipping path.
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
// As appropriate for iOS, the code below assumes a coordinate system with
// the x-axis pointing to the right and the y-axis pointing down (flipped from the standard Cartesian convention).
// Therefore, 0 degrees = East, 90 degrees = South, 180 degrees = West,
// -90 degrees = 270 degrees = North (once again, flipped from the standard Cartesian convention).
CGFloat startingAngle = 90.0; // South
CGFloat endingAngle = -45.0; // North-East
BOOL weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection = YES; // change this to NO if necessary
CGFloat startingThickness = 2.0;
CGFloat endingThickness = 12.0;
CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat meanRadius = 0.9 * fminf(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0);
// the parameters above should be supplied by the user
// the parameters below are derived from the parameters supplied above
CGFloat deltaAngle = fabsf(endingAngle - startingAngle);
// projectedEndingThickness is the ending thickness we would have if the two arcs
// subtended an angle of 180 degrees at their respective centers instead of deltaAngle
CGFloat projectedEndingThickness = startingThickness + (endingThickness - startingThickness) * (180.0 / deltaAngle);
CGFloat centerOffset = (projectedEndingThickness - startingThickness) / 4.0;
CGPoint centerForInnerArc = CGPointMake(center.x + centerOffset * cos(startingAngle * M_PI / 180.0),
center.y + centerOffset * sin(startingAngle * M_PI / 180.0));
CGPoint centerForOuterArc = CGPointMake(center.x - centerOffset * cos(startingAngle * M_PI / 180.0),
center.y - centerOffset * sin(startingAngle * M_PI / 180.0));
CGFloat radiusForInnerArc = meanRadius - (startingThickness + projectedEndingThickness) / 4.0;
CGFloat radiusForOuterArc = meanRadius + (startingThickness + projectedEndingThickness) / 4.0;
CGPathAddArc(path,
NULL,
centerForInnerArc.x,
centerForInnerArc.y,
radiusForInnerArc,
endingAngle * (M_PI / 180.0),
startingAngle * (M_PI / 180.0),
!weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
);
CGPathAddArc(path,
NULL,
centerForOuterArc.x,
centerForOuterArc.y,
radiusForOuterArc,
startingAngle * (M_PI / 180.0),
endingAngle * (M_PI / 180.0),
weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
);
CGContextAddPath(context, path);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillPath(context);
CGPathRelease(path);
}
One solution could be to generate a polyline manually. This is simple but it has the disadvantage that you'd have to scale up the amount of points you generate if the control is displayed at high resolution. I don't know enough about iOS to give you iOS/ObjC sample code, but here's some python-ish pseudocode:
# lower: the starting angle
# upper: the ending angle
# radius: the radius of the circle
# we'll fill these with polar coordinates and transform later
innerSidePoints = []
outerSidePoints = []
widthStep = maxWidth / (upper - lower)
width = 0
# could use a finer step if needed
for angle in range(lower, upper):
innerSidePoints.append(angle, radius - (width / 2))
outerSidePoints.append(angle, radius + (width / 2))
width += widthStep
# now we have to flip one of the arrays and join them to make
# a continuous path. We could have built one of the arrays backwards
# from the beginning to avoid this.
outerSidePoints.reverse()
allPoints = innerSidePoints + outerSidePoints # array concatenation
xyPoints = polarToRectangular(allPoints) # if needed
A view with a spiral .. 2023
It's very easy to draw a spiral mathematically and there are plenty of examples around.
https://github.com/mabdulsubhan/UIBezierPath-Spiral/blob/master/UIBezierPath%2BSpiral.swift
Put it in a view in the obvious way:
class Example: UIView {
private lazy var spiral: CAShapeLayer = {
let s = CAShapeLayer()
s.strokeColor = UIColor.systemPurple.cgColor
s.fillColor = UIColor.clear.cgColor
s.lineWidth = 12.0
s.lineCap = .round
layer.addSublayer(s)
return s
}()
private lazy var sp: CGPath = {
let s = UIBezierPath.getSpiralPath(
center: bounds.centerOfCGRect(),
startRadius: 0,
spacePerLoop: 4,
startTheta: 0,
endTheta: CGFloat.pi * 2 * 5,
thetaStep: 10.radians)
return s.cgPath
}()
override func layoutSubviews() {
super.layoutSubviews()
clipsToBounds = true
spiral.path = sp
}
}