I'm new to core graphics and I'm struggling with a simple task of putting a sweeping circle inside a square. The outcome I got looks like this:
The circle won't appear at the center of the square, and the size of the circle appears much smaller than I specified.
Below is my drawRect method for drawing the circle. I have put the printed-out variable values while debugging in the comments for your convenience. I also printed out the value passed to initWithFrame: frame=(0 0; 256 256). The frame is the orange square you see in the picture.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat midX = CGRectGetMidX(self.bounds); // bounds = (0 0; 256 256); midX = 128
CGFloat midY = CGRectGetMidY(self.bounds); // midY = 128
CGFloat radius = midY - 4; // radius = 124
// Outer grey pie
[endColor setFill];
CGContextMoveToPoint(context, midX, midY); // move to center
CGContextAddEllipseInRect(context, CGRectMake(midX - radius, midY - radius, radius * 2, radius * 2)); // adds a circle of radius = square_side_length - 4
CGContextFillPath(context); // fill the circle above with grey
// Show the clock
NSTimeInterval seconds = [[NSDate date] timeIntervalSince1970];
CGFloat mod = fmod(seconds, self.period);
CGFloat percent = mod / self.period;
[fillColor setFill];
CGFloat start = -M_PI_2;
CGFloat end = 2 * M_PI;
CGFloat sweep = end * percent + start;
CGContextMoveToPoint(context, midX, midY);
CGContextAddArc(context, midX, midY, radius, start, sweep, 0); // radius = square_side_length - 24
CGContextFillPath(context);
// Innermost white pie
radius -= 50; // radius = square_side_length - 54
[bgColor setFill]; // white
CGContextMoveToPoint(context, midX, midY);
CGContextAddEllipseInRect(context, CGRectMake(midX - radius, midY - radius, radius * 2, radius * 2));
CGContextFillPath(context);
}
And below is the code that adds the clock to its superview:
clock = [[ProgressClock alloc] initWithFrame:self.clockHolder.bounds // bounds=[0 0; 256 256]
period:[TOTPGenerator defaultPeriod]
bgColor:[UIColor whiteColor]
strokeColor:[UIColor colorWithWhite:0 alpha:0.2]
fillColor:[UIColor blueColor]
endColor:[UIColor grayColor]
shade:NO];
[self.clockHolder addSubview:clock];
Can anyone spot the mistake I made? Thanks in advance.
Thanks a lot to #originaluser2's comment, I have fixed this issue simply by moving the clock presenting logic from viewDidLoad to viewDidAppear and the clock showed up perfectly. There was nothing wrong with the drawing code I posted; however the auto-layout initialization and the animation of my clock happened in a sequence that gave my drawing canvas a wrong frame. By putting the drawing logic in viewDidAppear, we are guaranteed that all the auto-layout setup has been completed, thus frames are fixed, before continue onto drawing the circle.
This code is creating a UIBezierPath, which is ROUND in shape as given image.
CGPoint arcCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
CGFloat radius = CGRectGetWidth(self.bounds) / 2.0f;
float angle = value * 360.0;
float x = radius * sin(angle*M_PI/180.0);
float y = radius * cos(angle*M_PI/180.0);
CGPoint capArcCenter = CGPointMake(arcCenter.x + x, arcCenter.y - y);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:capArcCenter
radius:self.gaugeWidth*_lineLayer.shadowRadius / 2.0f
startAngle:(3.0f * M_PI_2)
endAngle:(3.0f * M_PI_2) + (2.0f * M_PI)
clockwise:YES];
return path;
I want this as Line crossing vertically, as below....
How to create a UIBezierPath, crossing like SECONDS line in clock.
Thanks.
Assuming you are trying to make a separate UIBezierPath to do this I would suggest simply create the path you want to represent that line say:
myPath = [UIBezierPath bezierPath];
[myPath moveToPoint:startpoint];
[myPath addLineToPoint:endpoint];
And then apply a CGAffineTransform to it to rotate it around the gauge face.
Hopefully that helps
I want to create a perfect semi circle at the bottom of my view and now I'm getting just an arc in the top of a rectangle (see attached picture). This is the code I'm using is the following where circularRect is origin = (x = 128, y = 514), size = (width = 64, height = 54)
CGFloat arcHeight = 60.0;
CGRect arcRect = CGRectMake(circularRect.origin.x, circularRect.origin.y + circularRect.size.height - arcHeight, circularRect.size.width, arcHeight);
CGFloat arcRadius = 60;
CGPoint arcCenter = CGPointMake(arcRect.origin.x + arcRect.size.width/2, arcRect.origin.y + arcRadius);
CGFloat angle = acos(arcRect.size.width / (2*arcRadius));
CGFloat startAngle = 270 * M_PI/180 + angle;
CGFloat endAngle = 90 * M_PI/180 - angle;
CGContextAddArc(context, arcCenter.x, arcCenter.y, arcHeight, startAngle, endAngle, 1);
CGContextClip(context);
CGContextClearRect(context, arcRect);
CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
CGContextFillRect( context, arcRect);
What I'm doing wrong? Thanks!
The radius you're supplying to the arc function is too large. Your rectangle width being 64, you can only fit a circle that has a radius of 32 or less in it.
Our designer has asked me to recreate this:
Im subclassing UIView, and I've overridden the drawRect command like this:
[super drawRect:frame];
CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y;
CGFloat w = self.frame.size.width;
CGFloat h = self.frame.size.height;
CGFloat lineWidth = lineWidthRequested;
CGPoint centerPoint = CGPointMake(w/2, h/2);
CGFloat radius = radiusRequested;
CGFloat startAngle = 3 * M_PI / 2;
CGFloat endAngle = startAngle + percentage * 2 * M_PI;
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
NO);
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor colorWithRed:239/255.0 green:101/255.0 blue:47/255.0 alpha:1.0].CGColor);
CGContextDrawPath(c, kCGPathFill);
What I ended up with is this:
Gotta still draw the arrowhead. Gonna be easy, right?
After struggling to remember my trig, I found rotation of points around a center on this page:
Rotating a point around an origin in VB
But when I tried translation to objective C to draw the arrowhead, I'm getting very odd results. Here's the code further down in drawRect:
CGFloat triangle[3][2] = {{centerPoint.x + 10, h - (centerPoint.y + radius)},
{centerPoint.x, h - (centerPoint.y + radius + lineWidth/2)},
{centerPoint.x, h - (centerPoint.y + radius - lineWidth/2)}};
for (int idx=0;idx < 3; idx++) {
// translate to origin
triangle[idx][0] -= centerPoint.x;
triangle[idx][1] -= centerPoint.y;
}
CGFloat angDistance = endAngle - startAngle;
CGFloat ct = cos(angDistance);
CGFloat st = sin(angDistance);
for (int idx=0;idx < 3; idx++) {
// rotate
triangle[idx][0] = ct * triangle[idx][0] - st * triangle[idx][1];
triangle[idx][1] = -st * triangle[idx][0] + ct * triangle[idx][1];
}
for (int idx=0;idx < 3; idx++) {
// translate back to position
triangle[idx][0] += centerPoint.x;
triangle[idx][1] += centerPoint.y;
}
NSLog(#"Rotating through %g, %06.1f,%06.1f , ct - %g, st - %g",angDistance, triangle[0][0],triangle[0][1],ct, st);
// XXX todo draw the filled triangle at end.
// draw a red triangle, the point of the arrow
CGContextSetFillColorWithColor(c, [[UIColor greenColor] CGColor]);
CGContextMoveToPoint(c, triangle[0][0], triangle[0][1]);
CGContextAddLineToPoint(c, triangle[1][0], triangle[1][1]);
CGContextAddLineToPoint(c, triangle[2][0], triangle[2][1]);
CGContextFillPath(c);
I was expecting that I make these points, then translate them to an origin, rotate, and then translate them back, I'd be laughing.
However, that's not what's happening...as the percentage increases from 0 to 2pi, the arrowhead draws itself in a vaguely triangular route. When the angDistance is zero or pi, the arrowhead is in the right place. As I head towards pi/2 or 3pi/2 though, the arrowhead heads off towards the lower corners of an enclosing rect.
I must be doing something blatantly stupid, but I can't for the life of me see it.
Any ideas?
Thanks,
-Ken
I'd suggest constructing a path for the entire outline of the desired shape and then "fill" that path with the desired color. That eliminates any risk of any gaps or anything not quite lining up.
Thus, this path might consisting of an arc for the outside of the arrow, two lines for the head of the arrow, an arc back for the inside of the arrow, and then close the path. That might look like:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, self.arrowColor.CGColor);
CGPoint center = CGPointMake(rect.size.width / 2.0, rect.size.height / 2.0);
CGContextMoveToPoint(context, center.x + cosf(self.startAngle) * (self.radius + self.lineWidth / 2.0),
center.y + sinf(self.startAngle) * (self.radius + self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius + self.lineWidth / 2.0, self.startAngle, self.endAngle, !self.clockwise);
CGFloat theta = asinf(self.lineWidth / self.radius / 2.0) * (self.clockwise ? 1.0 : -1.0);
CGFloat pointDistance = self.radius / cosf(theta);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle + theta) * pointDistance,
center.y + sinf(self.endAngle + theta) * pointDistance);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle) * (self.radius - self.lineWidth / 2.0),
center.y + sinf(self.endAngle) * (self.radius - self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius - self.lineWidth / 2.0, self.endAngle, self.startAngle, self.clockwise);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
}
The only trick here was coming up with the right point for the end of the arrow. I've improved the choice to handle fatter arrows a little better, but you should feel free to use whatever you feel is best for your application.
Thus, the following code:
self.arrowView.radius = 100;
self.arrowView.arrowColor = [UIColor blueColor];
self.arrowView.lineWidth = 40;
self.arrowView.startAngle = -M_PI_2;
self.arrowView.endAngle = M_PI;
self.arrowView.clockwise = TRUE;
would yield the following (which I'm animating with a CADisplayLink):
This uses the start angle of zero as meaning the "3 o'clock" position, but you can obviously tweak this as you see fit. But hopefully it illustrates one approach to the problem.
By the way, while I've answered the question of how to do this with CoreGraphics, I wouldn't necessarily suggest doing so. For example, in https://github.com/robertmryan/CircularArrowDemo, I don't implement drawRect, but instead update a CAShapeLayer. By doing this, not only do I avoid drawRect inefficiencies, but one could theoretically also change how you use this CAShapeLayer (e.g. use it as a mask for some UIView, revealing some more interesting color gradation (or other image) behind it).
Here is another solution (not very scalable though). This solution assumes this is like a logo where the angle/percent of circle drawn will not change.
- (void)drawRect:(CGRect)rect {
UIBezierPath *circleOutline = [UIBezierPath bezierPath];
[self.circleColor setStroke];
[circleOutline setLineWidth:self.bounds.size.width*0.15];
[circleOutline addArcWithCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/2-circleOutline.lineWidth/2 startAngle:3*M_PI/2 endAngle:3*M_PI/4 clockwise:YES];
[circleOutline stroke];
[self addArrowView:circleOutline];
}
- (void)addArrowView:(UIBezierPath *)path {
for (int x = 0; x < self.bounds.size.width/2; x++) {
for (int y = self.bounds.size.height/2; y < self.bounds.size.height; y++) {
if ([path containsPoint:CGPointMake(x, y)]) {
// Pythagorean Theorem - We want the diagonal length of the square to be lineWidth, so we need to calculate what size
// to make each side of the square to make the diagonal equal to lineWidth
double sideLength = sqrt((path.lineWidth*path.lineWidth)/2);
UIView *arrowView = [[UIView alloc] initWithFrame:CGRectMake(x-sideLength/2, y-sideLength/2, sideLength, sideLength)];
arrowView.backgroundColor = self.circleColor;
[self addSubview:arrowView];
return;
}
}
}
}
would yield:
i have a situation where the user will tap on the screen and enters the degrees and a line should to be draw from the tapped point in the given degree.
i am using the following path code to draw the line but i am not able to draw the line in the correct degree.
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path,NULL,touchx,touchy);
CGPathAddLineToPoint(path, NULL,endX, endY);
lineLayer = [CAShapeLayer layer];
[lineLayer setPath:path];
[lineLayer setFillColor:[[UIColor lightTextColor] CGColor]];
[lineLayer setStrokeColor:[[UIColor blackColor] CGColor]];
[lineLayer setAnchorPoint:CGPointMake(0.0f, 0.0f)];
[lineLayer setPosition:CGPointMake(0.0f, 0.0f)];
[drawingview.layer addSublayer:lineLayer];
CGPathRelease(path);
You posted code that creates a path with a start point, and and end point, but did not post the most important bit: how you calculate the end point.
I assume that's the part you need help with.
You need to use basic trigonometry. Trig on iOS is based on radians, so you will need to convert degrees to radians:
CGFloat radians = M_PI * degrees / 180.0;
Then you need to calculate your end point based on your start point.
Remember the mnemonic of the Indian chief "SOH CAH TOA" (sine = opposite/hypotenuse, cosine = adjacent/hypotenuse, tangent = opposite/adjacent).
Drawing a diagram of a right triangle with the angle in question on the left and the right angle on the right:
The X measurement is the adjacent side.
So, if cosine = adjacent/hypotenuse, then adjacent = cos * hypotenuse.
The hypotenuse of the triangle is the radius of the circle. Replacing variables:
x = cos(angle) * radius
The y measurement is the opposite side.
If sine = opposite/hypotenuse, then opposite = sine * hypotenuse.
The hypotenuse of the triangle is the radius of the circle. Replacing variables:
y = sin(angle) * radius
So your code to calculate the end-point might look like this:
CGFloat radius = 50; //Picked out of thin air; decide on a radius value
CGFloat deltaX = radius * cosf(radians);
CGFloat deltaY = radius * sinf(radians);
endX = touchX+deltaX;
endy = touchY+deltaY;