Let’s take a look at how we can convert screen coordinates to SVG coordinates. The concept is similar to CSS, where we can use the translate method to move an element on the screen relative to its parent. Are we able to apply the same concept to SVG using JavaScript? Do we have a translation method in SVG? How do we use it? Let’s try it out and find out!
There are many vector coordinate systems, and the most commonly used are the screen coordinate system and the SVG coordinate system. SVG coordinates are more complex than screen coordinates, but they can also be converted to screen coordinates, which is what we will discuss today.
In this article, we will write a script that converts screen coordinates to SVG coordinates. For an overview, find the demo below and edit the code to see the immediate output.
Screen Coordinates to SVG Coordinates demo below
See the Pen Screen coordinates to SVG coordinates by Sten Hougaard (@netsi1964) on CodePen.
HTML Code
<div class="flex">
<div class="half-width">
<svg
width="100%"
height="300"
viewBox="0 0 1055 581"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path stroke="black" fill="none" d="M 117 52 L 483 161 697 168"></path>
<circle cx="117" cy="52" r="15"></circle>
<text x="117" y="22">(117,52)</text>
<circle cx="483" cy="161" r="15"></circle>
<text x="483" y="131">(483,161)</text>
<circle cx="697" cy="168" r="15"></circle>
<text x="697" y="138">(697,168)</text>
<text id="dynText" x="0" y="-20"></text>
<circle
fill="hsla(120,50%,85%,.5)"
r="15"
id="svgClientX"
stroke="hsla(120,50%,45%,.75)"
/>
</svg>
</div>
<div>
<div class="flex flex--fixed">
<input type="checkbox" class="ctm" />
<h2>Use CTM transform</h2>
</div>
<div class="usedCode"><em>Try to move mouse over SVG drawing</em></div>
</div>
<div class="container"></div>
</div>
CSS Code
.flex {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.flex--vertical {
flex-direction: column;
}
.row {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
margin: 5px 0;
}
.half-width {
width: 100%;
padding: 15px;
}
svg {
outline: solid 1px black;
}
input {
padding: 0.5em;
width: 2em;
height: 2em;
}
input[type="checkbox"] {
display: block;
width: 28px;
height: 28px;
border-radius: 50%;
background: #cca92c;
cursor: pointer;
box-shadow: 0 0 0 rgba(204, 169, 44, 0.4);
animation: pulse 2s infinite;
align-self: center;
margin-right: 10px;
}
input[type="checkbox"]:hover,
input[type="checkbox"]:checked {
animation: none;
}
@-webkit-keyframes pulse {
0% {
-webkit-box-shadow: 0 0 0 0 rgba(204, 169, 44, 1);
}
70% {
-webkit-box-shadow: 0 0 0 10px rgba(204, 169, 44, 0);
}
100% {
-webkit-box-shadow: 0 0 0 0 rgba(204, 169, 44, 0);
}
}
@keyframes pulse {
0% {
-moz-box-shadow: 0 0 0 0 rgba(204, 169, 44, 1);
box-shadow: 0 0 0 0 rgba(204, 169, 44, 0.4);
}
70% {
-moz-box-shadow: 0 0 0 10px rgba(204, 169, 44, 0);
box-shadow: 0 0 0 10px rgba(204, 169, 44, 0);
}
100% {
-moz-box-shadow: 0 0 0 0 rgba(204, 169, 44, 0);
box-shadow: 0 0 0 0 rgba(204, 169, 44, 0);
}
}
@media (max-width: 1000px) {
.flex:not(.flex--fixed) {
display: flex;
flex-direction: column;
}
svg {
height: 120px;
}
}
JavaScript Code
const optimizedResize = (function () {
var callbacks = [],
running = false;
// fired on resize event
function resize() {
if (!running) {
running = true;
if (window.requestAnimationFrame) {
window.requestAnimationFrame(runCallbacks);
} else {
setTimeout(runCallbacks, 66);
}
}
}
// run the actual callbacks
function runCallbacks() {
callbacks.forEach(function (callback) {
callback();
});
running = false;
}
// adds callback to loop
function addCallback(callback) {
if (callback) {
callbacks.push(callback);
}
}
return {
// public method to add additional callback
add: function (callback) {
if (!callbacks.length) {
window.addEventListener('resize', resize);
}
addCallback(callback);
}
}
}());
function onMouseMove(evt) {
const {
clientX,
clientY
} = evt || {
clientX: 0,
clientY: 0
}
point.x = clientX;
point.y = clientY;
let useCTMTransform = '';
let transformedCoordinate = `(${Math.round(point.x)}, ${Math.round(point.y)})`;
if (ctm.checked) {
point = point.matrixTransform(svg.getScreenCTM().inverse());
transformedCoordinate = `(${Math.round(point.x)}, ${Math.round(point.y)})`;
useCTMTransform = `\n point = point.matrixTransform(eleSvg.getScreenCTM().inverse());
// point = ${transformedCoordinate}`
}
eleDynText.setAttribute('x', point.x + 15);
eleDynText.setAttribute('y', point.y + 25);
eleDynText.innerHTML = transformedCoordinate;
svgClientX.setAttribute('cx', point.x);
svgClientX.setAttribute('cy', point.y);
eleCode.innerHTML = `<pre>const eleSvg = document.querySelector('svg');
eleSvg.addEventListener('mousemove', ({clientX, clientY}) => {
let point = eleSvg.createSVGPoint();
point.x = clientX; // ${clientX}
point.y = clientY; // ${clientY}${useCTMTransform}\n})</pre>`
}
var ctm = document.querySelector('.ctm'),
svg = document.querySelector('svg'),
ctmcode = document.querySelector('.ctmcode'),
eleCode = document.querySelector('.usedCode'),
eleDynText = document.querySelector('#dynText');
svg.addEventListener('mousemove', onMouseMove);
var point = svg.createSVGPoint();
onMouseMove();