Is there a way to let users draw a triangle using a circle (transparent circle) in iOS. I was thinking setting three equal points in the circle to create the triangle and as it stretch the circle the triangle gets build. Maybe using bezierPathWithArcCenter or bezierPathWithOvalInRect. Anyone ever done this?
Assuming you've already got gesture recognizers or some other system in place for capturing the location of the user's touches, doing the drawing itself is straightforward.
CGContextRef context = UIGraphicsGetCurrentContext();
// Draw circle
CGFloat derivedRadius = self.radius * self.scale;
CGPoint origin = CGPointMake(self.center.x - derivedRadius, self.center.y - derivedRadius);
CGContextAddEllipseInRect(context, CGRectMake(origin.x, origin.y, derivedRadius * 2, derivedRadius * 2));
// Draw triangle
CGFloat t1 = self.rotation;
CGFloat t2 = self.rotation + ((2 * M_PI) / 3);
CGFloat t3 = self.rotation + ((4 * M_PI) / 3);
CGPoint p1 = CGPointMake(self.center.x + cosf(t1) * derivedRadius, self.center.y + sinf(t1) * derivedRadius);
CGPoint p2 = CGPointMake(self.center.x + cosf(t2) * derivedRadius, self.center.y + sinf(t2) * derivedRadius);
CGPoint p3 = CGPointMake(self.center.x + cosf(t3) * derivedRadius, self.center.y + sinf(t3) * derivedRadius);
CGPoint endpoints[] = { p1, p2, p3, p1 };
CGContextAddLines(context, endpoints, 4);
CGContextStrokePath(context);
Where self.radius, self.scale, self.center, and self.rotation are properties that capture the result of the user's gestures. Rotation should be in radians.
Related
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 am drawing a curved line using cubed bezier curve (UIBezierPath class) using two control points that are in opposite corners of our points. I need to draw it with gradient that depends on y value. I appreciate any help because now I've stucked in. Here is my code:
CGPoint point1 = CGPointMake(50, 70);
CGPoint point2 = CGPointMake(70, 75);
bool drawACurve;
drawACurve = true;
float tangent;
if ((int)(point2.y - point1.y) == 0) {
tangent = 0.0f;
drawACurve = false;
}
else {
tangent = (point2.x - point1.x) / (point2.y - point1.y);
}
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 setLineWidth:1.0];
[[UIColor blueColor] set];
if (drawACurve)
{
// Draw a curve
float factor;
CGPoint controlPoint1;
CGPoint controlPoint2;
factor = MAX_FACTOR;
if (fabs(tangent) >= 1.0 && fabs(tangent) < TANGENT_BOUND_FOR_LINE)
factor = MIN_FACTOR;
if (tangent > 0) {
controlPoint1 = CGPointMake(point2.x - point2.x * factor, point1.y - point1.y * factor);
controlPoint2 = CGPointMake(point1.x + point1.x * factor, point2.y + point2.y * factor);
}
else {
// For tangent less than zero
controlPoint1 = CGPointMake(point2.x - point2.x * factor, point1.y + point1.y * factor);
controlPoint2 = CGPointMake(point1.x + point1.x * factor, point2.y - point2.y * factor);
}
[path1 moveToPoint:point1];
[path1 addCurveToPoint:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2];
}
else
{
// Draw a line
[path1 moveToPoint:point1];
[path1 addLineToPoint:point2];
}
[path1 stroke];
If I've understood you correctly, here's one solution. I've drawn the path using a simple blue-to-green gradient, varying in the y-direction. I've commented the code to explain what it's doing.
CGPoint point1 = CGPointMake(50, 70);
CGPoint point2 = CGPointMake(70, 75);
bool drawACurve;
drawACurve = true;
float tangent;
if ((int)(point2.y - point1.y) == 0) {
tangent = 0.0f;
drawACurve = false;
}
else {
tangent = (point2.x - point1.x) / (point2.y - point1.y);
}
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 setLineWidth:1.0];
if (drawACurve)
{
// Draw a curve
float factor;
CGPoint controlPoint1;
CGPoint controlPoint2;
factor = MAX_FACTOR;
if (fabs(tangent) >= 1.0 && fabs(tangent) < TANGENT_BOUND_FOR_LINE)
factor = MIN_FACTOR;
if (tangent > 0) {
controlPoint1 = CGPointMake(point2.x - point2.x * factor, point1.y - point1.y * factor);
controlPoint2 = CGPointMake(point1.x + point1.x * factor, point2.y + point2.y * factor);
}
else {
// For tangent less than zero
controlPoint1 = CGPointMake(point2.x - point2.x * factor, point1.y + point1.y * factor);
controlPoint2 = CGPointMake(point1.x + point1.x * factor, point2.y - point2.y * factor);
}
[path1 moveToPoint:point1];
[path1 addCurveToPoint:point2 controlPoint1:controlPoint1 controlPoint2:controlPoint2];
}
else
{
// Draw a line
[path1 moveToPoint:point1];
[path1 addLineToPoint:point2];
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // You could choose another color space.
CGFloat locations[2] = {0.f, 1.f}; // Locations for a simple linear gradient with a start color and end color at either end of the drawing points.
CGFloat colorComponents[8] = {0.f, 0.f, 1.f, 1.f, // Start blue
0.f, 1.f, 0.f, 1.f}; // Finish green
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colorComponents, locations, 2); // Create the gradient. Can also be created with CGGradientCreateWithColors().
CGColorSpaceRelease(colorSpace);
CGPathRef outerPath = CGPathCreateCopyByStrokingPath(path1.CGPath, NULL, path1.lineWidth, path1.lineCapStyle, path1.lineJoinStyle, path1.miterLimit); // This function creates a path that outlines another, using various stroke options. Pass in the stroke options from path1.
// From the docs: The new path is created so that filling the new path draws the same pixels as stroking the original path.
CGContextRef context = UIGraphicsGetCurrentContext(); // Get the current graphics context.
CGContextAddPath(context, outerPath); // Add the new path.
CGContextClip(context); // Clip the context to the path.
CGRect boundingBox = CGPathGetBoundingBox(outerPath); // I'm just doing this to get the startPoint and endPoint for the gradient drawing.
CGPathRelease(outerPath);
CGPoint startPoint = boundingBox.origin;
CGPoint endPoint = CGPointMake(boundingBox.origin.x, CGRectGetHeight(boundingBox)); // Different from startPoint in y value, as requested.
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0); // Draw the gradient.
CGGradientRelease(gradient);
I believe for some could be very simple to answer and help me.
I have a circle defined in drawRect and wrote a code to define arc of the circle.
CGFloat width = rect.size.width-rect.origin.x;
CGFloat height = rect.size.height-rect.origin.y;
CGFloat xPos = rect.origin.x;
CGFloat yPos = rect.origin.y;
CGFloat arcStake = (width * 2) * 0.25;
CGFloat radius = height/2;
CGPoint centre = CGPointMake(xPos+width/2, yPos+height/2);
CGFloat angle = acos(arcStake/(2*radius));
CGFloat startAng = radians(180) + angle;
CGFloat endAng = radians(360) - angle;
// Define 2 CGPoints of arc
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, centre.x, centre.y, radius, startAng, endAng, 0);
CGPathAddLineToPoint(path, NULL, xPos+width/2, yPos+height/2);
CGPathCloseSubpath(path);
What I want is to define 2 CGPoints of arc.
Here is the image to make it clearer.
A point on a circle with radius r at angle a (where a is measured from the rightmost point of the circle has the following coordinates:
x = r*cos(a) + center.x
y = r*sin(a) + center.y
Currently in my drawRect: method I am drawing a line between every point in my set of points, each represented as a CGPoint; however, now I would like to fill in the area that is within the region of these set points. I cannot figure how to do this using the quartz api, is there a way?
Right now, the points are ordered. So it is possible to recognize what point represents the first point of the polygon and so on.
Add your points to an UIBezierPath and then use it's fill method.
This code sample from Apple shows what you need to do:
-(void)drawInContext:(CGContextRef)context
{
// Drawing with a white stroke color
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
// Drawing with a blue fill color
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
CGPoint center;
// Add a star to the current path
center = CGPointMake(90.0, 90.0);
CGContextMoveToPoint(context, center.x, center.y + 60.0);
for(int i = 1; i < 5; ++i)
{
CGFloat x = 60.0 * sinf(i * 4.0 * M_PI / 5.0);
CGFloat y = 60.0 * cosf(i * 4.0 * M_PI / 5.0);
CGContextAddLineToPoint(context, center.x + x, center.y + y);
}
// And close the subpath.
CGContextClosePath(context);
// Now add the hexagon to the current path
center = CGPointMake(210.0, 90.0);
CGContextMoveToPoint(context, center.x, center.y + 60.0);
for(int i = 1; i < 6; ++i)
{
CGFloat x = 60.0 * sinf(i * 2.0 * M_PI / 6.0);
CGFloat y = 60.0 * cosf(i * 2.0 * M_PI / 6.0);
CGContextAddLineToPoint(context, center.x + x, center.y + y);
}
// And close the subpath.
CGContextClosePath(context);
// Now draw the star & hexagon with the current drawing mode.
CGContextDrawPath(context, drawingMode);
}
Note, this was mentioned in an answer to a similar question.