Mouse position inside autoscaled SVG

JavascriptSvg

Javascript Problem Overview


I am experiencing troubles concerning the position of mouse cursor inside my SVG document. I'd like to design a potentiometer that will follow the cursor when dragged, using JavaScript in an HTML page.

I tried evt.clientX/Y and evt.screenX/Y but as my SVG is in autoscale, coordinates inside my SVG are different. I have been searching for an answer for days now but I couldn't find any solution (either knowing my SVG rescaling factor in real time or have a function for mouse location in SVG coordinates system).

The rotation will follow a simple rule:

if (evt.screenX < xc)
  ang = Math.atan((evt.screenY - yc) / (evt.screenX - xc)) * 360/(2*Math.PI) - 90;  

if (evt.screenX > xc)
  ang = Math.atan((evt.screenY - yc) / (evt.screenX - xc)) * 360/(2*Math.PI) + 90;  

With (xc;yc) as center of rotation and replacing all evt.screenX/Y by the coordinates of the mouse inside my SVG.

Javascript Solutions


Solution 1 - Javascript

See this code, which not only shows how to transform from screen space to global SVG space, but also how to transform a point from SVG space into the transformed space of an element:
http://phrogz.net/svg/drag_under_transformation.xhtml

In short:

// Find your root SVG element
var svg = document.querySelector('svg');

// Create an SVGPoint for future math
var pt = svg.createSVGPoint();

// Get point in global SVG space
function cursorPoint(evt){
  pt.x = evt.clientX; pt.y = evt.clientY;
  return pt.matrixTransform(svg.getScreenCTM().inverse());
}

svg.addEventListener('mousemove',function(evt){
  var loc = cursorPoint(evt);
  // Use loc.x and loc.y here
},false);

Edit: I've created a sample tailored to your needs (albeit only in global SVG space):
http://phrogz.net/svg/rotate-to-point-at-cursor.svg

It adds the following method to the above:

function rotateElement(el,originX,originY,towardsX,towardsY){
  var angle = Math.atan2(towardsY-originY,towardsX-originX);
  var degrees = angle*180/Math.PI + 90;
  el.setAttribute(
    'transform',
    'translate('+originX+','+originY+') ' +
      'rotate('+degrees+') ' +
      'translate('+(-originX)+','+(-originY)+')'
  );
}

Solution 2 - Javascript

Getting the correct svg mouse coordinate is tricky. First of all, a common way is to use the clientX and clientY of the event property an substract it with getBoundingClientRect() and clientLeft respectively clientTop.

svg.addEventListener('click', event =>
{
    let bound = svg.getBoundingClientRect();

    let x = event.clientX - bound.left - svg.clientLeft - paddingLeft;
    let y = event.clientY - bound.top - svg.clientTop - paddingTop;
}

But, if the svg has a padding style information greater then zero, the coordinate is shifting. So this information must be also substract:

let paddingLeft = parseFloat(style['padding-left'].replace('px', ''));
let paddingTop = parseFloat(style['padding-top'].replace('px', ''));

let x = event.clientX - bound.left - svg.clientLeft - paddingLeft;
let y = event.clientY - bound.top - svg.clientTop - paddingTop;

And the not so nice think is, that in some browsers the border property also shift the coordinate, and in other not. I found out, that the shift takes place if the x and y of the event property is not available.

if(event.x === undefined)
{
    x -= parseFloat(style['border-left-width'].replace('px', ''));
    y -= parseFloat(style['border-top-width'].replace('px', ''));
}

After this transformation the x and y coordinate can out of bound, that should be fix. But that not the think.

let width = svg.width.baseVal.value;
let height = svg.height.baseVal.value;

if(x < 0 || y < 0 || x >= width || y >= height)
{
    return;
}

This solution can use for click, mousemove, mousedown, ... and so on. You can reach a live demo here: https://codepen.io/martinwantke/pen/xpGpZB

Solution 3 - Javascript

@Phrogz: Thanks for your wonderful example and I learned from that. I have changed some of it like below to make it a bit easy right. As I thinking that like we handle mouse events in core java we can also handle same way here so I tried my way in your example.

I have removed "rotateElement" function as I think that it is some difficult and i find a substitute if it.

See below code:

var svg=document.getElementById("svg1");
var pt=svg.createSVGPoint();
var end_small=document.getElementById("end_small");
var line=document.getElementById("line1");

end_small.addEventListener('mousemove', function(evt) {

    var loc=getCursor(evt);
    end_small.setAttribute("cx",loc.x);
    end_small.setAttribute("cy",loc.y);

    loc = getCursor(evt); // will get each x,y for mouse move

    line.setAttribute('x2',loc.x); // apply it  as end points of line
    line.setAttribute('y2',loc.y); // apply it as end points of line

}, false);

function getCursor(evt) {
    pt.x=evt.clientX;
    pt.y=evt.clientY;
    return pt.matrixTransform(svg.getScreenCTM().inverse());
}

So what I have done is I have just added listener only to small circle not whole SVG and everytime when mouse moved by you I will get x, y from getCursor() function as stated above and I will give this x, y as x2, y2 of my line thats it does not translate and does not rotate. You must move your mouse to to circle and then slowly move and if your mouse leave circle then line will not move as we have just added listener only on small circle right.

Solution 4 - Javascript

Use DOMPoint or DOMPointReadOnly

Example

show cursor point on the SVG.

<svg style="margin-left:35%; width:30%; border: 1px dashed blue">
  <text x="30" y="30">click me</text>
  <circle id="mouseCursor" cx="0" cy="0" r="4" fill="#00ff00" visibility="hidden"></circle>
</svg>
<script>
  const svg = document.querySelector(`svg`)

  svg.onclick = (e) => {
    const domPoint = new DOMPointReadOnly(e.clientX, e.clientY)
    const pt = domPoint.matrixTransform(svg.getScreenCTM().inverse())
    const frag = document.createRange().createContextualFragment(`<circle cx="${pt.x}" cy="${pt.y}" r="2"></circle>`)
    svg.append(frag)
    svg.innerHTML = svg.innerHTML
  }

  // Below is for showing the cursor.
  svg.onmousemove = (e) => {
    const pt = new DOMPointReadOnly(e.clientX, e.clientY).matrixTransform(svg.getScreenCTM().inverse())
    const circleMouse = svg.querySelector(`#mouseCursor`)
    circleMouse.setAttribute("cx", `${pt.x}`)
    circleMouse.setAttribute("cy", `${pt.y}`)
  }
  svg.onmouseover = () => svg.querySelector(`#mouseCursor`).setAttribute("visibility", "visible")
  svg.onmouseout = () => svg.querySelector(`#mouseCursor`).setAttribute("visibility", "hidden")
</script>

> Note: SVGPoint is deprecatedcreateSVGPoint. Use DOMPoint or DOMPointReadOnly instead.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRiwallView Question on Stackoverflow
Solution 1 - JavascriptPhrogzView Answer on Stackoverflow
Solution 2 - JavascriptMartin WantkeView Answer on Stackoverflow
Solution 3 - JavascriptMehul PopatView Answer on Stackoverflow
Solution 4 - JavascriptCarsonView Answer on Stackoverflow