I have a UIView that contains a 'pie' with irregular sized slices and I need them to be clickable regions. Through a lot of trial and error I wrote a method to produce the pie, but now I am at a loss for how to make these slices into regions. Currently this method returns void so I need to modify to return something, like a CGPath? I guess where I'm really confused is how to take what I have and put it into a CGPath and return it. Any suggestions? thanks!
-(void)addClickableRegion:(float)beginDegrees to:(float)endDegrees withContext:(CGContextRef)ctx withRadiusOf:(int)rad{
int x = self.bounds.size.height / 2;
int y = self.bounds.size.width / 2;
float red = (arc4random() % 255)/255.0;
float green = (arc4random() % 255)/255.0;
float blue = (arc4random() % 255)/255.0;
CGContextMoveToPoint(ctx, x, y);
CGContextAddArc(ctx, x, y, rad, (beginDegrees) * M_PI/180.0, (endDegrees) * M_PI/180.0, 0);
CGContextSetRGBFillColor(ctx, red, green, blue, 1.0);
CGContextFillPath(ctx);
CGContextClosePath(ctx);
}
You are already creating paths, you're just creating them in the context rather than creating them and then adding them to the context. You don't need to do that. You can use CGPathCreateMutable, CGPathAddArc and CGContextAddPath.
You can also use UIBezierPath, either via bezierPathWithCGPath: or directly (and then get the CGPath from them).
And, once you have bezier paths, you can call containsPoint: on them to hit test touches.
Related
I have an app with a color wheel and I'm trying to pick a random color within the color wheel. However, I'm having problems verifying that the random point falls within the color wheel.
Here's the code as it currently is:
CGPoint randomPoint = CGPointMake(arc4random() % (int)colorWheel.bounds.size.width, arc4random() % (int)colorWheel.bounds.size.height);
UIColor *randomColor = [self colorOfPoint:randomPoint];
CGPoint pointInView = [colorWheel convertPoint:randomPoint fromView:colorWheel.window];
if (CGRectContainsPoint(colorWheel.bounds, pointInView)) {
NSLog(#"%#", randomColor);
}
else {
NSLog(#"out of bounds");
}
A couple of other methods of verifying the point that I've tried with no luck:
if (CGRectContainsPoint(colorWheel.frame, randomPoint)) {
NSLog(#"%#", randomColor);
}
if ([colorWheel pointInside:[self.view convertPoint:randomPoint toView: colorWheel] withEvent: nil]) {
NSLog(#"%#", randomColor);
}
Sometimes it'll output "out of bounds", and sometimes it'll just output that the color is white (the background around the color wheel is currently white but there's no white in the color wheel image).
The color wheel image is a circle, so I'm not sure if that's throwing off the test, although it seems like white pops up way too frequently for it to just be a transparent square outline around the image giving a white color.
If you want to generate a random point in a circle, you would do better to pick your point in polar coordinates and then convert it to Cartesian.
The polar coordinate space uses two dimesions, radius and angle. Radius is just the distance from the center, and angle usually starts at "due east" for 0, and goes around counter-clockwise up to 2π (that's in radians, 360˚ of course in degrees).
Presumably your wheel is divided into simple wedges, so the radius actually doesn't matter; you just need to pick a random angle.
uint32_t angle = arc4random_uniform(360);
// Radius will just be halfway from the center to the edge.
// This assumes the circle is exactly enclosed, i.e., diameter == width
CGFloat radius = colorWheel.bounds.size.width / 4;
This function will give you a Cartesian point from your polar coordinates. Wikipedia explains the simple math if you're interested.
/** Convert the polar point (radius, theta) to a Cartesian (x,y). */
CGPoint poltocar(CGFloat radius, CGFloat theta)
{
return (CGPoint){radius * cos(theta), radius * sin(theta)};
}
The function uses radians for theta, because sin() and cos() do, so change the angle to radians, and then you can convert:
CGFloat theta = (angle * M_PI) / 180.0
CGPoint randomPoint = poltocar(radius, theta);
One last step: this circle has its origin at the same place as the view, that is, in the corner, so you need to translate the point to use the center as the origin.
CGPoint addPoints(CGPoint lhs, CGPoint rhs)
{
return (CGPoint){lhs.x + rhs.x, lhs.y, rhs.y};
}
CGPoint offset = (CGPoint){colorWheel.bounds.size.width / 2,
colorWheel.bounds.size.height / 2};
randomPoint = addPoints(randomPoint, offset);
And your new randomPoint will always be within the circle.
I agree with #JoshCaswell's approach, but FYI, the reason the OP code is not working is that the test for inside a circle is incorrect.
The coordinate conversion is unnecessary, and the test against a rectangle is sure to be wrong. Instead, work out how far the random point is from the center and compare that with the radius.
CGFloat centerX = colorWheel.bounds.size.width / 2.0;
CGFloat centerY = colorWheel.bounds.size.height / 2.0;
CGFloat distanceX = centerX - randomPoint.x;
CGFloat distanceY = centerY - randomPoint.y;
CGFloat distance = distanceX*distanceX + distanceY*distanceY;
CGFloat radius = colorWheel.bounds.size.width / 2.0; // just a guess
CGFloat r2 = radius*radius;
// this compares the square of the distance with r^2, to save a sqrt operation
BOOL isInCircle = distance < r2;
In my iOS application I have a texture applied to a sphere rendered in OpenGLES1. The sphere can be rotated by the user. How can I track where a given point on the texture is in 2D space at any given time?
For example, given point (200, 200) on a texture that's 1000px x 1000px, I'd like to place a UIButton on top of my OpenGL view that tracks the point as the sphere is manipulated.
What's the best way to do this?
On my first attempt, I tried to use a color-picking technique where I have a separate sphere in an off-screen framebuffer that uses a black texture with a red square at point (200, 200). Then, I used glReadPixels() to track the position of the red square and I moved my button accordingly. Unfortunately, grabbing all the pixel data and iterating it 60 times a second just isn't possible for obvious performance reasons. I tried a number of ways to optimize this hack (eg: iterating only the red pixels, iterating every 4th red pixel, etc), but it just didn't prove to be reliable.
I'm an OpenGL noob, so I'd appreciate any guidance. Is there a better solution? Thanks!
I think it's easier to keep track of where your ball is instead of searching for it with pixels. Then just have a couple of functions to translate your ball's coordinates to your view's coordinates (and back), then set your subview's center to the translated coordinates.
CGPoint translatePointFromGLCoordinatesToUIView(CGPoint coordinates, UIView *myGLView){
//if your drawing coordinates were between (horizontal {-1.0 -> 1.0} vertical {-1 -> 1})
CGFloat leftMostGLCoord = -1;
CGFloat rightMostGLCoord = 1;
CGFloat bottomMostGLCoord = -1;
CGFloat topMostGLCoord = 1;
CGPoint scale;
scale.x = (rightMostGLCoord - leftMostGLCoord) / myGLView.bounds.size.width;
scale.y = (topMostGLCoord - bottomMostGLCoord) / myGLView.bounds.size.height;
coordinates.x -= leftMostGLCoord;
coordinates.y -= bottomMostGLCoord;
CGPoint translatedPoint;
translatedPoint.x = coordinates.x / scale.x;
translatedPoint.y =coordinates.y / scale.y;
//flip y for iOS coordinates
translatedPoint.y = myGLView.bounds.size.height - translatedPoint.y;
return translatedPoint;
}
CGPoint translatePointFromUIViewToGLCoordinates(CGPoint pointInView, UIView *myGLView){
//if your drawing coordinates were between (horizontal {-1.0 -> 1.0} vertical {-1 -> 1})
CGFloat leftMostGLCoord = -1;
CGFloat rightMostGLCoord = 1;
CGFloat bottomMostGLCoord = -1;
CGFloat topMostGLCoord = 1;
CGPoint scale;
scale.x = (rightMostGLCoord - leftMostGLCoord) / myGLView.bounds.size.width;
scale.y = (topMostGLCoord - bottomMostGLCoord) / myGLView.bounds.size.height;
//flip y for iOS coordinates
pointInView.y = myGLView.bounds.size.height - pointInView.y;
CGPoint translatedPoint;
translatedPoint.x = leftMostGLCoord + (pointInView.x * scale.x);
translatedPoint.y = bottomMostGLCoord + (pointInView.y * scale.y);
return translatedPoint;
}
In my app I choose to use the iOS coordinate system for my drawing too. I just apply a projection matrix to my whole glkView the reconciles the coordinate system.
static GLKMatrix4 GLKMatrix4MakeIOSCoordsWithSize(CGSize screenSize){
GLKMatrix4 matrix4 = GLKMatrix4MakeScale(
2.0 / screenSize.width,
-2.0 / screenSize.height,
1.0);
matrix4 = GLKMatrix4Translate(matrix4,-screenSize.width / 2.0, -screenSize.height / 2.0, 0);
return matrix4;
}
This way you don't have to translate anything.
I am trying to find a way to create a random closed smooth path (CGPath or UIBezierPath). I have read about the De Casteljau's algorithm and tons other articles about Bezier paths but it does not seem to fit to what I try to achieve.
I thought about creating a circle CGPath. Then I would multiplicate the path by a function that would distort the positions of the points say, sine or cosine. However I don't know if this is the right direction to go since the path would not have a random shape.
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, nil, 0.0f, 0.0f, 100.0f, 2 * M_PI, 0.0f, true);
...
CGPathRelease(circle);
It would be great if anyone could point me in a right direction how to start implementing it. Example of a path I am trying to generate:
What you've drawn looks like a distorted circle.
Assuming that's what you are after, here is what I would do:
Write code that steps an angle from 0 to 2pi by a fixed number of steps. (Try 8) Have the angle vary by some small random amount less than ± pi/steps.
Pick a base radius that is somewhat less than 1/2 the length of a side of the enclosing square, so there is room to make your points go inside or outside the base radius without going outside your bounding square. Try 3/8 of your bounding box length.
For each slightly randomized angle value along the circle, calculate a radius value that is base radius ± a random value from 0 to base radius/2.
Use sine and cosine to convert your angle and radius values into x and y coordinates for a point.
Add each point to an array. If you use those points to create a closed path, it would give you an 8-sided irregular non-selfintersecting polygon that is a distorted circle.
Now use those points as the control points for a Catmull-Rom spline to turn it into a smooth curve.
EDIT: I created a project on github called RandomBlobs that does what I describe above, as well as another approach:
Break the square area into a 3x3 grid of smaller squares. Ignore the center square.
Walk around the 8 remaining squares clockwise. For each square, pick a random x/y coorindate inside the square (but prevent it from getting too close to the edges.)
Create closed UIBezierPath connecting the 8 points in order.
Use Catmull-Rom smoothing to turn the irregular octagon into a smooth curve.
Yet a third approach would probably be even simpler:
Use a circular layout like in the first approach outlined above. Pick random control points. But then instead of using Catmull-Rom splines, bisect the angle between each pair of endpoints on the distorted circle and add a control point for a quadratic Bezier curve, also with a randomized radius value. So as you walk around the circle, you'd have alternating endpoints and control points. You might need to add some constraints to the bezier control points so you don't have "kinks" in your curved shape (In order to avoid kinks, the control points for neighboring Bezier curves need to follow a line through the shared end-point of the two curves.)
Here are a couple of sample images from the RandomBlobs project. The images I've uploaded are scaled down. The program optionally shows the control points it uses to generate each image, but you can't really see the control points in the scaled-down image.
First, a circle-based blob (using the first method that Josh Caswell and I suggested):
In that picture, the starting circle shape is shown in light gray:
And second, a blob based on the second square-based technique I described:
And in that picture, the grid of squares is shown for reference. The shape is based on a random point in each of the points in the grid (excluding the center square).
I've try to build your path, but it's not perfect... Anyhow, I'll share my test ;-D Hop this can help.
//
// DrawView.h
// test
//
// Created by Armand DOHM on 03/03/2014.
//
//
#import <UIKit/UIKit.h>
#interface DrawView : UIView
#end
//
// DrawView.m
// test
//
// Created by Armand DOHM on 03/03/2014.
//
//
#import "DrawView.h"
#import <math.h>
#implementation DrawView
- (void)drawRect:(CGRect)rect
{
float r; //radius
float rmax = MIN(rect.size.height,rect.size.width) * .5; //max radius
float rmin = rmax * .1; //min radius
NSMutableArray *points = [[NSMutableArray alloc] init];
/*cut a circle into x pies. for each of this pie take a point at a random radius
link all of this point with quadcurve*/
for (double a=0;a < 2 * M_PI;a += M_PI / 10) {
r = rmin + ((arc4random_uniform((int)(rmax - rmin) * 100)) / 100.0f);
CGPoint p = CGPointMake((rect.size.width / 2) + (r * cos (a)) , (rect.size.height / 2) + (r * sin (a)));
[points addObject:[NSValue valueWithCGPoint:p]];
}
UIBezierPath *myPath=[[UIBezierPath alloc]init];
myPath.lineWidth=2;
[myPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
r = rmin + ((arc4random_uniform((int)(rmax - rmin) * 100)) / 100.0f);
[myPath moveToPoint:CGPointMake((rect.size.width / 2) + (r * cos (0)) , (rect.size.height / 2) + (r * sin (0)))];
for (int i = 0; i < points.count; i+=2) {
NSValue *value = [points objectAtIndex:i];
CGPoint p1 = [value CGPointValue];
value = [points objectAtIndex:(i+1)];
CGPoint p2 = [value CGPointValue];
[myPath addQuadCurveToPoint:p2 controlPoint:p1];
}
[myPath closePath];
[myPath stroke];
}
#end
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...
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
}
}