SVG coordinates with transform matrix

I want to implement the functionality like svg-edit on rectangle element

  1. Rotate rectangle
  2. Resizing
  3. Drag

Rotating the SVG rectangle it works fine, but when I want to resize the rectangle it has a problem. The coordinates are not working right; I use the transform matrix to rotate targetelement.setAttribute(transform,rotate(45,cx,cy)) but when the element has been rotated the coordinates are moved. I'm also using inverse function to inverse the transform matrix it resolves the problem but its not working with drag function.


Solution 1:

I have created a working example of what I believe you are describing on my site here:
http://phrogz.net/svg/drag_under_transformation.xhtml

In general, you convert the mouse cursor into the local space of an object by:

  1. Creating a mousemove event handler:

    var svg = document.getElementsByTagName('svg')[0];
    document.documentElement.addEventListener('mousemove',function(evt){
      ...
    },false);
    
  2. In that event handler, convert the mouse coordinates (in pixels) into the global space of your SVG document:

    var pt = svg.createSVGPoint();
    pt.x = evt.clientX;
    pt.y = evt.clientY;
    var globalPoint = pt.matrixTransform(svg.getScreenCTM().inverse());
    
  3. Convert the global point into the space of the object you are dragging:

    var globalToLocal = dragObject.getTransformToElement(svg).inverse();
    var inObjectSpace = globalPoint.matrixTransform( globalToLocal );
    

For Stack Overflow posterity, here's the full source of my SVG+XHTML demo (in case my site is down):

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head> 
<meta http-equiv="content-type" content="application/xhtml+xml;charset=utf-8"/>
<title>Dragging Transformed SVG Elements</title>
<style type="text/css" media="screen">
  html, body {
    background:#eee; margin:0;
    user-select:none; -moz-user-select:none; -webkit-user-select:none;
  }
  p { margin:0.5em; text-align:center }
  svg {
    position:absolute; top:5%; left:5%; width:90%; height:90%;
    background:#fff; border:1px solid #ccc
  }
  svg rect { stroke:#333 }
  svg .drag { cursor:move }
  svg .sizer { opacity:0.3; fill:#ff0; stroke:#630;}
  #footer {
    position:absolute; bottom:0.5em; margin-bottom:0;
    width:40em; margin-left:-20em; left:50%; color:#666;
    font-style:italic; font-size:85%
  }
  #dragcatch { position:absolute; left:0; right:0; top:0; bottom:0; z-index:-1}
</style>
</head><body>
<p>Showing how to drag points inside a transformation hierarchy.</p>
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg"
 xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full">
  <g transform="scale(1.2,0.8)">
    <rect transform="translate(50,20) rotate(30)"
     class="drag resize" x="50" y="30" width="50" height="30" fill="#69c" />
    <rect class="drag resize" x="5" y="5" width="90" height="50" fill="#c66" />
  </g>
</svg>
<p id="footer">
  Copyright © 2011 <a href="mailto:[email protected]">Gavin Kistner</a>. 
  Comments/criticisms welcome.
</p>
<script type="text/javascript"><![CDATA[
  var svg   = document.getElementsByTagName('svg')[0];
  var svgNS = svg.getAttribute('xmlns');
  var pt    = svg.createSVGPoint();

  function createOn(root,name,prop){
    var el = document.createElementNS(svgNS,name);
    for (var a in prop) if (prop.hasOwnProperty(a)) el.setAttribute(a,prop[a]);
    return root.appendChild(el);
  }

  function rectCorner(rect){
    pt.x = rect.x.animVal.value + rect.width.animVal.value;
    pt.y = rect.y.animVal.value + rect.height.animVal.value;
    return pt.matrixTransform(rect.getTransformToElement(svg));
  }

  function pointIn(el,x,y){
    pt.x = x; pt.y = y;
    return pt.matrixTransform(el.getTransformToElement(svg).inverse());
  }

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

  // Make all rects resizable before drag, so the drag handles become drag
  for (var a=svg.querySelectorAll('rect.resize'),i=0,len=a.length;i<len;++i){
    (function(rect){
      var dot = createOn(svg,'circle',{'class':'drag sizer',cx:0,cy:0,r:5});
      var moveDotToRect = function(){
        var corner = rectCorner(rect);
        dot.setAttribute('cx',corner.x);
        dot.setAttribute('cy',corner.y);
      }
      moveDotToRect();
      rect.addEventListener('dragged',moveDotToRect,false);
      dot.addEventListener('dragged',function(){
        var rectXY = pointIn(rect,dot.cx.animVal.value,dot.cy.animVal.value);
        var w = Math.max( rectXY.x-rect.x.animVal.value, 1 );
        var h = Math.max( rectXY.y-rect.y.animVal.value, 1 );
        rect.setAttribute('width', w);
        rect.setAttribute('height',h);
      },false);
    })(a[i]);
  }

  for (var a=svg.querySelectorAll('.drag'),i=0,len=a.length;i<len;++i){
    (function(el){
      var onmove; // make inner closure available for unregistration
      el.addEventListener('mousedown',function(e){
        el.parentNode.appendChild(el); // move to top
        var x = el.tagName=='circle' ? 'cx' : 'x';
        var y = el.tagName=='circle' ? 'cy' : 'y';
        var mouseStart   = cursorPoint(e);
        var elementStart = { x:el[x].animVal.value, y:el[y].animVal.value };
        onmove = function(e){
          var current = cursorPoint(e);
          pt.x = current.x - mouseStart.x;
          pt.y = current.y - mouseStart.y;
          var m = el.getTransformToElement(svg).inverse();
          m.e = m.f = 0;
          pt = pt.matrixTransform(m);
          el.setAttribute(x,elementStart.x+pt.x);
          el.setAttribute(y,elementStart.y+pt.y);
          var dragEvent = document.createEvent("Event");
          dragEvent.initEvent("dragged", true, true);
          el.dispatchEvent(dragEvent);
        };
        document.body.addEventListener('mousemove',onmove,false);
      },false);
      document.body.addEventListener('mouseup',function(){
        document.body.removeEventListener('mousemove',onmove,false);
      },false);
    })(a[i]);
  }
]]></script>
<div id="dragcatch"></div>
</body></html>

Solution 2:

For those who use Chrome please add the following lines after

var pt    = svg.createSVGPoint();

SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
  return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};

More info here: https://github.com/cpettitt/dagre-d3/issues/202