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".
Newest faster version Here!
xxxxxxxxxx
<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>
body{
font: 18px 'NHaasGroteskDSPro-65Md';
}
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 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 line_path = pathNode;
var length_at_point = 0,
total_length = line_path.getTotalLength();
while ((Math.trunc(line_path.getPointAtLength(length_at_point).x) != Math.trunc(point[0]) || Math.trunc(line_path.getPointAtLength(length_at_point).y) != Math.trunc(point[1])) && length_at_point < total_length) {
length_at_point++;
};
var point = line_path.getPointAtLength(length_at_point),
prev = {},
next = {},
delta = {};
if (length_at_point > 1 && length_at_point < (total_length - 1)) {
prev = line_path.getPointAtLength(length_at_point - 1);
next = line_path.getPointAtLength(length_at_point + 1);
delta = {
x: next.x - prev.x,
y: next.y - prev.y
}
} else {
// don't worry about the first and last pixel or so
return;
};
var LENGTH = 40; //length of tangent line
return {
id:'tangent'
, "stroke-width":2
, stroke:'red'
, x1:(point.x - delta.x * LENGTH)
, y1:(point.y - delta.y * LENGTH)
, x2:(point.x + delta.x * LENGTH)
, y2:(point.y + delta.y * LENGTH)
};
}
function closestPoint(pathNode, point) {
var pathLength = pathNode.getTotalLength();
var aga = pathNode.pathSegList;
var precision = pathLength / pathNode.pathSegList.numberOfItems * .125;
var best;
var bestLength;
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;
} else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = pathNode.getPointAtLength(afterLength))) < bestDistance) {
best = after, bestLength = afterLength, bestDistance = afterDistance;
} else {
precision *= .5;
}
}
best = [best.x, best.y];
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>
https://d3js.org/d3.v3.min.js