fabiospampinato / cash

An absurdly small jQuery alternative for modern browsers.
MIT License
6.5k stars 266 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?

kenwheeler commented 8 years ago

I think cash is pretty good as is, feature wise. For animation, I wouldn't use anything other than velocity for most implementations. Something like spinning an element when clicked enters the territory of css animation based on class changes.

shshaw commented 8 years ago

Yeah, I use CSS animations / class toggles wherever I can but programmatic animations are still a need. Velocity isn't any better in terms of file size ( 30+kb minified ).

When I don't need a boatload of features, I try to find something lightweight & performant. A simple $().animate would be quite useful as an extension for those situations, but I understand if it's not something you think cash should support.

shshaw commented 8 years ago

Here's a basic conversion that animates CSS properties on collections in 1.8kb: http://codepen.io/shshaw/pen/WwLKVY?editors=0010

Definitely could be simplified more, especially cutting extra features that jQuery.animate doesn't have ( yoyo, reverse, variable duration, repeat, delay, pause, etc. ). Gets it down to around 1.3kb at first pass

Adding transform ( x, y, scale, rotate ) support would be pretty easy and would allow animations to be even more performant.

tommiehansen commented 8 years ago

Velocity.js is great for programmatic animation but i do agree with @shshaw in that loading velocity for some basic animations via CSS is nearly always total overkill.

Imagine simple stuff as show/hide or page-object transitions with 'stagger'.

shshaw commented 8 years ago

With Custom Builds (issue #94), you could have a cash's core + css + animation in ~4kb, 2kb gzipped for handling all sorts of complex animations. That would be a lot of functionality in a tiny package.

shshaw commented 8 years ago

Basic transform support with prefixed properties support. Compresses to about 2kb, and might be trimmable.

Needs support for reading existing transform properties, potentially building a transform matrix instead of the individual transform functions.

tommiehansen commented 8 years ago

@shshaw So will this happen? Animation or motion in UI really is pretty much now but what technique(s) that are used is in constant flux (latest being the "FLIP" technique by Google) so basic wrappers and some extensions would be a great thing since such base extensions easily can be rewritten or forked to work with A, B or C etc.

shshaw commented 8 years ago

@tommiehansen FLIP may be too heavy handed of a technique for a general animation engine. Based on what I've seen, FLIP works well when you're planning & coding every piece of it and only animating x, y, scale & opacity, but I don't know if it would work well in a general setting. I'll have to look at FlipJS to see how they handle it.

That did make me think: in keeping with Cash's 'mission' of using native browser APIs, we could use Element.animate() if available, and fall back to requestAnimationFrame. Browser support isn't the best, but once Firefox enables it by default in the next version then you're at about 50% coverage. Shouldn't be hard to adapt my implementation to do that.

tommiehansen commented 8 years ago

@shshaw Yes, that's why i wrote about techniques being in 'constant flux' and the anim engine therefor needing to be very generic. It seems i either was not clear or you just didn't read what i wrote. :)

And yes -- using Element.animate() + fallback would be great.

shshaw commented 8 years ago

@tommiehansen I just misinterpreted your mention of FLIP as a recommendation.

As for your original question: 'will this happen?': I've gotten to go ahead from @kenwheeler. I'll start a branch for this after I've cleaned up my concept a little bit. Contributions are welcome. Don't know how quickly I'll be able to roll it out, but an alpha should be available soon for testing.

shshaw commented 8 years ago

Here's what's on my list for $.fn.animate features. Most are already implemented in the latest prototype, but need some cleanup and fine tuning.

List now moved to main issue

Any other essentials I seem to be missing?

shshaw commented 8 years ago

Regarding missing features:

Most of those I have an implementation of, but I've trimmed out. These specfic items wouldn't add much weight.

tommiehansen commented 8 years ago

Reverse is good for playing something in reverse and is helpful. Need options to change duration and easing though since one often want to animate in with A but out with B.

elem.animate({ y: 0, opacity: 1 }, { duration: 500, easing: easeOutQuad }) elem.reverse({ duration: 250, easing: easeInQuad })

Another thing is staggering which is always useful when one animates a group of elements.

Examples http://codepen.io/tommiehansen/full/QNRYJR/ http://codepen.io/tommiehansen/full/ZWNRoY/

shshaw commented 8 years ago

Cool demos, @tommiehansen. I think having iterations and direction properties makes sense, and shouldn't add much weight.

tommiehansen commented 8 years ago

Btw -- seeing as the Web Animations API is on the rise an idea would be to use that syntax because that would make it much more easy to transition over in the near future. Chrome and Firefox already have support for it and the MS Edge team has flagged it from 'under consideration' to 'medium priority'.

The syntax is pretty straight forward and it shouldn't be too hard for anyone that have ever used something like jQuery to use WAAP.

shshaw commented 8 years ago

I'm making a cross between the Web Animations API and the jQuery.animate API to allow for complete and start callbacks. I am also not supporting the full keyframes interface like the Web Animations API, but simply animating from the current state to the state provided as the first parameter.

Here's the latest prototype. Still a bit rough at the moment, but it is working with objects and elements (with and without Element.animate) and supports transforms.

// Animate from the current state to these properties
var animateTo = {
        x: '200px', // translateX shortcut
        scale: 0.5 
      },

      animationOptions = {
        // jQuery-like animation options
        start: function(){ console.log('Animation has started'); },
        complete: function(){ console.log('Animation is complete!'); },

        // jQuery/Web Animation API equal options
        easing: 'linear',
        duration: 400,

        // Web Animation API style options
        iterations: 2,
        direction: 'alternate',
        delay: 1000
      };

$('#myDiv').animate( animateTo, animationOptions );

The Web Animation API's fill was preventing further transform animations from working, so I have to manually apply the styles after the animation is complete in order to prevent the element from jumping back to the original state.

Feel free to fork & play around with that. Let me know if you catch any oddities. I'm aware of a few issues, but the functionality is about there.

tommiehansen commented 8 years ago

Tested it briefly. The easy stuff, A -> B seems to work as expected.

But something that's immediately noticable is that the complete function is broken since it fires once per item in a collection. So complete: function(){ myComplexFunction(); } would fire once per item in a collection. Need to check length of collection and fire the complete action after the last iteration has completed its animation.

If it fired once per collection it would be easy to do animate 1 > 2 > 3 like this: http://codepen.io/tommiehansen/pen/Razoex?editors=0010

...or even write a little function that iterates over an array of frames and just fires them one after the other.

shshaw commented 8 years ago

For jQuery.animate: "If supplied, the start, step, progress, complete, done, fail, and always callbacks are called on a per-element basis;", which is inline with Web Animation API's individual animations and finish event.

If that's how we proceed, then this for the callbacks should be the element being animated, so as in your example, you could do this:

el.animate(frames[0], {
    easing:     opts.easing,
    duration:       opts.duration,
    complete:       function(){ $(this).animate(frames[1], opts); }
});

Perhaps two separate callbacks are needed: complete for each element and collectionComplete (or allComplete?) run when the collection is finished animating, which would be especially useful if a stagger is implemented.

I have a newer prototype that's trimmed off a full kb from the minified version (from 4.5kb to 3.3kb) while retaining the current functionality, and the code should be a little easier to parse.

It's still likely that we'll need to implement a transform matrix conversion, and I'm looking at a Bezier curve convertor that should allow us to have parity with CSS easings.

shshaw commented 8 years ago

I've updated the original issue with the essential features list and some possible features. Take a look and let me know if anything we've talked about is missing or any other features you've thought of.

shshaw commented 8 years ago

Latest prototype up. 4kb minified, 2kb gzipped with staggering, Bezier conversions and Transform matrix calculations (buh, what a nightmare).

The current CSS bezier conversions for ease, ease-in-out, etc, aren't 100% identical. Need to tweak the curve values.

tommiehansen commented 8 years ago

You work fast, trying it out now. :)

tommiehansen commented 8 years ago

Edit: It seems to work great, the only caveat is of course the lack of being able to easily create more then two frames since multiple complete quickly becomes very messy. See my updated example at http://codepen.io/tommiehansen/pen/Razoex?editors=0010

You said that you'd leave out support for it though so it isn't a bug or fault per say.

Velocity.js does have it in this regard with its sequence, check out a very old example i made: http://codepen.io/tommiehansen/pen/netCu?editors=0010

Click the Demo loop (infinite) button to play all the sequences. As an example only a single click on the button "popular photos" triggers 4 animations (0: scale down/up button, 1: slide out button, 2: slide in div, 3: fade in images with staggering).

shshaw commented 8 years ago

Glad it's working as expected! Feel free to run some performance tests, especially compared to jQuery.animate and Velocity. I'm not very familiar with Velocity.

I haven't ruled out a allComplete callback yet, but I'm trying to figure out the appropriate way to implement it. I think a callback is necessary per element, especially with stagger, but not sure what the best way to distinguish the two would be.

Any thoughts?

tommiehansen commented 8 years ago

Compare against jquery.animate seems redundant. No one really uses jq animate for 'real' animations. Against velocity it would be raw js animations vs css3 animations, there are already stuff on that. The big diff is that css3 animations is native to the browsers which, in my opinion, is always better.

Something that is slighly urgent, for dev-purposes at least, is some sanity check for infinite loops. My browser constatly hangs when trying loops at Codepen.

The complete per element is quite odd. Depends on how one see an animation. I don't really have an answer on that except that it depends on if one see a "frame" or an animation as something that occurs (which it technically do) on multiple items.

Another "problem" with just A > B animations is that these can already be done sufficiently in plain CSS and JS just like @kenwheeler said and if one needs simple callbacks one could just include something like ba.js, https://github.com/Arood/bajs

Before you write more code maybe you could/should check out how the WAAPI handles these things, this is a good guide: http://danielcwilson.com/blog/2015/07/animations-intro/

tommiehansen commented 8 years ago

In trying to really test the latest build i began trying to recreate http://codepen.io/tommiehansen/pen/netCu?editors=0010

..here: http://codepen.io/tommiehansen/pen/yOdEZm?editors=0100

Opacity doesn't seem to work for some elements and the lib quickly becomes annoying due to the lack of any sequencing, que-system or timeline of any sorts. Even the old jquery.animate handles ques (chaining):

element
  .animate(props, opts)
  .animate(props, opts)
  .animate(props, opts);

But chaining in that way isn't really good because that too becomes a bit too much as soon as one reaches over 4 frames, that's why libs like Velocity js got a sequence func:

var frames = [
  [{ props }, { opts }],
  [{ props }, { opts }],
  [{ props }, { opts }],
]

$.runSequence(frames);

A diff in Velocity JS is also that it reads the previous state, something that WAAPI doesn't seem to do at the moment. This makes it easier to create sequences since one doesn't need to write all props for each and every frame within a sequence.

Also note that everything 'fun' in Velocity JS requiers the UI-extension which adds even more bulk (33kb more).

shshaw commented 8 years ago

Not sure what the issue with Infinity iterations would be, but I've added a check that may help. It's working as expected for me, so can you send an example of the freezing?

I have been following the WAAPI documentation and implementing everything as close to that as possible.

Regarding callbacks, WAAPI animations are all per element, and the finish and cancel event callbacks are also per element. CSS animations callbacks via animationend events are also per element. Again, I'm not ruling out an allComplete, just explaining why a complete ( or finish ) per element is needed.

The real advantage with $.fn.animate and other animation engines is being able to set up and call animations on the fly. A to B animations in a programmatic way, (not known ahead of time, as a CSS animation could handle that) especially with looping and cross browser support are not exactly simple in plain JavaScript, unless there's a technique I'm unaware of, for a few reasons:

I do like ba.js's way of creating a CSS animation that's applied to the element. A method like that could potentially save a good bit of code, and would likely be a worthwhile fallback all the way to IE10. However, JS animations would override existing CSS animations and could not be combined, though that may already be the case when animating transforms in particular.

Overall, it sounds like you mainly want a way to set up more complex animations in a CSS keyframes style syntax, which may be too far out of scope for this.

shshaw commented 8 years ago

Sorry, was posting that as your latest message came through, so there may be some redundancy.

Sequencing isn't far fetched in the manner which you describe ( array of arrays featuring properties and options ), as it would just be re-calling Animate for the next set of frames at the end.

tommiehansen commented 8 years ago

Ok, then implement an allComplete, allDone, or something that somehow describes that it is something that is triggered after an array of elements has finished animating.

Sequencing and/or simplified sequencing would be awesome.

Don't feel that what i write about is too complex. Sequencing is basically just a check for the last elem on the next 'frame' as you say. What you could do is check if the props is an object or an array and then just trigger sequencing if it's the latter.

shshaw commented 8 years ago

It's on the list. Still figuring out the best way to implement and best naming conventions, so any suggestions are welcome.

I'm also seeing some issues with opacity. Not sure where that's coming from.

tommiehansen commented 8 years ago

What you could do is to try to re-create my re-creation to get a better feel of the general pitfalls: http://codepen.io/tommiehansen/pen/yOdEZm?editors=0010

shshaw commented 8 years ago

Just updated the prototype with an allComplete callback.

Trying to figure out the opacity bug currently, but the overall setup now that allComplete is in place is better for a simple sequencing setup.

The expectation would be that the next sequence event would fire after all staggered elements have finished animating, correct?

shshaw commented 8 years ago

Ah! Looks like I was removing some properties from the original end object, so opacity would only apply to one.

The prototype has been updated. 3.83kb minified, 1.98kb gzipped

shshaw commented 8 years ago

Tommie, give the latest prototype a try. I think you'll like it ;-)

Compiled Size: 2.05KB gzipped (3.98KB uncompressed)

Probably need to change the callbacks to be frameComplete and allComplete

tommiehansen commented 8 years ago

Yes, that would be the expected behaviour. Has nothing to do with staggering though but rather has to do with collections, collections can be anything like $('h1, p, .box')

Say one would want a startpage where one slides up a bounch of elements and after that has completed show a background image or start a video in the background. That's something that is more common then my box-thing.

tommiehansen commented 8 years ago

Yes, i also saw you added the whole lib as a pen so that it can easily be included -- great. :)

Trying the card-animation with the new stuff now.

tommiehansen commented 8 years ago

Your changes really brings the code needed to written way down now, but i still hit a speed-bump quite early since there is no simple way to run sequence A on elem 1 and then sequence B on elem 2 without the code starting to get messy. Now it requires one to add "allComplete" to the animation frames themselves or maybe i've misunderstood how it works.

var myFrames = [
  [{ y:0 }, { duration:100 }],
  [{ y:100 }, { duration:100, allComplete: function(){ /*messy */ } }],
];

For ones sanity one really want to separate complete-calls away from any sequencing:

elem1.animate( myFrames, { allComplete: function(){
  elem2.animate( otherFrames );
})

Even that quickly becomes messy though, so:

var myFrames = {
  [{ elem1, prop2, prop3 }, { opts1 } ],
  [{ elem1, prop2, prop3 }, { opts2 }],
  [{ elem2, prop2, prop3 }, { opts3 } ],
};

$.animate( myFrames, { allComplete: function() { console.log('finished sequence'); }})

But the latter would move the entire thing away from the WAAPI which might not be that wise. The WAAPI is hardly the gold standard though, there's tons of stuff that's still missing and the "grouping" and "keyframeEffects" (effects ... ?!) is odd and alien at best; http://danielcwilson.com/blog/2015/09/animations-part-4/

shshaw commented 8 years ago

Yes, you can include $.fn.animate using this pen.

To clarify some behavior:

// You can use one keyframe & options object.
$('h1').animate({ x: 200 },{ duration: 1000 });

// Or you can use a keyframes array
$('h1').animate([
  { x: 200 } // keyframe 1 (not the starting state, but the first keyframe to animate to)
  { x: 100 } // keyframe 2
  [{ x: 0, rotate: 360 },{ duration: 500 }] // keyframe 3, with custom options. These options will be merged via `$.extend` with the global options.
],
{ duration: 1000 } // Options for all keyframes
);

Right now, I'm working on the callbacks and this is what I have so far:

        start: noop, // animation start
        complete: noop, // animation complete

        itemStart: noop, // an item's animation has started
        itemProgress: noop, // progress on any item in the animation
        itemComplete: noop, // an item's animation has completed

        frameStart: noop, // a new keyframe has started
        frameComplete: noop, // a keyframe has been completed

The prototype has been updated with the new callbacks.

tommiehansen commented 8 years ago

With the new stuff a complete' in a sequence seems to act as a allFramesComplete so this sort of works:

$('button').on('click', function(){

    var $t = $(this);

    $.animate($t, buttonFrames, {
        complete: function(){
            $.animate($('.back'), backFrames, {
                complete: function(){
                        $.animate($('.backImg'), { opacity: 1 }, { duration: 200, stagger: 60 })
                }
            });
        }
    });
})

But it gets quite messy quite quickly. It does almost work though, the opacity for the images last in the que refuses to animate for some reason.

Edit: They do actually animate but only if i also include a transform value.

shshaw commented 8 years ago

Yeah, complete means "the entire animation has complete", reference my list above for the callback definitions.

This isn't intended to be TimelineMax or anything. Not sure how sequencing is handled in Velocity (I cannot parse their documentation, what a mess!), but sequencing multiple animations across different elements is a bit beyond the scope of this. The 'callback soup' like you have would be how you'd handle that for moderately complex animations. Anything beyond that, and you'd be better served by a full animation library.

shshaw commented 8 years ago

The opacity issue may be related to a cash bug I've discovered. You can't set $(el).css({ opacity: 0 }) or any other zero / false-y value. Looks like another version push is coming :-)

tommiehansen commented 8 years ago

It almost is great though, so props for that. :-) And that it is almost TimelineMax isn't such a bad thing. Even if only using one callback to trigger animation #2 it's still much more then most libs do or can do comfortably.

So now http://codepen.io/tommiehansen/pen/yOdEZm?editors=0010 actually is working even if the callbacks are a bit messy. Click "Push me" to trigger the longer sequence.

shshaw commented 8 years ago

Great demo. Love the animations.

If you have any ideas for a simple way cross-element animations could be queued outside of callbacks, please let me know. I can't parse how Velocity does it, and I think GSAP's syntax would be too much for this.

tommiehansen commented 8 years ago

Yes -- no GSAP syntax. :-)

I'll experiment with it more as soon as i got time (which equals the entire weekend since my girl is working). It fits the bill as well since i've been scouring the internet for a decent and non-bloated animation engine so that i don't have to write or include helper1, helper2, helper3 each and every time i want to do more then a simple statechanges aka "A > B animations" (ie the stuff that would end up in a stylesheet anyway).

That's why i began writing small libs such as animation.stagger, demo: http://codepen.io/tommiehansen/full/ZWNRoY/ full src: https://github.com/tommiehansen/base/blob/master/js/animation.stagger/animation.stagger.js

Which basically just loops through elements and adds animation-delay or transition-delay but also has the option to end staggering after n elements. It leaves all other things to CSS and is great if one just want to add staggering without resorting to :nth-child or some ugly mixin in SASS and LESS.

tommiehansen commented 8 years ago

Any way to reset an element?

shshaw commented 8 years ago

A zero duration animation that resets the original state? :-)

No, currently it acts as fill: both, so whatever the end state is (accounting for direction: alternate), that's applied.

I am looking at implementing a stop method to allow for killing infinite animations, just haven't integrated that yet. On Sat, May 21, 2016 at 2:14 AM Tommie Hansen notifications@github.com wrote:

Any way to reset an element?

— You are receiving this because you were assigned. Reply to this email directly or view it on GitHub https://github.com/kenwheeler/cash/issues/119#issuecomment-220763280

tommiehansen commented 8 years ago

Yes -- i "solved" it that way, but it's not very intuitive though. :)

shshaw commented 8 years ago

Looks like the WAAPI equivalents would be:

var player = element.animate(/* ... */);
console.log(player.playState); //"running"

player.pause(); //"paused"
player.play(); //"running"
player.cancel(); //"idle"... jump to original state
player.finish(); //"finished"...jump to end state

The Animator class is being returned by $.fn.animate, so prototype methods should be able to allow for that. Play/pause may be tricky for the requestAnimationFrame fallback, but I've done it before. On Sat, May 21, 2016 at 6:37 AM Tommie Hansen notifications@github.com wrote:

Yes -- i "solved" it that way, but it's not very intuitive though. :)

— You are receiving this because you were assigned. Reply to this email directly or view it on GitHub https://github.com/kenwheeler/cash/issues/119#issuecomment-220773070

tommiehansen commented 8 years ago

Yeah, i read something about that too.

And actually -- if you want to go the WAAPI way i read ... somewhere that it actually uses the syntax that i nearly proposed before;

var myFrames = [
  [elem1, {  props }, { opts }],
  [elem2, {  props }, { opts }],
  [elem3, {  props }, { opts }],
];

It would seem they've been inspired by Velocity JS or any other that uses the same syntax for sequences.

tommiehansen commented 8 years ago

Tried a little and this actually works, it's slighly ugly and i have no idea if it will works with multiple frames within each 'frame' ...

function seq2(sequence, curNum, len){

    curNum = curNum || 0;
    len = len || sequence.length;

    var cur = sequence[curNum];

    if(curNum+1 < len ){
            cur.opts.complete = function(){ seq2(sequence, curNum+1, len); }; 
    }
    // trigger animation
    cur.e.animate( cur.frame, cur.opts )

}

The func() compresses down to around 131 bytes (without gzip) but i'm guessing that you can do it much better. :-)

Edit: Added codePen for it http://codepen.io/tommiehansen/pen/LNwPvx?editors=0010

tommiehansen commented 8 years ago

Demo of the func in action and with using lots of sequencing: http://codepen.io/tommiehansen/full/bpXNPr/

Hit the 'Play all' -button to play all.