Lynel Hudson
Lynel Hudson

Reputation: 2405

Move transform-origin back to the center of the element in CSS

When CSS elements are transformed out of their original location the transform-origin doesn't move with them; making all subsequent transforms and rotations still originate from it's original transform-origin ( example below ).

There is one way around this that I know of... and that's to keep adding all new transforms to the right of the previous ones. like this: transform: translateY ( -5rem ) rotate( 45deg ) translateY( -10rem ) rotate( 15deg )... etc. This seems to always start the new transforms from the center of the current element as desired.

the problem

When you are transforming an element based on user input using this technique you will keep adding transforms to the DOM...endlessly. Taking up a lot of memory and causing other unwanted sliding effects ( possibly ).

Here is an animation showing how the transform-origin doesn't move to the center of the element after each transform:

'use strict';

function firstRotation() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'First, the V is rotated around it\'s central axis.\
                          The <b>transform-origin</b> is centered.';
    
    v.classList.add( 'first-rotation' );
    status.classList.add( 'update' );
  }, 1000 ); 
}

function firstTranslation() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'Next, the element is translated forward in it\'s \
                        current orientation. The <b>transform-origin</b> stays\
                        behind where it was.';
                        
    v.classList.remove( 'first-rotation' );
    v.classList.add( 'first-translation' );
  }, 6000 ); 
}

function info() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'This next animation is where it\'s evident that\
                        the <b>transform-origin</b> is no longer centered, but\
                        back where it was at the beginning of these transforms';
    
  }, 11000 ); 
}

function lastRotation() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'This last rotation is far wider than desired because the\
                        transform origin is back where it started.'
  
    v.classList.remove( 'first-translation' );
    v.classList.add( 'last-rotation' );
  }, 16000 ); 
}

function end() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.classList.remove( 'update' );
  }, 21000 ); 
}

function start() {
  firstRotation();
  firstTranslation();
  info();
  lastRotation();
  end();
}

start();
/* / / / / / / / / / / / / / ANIMATION DEFINITIONS / / / / / / / / / / / / / */
.first-rotation, .first-translation, .update, .last-rotation {
  animation-duration: 5s;
  animation-timing-function: ease-in-out;
  animation-fill-mode: forwards;
}
.first-rotation {
  animation-name: first-rotation;
}
.first-translation {
  animation-name: first-translation;
}
.update {
  animation-name: update;
  animation-iteration-count: infinite;
}
.last-rotation {
  animation-name: last-rotation;
}

/*/ / / / / / / / / / / / / / ANIMATION KEYFRAMES / / / / / / / / / / / / / /*/
@keyframes first-rotation {
  100% {
    transform: rotate( 315deg );
  }
}
@keyframes first-translation {
  0% {
    transform: rotate( 315deg );
  }
  100% {
    transform: rotate( 315deg ) translate( 0, -5rem );
  }
}
@keyframes update {
  0% {
    background-color: mediumslateblue;
  }
}
@keyframes last-rotation {
  0% {
    transform: rotate( 315deg ) translate( 0, -5rem );
  }
  100% {
    transform: rotate( 400deg ) translate( 0, -5rem );
  }
}
<head>
  <style>
    @import url( "https://fonts.googleapis.com/css?family=Nunito" );

    html {
      overflow: hidden;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100%;
      font-family: "Nunito", sans-serif;
    }

    html,
    body {
      margin: 0;
      padding: 0;
    }

    .v {
      display: block;
      font-size: 2rem;
      transform: rotate( 180deg );
    }

    p {
      width: 100%;
      padding: 0.5rem;
      position: absolute;
      bottom: 0;
      left: 0;
    }
  </style>
</head>
<body>
  <div id="v-wrp" class="v-wrp">
    <b class="v">V</b>
  </div>
  
  <p id="status" class="status"></p>
</body>

the question

I need to find a way in CSS or JS to reset the position of the transform-origin or move it back to the center of the transformed element. Adding more transforms to the right is one technique that isn't working for me in a real time interactive environment. An attempt can be seen here: Transforms are added...endlessly.

How can I either calculate the location of a transformed element and move the transform-origin back to it's center OR how can I take multiple transform values and condense them into one value that keeps the element in it's same place?

Upvotes: 16

Views: 3784

Answers (4)

Manstie
Manstie

Reputation: 465

You will be able to make use of JavaScript's DOMMatrix or your own values to keep track of the translation and rotation values.

Here is a very basic example like your asteroids game: https://jsfiddle.net/omjktrsh/

The matrix provides you with functions to manipulate it without having to learn all the math. The 2D variant has six properties which are part of a 3x3 matrix:

a c e
b d f
0 0 1

where:

  • a = x axis scale
  • b = y axis shear
  • c = x axis shear
  • d = y axis scale
  • e = x axis translate
  • f = y axis translate

You can use rotateSelf(deg) to have the matrix perform a rotation on its current values, or rotate(deg) to have the matrix return a new matrix with the added rotation (leaving the original matrix with the same values).

You can use translateSelf(x, y) to move it around, relative to its current rotation. If you want to move it around not relative to its current rotation, you can manipulate the e and f values directly like so:

matrix.e += x; // x axis translation amount
matrix.f += y; // y axis translation amount

You can easily apply as many rotations as you want without needing to stack them in CSS, and then use toString() to output the matrix in its CSS form:

const matrix = new DOMMatrix();
const el = document.getElementById("transform-me");
// manipulate matrix as much as you want
...
matrix.translateSelf(...);
...
matrix.rotateSelf(...);
...
// apply matrix to elements css -> matrix(a, b, c, d, e, f)
el.style.transform = matrix.toString();

The drawback is DOMMatrix has less browser support: https://caniuse.com/dommatrix

If you would like to support browsers that do not support DOMMatrix, you can still use your own maths and the transform: matrix(a, b, c, d, e, f) syntax in these browsers: https://caniuse.com/transforms2d

If you would like to know the maths behind 2d transformation matrices, there are examples on Wikipedia. Note that some resources use a flipped matrix to the one above:

a b 0
c d 0
e f 1

Upvotes: 1

creativearish
creativearish

Reputation: 11

If anyone is still looking for something like this.

@jmcgrory is right, it would be really convenient to have separate layers. But for certain scenario following will work.

If you can use svg then transform-box may help.

But if you are using any other element than it will be difficult as reference box (can be considered as initial position of element) does not change after applying transform, and transform-origin will be treated according to its reference box. transform-origin needs to be moved along with translate, it gets more complicated with transform: rotate as you will have to calculate the x and y length of movement of an element and based on that you will need to replace transform (if used for any one axis) to x and y and update the transform origin accordingly, this method is very tricky but it works well and depends totally on mathematical calculations of movement of element (you won't need to write too much css but will have to do some calculations).

I've copied the snippet from question and make it work as expected. Please check the below code for reference.

'use strict';

function firstRotation() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'First, the V is rotated around it\'s central axis.\
                          The <b>transform-origin</b> is centered.';
    
    v.classList.add( 'first-rotation' );
    status.classList.add( 'update' );
  }, 1000 ); 
}

function firstTranslation() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'Next, the element is translated forward in it\'s \
                        current orientation. The <b>transform-origin</b> stays\
                        behind where it was.';
                        
    v.classList.remove( 'first-rotation' );
    v.classList.add( 'first-translation' );
  }, 6000 ); 
}

function info() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'This next animation is where it\'s evident that\
                        the <b>transform-origin</b> is no longer centered, but\
                        back where it was at the beginning of these transforms';
    
  }, 11000 ); 
}

function lastRotation() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.innerHTML = 'This last rotation is working correctly with calculated transform-origin.'
  
    v.classList.remove( 'first-translation' );
    v.classList.add( 'last-rotation' );
  }, 16000 ); 
}

function end() { 
  var v = document.getElementById( 'v-wrp' ),
      status = document.getElementById( 'status' )
      
  setTimeout( function() { 
    status.classList.remove( 'update' );
  }, 21000 ); 
}

function start() {
  firstRotation();
  firstTranslation();
  info();
  lastRotation();
  end();
}

start();
/* / / / / / / / / / / / / / ANIMATION DEFINITIONS / / / / / / / / / / / / / */
.first-rotation, .first-translation, .update, .last-rotation {
  animation-duration: 5s;
  animation-timing-function: ease-in-out;
  animation-fill-mode: forwards;
}
.first-rotation {
  animation-name: first-rotation;
}
.first-translation {
  animation-name: first-translation;
}
.update {
  animation-name: update;
  animation-iteration-count: infinite;
}
.last-rotation {
  animation-name: last-rotation;
}

/*/ / / / / / / / / / / / / / ANIMATION KEYFRAMES / / / / / / / / / / / / / /*/
@keyframes first-rotation {
  100% {
    transform: rotate( 315deg );
  }
}
@keyframes first-translation {
  0% {
    transform: rotate( 315deg );
  }
  100% {
    transform-origin: calc(-3.536em + 10px) calc(-3.536em + 22px);
    transform: rotate( 315deg ) translate( -3.536rem, -3.536rem );
  }
}
@keyframes update {
  0% {
    background-color: mediumslateblue;
  }
}
@keyframes last-rotation {
  0% {
    transform-origin: calc(-3.536em + 10px) calc(-3.536em + 22px);
    transform: rotate( 315deg ) translate( -3.536rem, -3.536rem );
  }
  100% {
    transform-origin: calc(-3.536em + 10px) calc(-3.536em + 22px);
    transform: rotate( 400deg ) translate( -3.536rem, -3.536rem );
  }
}
<head>
  <style>
    @import url( "https://fonts.googleapis.com/css?family=Nunito" );

    html {
      overflow: hidden;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100%;
      font-family: "Nunito", sans-serif;
    }

    html,
    body {
      margin: 0;
      padding: 0;
    }

    .v {
      display: block;
      font-size: 2rem;
      transform: rotate( 180deg );
    }

    p {
      width: 100%;
      padding: 0.5rem;
      position: absolute;
      bottom: 0;
      left: 0;
    }
  </style>
</head>
<body>
  <div id="v-wrp" class="v-wrp">
    <b class="v">V</b>
  </div>
  
  <p id="status" class="status"></p>
</body>

Upvotes: 0

jmcgrory
jmcgrory

Reputation: 843

Your issue is a structural one. Keep your point of reference (transform-origin) as is and separate your rotate and translate transforms by layer (a containing element that is translated from it's origin and an interior containing element that rotates as you wish).

Structurally separating these elements of transform will prevent "cross-origin" issues such as these from the outset.

Upvotes: 0

user9077625
user9077625

Reputation:

You can also use top and left to translate the #v-wrp. And you can create an animation in the transform-origin property. Try it in the <b> element. I hope it will be working right now.

Upvotes: 0

Related Questions