10

Scenario:

I have a set of CGPaths. They are mostly just lines (i.e. not closed shapes). They are drawn on the screen in a UIView's draw method.

How can I check if the user tapped near one of the paths?

Here's what I had working:

UIGraphincsBeginImageContext(CGPathGetBoundingBox(path));
CGContextRef g = UIGraphicsGetCurrentContext();
CGContextAddPath(g,path);
CGContextSetLineWidth(g,15);
CGContextReplacePathWithStrokedPath(g);
CGPath clickArea = CGContextCopyPath(g);  //Not documented
UIGraphicsEndImageContext();

So what I'm doing is creating an image context, because it has the functions I need. I then add the path to the context, and set the line width to 15. Stroking the path at this point would create the click area I can check inside of to find clicks. So I get that stroked path by telling the context to turn the path into a stroked path, then copying that path back out into another CGPath. Later, I can check:

if (CGPathContainsPoint(clickArea,NULL,point,NO)) { ...

It all worked well and good, but the CGContextCopyPath, being undocumented, seemed like a bad idea to use for obvious reasons. There's also a certain kludginess about making a CGContext just for this purpose.

So, does anybody have any ideas? How do I check if a user tapped near (in this case, within 15 pixels) of any area on a CGPath?

Ed Marty
  • 39,590
  • 19
  • 103
  • 156

3 Answers3

23

In iOS 5.0 and later, this can be done more simply using CGPathCreateCopyByStrokingPath:

CGPathRef strokedPath = CGPathCreateCopyByStrokingPath(path, NULL, 15,
    kCGLineCapRound, kCGLineJoinRound, 1);
BOOL pointIsNearPath = CGPathContainsPoint(strokedPath, NULL, point, NO);
CGPathRelease(strokedPath);

if (pointIsNearPath) ...
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
2

Well, I figured out an answer. It uses CGPathApply:

clickArea = CGPathCreateMutable();
CGPathApply(path,clickArea,&createClickArea);

void createClickArea (void *info, const CGPathElement *elem) {
  CGPathElementType type = elem->type;
  CGMutablePathRef path = (CGMutablePathRef)info;
  static CGPoint last;
  static CGPoint subpathStart;
  switch (type) {
    case kCGPathElementAddCurveToPoint:
    case kCGPathElementAddQuadCurveToPoint:
      break;
    case kCGPathElmentCloseSubpath:
    case kCGPathElementMoveToPoint: {
      CGPoint p = type == kCGPathElementAddLineToPoint ? elem->points[0] : subpathStart;
      if (CGPointEqualToPoint(p,last)) {
        return;
      }
      CGFloat rad = atan2(p.y - last.y, p.x - last.x);
      CGFloat xOff = CLICK_DIST * cos(rad);
      CGFloat yOff = CLICK_DIST * sin(rad);
      CGPoint a = CGPointMake(last.x - xOff, last.y - yOff);
      CGPoint b = CGPointMake(p.x + xOff, p.y + yOff);
      rad += M_PI_2;
      xOff = CLICK_DIST * cos(rad);
      yOff = CLICK_DIST * sin(rad);
      CGPathMoveToPoint(path, NULL, a.x - xOff, a.y - yOff);
      CGPathAddLineToPoint(path, NULL, a.x + xOff, a.y + yOff);
      CGPathAddLineToPoint(path, NULL, b.x + xOff, b.y + yOff);
      CGPathAddLineToPoint(path, NULL, b.x - xOff, b.y - yOff);
      CGPathCloseSubpath(path);
      last = p;
      break; }
    case kCGPathElementMoveToPoint:
      subpathStart = last = elem->points[0];
      break;
  }
}

Basically it's just my own method for ReplacePathWithStrokedPath, but it only works with lines for right now.

Ed Marty
  • 39,590
  • 19
  • 103
  • 156
  • Thanks a lot for posting this! i myself wouldnt have a clue about it. Although the accepted answer works fine, but this function basically saved me as it gives you flexibility - i can treat path elements differently: exactly what i needed. Thanks again! – user1244109 Aug 12 '14 at 18:20
0

In Swift

let area = stroke.copy(strokingWithWidth: 15, lineCap: .round, lineJoin: .round, miterLimit: 1)
if (area.contains(point)) { ... }
duan
  • 8,515
  • 3
  • 48
  • 70