133

I need to resize and rotate certain elements in SVG document using javascript. The problem is, by default, it always applies the transform around the origin at (0, 0) – top left.

How can I re-define this transform anchor point?

I tried using the transform-origin attribute, but it does not affect anything.

This is how I did it:

svg.getDocumentById('someId').setAttribute('transform-origin', '75 240');

It does not seem to set the pivotal point to the point I specified although I can see in Firefox that the attribute is correctly set. I tried things like center bottom and 50% 100% with and without parenthesis. Nothing worked so far.

Can anyone help?

Hafenkranich
  • 1,696
  • 18
  • 32
CTheDark
  • 2,507
  • 3
  • 18
  • 18
  • FWIW, supposedly fixed as of Firefox 19 beta 3, though I'm still having problems in Firefox 22. Mozilla bugzilla listing: https://bugzilla.mozilla.org/show_bug.cgi?id=828286 – Toph Jul 08 '13 at 05:15
  • @CTheDark, could you reconsider the accepted answer for this please? We now have a more modern solution: https://stackoverflow.com/a/62720107/4375751 – George Sep 27 '20 at 17:51

8 Answers8

174

To rotate use transform="rotate(deg, cx, cy)", where deg is the degree you want to rotate and (cx, cy) define the centre of rotation.

For scaling/resizing, you have to translate by (-cx, -cy), then scale and then translate back to (cx, cy). You can do this with a matrix transform:

transform="matrix(sx, 0, 0, sy, cx-sx*cx, cy-sy*cy)"

Where sx is the scaling factor in the x-axis, sy in the y-axis.

Persijn
  • 14,624
  • 3
  • 43
  • 72
Peter Collingridge
  • 10,849
  • 3
  • 44
  • 61
  • Thank you so much. Rotate works perfectly I think, but scaling/resizing always end up having off by some random amount. I tried using the matrix and doing them separately like "translate(cx*sx, cy*sy) scale(sx, sy)". The same result. – CTheDark Jul 16 '11 at 00:03
  • 6
    Wouldn't it be transform="matrix(sx, 0, 0, sy, cx-sx*cx, cy-sy*cy)"? Because you want to only translate by the difference. I think this logic is correct, but it still gives me off positions... – CTheDark Jul 16 '11 at 01:13
  • Now it works! I was just using wrong cx and cy values! THanks a lot! – CTheDark Jul 16 '11 at 01:40
  • @Charlemagne Are you saying that the original formula or your formula worked for you? Because only Charlemagne's formula worked for me. – Max Nanasy Feb 06 '13 at 03:53
  • @PeterCollingridge will this same matrix transform work for regular (non-SVG) css? I've been trying to do the math myself, but am having probelms. – Jay Stevens Jul 31 '14 at 14:02
  • 1
    @JayStevens Yes, the matrix transform will work for any system, it's just maths. The only difference might be the notation. As far as I can tell, the CSS matrix transform uses the same notation as for SVGs, so the same six numbers in that order should work. – Peter Collingridge Aug 01 '14 at 11:17
  • 1
    Just to clarify, cx and cy need to be offsets of the _center_ of the box your scaling in. This tripped me up for a bit, as I was thinking in CSS box models. @forresto alludes to this in his answer below. – Jason Farnsworth Dec 14 '15 at 06:14
  • @PeterCollingridge Thanks for the info. I want to resize an svg element according to mousemove. How should i calculate sx,sy? – Ashok kumar May 27 '16 at 06:08
  • @PeterCollingridge does it mean that when I also apply `translate` the matrix would look like this `transform="matrix(sx, 0, 0, sy, tx+cx-sx*cx, ty+cy-sy*cy)"`? – thednp Aug 25 '16 at 19:55
  • @thednp Correct, assuming the translation happens after the other transformations (which is most likely the case). – Peter Collingridge Aug 26 '16 at 14:48
  • the portion of your solution that help me was: transform="rotate(deg, cx, cy)". I was using it to rotate svg text on a vertical line in Nuxt. Concrete code example below: – sandalwoodsh Jun 29 '23 at 02:33
119

New 2020 solution

svg * { 
  transform-box: fill-box;
}

applying transform-box: fill-box will make an element within an SVG behave as a normal HTML element. Then you can apply transform-origin: center (or something else) as you would normally

that's right, transform-box: fill-box. These days, there's no need for any complicated matrix stuff

George
  • 1,706
  • 1
  • 11
  • 12
  • 20
    VERY useful solution, this sould be upvoted higher. this is exactly what I needed; thank you! svg and html elements have subtle differences when transforming and transitioning. i was perplexed with an animation bug for hours until i found this answer... with `transform-box: fill-box;` you can make svg elementes respect `transform-origin` in a sane manner / like you'd expect! – zfogg Aug 20 '20 at 06:23
  • 2
    no worries dude! I had similar relief when I found it (thank you @Robert Longson)!! It fixed the massive headache I was facing trying to control dots on an svg map :sweat: :sweat: :sweat: – George Sep 27 '20 at 17:49
  • 4
    Please note: transform-box was introduced fully in 2017 - 6 years after this question was asked & first answered. Thus you might have to forgive people for 'missing the trick'. – SimplSam Nov 20 '20 at 00:47
  • 1
    Hey SimplSam, I have updated the answer to be more friendly towards the past – George Dec 04 '20 at 12:11
  • this definitely the right answer, just add it to whatever SVG element that is being animated. you don't even need transform-origin since its the default behavior – Ziv Feldman Jun 22 '21 at 23:04
  • 1
    Maybe clarify that you need to replace `svg` with a specific element or needing to use something like `svg *`. Maybe use `[your svg element]` instead of `svg`. Or maybe simply remove the selector from your example? – Peter Uithoven Aug 11 '21 at 15:07
  • 1
    After 2 hours of researching options I luckily stumbled upon your magical solution which will save me _days_ of work on a project. Thanks! – Joel Farris Mar 23 '22 at 23:19
  • 1
    I have been looking for a tool like this for a long time, thanks mate! – louielyl Jun 10 '22 at 02:21
22

If you can use a fixed value (not "center" or "50%"), you can use CSS instead:

-moz-transform-origin: 25px 25px;
-ms-transform-origin:  25px 25px;
-o-transform-origin: 25px 25px;
-webkit-transform-origin:  25px 25px;
transform-origin: 25px 25px;

Some browsers (like Firefox) won't handle relative values correctly.

Hafenkranich
  • 1,696
  • 18
  • 32
15

For scaling without having to use the matrix transformation:

transform="translate(cx, cy) scale(sx sy) translate(-cx, -cy)"

And here it is in CSS:

transform: translate(cxpx, cypx) scale(sx, sy) translate(-cxpx, -cypx)
cmititiuc
  • 660
  • 6
  • 14
14

If you're like me and want to pan and then zoom with transform-origin, you'll need a little more.

// <g id="view"></g>
var view = document.getElementById("view");

var state = {
  x: 0,
  y: 0,
  scale: 1
};

// Origin of transform, set to mouse position or pinch center
var oX = window.innerWidth/2;
var oY = window.innerHeight/2;

var changeScale = function (scale) {
  // Limit the scale here if you want
  // Zoom and pan transform-origin equivalent
  var scaleD = scale / state.scale;
  var currentX = state.x;
  var currentY = state.y;
  // The magic
  var x = scaleD * (currentX - oX) + oX;
  var y = scaleD * (currentY - oY) + oY;

  state.scale = scale;
  state.x = x;
  state.y = y;

  var transform = "matrix("+scale+",0,0,"+scale+","+x+","+y+")";
  //var transform = "translate("+x+","+y+") scale("+scale+")"; //same
  view.setAttributeNS(null, "transform", transform);
};

Here it is working: http://forresto.github.io/dataflow-prototyping/react/

forresto
  • 12,078
  • 7
  • 45
  • 64
  • Your github link 404s – NuclearPeon Apr 03 '17 at 16:42
  • Hello @NuclearPeon, this is awesome, but I can't seem to implement it in my code. The SVG element i'd like it to apply to is already a variable called "mainGrid". I tried replacing the instance of "view" with "mainGrid" with no effect. What am I missing? Thanks! – sakeferret Jul 18 '19 at 21:37
  • @sakeferret the most obvious things to check for is that the `id` matches in the `getElementById` and the documents' `id="..."` attribute. It's hard to know for sure without seeing the code. If you don't want to post it as an SO question, you can try creating the simplest example using your attribute names until it works/you find the issue, then add the rest back in. – NuclearPeon Jul 21 '19 at 17:23
1

Setting the attribute (transform-origin="center") of the embedded element just inside the DOM did the trick for me

          <circle
          fill="#FFFFFF"
          cx="82"
          cy="81.625"
          r="81.5"
          transform-origin="center"
        ></circle>

Works well with using css transforms

unx
  • 74
  • 5
  • this doesn't work all the time. in some cases you'll need to add `transform-box: fill-box`. see this answer https://stackoverflow.com/a/62720107/4375751 – George Nov 11 '20 at 17:12
  • 1
    I am talking of using the attribute inside the DOM ((`transform-origin="center"`), not (`transform-origin: center`)). Tested it in all modern browsers and it worked without using fill-box – unx Nov 17 '20 at 15:51
0

You can build whatever you want around 0,0 origin, then put it into a group <g></g> and matrix-translate the whole group. Like that, the object will always rotate around 0,0 , but the whole group is moved (translated) elsewhere and translation matrix is applied after the rotation.

<g transform="matrix(1 0 0 1 160 200)">
  <polygon id="arrow-A" class="arrow" points="0,4 -90,0 0,-4 "/>
</g>


<style>

.arrow {
  transform: rotate(60deg);
}

/* OR */

#arrow-A {
  transform: rotate(60deg);
}

</style>

OR scripting: 

<script> 
  document.getElementById("arrow-A").setAttribute("transform", "rotate(60)");
</script>

This will create an arrow (eg. for a gauge), the broader end at [0,0] and move it to [160, 200]. Whichever rotation is applied to class "arrow", will rotate it around [160, 200].

Tested in: Chrome, Firefox, Opera, MS Edge

Paal
  • 9
  • 3
-1

I had a similar issue. But I was using D3 to position my elements, and wanted the transform and transition to be handled by CSS. This was my original code, which I got working in Chrome 65:

//...
this.layerGroups.selectAll('.dot')
  .data(data)
  .enter()
  .append('circle')
  .attr('transform-origin', (d,i)=> `${valueScale(d.value) * Math.sin( sliceSize * i)} 
                                     ${valueScale(d.value) * Math.cos( sliceSize * i + Math.PI)}`)
//... etc (set the cx, cy and r below) ...

This allowed me to set the cx,cy, and transform-origin values in javascript using the same data.

BUT this didn't work in Firefox! What I had to do was wrap the circle in the g tag and translate it using the same positioning formula from above. I then appended the circle in the g tag, and set its cx and cy values to 0. From there, transform: scale(2) would scale from the center as expected. The final code looked like this.

this.layerGroups.selectAll('.dot')
  .data(data)
  .enter()
  .append('g')
  .attrs({
    class: d => `dot ${d.metric}`,
    transform: (d,i) => `translate(${valueScale(d.value) * Math.sin( sliceSize * i)} ${valueScale(d.value) * Math.cos( sliceSize * i + Math.PI)})`
  })
  .append('circle')
  .attrs({
    r: this.opts.dotRadius,
    cx: 0,
    cy: 0,
  })

After making this change, I changed my CSS to target the circle instead of the .dot, to add the transform: scale(2). I didn't even need use transform-origin.

NOTES:

  1. I am using d3-selection-multi in the second example. This allows me to pass an object to .attrs instead of repeating .attr for every attribute.

  2. When using a string template literal, be aware of line-breaks as illustrated in the first example. This will include a newline in the output and may break your code.

Jamie S
  • 2,029
  • 2
  • 13
  • 19
  • 2
    Perhaps if you set transform-box: fill-box; Firefox would have worked the way you wanted it to. – Robert Longson Mar 21 '18 at 18:20
  • tried that. didn't seem to work. It **did** work after i changed the `svg:transform-origin.enabled` pref in the firefox's `about:config` page. But... that didn't seem like an adequate solution. I also found a discrepancy between `transform-origin: 50% 50%` and `transform-origin: center center`. – Jamie S Mar 21 '18 at 18:50
  • the svg.transform-origin.enabled pref was removed many versions ago. Unless you're using a very old version there should be no difference between 50% and center. Feel free to raise a bugzilla bug if there is a difference in Firefox 59 or later. – Robert Longson Mar 21 '18 at 20:58