greensock / GSAP

GSAP (GreenSock Animation Platform), a JavaScript animation library for the modern web
https://gsap.com
19.83k stars 1.72k forks source link

CSS transform 3d doesn't work when the element is with display none. #4

Closed edankwan closed 11 years ago

edankwan commented 11 years ago
<html>
    <head>
        <title>Prototype</title>
        <style>
            #box{
                position: absolute;
                width: 100px;
                height: 100px;
                background-color: #f00;
            }
        </style>
        <script src="http://cdn.jsdelivr.net/gsap/12/TweenMax.min.js"></script>
    </head>
    <body>
        <div id="box"></div>
        <script>
            (function(){
                var box = document.getElementById('box');
                function init(){
                    box.style.display = 'none';
                    TweenMax.to(box, 1, {css: {left: 100}}); // works
                    //TweenMax.to(box, 1, {css: {x: 100}}); // works
                    //TweenMax.to(box, 1, {css: {transform: 'translate3d(100px,0,0)'}}); // doesnt work
                    setTimeout(function(){
                    box.style.display = 'block';
                    }, 100);
                }
                init();
            }())
        </script>
    </body>
</html>

It seems that for transform 3d, tweenMax will not parse the tween if the getComputedStyle() return a empty string rather than "none". It doesn't work with set() as well.

jackdoyle commented 11 years ago

Ah yes, very interesting - the browser doesn't report things correctly if display:none is applied. The latest update should resolve this - we force it to "block" internally (temporarily, just to read things properly and then change it back). Thanks for pointing this out.

edankwan commented 11 years ago

I don't think it can resolve the problem because if its parent is display none or its grandparent(parent's parent) is display none, it is still fucked. If you loop it up to the document body, it will cause serious css reflow. And also, if the dom is not yet in the body, it is not gonna work. The project I am working on is that the dom is not in the body yet but I just want to initialize it.

I think the better solution would be creating a fake body, and insert the element in it and getComputedStyle. The only drawback is that if the transform properties are using percentage and its width and height are using percentage as well, we won't get the right values. But I guess it should be a reasonable solution rather than toggle it display block and none.

jackdoyle commented 11 years ago

I think a better solution would be to just define the individual properties rather than the "transform" in your scenario. Like instead of transform:"transform3d(100px, 0, 0)", you can do x:100. This ensures that the end values get read correctly.

I'm not quite sure what you meant by a "fake body" - you mean check if document.body exists and if not, create it, add the element, check the transform, and then remove both? Definitely sounds...well...less than ideal and performative. Could cause other issues too. Do you have any sample code that you'd recommend specifically? Maybe I'm just missing something obvious.

edankwan commented 11 years ago

if I use x instead of translate3d(100px,0,0), it will use matrix2d instead of matrix3d and some browsers don't enable hardware acceleration with matrix2d.

For the fake body, what you said is exactly what I meant but I don't think there will slow down the performance that much, because we can detect if the inline style already exists, if yes, then use it as reference.

Then only elements without inline style or elements with inline style but contains percentage will do the fake body thing. And also, we only do that at the beginning of the animation, so it doesn't really matter.

The most important thing is.... No one will actually use tweener to animate 3000 dom elements directly :-)

jackdoyle commented 11 years ago

Awe, come on, who hasn't had a project that required animating at least 3000 dom elements? It's 2013 - everybody is gonna start doing it. ;-)

Performance aside, I try VERY hard to avoid messing with other parts of the dom unless it's absolutely necessary. I don't like making assumptions about the environment or risking collisions with other stuff the developer is doing. What if (for whatever reason) they're running in an environment where there is no such thing as a body tag? What if they've got listeners set up to discover when something is added to the tree? It's unlikely, but possible. So my preference is to find other workarounds if at all possible. In your case, if your goal is to force 3D rendering (GPU), you could set z to 0.01 or something like that. Keep in mind that the old trick of using transform3d() or translateZ(0) no longer works in some browsers. That was a temporary hack that apparently some browser vendors are circumventing now.

edankwan commented 11 years ago

Since right now, it does nothing, to make it as default - "none" will be more reasonable. Hmm... maybe at least make the set() working in this case.

jackdoyle commented 11 years ago

I didn't quite understand your comment. Could you elaborate?

In the updated version, the only time it wouldn't work is if the object's ancestor has display:none AND you're trying to apply things directly to a transform as a string like transform:"translate3d(100px,0,0)". However, it should work in virtually all other cases. For example, if you set it with properties like x, y, z, scaleX, scaleY, rotation, etc. it should work fine. Am I missing something?

edankwan commented 11 years ago

In the example above, if you change that line to this, it still doesnt work: TweenMax.set(box, {css: {transform: 'translate3d(100px,0,0)'}});

What I said is like... right now, it does nothing if the dom element is set to display 'none'. Can you just make it to 'none' or 'translate(0,0)' as default value if getComputed style return an empty string? at least it doesn't break the animation.

jackdoyle commented 11 years ago

No, with the latest update it works fine for me (even with display:none) - did you try the version I pushed out earlier today?

edankwan commented 11 years ago

Not working if the dom element is created but not in the body.

(function(){
    var box = document.createElement('div');
    box.setAttribute('id', 'box');
    function init(){
        //TweenMax.to(box, 1, {css: {left: 100}}); // works
        //TweenMax.to(box, 1, {css: {x: 100}}); // works
        TweenMax.to(box, 1, {css: {transform: 'translate3d(100px,0,0)'}}); // doesnt work
        //TweenMax.set(box, {css: {transform: 'translate3d(100px,0,0)'}}); // doesnt work
        setTimeout(function(){
            document.body.appendChild(box);
        }, 100);
    }
    init();
}())
edankwan commented 11 years ago

In the latest version, if the element is not in the body, your tweener will assume its left/top is 0. So... assuming transform is "none" wont break anything but just follow the same behavior like with left/top.

If you think using fake body can be an issue and slow down the performance, then I don't think it is a good idea to toggle the display block/none as it will have the same performance issue.

jackdoyle commented 11 years ago

Toggling the display is necessary either way. And re-nesting an element in the dom tree degrades performance further.

To be clear, the current behavior DOES assume a normal transform of x:0, y:0, scaleX/scaleY:1, rotation:0 as the starting point. This issue simply has to do with the END values not being reported correctly by the browser. When you provide them as a transform string like transform:"translate3d(100px, 0, 0)", CSSPlugin must apply that to the object and then read back the matrix and interpret it (because your transform could have any number of sequential values like "rotateX(20deg) skewY(10deg) translateZ(10px) rotateX(30deg)..." (notice two rotateX). That's why I suggested defining the values directly like x:100, z:1, etc. That's not only more efficient and performative, but it gets around having to apply your string value and interpret the resulting matrix. See what I mean?

edankwan commented 11 years ago

Oh wait... I just wonder... did your tweener use this: http://tog.acm.org/resources/GraphicsGems/gemsii/unmatrix.c to deal with the matrix3d? Or you just separate the transformation into several parts and tweening the individual values?

jackdoyle commented 11 years ago

No, I did not use unmatrix. I authored the code pretty much from scratch. Fun stuff, working with matrices (not). And yes, we tween only the necessary properties (like scaleX, scaleY, x, y, rotation, etc.) and calculate the appropriate matrix from those.

edankwan commented 11 years ago

If you guys unmatrix it and use that tween the parameters of those transformations, it can accept all kind of transformations with different order. And it will be awesome to be able to do some complicated transformation.

In this case, can you reset the values to 0 for each user input's transformations if the returned getComputedStyle() is empty string? Like if the user try to tween the transform to/set "rotateX(20deg) skewY(10deg) translateZ(10px) rotateX(30deg)", make "rotateX(0) skewY(0) translateZ(0) rotateX(0)" as default value.

jackdoyle commented 11 years ago

Right, that's exactly what we're already doing. It seems like somehow we're not understanding each other. Let me summarize:

YES, it currently defaults transform values to normal (x:0, y:0, scaleX/scaleY:1, rotation:0, etc.). If the getComputedStyle() returns an empty string or none, these default values are used.

YES, we "unmatrix" the values, so you can apply pretty much anything you want.

Both of the things you're asking for are already in there and working.

edankwan commented 11 years ago

You guys are awesome!!! Looking forward to the update!!! Thanks!

jackdoyle commented 11 years ago

Great. And just to be clear, no update is necessary - the behavior I described is already in the current version :) You should be able to use it today. Let us know if you run into any issues.

Enjoy!

edankwan commented 11 years ago

It still doesn't resolve the issue that when the domElement is not inside the body yet:

<script>
(function(){
    var box = document.createElement('div');
    box.setAttribute('id', 'box');
    function init(){
        //TweenMax.to(box, 1, {css: {left: 200}}); // works and assuming the left is 0 when is it not in the body
        TweenMax.to(box, 1, {css: {transform: 'translate3d(100px,0,0)'}}); // doesnt work
        setTimeout(function(){
            document.body.appendChild(box);
        }, 100);
    }
    init();
}())
</script>
jackdoyle commented 11 years ago

I think I must be doing a bad job of explaining this...let me try again.

You can solve that issue by defining the transforms individually, like x:100, y:0, z:0. If you want to kick in 3D transforms, you can set z:0.1 or something small.

The reason your example doesn't work is because the browser (at least Chrome) refuses to report transforms when the element isn't in the body or is display:none. This has nothing to do with GSAP. You can try it yourself - use getComputedStyle(). So when GSAP applies your transform:"translate3d(100px,0,0)" and then asks the browser for the computed matrix (or matrix3d()), the browser acts as though no transforms were applied, thus GSAP uses the default values.

Another way you can solve it is by making sure your element is in the body when the tween renders for the first time. But again, the most efficient way by FAR is to just define the end values individually like I said above (x:100).

edankwan commented 11 years ago

Yes, I know the browser won't report the matrix3d when you use getComputedStyle() on the dom which is not in the body. It will report only an empty string, there are 2 solutions: 1) check if the body is ready, if yes, put the dom element in it, do getComputedStyle() to get the default value or if the body is not ready, create one and do the same thing. Using this, the performance won't be slowing down that much as this procedure will only perform at the beginning of the animation and if there is inline style already, you should be able to grab to before using getComputedStyle. 2) See the pattern of the input value like if the user wants to TweenMax.set/TweenMax.to(foo, 1, {transform : 'translate3d(100px,0,0) scale3d(2,2,1)'}); then use the same pattern and set the values to zero like 'translate3d(0,0,0) scale(1,1,1)'.

Since you said that you "unmatrix" value at #issuecomment-13145302 but also said that you didn't use "unmatrix" at #issuecomment-13139384, I am a bit confused. If you are let the browser to calculate the matrix by using getComputedStyle() to return a matrix2d/matrix3d, for using the "fake body" solution, you don't actually need to put that target dom element to the body/fake body, you can create and leave one single dummy element to do all this kind of dirty work and dont really need to add/remove it everything you need to get the computed style from a dom element which is not in the body.

The bottom line is that... using left/x are both working fine with getComputedStyle reporting an empty string, why using transform doesn't work? I personally prefer using transform string as it is easier to know the transformation in the 3d world like rotateX 30deg -> translateZ 100px -> rotateY 30deg. It is easier to visualize it but in order to do the same thing with translate/rotate/scale... only once each is a bit hard to do the same thing. I would love to use TweenMax to handle more complicated animation instead of creating useless containers and abstracted the tweening part and do the dirty work myself. As what you said, people animate 3000 dom elements at the same time, and I guess people do crazy complicated 3d animation in 2013 as well ;-)

I am a real fan of TweenMax and I have been using it when I was a Flash developer. Good work anyway!

jackdoyle commented 11 years ago

Glad to hear you've been using TweenMax for so long - that's fantastic. And it also sounds like you're doing some aggressive animation which is also very cool.

Unfortunately it seems that I'm not able to communicate the concept in a way that makes it clear to you regarding the 3D values. It sounds like you still think that the starting values are the issue and that I can just set them to 0 (and 1 for scale). That isn't the problem at all - those values are already set to exactly what you suggested. The problem is with the end values and the browser reporting them accurately when the element isn't in the body and display:block.

As for "unmatrix-ing" the values, yes, we're interpreting the matrix values and extrapolating the scale/rotation/perspective/translation from the matrix. We don't use the specific method that you linked to, that's all - we crafted our own algorithm.

Like I said, adding an element to the body (and potentially creating a whole new element) isn't a trivial thing (performance-wise or extra kb-wise) and it can potentially cause other problems that could interfere with the developer's app/site/game. I'm just not comfortable doing that or adding in the conditional logic to do all the checks. I don't think the cost outweighs the benefits. If we get a lot of requests for that kind of feature, we'll certainly reconsider, but it seems like this is very much an edge case and you could accomplish what you want using an onStart in the startAt object (which would simply run a function right before the tween instantiates and the starting values are calculated). Like TweenLite.to(element, 1, {startAt:{onStart:function() {...do stuff...}}, transform:"translate3d(100px,0px,0px)});

I hope that helps. Happy tweening! And thanks for the suggestions.