Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

affine transforms for animations #18

Open
jaukia opened this issue Nov 18, 2010 · 16 comments
Open

affine transforms for animations #18

jaukia opened this issue Nov 18, 2010 · 16 comments

Comments

@jaukia
Copy link

jaukia commented Nov 18, 2010

Hi,

Your 2d transform library is impressive! I'm the author of the Zoomooz library and I've been thinking about replacing animation and transform code of my own with those in you library. My test branch for 2d transform integration is here:
https://github.com/jaukia/zoomooz/tree/transformlib

The problem is that it would seem that the more complex transformations do not would with the 2d transform library as well as they work with my previous custom code. I believe this is because in my code the animation is done by interpolating the affine transform elements corresponding to the transformations (instead of interpolating the transformations directly).

Are you aware of the issue and do you think my hypothesis could be correct? Do you have any plans related to this? Feel free to use parts of my code, it is licenced with the standard MIT + GPL2 jquery licences!

@heygrady
Copy link
Owner

"The problem is that it would seem that the more complex transformations do not [work] with the 2d transform library as well as they work with my previous custom code."

My code is really straight-forward. I took the matrices as defined in Wikipedia and in the SVG Spec. Then I used the full calculations for multiplication instead of using loops. That same optimization was in the WebKit core as well so that seemed like a good choice.

Interestingly, the only browser that ever uses this is IE since the other browsers support this directly in CSS. For IE we need to multiply out all of the composite matrices to get the final matrix and apply that with a filter and work with it to fake the translate and origin features of CSS3 that the IE matrix filter doesn't support.

Why do you propose that we'd need to decompose a matrix? What parts of my code don't work for your use-case? I'm a little lost on the concept of "interpolating the element" as opposed to "interpolating the transformation." In WebKit and Mozilla and Opera the "interpolation" is done by the browser itself. In IE, the matrix is also applied with a filter that does the actual interpolation.

@heygrady
Copy link
Owner

It should be noted that for my project I'm not at all interested in a full featured matrix calculation library. It only needs to multiply two matrices together. For some rarer use-cases I also needed to be able to calculate an inverse matrix but I've never actually needed decomposition.

@jaukia
Copy link
Author

jaukia commented Nov 23, 2010

Hi, sorry for not being clear enough in the first message! The problem occurs with animating matrix-transformations. Simply animating each field of a transformation matrix separately does not work correctly, see here for example how 180 degree rotation fails:

http://janne.aukia.com/htmltests/transform-test/

@heygrady
Copy link
Owner

Cool test. You're specifically talking about if I animate a matrix as opposed to the other functions. Based on your example it looks like you're correct that doing a decomposition is the correct approach for the animation. I'll add this to my list of fixes to make.

Am I correct in assuming that I'd only ever need to decompose in the case that I'm animating a matrix?

If I had something like this:
$("#i1").animate({matrix: [-1,0,0,-1], rotate: '45deg'});

The proper thing to do is:

  1. multiply the matrix by the rotate matrix
  2. decompose that matrix
  3. animate the new properties

@jaukia
Copy link
Author

jaukia commented Nov 23, 2010

Yes, exactly, that should be the proper way.

The decompose is needed at least when animating a matrix, of the other properties I am not fully certain. For example, if the current transformation of an element has a different set of css transformations (or they are in different order), it might make sense to decompose the original transformation as well as the target transformation and then animate these (normalized) new properties. Sorry for the crappy explanation :).

If you like, you can always do the decomposition:

  1. multiply all of the transformations into a matrix
  2. decompose the matrix
  3. animate the decomposed properties

Then, the animations would always work correctly.

@heygrady
Copy link
Owner

The decomposition will always take time to complete and if it isn't usually necessary then there's no reason to do it except in rare cases.

For instance:
// There's no need to decmopose this transform, it's already decomposed
$('#example').css({rotate: '30deg'});
$('#example').animate({rotate: '+=30deg'});

There is no reason to decompose the above because it's actually already decomposed. It already animates correctly in Internet Explorer and CSS3 supported browsers. Each step of the animation is animating the rotate itself.

There is a small inefficiency in that if you animate multiple transform properties at once they are animated independently. This is largely due to the nature of how jQuery animate works. Decomposition wouldn't help this. The real solution would be to set the properties all at once.

For instance:
// Transform properties are applied independently
$('#example').animate({rotate: '+=30deg', scale: 0.75});

// This doesn't work yet as of 0.9.0 and still wouldn't require decomposition but would potentially be more efficient
$('#example').animate({transform: {rotate: '+=30deg', scale: 0.75}});

But, from what you've shown me, if a matrix value is set then the transform needs to be decomposed in order to animate correctly. Even then, though, only the matrix itself needs to be decomposed. In the case of animation I'm wary of trying to do too many calculations on each animation step. But it's probably unavoidable when animating a matrix.

// Reflect applies matrix(-1, 0, 0, -1, 0, 0), should decompose the matrix to make it look correct when it animates
$('#example').animate({reflect: true});

// Animating seperately is an advantage here.
// Essentially the reflect matrix would be decomposed and then animated
// At each step in the animation it would be recomposed to a matrix for simplicity
$('#example').animate({reflect: true, scale: 0.75});

@jaukia
Copy link
Author

jaukia commented Nov 25, 2010

It is enough to do the calculation (affine transform) for the matrix only when starting the animation. From there on, you can just animate between the initial affine transform values and the final values.

For example, if you have:

 matrix(0.000000, 0.707107, -1.414213, 0.707107, 0.000000, 0.000000)

This can be normalized with:

    function affineTransform(m) {
        var a=m[0],b=m[1],c=m[2],d=m[3],e=m[4],f=m[5];

        if(Math.abs(a*d-b*c)<0.01) {
            console.log("fail!");
            return;
        }
        
        var tx = e, ty = f;
        
        var sx = Math.sqrt(a*a+b*b);
        a = a/sx;
        b = b/sx;
        
        var k = a*c+b*d;
        c -= a*k;
        d -= b*k;
        
        var sy = Math.sqrt(c*c+d*d);
        c = c/sy;
        d = d/sy;
        k = k/sy;
        
        if((a*d-b*c)<0.0) {
            a = -a;
            b = -b;
            c = -c;
            d = -d;
            sx = -sx;
            sy = -sy;
        }
    
        var r = Math.atan2(b,a);
        return {"tx":tx, "ty":ty, "r":r, "k":Math.atan(k), "sx":sx, "sy":sy};
    }

You may use it like this:

m = [0.000000, 0.707107, -1.414213, 0.707107, 0.000000, 0.000000];
var aff = affineTransform(m);

As output, in var aff, you get:

{ k: 0.46364789184359106,
  r: 1.5707963267948966,
  sx: 0.707107,
  sy: 1.414213,
  tx: 0,
  ty: 0}

These params correspond directly to css transform statements and can be converted back with:

    function matrixCompose(ia) {
        var ret = "translate("+roundNumber(ia.tx,6)+"px,"+roundNumber(ia.ty,6)+"px) ";
        ret += "rotate("+roundNumber(ia.r,6)+"rad) skewX("+roundNumber(ia.k,6)+"rad) ";
        ret += "scale("+roundNumber(ia.sx,6)+","+roundNumber(ia.sy,6)+")";
        return ret;
    }
    
    function roundNumber(number, precision) {
        precision = Math.abs(parseInt(precision,10)) || 0;
        var coefficient = Math.pow(10, precision);
        return Math.round(number*coefficient)/coefficient;
    }

Example with aff:

matrixCompose(aff);

The output of this is:

"translate(0px,0px) rotate(1.570796rad) skewX(0.463648rad) scale(0.707107,1.414213)"

So basically, this is a way to transform any simple or complex set of transforms into a combination of translate, rotate, skewX and scale. And when you do this conversion to both the animation start and end states, then you can be quite sure that the animation is reasonable and there are no weirdnesses (at least when the corresponding angles are less than PI apart from each other).

Disclaimer: This is as far as I understand this, there might be errors :).

@heygrady
Copy link
Owner

I committed 0.9.1pre with your suggestions. Please test it out. Notice that I had to make a small change to your decomposition script when you were negating everything in certain circumstances, the sy variable should not ever be negated (based on testing the reflect matrices).

@heygrady
Copy link
Owner

BTW, I'm curious why you're using a custom round function. Couldn't you do this instead:

function roundNumber(number, precision) {
    return parseFloat(number.toFixed(precision));
}

@jaukia
Copy link
Author

jaukia commented Nov 27, 2010

Cool, I'll have to try out the 0.9.1pre version!

Good question about the rounding function! I honestly have no idea why I didn't use toFixed. I probably hadn't noticed it around :). I don't know which one is faster, though. With toFixed there is an extra float-string-float conversion.

@jaukia
Copy link
Author

jaukia commented Nov 27, 2010

At least on my Safari the custom round function seems to be faster:

function roundNumber1(number, precision) {
    return parseFloat(number.toFixed(precision));
}

function roundNumber2(number, precision) {
    precision = Math.abs(parseInt(precision,10)) || 0;
    var coefficient = Math.pow(10, precision);
    return Math.round(number*coefficient)/coefficient;
}

function testRoundingFunction(func) {
    var st=(new Date()).getTime();
    for(var i=0;i<1000000;i++) {
        func(i/3,6);
    };
    return (new Date()).getTime()-st;
}

console.log("round with toFixed", testRoundingFunction(roundNumber1));
console.log("round with Math.pow", testRoundingFunction(roundNumber2));

Output in Safari:

round with toFixed 1367
round with Math.pow 163

On latest WebKit:

round with toFixed 912
round with Math.pow 137

On latest Firefox nightly:

round with toFixed 844
round with Math.pow 498

However, on Firefox 3.6 they are almost the same:

round with toFixed 2524
round with Math.pow 2445

Interesting :)

@jaukia
Copy link
Author

jaukia commented Dec 1, 2010

Hi, I tested now out the 0.9.1 version. There seem to be still issues with the transformations, see these examples (which work in Safari/Webkit):

Skew is performed in wrong direction, or something:
http://janne.aukia.com/htmltests/transform-test/skew.html

Translate in matrix not working:
http://janne.aukia.com/htmltests/transform-test/translate.html

@heygrady
Copy link
Owner

heygrady commented Dec 4, 2010

Just committed 0.9.2. There was a pretty serious typo. Should be better now. Thanks for your continued testing!!

@jaukia
Copy link
Author

jaukia commented Dec 6, 2010

Great that you have had the energy to spend your time on this! I'm still having some issues with the matrix transformations. They might be related to these:

Your library would not seem to care about the initial css transformation of the element. This might be intentional and not a bug:
http://janne.aukia.com/htmltests/transform-test/respectstate.html

The transforms for an element are apparently applied on top of each other, see this example:
http://janne.aukia.com/htmltests/transform-test/previoustransform.html
(is there a way to rewrite the transformation for an element, so that it would work in the same way as the css version?)

@heygrady
Copy link
Owner

heygrady commented Dec 6, 2010

You're really putting me through the paces :) Thanks for these examples, it really helps give me something concrete to work with.

The first one is intentional because until you showed me about decomposing the matrix I was a little stumped about how to handle that. Most browsers return matrix as the only function no matter what you had set previously which is pretty worthless to the average user. I was planning on adding that in soon.

On the second one, that's also intentional. I'm not entirely sure how to deal with this one. I had a few bugs a while ago about this same issue and decided the way I designed it was preferable.

Option 1: You should keep track of what's set before you animate and animate it out.

// Same as your example
$('#example').css({rotate: '45deg'}); // using CSS respects previously set properties
$('#example').animate({translateX: '300px'}); // so does animate

// Achieves the desired effect (and actually what the browser is doing)
$('#example').transform({rotate: '45deg'});  // transform resets the transform
$('#example').animate({translateX: '300px', rotate: 0}); // just remove the rotate

Option 2: I can keep track of that in my library and handle it automatically. My big question is if that's the right behavior. In your CSS-only example, you're literally switching transform properties which is nice. But the way I've exposed the different transform properties they're meant to work like height and width: independently. I think that's what most users are expecting to do. You're clearly a more advanced user. This is why I added an option to preserve the previous transformations. It is set to true for css and animate.

$('#example').transform({rotate: '35deg'}); // overwrite
$('#example').transform({skewX: '15deg'}); // overwrites rotate
$('#example').transform({rotate: '35deg'}, {preserve: true}); // keeps skewX and tacks rotate to the end
$('#example').css({skewY: '15deg'});  // always preserves previously set transforms
$('#example').animate({scale: 1.5});  // this does too
$('#example').css({skewY: '-15deg'});  // but this will overwrite skewY

Solution: What I've had planned is adding "transform" as an animation-ready property. I'll work on this next.

$('#example').css({transform: {rotate: '45deg'}}); // this would overwrite
$('#example').animate({transform: {translateX: '300px'}}); // this is what you're looking for

@jaukia
Copy link
Author

jaukia commented Dec 7, 2010

Ok, thanks for the info, these kinds of issues are a bit difficult to implement in a smart way, I'm sure. I'll have to see, how these fit in the stuff i'm doing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants