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.
Here’s how to define a “fat” bezier curve for use as a hit test area
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:
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>