fabiospampinato / cash

An absurdly small jQuery alternative for modern browsers.
MIT License
6.93k stars 270 forks source link

Animation #119

Closed shshaw closed 3 years ago

shshaw commented 8 years ago

Latest Prototype

Cash.fn.animate essentials:

I think the future of Cash may need to center around modularity with lightweight extensions/plugins. There are only so many DOM methods we can add that are worthwhile, and performance optimizations will only go so far.

The main Cash package will always have the most useful methods to help beginners get started, but I picture developers being able to select the features/modules they need to get a lightweight package (less than 10-15kb) that gives them exactly what they need for a project without any cruft like we've talked about in #94.

Thinking along those terms, I have a basic animation engine (two files: animateCore.js and canvallax.Animate.js that's quite performant. It could easily be adapted as a cash plugin, minified to about 1kb gzipped, 2kb minified.

Currently it's more along the lines of GSAP TweenMax syntax and designed to animate properties of objects, but it could be adapted to handle CSS properties and more closely match the jQuery syntax for compatibility while introducing some more advanced functions (delay, repeat, animateFrom, animateFromTo, etc).

Like the discussion for AJAX ( #103 ), this wouldn't be an addition to the core, but a separate module for those that want a lightweight but familiar syntax to run simple animations. jQuery with the animation module is at least 50kb, and TweenLite + jQuery support is around 30kb. That's a lot to add for simple effects like spinning an element when clicked or to smoothing a transition from point A to point B.

Any thoughts on animation or the future of Cash?

shshaw commented 8 years ago

Looks like you may be describing the KeyframeEffect class that the Animation class uses, the base of Element.animate.

Good demos. It's not crazy the have sequencing, but the syntax need some work in terms of how it would work in Cash.

tommiehansen commented 8 years ago

Yes.

And yeah, it probably do. See it more as an annoying sample that just shows that it is possible with very little code but still something that cuts down on the codebloat considerably.

Another thing is btw tracking state changes since it quite quickly becomes problematic to reset any changes that has been made. In the demo i needed to check my css to see what my initial states were and reset these with static values which is something that just won't work. Or have i missed something?

Btw -- a lib that is quite small, 5kb, is maybe something else to look at; snabbt.js. https://daniel-lundin.github.io/snabbt.js/

shshaw commented 8 years ago

Ah, yes. I'd forgotten about Snabbt. 5kb (gzipped) is still over double the target & current weight of this, and we already have a good portion of snabbt's functionality (and a lot of other functionality snabbt doesn't have), so I think we're in a good spot :-)

Regarding state changes, a cancel and finish method like in the WAAPI would allow you to reset to the original state or jump to the final state. Once those methods are implemented, if you save a reference to the first animation (assuming you do multiple animations), you should be able to call cancel to reset to that initial state even though the animation is finished ( var anim = $(el).animate(...); ... anim.cancel() ). Is that what you're referring to?

tommiehansen commented 8 years ago

Yes, it's currently better except for the debugging stuff. :-)

For now i also think it's better to have the WAAPI-syntax (in the cases where it makes sense -- it's hardly perfect) but leave the WAAPI-specific functions out since we're still probably at least a year from WAAPI being a real option on what i would call "real sites". The browsers that has implemented it also have done so in their own specific flavour so even if function A and B works in browser C and D it isn't certain that they work the same.

Also @shshaw -- while the weight in itself is a good goal (because one have to think about the end size, not just the size of an individual lib) i think priority one may be to outline what the bare minimum is to not have something where one would use pure CSS-animations instead and to make it worthwhile to actually use the library. Here's an example where it wouldn't make sense to even include something as small as the current version of cash.animate: http://codepen.io/tommiehansen/full/RaXZzW/

What's hard to do with CSS-animations or what is hard to maintain?

  1. Sequencing / Timeline control
  2. Functions to finish(), end(), beforestart() etc
  3. Controlling paints and reflows
    CSS simply does not do this in itself, but it's the #1 problem with animating the DOM; here's where stuff such as FLIP by Google comes in and the ability to do things before an animation start beforestart(). This is the difference between having smooth animations and having animations that stutter somewhere during its life cycle. Perf for animation is simply not a CSS vs Javascript -thing even if some libs have one believe that. Sure, in some situations JS can give 2 fps more but stuttering is pretty much always related to layout trashing of some kind.
  4. Start/End states
    In order to reset things that have gone 'out of view' and similar things or do any sort of processing, related to #3.
  5. Organization
    Jumping between CSS <> JS is a terrible workflow. CSS is for presentation but animations nearly always fall in the 'in-between' which makes it harder to maintain. If animating with the help of JS as much as possible should be within the same language. Else it's like working in a project where one have to use a different language each second word.

Here start/end -states comes in again but one must be vary of that things such as .getComputedStyle() triggers paints. It is extraordinarily important to not have a lib that exessively writes/reads the DOM or does so in a stupid manor (read/writes should be qued). A problem related to this is that simple demos just doesn't reveal the problems with these things since there aren't enough DOM-nodes or enough complex DOM-nodes (with styling) for it to become a visible problem. It's one thing to animate10 simple div's on a page with 0 childnodes, it's an entirely different thing to animate 2 div's on a page with 10 parent-nodes where all these have 10 childnodes and all these have 20 childnodes; all with CSS-styling etc etc.

Edit: All of these reflows/repaints/layout trashing is ofc out of the scope for the lib itself, but the lib should at least be vary of these and helpful in the way that it doesn't itself triggers bad things and gives the user some control over all the "when/what/then". :-)

shshaw commented 8 years ago

I haven't forgotten about this, just been slammed at work so haven't been able to dive any deeper into it.

Thanks for the big list, @tommiehansen. I didn't recall reading it before so I'm going over it now.

Regarding 1, Sequencing/Timeline control could be accomplished in this manner, similar to what you'd written before:

$.animate([
  ['#myElement', {  props }, { opts }],
  ['.element2', {  props }, { opts }],
  [document.getElementById('element3'), {  props }, { opts }],
], { animationOpts });

animationOpts would contain callbacks for the full animation. This could be used as default options for the keyframes, for example to set the easing for all keyframes unless overwritten by that keyframe's options.

Number 2 and number 5 are already solved by the current code, though sequencing may provide more opportunities to improve this.

Number 4 'end states' could be done with the supporting fill property. Right now, it's handled the equivalent to fill: both, so that if the animation is reversed, the animation keeps the values of the start state. Are you describing something different than that?

The only time getComputedStyle is called is when the animation starts, so that the animation can be calculated. I don't see a way around that. Same with number 3, the lib will be built to not cause unnecessary paint/reflows, but if you're animating height or other non-'safe' properties you're going to get thrashing.

shshaw commented 8 years ago

Stumbled across this today: https://github.com/juliangarnier/anime

Not quite the syntax that we've been discussing and it looks like chaining/keyframing isn't really built-in, but it does have a solid feature set and weighs only 10kb minified.

tommiehansen commented 8 years ago

@shshaw Sorry for the late replies, as you i'm often quite occupied with other things. :)

I've stumbled upon anime.js as well and while it may be good it, just as you say, lacks chaining and keyframing hence it is basically as useful as using raw CSS3-animations (greatly simplified of course) when it comes to the "create a simple animation in a short amount of time". For creating advanced animations one would probably just go with Velocity.js or TweenLite/Max from Greensock.

There's probably a lot one could borrow from anime.js though. :)

Edit: The demo i created before actually highlights sequencing in simple UI interactions http://codepen.io/tommiehansen/pen/bpXNPr

The thing is that UI elements comes more alive and becomes more natural if you add the "in-between" frames. Just click on the button in that codePen demo, what happens is all of these things:

  1. Button gets 'pushed' (scale)
  2. Button slides out of view (translateY)
  3. Layer 'back' slides into view
  4. Images slides up into view with staggering
  5. Close buttons slides down into view
  6. Close buttons does a funky animation (that is just a proof of concept and would never be used ofc)

Now imagine doing that with a system that has no sort of queing or sequencing.

Usually these things look and behave like this:

  1. Button get pushed (nothing happens or one does a unsyncronous animation on it)
  2. 'back' slides up

This is an unnatural flow that would never occur in nature or any sort of setting. Just imagine all the 'keyframes' required for something as simple as opening a door and walking through it:

  1. Walk up to the door
  2. Grab the handle of the door
  3. Keep holding handle and push the door
  4. Walk through the door still holding the handle
  5. Release the handle gently
  6. Walk a little furter to clear the door
  7. Turn around
  8. Grab the handle at the opposite side
  9. Close the door
  10. Turn around
  11. Walk forwards and do whatever the objective was that required you to open/close the door

And that's just with something as simple as opening and closing a door.

What you could do is to recreate a sequence similar to the one i did in my demo since these things is slighly hard to explain but becomes very clear once one start writing code. Just do something that has a button or something where multiple animations would be more natural. Imagine something like a light switch or any enter/exist where "enter" shows A and exit returns to default state (and possibly reverses a set of sequenced animations in some way).

fabiospampinato commented 6 years ago

I think we shouldn't add support for this if the API would be completely different from jQuery's implementation, in that case just include that other thing into your bundle along with cash.

I'm ok with including support for animation as a plugin, basically included in the repo but disabled by default in our bundles, that's basically a subset of what jQuery implements.

tommiehansen commented 5 years ago

I think we shouldn't add support for this if the API would be completely different from jQuery's implementation, in that case just include that other thing into your bundle along with cash.

I'm ok with including support for animation as a plugin, basically included in the repo but disabled by default in our bundles, that's basically a subset of what jQuery implements.

All extras as plugins is a good way to do it.

When it comes to animation jquery isn't the gold standard to follow though. This due to its lack of sane sequencing, something that is discussed in this thread. Without it one quickly becomes stuck in a callback hell of sorts. And without sequencing there really is no need for any animation plugin.

varunsridharan commented 4 years ago

@fabiospampinato is there any update on having animations as a plugin ?

fabiospampinato commented 4 years ago

@varunsridharan No.

varunsridharan commented 4 years ago

@fabiospampinato ok cool.

Well i was looking to migrate my WordPress Framework away from jquery and found Cash as a best alternative.

Thanks for making a wonderful library. :-)

and i was creating a custom lib using your code and was able to add a very basic animation system and here the code.

and the code was taken from TinyAnimate with some customization


/**
 * TinyAnimate.easings
 * Adapted from jQuery Easing
 */
const M                  = Math;
const easings            = {};
easings.linear           = ( t, b, c, d ) => c * t / d + b;
easings.easeInQuad       = ( t, b, c, d ) => c * ( t /= d ) * t + b;
easings.easeOutQuad      = ( t, b, c, d ) => -c * ( t /= d ) * ( t - 2 ) + b;
easings.easeInOutQuad    = ( t, b, c, d ) => ( ( t /= d / 2 ) < 1 ) ? c / 2 * t * t + b : -c / 2 * ( ( --t ) * ( t - 2 ) - 1 ) + b;
easings.easeInCubic      = ( t, b, c, d ) => c * ( t /= d ) * t * t + b;
easings.easeOutCubic     = ( t, b, c, d ) => c * ( ( t = t / d - 1 ) * t * t + 1 ) + b;
easings.easeInOutCubic   = ( t, b, c, d ) => ( ( t /= d / 2 ) < 1 ) ? c / 2 * t * t * t + b : c / 2 * ( ( t -= 2 ) * t * t + 2 ) + b;
easings.easeInQuart      = ( t, b, c, d ) => c * ( t /= d ) * t * t * t + b;
easings.easeOutQuart     = ( t, b, c, d ) => -c * ( ( t = t / d - 1 ) * t * t * t - 1 ) + b;
easings.easeInOutQuart   = ( t, b, c, d ) => ( ( t /= d / 2 ) < 1 ) ? c / 2 * t * t * t * t + b : -c / 2 * ( ( t -= 2 ) * t * t * t - 2 ) + b;
easings.easeInQuint      = ( t, b, c, d ) => c * ( t /= d ) * t * t * t * t + b;
easings.easeOutQuint     = ( t, b, c, d ) => c * ( ( t = t / d - 1 ) * t * t * t * t + 1 ) + b;
easings.easeInOutQuint   = ( t, b, c, d ) => ( ( t /= d / 2 ) < 1 ) ? c / 2 * t * t * t * t * t + b : c / 2 * ( ( t -= 2 ) * t * t * t * t + 2 ) + b;
easings.easeInSine       = ( t, b, c, d ) => -c * M.cos( t / d * ( M.PI / 2 ) ) + c + b;
easings.easeOutSine      = ( t, b, c, d ) => c * M.sin( t / d * ( M.PI / 2 ) ) + b;
easings.easeInOutSine    = ( t, b, c, d ) => -c / 2 * ( M.cos( M.PI * t / d ) - 1 ) + b;
easings.easeInExpo       = ( t, b, c, d ) => ( t == 0 ) ? b : c * M.pow( 2, 10 * ( t / d - 1 ) ) + b;
easings.easeOutExpo      = ( t, b, c, d ) => ( t == d ) ? b + c : c * ( -M.pow( 2, -10 * t / d ) + 1 ) + b;
easings.easeInOutExpo    = ( t, b, c, d ) => {
    if( t == 0 ) {
        return b;
    }
    if( t == d ) {
        return b + c;
    }
    return ( ( t /= d / 2 ) < 1 ) ? c / 2 * M.pow( 2, 10 * ( t - 1 ) ) + b : c / 2 * ( -M.pow( 2, -10 * --t ) + 2 ) + b;
};
easings.easeInCirc       = ( t, b, c, d ) => -c * ( M.sqrt( 1 - ( t /= d ) * t ) - 1 ) + b;
easings.easeOutCirc      = ( t, b, c, d ) => c * M.sqrt( 1 - ( t = t / d - 1 ) * t ) + b;
easings.easeInOutCirc    = ( t, b, c, d ) => ( ( t /= d / 2 ) < 1 ) ? -c / 2 * ( M.sqrt( 1 - t * t ) - 1 ) + b : c / 2 * ( M.sqrt( 1 - ( t -= 2 ) * t ) + 1 ) + b;
easings.easeInElastic    = ( t, b, c, d ) => {
    var p = 0;
    var a = c;
    if( t == 0 ) {
        return b;
    }
    if( ( t /= d ) == 1 ) {
        return b + c;
    }
    if( !p ) p = d * .3;
    if( a < M.abs( c ) ) {
        a     = c;
        var s = p / 4;
    } else {
        var s = p / ( 2 * M.PI ) * M.asin( c / a );
    }
    return -( a * M.pow( 2, 10 * ( t -= 1 ) ) * M.sin( ( t * d - s ) * ( 2 * M.PI ) / p ) ) + b;
};
easings.easeOutElastic   = ( t, b, c, d ) => {
    var p = 0;
    var a = c;
    if( t == 0 ) {
        return b;
    }
    if( ( t /= d ) == 1 ) {
        return b + c;
    }
    if( !p ) p = d * .3;
    if( a < M.abs( c ) ) {
        a     = c;
        var s = p / 4;
    } else {
        var s = p / ( 2 * M.PI ) * M.asin( c / a );
    }
    return a * M.pow( 2, -10 * t ) * M.sin( ( t * d - s ) * ( 2 * M.PI ) / p ) + c + b;
};
easings.easeInOutElastic = ( t, b, c, d ) => {
    var p = 0;
    var a = c;
    if( t == 0 ) {
        return b;
    }
    if( ( t /= d / 2 ) == 2 ) {
        return b + c;
    }
    if( !p ) p = d * ( .3 * 1.5 );
    if( a < M.abs( c ) ) {
        a     = c;
        var s = p / 4;
    } else {
        var s = p / ( 2 * M.PI ) * M.asin( c / a );
    }
    if( t < 1 ) {
        return -.5 * ( a * M.pow( 2, 10 * ( t -= 1 ) ) * M.sin( ( t * d - s ) * ( 2 * M.PI ) / p ) ) + b;
    }
    return a * M.pow( 2, -10 * ( t -= 1 ) ) * M.sin( ( t * d - s ) * ( 2 * M.PI ) / p ) * .5 + c + b;
};
easings.easeInBack       = ( t, b, c, d, s ) => {
    if( isUndefined( s ) ) {
        s = 1.70158;
    }
    return c * ( t /= d ) * t * ( ( s + 1 ) * t - s ) + b;
};
easings.easeOutBack      = ( t, b, c, d, s ) => {
    if( isUndefined( s ) ) {
        s = 1.70158;
    }
    return c * ( ( t = t / d - 1 ) * t * ( ( s + 1 ) * t + s ) + 1 ) + b;
};
easings.easeInOutBack    = ( t, b, c, d, s ) => {
    if( isUndefined( s ) ) {
        s = 1.70158;
    }
    return ( ( t /= d / 2 ) < 1 ) ? c / 2 * ( t * t * ( ( ( s *= ( 1.525 ) ) + 1 ) * t - s ) ) + b : c / 2 * ( ( t -= 2 ) * t * ( ( ( s *= ( 1.525 ) ) + 1 ) * t + s ) + 2 ) + b;
};
easings.easeInBounce     = ( t, b, c, d ) => {
    return c - easings.easeOutBounce( d - t, 0, c, d ) + b;
};
easings.easeOutBounce    = ( t, b, c, d ) => {
    if( ( t /= d ) < ( 1 / 2.75 ) ) {
        return c * ( 7.5625 * t * t ) + b;
    } else if( t < ( 2 / 2.75 ) ) {
        return c * ( 7.5625 * ( t -= ( 1.5 / 2.75 ) ) * t + .75 ) + b;
    } else if( t < ( 2.5 / 2.75 ) ) {
        return c * ( 7.5625 * ( t -= ( 2.25 / 2.75 ) ) * t + .9375 ) + b;
    } else {
        return c * ( 7.5625 * ( t -= ( 2.625 / 2.75 ) ) * t + .984375 ) + b;
    }
};
easings.easeInOutBounce  = ( t, b, c, d ) => ( t < d / 2 ) ? easings.easeInBounce( t * 2, 0, c, d ) * .5 + b : easings.easeOutBounce( t * 2 - d, 0, c, d ) * .5 + c * .5 + b;

const animation      = function( from, to, duration, update, easing, done ) {
    if( !isNumber( from ) || !isNumber( to ) || !isNumber( duration ) || !isFunction( update ) ) {
        return 'OOO';
    }

    if( isString( easing ) && easings[ easing ] ) {
        easing = easings[ easing ];
    }

    if( !isFunction( easing ) ) {
        easing = window[ easing ] || null;
    }

    if( !isFunction( easing ) ) {
        easing = easings.linear;
    }

    if( !isFunction( done ) ) {
        done = function() {
        };
    }

    let canceled = false,
        change   = to - from,
        rAF      = v.win.requestAnimationFrame || ( ( callback ) => v.win.setTimeout( callback, 1000 / 60 ) ),
        loop     = function( timestamp ) {
            if( canceled ) {
                return;
            }
            var time = ( timestamp || +new Date() ) - start;
            if( time >= 0 ) {
                update( easing( time, from, change, duration ) );
            }
            if( time >= 0 && time >= duration ) {
                update( to );
                done();
            } else {
                rAF( loop );
            }
        };

    update( from );
    var start = v.win.performance && v.win.performance.now ? v.win.performance.now() : +new Date();
    rAF( loop );

    return { cancel: () => canceled = true, };
};
animation.animateCSS = function( element, property, from, to, duration, easing, done ) {
    let existing  = regex.cssProperty.exec( core( element ).css( property ) ),
        animateTo = regex.cssProperty.exec( to ),
        toNumber  = ( isUndefined( animateTo[ 1 ] ) ) ? to : animateTo[ 1 ],
        toUnit    = ( isUndefined( animateTo[ 2 ] ) ) ? existing[ 2 ] : animateTo[ 2 ];
    from          = ( isNull( from ) ) ? existing[ 1 ] : from;

    let update = function( value ) {
        return element.style[ property ] = value + toUnit;
    };

    from     = parseFloat( from ) || 0;
    toNumber = parseFloat( toNumber ) || 0;
    duration = parseInt( duration ) || 0;

    return animation( from, toNumber, duration, update, easing, done );
};
animation.cancel     = function( instance ) {
    if( !instance ) {
        return;
    }
    instance.cancel();
};

fn.animation = function( property, to, duration, easing, callback ) {
    return this.each( ( i, elem ) => {
        animation.animateCSS( elem, property, null, to, duration, easing, callback );
    } );
};
fabiospampinato commented 3 years ago

Closing as an official animation plugin won't be written, use one of the suggested ones. If anybody knows of any plugin that's compatible with Cash and implements the same API as jQuery well I'll link to it in the readme.