Question

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.

Was it helpful?

Solution

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>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top