Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I translate mouse movement distances to SVG coordinate space?

I have an SVG visualization of the distribution of CSS4 color keywords in HSL space here: https://meyerweb.com/eric/css/colors/hsl-dist.html

I recently added zooming via the mouse wheel, and panning via mouse clack-and-drag. I’m able to convert a point from screen space to SVG coordinate space using matrixTransform, .getScreenCTM(), and .inverse() thanks to example code I found online, but how do I convert mouse movements during dragging? Right now I’m just shifting the viewBox coordinates by the X and Y values from event, which means the image drag is faster than the mouse movement when zoomed in.

As an example, suppose I’m zoomed in on the image and am dragging to pan, and I jerk the mouse leftwards and slightly downwards. event.movementX returns -37 and event.movementY returns 6. How do I determine how far that equates to in SVG coordinates, so that the viewBox coordinates are shifted properly?

(Note: I’m aware that there are libraries for this sort of thing, but I’m intentionally writing vanilla JS code in order to learn more about both SVG and JS. So please, don’t post “lol just use library X” and leave it at that. Thanks!)

Edited to add: I was asked to post code. Posting the entire JS seems overlong, but this is the function that fires on mousemove events:

function dragger(event) {
    var target = document.getElementById('color-wheel');
    var coords = parseViewBox(target);
    coords.x -= event.movementX;
    coords.y -= event.movementY;
    changeViewBox(target,coords);
}

If more is needed, then view source on the linked page; all the JS is at the top of the page. Nothing is external except for a file that just contains all the HSL values and color names for the visualization.

like image 831
Eric A. Meyer Avatar asked Dec 23 '22 23:12

Eric A. Meyer


1 Answers

My recommendation: Don't worry about the movementX/Y properties on the event. Just worry about where the mouse started and where it is now.

(This has the additional benefit that you get the same result even if you miss some events: maybe because the mouse moved out of the window, or maybe because you want to group events so you only run the code once per animation frame.)

For where the mouse started, you measure that on the mousedown event. Convert it to a position in the SVG coordinates, using the method you were using, with .getScreenCTM().inverse() and .matrixTransform(). After this conversion, you don't care where on the screen this point is. You only care about where it is in the picture. That's the point in the picture that you're always going to move to be underneath the mouse.

On the mousemove events, you use that same conversion method to find out where the mouse currently is within the current SVG coordinate system. Then you figure out how far that is from the point (again, in SVG coordinates) that you want underneath the mouse. That's the amount that you use to transform the graphic. I've followed your example and am doing the transform by shifting the x and y parts of the viewBox:

function move(e) {
  var targetPoint = svgCoords(event, svg);
  shiftViewBox(anchorPoint.x - targetPoint.x,
               anchorPoint.y - targetPoint.y);
}

You can also shift the graphic around with a transform on a group (<g> element) within the SVG; just be sure to use that same group element for the getScreenCTM() call that converts from the clientX/Y event coordinates.

Full demo for the drag to pan. I've skipped all your drawing code and the zooming effect. But the zoom should still work, because the only position you're saving in global values is already converted into SVG coordinates.

var svg = document.querySelector("svg");
var anchorPoint;

function shiftViewBox(deltaX, deltaY) {
	svg.viewBox.baseVal.x += deltaX;
	svg.viewBox.baseVal.y += deltaY;
}

function svgCoords(event,elem) {
	var ctm = elem.getScreenCTM();
	var pt = svg.createSVGPoint();
    // Note: rest of method could work with another element,
    // if you don't want to listen to drags on the entire svg.
    // But createSVGPoint only exists on <svg> elements.
	pt.x = event.clientX;
	pt.y = event.clientY;
	return pt.matrixTransform(ctm.inverse());
}

svg.addEventListener("mousedown", function(e) {
  anchorPoint = svgCoords(event, svg);
  window.addEventListener("mousemove", move);
  window.addEventListener("mouseup", cancelMove);
});

function cancelMove(e) {
  window.removeEventListener("mousemove", move);
  window.removeEventListener("mouseup", cancelMove);
  anchorPoint = undefined;
}

function move(e) {
  var targetPoint = svgCoords(event, svg);
  shiftViewBox(anchorPoint.x - targetPoint.x,
               anchorPoint.y - targetPoint.y);
}
body {
  display: grid;
  margin: 0;
  min-height: 100vh;
}

svg {
  margin: auto;
  width: 70vmin;
  height: 70vmin;
  border: thin solid gray;
  cursor: move;
}
<svg viewBox="-40 -40 80 80">
  <polygon fill="skyBlue"
           points="0 -40, 40 0, 0 40 -40 0" />
</svg>
like image 103
AmeliaBR Avatar answered Dec 25 '22 22:12

AmeliaBR