SVG pointer

The svg element introduces a powerful context, a scope in which you are able to draw innumerable, colorful drawings.

<svg viewBox="0 0 100 100">
	<!-- ... -->
</svg>

You might be familiar with the possible elements nested between the two tags, like circle. And a quick look in the specification leads you to a visual setting a few attributes — r for the radius, cx and cy for the position of the center.

<circle r="2" cx="1" cy="1" />

As soon as the browser parses the document you are greeted by the basic figure. But it may so happen that you want to draw this figure on demand, following user interaction.

Consider a click of the mouse, or a tap of a finger. You can respond to the occasion with events such as pointerdown.

const element = document.querySelector('svg');

element.addEventListener('pointerdown', handlePointer);

But you need a few lines of code to find the right spot.

Coordinates

In the callback function for the chosen event you gain access to the Event object, and among the properties, to coordinates such as clientX and clientY.

function handlePointer(event) {
	const { clientX, clientY } = event;
}

The issue: these coordinates describe an x, y point on the page. They cannot be used in the svg element and the boundaries set with the viewBox attribute. Or at least, not if you want to draw in the visible area.

You need to convert the values, and for this, there are at least two ways. It is likely there is a winner between the two, and even a third choice topping both, but both work to achieve our goal.

BoundingClientRect

clientX and clientY describe the svg element relative to the page, so you need to consider the position and the dimensions of the node itself. You can do this with the getBoundingClientRect method.

const bCR = element.getBoundingClientRect();

Invoke the function on the element and the result is an object with several properties, among which: x, y, width and height. With this information you can adapt the coordinates retrieved from the event.

For example, and for the horizontal coordinate. First, you want to identify the position relative to the start, to the left side. For this subtract the potential offset the element has in the DOM.

const x = clientX - bCR.x;

With the value divide the number by the width of the node.

const x = (clientX - bCR.x) / bCR.width;

In doing so, you find a number in the 0 to 1 range. This is a rather convenient range when you know one more thing: the dimensions of the canvas set through the viewBox.

This is the last step, and possibly the one making you doubt the solution.

const x = ((clientX - bCR.x) / bCR.width) * 100;

Multiply the decimal by the hard-coded width and you find the correct coordinate, in the context of the svg element.

Repeat the process for the vertical offset, with the height of the element and of the canvas and you have found it, a first way.

You had to adapt the number to the viewBox. In this instance, it was a simple matter of multiplying the value by the width, by the height, but the process can be even more complex. If you were to change the origin of the canvas with the first two arguments, you’d need to account for these offsets as well.

const x = ((clientX - bCR.x) / bCR.width) * bBox.width + bBox.x;

But even if roundabout, the code works.

Point matrix

You need to convert the coordinates of the page to the coordinate system of the svg element, and a couple of detailed documents explain how. The process also includes matrices, but do not worry, you don’t have to do the math yourself.

On one side, the SVG specification tips you to the getScreenCTM method.

const matrix = element.getScreenCTM();

When you call the function on the element you are able to move from the SVG transform matrix to the one relevant to the screen. The return value is an instance of DOMMatrix, but to your keen eyes, it is also not what we want.

We don’t want to move from the viewbox to the screen, but the other way round. From “world” coordinates to the system in use within the graphic. Luckily, you can invert the matrix with the inverse method.

const matrix = element.getScreenCTM().inverse();

You have the matrix, and to avoid complicated math, you have a way to move from one set of coordinates to the other within a CSS module, specifically with the DOMPoint interface. Create a point with the new keyword, using the client values as arguments.

const point = new DOMPoint(clientX, clientY);

And through the point, make use of the matrix with the matrixTransform function.

const svgPoint = point.matrixTransform(matrix);
const { x, y } = svgPoint;

The method prompts an object with the desired coordinates, and admittedly, concludes a rather involved process. There are multiple specifications, methods and classes. But if you manage to wrap your head around the sequence you are finally able to move from screen to viewBox through the transform matrix.

This time, you can update the viewBox and its four numbers any which way. getScreenCTM will do the job for you to find the right numbers.

But this time, you might have also grown tired, and weighed by a more existential question. “Can’t I just use a canvas element to draw dots?”. The Canvas API offers an alternative — and possibly a more performant alternative as well —, but there are features, a few special effects, for which you’ll gladly know how to use vectors.

And even a sparse room, inspired by a colorful title, will convince you of that. The only thing missing is your input, but you can fill in the rest.