5

I need to draw nice stroked block arrow using SVG from one point (x0,y0) to another (x1,y1), like the one on the picture.

arrow

The only way I can imagine is to use a line (two lines basically to simulate stroke and fill) with marker, but it looks kind of ugly due to overlaping strokes.

Ideally both line and marker should be filled with the same color and should have the same stroke color, and overall arrow width can be fixed (but if I could parametrize that as well it would be cool). Basically it should look the same as on picture provided and should be able to be drawn by just providing coordinates of two points. Is it even possible?

1
  • I guess you could create the whole thing as a symbol that contains a path which is the entire arrow. You could use the <use> tag and a transform to place that symbol wherever you wanted. Commented Jan 19, 2017 at 9:32

3 Answers 3

9

I was bored, so here you go. I have written a function to generate a path of the right shape.

You just need to give it the "from" and "to" coords, the line width, arrowhead width, and arrowhead length.

Enjoy!

var from = {x: 50, y: 250};
var to = {x: 250, y: 100};

var lineWidth = 30;
var arrowheadWidth = 60;
var arrowheadLength = 50;

var svg = document.getElementById("test");

drawArrow(svg, from, to, lineWidth, arrowheadWidth, arrowheadLength);


function drawArrow(svg, from, to, lineWidth, arrowheadWidth, arrowheadLength)
{
  var dx = to.x - from.x;
  var dy = to.y - from.y;
  // Calculate the length of the line
  var len = Math.sqrt(dx * dx + dy * dy);
  if (len < arrowheadLength) return;

  // The difference between the line width and the arrow width
  var dW = arrowheadWidth - lineWidth;
  // The angle of the line
  var angle = Math.atan2(dy, dx) * 180 / Math.PI;
  // Generate a path describing the arrow. For simplicity we define it as a
  // horizontal line of the right length, and starting at 0,0. Then we rotate
  // and move it into place with a transform attribute.
  var d = ['M', 0, -lineWidth/2,
           'h', len - arrowheadLength,
           'v', -dW / 2,
           'L', len, 0,
           'L', len - arrowheadLength, arrowheadWidth / 2,
           'v', -dW / 2,
           'H', 0,
           'Z' ];
  var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  path.setAttribute("d", d.join(' '));
  path.setAttribute("transform", "translate("+from.x+","+from.y+") rotate("+angle+")");
  path.setAttribute("class", "arrow-line");
  svg.appendChild(path);
}
.arrow-line {
  fill: gold;
  stroke: black;
  stroke-width: 6;
}
<svg id="test" width="300" height="300">
</svg>

1
  • we had the same idea, but you where a bit faster ;-) Commented Jan 19, 2017 at 14:33
3

the easiest way to do this is to just use script to create an arrow. Here i simply determine the length and the angle of the arrow from the two points p1 and p2, and then create a simple path in the correct length and rotate it by the calculated angle:

svgns="http://www.w3.org/2000/svg"
function arrow(p1,p2){
  var h1=15 // line thickness
  var h2=35 // arrow height
  var w2=22 // arrow width
  var deg = Math.atan2(p1.y - p2.y, p1.x - p2.x) * (180 / Math.PI);
  var len = Math.sqrt(Math.pow(p1.y - p2.y,2)+Math.pow(p1.x - p2.x,2))
  var arr = document.createElementNS(svgns,"path")
  var d = `M${p1.x} ${p1.y-h1/2}v${h1}h${h2/2-len}v${(h2-h1)/2}l${-w2} ${-h2/2}l${w2} ${-h2/2}v${(h2-h1)/2}z`
  arr.setAttribute("d",d)
  arr.setAttribute("transform",`rotate(${deg} ${p1.x} ${p1.y})`)
  arr.classList.add("arrow")
  return arr
}

var a1 = arrow({x:50,y:50},{x:200,y:200})
var a2 = arrow({x:450,y:50},{x:300,y:200})
var a3 = arrow({x:450,y:450},{x:300,y:300})
var a4 = arrow({x:50,y:450},{x:200,y:300})
svg.appendChild(a1)
svg.appendChild(a2)
svg.appendChild(a3)
svg.appendChild(a4)
.arrow{stroke-width:3px}
.arrow:nth-of-type(1){fill:green;stroke:lime}
.arrow:nth-of-type(2){fill:red;stroke:orange}
.arrow:nth-of-type(3){fill:blue;stroke:turquoise}
.arrow:nth-of-type(4){fill:violet;stroke:pink}
<svg id="svg" viewBox="0 0 500 500" width="400" height="400">

</svg>

if you try to be fancy and find a scriptless solution, there will be a lot of loop you have to hop... you will need at least 4 arrows each pointing from top left to bottom right, from top right to bottom left, from bottom left to top right and from bottom right to top left...

here is a prove of concept that it is doable, but i strongly advice against it...

svg{overflow:visible;}
<svg width="200" height="200" style="overflow:visible" stroke="red" color="orange" opacity="0.5">
  <marker id="ah" viewBox="0 0 10 10" orient="auto" refX="10" refY="5" overflow="visible">
    <path d="M0 0L10 5L0 10z"  stroke-width="1"/>
  </marker>
  <marker id="ah2" viewBox="0 0 10 10" orient="auto" refX="10" refY="5">
    <path d="M0 0L10 5L0 10z" fill="currentColor" stroke="none"/>
  </marker>
  <marker id="block" viewBox="0 0 10 10" orient="auto" refX="9" refY="5">
    <rect x="0" y="0" width="10" height="10" stroke="white" stroke-width="1"/>
  </marker>
  <marker id="block2" viewBox="0 0 10 10" orient="auto" refX="9" refY="5">
    <rect x="0" y="0" width="10" height="10" stroke-width="5"/>
  </marker>
  <mask id="m1">
    <rect x="-10%" y="-10%" width="110%" height="110%" fill="white"/>
    <line x1="99.999%" y1="99.999%" x2="100%" y2="100%" stroke-width="20" marker-end="url(#block)"/>
  </mask>
  <line x1="0.001%" y1="0.001%" x2="0%" y2="0%" stroke-width="8" marker-end="url(#block2)"/>
  <line x1="0" y1="0" x2="100%" y2="100%" stroke-width="25"  mask="url(#m1)"/>
  <line x1="99.999%" y1="99.999%" x2="100%" y2="100%" stroke-width="20" marker-end="url(#ah)"/>
  <line x1="0" y1="0" x2="100%" y2="100%" stroke-width="20" stroke="currentColor" mask="url(#m1)"/>
  <line x1="99.999%" y1="99.999%" x2="100%" y2="100%" stroke-width="20" marker-end="url(#ah2)"/>
</svg>

2

After sitting a few hours of triple checking all my math:
Created an normalized arrow in the SVG defs tag
Then scaling the arrow after the provided coordinates. (Added a static height XD)

document.addEventListener("DOMContentLoaded", function(event) {
  var svgDoc = document.getElementById("arrowSvg");
  var useArrow = svgDoc.getElementById("customArrow");
  var extraData = useArrow.getAttribute("extra:data");
  extraData = extraData.split(" ");
  var x1 = parseInt(extraData[0]);
  var x2 = parseInt(extraData[1]);
  var y1 = parseInt(extraData[2]);
  var y2 = parseInt(extraData[3]);
  var arrowHeight = 15;
  //Calculate the rotation needed
  var deltaY = y1 - y2;
  var deltaX = x2 - x1;
  var angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
  //Distance between the two points.
  var distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
  useArrow.setAttribute("transform", 
                        "translate("+(x1+(deltaX/2))+" "+(y1-(deltaY/2))+") "+
                        "rotate(" + -1*angle +") " +
                        "matrix("+distance+", 0, 0, "+arrowHeight+", "+(0.5-distance*0.5)+","+(0.5-arrowHeight* 0.5)+")");

});
svg {
  width: 50%;
  border: 1px solid black;
}
.arrow {
  stroke: black;
  stroke-width: 0.05;
  fill: yellow;
}
<svg id="arrowSvg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:extra="ExtraNameSpace">>
  <defs>
    <path id="idArrow" class="arrow" d="M0,0.25 0.60,0.25 
                         0.60,0 1,0.5 0.60,1
                         0.60,0.75 0,0.75z" />
  </defs>
  <!--- Extra Data Param: x1 x2 y1 y2--->
  <use id="customArrow" xlink:href="#idArrow" extra:data="10 90 90 5" />

</svg>

0

Not the answer you're looking for? Browse other questions tagged or ask your own question.