omniscientjs / omniscient

A library providing an abstraction for React components that allows for fast top-down rendering embracing immutable data for js

Home Page:http://omniscientjs.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Async Animations

ccorcos opened this issue · comments

So this is kind of a mouth full -- I wasn't planning on writing something this long, but it happened -- sorry about that. Let me know if you're interested in this idea and would like to help, collaborate, or have any advice on it. Thanks!


I just ran into this project and I've been working on this sort of pattern with React for a few weeks now -- a global top-level app state and a render function. Its working very well for me and I'm glad to find someone with the same idea.

Anyways, one of the main reasons I ended up with this pattern was to facilitate animating the user interface. I'm not going to argue over "web vs native", but I think one of the biggest factors holding back the web is animation. We're used to refreshing web pages if something gets messed up, and we're used to seeing un-animated changes to the UI (even Facebook does it), but that's unacceptable in the mobile app world (probably because Apple set the bar so high) -- so that my pep talk. If you guys think this is interesting, I'd love to collaborate.

Here's what I have in mind so far -- but everything is up in the air. There's a global refs object which holds references to all the views (React components). If theres a view called home, then to get the header ref (thats inside home), we can specify a path like home.header. I have a function that does all the traversing and wraps with jQuery called $ref. When a the user performs an action, like transition to another page, I could do a simple slide animation like this using Velocity.js:

// segue from home to profile
animation = function(done) {
  $ref('home').velocity('transition.slideLeftOut', {
    complete: function() {
      updateState({route: 'profile'})
      render(function(){
        $ref('profile').velocity('transition.slideRightIn', {complete:done})
      })
    }
  })
}

updateState updates the global application state (I used to deep-clone the state to keep it immutable, but now I used the optimization you guys mentioned ;) ), and render will render the top-level component with the top-level state (React.render has a callback option after rendering). Now we have a transition from one page to the other. This same concept could be used to fade in and out error messages, adding messages to a chatroom, receiving updated data from the server, etc.

Now that we have an async function the animates a change to the UI, we need to make sure that they don't clobber each other. For example, if we are changing an error message in the UI and transitioning to another page at the same time, the render functions might clobber each other... Thus, the workhorse that I'm trying to build is going to be a class that arbitrates these animations -- it will run animations in parallel, queue animations, and drop animations as necessary. To respond to a user action or some data form the server, we may do something like this:

enqueueAnimation('transition from home to profile', animation)
// or
enqueueAnimation('error on home', animation)
// or
enqueueAnimation('new message', animation)

When an animation is "enqueued", we can use regular expressions on the key to determine what to do with it. My idea is to represent animations as a sort of linked-list graph. Any animations that are currently running exist in an array called current (the roots of a tree/graph). Each animation has a next property which references the animation (async function) that will run after it finishes. By traversing this graph, we can figure out what to do with the animation.

For example, if we have an application in which data X, Y, and Z can change in parallel without clobbering each other, we may have a current array of [X,Y,Z](the objects representing those animations). Suppose X changes very quickly many times (maybe the user is spamming a button). Maybe we dont care about the intermediate states and only want to show the user the latest value of X. We can queue up only the latest change to start after X finishes, by setting it to the next property of the current X animation. Or if you want to queue up all the changes to X, you can simply traverse to the end of the X chain, and set this animation to the next value, queuing everything up. Now suppose you want to transition to another page. We want the current animations (X, Y, and Z) to finish before executing the transition so they dont clobber each other, but we dont care about the queued animations that havent started yet, so we set set the next property of all animations in the current array to call the transition function using something like nthCallOf(current.length, startTransition). All the other animations will be orphaned and cleaned up by garbage collection. Then suppose, the user tries to execute another transition before the transition is complete. This would clobber the transition thats already queued up. So we can traverse the graph, and if we see a transition already lined up, we can forget this animation. When we get X, Y, and Z updates before or during the transition, we can drop those as well.

That was an interesting read.

I was about to ask if you had a reference implementation till i realized this was a thought experiment.

I'm unsure if something like $ref('home') is the right way to go, and I'm thinking that there may be a more declarative approach.

So I built something yesterday... Its not finished yet though. The thing with animations is they're inherently very procedural and can be pretty complicated.

What do you mean, more declarative? There has to be some way of grabbing elements from React...