I saw this clock example on objc.io and I wanted to add the numbers to the clock face.
http://www.objc.io/issue-12/animating-custom-layer-properties.html
So, in the -(id)init{} method I added this code using a formula that I found on the Internet:
float Cx = self.bounds.size.width / 2;
float Cy = self.bounds.size.height / 2;
float Rx = 100;
float Ry = 100;
for (int i=1; i<=12; i++) {
float theta = i / 12.0 * 2.0 * M_PI;
float X = Cx + Rx * cos(theta);
float Y = Cy + Ry * sin(theta);
CATextLayer *aTextLayer = [[CATextLayer alloc] init];
[aTextLayer setFont:#"Helvetica-Bold"];
[aTextLayer setFontSize:20];
[aTextLayer setFrame:CGRectMake(X, Y, 50, 20)];
[aTextLayer setString:[NSString stringWithFormat:#"%d", i]];
[aTextLayer setAlignmentMode:kCAAlignmentCenter];
[aTextLayer setForegroundColor:[[UIColor greenColor] CGColor]];
[self addSublayer:aTextLayer];
}
For some reason, my 12 o'clock has shifted to 3 o'clock.
Can anyone spot what I'm doing wrong?
The trig functions wind from the positive x-axis toward the positive y-axis. In a CALayer, positive y is down, so the start of your loop is 3 o'clock, and the angle proceeds clockwise, which is how you want it for a clock, and the opposite of how things work on the standard (+y==UP) cartesian plane.
The next thing to notice is that it's simpler to loop twelve positions 0..11 and compute the hours and angles from the position.
Finally, the frames of your labels are set to an arbitrary size #{50,20} and the text is centered, this is probably the cause of the x offset. You want the computed x,y to be the labels' centers, so you'll need to fudge the origins a little...
for (int position=0; position <12; position ++) {
int hour = (position+3) % 12;
float theta = position / 6.0 * M_PI;
float X = Cx + Rx * cos(theta);
float Y = Cy + Ry * sin(theta);
// X,Y calculated as you have done here should be the center of the layer
// the quick fix is to subtract half the width and height to get the origin
CGRect frame = CGRectIntegral(CGRectMake(X-25, Y-10, 50, 20));
CATextLayer *aTextLayer = [[CATextLayer alloc] init];
[aTextLayer setFont:#"Helvetica-Bold"];
[aTextLayer setFontSize:20];
[aTextLayer setFrame:frame];
// note that we use "hour" here...
[aTextLayer setString:[NSString stringWithFormat:#"%d", hour]];
[aTextLayer setAlignmentMode:kCAAlignmentCenter];
[aTextLayer setForegroundColor:[[UIColor greenColor] CGColor]];
[self addSublayer:aTextLayer];
}
You might find it helpful to use a radius for the positioning of these labels that's distinct from the radius used to draw the circle. To position the numbers inside, use a smaller radius, or use a larger one to draw them outside the filled dial.
From your formula it looks like if you swapped the Cos and Sin then negated the Cos term you might get what you're looking for.
Edit: meant to include this too. I noticed that they did this too at the link you posted one of there lines of code where they draw the clock hands is this:
CGContextAddLineToPoint(ctx, center.x + sin(angle) * 80, center.y - cos(angle) * 80);
it looks like they also swapped the cos and sin and negated the cos term
Related
I am trying to find angles of rotation for a series of light and dark rectangular UIViews placed at regular points on a circle perimeter. Each point on the circle is calculated as an angle of displacement from the centre and I have tried using the same angle to rotate each UIView so it radiates from the centre. But I didn't expect it to look like this.
I expected the angle of displacement from the centre to be the same as the angle of rotation for each new UIView. Is this assumption correct ? and if so, how do I make the base of each UIView a tangent to a circle so they radiate from the centre ?
UPDATE
In case someone finds it useful here is an update of my original code. The problem as explained by rmaddy has been rectified.
I’ve included two versions of the transform statement and their resulting rotated UIViews. Result on the left uses radians + arcStart + M_PI / 2.0, result on right uses radians + arcStart.
Here is the method.
- (void)sprocket
{
CGRect canvas = [UIScreen mainScreen].bounds;
CGPoint circleCentre = CGPointMake((canvas.size.width)/2, (canvas.size.height)/2);
CGFloat width = 26.0f;
CGFloat height = 50.0f;
CGPoint miniViewCentre;
CGFloat circleRadius = 90;
int miniViewCount = 16;
for (int i = 0; i < miniViewCount; i++)
{
// to place the next view calculate angular displacement along an arc
CGFloat circumference = 2 * M_PI;
CGFloat radians = circumference * i / miniViewCount;
CGFloat arcStart = M_PI + 1.25f; // start circle from this point in radians;
miniViewCentre.x = circleCentre.x + circleRadius * cos(radians + arcStart);
miniViewCentre.y = circleCentre.y + circleRadius * sin(radians + arcStart);
CGPoint placeMiniView = CGPointMake(miniViewCentre.x, miniViewCentre.y);
CGRect swivellingFrame = CGRectMake(placeMiniView.x, placeMiniView.y, width, height);
UIView *miniView = [[UIView alloc] initWithFrame:swivellingFrame];
if ((i % 2) == 0)
{
miniView.backgroundColor = [UIColor darkGrayColor];
}
else
{
miniView.backgroundColor = [UIColor grayColor];
}
miniView.layer.borderWidth = 1;
miniView.layer.borderColor = [UIColor blackColor].CGColor;
miniView.layer.cornerRadius = 3;
miniView.clipsToBounds = YES;
miniView.layer.masksToBounds = YES;
miniView.alpha = 1.0;
// using the same angle rotate the view around its centre
miniView.transform = CGAffineTransformRotate(miniView.transform, radians + arcStart + M_PI / 2.0);
[page1 addSubview:miniView];
}
}
The problem is your calculation of the center of each miniView is based on radians plus arcStart but the transform of each miniView is only based on radians.
Also note that angle 0 is at the 3 o'clock position of the circle. You actually want a 90° (or π/2 radians) rotation of miniView so the rectangle "sticks out" from the circle.
You need two small changes to make your code work:
Change the loop to:
for (int i = 0; i < miniViewCount; i++)
And change the transform:
miniView.transform = CGAffineTransformRotate(miniView.transform, radians + arcStart + M_PI / 2.0);
I have the x and y coordinates, and the rotation of a UIImageView that I need to place on its original position. The coordinates correspond to the ones of the view once it had been rotated.
The problem that I have found is that if I initialize the view with the given x and y, and perform the rotation afterwards, the final position is not correct, because the order in which the transformations were applied was not correct:
float x, y, w, h; // These values are given
UIImageView *imageView = [[UIImageView alloc] init];
// Apply transformations
imageView.frame = CGRectMake(x, y, w, h);
imageView.transform = CGAffineTransformRotate(imageView.transform, a.rotation);
If I try to use the x and y to translate the view once it has been rotated, then the final x and y are completely wrong:
float x, y, w, h; // These values are given
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(0, 0, w, h);
// Apply transformations
imageView.transform = CGAffineTransformTranslate(imageView.transform, x, y);
imageView.transform = CGAffineTransformRotate(imageView.transform, a.rotation);
I have tried updating the center of the view after applying the rotation with incorrect results too.
I am looking for some advice or tips on how to deal with this in order to achieve the result that I need.
Thanks in advanced!
I'm using this C function to make rotation transform around center:
static inline CGAffineTransform CGAffineTransformMakeRotationAroundCenter(double width, double height, double rad) {
CGAffineTransform t = CGAffineTransformMakeTranslation(height/2, width/2);
t = CGAffineTransformRotate(t, rad);
t = CGAffineTransformTranslate(t, -width/2, -height/2);
return t;
}
You need to specify width, height and angle in radians.
Does this solve you problem?
I was able to fix this by calculating the offset in the Y axis between the original Y position where the frame should be, and the origin of the transformed view.
The functions provided in this answer for a similar question provide a way to calculate the new origin of the frame by creating a point with the minimum X and Y among all the new corners:
-(CGPoint)frameOriginAfterTransform
{
CGPoint newTopLeft = [self newTopLeft];
CGPoint newTopRight = [self newTopRight];
CGPoint newBottomLeft = [self newBottomLeft];
CGPoint newBottomRight = [self newBottomRight];
CGFloat minX = fminf(newTopLeft.x, fminf(newTopRight.x, fminf(newBottomLeft.x, newBottomRight.x)));
CGFloat minY = fminf(newTopLeft.y, fminf(newTopRight.y, fminf(newBottomLeft.y, newBottomRight.y)));
return CGPointMake(minX, minY);
}
Afterwards I calculated the offset in the Y axis and applied it to the center of the transformed view:
// Adjust Y after rotating to compensate offset
CGPoint center = imageView.center;
CGPoint newOrigin = [imageView frameOriginAfterTransform]; // Frame origin calculated after transform
CGPoint newCenter = CGPointZero;
newCenter.x = center.x;
newCenter.y = center.y + (y - newOrigin.y);
imageView.center = newCenter;
For some reason the offset is only affecting the Y axis, although at first I thought it was going to affect both X and Y.
Hope this helps!
My app draws a bunch of circles and then draws text within them following the curves as shown below.
The problem as you can see is that some of the text is upsidedown. Its easy enough for me to detect cases where this will happen, so I set about trying to invert that text. The result however is as follows...
As you can see, the letter spacing has been totally jumbled.
I've gotten stuck at this point, I can't see how its losing the spacing so badly & trial and error has failed to find a fix. Any help would be much appreciated. A snippet of the relevant part of my code is below.
#define DEGREES_TO_RADIANS(degrees) ((M_PI * degrees)/ 180)
- (void)drawCurvedLabelForText:(NSString *)titleText withAngle:(float)angle centre:(CGPoint)centre andRadius:(float)textRadius
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIFont *font = [UIFont systemFontOfSize:16.0f];
char *fontName = (char *)[font.fontName cStringUsingEncoding:NSASCIIStringEncoding];
CGContextSelectFont(context, fontName, 12, kCGEncodingMacRoman);
float perimeter = 2 * M_PI * textRadius;
// detect if text is upside-down and needs rotating
BOOL invert = (angle > DEGREES_TO_RADIANS(180.0f);
if (invert)
{
titleText = [self reverseString:titleText];
}
// draw each letter in turn, with correct rotation for each
for (NSUInteger index=0; index<[titleText length]; index++)
{
CGContextSaveGState(context);
NSRange range = {index, 1};
NSString *letter = [titleText substringWithRange:range];
char *c = (char *)[letter cStringUsingEncoding:NSASCIIStringEncoding];
CGSize charSize = [letter sizeWithFont:font];
float x = centre.x + textRadius * cos(angle);
float y = centre.y + textRadius * sin(angle);
float rotateAngle = 0.5 * M_PI;
if (invert)
{
// flip the letter 180 degrees the opposite direction so its not upsidedown
rotateAngle = -0.5 * M_PI;
}
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformMake(1.0,0.0,0.0,-1.0,0.0,0.0));
// draw the text
CGContextTranslateCTM(context, x, y);
CGContextRotateCTM(context, (angle - rotateAngle));
CGContextShowTextAtPoint(context, 0, 0, c, strlen(c));
CGContextRestoreGState(context);
float letterAngle = (charSize.width / perimeter * -2 * M_PI);
angle += letterAngle;
}
}
- (NSString *)reverseString:(NSString *)source
{
NSMutableString *target = [NSMutableString string];
NSUInteger index = [source length];
while (index > 0)
{
index--;
NSRange subStringRange = NSMakeRange(index, 1);
[target appendString:[source substringWithRange:subStringRange]];
}
return target;
}
Note that I've typed it in by hand, making alterations along the way so its more standalone than my actual code which uses class properties, so its possible there's mistakes in the above but the logic should be clear.
My assumption is that "letterAngle", which uses the width of the last drawn letter to determine how much to add onto the "angle" variable for the placement of the next letter, is somehow wrong when the letters are being rotated 180 degrees. Either that or the rotation is shifting it to a different location & I need to offset it somehow with the x/y coordinates. I haven't found answers with either of these though & its possible it could be something else.
Here's a work-around answer, but not really a satisfactory one - use the only monospaced font available on iOS which is Courier. This rotates the text and keeps the spacing the same.
With all other fonts the issue remains unsolved as yet.
I want to label every part of the following curve like in the figure. The label origin should start from the middle of the curve segment see the graphic. I think that something is not quite right in my logic. Could you assist where is the error in my code? I think that the code and the figure explain itself.
Thanks for your time and help!
CGFloat radius = self.height / 3.2f; // radius of the curve
CGPoint center = CGPointMake(self.width / 2.f, self.height / 2.f); //center of the curve
UIBezierPath* circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:degreesToRadian(270.f) endAngle:DEGREES_TO_RADIANS(269.999f) clockwise:YES]; // go around like the clock
CGFloat strokeStart = 0.f;
// Add the parts of the curve to the view
for (int i = 0; i < segmentValues.count; i++) {
NSNumber *value = [segmentValues objectAtIndex:i];
CGFloat strokeEnd = [value floatValue] / kPercentHundred;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = circlePath.CGPath;
circle.lineCap = kCALineCapButt;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [[colors objectAtIndex:i] CGColor];
circle.lineWidth = kWidhtLine;
circle.zPosition = 1.f;
circle.strokeStart = strokeStart;
circle.strokeEnd = strokeStart + strokeEnd;
[self.layer addSublayer:circle];
UIView *example = [[UIView alloc] init];
example.backgroundColor = kColorRed;
example.size = CGSizeMake(10, 10);
// middle = "(circle.strokeStart + circle.strokeEnd) / 2.f"
// * convert percent to degree
CGFloat angle = (circle.strokeStart + circle.strokeEnd) / 2.f * 360.f;
// determine the point and add the right offset
example.origin = CGPointMake(cosf(angle) * radius + center.x, sinf(angle) * radius + center.y);
[self addSubview:example];
strokeStart = circle.strokeEnd;
}
What I get see the figure.
What I expect in concept is something similar to the following figure.
There are two key issues:
You calculate the angle via:
CGFloat angle = (circle.strokeStart + circle.strokeEnd) / 2.f * 360.f;
You then proceed to use that for the cos and sin functions. Those expect radian values, though, so you have to convert this to radians before you use it in those functions. Thus, yielding:
CGFloat angle = degreesToRadian((circle.strokeStart + circle.strokeEnd) / 2.f * 360.f);
(BTW, I don't know if you prefer degreesToRadian or DEGREES_TO_RADIANS, but you should probably use one or the other.)
Or, more simply, you can convert to radians directly:
CGFloat angle = (circle.strokeStart + circle.strokeEnd) / 2.f * M_PI * 2.0;
You have rotated the circle 90 degrees (presumably so that it would start a "12 o'clock" rather than "3 o'clock"). Well, if you rotate the circle, then you have to rotate angle when you calculate where to put your example view, as well:
example.center = CGPointMake(cosf(angle - M_PI_2) * radius + center.x, sinf(angle - M_PI_2) * radius + center.y);
This yields:
I'm not sure what your goal is. You say you want to create labels, but what you create in your code is generic UIView objects.
Then you are setting a property "origin" on the view, but UIView does not have an origin as far as I know. It has a frame, which has an origin component, but not an origin property. I'm guessing that the code you posted is not actual, running code.
Do you want the labels to be centered on top of the circle? If so, you should both set the text alignment to NSTextAlignmentCenter, and set the center property of the views rather than the frame.origin.
I have a working sample project on github that creates an analog clock and puts labels for the hours around it. You can look at the code for an idea of how to do it:
Analog clock sample project
(The project also shows how to use the new spring physics UIView animation method.)
hmm it i a little late for me ;)
I think you have to use UIView::setCenter this will solve you problem. Then the center of the view is on your circle.
Edit: And it is easier to manipulate the position.
I already know how to place the points with same distance on top of a circle:
double slice = 2 * M_PI / [icons count];
for (int i = 0; i < [icons count]; i++)
{
double angle = slice * i;
int newX = (int)(cen.x + rad * cos(angle));
int newY = (int)(cen.y + rad * sin(angle));
CGPoint point = CGPointMake(newX, newY);
}
Depending on the number of elements in my array the position of the points is always different (of course) but how can I manage to put the first point always at the same spot on the circle e.g. at the top most point of the circle?
Add a constant value to your angle. The points should start out on the right of the origin (in standard cartesian coordinates where 0,0 is in the center and X and Y increase to the right and up.)
To shift the first point to the top, add pi/2 to your angle.
It looks like you're using iOS coordinates, where 0,0 is at the top left of the sceen and Y increases DOWN, which flips normal cartesian coordinates on the X axis. Thus you'd need to subtract pi/2 from the angle:
double slice = 2 * M_PI / [icons count];
for (int i = 0; i < [icons count]; i++)
{
double angle = slice * i - M_PI_2;
int newX = (int)(cen.x + rad * cos(angle));
int newY = (int)(cen.y + rad * sin(angle));
CGPoint point = CGPointMake(newX, newY);
}