Drawing a pattern along a path - ios
My goal is to take a pattern like this
and draw it repeatedly along a circular path to produce something similar to this image:
I found several code examples in other questions and an full demo project here but the result is this:
I think the difference between the two images is obvious, but I find it hard to describe (pardon my lack of graphics vocabulary). The result seems to be tiling without the desired rotation/deformation of the pattern. I think I can live with the lack of deformation, but the rotation is key. I think that perhaps the draw callback could/should be modified to include a rotation, but can't figure out how to retrieve/determine the angle at the point of the callback.
I considered an approach where I manually deformed/rotated the image and drew it several times around a centerpoint to achieve the effect I want, but I believe that CoreGraphics could do it with more efficiency and with less code.
Any suggestions about how to achieve the result I want would be appreciated.
Here is the relevant code from the ChalkCircle project:
const float kPatternWidth = 8;
const float kPatternHeight = 8;
void DrawPatternCellCallback(void *info, CGContextRef cgContext)
{
UIImage *patternImage = [UIImage imageNamed:#"chalk_brush.png"];
CGContextDrawImage(cgContext, CGRectMake(0, 0, kPatternWidth, kPatternHeight), patternImage.CGImage);
}
- (void)drawRect:(CGRect)rect {
float startDeg = 0; // where to start drawing
float endDeg = 360; // where to stop drawing
int x = self.center.x;
int y = self.center.y;
int radius = (self.bounds.size.width > self.bounds.size.height ? self.bounds.size.height : self.bounds.size.width) / 2 * 0.8;
CGContextRef ctx = UIGraphicsGetCurrentContext();
const CGRect patternBounds = CGRectMake(0, 0, kPatternWidth, kPatternHeight);
const CGPatternCallbacks kPatternCallbacks = {0, DrawPatternCellCallback, NULL};
CGAffineTransform patternTransform = CGAffineTransformIdentity;
CGPatternRef strokePattern = CGPatternCreate(
NULL,
patternBounds,
patternTransform,
kPatternWidth, // horizontal spacing
kPatternHeight,// vertical spacing
kCGPatternTilingNoDistortion,
true,
&kPatternCallbacks);
CGFloat color1[] = {1.0, 1.0, 1.0, 1.0};
CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetStrokeColorSpace(ctx, patternSpace);
CGContextSetStrokePattern(ctx, strokePattern, color1);
CGContextSetLineWidth(ctx, 4.0);
CGContextMoveToPoint(ctx, x, y - radius);
CGContextAddArc(ctx, x, y, radius, (startDeg-90)*M_PI/180.0, (endDeg-90)*M_PI/180.0, 0);
CGContextClosePath(ctx);
CGContextDrawPath(ctx, kCGPathStroke);
CGPatternRelease(strokePattern);
strokePattern = NULL;
CGColorSpaceRelease(patternSpace);
patternSpace = NULL;
}
.SOLUTION FROM SAM
I modified sam's solution to handle non-square patterns, center the result, and remove hard coded numbers by calculating them from the passed in image:
#define MAX_CIRCLE_DIAMETER 290.0f
#define OVERLAP 1.5f
-(void) drawInCircle:(UIImage *)patternImage
{
int numberOfImages = 12;
float diameter = (MAX_CIRCLE_DIAMETER * numberOfImages * patternImage.size.width) / ( (2.0 * M_PI * patternImage.size.height) + (numberOfImages * patternImage.size.width));
//get the radius, circumference and image size
CGRect replicatorFrame = CGRectMake((320-diameter)/2.0f, 60.0f, diameter, diameter);
float radius = diameter/2;
float circumference = M_PI * diameter;
float imageWidth = circumference/numberOfImages;
float imageHeight = imageWidth * patternImage.size.height / patternImage.size.width;
//create a replicator layer and add it to our view
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = replicatorFrame;
[self.view.layer addSublayer:replicator];
//configure the replicator
replicator.instanceCount = numberOfImages;
//apply a rotation transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, M_PI / (numberOfImages/2), 0, 0, 1);
replicator.instanceTransform = transform;
//create a sublayer and place it inside the replicator
CALayer *layer = [CALayer layer];
//the frame places the layer in the middle of the replicator layer and on the outside of
//the replicator layer so that the the size is accurate relative to the circumference
layer.frame = CGRectMake(radius - (imageWidth/2.0) - (OVERLAP/2.0), -imageHeight/2.0, imageWidth+OVERLAP, imageHeight);
layer.anchorPoint = CGPointMake(0.5, 1);
[replicator addSublayer:layer];
//apply a perspective transform to the layer
CATransform3D perspectiveTransform = CATransform3DIdentity;
perspectiveTransform.m34 = 1.0f / -radius;
perspectiveTransform = CATransform3DRotate(perspectiveTransform, (M_PI_4), -1, 0, 0);
layer.transform = perspectiveTransform;
//set the image as the layer's contents
layer.contents = (__bridge id)patternImage.CGImage;
}
Using Core Animation's replicator layer, I managed to create this result:
I think it's close to what your looking for. In this example all the images are square with a 3d X rotation applied to each of them.
#import <QuartzCore/QuartzCore.h>
//set the number of images and the diameter (width) of the circle
int numberOfImages = 30;
float diameter = 450.0f;
//get the radius, circumference and image size
float radius = diameter/2;
float circumference = M_PI * diameter;
float imageSize = circumference/numberOfImages;
//create a replicator layer and add it to our view
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = CGRectMake(100.0f, 100.0f, diameter, diameter);
[self.view.layer addSublayer:replicator];
//configure the replicator
replicator.instanceCount = numberOfImages;
//apply a rotation transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, M_PI / (numberOfImages/2), 0, 0, 1);
replicator.instanceTransform = transform;
//create a sublayer and place it inside the replicator
CALayer *layer = [CALayer layer];
//the frame places the layer in the middle of the replicator layer and on the outside of the replicator layer so that the the size is accurate relative to the circumference
layer.frame = CGRectMake(radius - (imageSize/2), -imageSize/2, imageSize, imageSize);
layer.anchorPoint = CGPointMake(0.5, 1);
[replicator addSublayer:layer];
//apply a perspective transofrm to the layer
CATransform3D perspectiveTransform = CATransform3DIdentity;
perspectiveTransform.m34 = 1.0f / -radius;
perspectiveTransform = CATransform3DRotate(perspectiveTransform, (M_PI_4), -1, 0, 0);
layer.transform = perspectiveTransform;
//set the image as the layer's contents
layer.contents = (__bridge id)[UIImage imageNamed:#"WCR3Q"].CGImage;
Related
Create a semi circle with Core Graphics
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.
How to rotate UIView in 3D?
I have very simple task, but I can't understand CATransform3DRotate. I have 3 UIImageView with equal height and width, and I need set some transformation for left and right items for next result: https://dl.dropboxusercontent.com/u/15196617/result.png Original screen: https://dl.dropboxusercontent.com/u/15196617/original.png My sources: CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.f / self.prevItemImageView.bounds.size.width; transform = CATransform3DRotate(transform, (CGFloat) -(M_PI / 3.f), 0, 1.f, 0); self.prevItemImageView.layer.anchorPoint = CGPointMake(1.f, 0.5f); self.prevItemImageView.layer.transform = transform; transform = CATransform3DIdentity; transform.m34 = -1.f / self.nextItemImageView.bounds.size.width; transform = CATransform3DRotate(transform, (CGFloat) (M_PI / 3.f), 0, 0.5f, 0); self.nextItemImageView.layer.anchorPoint = CGPointMake(0.f, 0.5f); self.nextItemImageView.layer.transform = transform; It is not work.
What you want to do is apply the transform with altered .m34 component to the superlayer of your image views, as a sublayerTransform. That is what gives transforms on the image views the appearance of depth. This image started life as a square. But it looks rotated in 3D because its superlayer has a sublayerTransform:
Drawing Circular Gradient
I would like to draw a circular gradient like in the example picture below. I can manage a radial gradient easily but I am not sure how to do a circular one. I was thinking of drawing a gradient on a line and then transforming it to a circle. Would that be possible? This is how I draw the radial gradient: CGFloat a = MIN(self.frame.size.width, self.frame.size.height) - _lineWidth; //Get current context CGContextRef mainContext = UIGraphicsGetCurrentContext(); CGRect newRect = rect; newRect.origin.x = ((self.frame.size.width - a)/2.0f); newRect.origin.y = ((self.frame.size.height - a)/2.0f); newRect.size.width = a; newRect.size.height = a; // Fill circle const CGFloat *_components = CGColorGetComponents(_fillColor.CGColor); CGFloat red = _components[0]; CGFloat green = _components[1]; CGFloat blue = _components[2]; CGFloat alpha = _components[3]; CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB(); CGFloat redBallColors[] = { red,green,blue,0.5, red,green,blue,alpha, }; CGFloat glossLocations[] = {0.0, 1.0}; CGGradientRef ballGradient = CGGradientCreateWithColorComponents(baseSpace, redBallColors, glossLocations, 2); CGPoint startPoint = self.center; CGPoint endPoint = self.center; CGContextDrawRadialGradient(mainContext, ballGradient, startPoint, 0, endPoint, a/2, 0); Thanks a lot!
How to draw a circle pixel by pixel?
I am able to draw a square pixel by pixel as below for(int i=0 ;i<drawbox.size.width/2;i++) { for (int j=0; j<drawbox.size.height/2; j++) { Point.y++; NSLog(#"point:%f,%f",Point.x,Point.y); } Point.x++; } Here drawrect is CGRect and Point is the CGPoint I am using to draw pixel by pixel I iterate over this and find a square to be made. This square is filled with each pixel so it just not draw a square with border but it includes all pixels within the square. I want the same thing but for circle (filled circle's pixels). How can I achieve this?
Override your drawRect with following code : You need to take care of 5 things : SIDE_WEITH = Width of Circle, Color constants : _r = Red _g = Green _b = Blue _a = Alpha And set Progress as per your need : _progress That it. - (void)drawRect:(CGRect)rect { [super drawRect:rect]; //// [image drawInRect:rect]; // find the radius and position for the largest circle that fits in the UIView's frame. int radius, x, y; int offset = SIDE_WEITH; // in case the given frame is not square (oblong) we need to check and use the shortest side as our radius. CGRect frame = self.frame; if (frame.size.width > frame.size.height) { radius = frame.size.height; // we want our circle to be in the center of the frame. int delta = frame.size.width - radius; x = delta/2 - 1; y = 0; } else { radius = frame.size.width; int delta = frame.size.height - radius; y = delta/2 - 1; x = 0; } // store the largest circle's position and radius in class variable. _outerCircleRect = CGRectMake(x, y, radius, radius); // store the inner circles rect, this inner circle will have a radius 10pixels smaller than the outer circle. // we want to the inner circle to be in the middle of the outer circle. //_innerCircleRect = CGRectMake(x+offset, y+offset, radius-2*offset , radius-2*offset); _innerCircleRect = CGRectMake(x+offset, y+offset, radius-2*offset , radius-2*offset); // get the drawing canvas (CGContext): CGContextRef context = UIGraphicsGetCurrentContext(); // save the context's previous state: CGContextSaveGState(context); // our custom drawing code will go here: // Draw the gray background for our progress view: // gradient properties: CGGradientRef myGradient; // You need tell Quartz your colour space (how you define colours), there are many colour spaces: RGBA, black&white... CGColorSpaceRef myColorspace; // the number of different colours size_t num_locations = 3; // the location of each colour change, these are between 0 and 1, zero is the first circle and 1 is the end circle, so 0.5 is in the middle. CGFloat locations[3] = { 0.0, 0.5 ,1.0 }; // this is the colour components array, because we are using an RGBA system each colour has four components (four numbers associated with it). CGFloat components[12] = { 0.4, 0.4, 0.4, 0.9, // Start colour 0.9, 0.9, 0.9, 1.0, // middle colour 0.4, 0.4, 0.4, 0.9 }; // End colour myColorspace = CGColorSpaceCreateDeviceRGB(); myGradient = CGGradientCreateWithColorComponents (myColorspace, components,locations, num_locations); // gradient start and end points CGPoint myStartPoint, myEndPoint; CGFloat myStartRadius, myEndRadius; myStartPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myStartPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myEndPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myEndPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myStartRadius = _innerCircleRect.size.width/2 ; myEndRadius = _outerCircleRect.size.width/2; // draw the gradient. /*CGContextDrawRadialGradient(context, myGradient, myStartPoint, myStartRadius, myEndPoint, myEndRadius, 0); CGGradientRelease(myGradient);*/ // draw outline so that the edges are smooth: // set line width //CGContextSetLineWidth(context, 1); // set the colour when drawing lines R,G,B,A. (we will set it to the same colour we used as the start and end point of our gradient ) /*CGContextSetRGBStrokeColor(context, 0.4,0.4,0.4,0.9); // draw an ellipse in the provided rectangle CGContextAddEllipseInRect(context, _outerCircleRect); CGContextStrokePath(context);*/ /*CGContextAddEllipseInRect(context, _innerCircleRect); CGContextStrokePath(context);*/ // Draw the progress: // First clip the drawing area: // save the context before clipping CGContextSaveGState(context); CGContextMoveToPoint(context, _outerCircleRect.origin.x + _outerCircleRect.size.width/2, // move to the top center of the outer circle _outerCircleRect.origin.y +1); // the Y is one more because we want to draw inside the bigger circles. // add an arc relative to _progress CGContextAddArc(context, _outerCircleRect.origin.x + _outerCircleRect.size.width/2, _outerCircleRect.origin.y + _outerCircleRect.size.width/2, _outerCircleRect.size.width/2-1, -M_PI/2, (-M_PI/2 + _progress*2*M_PI), 0); CGContextAddArc(context, _outerCircleRect.origin.x + _outerCircleRect.size.width/2, _outerCircleRect.origin.y + _outerCircleRect.size.width/2, _outerCircleRect.size.width/2 - 9, (-M_PI/2 + _progress*2*M_PI), -M_PI/2, 1); // use clode path to connect the last point in the path with the first point (to create a closed path) CGContextClosePath(context); // clip to the path stored in context CGContextClip(context); // Progress drawing code comes here: // set the gradient colours based on class variables. CGFloat components2[12] = { _r, _g, _b, _a, // Start color ((_r + 0.5 > 1) ? 1 : (_r+0.5) ) , ((_g + 0.5 > 1) ? 1 : (_g+0.5) ), ((_b + 0.5 > 1) ? 1 : (_b+0.5) ), ((_a + 0.5 > 1) ? 1 : (_a+0.5)), _r, _g, _b, _a }; // End color myGradient = CGGradientCreateWithColorComponents (myColorspace, components2,locations, num_locations); myStartPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myStartPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; myEndPoint.x = _innerCircleRect.origin.x + _innerCircleRect.size.width/2; myEndPoint.y = _innerCircleRect.origin.y + _innerCircleRect.size.width/2; // set the radias for start and endpoints a bit smaller, because we want to draw inside the outer circles. myStartRadius = _innerCircleRect.size.width/2; myEndRadius = _outerCircleRect.size.width/2; CGContextDrawRadialGradient(context, myGradient, myStartPoint, myStartRadius, myEndPoint, myEndRadius, 0); // release myGradient and myColorSpace CGGradientRelease(myGradient); CGColorSpaceRelease(myColorspace); // draw circle on the outline to smooth it out. CGContextSetRGBStrokeColor(context, _r,_g,_b,_a); // draw an ellipse in the provided rectangle CGContextAddEllipseInRect(context, _outerCircleRect); CGContextStrokePath(context); CGContextAddEllipseInRect(context, _innerCircleRect); CGContextStrokePath(context); //restore the context and remove the clipping area. CGContextRestoreGState(context); // restore the context's state when we are done with it: CGContextRestoreGState(context); /*CGPathRef circlePath = CGPathCreateMutable(); CGPathAddEllipseInRect(circlePath , NULL , rect); CAShapeLayer *circle = [[CAShapeLayer alloc] init]; circle.path = circlePath; circle.opacity = 0.5; [self.imageView.layer addSublayer:circle]; CGPathRelease( circlePath ); [circle release];*/ }
for (int i=0; i<drawbox.size.width/2; i++) { for (int j=(int)(-drawbox.size.height/4.0 * sqrt(1 - pow(4.0*i/drawbox.size.width - 1, 2)) + drawbox.size.height/4.0); j<=(int)(drawbox.size.height/4.0 * sqrt(1 - pow(4.0*i/drawbox.size.width - 1, 2)) + drawbox.size.height/4.0); j++) { Point.y++; NSLog(#"point:%f,%f",Point.x,Point.y); } tStartPoint.x++; } This will draw an ellipse with the same center the rectangle has you drow in the question.
UIImageView & CGAffineTransformScale, getting wrong path
I'm trying to add a border to image (UIImageView), everything works fine except one thing: when i'm resizing image i'm getting larger ( or smaller) shape. Here's some code: CGPoint center = view.center; // view is a UIImageView CGAffineTransform transform = CGAffineTransformIdentity; CGFloat angle = [(NSNumber *)[view valueForKeyPath:#"layer.transform.rotation.z"] floatValue]; CGFloat scale = [(NSNumber *)[view valueForKeyPath:#"layer.transform.scale"] floatValue]; transform = CGAffineTransformTranslate(transform, center.x, center.y); transform = CGAffineTransformScale(transform, scale, scale); transform = CGAffineTransformRotate(transform, angle); transform = CGAffineTransformTranslate(transform, -center.x, -center.y); CGPathRef path = CGPathCreateWithRect(view.frame, &transform); // looks like the problem is here shape.path = path; // shape is a CAShapeLayer CGPathRelease(path);
Solved by replacing CGPathRef path = CGPathCreateWithRect(view.frame, &transform); with CGPathAddLines(path, nil, points, 5);