|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<style> |
|
path { |
|
fill: none; |
|
stroke: #fc0; |
|
stroke-width: 2px; |
|
} |
|
|
|
.arrow { |
|
stroke-width: 1px; |
|
stroke: #444; |
|
} |
|
|
|
#arrowhead path { |
|
stroke: none; |
|
fill: black; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500"> |
|
<path id="loop" d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9 |
|
c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2 |
|
c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8 |
|
c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29 |
|
c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4 |
|
c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2 |
|
c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/> |
|
<defs> |
|
<marker id="arrowhead" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="6" markerHeight="6" orient="auto"> |
|
<path d="M 0 0 L 10 5 L 0 10 z" /> |
|
</marker> |
|
<path id="test" /> |
|
</defs> |
|
</svg> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
|
|
var path = document.getElementById("loop"), |
|
test = document.getElementById("test"), |
|
length = path.getTotalLength(), |
|
desiredArrowLength = 40, |
|
offset = 15, |
|
numArrows = 20; |
|
|
|
var line = d3.line(); |
|
|
|
d3.select("svg").selectAll(".arrow") |
|
.data(arrows(20)) |
|
.enter() |
|
.append("path") |
|
.attr("class", "arrow") |
|
.attr("marker-end", "url(#arrowhead)") |
|
.attr("d", line); |
|
|
|
function arrows(numArrows) { |
|
|
|
var points = [], |
|
point, |
|
previousLength = outerLength = 0; |
|
|
|
test.setAttribute("d", ""); |
|
|
|
for (var i = 0; i < length; i += 3) { |
|
points.push(point = offsetPoint(i)); |
|
test.setAttribute("d", line(points)); |
|
previousLength = outerLength; |
|
outerLength = test.getTotalLength(); |
|
point.push(outerLength - previousLength); |
|
} |
|
|
|
var interval = outerLength / numArrows, |
|
arrows = [[]], |
|
position = 0; |
|
|
|
points.forEach(function(point){ |
|
|
|
position += point[2]; |
|
|
|
if (position >= interval) { |
|
arrows.push([]); |
|
position = point[2]; |
|
} |
|
|
|
if (position <= desiredArrowLength) { |
|
arrows[arrows.length - 1].push(point); |
|
} |
|
|
|
}); |
|
|
|
return arrows.slice(0, numArrows); |
|
|
|
} |
|
|
|
function offsetPoint(l) { |
|
|
|
var angle = angleAtLength(l) - Math.PI / 2, |
|
point = pointAtLength(l); |
|
|
|
return [ |
|
point[0] + offset * Math.cos(angle), |
|
point[1] + offset * Math.sin(angle) |
|
]; |
|
|
|
} |
|
|
|
function pointAtLength(l) { |
|
|
|
var xy = path.getPointAtLength(l); |
|
return [xy.x, xy.y]; |
|
|
|
} |
|
|
|
// Approximate tangent |
|
function angleAtLength(l) { |
|
|
|
var a = pointAtLength(Math.max(l - 0.01,0)), // this could be slightly negative |
|
b = pointAtLength(l + 0.01); // browsers cap at total length |
|
|
|
return Math.atan2(b[1] - a[1], b[0] - a[0]); |
|
|
|
} |
|
|
|
</script> |