|
<!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="//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(); |
|
|
|
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(points, 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 closestPointOLD(pathNode, point) { |
|
var pathLength = pathNode.getTotalLength(), |
|
precision = 8, |
|
best, |
|
bestLength, |
|
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 /= 2; |
|
while (precision > 0.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 /= 2; |
|
} |
|
} |
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function closestPoint(points, point) { |
|
var pathLength = getLineDistance(points), //pathNode.getTotalLength(), |
|
precision = 8, |
|
best, |
|
bestLength, |
|
bestDistance = Infinity; |
|
|
|
// linear scan for coarse approximation |
|
for (var scan, scanLength = 0, scanDistance; scanLength <= pathLength; scanLength += precision) { |
|
if ((scanDistance = distance2(scan = getPointAtDistance(points,scanLength))) < bestDistance) { |
|
best = scan, bestLength = scanLength, bestDistance = scanDistance; |
|
} |
|
} |
|
|
|
// binary search for precise estimate |
|
precision /= 2; |
|
while (precision > 0.5) { |
|
var before, |
|
after, |
|
beforeLength, |
|
afterLength, |
|
beforeDistance, |
|
afterDistance; |
|
|
|
|
|
if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(before = getPointAtDistance(points,beforeLength))) < bestDistance) { |
|
best = before, bestLength = beforeLength, bestDistance = beforeDistance; |
|
} else if ((afterLength = bestLength + precision) <= pathLength && (afterDistance = distance2(after = getPointAtDistance(points,afterLength))) < bestDistance) { |
|
best = after, bestLength = afterLength, bestDistance = afterDistance; |
|
} else { |
|
precision /= 2; |
|
} |
|
} |
|
|
|
best = [best.x, best.y]; |
|
best.distance = Math.sqrt(bestDistance); |
|
return best; |
|
|
|
|
|
|
|
|
|
function dist(p1,p2){ |
|
var dx = p1[0] - p2[0], |
|
dy = p1[1] - p2[1]; |
|
|
|
return Math.sqrt((dx * dx) + (dy * dy)); |
|
} |
|
|
|
|
|
|
|
function getDistancesAlongLine(line){ |
|
|
|
var tot = 0; |
|
for(var i=0;i < line.length;i ++){ |
|
if(i == 0){ |
|
line[i].dist = 0; |
|
line[i].distto = 0; |
|
}else{ |
|
line[i].dist = dist(line[i-1],line[i]) |
|
line[i].distto = line[i].dist + tot; |
|
} |
|
tot += line[i].dist; |
|
|
|
} |
|
line.bisector = d3.bisector(function(d){ |
|
return d.distto; |
|
}).left; |
|
line.dist = tot; |
|
|
|
} |
|
|
|
function getLineDistance(line){ |
|
if(!line.dist){ |
|
getDistancesAlongLine(line); |
|
} |
|
return line.dist; |
|
} |
|
|
|
function changeX(m,dist){ |
|
return m == Infinity ? dist : dist * (m / Math.sqrt(1 + m*m)) |
|
} |
|
|
|
function changeY(m,dist){ |
|
return m == Infinity ? 0 : dist * (1 / Math.sqrt(1 + m*m)) |
|
} |
|
|
|
function getPointAtDistance(_line,dist){ |
|
if(!_line.bisector){ |
|
getDistancesAlongLine(_line); |
|
} |
|
|
|
var i = _line.bisector(_line,dist) - 1; |
|
if(i < 0){ |
|
i = 0; |
|
} |
|
var _dist = _line[i].distto,//d3.sum(_line.slice(0,i),function(d){return d.dist}), |
|
_remain = dist - _dist, |
|
pt = _line[i]; |
|
|
|
if(_remain == 0 || i == (_line.length-1)){ |
|
return pt; |
|
}else{ |
|
|
|
var nxtpt = _line[i + 1]; |
|
var x = pt[0] - (pt[0] - nxtpt[0]) * (_remain/nxtpt.dist), |
|
y = pt[1] - (pt[1] - nxtpt[1]) * (_remain/nxtpt.dist); |
|
|
|
return {x:x,y:y}; |
|
|
|
} |
|
|
|
} |
|
|
|
function distance2(p) { |
|
var dx = p.x - point[0], |
|
dy = p.y - point[1]; |
|
return dx * dx + dy * dy; |
|
} |
|
} |
|
|
|
</script> |