Drawing a Hexagon with UIBezierPath (point first) - ios

I am trying to draw a Hexagon using UIBezierPath and ZEPolygon, it works great but my hexagon is flat on top. I have tried everything to get it to draw form a point in the middle including a 180 degree transform on the path which work but everything else break.
This is how it looks now
This is how i would like it to look
My code is below
UIImageView *maskedImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"image.png"]];
UIBezierPath *nonagon = [UIBezierPath bezierPathWithPolygonInRect:maskedImageView.frame numberOfSides:6];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = nonagon.CGPath;
maskedImageView.layer.mask = shapeLayer;
[self.view addSubview:maskedImageView];
This is the library i used for the drawing the bezier path
Thanks for any help

When you rotate a UIBezierPath with a CGTransform, it will be rotated around the point (0,0), and for your path the point (0,0) is the top left corner of your shape. This is why the offset is incorrect when you just rotate by 90 degrees w/o doing anything else - its rotating around the wrong point.
So before you rotate, you need to center the path to the point (0,0), then rotate it, then move it back so that (0,0) is at its top left.
the following code will rotate the polygon 90 degrees:
// get the size of the image, we'll need this for our path and for later too
CGRect boundsForPoly = maskedImageView.frame;
// create our path inside the rect
UIBezierPath *nonagon = [UIBezierPath bezierPathWithPolygonInRect:boundsForPoly numberOfSides:6];
// center the polygon on (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(-boundsForPoly.size.width/2, -boundsForPoly.size.height/2)];
// rotate it 90 degrees
[nonagon applyTransform:CGAffineTransformMakeRotation(M_PI/2)];
// now move it back so that the top left of its bounding box is (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(nonagon.bounds.size.width/2, nonagon.bounds.size.height/2)];
this will rotate the polygon 90 degrees, and keep it's top left corner at (0,0)
The blue outline is your path before, and the green outline is after the rotation:

There is a problem with the answer from adam.wulf is with the line
[nonagon applyTransform:CGAffineTransformMakeTranslation(nonagon.bounds.size.width/2, nonagon.bounds.size.height/2)];
It doesn't center the polygon to the center of the frame. It should be instead
//Centered version
[nonagon applyTransform:CGAffineTransformMakeTranslation(boundsForPoly.size.width/2, boundsForPoly.size.height/2/2)];
Thus the code should look like following from adam.wulf
// get the size of the image, we'll need this for our path and for later too
CGRect boundsForPoly = maskedImageView.frame;
// create our path inside the rect
UIBezierPath *nonagon = [UIBezierPath bezierPathWithPolygonInRect:boundsForPoly numberOfSides:6];
// center the polygon on (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(-boundsForPoly.size.width/2, -boundsForPoly.size.height/2)];
// rotate it 90 degrees
[nonagon applyTransform:CGAffineTransformMakeRotation(M_PI/2)];
// now move it back so that the top left of its bounding box is (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(boundsForPoly.size.width/2, boundsForPoly.size.height/2/2)];

Related

Preserve CAShapeLayer's lineWidth in a zoomable UIView

I have a custom UIView, which has a UIImageView as its' subview to display an image and a CAShapeLayer as a sublayer to draw on it. The UIView is zoomable, I use a UIPinchGestureRecognizer to zoom in and out of the view. I can draw straight lines on the view using a CGPathRef. The problem is, when I pinch to zoom the view, the lines I draw also zoom and become thick. How to zoom in the view, keeping the lines thin?
Before zooming (green lines are the ones I draw):
And when I zoom:
What I want is, the lines to be as thin as they were before I zoomed in.
I tried to:
CGMutablePath path = CGPathCreateMutable();
//line drawing code here
//scale path to 1.0?
CGAffineTransform transform = CGAffineTransformMakeScale(1.0, 1.0);
CGPathRef transformedPath = CGPathCreateCopyByTransformingPath(path, &transform);
self.drawingLayer.path = transformedPath;
CGPathRelease(path);
CGPathRelease(transformedPath);
How do I prevent path width from enlarging when zooming?
You need to divide the line width with the zoom factor.By default line width is 1.
So create property for the same and when you apply zoom with gesture divide it.
i.e zoom = 4.0 then line width = 1.0/4.0
and same as if zoom is 2 then 1.0/2.0
Hope it is helpful

Collision bounding path origin in UIView iOS 9

I'm trying to setup the collison bounding path of a custom UIView. What I currently do is converting a SVG file to a path using PocketSVG and creating an UIBezierPath from it. Here's my code:
#implementation CustomView {
UIBezierPath * _mask;
CGPathRef _myPath;
}
- (UIDynamicItemCollisionBoundsType) collisionBoundsType {
return UIDynamicItemCollisionBoundsTypePath;
}
- (UIBezierPath *) collisionBoundingPath {
_myPath = [PocketSVG pathFromSVGFileNamed:#"line5"];
_mask = [UIBezierPath bezierPathWithCGPath:_myPath];
return _mask;
}
It works "correctly" but the UIBezierPath is drawn at the center of my view so this is what I get:
Left: Actual behavior ---> Right: Expected behavior
And since the collision bounds are invisible the collision seems to happen before touching the view visually (they are actually touching because of the origin of the collision bounds path).
According to Apple documentation regarding collisionBoundingPath:
The path object you create must represent a convex polygon with
counter-clockwise or clockwise winding, and the path must not
intersect itself. The (0, 0) point of the path must be located at the
center point of the corresponding dynamic item. If the center point
does not match the path’s origin, collision behaviors may not work as
expected.
So my main question is, since the (0,0) coordinates of the path must be at the center of my view, how can I achieve what I'm looking for? The perfect scenario would be if UIBezierPath start drawing at the origin of its UIView but since I'm adding a path from a SVG it is automatically draw from the center of the view.
You can use a transformation to move the CGPath into the appropriate position. For example:
- (UIBezierPath *) collisionBoundingPath {
_myPath = [PocketSVG pathFromSVGFileNamed:#"line5"];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-self.bounds.size.width * 0.5,-self.bounds.size.height * 0.5);
CGPathRef movedPath = CGPathCreateCopyByTransformingPath(_myPath,&translation);
_mask = [UIBezierPath bezierPathWithCGPath:movedPath];
return _mask;
}

iOS - Draw image with CGContext and transform

I am trying to draw an image on top of another image. I have the image's size, transform and origin. My code below shows correct size and transform angle but not at the correct point.
Code:
UIGraphicsBeginImageContextWithOptions(backgroundImage.size, NO, [[UIScreen mainScreen] scale]);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect baseRect = CGRectMake(0, 0, backgroundImage.size.width, backgroundImage.size.height);
[backgroundImage drawInRect:baseRect];
CGRect newRect = CGRectMake(x, y, width, height);
CGContextTranslateCTM(context, x, y);
CGContextConcatCTM(context, watermarkImageView.transform);
CGContextTranslateCTM(context, -x, -y);
[watermarkImageView.image drawInRect:newRect];
UIImage* result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
The watermark image should be placed like this:
But currently its looking like this:
What did I miss?
Thanks in advance
EDIT
The x,y is the edge of the bounding box
Your code doesn't show what watermarkImageView.transform is and that is important because when you concat transformations, the effects of previous transformations will also effect all the following transformations.
E.g. a translation that moves the object 10 pixels along the x-axis will move the object 10 pixels to the right. However, if you first have a rotation that rotates the object by 45 degrees and then add a translation that moves 10 pixels along the x-axis, the object will not move 10 pixels to the right, it will move 10 pixels along a line that is 45 degrees rotated, which means it will move about 7 pixels up and 7 pixels to the right. That's because a rotation does not really rotate the object itself, it actually rotates the whole coordinate system which causes the object to be drawn rotated.
See this image:
Initially the translation coordinate system (red lines) matches the "real coordinate" system. But after the rotation by 45 degrees, the translation coordinate system has been rotated and now translating across the red lines moves the object diagonally.
Think about a sheet of paper and a stamp. The stamp always has the same position and the same orientation, you cannot move or rotate the stamp. But you can move and rotate the sheet of paper below the stamp! And that's what your transformations do. They transform the sheet before the stamp is pressed upon it.
For most people it is very hard to imagine the effects of transforming the whole space, it's much easier for them to think about transforming the object. The trick is: You must read your transformations in the opposite order than you wrote them. I guess what you want to do is actually:
CGContextTranslateCTM(context, x, y);
CGContextConcatCTM(context, watermarkImageView.transform);
CGContextTranslateCTM(context,
-watermarkImageView.size.with * 0.5,
-watermarkImageView.size.height * 0.5
);
Now read them in the opposite order (from bottom to top). First you center the watermark around (0,0) by moving it up half the height and left half the width. Now the center of your watermark is exactly at (0,0). Then you rotate it as desired. Finally you move it to the desired position. Of course you wrote all transformations the other way round but that's only because you are transforming the coordinate space, not the object.
Centering your watermark prior to rotation is important because rotation always rotates around (0,0) coordinates. If you'd just rotate, the rotation looks like this:
That's not what you want as it will not just rotate the object but also changes its position. If you center the image around (0,0) first, the rotation looks like this instead:
The answer to my question was
I had to translate to the centre of where I want to draw the context.
CGContextTranslateCTM(context, imageView.center.x, imageView.center.y);
Then rotate context.
CGFloat angle = [(NSNumber *)[imageView valueForKeyPath:#"layer.transform.rotation.z"] floatValue];
CGContextRotateCTM(context, angle);
Then draw
[imageView.image drawInRect:CGRectMake(-width * 0.5f, -height * 0.5f, width, height)];

Circle center of arc created by CGPathAddArc has inconsistent coordinates

I have a custom UIControl that looks like:
The white ring is drawn in drawRect as follows:
//Get current context
CGContextRef mainContext = UIGraphicsGetCurrentContext();
/** Draw the Path **/
//Create the path
CGContextAddArc(mainContext, self.frame.size.width/2, self.frame.size.height/2, radius, 0, 2*M_PI, 0);
//Set the stroke color to white
[[UIColor whiteColor]setStroke];
//Define line width and cap
CGContextSetLineWidth(mainContext, HOUR_PICKER_BACKGROUND_WIDTH);
CGContextSetLineCap(mainContext, kCGLineCapButt);
//Draw the path
CGContextDrawPath(mainContext, kCGPathStroke);
The black circle is a CAShapeLayer and is drawn as follows:
hourSelector = [CAShapeLayer layer];
hourSelector.path = [UIBezierPath bezierPathWithRoundedRect:selectorPosition cornerRadius:11.0].CGPath;
hourSelector.fillColor = [UIColor colorWithRed:65.0f/255.0f green:75.0f/255.0f blue:86.0f/255.0f alpha:1.0f].CGColor;
hourSelector.strokeColor = [UIColor colorWithRed:65.0f/255.0f green:75.0f/255.0f blue:86.0f/255.0f alpha:1.0f].CGColor;
hourSelector.lineWidth = 1;
[self.layer addSublayer:hourSelector];
I have been able to implement the dragging of the black circle anywhere on the white ring. When I let the black circle go, I want to be able to animate the black circle to a certain position on the ring. When the finger is lifted off the circle, I have the value in radians of the ending touch position as well as the desired final position of the circles. I also have the points in rectangular/Cartesian coordinates. I'm trying to implement the desired behavior by drawing an arc between the two points and changing the position of the circle along the arc with a CAKeyFrameAnimation.
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 1.0;
CGMutablePathRef arcPath = CGPathCreateMutable();
CGPathAddArc(arcPath, NULL, self.frame.size.width/2, self.frame.size.height/2, radius, endingTouchRadian, finalPositionRadian,0);
pathAnimation.path = arcPath;
CGPathRelease(arcPath);
[hourSelector addAnimation:pathAnimation forKey:#"moveHour"];
My issue is that the arc created is nowhere on the white ring. self.frame.size.height (and the corresponding width) is the same circle center used to create the white ring but the arc is created off the screen. Furthermore, using 0,0 for x,y does not return the top left of the frame but the exact center of the main window. The center of the arc circle also seems to change every time I rotate the circle and let go.
Am I missing out on how to get the center of the arc created to be the same as the center of the white circle?
I figured it out. I was creating the CGPath outside drawRect which was redrawing the view with a different origin. I don't understand what exactly was going on underneath but moving the CGPath creation code to drawRect resolved the issue. Thanks to #Unheilig for pointing me in the right direction with his comment.
Wouldn't the center if your circle be
CGRectGetMidX(selectorPosition), CGRectGetMidY(selectorPosition)
You're using half the view's width and height, ignoring it's origin.

How to Rotate CAShapeLayer containing UIBezierPath? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Rotate CGPath without changing its position
I searched and tested a variety of code for a couple of hours and I can't get this to work.
I am adding an arbitrary UIBezierPath at a random location to a CAShapeLayer which gets added to a view. I need to rotate the path so that I can handle device rotations. I can rotate the layer instead of the path. I just need the result to be rotated.
I already have methods to handle transforming the bezier path by scaling and translation. It works great, but now I need to simply rotate 90 degrees left or right.
Any recommendations on how to do this?
Basic code:
UIBezierPath *path = <create arbitrary path>
CAShapeLayer *layer = [CAShapeLayer layer];
[self addPathToLayer:layer
fromPath:path];
// I could get the center of the box but where is the box center for the view it is in?
// CGRect box = CGPathGetPathBoundingBox(path.CGPath);
// layer.anchorPoint = ? How to find the center of the box for the anchor point?
// Rotating here appears to rotate around 0,0 of the view
layer.transform = CATransform3DMakeRotation(DegreesToRadians(-90), 0.0, 0.0, 1.0);
I see the following post:
BezierPath Rotation in a UIView
I suppose I could rotate as-is and then translate the path back into place. I just need to figure out what the translation values would be.
I should also state that what I am seeing after I try to rotate is that the image moves off-screen somewhere. I tried rotating 25 degrees to see movement and it pivots around the view's origin of 0,0 so that if I rotate 90 degrees the image is off-screen. I am running these test WITHOUT rotating the device - just to see how rotation works.
UPDATE #1 - 12/4/2012: For some bizarre reason if I set the position to a value I found empirically it moves the rotated bezier path into the correct position after rotation:
layer.position = CGPointMake(280, 60);
This values are a guess from starting/stopping the app and making adjustments. I have no idea why I need to adjust the position on rotation. The anchor point should be in the center of the layer. However, I did find that both the frame and position of a CAShapeLayer are all ZERO even though the path is set, and also the fact that the path is in the correct position within the view. The 280, 60 position shifts the path into what would be the center of the path bounding box when a rotation of +90 is made. If I change the rotation value I need to adjust the position. I should not have to do this manually adjustment.
I think a last resort is to somehow convert the bezier path to an image and then add it. I found that if I set the layer content to an image, then rotate, it rotates about its center point with no positional adjustment needed. Not so with setting the path.
UPDATE #2 12/4/2012 - I tried setting the frame and with fiddling I get it to center as follows:
CGRect box = CGPathGetPathBoundingBox(path.CGPath);
CGRect rect = CGRectMake(0, 0, box.origin.x + (3.5 * box.size.width), box.origin.y + (3.5 * box.size.height));
layer.frame = rect;
layer.transform = CATransform3DMakeRotation(DegreesToRadians(90), 0.0, 0.0, 1.0);
Why multiply by 3.5? I have no clue. I found that adding the box origin with about 3.5 times the size of the box shifts the rotated CAShapeLayer path to about where it should be.
There must be a better way to do this. This is a better solution than my previous post since the frame size does not depend on the rotation angle. I just don't know why the frame needs to be set to the value I am setting it to. I THOUGHT it should be
CGRectMake(0, 0, box.origin.x + (box.size.width / 2), box.origin.y + (box.size.height / 2));
However, it shifts the image to the left too much.
Another clue I found is that if I set the frame of [self view].frame (the frame of the entire parent view, which is the screen of the iPhone), then rotate, the rotation point is the center of the screen, an the path/image orbits around this center point. This is why I tried shifting the frame to what the center of the path should be so that it orbits around the box center.
UPDATE #3 12/4/2012 - I tried to render the layer as an image. However, it appears that just setting the path of a layer does not make it an "image" in the layer since it is empty
CGRect box = CGPathGetPathBoundingBox(path.CGPath);
layer.frame = box;
UIImage *image = [ImageHelper imageFromLayer:layer]; // ImageHelper library I created
CAShapeLayer *newLayer = [CAShapeLayer layer];
newLayer.frame = CGRectMake(box.origin.x, box.origin.y, image.size.width, image.size.height);
newLayer.contents = (id) image.CGImage;
It appears that rotating the layer with its path set is no different than simply rotating the bezier path itself. I will go back to rotating the bezier path and see if I can fiddle with the position elements or something. There's got to be a solution to this.
Goal: Rotate a UIBezierPath around its center point within the view it was originally created in.
UPDATE #4 12/4/2012 - I ran a series of tests measuring the values needed for translation in order to place a UIBezierPath in its previous center location.
CGAffineTransform rotate = CGAffineTransformMakeRotation(DegreesToRadians(-15));
[path applyTransform:rotate];
// CGAffineTransform translate = CGAffineTransformMakeTranslation(-110, 70); // -45
CGAffineTransform translate = CGAffineTransformMakeTranslation(-52, -58); // -15
[path applyTransform:translate];
However, the ratios of x/y translations do not correspond so I cannot extrapolate what translation is required based on the angle. It appears that 'CGAffineTransformMakeRotation' uses some arbitrary anchor put to make the rotation, which at the moment appears to be maybe (viewWidth / 2, 0). I am making this much harder than it needs to be. There's something I am missing to make a simple rotation so that the center point is maintained. I just need to "spin" the path 90 degrees left or right.
UPDATE #5 12/4/2012 - After running additional tests it appears that the anchor point for rotating a UIBezierPath is the origin from where all of the points were drawn. In this case the origin is 0,0 and all of the points are relative to that point. Therefore, it a rotation is applied, the rotation is occurring around the origin, and is why the path shifts up-right on -90 and up-left on 90. I need to somehow set the anchor point for the rotation to the center so it "spins" around the center, rather than the original origin point. 12 hours spent on this one issue.
After some detailed analysis and graphing the bounding box on paper I found my assertion that the origin of 0,0 is correct.
A solution to this problem is to translate the path (the underlying matrix) to the origin, with the center of the bounding box at origin, rotate, then translate the path back to its original location.
Here's how to rotate a UIBezierPath 90 degrees:
CGAffineTransform translate = CGAffineTransformMakeTranslation(-1 * (box.origin.x + (box.size.width / 2)), -1 * (box.origin.y + (box.size.height / 2)));
[path applyTransform:translate];
CGAffineTransform rotate = CGAffineTransformMakeRotation(DegreesToRadians(90));
[path applyTransform:rotate];
translate = CGAffineTransformMakeTranslation((box.origin.x + (box.size.width / 2)), (box.origin.y + (box.size.height / 2)));
[path applyTransform:translate];
Plug in -90 degrees to rotate in the other direction.
This formula can be used when rotating the device from portrait to landscape and vice/versa.
I still don't think this is the ideal solution but the result is what I need for now.
If anyone has a better solution for this please post.
UPDATE 12/7/2012 - I found what I think is the best solution, and very simple as I though it would be. Rather than using rotate, translate, and scale methods on the bezier path, I instead extract the array of points as CGPoint objects, and scale/translate them as needed based on the view size as well as the orientation. I then create a new bezier path and set the layer to this path.
The result is perfect scaling, translation, rotation.

Resources