I have tried everything I can think of, but none of its working. In an iOS UIView drawRect, I want a function that takes an image, a point on the image, scale, and an angle, and draws the image rotated around the given point at the center of the view. So if I want to rotate around the origin (0,0) the edge of my image will appear in the center of the view.
Here's my first version of the code:
-(void)drawImage:(UIImage*)image atAngle:(float)theta atScale:(float)scale whereCenter:(JBPoint*)center inContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
//Rotate
CGContextTranslateCTM (context, -center.x, -center.y);
CGContextRotateCTM (context, theta);
CGContextTranslateCTM (context, center.x, center.y);
//Scale
CGContextScaleCTM(context, scale, scale);
//Draw
GContextDrawImage(context, self.frame, image.CGImage);
UIGraphicsPopContext();
}
However, this skewed the image such that it was not an affine transformation even though I only performed affine transformations on it.
After fiddling with it for a time, I just decided to do it by hand, and calculate the locations of the four corner points and draw it then:
-(void)drawImage:(UIImage*)image atAngle:(float)theta atScale:(float)scale whereCenter:(JBPoint*)center inContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
CGContextRotateCTM (context, theta);
float ux = cos(theta);
float uy = sin(theta);
float vx = sin(theta);
float vy = -cos(theta);
CGPoint p1 = CGPointMake(self.frame.size.width/2.0f-ux*scale*center.x-vx*scale*(image.size.height - center.y), self.frame.size.height/2.0f-uy*scale*center.x-vy*scale*(image.size.height - center.y));
CGPoint p2 = CGPointMake(p1.x+ux*image.size.width*scale, p1.y+uy*image.size.width*scale);
CGPoint p3 = CGPointMake(p2.x+vx*image.size.height*scale, p2.y+vy*image.size.height*scale);
CGPoint p4 = CGPointMake(p1.x+vx*image.size.height*scale, p1.y+vy*image.size.height*scale);
//Sanity check. These all should be the same since I want to center at the center of the frame
NSLog(#"Center: %f, %f", (p1.x+p3.x)/2.0f, (p1.y+p3.y)/2.0f);
NSLog(#"Center: %f, %f", (p2.x+p4.x)/2.0f, (p2.y+p4.y)/2.0f);
NSLog(#"Center: %f, %f", self.frame.size.width/2.0f, self.frame.size.height/2.0f);
float minX = MIN(MIN(p1.x, p2.x), MIN(p3.x, p4.x));
float minY = MIN(MIN(p1.y, p2.y), MIN(p3.y, p4.y));
float maxX = MAX(MAX(p1.x, p2.x), MAX(p3.x, p4.x));
float maxY = MAX(MAX(p1.y, p2.y), MAX(p3.y, p4.y));
CGContextSetAlpha(context, alpha);
CGContextScaleCTM(context, 1, -1);
CGContextDrawImage(context, CGRectMake(minX, -minY, (maxX-minX), -(maxY-minY)), image.CGImage);
UIGraphicsPopContext();
}
This works for angles of 0, but as soon as the angle becomes 90 degrees (pi/2) the image is extremely skewed, which doesn't make any sense.
This seems to be such a simple problem. Why is it so hard? Does anyone see what I'm doing wrong?
Related
I am developing an app in which user can draw lines. The desktop version of the same app is able to draw lines on negative coordinates and I would like to have same capability in iOS app too.
What I've tried so far:-
I have a UIViewController inside which I have overridden - (void)drawRect:(CGRect)rect and drawn the line. Here is the code I am using:-
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, self.bounds);
CGPoint startPoint = CGPointMake(self.startHandle.frame.origin.x + self.startHandle.frame.size.width/2, self.startHandle.frame.origin.y + self.startHandle.frame.size.height/2);
CGPoint endPoint = CGPointMake(self.endHandle.frame.origin.x + self.endHandle.frame.size.width/2, self.endHandle.frame.origin.y + self.endHandle.frame.size.height/2);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetLineWidth(context, 2.0f);
CGContextMoveToPoint(context, startPoint.x, startPoint.y ); //start at this point
CGContextAddLineToPoint(context, endPoint.x, endPoint.y); //draw to this point
It works very well until I have startPoint.x as a negative value. With negative value, I don't see the portion of line that is behind 0,0 coordinates. Any help or information is appreciated.
Following on from what #matt said (he has since deleted his answer, shame as it was still useful), you simply want to apply a transform to the context in order to draw in a standard cartesian coordinate system, where (0,0) is the center of the screen.
The context provided to you from within drawRect has its origin at the top left of the screen, with the positive y-direction going down the screen.
So first, we'll want to first flip the y-axis so that the positive y-direction goes up the screen.
We can do this by using CGContextScaleCTM(context, 1, -1). This will reflect the y-axis of the context. Therefore, this reflected context will have a y-axis where the positive direction goes up the screen.
Next, we want to translate the context to the correct position, so that (0,0) corresponds to the center of the screen. We should therefore shift the y-axis down by half of the height (so that 0 on the y-axis is now at the center), and the x-axis to the right by half of the width.
We can do this by using CGContextTranslateCTM(context, width*0.5, -height*0.5), where width and height are the width and height of your view respectively.
Now your context coordinates will look like this:
You could implement this into your drawRect like so:
-(void) drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
// scale and translate to the standard cartesian coordinate system where the (0,0) is the center of the screen.
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, width*0.5, -height*0.5);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 2.0);
// add y-axis
CGContextMoveToPoint(ctx, 0, -height*0.5);
CGContextAddLineToPoint(ctx, 0, height*0.5);
// add x-axis
CGContextMoveToPoint(ctx, -width*0.5, 0);
CGContextAddLineToPoint(ctx, width*0.5, 0);
// stroke axis
CGContextStrokePath(ctx);
// define start and end points
CGPoint startPoint = CGPointMake(-100, 100);
CGPoint endPoint = CGPointMake(100, -200);
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextSetLineWidth(ctx, 5.0);
CGContextMoveToPoint(ctx, startPoint.x, startPoint.y );
CGContextAddLineToPoint(ctx, endPoint.x, endPoint.y);
CGContextStrokePath(ctx);
}
This will give the following output:
I added the red lines to represent the y and x axis, in order to clarify what we've done here.
CGContextClearRect(context, self.bounds);
You can draw out of this bounds,please make a larger rect.
I am trying to draw an annotation for map , my view is a subclass of MKAnnotationView
I need a shape something like shown below
What I am getting is like this :
Here is the code which I am using :
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
You can achieve the desired effect if you replace your lines with quad curves:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddQuadCurveToPoint(ctx, topLeft.x, height * 2 / 3, midBottom.x, midBottom.y);
CGContextAddQuadCurveToPoint(ctx, topRight.x, height * 2 / 3, topRight.x, topRight.y + height/2);
// CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
// CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
This employs a quadratic bezier curve, which is a good curve when you don't want inflection points. You can achieve a similar curve with the cubic bezier, but if you're not careful with your control points, you can get undesired inflection points. The quadratic curve is just a little easier with only one control point per curve.
By choosing control points with x values the same as the start and end of the semicircle, it ensures a smooth transition from the circle to the curve leading down to the point. By choosing y values for those control points which are relatively close to the start and end of the semicircle, it ensures that the curve will transition quickly from the semicircle to the point. You can adjust these control points, but hopefully this illustrates the idea.
For illustration of the difference between these two types of curves, see the Curves section of the Paths chapter of the Quartz 2D Programming Guide.
I am a new ios developer and here's my problem.I draw a shape with this code :
-(void)drawFirstShape:(float)xcoord :(float)ycoord :(CGContextRef)c {
float cote = 70.0f;
float x = 60.0f;
float y = 35.0f;
CGFloat strokecolor[4] = {1.0f, 1.0f, 1.0f, 1.0f};
CGContextSetStrokeColor(c, strokecolor);
CGContextSetLineWidth(c, 4.0f);
CGContextBeginPath(c);
CGContextMoveToPoint(c, xcoord, ycoord);
CGContextAddLineToPoint(c, xcoord+x, ycoord+y);
CGContextAddLineToPoint(c, xcoord+x, ycoord+cote+y);
CGContextAddLineToPoint(c, xcoord, ycoord+cote+2*y);
CGContextClosePath(c);
CGContextDrawPath(c, kCGPathStroke);
}
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
[self drawFirstShape:160.0f :100.0f:c];
}
I want to create relative coordinates instead of absolute ones.
The aim is to stabilize this code to use it with a 3.5inch and 4inch screen. Can someone help me please?
If you want to use relative coordinates then use relative coordinates :)
All positions and width/height should be defined as float or double from 0 to 1.
Then to get real coordinate - just multiply these relative coordinates to frame sizes (or to rect in your case) and you will get absolute coordinates or sizes.
Example (I just used your code with small changes. keep in mind that I changed sizes to relative and you will need to change them to be correct ones):
-(void)drawFirstShape:(float)xcoord :(float)ycoord :(CGContextRef)c inRect:(CGRect)rect{
xcoord = xcoord*rect.size.width;
ycoord = ycoord*rect.size.height;
float cote = 0.07f*rect.size.width;
float x = 0.05f*rect.size.width;
float y = 0.025f*rect.size.height;
CGFloat strokecolor[4] = {1.0f, 1.0f, 1.0f, 1.0f};
CGContextSetStrokeColor(c, strokecolor);
CGContextSetLineWidth(c, 4.0f);
CGContextBeginPath(c);
CGContextMoveToPoint(c, xcoord, ycoord);
CGContextAddLineToPoint(c, xcoord+x, ycoord+y);
CGContextAddLineToPoint(c, xcoord+x, ycoord+cote+y);
CGContextAddLineToPoint(c, xcoord, ycoord+cote+2*y);
CGContextClosePath(c);
CGContextDrawPath(c, kCGPathStroke);
}
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
[self drawFirstShape:0.2 :0.1 :c inRect:rect];
}
With this formula I got angle
double rotateAngle = atan2(y,x)
with this code I can draw a rectangle
CGRect rect = CGRectMake(x,y , width ,height);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
How can I rotate the rectangle around the angle ?
Here's how you'd do that:
CGContextSaveGState(context);
CGFloat halfWidth = width / 2.0;
CGFloat halfHeight = height / 2.0;
CGPoint center = CGPointMake(x + halfWidth, y + halfHeight);
// Move to the center of the rectangle:
CGContextTranslateCTM(context, center.x, center.y);
// Rotate:
CGContextRotateCTM(context, rotateAngle);
// Draw the rectangle centered about the center:
CGRect rect = CGRectMake(-halfWidth, -halfHeight, width, height);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
CGContextRestoreGState(context);
I'm trying to make a more sophisticated drawing of a UILabels (or a UIView, putting the UILabel on top, should that be required) background. Since I want this to resize automatically when the user changes iPhone orientation, I am trying to implement this in a subclasses drawRect function. I would love to be able to tune this all in code, omitting the need for pattern images, if it's possible.
I got some hints from some former posts on the subject:
Gradients on UIView and UILabels On iPhone
and
Make Background of UIView a Gradient Without Sub Classing
Unfortunately they both miss the target, since I wanted to combine gradient with custom drawing. I want the CGGradient to be clipped by the line frame I am drawing (visible at the rounded corners) but I can't accomplish this.
The alternative would be to use the CAGradientLayer, which seem to work quite good, but that would require some resising of the frame at rotation, and the gradient layer seem to draw on top of the text, no matter how I try.
My question therefor is twofold
How do I modify the code below to make the gradient clip to the drawn "frame"
How do I make CAGradientLayer draw behind the text of the UILabel (or do I have to put a UIView behind the UILabel with the CAGradientLayer as background)
Here is my drawrect function:
- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(c, [[UIColor clearColor] CGColor]);
CGContextSetStrokeColorWithColor(c, [strokeColor CGColor]);
CGContextSetLineWidth(c, 1);
CGFloat minx = CGRectGetMinX(rect), midx = CGRectGetMidX(rect), maxx = CGRectGetMaxX(rect) ;
CGFloat miny = CGRectGetMinY(rect), midy = CGRectGetMidY(rect), maxy = CGRectGetMaxY(rect) ;
minx = minx + 1;
miny = miny + 1;
maxx = maxx - 1;
maxy = maxy - 1;
CGGradientRef glossGradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 0.6, 0.6, 0.6, 1.0, // Start color
0.3, 0.3, 0.3, 1.0 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGRect currentBounds = [self bounds];
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGPoint lowCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGContextDrawLinearGradient(c, glossGradient, topCenter, midCenter, 0);
CGGradientRelease(glossGradient);
CGColorSpaceRelease(rgbColorspace);
CGContextMoveToPoint(c, minx, midy);
CGContextAddArcToPoint(c, minx, miny, midx, miny, ROUND_SIZE_INFO);
CGContextAddArcToPoint(c, maxx, miny, maxx, midy, ROUND_SIZE_INFO);
CGContextAddArcToPoint(c, maxx, maxy, midx, maxy, ROUND_SIZE_INFO);
CGContextAddArcToPoint(c, minx, maxy, minx, midy, ROUND_SIZE_INFO);
// Close the path
CGContextClosePath(c);
// Fill & stroke the path
CGContextDrawPath(c, kCGPathFillStroke);
// return;
[super drawRect: rect];
What you are looking for is CGContextClip()
Create your path and gradient as in your code, and then
CGContextClip(c);
CGContextDrawLinearGradient(c, glossGradient, topCenter, midCenter, 0);
CGGradientRelease(glossGradient);
(Remove your old call to CGContextDrawLinearGradient)
Also remember to save and restore your context before and after you do any clipping
If your clipping is inverted from what you want, try CGContextEOClip() (or draw your path in reverse order)