Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserve descendant elements' size while scaling the parent element

Tags:

svg

I have an XHTML page with SVG embedded into it as an <svg> element. I need to implement scaling, but there is a requirement that inner <text> elements must not scale. An obvious solution is

<svg ...>
  <!-- Root <g> to programmatically `setAttribute("transform", ...)` -->
  <g transform="scale(0.5)">
    <!-- Various inner elements here -->
    <!-- Here is a text element.
         Additional <g> is to apply text position which
         *must* depend on the scaling factor set above -->
    <g transform="translate(100 50)">
      <!-- Apply the opposite scaling factor.
           Must be done for every `<text>` element on each change of the scaling factor... -->
      <text x="0" y="0" transform="scale(2)">Hello, World!</text>
    </g>
  </g>
</svg>

Is there a better solution than that? Maybe, there is a way to "reset" the resulting scaling factor on inner <text> elements? The code must work in Firefox and Chrome.

UPD. There is also an option to place text elements outside the element being scaled and manually control text elements' positions. It avoids visual glitches appearing on the text because of scaling. Currently I use this method.

like image 230
Evgeny A. Avatar asked Jan 16 '12 13:01

Evgeny A.


2 Answers

There is transform="ref(svg)" which is defined in SVG Tiny 1.2. To the best of my knowledge this is not implemented in any browsers except Opera (Presto).

<svg xmlns="http://www.w3.org/2000/svg" font-size="24" text-rendering="geometricPrecision">
  <!-- Root <g> to programmatically `setAttribute("transform", ...)` -->
  <text x="0" y="1em" stroke="gray" fill="none">Hello, World!</text>
  <g transform="scale(0.5) rotate(25)">
    <!-- Various inner elements here -->
    <!-- Here is a text element.
         Additional <g> is to apply text position which
         *must* depend on the scaling factor set above -->
    <g transform="translate(100 50)">
      <!-- Apply the opposite scaling factor.
           Must be done for every `<text>` element on each change of the scaling factor... -->
      <text x="0" y="1em" transform="ref(svg)">Hello, World!</text>
    </g>
  </g>
</svg>

In the above example the text should appear where the gray outline is (in the top-left corner). No rotation or scaling should be applied to the element that has transform="ref(svg)", for the purposes of transformations it's as if it was a direct child of the root svg element.

like image 104
Erik Dahlström Avatar answered Apr 14 '23 17:04

Erik Dahlström


If it is acceptable to (a) use JavaScript and (b) specify the position of each unscaling element via transform="translate(…,…)" either on the element's themselves or in a wrapping group (as you have done), then you can use my unscaling code:
http://phrogz.net/svg/libraries/SVGPanUnscale.js

Demo: http://jsfiddle.net/uPSc4/

As you can see in the demo it undoes all transformation except translation (including scale, rotation, and skew) while keeping the elements positioned where they are supposed to be.

See also this demo, which allows you to zoom and pan the graphic using SVGPan while keeping certain marking elements unscaled:
http://phrogz.net/svg/scale-independent-elements.svg

Here's all the code you need:

// Copyright 2012 © Gavin Kistner, [email protected]
// License: http://phrogz.net/JS/_ReuseLicense.txt

// Removes all transforms applied above an element.
// Apply a translation to the element to have it remain at a local position
function unscale(el){
  var svg = el.ownerSVGElement;
  var xf = el.scaleIndependentXForm;
  if (!xf){
    // Keep a single transform matrix in the stack for fighting transformations
    // Be sure to apply this transform after existing transforms (translate)
    xf = el.scaleIndependentXForm = svg.createSVGTransform();
    el.transform.baseVal.appendItem(xf);
  }
  var m = svg.getTransformToElement(el.parentNode);
  m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
  xf.setMatrix(m);
}

Here's the demo code, in case JSFiddle is down:

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:x="http://www.w3.org/1999/xlink"
     font-size="36">
  <g transform="matrix(1 0 0 1 0 0)">
    <g transform="translate(100 50)"><text x="0" y="1em">A!</text></g>
    <g transform="translate(200 150)"><text x="0" y="1em">B!</text></g>
    <g transform="translate(150 75)"><text x="0" y="1em">C!</text></g>
  </g>
  <script x:href="http://phrogz.net/svg/libraries/SVGPanUnscale.js"></script>
  <script>
    var root = document.querySelector('g'),
        text = document.querySelectorAll('text'),
        xf   = root.transform.baseVal.getItem(0);

    // Keep scaling, spinning, and sliding the outer graphic
    // but "unscale" each text element afterwards.    
    setInterval(function(){
      var m = xf.matrix.scale(0.99).translate(10,0).rotate(1);
      xf.setMatrix(m);
      [].forEach.call(text,unscale);
    },100);​
  </script>
</svg>​
like image 43
Phrogz Avatar answered Apr 14 '23 17:04

Phrogz