How to draw a gradient line (fading in/out) with Core Graphics/iPhone?

Walchy picture Walchy · Aug 20, 2009 · Viewed 39.8k times · Source

I know how to draw a simple line:

CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);

And I know how to do a gradient rectangle, i.g.:

CGColorSpaceRef myColorspace=CGColorSpaceCreateDeviceRGB();
size_t num_locations = 2;
CGFloat locations[2] = { 1.0, 0.0 };
CGFloat components[8] = { 0.0, 0.0, 0.0, 1.0,    1.0, 1.0, 1.0, 1.0 };

CGGradientRef myGradient = CGGradientCreateWithColorComponents(myColorspace, components, locations, num_locations);

CGPoint myStartPoint, myEndPoint;
myStartPoint.x = 0.0;
myStartPoint.y = 0.0;
myEndPoint.x = 0.0;
myEndPoint.y = 10.0;
CGContextDrawLinearGradient (context, myGradient, myStartPoint, myEndPoint, 0);

But how could I draw a line with a gradient, i.g. fading in from black to white (and maybe fading out to black on the other side as well) ?

Answer

Benjohn picture Benjohn · Jul 30, 2014

It is possible to stroke arbitrary paths with a gradient, or any other fill effect, such as a pattern.

As you have found, stroked paths are not rendered with the current gradient. Only filled paths use the gradient (when you turn them in to a clip and then draw the gradient).

However, Core Graphics has an amazingly cool procedure CGContextReplacePathWithStrokedPath that will transform the path you intend to stroke in to a path that is equivalent when filled.

Behind the scenes, CGContextReplacePathWithStrokedPath builds up an edge polygon around your stroke path and switches that for the path you have defined. I'd speculate that the Core Graphics rendering engine probably does this anyway in calls to CGContextStrokePath.

Here's Apple's documentation on this:

Quartz creates a stroked path using the parameters of the current graphics context. The new path is created so that filling it draws the same pixels as stroking the original path. You can use this path in the same way you use the path of any context. For example, you can clip to the stroked version of a path by calling this function followed by a call to the function CGContextClip.

So, convert your path in to something you can fill, turn that in to a clip, and then draw your gradient. The effect will be as if you had stroked the path with the gradient.

Code

It'll look something like this…

    // Get the current graphics context.
    //
    const CGContextRef context = UIGraphicsGetCurrentContext();

    // Define your stroked path. 
    //
    // You can set up **anything** you like here.
    //
    CGContextAddRect(context, yourRectToStrokeWithAGradient);

    // Set up any stroking parameters like line.
    //
    // I'm setting width. You could also set up a dashed stroke
    // pattern, or whatever you like.
    //
    CGContextSetLineWidth(context, 1);

    // Use the magical call.
    //
    // It turns your _stroked_ path in to a **fillable** one.
    //
    CGContextReplacePathWithStrokedPath(context);

    // Use the current _fillable_ path in to define a clipping region.
    //
    CGContextClip(context);

    // Draw the gradient.
    //
    // The gradient will be clipped to your original path.
    // You could use other fill effects like patterns here.
    //
    CGContextDrawLinearGradient(
      context, 
      yourGradient, 
      gradientTop, 
      gradientBottom, 
      0
    );

Further notes

It's worth emphasising part of the documentation above:

Quartz creates a stroked path using the parameters of the current graphics context.

The obvious parameter is the line width. However, all line drawing state is used, such as stroke pattern, mitre limit, line joins, caps, dash patterns, etc. This makes the approach extremely powerful.

For additional details see this answer of this S.O. question.