'Multiples transformations matrix on SVG, get cursor point

This question is probably more related to math than svg itself.

Inside a main svg, got multiples transformed svg, (from different events), by a viewBox attribute.

Inside those svg, others elements are grouped in a g, modified using matrix transformations.

How to obtain the points on the elements transformed from the mouse pointer.

The goal could be to draw a point or obtain the related point, as a graph chart.

let point = document.getElementById("main").createSVGPoint();

// The mouse cursor points
point.x = 180
point.y = 63
mouse = point.matrixTransform(sub.getCTM())

// console.log(mouse)
// Output 
//  "x": 611.75439453125,
//  "y": 68.71578979492188


// Testing:
circle.setAttribute("cx", 611.75439453125)
circle.setAttribute("cy", 68.71578979492188)
// Not working
<!-- Parent svg -->
<!-- Not preserving aspect ratios -->
<svg id="main" viewBox="0 0 300 400">

  <!-- Includes others svg, transformed with a viewBox -->
  <!-- Not preserving aspect ratios -->
  <svg id="group1" viewBox="7 54 10 570">
       
      <!-- Group element modified with a matrix -->
      <!-- Using css to define the matrix behave identicaly -->
      <!-- All elements in this group are transformed by this matrix -->
      <g id="sub" transform="matrix(4.5,0,0,0.84,-140,99)">
        
        <!-- Exemple element in group -->
        <polyline points="4 65.94338623003772 5 78 6 50.10565885410098 7 40.95007190251531 8 53.698867220021675 9 49.43265131064406 10 44.36112722960851 11 56.329540580770356 12 49.785452985846554 13 44.10803037565898 14 40.537830814642945 15 41.84933269419995 16 38.33857254585345 17 43.590332265307744 18 49.16421525342487 19 49.49017332290519 20 42.51658803643061 21 46.943865580139814 22 36.27544970608283 23 38.070136488634255 24 43.46186643792423 25 42.20788657062835 26 48.37424628503659 27 25.58210762671243 28 23.927391073996347 29 22.349370537628886 30 30.592274894669004 31 21.97356005752208 32 24.960869894290738 33 23.221787723591348 34 17.41781668642936 35 2 36 19.335217095138404 37 39.60405681560149 38 38.49579937936788 39 32.47132729520408 40 25.016474506143126 41 27.037414536922626 42 27.541690844412955 43 20.37253071624997 44 9.872846078159213 45 17.79362716653617 46 13.107500567651172 47 24.955117693064494 48 24.247596942250766 49 19.728284178923616 50 11.341574791230315 51 8.248807931982782 52 10.697328253903962 " ></polyline>
   
        <!-- This circle should be at the cursor -->
        <circle id="circle" cx="50" cy="50" r="50"fill="blue">

      </g>
      
  </svg>

   <!-- Rectangles symbolizing the mouse cursor -->
   <rect width="1000" height="1" x="0" y="63" ></rect>
   <rect width="1" height="500" x="180" y="0"></rect>
   
</svg>

Svg has numerous bindings related to transformations, we can retrieve the transformation matrix of each elements with getCTM() and getBBox(), and use matrixTransform.

This works for one level of transformation?

How to chain multiples matrix transformations?



Solution 1:[1]

If you want the point relative to the transformed area, then it should get reflected as the offsetX and offsetY properties of their corresponding MouseEvents.

However, there seems to be a bug in Webkit / Blink browsers with this regard, so this actually only works in Firefox (and maybe IE?)...

const poly = document.querySelector('polyline');
poly.addEventListener('mousemove', evt => {
  circle.setAttribute("cx", evt.offsetX + 2.5);
  circle.setAttribute("cy", evt.offsetY + 2.5);
});
<!-- Parent svg -->
<!-- Not preserving aspect ratios -->
<svg id="main" viewBox="0 0 300 400">

  <!-- Includes others svg, transformed with a viewBox -->
  <!-- Not preserving aspect ratios -->
  <svg id="group1" viewBox="7 54 10 570">
       
      <!-- Group element modified with a matrix -->
      <!-- Using css to define the matrix behave identicaly -->
      <!-- All elements in this group are transformed by this matrix -->
      <g id="sub" transform="matrix(4.5,0,0,0.84,-140,99)">
        
        <!-- Exemple element in group -->
        <polyline points="4 65.94338623003772 5 78 6 50.10565885410098 7 40.95007190251531 8 53.698867220021675 9 49.43265131064406 10 44.36112722960851 11 56.329540580770356 12 49.785452985846554 13 44.10803037565898 14 40.537830814642945 15 41.84933269419995 16 38.33857254585345 17 43.590332265307744 18 49.16421525342487 19 49.49017332290519 20 42.51658803643061 21 46.943865580139814 22 36.27544970608283 23 38.070136488634255 24 43.46186643792423 25 42.20788657062835 26 48.37424628503659 27 25.58210762671243 28 23.927391073996347 29 22.349370537628886 30 30.592274894669004 31 21.97356005752208 32 24.960869894290738 33 23.221787723591348 34 17.41781668642936 35 2 36 19.335217095138404 37 39.60405681560149 38 38.49579937936788 39 32.47132729520408 40 25.016474506143126 41 27.037414536922626 42 27.541690844412955 43 20.37253071624997 44 9.872846078159213 45 17.79362716653617 46 13.107500567651172 47 24.955117693064494 48 24.247596942250766 49 19.728284178923616 50 11.341574791230315 51 8.248807931982782 52 10.697328253903962 " ></polyline>
   
        <!-- This circle should be at the cursor -->
        <circle id="circle" cx="5" cy="5" r="5" fill="blue" pointer-events="none">

      </g>
      
  </svg>

   <!-- Rectangles symbolizing the mouse cursor -->
   <rect width="1000" height="1" x="0" y="63" ></rect>
   <rect width="1" height="500" x="180" y="0"></rect>
   
</svg>

If you wish to transform arbitrary values, then you use the technique described in this Answer which consists in dispatching such a MouseEvent on your element:

const point = {x:180, y:63};
const poly = document.querySelector('polyline');
poly.addEventListener('mousemove', evt => {
  point.x = evt.offsetX;
  point.y = evt.offsetY;
}, {once: true});
const evt = new MouseEvent('mousemove', {
  clientX: point.x, 
  clientY: point.y
});
poly.dispatchEvent(evt);
console.log(point);
circle.setAttribute("cx",  point.x);
circle.setAttribute("cy", point.y);
<!-- Parent svg -->
<!-- Not preserving aspect ratios -->
<svg id="main" viewBox="0 0 300 400">

  <!-- Includes others svg, transformed with a viewBox -->
  <!-- Not preserving aspect ratios -->
  <svg id="group1" viewBox="7 54 10 570">
       
      <!-- Group element modified with a matrix -->
      <!-- Using css to define the matrix behave identicaly -->
      <!-- All elements in this group are transformed by this matrix -->
      <g id="sub" transform="matrix(4.5,0,0,0.84,-140,99)">
        
        <!-- Exemple element in group -->
        <polyline points="4 65.94338623003772 5 78 6 50.10565885410098 7 40.95007190251531 8 53.698867220021675 9 49.43265131064406 10 44.36112722960851 11 56.329540580770356 12 49.785452985846554 13 44.10803037565898 14 40.537830814642945 15 41.84933269419995 16 38.33857254585345 17 43.590332265307744 18 49.16421525342487 19 49.49017332290519 20 42.51658803643061 21 46.943865580139814 22 36.27544970608283 23 38.070136488634255 24 43.46186643792423 25 42.20788657062835 26 48.37424628503659 27 25.58210762671243 28 23.927391073996347 29 22.349370537628886 30 30.592274894669004 31 21.97356005752208 32 24.960869894290738 33 23.221787723591348 34 17.41781668642936 35 2 36 19.335217095138404 37 39.60405681560149 38 38.49579937936788 39 32.47132729520408 40 25.016474506143126 41 27.037414536922626 42 27.541690844412955 43 20.37253071624997 44 9.872846078159213 45 17.79362716653617 46 13.107500567651172 47 24.955117693064494 48 24.247596942250766 49 19.728284178923616 50 11.341574791230315 51 8.248807931982782 52 10.697328253903962 " ></polyline>
   
        <!-- This circle should be at the cursor -->
        <circle id="circle" cx="5" cy="5" r="5" fill="blue" pointer-events="none">

      </g>
      
  </svg>

   <!-- Rectangles symbolizing the mouse cursor -->
   <rect width="1000" height="1" x="0" y="63" ></rect>
   <rect width="1" height="500" x="180" y="0"></rect>
   
</svg>

Note that WebKit / Blink do set it correctly on HTML elements as demonstrated in this Q/A...

Solution 2:[2]

The problem is that getCTM() only returns the transformation up to the nearest enclosing <svg> element. Instead you can use getScreenCTM() to calculate the transformation relative to the window. Except we actually need to take the inverse of this transformation so it will be subtracted it from the mouse point. Then we make sure everything is relative to the top-level svg container.

This will be accurate no matter how many layers of transformations the target element is nested under.

<svg id="main" viewBox="0 0 300 400">
  <svg id="group1" viewBox="7 54 10 570">
    <g id="sub" transform="matrix(4.5,0,0,0.84,-140,99)">
      <polyline points="4 65.94338623003772 5 78 6 50.10565885410098 7 40.95007190251531 8 53.698867220021675 9 49.43265131064406 10 44.36112722960851 11 56.329540580770356 12 49.785452985846554 13 44.10803037565898 14 40.537830814642945 15 41.84933269419995 16 38.33857254585345 17 43.590332265307744 18 49.16421525342487 19 49.49017332290519 20 42.51658803643061 21 46.943865580139814 22 36.27544970608283 23 38.070136488634255 24 43.46186643792423 25 42.20788657062835 26 48.37424628503659 27 25.58210762671243 28 23.927391073996347 29 22.349370537628886 30 30.592274894669004 31 21.97356005752208 32 24.960869894290738 33 23.221787723591348 34 17.41781668642936 35 2 36 19.335217095138404 37 39.60405681560149 38 38.49579937936788 39 32.47132729520408 40 25.016474506143126 41 27.037414536922626 42 27.541690844412955 43 20.37253071624997 44 9.872846078159213 45 17.79362716653617 46 13.107500567651172 47 24.955117693064494 48 24.247596942250766 49 19.728284178923616 50 11.341574791230315 51 8.248807931982782 52 10.697328253903962 " />
      <circle id="circle" cx="50" cy="50" r="50" fill="blue" />
    </g>
  </svg>
  <rect width="1000" height="1" x="0" y="63" />
  <rect width="1" height="500" x="180" y="0" />   
  <script>
    let sub = document.getElementById('sub');
    let circle = document.getElementById('circle');
    let main = document.getElementById("main");

    let mouse = main.createSVGPoint();
    mouse.x = 180; mouse.y = 63;
    
    function getMatrix(el) {
      let matrix = el.getScreenCTM().inverse();
      matrix = matrix.multiply(main.getScreenCTM());
      return matrix;
    }
    
    let result = mouse.matrixTransform(getMatrix(sub));

    circle.setAttribute("cx", result.x);
    circle.setAttribute("cy", result.y);
  </script>
</svg>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Besworks