ChatGPT解决这个技术问题 Extra ChatGPT

How to set transform origin in SVG

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?

FWIW, supposedly fixed as of Firefox 19 beta 3, though I'm still having problems in Firefox 22. Mozilla bugzilla listing: bugzilla.mozilla.org/show_bug.cgi?id=828286
@CTheDark, could you reconsider the accepted answer for this please? We now have a more modern solution: stackoverflow.com/a/62720107/4375751

P
Persijn

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.


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(cxsx, cysy) scale(sx, sy)". The same result.
Wouldn't it be transform="matrix(sx, 0, 0, sy, cx-sxcx, cy-sycy)"? Because you want to only translate by the difference. I think this logic is correct, but it still gives me off positions...
Now it works! I was just using wrong cx and cy values! THanks a lot!
@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.
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.
G
George
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


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!
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:
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'.
Hey SimplSam, I have updated the answer to be more friendly towards the past
This is the best answer
H
Hafenkranich

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.


Wow. Would +10 if I could. This saved my day! Thanks.
How to do this and just this in JavaScript?
Like element.setAttribute('transform-origin', '25px 25px') ?
f
forresto

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/


Your github link 404s
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 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.
c
cmititiuc

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)

P
Paal

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.

OR scripting:

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


J
Jamie S

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:

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. 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.


Perhaps if you set transform-box: fill-box; Firefox would have worked the way you wanted it to.
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.
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.
u
unx

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


this doesn't work all the time. in some cases you'll need to add transform-box: fill-box. see this answer stackoverflow.com/a/62720107/4375751
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