CSS Transform with element resizing

CssCss Transforms

Css Problem Overview


I found this: https://stackoverflow.com/questions/7565542/width-height-after-transform

and several others, but nothing is not quite what I'm looking for. What I want is to scale something to 50% of it's size (with nice animated transition of course) and have the page layout re-adjust to the new (visual) size of the element. What seems to happen by default is that the element still retains it's original size in the layout structure, and is merely drawn with the associated transformations.

Basically I want the user to click on a block of text (or other content), and then scale that text to 50% (or whatever) of it's size and stick it in a panel below to indicate that it's been selected. I don't want 50% whitespace around the element after I move it.

I've tried re-setting the height/width, but that causes the element iself to undergo a new layout and then the scale is applied to the new layout and this still leaves me with 50% white space after all is finished. My mozilla specific (for simplicity) code looks like this:

<script type="text/javascript">
 function zap(e) {

  var height = e.parentNode.offsetHeight/2 + 'px';
  var width = e.parentNode.offsetWidth/2 + 'px';
  e.parentNode.style.MozTransform='scale(0.0)'; 
  e.parentNode.addEventListener(
    'transitionend', 
    function() { 
      var selectionsArea = document.getElementById("selected");
      selectionsArea.appendChild(e.parentNode);
      e.parentNode.style.MozTransform='scale(0.5,0.5)'; 
      e.parentNode.style.display = 'inline-block';
      e.parentNode.style.height = height;
      e.parentNode.style.width = width;
    },
    true) 
}
</script>

I'm really hoping I don't have to go into messing with negative margins or relative positioning to achieve this...

Edit: Since writing this I found a very similar question with neither comments or answers. It differs slightly as I don't care about it being an html5 specific solution, I suspect the solution either doesn't exist or lies in CSS/javascript regardless of the html level.

https://stackoverflow.com/questions/10627306/scale-zoom-a-dom-element-and-the-space-it-occupies-using-css3-transform-scale

Edit 2: (another non-working attempt)

I've also tried this, but the scale and the translate functions seem to interact in a manner that makes the final position impossible to predict... and scale before transform doesn't appear to be the same as transform then scale. Unfortunately it's not just a matter of adjusting the translate by the scale factor ...

function posX(e) {
	return e.offsetLeft + (e.offsetParent ? posX(e.offsetParent) : 0)
}

function posY(e) {
	return e.offsetTop + (e.offsetParent ? posY(e.offsetParent) : 0)
}

function placeThumbNail(e, container, x, y, scale) {
	var contX = posX(container);
	var contY = posY(container);
	var eX = posX(e);
	var eY = posY(e);
	var eW = e.offsetWidth;
	var eH = e.offsetHeight;

	var margin = 10;
	var dx = ((contX - eX) + margin + (x * eW * scale));
	var dy = ((contY - eY) + margin + (y * eH * scale));

	// assumes identical objects     
	var trans = 'translate(' + dx + 'px, ' + dy + 'px) ';
	var scale = 'scale(' + scale + ',' + scale + ') ';
	e.style.MozTransform =  trans + scale ; 
}

Even if it worked, the above would still require me to first move the element to the bottom of the page before running this code (to get rid of the whitespace), and furthermore I would need to recalculate the transform on re-size... so I wasn't too happy with this attempt to start with anyway.

Also note that if you use scale + trans vs trans + scale above the result is drastically different.

Edit 3: I figured out the placement issues... the transforms are applied in the order specified, and scale(0.5,0.5) essentially changes the size of a pixel to half what it was originally (probably hinting at how they implemented it internally). If I put translate is first, translating slightly less/more to account for the change in the position of the upper left corner caused by scale will do the trick, but managing the location of the objects and adjusting the transforms when the dom changes in a reasonable manner is still eluding me. This sort of feels wrong because I am drifting in the direction of writing my own layout manager in javascript just to get this effect.

Css Solutions


Solution 1 - Css

The problem I noticed is that when element scales, browser change its pixels ratio, not pixels amount. Element is smaller but it doesn't change its actual pixel size in DOM. Because of that I don't think that CSS-only solution exist.

I put scalable element into container which keeps the same pixel ratio as rest of elements. Using Java Script I simply change container's size. Everything is still based on CSS3 transform: scale. Maybe JS code could be simplier, but now it's all about the idea (just a proof of concept);) Fiddle with two examples: http://jsfiddle.net/qA5Tb/9/

HTML:

<div class="overall-scalable">
    <div class="scalable" scalex='0.5' scaley='0.5'>
        Nunc et nisi ante. Integer in blandit nisi. Nulla facilisi. Vestibulum vulputate sapien eget mauris elementum sollicitudin. Nullam id lobortis dolor. Nulla vitae nibh vitae sem volutpat pretium. Nunc et nisi ante. Integer in blandit nisi. Nulla facilisi. Vestibulum vulputate sapien eget mauris elementum sollicitudin. Nullam id lobortis dolor. Nulla vitae nibh vitae sem volutpat pretium.
    </div>
</div>

CSS:

.overall-scalable {width: 350px; height: 150px; overflow: hidden; -webkit-transition: all 1s;}
.scalable {color: #666; width: 350px; height: 150px; -webkit-transform-origin: top left; -webkit-transition: all 1s;}

JS:

$('button').click(function() {
    $('.scalable').each(function(){
        rescale($(this));
    })
});

function rescale(elem) {
    
    var height = parseInt(elem.css('height'));
    var width = parseInt(elem.css('width'));
    var scalex = parseFloat(elem.attr('scalex'));
    var scaley = parseFloat(elem.attr('scaley'));
    
    if (!elem.hasClass('rescaled')){
        var ratioX = scalex;
        var ratioY = scaley;
    }else{          
        var ratioX = 1;
        var ratioY = 1;
    }
    
    elem.toggleClass('rescaled');
    elem.css('-webkit-transform', 'scale('+ratioX +', '+ratioY+')');        
    elem.parent().css('width', parseInt(width*ratioX) + 'px');
    elem.parent().css('height', parseInt(height*ratioY) + 'px');
}​

Solution 2 - Css

I finally figured out a simple solution and it is much similar than where I started from.

I will admit, it took many attempts to figure this out (over the last year or so) to finally figure it out. I was actually looking for an answer to this question. I have had problems with this before. Anyways, I started working this out again and I did find a simple solution.

Anyways, the trick was to use the margin bottom together with the CSS calc script.

Also, you must have to have absolute height declared for the the object (div). I dont think this will work with if the div can expand, otherwise, after many attempts and hours on this I finally have a very simple, clean, working solution.

So, for example, height = 300px (not 30%, or no height). I would even set overflow-y = hidden if you can. If you want the div to grow (not have an absolute height), I believe you will need JavaScript. I did not try it. Otherwise this is working correctly in all tests.

I am sure this will work nice with SASS or LESS since you could declare some of the repeated things using variables in the CSS. I have not tried that yet.

Some important comments on my solution:

  1. Notice I am using the height of the object and a CSS calc method together with the transform size to figure out the bottom margin.
  2. I color coded my work to help show what is happening.
  3. the wrap div does have padding are there only to show it is working. They are absolute and it does not affect the internal object.
  4. If you were building a responsive page, like I am, you can use media queries vs classes to get the same affect.
  5. I am using transform-origin: 50% 0; This code ended up not being as important as I originally thought it would be. I am using the bottom margin to resolve the problems I had had.
  6. In this example, I am just centering the div in the parent div. If you need to place the div, it is a little more complicated, but a similar solution will work. I did not attempt it.
  7. You could use this same solution with jQuery vs CSS. The idea is the same. You would search for all elements with the transform in the page and add the CSS as needed.

I have also have set up a Codepen to test this with and for sharing.

https://codepen.io/cyansmiles/details/yvGaVP

.box-wrap {
  display: block;
  position: relative;
  box-sizing: border-box;
  width: 100%;
  margin: auto;
  text-align: center;
  margin: 30px 10px;
  padding: 40px;
  background: yellow;
}

.box-wrap:before {
  content: "";
  display: block;
  box-sizing: border-box;
  position: absolute;
  background: rgba(0, 0, 0, 0.5);
  width: calc(100% - 80px);
  height: calc(100% - 80px);
  top: 40px;
  left: 40px;
}

.box {
  display: inline-block;
  box-sizing: border-box;
  position: relative;
  transform-origin: 50% 0;
  width: 300px;
  height: 150px;
  background: red;
}

.box.size-80 {
  transform: scale(0.8);
  margin: 0 auto calc(-150px * (1 - 0.8));
}

.box.size-70 {
  transform: scale(0.7);
  margin: 0 auto calc(-150px * (1 - 0.7));
}

.box.size-60 {
  transform: scale(0.6);
  margin: 0 auto calc(-150px * (1 - 0.6));
}

.box.size-50 {
  transform: scale(0.5);
  margin: 0 auto calc(-150px * (1 - 0.5));
}

//etc

<div class="box-wrap">
  <div class="box">
    <h1>Box at 100%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-80">
    <h1>Box at 80%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-70">
    <h1>Box at 70%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-60">
    <h1>Box at 60%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-50">
    <h1>Box at 50%</h1>
  </div>
</div>

Solution 3 - Css

I had to tweak my code a little from above.

It now works on all sides (not just up and down). I see a little edit, I think that is CSS issue (you could add another pixel to code to resolve this).

Here is the working Codepen link. Feel free to let me know if you use it. It felt good to finally figure this out.

https://codepen.io/cyansmiles/pen/bLOqZo

ps. I am forking my codepen to add a moveable CSS.

.box-wrap {
  display: block;
  position: relative;
  box-sizing: border-box;
  width: 100%;
  margin: auto;
  text-align: center;
  margin: 30px 10px;
  padding: 40px;
  background: yellow;
}

.box-wrap:before {
  content: "";
  display: block;
  box-sizing: border-box;
  position: absolute;
  background: rgba(0, 0, 0, 0.5);
  width: calc(100% - 80px);
  height: calc(100% - 80px);
  top: 40px;
  left: 40px;
}

.box {
  display: inline-block;
  box-sizing: border-box;
  position: relative;
  transform-origin: 50% 0;
  width: 300px;
  height: 150px;
  background: red;
  color: #fff;
}

.box.size-80 {
  background: red;
  transform: scale(0.8);
  margin: 0 calc((-300px * (1 - 0.8)) / 2) calc(-150px * (1 - 0.8));
}

.box.size-70 {
  background: blue;
  transform: scale(0.7);
  margin: 0 calc((-300px * (1 - 0.7)) / 2) calc(-150px * (1 - 0.7));
}

.box.size-60 {
  background: green;
  transform: scale(0.6);
  margin: 0 calc((-300px * (1 - 0.6)) / 2) calc(-150px * (1 - 0.6));
}

.box.size-50 {
  background: orange;
  transform: scale(0.5);
  margin: 0 calc((-300px * (1 - 0.5)) / 2) calc(-150px * (1 - 0.5));
}

//etc

<div class="box-wrap">
  <h1>This is a bunch of boxes!</h1>
  <div class="box size-50">
    <h1>Box at 50%</h1>
  </div>
  <div class="box size-60">
    <h1>Box at 60%</h1>
  </div>
  <div class="box size-80">
    <h1>Box at 80%</h1>
  </div>
  <div class="box size-70">
    <h1>Box at 70%</h1>
  </div>
  <div class="box size-50">
    <h1>Box at 50%</h1>
  </div>
  <div class="box size-60">
    <h1>Box at 60%</h1>
  </div>
  <div class="box size-50">
    <h1>Box at 50%</h1>
  </div>
  <div class="box size-60">
    <h1>Box at 60%</h1>
  </div>
  <div class="box size-80">
    <h1>Box at 80%</h1>
  </div>
  <div class="box size-70">
    <h1>Box at 70%</h1>
  </div>
  <div class="box size-50">
    <h1>Box at 50%</h1>
  </div>
  <div class="box size-60">
    <h1>Box at 60%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box">
    <h1>Box at 100%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-80">
    <h1>Box at 80%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-70">
    <h1>Box at 70%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-60">
    <h1>Box at 60%</h1>
  </div>
</div>

<div class="box-wrap">
  <div class="box size-50">
    <h1>Box at 50%</h1>
  </div>
</div>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGusView Question on Stackoverflow
Solution 1 - CssSófkaView Answer on Stackoverflow
Solution 2 - CssNathan SharfiView Answer on Stackoverflow
Solution 3 - CssNathan SharfiView Answer on Stackoverflow