Search code examples
animationmatrixsvginkscape

Calculate path center after transformation matrices have been applied


I made a logo in Inkscape. For learning I wanted to make a wheel shape in the logo rotate by the animation support in SVG.

It was easy to implement the rotation, but it was difficult for me to be able to specify the correct axis of rotation. The shape was a cog wheel and I wanted it to rotate around its center. Trial and error gave that the xy-coordinate (47.1275, 1004.17) (whose components are strangely asymmetric, but I guess that has to do with the transformation matrices Inkscape applies) was a good approximation (see animateTransform tag below), but how would I get that from first principles?

<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 321.281 150.799" xmlns:dc="http://purl.org/dc/elements/1.1/">
    <g transform="translate(-9.9178912,-891.57237)">
        <g transform="matrix(1.9522781,0,0,1.9522781,4.6434311,-1008.1558)">
            <animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 47.1275 1004.17" to="45 47.1275 1004.17" dur="2s" fill="freeze" additive="sum" repeatCount="indefinite" />
            <g transform="matrix(0.65043772,0,0,0.65043772,-143.67477,980.4256)" stroke="#666" stroke-miterlimit="4" stroke-dasharray="none" stroke-width="7.68713093" fill="none">
                <path stroke-linejoin="miter" d="m293.404-3.51576c-2.73916,0-5.41514,0.287192-8,0.8125v6.1875c-3.47484,0.838872-6.7198,2.18462-9.6875,4l-4.375-4.375c-2.24264,1.48612-4.29226,3.22977-6.1875,5.125s-3.63888,3.94486-5.125,6.1875l4.375,4.375c-1.81538,2.9677-3.16112,6.21265-4,9.6875h-6.1875c-0.5253,2.58486-0.8125,5.26083-0.8125,8s0.2872,5.41515,0.8125,8h6.1875c0.83888,3.47485,2.18462,6.7198,4,9.6875l-4.375,4.375c1.48612,2.24264,3.22976,4.29227,5.125,6.1875s3.94486,3.63888,6.1875,5.125l4.375-4.375c2.9677,1.81538,6.21266,3.16113,9.6875,4v6.1875c2.58486,0.525308,5.26082,0.8125,8,0.8125,2.73916,0,5.41514-0.287192,8-0.8125v-6.1875c3.47484-0.838872,6.7198-2.18462,9.6875-4l4.375,4.375c2.24264-1.48612,4.29226-3.22977,6.1875-5.125s3.63888-3.94486,5.125-6.1875l-4.375-4.375c1.81538-2.9677,3.16112-6.21266,4-9.6875h6.1875c0.5253-2.58485,0.8125-5.26083,0.8125-8s-0.2872-5.41515-0.8125-8h-6.1875c-0.83888-3.47485-2.18462-6.7198-4-9.6875l4.375-4.375c-1.48612-2.24264-3.22976-4.29227-5.125-6.1875s-3.94486-3.63888-6.1875-5.125l-4.375,4.375c-2.9677-1.81538-6.21266-3.16113-9.6875-4v-6.1875c-2.58486-0.525308-5.26084-0.8125-8-0.8125z" stroke-dashoffset="162" stroke="#666" stroke-linecap="butt" stroke-miterlimit="4" stroke-dasharray="none" stroke-width="7.68713093" fill="none"/>
            </g>
        </g>
    </g>
</svg>

From what I've read in the specification I would say the transformation matrices applied are

1.9522781  0              4.6434311
0          1.9522781  -1008.1558
0          0              1

and

0.65043772  0          -143.67477
0           0.65043772  980.4256
0           0             1

Are they applied on the xyz-coordinate (-9.9178912,-891.57237,0) after the translation transformation?

I guess a correct analysis above would get me the top left point of the path described, or maybe the coordinate for the first handle. After that, does one have to parse the path to decide the bounding-box and thereby the center (since it concerns a somewhat circular object) of the path?

Is it all a lesson in not trying to manually do animation on freely created shapes?


Solution

  • I think the transformations will be applied from the innermost outward, so transform="translate(-9.9178912,-891.57237)" will be done last. But you can ignore the other transformations if you put your animation in the innermost region, i.e. within the path itself:

       <g transform1>
         <g transform2>
           <g transform3>
             <path d="coordinates">
               <animateTransform your transformation here>
             </path>
           </g>
         </g>
       </g>
    

    Then you just need to find the centre of your path, which is easy to do in Inkscape, but tricky to do on-the-fly (related question here: programmatically How to get shape width in SVG document using java).

    Personally, I'd use a script within the svg so you can use getBBox to find the bounding box of your shape. If you add the following element into your SVG you can make any element with the id="cog" turn about its centre:

    <script type="text/ecmascript"><![CDATA[
      var svgNS = "http://www.w3.org/2000/svg";
    
      function init(evt)
      {
        if ( window.svgDocument == null )
        {
            svgDocument = evt.target.ownerDocument;
        }
        addRotateTransform('cog');
      }
    
      function addRotateTransform(target_id)
      {
        var element_to_rotate = svgDocument.getElementById(target_id);
        var my_transform = svgDocument.createElementNS(svgNS, "animateTransform");
    
        var bb = element_to_rotate.getBBox();
        var cx = bb.x + bb.width/2;
        var cy = bb.y + bb.height/2;
    
        my_transform.setAttributeNS(null, "attributeName", "transform");
        my_transform.setAttributeNS(null, "attributeType", "XML");
        my_transform.setAttributeNS(null, "type", "rotate");
        my_transform.setAttributeNS(null, "dur", "4s");
        my_transform.setAttributeNS(null, "repeatCount", "indefinite");
        my_transform.setAttributeNS(null, "from", "0 "+cx+" "+cy);
        my_transform.setAttributeNS(null, "to", "360 "+cx+" "+cy);
    
        element_to_rotate.appendChild(my_transform);
        my_transform.beginElement();
      }
    ]]></script>
    

    You also need to add onload="init(evt)" as an attribute to the SVG tag. e.g.

    <svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.w3.org/2000/svg"
         version="1.1"
         xmlns:cc="http://creativecommons.org/ns#"
         xmlns:xlink="http://www.w3.org/1999/xlink"
         viewBox="0 0 321.281 150.799"
         xmlns:dc="http://purl.org/dc/elements/1.1/"
         onload="init(evt)">
    

    This will call the init() function when the SVG is first loaded. The init() function calls addRotateTransform() which find the element with a given id. It then finds the centre of that object using getBBox() and adds an animateTransform method with the relevant centres. You can change the dur attribute which determines the speed of a full rotation.

    It might seem like a lot of code, but I think it's the easiest way to determine the centre of a path. It also means to can easily add other rotating elements by add addRotateTransform('whatever-id'); to the init() function.