Search code examples
iosquartz-2dfillareabezier

How to fill the area below a path in Quartz 2D?


I am using Quartz 2D in a UIView object to draw several curves, like this example:

Two curves drawn using Quartz 2D with <code>CGContextStrokePath</code>

This is the code I have right now (I have omitted the calculation of control points and other stuff):

for (int i = 1; i < points.count; ++i) {
  CGContextMoveToPoint(context, previousXValue, previousYValue);
  CGContextAddCurveToPoint(context,
    firstControlPointXValue, firstControlPointYValue,
    secondControlPointXValue, secondControlPointYValue,
    xValue, yValue
  );

  // CGContextFillPath(context);
  CGContextStrokePath(context);
}

Now I would like to fill the area below the curve, but if I use CGContextFillPath the result is the following:

Two curves filled using Quartz 2D with <code>CGContextFillPath</code>

which makes sense, because according to Quartz 2D documentation:

When you fill the current path, Quartz acts as if each subpath contained in the path were closed. It then uses these closed subpaths and calculates the pixels to fill.

Then I tried to move the path to the lower right corner and close the path, but the fill method doesn't have any effect:

CGContextMoveToPoint(context, rect.size.width, rect.size.height);
CGContextClosePath(context);
CGContextFillPath(context);

How could I fill the whole area below the curve and not just the subarea closed in every subpath?

EDIT:

I have found a temporary solution: drawing a shape using the curve and two vertical lines on each subpath:

for (int i = 1; i < points.count; ++i) {
  // Draw the curve
  CGContextMoveToPoint(context, previousXValue, previousYValue);
  CGContextAddCurveToPoint(context,
    firstControlPointXValue, firstControlPointYValue,
    secondControlPointXValue, secondControlPointYValue,
    xValue, yValue
  );
  CGContextStrokePath(context);

  // Draw a shape using curve's initial and final points
  CGContextMoveToPoint(context, previousXValue, rect.size.height);
  CGContextAddLineToPoint(context, previousXValue, previousYValue);
  CGContextAddCurveToPoint(context,
    firstControlPointXValue, firstControlPointYValue,
    secondControlPointXValue, secondControlPointYValue,
    xValue, yValue
  );
  CGContextAddLineToPoint(context, xValue, rect.size.height);
  CGContextFillPath(context);
}

I don't know if this is overkill, so improvements are also welcomed. Besides, I'm getting this result:

Two curves drawn using Quartz 2D with the area below them filled

Note that vertical lines appear as a result of drawing the subareas next to each other. How can that be avoided?


Solution

  • The idea is right, but you should not fill the individual curves, but rather, create a single path and then fill that. So, start with CGContextMoveToPoint to the lower left corner, CGContextAddLineToPoint to your first point, do the CGContextAddCurveToPoint for all of the curves, and at the end, do a CGContextAddLineToPoint to the lower right corner (and you might as well do a CGContextClosePath for good measure).

    This is a simplified example, where I just have an array of points, but it illustrates the idea:

    - (void)drawRect:(CGRect)rect
    {
        if (!self.points)
            [self configurePoints];
    
        DataPoint *point;
    
        CGContextRef context = UIGraphicsGetCurrentContext();
    
        CGColorRef color = [[UIColor redColor] CGColor];
        CGContextSetStrokeColorWithColor(context, color);
        CGContextSetFillColorWithColor(context, color);
    
        point = self.points[0];
    
        CGContextMoveToPoint(context, point.x, self.bounds.size.height);
        CGContextAddLineToPoint(context, point.x, point.y);
    
        for (point in self.points)
        {
            // you'd do your CGContextAddCurveToPoint here; I'm just adding lines
            CGContextAddLineToPoint(context, point.x, point.y);
        }
    
        point = [self.points lastObject];
        CGContextAddLineToPoint(context, point.x, self.bounds.size.height);
        CGContextClosePath(context);
    
        CGContextDrawPath(context, kCGPathFillStroke);
    }