Drag The circle through the svg area and discover the behaviour.
The "pathSegList" method was removed in chome version 58 so you need to include the javascript library "pathseg.js".
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Closest Point on a Path and Its Tangent</title>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="pathseg.js" charset="utf-8"></script>
<style>
path {
fill: none;
stroke: #000;
stroke-width: 3px;
}
circle {
fill: steelblue;
stroke: #fff;
stroke-width: 3px;
}
</style>
</head>
<body class="handmadepaper">
</body>
<script type="application/javascript">
var tan = null;
var points = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
var svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500);
var path = svg.append("path")
.data([points])
.attr('id','comet')
.attr("d", d3.svg.line()
.tension(0) // Catmull–Rom
.interpolate("cardinal-closed"));
svg.selectAll(".point")
.data(points)
.enter().append("circle")
.attr("r", 4)
.attr("transform", function(d) { return "translate(" + d + ")"; });
var circle = svg.append("circle")
.attr("r", 13)
.attr("cursor", "move")
.attr("transform", "translate(" + points[0] + ")")
.call(
d3.behavior.drag()
.on('dragstart', function(){
tan = svg.append('line');
var c = closestPoint(d3.select('#comet').node(),[d3.event.sourceEvent.x,d3.event.sourceEvent.y]);
var tangattr = getTangent(d3.select('#comet').node(),closestPoint(d3.select('#comet').node(),[d3.event.sourceEvent.x,d3.event.sourceEvent.y]));
for(keys in tangattr){
tan.attr(keys,tangattr[keys]);
}
})
.on('drag', function(){
d3.select(this).attr("transform", "translate(" + closestPoint(d3.select('#comet').node(),[d3.event.x,d3.event.y])[0] + ',' + closestPoint(d3.select('#comet').node(),[d3.event.x,d3.event.y])[1] + ")");
var tangattr = getTangent(d3.select('#comet').node(),closestPoint(d3.select('#comet').node(),[d3.event.x,d3.event.y]));
for(keys in tangattr){
tan.attr(keys,tangattr[keys]);
}
})
.on('dragend', function(){;
svg.selectAll('#tangent').remove();
tan = null;
})
);
function findAngle(p1, p2) {
return Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
}
function getTangent(pathNode, point){
var r = .5;
var prev,next;
try{
prev = pathNode.getPointAtLength(point.pLength - r);
}catch(e){
prev = pathNode.getPointAtLength(pathNode.getTotalLength()-r);
}
try{
next = pathNode.getPointAtLength(point.pLength + r);
}catch(e){
next = pathNode.getPointAtLength(0+r);
}
var delta = {
x: next.x - prev.x,
y: next.y - prev.y
};
var LENGTH = 40; //length of tangent line
return {
id:'tangent'
, "stroke-width":2
, stroke:'red'
, x1:(point[0] - delta.x * LENGTH / r)
, y1:(point[1] - delta.y * LENGTH / r)
, x2:(point[0] + delta.x * LENGTH / r)
, y2:(point[1] + delta.y * LENGTH / r)
};
}
function closestPoint(pathNode, point) {
var pathLength = pathNode.getTotalLength();
var aga = pathNode.pathSegList;
var precision = pathLength / pathNode.pathSegList.numberOfItems * .125;
var best;
var bestLength;
var pathPLenght;
var bestDistance = Infinity;
// linear scan for coarse approximation
for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) {
if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLength))) < bestDistance) {
best = scan, bestLength = scanLength, bestDistance = scanDistance;
}
}
// binary search for precise estimate
precision *= .5;
while (precision > .5) {
var before,
after,
beforeLength,
afterLength,
beforeDistance,
afterDistance;
if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLength))) < bestDistance) {
best = before, bestLength = beforeLength, bestDistance = beforeDistance; pathPLenght = beforeLength;
} else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) {
best = after, bestLength = afterLength, bestDistance = afterDistance; pathPLenght = afterLength;
} else {
precision *= .5;
}
}
best = [best.x, best.y];
best.pLength = pathPLenght;
best.distance = Math.sqrt(bestDistance);
return best;
function distance2(p) {
var dx = p.x - point[0],
dy = p.y - point[1];
return dx * dx + dy * dy;
}
}
</script>
</html></title>
</head>
<body>
</body>
</html>
https://d3js.org/d3.v3.min.js