ionade
ionade

Reputation: 1

iOS How to draw a stroke with an outline

I am looking for an output as in this image Expected Result

I need an outline to my stroke. My code is as follows

- (void)awakeFromNib {
    self.strokeArray = [NSMutableArray array];
    self.layerIndex = 0;
    self.isSolid = false;
    self.path = [[UIBezierPath alloc] init];
    self.innerPath = [[UIBezierPath alloc] init];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [self.path moveToPoint:p];
    [self.innerPath moveToPoint:p];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [self.path addLineToPoint:p];
    [self.innerPath addLineToPoint:p];
    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    [self.path addLineToPoint:p];
    [self.innerPath addLineToPoint:p];
    [self drawBitmap];
    [self setNeedsDisplay];
    [self.path removeAllPoints];
    [self.innerPath removeAllPoints];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}
- (void)drawRect:(CGRect)rect
{
    [self.incrementalImage drawInRect:rect];
    [self.brushColor setStroke];
    self.path.lineWidth = self.brushWidth;
    if(self.isEraser)
        [self.path strokeWithBlendMode:kCGBlendModeClear alpha:0.0];
    else
        [self.path stroke];
    self.innerPath.lineWidth = self.brushWidth - 10;
    [[UIColor clearColor] setStroke];
    [self.innerPath strokeWithBlendMode:kCGBlendModeClear alpha:1.0];
}
- (void)drawBitmap
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (!self.incrementalImage)
    {
        CGContextClearRect(context,       CGRectMake(0,0,self.bounds.size.width,self.bounds.size.height));
    }
    [self.incrementalImage drawAtPoint:CGPointZero];
    [self.brushColor setStroke];
    self.path.lineWidth = self.brushWidth;
    if(self.isEraser)
        [self.path strokeWithBlendMode:kCGBlendModeClear alpha:0.0];
    else
        [self.path stroke];
    self.innerPath.lineWidth = self.brushWidth - 10;
    [[UIColor clearColor] setStroke];
    [self.innerPath strokeWithBlendMode:kCGBlendModeClear alpha:1.0];
    self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
}

On screen what i get is as per this image, Actual Result

I understand that blend mode 'clear' gives an eraser effect. What i want is that the stroke should have a solid outline on the sides and be clear in the middle. It should not blend into the path right below it.The path below it should still be visible. How can i achieve this result?

Upvotes: 0

Views: 645

Answers (1)

robert
robert

Reputation: 2842

Have a look at this question: Generate a CGPath from another path's line width outline

I guess that's what you need: creating a polygon of the line and stroke that instead of the line. Use CGPathCreateCopyByStrokingPath to get a path of the polygon and stroke it.

OK, since CGPathCreateCopyByStrokingPath seems to be a bit buggy, you could create your own implementation. Something like the following algorithm, which assumes you have a NSArray of CGPoint packed in NSValue objects. It works pretty well until you start adding short overlapping lines which could of course easily happen while drawing. You could reduce this effect by only adding points in touchesMoved that have a larger distance (lineWidth/2?) to the previous added point. Another drawback is that the lines are simply straight (no kCGLineJoinRound or kCGLineCapRound) but maybe you could adjust the algorithm to do that.

+ (UIBezierPath*)polygonForLine:(NSArray *)points withLineWidth:(CGFloat)lineWidth{
    UIBezierPath *path = [UIBezierPath bezierPath];

    //stores the starting point to close the path
    CGPoint startPoint;

    //get the points to a c-array for easier access
    CGPoint cpoints[points.count];
    int numPoints = 0;
    for(NSValue *v in points){
        cpoints[numPoints++] = [v CGPointValue];
    }

    //store the last intersection to apply it for the next segement
    BOOL hasIntersection = NO;
    CGPoint intersectionPoint;

    for (int i=0;i<numPoints-1;i++){
        //get the current line segment
        CGPoint p1 = cpoints[i];
        CGPoint p2 = cpoints[i+1];
        CGPoint l1p1,l1p2;
        getOffsetLineSegmentForPoints(p1,p2,lineWidth,&l1p1,&l1p2);

        //if there had been an intersection with the previous segement, start here to get a nice outline
        if(hasIntersection){
            l1p1 = intersectionPoint;
        }

        //is there a next segment?
        if(i+2<numPoints){
            //get the next line segment
            p1 = cpoints[i+1];
            p2 = cpoints[i+2];
            CGPoint l2p1,l2p2;
            getOffsetLineSegmentForPoints(p1,p2,lineWidth,&l2p1,&l2p2);

            //calculate the intersection point with the current line segment
            hasIntersection = getLineIntersection(l1p1, l1p2, l2p1, l2p2, &intersectionPoint);

            //if they intersect, the current linesegment has to end here to get a nice outline
            if(hasIntersection){
                l1p2 = intersectionPoint;
            }
        }

        //write the current linesegment to the path
        if(i==0){
            //first point, move to it and store it for closing the path later on
            startPoint = l1p1;
            [path moveToPoint:startPoint];
        }else{
            [path addLineToPoint:l1p1];
        }
        [path addLineToPoint:l1p2];
    }

    //now do the same for the other side of the future polygon
    hasIntersection = NO;//reset intersections
    for (int i=numPoints-1;i>0;i--){
        //get the current line segment
        CGPoint p1 = cpoints[i];
        CGPoint p2 = cpoints[i-1];
        CGPoint l1p1,l1p2;
        getOffsetLineSegmentForPoints(p1,p2,lineWidth,&l1p1,&l1p2);

        //if there had been an intersection with the previous segement, start here to get a nice outline
        if(hasIntersection){
            l1p1 = intersectionPoint;
        }

        //is there a next segment?
        if(i-2>=0){
            //get the next line segment
            p1 = cpoints[i-1];
            p2 = cpoints[i-2];
            CGPoint l2p1,l2p2;
            getOffsetLineSegmentForPoints(p1,p2,lineWidth,&l2p1,&l2p2);

            //calculate the intersection point with the current line segment
            hasIntersection = getLineIntersection(l1p1, l1p2, l2p1, l2p2, &intersectionPoint);
            //if they intersect, the current linesegment has to end here to get a nice outline
            if(hasIntersection){
                l1p2 = intersectionPoint;
            }
        }

        //write the current linesegment to the path
        [path addLineToPoint:l1p1];
        [path addLineToPoint:l1p2];
    }

    //close the path
    [path addLineToPoint:startPoint];

    //we're done
    return path;
}

void getOffsetLineSegmentForPoints(CGPoint p1, CGPoint p2, CGFloat lineWidth, CGPoint *linep1, CGPoint *linep2){
    CGPoint offset = CGPointSub(p2, p1);
    offset = CGPointNorm(offset);
    offset = CGPointOrthogonal(offset);
    offset = CGPointMultiply(offset, lineWidth/2);

    (*linep1) = CGPointAdd(p1, offset);
    (*linep2) = CGPointAdd(p2, offset);
}

CGPoint CGPointSub(CGPoint p1, CGPoint p2){
    return CGPointMake(p1.x-p2.x, p1.y-p2.y);
}

CGPoint CGPointAdd(CGPoint p1, CGPoint p2){
    return CGPointMake(p1.x+p2.x, p1.y+p2.y);
}

CGFloat CGPointLength(CGPoint p){
    return sqrtf(powf(p.x,2)+powf(p.y,2));
}

CGPoint CGPointNorm(CGPoint p){
    CGFloat length = CGPointLength(p);
    if(length==0)
        return CGPointZero;
    return CGPointMultiply(p, 1/length);
}

CGPoint CGPointMultiply(CGPoint p, CGFloat f){
    return CGPointMake(p.x*f, p.y*f);
}

CGPoint CGPointOrthogonal(CGPoint p){
    return CGPointMake(p.y, -p.x);
}

BOOL getLineIntersection(CGPoint l1p1, CGPoint l1p2, CGPoint l2p1,
                       CGPoint l2p2, CGPoint *intersection)
{
    CGPoint s1 = CGPointSub(l1p2, l1p1);
    CGPoint s2 = CGPointSub(l2p2, l2p1);
    float determinant = (-s2.x * s1.y + s1.x * s2.y);
    if(determinant==0)
        return NO;
    CGPoint l2p1l1p1 = CGPointSub(l1p1, l2p1);
    float s, t;
    s = (-s1.y * l2p1l1p1.x + s1.x * l2p1l1p1.y) / determinant;
    t = ( s2.x * l2p1l1p1.y - s2.y * l2p1l1p1.x) / determinant;

    if (s >= 0 && s <= 1 && t >= 0 && t <= 1){
        if (intersection != NULL){
            (*intersection).x = l1p1.x + (t * s1.x);
            (*intersection).y = l1p1.y + (t * s1.y);
        }

        return YES;
    }

    return NO;
}

Upvotes: 3

Related Questions