简体   繁体   中英

How do I detect a touch on a UIBezierPath and move a ball along that?

How do I move a ball along a specific UiBezierPath? Is that possible?

I've tried everything including doing a hit test using

-(BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath*)path inFillArea:(BOOL)inFill

How do I detect a touch along a path?

Firstly ... it is an absolute fact that:

there is, definitely, NO method, provided by Apple,
to extract points from a UIBezierPath .

That is an absolute fact as of February 2011, in the latest OS, and everything just on the horizon. I've spoken to the relevant engineers at Apple about the issue. It's annoying in many game programming contexts, but that's the fact. So, it could be that answers your question?

Secondly: don't forget it's as easy as pie to animate something along a UIBezierPath. There are numerous answers on SO, for example How can I animate the movement of a view or image along a curved path?

Thirdly: Regarding finding out where touches happened, if that's what you're asking, as Cory said, this is a hard problem! But you can use CGContextPathContainsPoint to find out if a point (or touch) was on a path of a given thickness.

After that, assuming we are talking about cubics, you need to find points and likely velocities (aka tangents) along a bezier.

Here is the exact, total code to do that: Find the tangent of a point on a cubic bezier curve (on an iPhone) . I pasted it in below, too.

You will have to implement your own MCsim or iteration to find where it hit, depending on your situation. That's not so hard.

(Fourthly -- as a footnote, there's that new thing where you can progressively draw a bezpath, probably not relevant to your question but just a note.)


For the convenience of anyone reading in the future, here is the summary from the linked question of the two "handy" routines...

Finally, here in the simplest possible fashion are the two routines to calculate equidistant points (in fact, approximately equidistant) and the tangents of those, along a bezier cubic:

CGFloat bezierPoint(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
    {
    CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
    CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
    CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
    CGFloat C4 = ( a );

    return ( C1*t*t*t + C2*t*t + C3*t + C4  );
    }

CGFloat bezierTangent(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d)
    {
    CGFloat C1 = ( d - (3.0 * c) + (3.0 * b) - a );
    CGFloat C2 = ( (3.0 * c) - (6.0 * b) + (3.0 * a) );
    CGFloat C3 = ( (3.0 * b) - (3.0 * a) );
    CGFloat C4 = ( a );

    return ( ( 3.0 * C1 * t* t ) + ( 2.0 * C2 * t ) + C3 );
    }

The four precalculated values, C1 C2 C3 C4, are sometimes called the coefficients of the bezier. (Recall that abcd are usually called the four control points.) Of course, t runs from 0 to 1, perhaps for example every 0.05. Simply call these routines once for X and separately once for Y . Hope it helps someone !

there is a method in bezierpath class called containsPoint: .. refer: http://developer.apple.com/library/ios/#documentation/2ddrawing/conceptual/drawingprintingios/BezierPaths/BezierPaths.html

and you can detect weather the touch point is in bezier path object or not. I have used this with my own method by which a user can easily detect a touch on the bezier path (not inside or out sied if a circle or close path is there).

This code lets user select a bezier path drawing object on touch of it and a dashed line with animation appears on it. hope it helps someone. Here is code from my own project:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{

    if([[self tapTargetForPath:path] containsPoint:startPoint])  // 'path' is bezier path object
          {

                [self selectShape:currentSelectedPath];// now select a new/same shape

                NSLog(@"selectedShapeIndex: %d", selectedShapeIndex);

                break;
            }
}

// this method will let you easily select a bezier path ( 15 px up and down of a path drawing)
- (UIBezierPath *)tapTargetForPath:(UIBezierPath *)path
{
    if (path == nil) {
        return nil;
    }

    CGPathRef tapTargetPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, fmaxf(35.0f, path.lineWidth), path.lineCapStyle, path.lineJoinStyle, path.miterLimit);
    if (tapTargetPath == NULL) {
        return nil;
    }

    UIBezierPath *tapTarget = [UIBezierPath bezierPathWithCGPath:tapTargetPath];
    CGPathRelease(tapTargetPath);
    return tapTarget;
}

-(void)selectShape:(UIBezierPath *)pathToSelect
{
    centerline = [CAShapeLayer layer];
    centerline.path = pathToSelect.CGPath;
    centerline.strokeColor = [UIColor whiteColor].CGColor;
    centerline.fillColor = [UIColor clearColor].CGColor;
    centerline.lineWidth = 1.0;
    centerline.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInt:10], [NSNumber numberWithInt:5], nil];
    [self.layer addSublayer:centerline];

    // showing animation on line
    CABasicAnimation *dashAnimation;
    dashAnimation = [CABasicAnimation animationWithKeyPath:@"lineDashPhase"];

    [dashAnimation setFromValue:[NSNumber numberWithFloat:0.0f]];
    [dashAnimation setToValue:[NSNumber numberWithFloat:30.0f]];
    [dashAnimation setDuration:1.0f];
    [dashAnimation setRepeatCount:10000];

    [centerline addAnimation:dashAnimation forKey:@"linePhase"];
}

This is a really hard problem. You'll need to determine the point on the path closest to the touched point, which is a complicated math problem. This is the best thing I could find using a quick Google search. (Beware: the code is in BASIC.)

http://www.tinaja.com/glib/bezdist.pdf

I admit, I'm tempted to write this myself. It'd be very useful, and I like a challenge.

Mathematically, you can not determine if a point inside a curve, a pure mathematical one, as a curve has no surface. What you can do, is test if the point is inside the surface defined by the bounds of the closed curve created by CGPathCreateCopyByStrokingPath with CGPathContainsPoint . It creates the curve that is filled to render your curve on the screen.

If CGPathCreateCopyByStrokingPath is called with its parameter: CGFloat lineWidth less or equal to the minimum radius of curvature of the original curve, it will not self-intersect. Also the original curve should not self intersects.

If CGPathContainsPoint returns true, and both conditions I mentioned are met, you are guaranteed to find one and only point on the curve that is at a distance of lineWidth . There is no algebraic solution to this, so you have to find an approximate point on the curve that is close to the touched point with the method described here: Find a point, a given distance, along a simple cubic bezier curve. (On an iPhone!)

Swit: for @rakesh's answer

 func tapTargetForPath(_ path: CGPath) -> UIBezierPath? {
    let path = UIBezierPath(cgPath: path)
   guard let tapTargetPath = CGPath(__byStroking: path.cgPath,
           transform: nil,
           lineWidth: CGFloat(fmaxf(35.0, Float(path.lineWidth))),
           lineCap: path.lineCapStyle,
           lineJoin: path.lineJoinStyle,
           miterLimit: path.miterLimit) else {
            return nil
    }

    return UIBezierPath(cgPath: tapTargetPath)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM