Learn SVG by drawing an arrow
Tags: javascript, maths
Published: 2024-01-11 17:45:41

## How SVG works

The common way of representing digital images is via **pixels**. Take an image and break it down into small squares \("pixels"\), where each square has a single colour. If you have many tiny squares, the colours blend in, and the image looks smooth to the human eye. If you don't have enough squares, the image looks rough and jagged, and you can see the edges of the individual squares. This is the difference between high\-resolution screens/images \(more pixels\) and low\-resolution ones \(fewer pixels\).

But there's also [vector graphics](https://en.wikipedia.org/wiki/Vector_graphics) \(SVG = Scalable Vector Graphics\). They don't use pixels, but instead work by drawing the image on the display, using geometry \(maths\). For example, an arrow in PNG would consist of many, many squares lined up together in the shape of an arrow, while in SVG, it could consist of a line starting from some coordinates \(x1, y1\), then turning and turning until the shape of the arrow head is complete.

Both types of graphics have their use cases. Adobe has a good introductory explainer on [raster graphics vs vector graphics](https://www.adobe.com/creativecloud/file-types/image/comparison/raster-vs-vector.html).

\(PS: all of the illustrations in this article are SVGs. If you try zooming into them, you'll see that they don't get pixelated.\)

This article will demonstrate SVGs by constructing an arrow in HTML/JavaScript.

## Drawing with geometry

**SVG shapes are defined based on geometry**. When you define an `<svg>` element, you get an invisible Cartesian plane; there are x and y axes, and all points you will draw in this SVG are specified relative to an origin. Note that by default, x increases to the right as normal, but y increases downwards, and the origin is at the top left. I've made a grid to illustrate this:

xy5010015020025030050100150200250300

For instance, to draw a circle, we specify the coordinates of its centre \(`cx` and `cy`\), and the value of its radius; for a line, we specify its start \(`x1`, `y1`\) and end \(`x2`, `y2`\) coordinates; for a rectangle, start coordinates \(`x`, `y`\), width and height:

```html
<svg width="300" height="300" viewBox="0 0 300 300">
  <circle cx="150" cy="100" r="50" 
    style="fill: none; stroke: red; stroke-width: 2px;">
  </circle>
  <line x1="50" y1="50" x2="200" y2="200" 
    style="fill: none; stroke: black; stroke-width: 2px;">
  </line>
  <rect x="100" y="150" width="50" height="75"
    style="fill: none; stroke: green; stroke-width: 2px;">
  </rect>
</svg>
```

xy5010015020025030050100150200250300

\(Note that the grid lines and axes are not automatically added; these are separate SVG lines I added to make things clearer.\)

You may have noticed the `style` attributes; SVGs can be styled like regular HTML elements, with inline CSS, classes, and selectors.

We've used `<circle>`, `<rect>` and `<line>`, but at the basic level, you can construct any shape by describing its path \(the `<path>` element\). Here's one way to draw the same shapes as above:

```html
<svg width="300" height="300" viewBox="0 0 300 300">
  <!-- line -->
  <path d="M 50,50 L 200,200"
    style="fill: none; stroke: black; stroke-width: 2px;">
  </path>
  
  <!-- rectangle -->
  <path d="M 100,150 l 50,0 l 0,75 l -50,0 Z"
    style="fill: none; stroke: green; stroke-width: 2px;">
  </path>
  
  <!-- circle -->
  <path d="M 100,100 A 50 50 0 0 1 200,100 A 50 50 0 0 1 100,100"
    style="fill: none; stroke: red; stroke-width: 2px;">
  </path>
</svg>
```

xy5010015020025030050100150200250300

The `d` attribute has specific syntax, which I like to think is for instructing the "artist" on how to move their pen.

* For the line: `M 50,50 L 200,200` says "***M**ove your pen to x1=**50**,y1=**50**, then draw a straight **L**ine until you get to x2=**200**,y2=**200***".
* For the rectangle, `M 100,150 l 50,0 l 0,75 l -50,0 Z` draws a line from corner to corner: "***M**ove your pen to x1=**100**,y1=**150**, then draw a straight **l**ine until you get to x2=x1 \+ **50**,y2=y1 \+ **0**, then another **l**ine until x3=x2 \+ **0**,y3=y2 \+ **75**, then another **l**ine until x4=x3 **\- 50**,y4=y3 \+ **0**, then connect back to the start point \(**Z**\)*".
* For the circle, `M 100,100 A 50 50 0 0 1 200,100 A 50 50 0 0 1 100,100` moves to 100,100, and then draws one arc \(**A**\) with a radius of 50 until it gets to 200,100. Then from that point, it draws another arc with the same radius to connect to the start point. \(I won't explain the Arc syntax in detail because it's fairly complex.\)

I like [this article](https://kurtbruns.github.io/svg-tutorial/) for a quick explainer on the syntax, and of course [MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands) for a full reference.

`<path>` is useful when you need to draw an irregular shape. It can get super complex, though; for regular images, you'll probably use some graphic design software that generates it for you. You'll likely only need to work with paths directly when animating something or making some custom graphic element, although [you may want to use the Canvas API instead](https://css-tricks.com/when-to-use-svg-vs-when-to-use-canvas/).

## Manipulating SVGs with JavaScript

There's no special SVG JavaScript API; we must use the DOM APIs, similar to other HTML elements. An important difference is that you need to use `document.createElementNS()` instead of `document.createElement()`, otherwise the elements won't be rendered.

For instance, here's a small class for this:

```js
class Svg {
  static NAMESPACE_URI = 'http://www.w3.org/2000/svg';

  constructor(domElementId, width, height) {
    this.$svg = document.createElementNS(Svg.NAMESPACE_URI, 'svg');
    this.$svg.setAttribute('xmlns', Svg.NAMESPACE_URI);
    this.$svg.setAttribute('width', width);
    this.$svg.setAttribute('height', height);
    this.$svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
    document.querySelector(`#${domElementId}`).appendChild(this.$svg);
  }

  add(elementType, attributes = {}, styles = {}) {
    let $element = document.createElementNS(Svg.NAMESPACE_URI, elementType);
    Object.entries(attributes).forEach(([k, v]) => {
      $element.setAttribute(k, v);
    });
    Object.entries(styles).forEach(([k, v]) => {
      $element.style[k] = v;
    });
    this.$svg.appendChild($element);
    return $element;
  }
}
```

```html
<div id="container"></div>

<script>
let root = new Svg(`container`, 300, 300);
root.add(`path`,
  {d: `M 50,50 L 200,200`},
  {strokeWidth: `2px`, stroke: `green`}
);
root.add(`circle`,
  {cx: `150`, cy: `100`, r: `50`},
  {strokeWidth: `2px`, stroke: `red`, fill: 'none'}
)
</script>
```

For a full\-featured API, you can use a library such as [Snap.svg](http://snapsvg.io/) or [SVG.js](https://svgjs.dev/).

## Constructing an arrowhead

To add an arrowhead to this line, let's go with the `<path>` approach. Think about the shape of an arrowhead for a moment. How do you move your pen when drawing this?

> Tip: There's an easier way to draw an arrowhead in SVG \(skip straight to the ["marker"](#use-marker) section\). But if you want to have fun with the maths or go deeper, enjoy\!

To draw an arrowhead as a path, we need four things: the coordinates of the starting point P, and those of the corner points Q, R, and S. Then our SVG path will be \(assuming the pen is already at P\): `L Qx,Qy L Rx,Ry L Sx,Sy L Px,Py`. However, we only know the coordinates of the starting point P.

![](https://gist.github.com/shalvah/293dbc0da774dad451063af57f20e4d8/raw/98d1d891b7d9b1912e7be87c0e68f21d1c766441/arrowhead-points.svg)We also know the vertical height of the arrowhead, h, and its base, b, because an arrowhead is an isosceles triangle, and we can pick whatever height and base we want to make it look good.

So let's do a little trigonometry to find the unknowns. For each unknown point, we will calculate the "delta" \(Δ\) of its coordinates \(the distance from P in x and y\).

To find R, we can use the angle between line PR and the horizontal.

![](https://cdn.swish.ink/caf2243d-94c4-49d1-9204-11e4c63a6b5f/media/arrowhead-trig-1.svg)

This is a right\-angled triangle, so this gives us \\Delta R\_x = h \\cos \\theta and \\Delta R\_y = h \\sin \\theta. But the second diagram shows that the angle \\theta is also present in the smaller P\-triangle \(vertically opposite angles are equal\), so \\tan \\theta = \\frac{\\Delta P\_y}{\\Delta P\_x}.

So we have

\\theta = \\tan^{\-1} \\frac{\\Delta P\_y}{\\Delta P\_x}

\\Delta R\_x = h \\cos \\theta

\\Delta R\_y = h \\sin \\theta

This gives R's coordinates in terms of P's \(R\_x = P\_x \+ \\Delta R\_x and R\_y = P\_y \+ \\Delta R\_y\).

To find Q, we take the angle \\alpha between the arrowhead's base and the horizontal, which gives us \\Delta Q\_x = b \\cos \\alpha and \\Delta Q\_y = b \\sin \\alpha

![](https://cdn.swish.ink/caf2243d-94c4-49d1-9204-11e4c63a6b5f/media/arrowhead-trig-2.svg)

Now, this angle adds up with \\theta to make 90° \(or π/2 radians\), so we have that:

\\Delta Q\_x = b \\cos \(\\frac{\\pi}{2} \- \\theta\)

\\Delta Q\_y = b \\sin \(\\frac{\\pi}{2} \- \\theta\)

And Q is:

Q\_x = P\_x \- \\Delta Q\_x

Q\_y = P\_y \+ \\Delta Q\_y

We subtract the x\-delta, since the line \\overrightarrow{\\rm PQ} is going to the left \(x is decreasing\).

S is the reflection of Q, so it has the same deltas as Q, but in the opposite direction. For S, the y\-delta is negative, since \\overrightarrow{\\rm PS} is going downwards \(decreasing y\).

S\_x = P\_x \+ \\Delta Q\_x

S\_y = P\_y \- \\Delta Q\_y

And here's all this as a JavaScript function \(with h = 10, b = 5\):

```js
function makeArrow(destination, start = { x: 0, y: 0 }) {
  let height = 10;
  let base = 5;

  let P = destination;

  // I'd normally write this out as "theta", but JavaScript supports Unicode identifiers, nice 😎
  let θ = Math.atan((destination.y - start.y)/(destination.x - start.x));
  let α = Math.PI / 2 - θ;

  let ΔQ = { x: base * Math.cos(α), y: base * Math.sin(α) }
  let Q = { x: P.x - ΔQ.x, y: P.y + ΔQ.y }
  let S = { x: P.x + ΔQ.x, y: P.y - ΔQ.y }

  let ΔR = { x: height * Math.cos(θ), y: height * Math.sin(θ) }
  let R = { x: P.x + ΔR.x, y: P.y + ΔR.y }

  root.add(`path`,
    { d: `M ${start.x},${start.y} L ${P.x},${P.y} L ${Q.x},${Q.y} L ${R.x},${R.y} L ${S.x},${S.y} L ${P.x},${P.y}` },
    { strokeWidth: `2px`, stroke: `black` }
  );
}

makeArrow({x: 100, y: 200});
```

Here's an interactive playground. You can edit the values to see how this arrow behaves. Can you spot its limitations?

xy5010015020025030050100150200250300

Arrow end \(x, y\):

Arrow start \(x, y\):

Two things you may have noticed:

1. The arrowhead doesn't handle rotation correctly. For instance, `makeArrow({ x: 30, y: 0 }, {x: 100, y: 200})` produces an inverted arrowhead.
2. A minor annoyance is that the arrowhead actually points beyond the destination. For instance an arrow to \(100, 200\) will have its arrowhead a few units past \(100,200\).

We can adapt the calculations to account for these scenarios, but that's enough maths for today. Instead, we'll switch to using the `<marker>` element, which can automatically handle these issues for us.

## Using `<marker>`

The good news is that SVG already has an element for this. [The `<marker>` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker) is meant for placing "markers" on a shape. For instance:

* arrowheads \(duh\) or some other shape you want to put at the end of a line
* path breakpoints
* resize handles on an element
* drag\-and\-drop handles

The marker approach builds on the "manual" maths we've already done, but fixes these two limitations:

1. By setting `orient="auto-start-reverse"` on the marker, the arrowhead will be rotated appropriately to match the line's placement.
2. By setting `marker-end={url-to-the-marker}` on the line, the arrowhead will end exactly at our destination.

The marker approach comprises a few things: first, a `<marker>` element which has an `id`. We set `orient="auto-start-reverse"`, and set the `markerHeight` and `markerWidth` to our triangle height \(10\) and 2 \* base \(5\), respectively.

```html
<marker
  markerWidth="10" markerHeight="10" orient="auto-start-reverse" 
  id="arrowhead" style="stroke-width: 2px; stroke: black;">
    
</marker>
```

Within the `<marker>` element, we add a nested `<path>` describing the arrowhead \(only the triangle, not the rest of the arrow\). We don't have to use `<path>`, by the way; we could also use three `<line>`s.

```html
<marker ...>
   <path d="...something..."></path>
</marker>
```

How do we determine `d`? Well, we can use the same values we calculated earlier, but we don't need to. One benefit of `<marker>` is that its location on the grid doesn't matter. It can even be in another file; it will be rotated and rendered properly for any element it's attached to. This means our path does not need to depend on the *actual* coordinates of the line \(P, Q, R, S\). Instead, we can pretend P is at \(0, 0\) and we're facing upwards. This allows us to directly use the base and height as our deltas, giving us:

Q\_x = P\_x \- b = \-b

Q\_y = P\_y \+ 0 = 0

R\_x = P\_x \+ 0 = 0

R\_y = P\_y \+ h = h

S\_x = P\_x \+ b = b

S\_y = P\_y \- 0 = 0

And so we have the arrowhead path, which will remain the same regardless of the line:

```html
<marker ...>
   <path d="M 0,0 L -5,0 L 0,10 L 5,0 Z"></path>
</marker>
```

Finally, a `<line>` for the rest of the arrow as usual, setting its `marker-end` to the `id` of the marker:

```html
<line 
  x1="0" y1="0" x2="100" y2="200"
  marker-end="url(#arrowhead)" 
  style="stroke-width: 2px; stroke: black;">
</line>
```

Markers are reusable elements; we can draw many different lines which have the same marker, and it will be rendered and appropriately rotated for each. You can also set `marker-start` instead \(or both\), if you wish to have the arrow on the other end.

Porting this to JavaScript:

```js

let height = 10;
let base = 5;

let $marker = root.add(`marker`,
  {
    markerWidth: base * 2,
    markerHeight: height,
    orient: `auto-start-reverse`,
    id: 'arrowhead'
  },
  {strokeWidth: `2px`, stroke: `black`}
);

let $path = document.createElementNS(Svg.NAMESPACE_URI, 'path');
$path.setAttribute(`d`, `M 0,0 L -${base},0 L 0,${height} L ${base},0 Z`)
$marker.appendChild($path);

function makeArrowWithMarker(destination, start = {x: 0, y: 0}) {
  root.add(`line`,
    {
      x1: start.x,
      y1: start.y,
      x2: destination.x,
      y2: destination.y,
      'marker-end': `url(#${$marker.id})`,
    },
    { strokeWidth: `2px`, stroke: `black` }
  );
}

makeArrowWithMarker({x: 100, y: 200});
```

And finally, here it is in action:

xy5010015020025030050100150200250300

Arrow end \(x, y\):

Arrow start \(x, y\):

## Further reading

* Peter Collingridge has a good collection of [interactive SVG tutorials](https://www.petercollingridge.co.uk/tutorials/svg/)