8

I have an image that changes its height based on an overview/detail state. It needs to be a background-image with background-size: cover to ensure that the container is covered at all times.

As the transition needs to be performant, I see no other way than using transform: scaleY(0.5) on a wrapper-element and a corresponding transform transform: scaleY(2) on the inner element, that is cancelling out the scale effect.

At the beginning and the end, the math is correct. The transition looks wrong, though. I've prepared a small fiddle to illustrate what is going wrong:

Codepen - Fiddle

As you can see, the transitions, even though linear, don't exactly cancel out each other. I suppose there is an underlying mathematical problem, but I can't seem to find a solution to it.

nirazul
  • 3,928
  • 4
  • 26
  • 46
  • You are multiplying two linear functions, so it's expected that the result is a parabola. – Oriol Jul 13 '16 at 22:14

3 Answers3

4

edit: went ahead and made an example codepen for the bezier curve.

http://codepen.io/anon/pen/jAYOBO <-- (old)

Now with curve for transition back to normal element state:

http://codepen.io/anon/pen/oLpLLG <-- (new)

The issue you're facing is with the relativity of these operations. You'll have to give different values for the transitions in play. You have the math logic right regarding the transforms, however, the transitions you have that are modifying these characteristics aren't adjusted to maintain the same animation with their own respectively different values- the transition values are the same.

What this means is that even though you have a perfect 2:1 scale between the two objects being transitioned, their rate of change is inherently different at different steps in the process- regardless of their destination/end result. You need to adjust the transition settings if you want them equalized in appearance.

Still Confused? In other words....

Let's assume that you have 2 div boxes in separate containers that don't affect each other. Here's some CSS to convey their relevant properties:

div.a{
width:100px;
height:100px;
}

div.b{
width:200px;
height:200px;
}

We could throw a transition on these that's exactly the same: linear, 10s, and doubling their respective sizes with scale(). (div.a would be 200px in height/width and div.b would be 400px in height/width)

Now, while it may seem unrelated to your circumstances and obvious that these 2 would not 'grow' at the same rate - (different starts, different destinations [that don't equally negate the initial start differences], with same duration) - this is the same issue that you're coming across.

To rectify the problem and get your animations to behave in the manner you're expecting, you'll have to modify the timing functions of your transition.

To demonstrate that the phenomenon that I'm describing is indeed actually happening, I've gone ahead and changed your transition type for one of the transforming elements to one that eases and forked it on the codepen link below. Play with it, and you'll get what I'm talking about. If you need more info, comment on my answer!

http://codepen.io/anon/pen/qNVgBB

Helpful Link: http://cubic-bezier.com/

Further Reading: Quadratic Bezier Curve: Calculate Point

(Also, I'm sure that the math stack exchange folks would love to chew this stuff up for you. I'm almost positive they can give you a much clearer breakdown of how the curves work.) (Ugh, this is messy as heck now. I'll clean this up and reformat later)

Community
  • 1
  • 1
MassDebates
  • 917
  • 6
  • 10
  • Thank you very much for you explanation. This means, that I was on the right track :) Do you have any idea how I could calculate a cubic-bezier function that would cancel out the outer one? I'll keep the trial and error method as a last resort. – nirazul Jul 13 '16 at 18:43
  • I think that there are inherent issues with the structure you've got; it sounds like you might have an easier time with `transition`ing other properties that have to do with the `box model`, like `width` and `height`. If you'd still like to proceed with this, though, I'm sure I can come up with an example bezier curve that will help you in your `scaleY()` cancellation endeavors! I'll edit the answer and throw a codepen on top when I'm done. – MassDebates Jul 14 '16 at 05:25
  • Awesome! This is definitely worth accepting as an answer. Can I ask you how you ended up with the numbers in the curve function? I'd like to take this further for random scale factors and am not perfectly sure, what the formula would be. – nirazul Jul 14 '16 at 07:06
  • I apologize, but I won't be able to provide you a formula for this sort of thing. Not only is it affected by the context, but it would be more suitably taught in a classroom setting. Maybe Comments are difficult to use for this sort of thing. I'll add some further reading links, a tool that helps visualize curves, and the reverse to your codepen, too, so you can learn more from playing with it. Additionally, feel free to get a hold of me if you need help with curves in the future. – MassDebates Jul 14 '16 at 15:32
  • Thank you anyway, all help is greatly appreciated! – nirazul Jul 14 '16 at 15:49
  • If you haven't already, I think you may get the information you're looking for by checking out the Further reading link I've added to my answer post. Also, playing with that tool I've provided will also help clarify fuzzy details. – MassDebates Jul 14 '16 at 16:13
4

The outer element is interpolated between scaleY(1) and scaleY(0.5).

At time t, the transformation will be scaleY(1-t/2)

      t = 0                    t                     t = 1s
┌               ┐      ┌               ┐       ┌               ┐
│ 1   0   0   0 │      │ 1   0   0   0 │       │ 1   0   0   0 │
│ 0   1   0   0 │      │ 0 1-t/2 0   0 │       │ 0  1/2  0   0 │
│ 0   0   1   0 │      │ 0   0   1   0 │       │ 0   0   1   0 │
│ 0   0   0   1 │      │ 0   0   0   1 │       │ 0   0   0   1 │
└               ┘      └               ┘       └               ┘

The inner element is interpolated between scaleY(1) and scaleY(2).

At time t, the transformation will be scaleY(1+t)

      t = 0                    t                     t = 1s
┌               ┐      ┌               ┐       ┌               ┐
│ 1   0   0   0 │      │ 1   0   0   0 │       │ 1   0   0   0 │
│ 0   1   0   0 │      │ 0  1+t  0   0 │       │ 0   2   0   0 │
│ 0   0   1   0 │      │ 0   0   1   0 │       │ 0   0   1   0 │
│ 0   0   0   1 │      │ 0   0   0   1 │       │ 0   0   0   1 │
└               ┘      └               ┘       └               ┘

However, that's relatively to the outer. In absolute terms, the matrices are multiplied:

      t = 0                    t                     t = 1s
┌               ┐   ┌                      ┐   ┌               ┐
│ 1   0   0   0 │   │ 1     0      0     0 │   │ 1   0   0   0 │
│ 0  1*1  0   0 │   │ 0 1+t/2-t²/2 0     0 │   │ 0  2/2  0   0 │
│ 0   0   1   0 │   │ 0     0      1     0 │   │ 0   0   1   0 │
│ 0   0   0   1 │   │ 0     0      0     1 │   │ 0   0   0   1 │
└               ┘   └                      ┘   └               ┘

Then, yes, the start and end points correspond to the identity matrix.

But in between you have the parabola scaleY(1+t/2-t²/2).

enter image description here

It might be possible to achieve the desired effect with bezier curves.

Let f(t) and g(t) be the timing functions of .outer and .inner, respectively.

By definition, f(0) = g(0) = 0 and f(1) = g(1) = 1.

The scale of the outer will be given by

( 1-f(t)/2 ) ( 1+g(t) ) = 1 + g(t) - f(t)/2 - f(t)g(t)/2

We want that to be 1, so

f(t) = 2 g(t) / (1+g(t))
f'(t) = 2 g'(t) / (1+g(t))^2
f'(0) = 2 g'(0)
f'(1) = g'(1) / 2

That is, the starting slope of the outer must be the double than the inner, and viceversa for the ending slopes.

Choosing f(t) = t (linear) and g the bezier curve given by (.3, 0.15), (.7, .4) seems to produce good results. Note g'(0) = 2 = 2 f'(0) and g'(1) = 1/2 = 1/2 f'(0).

.outer, .inner, .inner-expectation {
  transition: transform 2s;
  height: 400px;
}
.outer, .inner {
  transition-timing-function: linear;
}
.inner {
  transition-timing-function: cubic-bezier(.3, 0.15, .7, .4);
}
.inner {
  background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/f/f4/Souq_Waqif%2C_Doha%2C_Catar%2C_2013-08-05%2C_DD_107.JPG/1920px-Souq_Waqif%2C_Doha%2C_Catar%2C_2013-08-05%2C_DD_107.JPG);
  background-size: cover;
}
a:hover ~ .outer {
  transform: scaleY(0.5);
}
a:hover ~ .outer .inner {
  transform: scaleY(2);
}
* {
  box-sizing: border-box;
}
a {
  display: block;
  position: absolute;
  top: 0;
  left: 200px;
  padding: 20px;
  background: wheat;
}
.text {
  position: absolute;
  top: 0;
  height: 100%;
}
.outer {
  background: #fcf8b3;
  position: absolute;
  top: 80px;
  left: 200px;
  width: 400px;
  height: 400px;
}
.inner, .inner-expectation {
  position: absolute;
  width: 300px;
  top: 0;
  left: 100px;
}
.inner .text, .inner-expectation .text {
  right: 0;
}
.inner-expectation {
  width: 20px;
  top: 80px;
  left: 610px;
  background: rgba(255, 0, 0, 0.5);
}
<a href="#">hover me</a>
<div class="outer">
  <div class="text">outer</div>
  <div class="inner">
    <div class="text">inner</div>
  </div>
</div>
<div class="inner-expectation"></div>

The problem is when hover ends, then the effect breaks. But this can't be solved perfectly.

When reversing, the scale of the outer will be (1+f(t))/2, and the scale of the inner 2-g(t) (relatively to the outer).

In absolute terms, the scale of the inner will be

(1+f(t))/2 * (2-g(t)) = 1 - g(t)/2  + f(t) - f(t)g(t)/2

And we want that to be 1. Then, f(t) = g(t) / ( 2-g(t) ).

But we already had f(t) = 2 g(t) / (1+g(t)).

g(t) / ( 2-g(t) ) = 2 g(t) / (1+g(t))   =>   g(t) = 1

But g(0) must be 0. Contradiction. You can't achieve a perfect result in both directions.

If you are willing to use JS, you could swap the timing functions of the outer and the inner when the mouse enters or leaves the target element.

Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Thank you very much! This helped me a lot in understanding the problem. I bow my head before your math skills. Is it possible to express your formula in a cubic-bezier function that can be attached to the inner element? – nirazul Jul 14 '16 at 07:10
  • @Nirazul I don't know much about Bezier curves, but my preliminary intuition says that it's not possible to stabilize the inner element at scale 1. But you can get close, probably. – Oriol Jul 14 '16 at 10:24
  • @Nirazul It seems I managed to get a seemingly perfect result. But only forwards. Both forwards and backwards is not possible. – Oriol Jul 16 '16 at 04:39
  • Anyway, it's been a surprisingly interesting problem :) – nirazul Jul 16 '16 at 08:55
0

scaleY() affects both parent and child elements sizing (such as height).

You are scaling the child element's height in two ways, simultaneously increasing and decreasing. What you are seeing is completely expected behavior given the values and properties you've set for the elements.

Assume the following:

  • the parent and child are both 100px in `height.
  • scaleY amount for the parent is 0.5(which also affect the child) and the child is 2.
  • the transition time is 1000ms.

At 500ms:

  • The parent should be scaled to 0.75 (100px * 0.75 = 75px height)
  • The child should be scaled to 1.5 (75px * 1.5 = 112.5px height)

At 1000ms:

  • The parent should be scaled to 0.5 (100px * 0.5 = 50px height)
  • The child should be scaled to 2) (50px * 2 = 100px height)

That is why you are experiencing the "grow then shrink" behavior.

I would not spend time fiddling with some cubic bezier values, rather, reproach your element structuring and design.

justinw
  • 3,856
  • 5
  • 26
  • 41
  • Thanks for your explanation! I know that such problems can be time-consuming, so I'd agree that restructuring would probably be best. But sometimes it's not possible and as it's a pure mathematical problem, I'm sure there's a solution lying around :) – nirazul Jul 14 '16 at 06:59