draw embossed arc using core graphics

core-graphics, ios, iphone

Solution

Unless you are going to be changing the shape dynamically, you would probably be better off just creating the image in an image editor. I know it's easy to create that effect in Photoshop, Illustrator, or Fireworks.

That said, drawing an inner shadow like that with Core Graphics requires several steps:

- Clip to the shape (using e.g. `CGContextClip` or `CGContextClipToMask`).

- Make a path or mask of everything but the shape.

- Set your shadow parameters (using `CGContextSetShadowWithColor`).

- Fill the path or mask from step 2. This casts a shadow inside the shape, and only the shadow is drawn because you clipped to the shape in step 1.

If you do all of that correctly, you can get a nice result like this:

Here's the code I wrote to draw that. I wrote it in the `drawRect:` of a custom view subclass, but you can easily use this code to draw into any graphics context.

- (void)drawRect:(CGRect)rect {
    CGContextRef gc = UIGraphicsGetCurrentContext();

First, I create a path that's just an arc:

    static CGFloat const kArcThickness = 20.0f;
    CGRect arcBounds = CGRectInset(self.bounds, 10.0f, 10.0f);
    CGPoint arcCenter = CGPointMake(CGRectGetMidX(arcBounds), CGRectGetMidY(arcBounds));
    CGFloat arcRadius = 0.5f * (MIN(arcBounds.size.width, arcBounds.size.height) - kArcThickness);
    UIBezierPath *arc = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:arcRadius startAngle:-M_PI / 3.0 endAngle:-2.0 * M_PI / 3.0 clockwise:NO];

Next, I ask Core Graphics to make a new path that is the outline of the `arc` path. Note how I ask it for a stroke width of `kArcThickness` and round line caps:

    CGPathRef shape = CGPathCreateCopyByStrokingPath(arc.CGPath, NULL, kArcThickness, kCGLineCapRound, kCGLineJoinRound, 10.0f);

I also need the inverse of that path: a path that includes every point except the points in `shape`. It so happens (although I don't think it's documented) that `CGContextCreateCopyByStrokingPath` and `CGPathAddRect` draw in opposite directions. So if I copy `shape` and draw an enormous rectangle around it, the non-zero winding rule means that the new path will be the inverse of `shape`:

    CGMutablePathRef shapeInverse = CGPathCreateMutableCopy(shape);
    CGPathAddRect(shapeInverse, NULL, CGRectInfinite);

Now I can actually start drawing. First, I'll fill in the shape with a light gray color:

    CGContextBeginPath(gc);
    CGContextAddPath(gc, shape);
    CGContextSetFillColorWithColor(gc, [UIColor colorWithWhite:.9 alpha:1].CGColor);
    CGContextFillPath(gc);

Next I actually perform the four steps I listed above. I have to save the graphics state so I can undo the clipping and shadow parameters when I'm done.

    CGContextSaveGState(gc); {

Step 1: clip to the shape:

        CGContextBeginPath(gc);
        CGContextAddPath(gc, shape);
        CGContextClip(gc);

Step 2: Well, I did this step already when I created `shapeInverse`.

Step 3: I set the shadow parameters:

        CGContextSetShadowWithColor(gc, CGSizeZero, 7, [UIColor colorWithWhite:0 alpha:.25].CGColor);

Step 4: I fill the inverse shape from step 2:

        CGContextBeginPath(gc);
        CGContextAddPath(gc, shapeInverse);
        CGContextFillPath(gc);

Now I restore the graphics state, which specifically restores the clipping path and unsets the shadow parameters.

    } CGContextRestoreGState(gc);

Finally, I'll stroke `shape` with a light gray to make the edge crisper:

    CGContextSetStrokeColorWithColor(gc, [UIColor colorWithWhite:.75 alpha:1].CGColor);
    CGContextSetLineWidth(gc, 1);
    CGContextSetLineJoin(gc, kCGLineCapRound);
    CGContextBeginPath(gc);
    CGContextAddPath(gc, shape);
    CGContextStrokePath(gc);

Of course I clean up when I'm done:

    CGPathRelease(shape);
    CGPathRelease(shapeInverse);
}

For more complex shapes, you can look at my answer here and my answer here.

Here's all the code together for easy copying:

- (void)drawRect:(CGRect)rect {
    CGContextRef gc = UIGraphicsGetCurrentContext();

    static CGFloat const kArcThickness = 20.0f;
    CGRect arcBounds = CGRectInset(self.bounds, 10.0f, 10.0f);
    CGPoint arcCenter = CGPointMake(CGRectGetMidX(arcBounds), CGRectGetMidY(arcBounds));
    CGFloat arcRadius = 0.5f * (MIN(arcBounds.size.width, arcBounds.size.height) - kArcThickness);
    UIBezierPath *arc = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:arcRadius startAngle:-M_PI / 3.0 endAngle:-2.0 * M_PI / 3.0 clockwise:NO];
    CGPathRef shape = CGPathCreateCopyByStrokingPath(arc.CGPath, NULL, kArcThickness, kCGLineCapRound, kCGLineJoinRound, 10.0f);
    CGMutablePathRef shapeInverse = CGPathCreateMutableCopy(shape);
    CGPathAddRect(shapeInverse, NULL, CGRectInfinite);

    CGContextBeginPath(gc);
    CGContextAddPath(gc, shape);
    CGContextSetFillColorWithColor(gc, [UIColor colorWithWhite:.9 alpha:1].CGColor);
    CGContextFillPath(gc);

    CGContextSaveGState(gc); {
        CGContextBeginPath(gc);
        CGContextAddPath(gc, shape);
        CGContextClip(gc);
        CGContextSetShadowWithColor(gc, CGSizeZero, 7, [UIColor colorWithWhite:0 alpha:.25].CGColor);
        CGContextBeginPath(gc);
        CGContextAddPath(gc, shapeInverse);
        CGContextFillPath(gc);
    } CGContextRestoreGState(gc);

    CGContextSetStrokeColorWithColor(gc, [UIColor colorWithWhite:.75 alpha:1].CGColor);
    CGContextSetLineWidth(gc, 1);
    CGContextSetLineJoin(gc, kCGLineCapRound);
    CGContextBeginPath(gc);
    CGContextAddPath(gc, shape);
    CGContextStrokePath(gc);

    CGPathRelease(shape);
    CGPathRelease(shapeInverse);
}

Problem

I am trying to implement a custom slider as shown in figure below. what I have done so far looks something like this please help me out for drawing the arc with such effect. my code is as below, what I am doing is drawing the arc using CGContextAddArc with line width kLineWidth. ``` - (void)drawThumbAtPoint:(CGPoint)sliderButtonCenterPoint inContext: (CGContextRef)context { UIGraphicsPushContext(context); CGContextBeginPath(context); CGContextMoveToPoint(context, sliderButtonCenterPoint.x, sliderButtonCenterPoint.y); CGImageRef imageRef = [UIImage imageNamed:@"circle25.png"].CGImage; CGRect rect = CGRectMake(sliderButtonCenterPoint.x - kThumbRadius, sliderButtonCenterPoint.y - kThumbRadius, kThumbRadius*2, kThumbRadius*2); CGContextDrawImage(context, rect, imageRef); //CGContextAddArc(context, sliderButtonCenterPoint.x, sliderButtonCenterPoint.y, kThumbRadius, 0.0, 2*M_PI, NO); CGContextFillPath(context); UIGraphicsPopContext(); } - (CGPoint)drawArcTrack:(float)track atPoint:(CGPoint)center withRadius:(CGFloat)radius inContext:(CGContextRef)context { UIGraphicsPushContext(context); CGContextBeginPath(context); float angleFromTrack = translateValueFromSourceIntervalToDestinationInterval(track, self.minimumValue, self.maximumValue, 0, M_PI/3);// 2*M_PI CGFloat startAngle = (4*M_PI)/3; CGFloat endAngle = startAngle + angleFromTrack; CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, NO); CGPoint arcEndPoint = CGContextGetPathCurrentPoint(context); CGContextStrokePath(context); UIGraphicsPopContext(); return arcEndPoint; } - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGPoint middlePoint; middlePoint.x = self.bounds.origin.x + self.bounds.size.width/2; middlePoint.y = self.bounds.origin.y + self.bounds.size.width; CGContextSetLineWidth(context, kLineWidth); CGFloat radius = [self sliderRadius]; [self.maximumTrackTintColor setStroke]; [self drawArcTrack:self.maximumValue atPoint:middlePoint withRadius:radius inContext:context]; [self.minimumTrackTintColor setStroke]; self.thumbCenterPoint = [self drawArcTrack:self.value atPoint:middlePoint withRadius:radius inContext:context]; [self.thumbTintColor setFill]; [self drawThumbAtPoint:self.thumbCenterPoint inContext:context]; } ```

Original source

Related problems