How can I use an SVG element’s transformation matrix to calculate destination coordinates?

313 Views Asked by At

I suspect there is some way to use an element’s transformation matrix to calculate its coordinates after being transformed, but I don’t know how to do so.

An example diagram explains this best:


A circle with points A, B, and a rotation center. A is the reference/starting point. B is like point A, but with a rotation applied around the rotation center. (All of the above is in a containing SVG, which is much larger in all directions. No assumptions can be made that the circle’s left bounding box matches the root SVG’s bounding box.) A: I know the distance from the rotation center, as well as the exact x and y coordinates. B: I can get a transformation matrix via B.getCTM(), but its x and y coordinates remain identical to those of A. Using the transformation matrix from B, I need to determine the x and y coordinates after transformation. In other words, I need the ability to create an element that appears in the same spot as B, but with no transformations applied.


I’m afraid I didn’t take the math route into programming. I can sort-of follow the surface-level details of what a transformation matrix is doing, but my understanding is sort of like being able to read music one note at a time, and only very slowly; I don’t really understand the tune at a higher level, so I don’t really understand the sound of a complete musical phrase—let alone the melody.

Similarly, I don’t understand how transformation matrices work. I have tried searching for explanations to grok transformation matrices, but everything I find is loaded with more math jargon I don’t understand. I just know that they work sort of like a function, and that they are incredibly flexible tools, but that’s it.

Of all the methods available to SVGMatrix (supposedly deprecated in favor of DOMMatrix, but Firefox Dev. ed. is still using SVGMatrix), I have no idea whether .inverse() or .multiply() is what I want, and no idea how to coax a simple set of x and y coordinates out of that matrix.

Note:

  • I am not interested in translating screen-to-SVG coordinates here.
  • I am only concerned with SVG (user-space) coordinates.
2

There are 2 best solutions below

4
On BEST ANSWER

You can use simple trigonometric transformation:

const rotatePoint = (point, center, rotateAngle) => {
  const dx = point.x - center.x;
  const dy = point.y - center.y;
  const distance = Math.hypot(dx, dy);
  const currentAngle = Math.atan(dy / dx);
  const nextAngle = currentAngle - rotateAngle;
  const nextDX = distance * Math.cos(nextAngle);
  const nextDY = distance * Math.sin(nextAngle);
  return {x: center.x + nextDX, y: center.y + nextDY};
};

The snippet displays rotation of a blue point around the red one (30 / 90 / 123 degrees counter-clockwise)

const rotatePoint = (point, center, angle) => {
    const dx = point.x - center.x;
  const dy = point.y - center.y;
  const distance = Math.hypot(dx, dy);
  const current = Math.atan(dy / dx);
  const next = current - angle;
  const nextDX = distance * Math.cos(next);
  const nextDY = distance * Math.sin(next);
  return {x: center.x + nextDX, y: center.y + nextDY};
};

const center = {x: 150, y: 150};
const start = {x: 200, y: 30};
const svg = d3.select('svg');

svg.append('circle')
    .attr('cx', center.x)
  .attr('cy', center.y)
  .attr('r', 5)
  .style('fill', 'red');
svg.append('circle')
    .attr('cx', start.x)
  .attr('cy', start.y)
  .attr('r', 5)
  .style('fill', 'blue');  

// Rotate 30 deg
const p30 = rotatePoint(start, center, Math.PI * 30 / 180); 
svg.append('circle')
    .attr('cx', p30.x)
  .attr('cy', p30.y)
  .attr('r', 5)
  .style('fill', 'green');  
// Rotate 90 deg
const p90 = rotatePoint(start, center, Math.PI * 90 / 180); 
svg.append('circle')
    .attr('cx', p90.x)
  .attr('cy', p90.y)
  .attr('r', 5)
  .style('fill', 'orange');
// Rotate 123 deg
const p123 = rotatePoint(start, center, Math.PI * 123 / 180); 
svg.append('circle')
    .attr('cx', p123.x)
  .attr('cy', p123.y)
  .attr('r', 5)
  .style('fill', 'yellow');  
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="250" height="200"></svg>

0
On

Once you have an SVG Circle,

You can calculate any SVG point on the circle with the standard getPointAtLength and getTotalLength functions.

Just be aware where the circle starts drawing (green spot)

pathLength converted to degrees:

<style>
  svg{ height:300px }
  text {
    font-size: 3px;
    fill: white;
    text-anchor: middle;
    dominant-baseline: middle;
  }
</style>
<svg id="SVG" viewBox="0 0 100 100">
  <circle cx="50%" cy="50%" r="50%" fill="pink"></circle>
  <circle id="CIRCLE" cx="50%" cy="50%" r="40%" 
          fill="none" stroke="black" stroke-dasharray="2"></circle>
  <circle cx="90%" cy="50%" r="5%" fill="green"></circle>
</svg>
<script>
  let circle_length = CIRCLE.getTotalLength();
  for (let degree = 0; degree < 360; degree += 45) {
    let { x , y } = CIRCLE.getPointAtLength(  degree * ( circle_length / 360 )  );
    let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
    group.innerHTML = `<circle cx="${x}" cy="${y}" r="3%" fill="blue"/>` +
                      `<text x="${x}" y="${y}">${degree}</text>`;
    SVG.append(group)
  }
</script>