Moving an SVG element after rotation

Refresh

March 2019

Views

823 time

1

Please help me understand the best practises of manipulating svg elements with their transforms using plain javascript.

I understand the coordinate system is passed down the nodes and chained etc etc.

What I'm trying to achieve is to continue the original translation on the element after rotation. Not along the axis after the rotation has been applied.

Do I have to clone-copy the first translate-transform and add it to the end of the transform list?

Many thanks if someone could shed some light on an eloquent way.

2 answers

3

The way you achieve this is by nesting the transforms. For example, have a look at the following sample SVG.

<svg width="600" height="200">
  <g>
    <rect x="0" y="50" width="100" height="100" fill="blue"/>
  </g>
</svg>

You can apply one transform to the <g> and another to the <rect>. They will be independent, but will combine for the overall effect.

So for example if I want to move everything right, I can apply a translation transform to the group.

<svg width="600" height="200">
  <g transform="translate(200,0)">
    <rect x="0" y="50" width="100" height="100" fill="blue"/>
  </g>
</svg>

Then if I want to rotate the rectangle in place, I can do so by applying a rotation transform to that.

<svg width="600" height="200">
  <g transform="translate(200,0)">
    <rect x="0" y="50" width="100" height="100" fill="blue"
          transform="rotate(45,50,100)"/>
  </g>
</svg>

Then, if I want, I can move the rect even further right, by updating the group's transform.

<svg width="600" height="200">
  <g transform="translate(400,0)">
    <rect x="0" y="50" width="100" height="100" fill="blue"
          transform="rotate(45,50,100)"/>
  </g>
</svg>

The way to think about this is that the <rect> is in it's own little world ("coordinate space" is the official term :) and is blissfully unaware of what is going on in its parent elements.

So if we use what we have learned above we can easily create the sort of animation you are after. The following animation consists of three phases. First we move the rectangle right, then we rotate it, then we continue right again. The rotation in the middle pahes dow not affect the third phase where we again move to the right.

var  outer = document.getElementById("outer");
var  inner = document.getElementById("inner");

var  tx = 0;     // the animated X position
var  angle = 0;  // the animated angle

/*
 * The first phase of the animation.
 * Function to step to the right until we hit tx=200.
 */
var  stepRightTo200 = function() {
  setTimeout(function() {
    tx += 4;
    outer.setAttribute('transform', 'translate('+tx+',0)');
    if (tx < 200)  // schedule another step in this phase
      stepRightTo200();
    else           // start next phase of animation
      rotateTo45();
  }, 32);
};

/*
 * The second phase of the animation.
 * Step the angle around until we hit 45 degrees.
 */
var  rotateTo45 = function() {
  setTimeout(function() {
    angle += 1;
    inner.setAttribute('transform', 'rotate('+angle+',50,100)');
    if (angle < 45)
      rotateTo45()
    else
      stepRightTo400();   // start third phase of animation
  }, 32);
};

/*
 * The third phase of the animation.
 * Step to the right until we hit tx=400.
 */
var  stepRightTo400 = function() {
  setTimeout(function() {
    tx += 4;
    outer.setAttribute('transform', 'translate('+tx+',0)');
    if (tx < 400)
      stepRightTo400();
  }, 32);
};

// Kick off first phase of animation
stepRightTo200();
<svg width="600" height="200">
  <g id="outer">
    <rect id="inner" x="0" y="50" width="100" height="100" fill="blue"/>
  </g>
</svg>

In the examples above, I have separated the "outer" transform out onto a parent group, but we don't really have to do that. We can nest transform operations into a single transform.

So we could simplify the third SVG example above to:

<svg width="600" height="200">
  <rect x="0" y="50" width="100" height="100" fill="blue"
        transform="translate(400,0) rotate(45,50,100)"/>
</svg>

The "outer" transform becomes the first one in the transform list. This is a good way to conceptualise a multipart transform if you ever need to create one. Start by creating (or imagining) a nested group structure, and apply your transforms to that from "outside" (left) to "inside" (right).

So finally, we can rewrite our animation script using this non-nested form.

var  inner = document.getElementById("inner");

var  tx = 0;     // the animated X position
var  angle = 0;  // the animated angle

/*
 * The first phase of the animation.
 * Function to step to the right until we hit tx=200.
 */
var  stepRightTo200 = function() {
  setTimeout(function() {
    tx += 4;
    inner.setAttribute('transform',
                       'translate('+tx+',0) rotate('+angle+',50,100)');
    if (tx < 200)  // schedule another step in this phase
      stepRightTo200();
    else           // start next phase of animation
      rotateTo45();
  }, 32);
};

/*
 * The second phase of the animation.
 * Step the angle around until we hit 45 degrees.
 */
var  rotateTo45 = function() {
  setTimeout(function() {
    angle += 1;
    inner.setAttribute('transform',
                       'translate('+tx+',0) rotate('+angle+',50,100)');
    if (angle < 45)
      rotateTo45()
    else
      stepRightTo400();   // start third phase of animation
  }, 32);
};

/*
 * The third phase of the animation.
 * Step to the right until we hit tx=400.
 */
var  stepRightTo400 = function() {
  setTimeout(function() {
    tx += 4;
    inner.setAttribute('transform',
                       'translate('+tx+',0) rotate('+angle+',50,100)');
    if (tx < 400)
      stepRightTo400();
  }, 32);
};

// Kick off first phase of animation
stepRightTo200();
<svg width="600" height="200">
  <rect id="inner" x="0" y="50" width="100" height="100" fill="blue"/>
</svg>

Hope this helps you understand how transforms work.

1

The best approach is to create a matrix transform, then request various transforms and use the consolidate() method. Below is an example

<!DOCTYPE HTML>
<html>
<head>
  <title>Transform Request</title>
</head>

<body onload=showSVGSource()>
<b><center>Transform Request Object + consolidate</b>
<div id=svgDiv style=background:lime;width:400px;height:400px; >
<svg id=mySVG width=400 height=400 >
<circle id=myCircle cx=0 cy=0 r=150 fill=yellow transform="translate(200,200)" />
</svg>
</div>
<br>
<button onClick=translateCircle()>translate</button>
<button onClick=scaleCircle()>scale</button>
<br>
<textarea id=svgSourceValue style=width:500px;height:100px;></textarea>
<center>
<script>
function translateCircle()
{
    var  objTransformRequestObj = mySVG.createSVGTransform()
    //---attach new  transform to element, init its transform list---
    var myTransListAnim=myCircle.transform
    var objTransList=myTransListAnim.baseVal

    objTransformRequestObj.setTranslate(40,0)
    objTransList.appendItem(objTransformRequestObj)
    objTransList.consolidate()
    showSVGSource()

}
function scaleCircle()
{
    var  objTransformRequestObj = mySVG.createSVGTransform()
    //---attach new  transform to element, init its transform list---
    var myTransListAnim=myCircle.transform
    var objTransList=myTransListAnim.baseVal

    objTransformRequestObj.setScale(.5,.3)
    objTransList.appendItem(objTransformRequestObj)
    objTransList.consolidate()
    showSVGSource()
}
function showSVGSource()
{
   svgSourceValue.value=svgDiv.innerHTML
}
</script>
</body>

</html>