|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
path { |
|
fill: none; |
|
stroke: #000; |
|
stroke-width: 1.5px; |
|
} |
|
|
|
line { |
|
fill: none; |
|
stroke: red; |
|
stroke-width: 1.5px; |
|
} |
|
|
|
circle { |
|
fill: red; |
|
} |
|
|
|
rect { |
|
fill: none; |
|
cursor: crosshair; |
|
pointer-events: all; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
|
|
var points = [[474,276],[586,393],[378,388],[338,323],[341,138],[547,252],[589,148], |
|
[346,227],[365,108],[562,62]]; |
|
|
|
var width = 960, |
|
height = 500; |
|
|
|
var line = d3.svg.line() |
|
.interpolate("cardinal"); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
var path = svg.append("path") |
|
.datum(points) |
|
.attr("d", line); |
|
|
|
var line = svg.append("line"); |
|
|
|
var circle = svg.append("circle") |
|
.attr("cx", -10) |
|
.attr("cy", -10) |
|
.attr("r", 3.5); |
|
|
|
svg.append("rect") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.on("mousemove", mousemoved); |
|
|
|
function mousemoved() { |
|
var m = d3.mouse(this), |
|
p = closestPoint(path.node(), m); |
|
line.attr("x1", p[0]).attr("y1", p[1]).attr("x2", m[0]).attr("y2", m[1]); |
|
circle.attr("cx", p[0]).attr("cy", p[1]); |
|
} |
|
|
|
function closestPoint(pathNode, point) { |
|
var pathLen = pathNode.getTotalLength(), |
|
precision = pathLen / pathNode.pathSegList.numberOfItems * .125, |
|
best, |
|
bestLen, |
|
bestDistance = Infinity; |
|
|
|
// linear scan for coarse approximation |
|
for (var scan, scanLen = 0, scanDistance; scanLen <= pathLen; scanLen += precision) { |
|
if ((scanDistance = distance2(scan = pathNode.getPointAtLength(scanLen))) < bestDistance) { |
|
best = scan, bestLen = scanLen, bestDistance = scanDistance; |
|
} |
|
} |
|
|
|
// binary search for precise estimate |
|
precision *= .5; |
|
while (precision > .5) { |
|
var before, |
|
after, |
|
beforeLen, |
|
afterLen, |
|
beforeDistance, |
|
afterDistance; |
|
if ((beforeLen = bestLen - precision) >= 0 |
|
&& (beforeDistance = distance2(before = pathNode.getPointAtLength(beforeLen))) |
|
< bestDistance) { |
|
best = before, bestLen = beforeLen, bestDistance = beforeDistance; |
|
} else if ((afterLen = bestLen + precision) <= pathLen |
|
&& (afterDistance = distance2(after = pathNode.getPointAtLength(afterLen))) |
|
< bestDistance) { |
|
best = after, bestLen = afterLen, 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> |