0

I have implemented a class that uses the quadline that is shown in the Modify Curves With Anchor Points tutorial.

this.shape = new Kinetic.Shape({
    drawFunc: function(canvas) {
      var context = canvas.getContext();
      context.beginPath();
      context.moveTo(self.anchors[0].getX(), self.anchors[0].getY());
      for(var i = 1; i < self.anchors.length; i+=2){
        context.quadraticCurveTo(self.anchors[i].getX(), self.anchors[i].getY(), self.anchors[i+1].getX(), self.anchors[i+1].getY()); 
      }
      context.strokeStyle = 'red';
      context.lineWidth = 4;
      context.stroke();
    },
    drawHitFunc: function(canvas) {
      /** Some Hit Test Code **/
    }
  });
this.shape.on('dblclick', click);

I originally thought that this would be trivial, as I could just hit test a fat line, but apparently this does not work.

How would I make a shape that would follow this line for hit testing purposes?

UPDATE

I think that I am getting close using the following drawhitFunc

drawHitFunc: function(canvas) {
      var context = canvas.getContext();
      context.beginPath();
      context.moveTo(self.anchors[0].getX(), self.anchors[0].getY()-10);
      for(var i = 1; i < self.anchors.length; i+=2){
        context.quadraticCurveTo(self.anchors[i].getX(), self.anchors[i].getY()-10, self.anchors[i+1].getX(), self.anchors[i+1].getY()-10);
      }

      context.lineTo(self.anchors[self.anchors.length-1].getX(), self.anchors[self.anchors.length-1].getY() + 10);
      for(var i = self.anchors.length - 2; i >= 0; i-=2){
        context.quadraticCurveTo(self.anchors[i].getX(), self.anchors[i].getY()+10, self.anchors[i-1].getX(), self.anchors[i-1].getY()+10);
      }
      canvas.fillStroke(this);
    }

The problem with the above code is that as the curve has a greater slope the hit area gets smaller because of how the offset is calculated. I think I need to do some calculations to get an offset based on the line perpendicular to the anchor and its next control point.

Community
  • 1
  • 1
stats
  • 455
  • 4
  • 18

1 Answers1

1

Here’s how to define a “fat” bezier curve for use as a hit test area

enter image description here

This illustration shows the original bezier curve in red.

The black filled area surrounding the curve is its “fat” hit test area.

The fat area is actually a closed polyline-path.

Here’s how to build the fat curve:

  • Travel along the curve from start to end in about 15-25 steps
  • At each step, calc a perpendicular line off that point on the curve
  • Extend the perpendicular line out from the curve by a distance (t)
  • Save the x/y endpoint of each extended perpendicular line
  • (These saved points will define the “fattened” polyline-path)

Notes:

If you move any anchor, you need to recalculate the fat path.

If you want your curve to be quadratic instead of cubic, just make the 2 control points identical.

For KineticJS hit-testing: use the polyline points to define the hit region using drawHitFunc.

Making 25 steps on the curve will usually do a good job on even "kinked" curves. If you know you will have relatively smooth curves, you could take fewer steps. Fewer steps results in less precision in following the exact path of the curve.

Here’s code and a Fiddle: http://jsfiddle.net/m1erickson/bKTew/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; padding:20px; }
    #canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    // endpoints,controlpoints
    var s={x:50,y:150};
    var c1={x:100,y:50};
    var c2={x:200,y:200};
    var e={x:250,y:50};
    var t=12;

    // polypoints is a polyline path defining the "fat" bezier
    var polypoints=[];
    var back=[];
    var p0=s;

    // manually calc the first startpoint
    var p=getCubicBezierXYatPercent(s,c1,c2,e,.02);
    var dx=p.x-s.x;
    var dy=p.y-s.y;
    var radians=Math.atan2(dy,dx)+Math.PI/2;
    polypoints.push(extendedPoint(s,radians,-t));

    // travel along the bezier curve gathering "fatter" points off the curve
    for(var i=.005;i<=1.01;i+=.04){

        // calc another further point
        var p1=getCubicBezierXYatPercent(s,c1,c2,e,i);

        // calc radian angle between p0 and new p1
        var dx=p1.x-p0.x;
        var dy=p1.y-p0.y;
        var radians=Math.atan2(dy,dx)+Math.PI/2;

        // calc a "fatter" version of p1 -- fatter by tolerance (t)
        // find a perpendicular line off p1 in both directions
        // then find both x/y's on that perp line at tolerance (t) off p1
        polypoints.push(extendedPoint(p1,radians,-t));
        back.push(extendedPoint(p1,radians,t));
        p0=p1;

    }


    // return data was collected in reverse order so reverse the return data
    back=back.reverse();

    // add the return data to the forward data to complete the path
    polypoints.push.apply(polypoints, back)

    // draw the "fat" bezier made by a polyline path
    ctx.beginPath();
    ctx.moveTo(polypoints[0].x,polypoints[0].y);
    for(var i=1;i<polypoints.length;i++){
        ctx.lineTo(polypoints[i].x,polypoints[i].y);
    }
    // be sure to close the path!
    ctx.closePath();
    ctx.fill();


    // just for illustration, draw original bezier
    ctx.beginPath();
    ctx.moveTo(s.x,s.y);
    ctx.bezierCurveTo(c1.x,c1.y,c2.x,c2.y,e.x,e.y);
    ctx.lineWidth=3;
    ctx.strokeStyle="red";
    ctx.stroke();


    // calc x/y at distance==radius from centerpoint==center at angle==radians
    function extendedPoint(center,radians,radius){
        var x = center.x + Math.cos(radians) * radius;
        var y = center.y + Math.sin(radians) * radius;
        return({x:x,y:y});
    }


    // cubic bezier XY from 0.00-1.00 
    // BTW, not really a percent ;)
    function getCubicBezierXYatPercent(startPt,controlPt1,controlPt2,endPt,percent){
        var x=CubicN(percent,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
        var y=CubicN(percent,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
        return({x:x,y:y});
    }

    // cubic helper formula at 0.00-1.00 distance
    function CubicN(pct, a,b,c,d) {
        var t2 = pct * pct;
        var t3 = t2 * pct;
        return a + (-a * 3 + pct * (3 * a - a * pct)) * pct
        + (3 * b + pct * (-6 * b + b * 3 * pct)) * pct
        + (c * 3 - c * 3 * pct) * t2
        + d * t3;
    }

}); // end $(function(){});

</script>

</head>

<body>
     <canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
markE
  • 102,905
  • 11
  • 164
  • 176
  • I think I am going to need the harder way, as the hit area using the space under the curve will be way too large. – stats Jul 14 '13 at 01:46
  • I have updated the original question to show how I am getting close to a solution for this. – stats Jul 14 '13 at 02:12
  • I am trying to work out how to do the fattened curve better. The context.isPointInPath would be great, but IE is definitely something I have to support. The cross browser pain will never end. – stats Jul 15 '13 at 16:39
  • I re-wrote my answer to show how to create a "fat" path that can be used to hit-test a bezier curve. – markE Jul 16 '13 at 16:41
  • That worked pretty great! I made use of the math, and modified and then did some further modifications to come up with a differing version that makes use of the quadraticCurveTo function instead of doing the slices. http://jsfiddle.net/M2rmw/1/ Thanks a lot for all your support here! – stats Jul 18 '13 at 02:24
  • there is a `context.isPointInStroke` api method in newer browsers. – cuixiping Jan 20 '16 at 13:40
  • @cuixiping. Yep, now there is `.isPointInStroke` (except IE/Edge). :-) – markE Jan 20 '16 at 22:20