Scaling A Centered Div In A Scrollable Container
Solution 1:
I used CSS transitions for the scaling animation.
#centered {
background-image: linear-gradient(to bottom,yellow,red);
height: 200px;
transform: scale(1);
transition: transform 1s ease-out;
width: 200px;
}
#scrollable {
align-items: center;
border: 1px solid red;
display: flex;
height: 300px;
justify-content: center;
overflow: auto;
width: 300px;
}
Update #1
- Using old school absolute centering (position absolute)
- I see your point about flexbox. It seems that flexbox centering has some limitations when the centered element is larger than its container.
Solution 2:
I am assuming that parts of the problem you are having can be summarized as:
- You have a complex structure in your inner
div
which you want to scale as a group. (If it were a single element, this would've been easy with a matrix). - When the inner
div
is scaled beyond the bounds of the container, you don't get scrollbars without controlling the width/height. - You want the inner
div
to remain centered and at the same time, scrollbars should reflect the correct position.
With this, there are two (three actually) easy options.
Option 1:
Using the same markup in the question, you could keep the div
centered when the scale factor is below 1. And for scale factors above 1, you change it to top-left. the overflow: auto
on the container will take care of the scroll on its own because the div
being scaled (via transform) is wrapped inside of another div
. You don't really need Javascript for this.
This solves your problems 1 and 2.
Fiddle 1: http://jsfiddle.net/abhitalks/c0okhznc/
Snippet 1:
* { box-sizing: border-box; }
#container {
position: relative;
border: 1px solid red; background-color: blue;
width: 400px; height: 300px;
overflow: auto;
}
.wrapper {
position: absolute;
background-color: transparent; border: 2px solid black;
width: 300px; height: 200px;
top: 50%; left: 50%;
transform-origin: top left;
transform: translate(-50%, -50%);
transition: all 1s;
}
.box {
width: 300px; height: 200px;
background: linear-gradient(to right, red, white);
border: 2px solid yellow;
}
.inner { width:50px; height: 50px; background-color: green; }
#s0:checked ~ div#container .wrapper { transform: scale(0.5) translate(-50%, -50%); }
#s1:checked ~ div#container .wrapper { transform: scale(0.75) translate(-50%, -50%); }
#s2:checked ~ div#container .wrapper { transform: scale(1) translate(-50%, -50%); }
#s3:checked ~ div#container .wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(1.5) translate(0%, 0%); }
#s4:checked ~ div#container .wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(2) ; }
#s5:checked ~ div#container .wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(3) ; }
<input id="s0" name="scale" data-scale="0.5" type="radio" />Scale 0.5
<input id="s1" name="scale" data-scale="0.75" type="radio" />Scale 0.75
<input id="s2" name="scale" data-scale="1" type="radio" checked />Scale 1
<input id="s3" name="scale" data-scale="1.5" type="radio" />Scale 1.5
<input id="s4" name="scale" data-scale="2" type="radio" />Scale 2
<input id="s5" name="scale" data-scale="3" type="radio" />Scale 3
<hr>
<div id="container">
<div id="wrapper" class="wrapper">
<div id="box" class="box">
<div class="inner"></div>
</div>
</div>
</div>
But this creates another problem. The overflow:auto
will cause a jump/flicker when the div
is scaled beyond the container. This can be easily solved by making it overflow:scroll
to show the scrollbars at all times (the way you are doing it already). Although, it works like a charm in Firefox, Chrome falters here and doesn't update the scrollbar position. The trick here is to use Javascript to force a reflow by changing the overflow
to auto
once your scaling completes. So you need to delay it a bit.
Fiddle 2: http://jsfiddle.net/abhitalks/u7sfef0b/
Snippet 2:
$("input").on("click", function() {
var scroll = 'scroll',
scale = +($(this).data("scale"));
if (scale > 1) { scroll = 'auto'; }
setTimeout(fixScroll, 300, scroll);
});
function fixScroll(scroll) { $("#container").css({ overflow: scroll }); }
* { box-sizing: border-box; }
#container {
position: relative;
border: 1px solid red; background-color: blue;
width: 400px; height: 300px;
overflow: scroll;
}
.wrapper {
position: absolute;
background-color: transparent; border: 2px solid black;
width: 300px; height: 200px;
top: 50%; left: 50%;
transform-origin: top left;
transform: translate(-50%, -50%);
transition: all 1s;
}
.box {
width: 300px; height: 200px;
background: linear-gradient(to right, red, white);
border: 2px solid yellow;
}
.inner { width:50px; height: 50px; background-color: green; }
#s0:checked ~ div#container .wrapper { transform: scale(0.5) translate(-50%, -50%); }
#s1:checked ~ div#container .wrapper { transform: scale(0.75) translate(-50%, -50%); }
#s2:checked ~ div#container .wrapper { transform: scale(1) translate(-50%, -50%); }
#s3:checked ~ div#container .wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(1.5) translate(0%, 0%); }
#s4:checked ~ div#container .wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(2) ; }
#s5:checked ~ div#container .wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(3) ; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="s0" name="scale" data-scale="0.5" type="radio" />Scale 0.5
<input id="s1" name="scale" data-scale="0.75" type="radio" />Scale 0.75
<input id="s2" name="scale" data-scale="1" type="radio" checked />Scale 1
<input id="s3" name="scale" data-scale="1.5" type="radio" />Scale 1.5
<input id="s4" name="scale" data-scale="2" type="radio" />Scale 2
<input id="s5" name="scale" data-scale="3" type="radio" />Scale 3
<hr>
<div id="container">
<div id="wrapper" class="wrapper">
<div id="box" class="box">
<div class="inner"></div>
</div>
</div>
</div>
Option 2:
In order to solve problem 3 of keeping the div
centered and at the same time maintaining the correct scrollbar position, you have to fallback on Javascript.
The principle remains the same that for scale factors above 1 you need to reset the top-left and translate
positions. You would also need to recalc the scaled width/height and then re-assign that to your wrapper div
. Then setting the scrollTop
and scrollLeft
on the container will be as easy as just getting the difference of the wrapper div
and the container div
.
This solves your problems 1, 2, and 3.
Fiddle 3: http://jsfiddle.net/abhitalks/rheo6o7p/1/
Snippet 3:
var $container = $("#container"),
$wrap = $("#wrapper"),
$elem = $("#box"),
originalWidth = 300, originalHeight = 200;
$("input").on("click", function() {
var factor = +(this.value);
scaler(factor);
});
function scaler(factor) {
var newWidth = originalWidth * factor,
newHeight = originalHeight * factor;
$wrap.width(newWidth); $wrap.height(newHeight);
if (factor > 1) {
$wrap.css({ left: 0, top: 0, transform: 'translate(0,0)' });
$elem.css({ transform: 'scale(' + factor + ')' });
setTimeout(setScroll, 400);
} else {
$elem.css({ transform: 'scale(' + factor + ') ' });
$wrap.css({ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' });
}
}
function setScroll() {
var horizontal, vertical;
horizontal = ($wrap.width() - $container.width()) / 2;
vertical = ($wrap.height() - $container.height()) / 2;
$container.stop().animate({scrollTop: vertical, scrollLeft: horizontal}, 500);
}
* { box-sizing: border-box; }
#container {
position: relative;
border: 1px solid red; background-color: blue;
width: 400px; height: 300px;
overflow: scroll;
}
.wrapper {
position: absolute;
background-color: transparent; border: 2px solid black;
width: 300px; height: 200px;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
transition: all 0.5s;
}
.box {
width: 300px; height: 200px;
background: linear-gradient(to right, red, white);
border: 2px solid yellow;
transform-origin: top left;
transition: all 0.5s;
}
.inner { width:50px; height: 50px; background-color: green; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<label for="slider1">Scale: </label>
<input id="slider1" type="range" min="0.5" max="3" value="1" step="0.25" list="datalist" onchange="scaleValue.value=value" />
<output for="slider1" id="scaleValue">1</output>
<datalist id="datalist">
<option>0.5</option>
<option>0.75</option>
<option>1</option>
<option>1.5</option>
<option>2</option>
<option>3</option>
</datalist>
<hr>
<div id="container">
<div id="wrapper" class="wrapper">
<div id="box" class="box">
<div class="inner"></div>
</div>
</div>
</div>
All scripts tested with IE-11, GC-43, and FF-38
.
Solution 3:
The hard part of this problem is that, in the middle of the zoom in process, you need to change from a model (where you have still margins) to another model where you need to expand the size of the container.
Since this happens in the middle of the transform, it's difficult to handle with a single property transform
The way that I have found to solve this is to change the min-height and min-width properties. This will give the posibility to auto detect this point and handle it gracefully
I am keeping in JS only the basic functionality, everything else is done in CSS
function setZoom3() {
var ele = document.getElementById("base");
ele.className = "zoom3";
}
function setZoom1() {
var ele = document.getElementById("base");
ele.className = "";
}
function setZoom05() {
var ele = document.getElementById("base");
ele.className = "zoom05";
}
.container {
width: 300px;
height: 300px;
overflow: scroll;
position: relative;
}
#base {
width: 300px;
height: 300px;
top: 0px;
left: 0px;
position: absolute;
min-width: 300px;
min-height: 300px;
transition: min-width 5s, min-height 5s;
}
.inner {
width: 200px;
height: 200px;
background-color: lightgreen;
background-image: linear-gradient(-45deg, red 5%, yellow 5%, green 95%, blue 95%);
top: 50%;
left: 50%;
position: absolute;
transform: translate(-50%, -50%);
transform-origin: top left;
transition: transform 5s;
}
#base.zoom3 {
min-width: 600px;
min-height: 600px;
}
.zoom3 .inner {
transform: scale(3) translate(-50%, -50%);
}
.zoom05 .inner {
transform: scale(0.5) translate(-50%, -50%);
}
.trick {
width: 20px;
height: 20px;
background-color: red;
position: absolute;
transform: translate(0px);
}
<div class="container">
<div id="base">
<div class="inner"></div>
</div>
</div>
<button onclick="setZoom3();">zoom 3</button>
<button onclick="setZoom1();">zoom 1</button>
<button onclick="setZoom05();">zoom 0.5</button>
The only issue left would be to set the scroll position, just in case you want it to keep in the center
Solution 4:
I've managed to solve it with a touch of logic. I've used GSAP for the animation and updates, but you could easily get it working with vanilla JS:
http://codepen.io/theprojectsomething/pen/JdZWLV
... Because of the need to scroll the parent div to keep the element centred (and no way to animate the scroll) you're not going to be able to get a smooth transition with CSS alone.
Note: Even without the scroll, a pure CSS transition seems to have trouble syncing (the offset, whether top/left, margin, or translate, is always catching up with the scale) ... this may be due to sub-pixel positioning? Someone else may be able to provide further insight here.
Full code from Codepen:
var $inner = document.querySelector('aside'),
$outer = document.querySelector('main'),
$anim = document.querySelector('[type="checkbox"]'),
$range = document.querySelector('[type="range"]'),
data = {
width: $outer.clientWidth,
value: 1
};
$range.addEventListener('input', slide, false);
function slide () {
$anim.checked ? animate(this.value) : transform(this.value);
}
function animate (value) {
TweenLite.to(data, 0.4, {
value: value,
onUpdate: transform
});
}
function transform (value) {
if( !isNaN(value) ) data.value = value;
var val = Math.sqrt(data.value),
offset = Math.max(1 - val, 0)*0.5,
scroll = (val - 1)*data.width*0.5;
TweenLite.set($inner, {
scale: val,
x: offset*100 + "%",
y: offset*100 + "%"
});
TweenLite.set($outer, {
scrollLeft: scroll,
scrollTop: scroll
});
}
window.onresize = function (){
data.width = $outer.clientWidth;
transform(data.value);
};
main, aside:before, footer {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
main {
width: 50vh;
height: 50vh;
min-width: 190px;
min-height: 190px;
background: black;
overflow: scroll;
box-sizing: border-box;
}
aside {
position: relative;
width: 100%;
height: 100%;
border: 1vh solid rgba(0, 0, 0, 0.5);
background-image: -webkit-linear-gradient(top, yellow, red);
background-image: linear-gradient(to bottom, yellow, red);
box-sizing: border-box;
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
aside:before {
content: "";
background: black;
border-radius: 50%;
width: 10%;
height: 10%;
display: block;
opacity: 0.5;
}
input {
display: block;
margin: 3em auto;
}
input:after {
content: attr(name);
color: white;
text-transform: uppercase;
position: relative;
left: 2em;
display: block;
line-height: 0.9em;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.16.1/TweenMax.min.js"></script>
<main>
<aside></aside>
</main>
<footer>
<input type="checkbox" name="animate" checked>
<input type="range" min="0.1" step="0.005" max="3">
</footer>
Solution 5:
Matthew King's fiddle was close to being correct.
The only thing that needs fixing is the div's offset. If your inner div is exceeding the boundaries of the scrollable div (negative offset) you need to set the offset to 0. Else you want the div to be centered.
Centering is however not that trivial. You are zooming the div's background with css, hence you are not affecting the actual width and height of your div and need to parse the scale matrix to calculate it's backgrounds dimension.
This solution works for me: https://jsfiddle.net/6e2g6vzt/11/
(at least for FF on Ubuntu, did not test on other browsers yet)
It is basically just one function to set the new offset, you don't even have to call it manually due to this line of code
$("#centered").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", setNewOffset);
which calls the setNewOffset
function after the transformation is completed.
As you can see the image 'jumps' to it's correct position after the transformation, maybe you want to add a smooth effect to cover that but i just wanted to show you how to get the correct offset.
Have a look at jQuery's documentation to learn more about .offset()
Credits:
Thanks to Jim Jeffers for the nice callback after transistions
https://stackoverflow.com/a/9255507/3586288
Thanks to Lea Verou for the great regex to parse the scale matrix
https://stackoverflow.com/a/5604199/3586288
Post a Comment for "Scaling A Centered Div In A Scrollable Container"