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

animations #487

Open
yoshuawuyts opened this issue Apr 28, 2017 · 11 comments
Open

animations #487

yoshuawuyts opened this issue Apr 28, 2017 · 11 comments

Comments

@yoshuawuyts
Copy link
Member

yoshuawuyts commented Apr 28, 2017

Been thinking about how to do animations a lil, and think having a component with Nanomorph would be the right place to add them. We should be able to do stuff like this at 60fps:

react-f1-chief

This is my current state of thinking; I'm not entirely sure how to approach this yet but I think laying out the constraints can help us with figuring out how to approach this. Apologies if I'm rambling a lil here, figured it'd be better to share notes & thoughts in the open, even if they're not as polished as they could be.

Constraints

There's a few things I think would be cool if we could pull off:

  • animate individual components
  • create complex full page animations with multiple components
  • have it run smoothly at 60fps
  • try and keep the new terminology and APIs introduced to a minimum
  • find a way to allow this to work for modals and (infinite) list elements too

Component lifecycle events

Nanomorph takes care pretty well of rendering things on a screen. If you want to add an element, it'll take care of it with little trouble. However, it can't pause element mutation, which is crucial for animations. For example, say an element has a fade-in effect when it's first rendered. If we re-render midway through the animation it'll look weird. So we want to make sure the animation completes before any further mutations occur. Stuff like a fade-out before a component is removed isn't even possible right now, so we should allow for that.

Types of events

There's generally 3 types of events that happen¹:

  • component will be rendered for the first time
  • component will be rendered, but it's not the first time
  • component will be removed from the DOM

Each of these events should be handed a lock that can be cleared using a callback. E.g. somewhat similar to:

component.on('rendered-for-the-first-time', function (done) {
   this.component.classList.add('my-500-ms-transition')
   setTimeout(done, 500) // tell we're done animating, allowing mutations to trigger again
})

While an animation is in progress, other animations and Nanomorph mutations should be put on hold.

The leave event

Because we just diff DOM nodes and don't do extra lifecycle tricks, we can't prevent elements from being unmounted. This means we can't trigger any animation when an element is removed from the DOM. To counter this we could, however, leave an element in the DOM that can instrument between its child elements and trigger hooks on each of them.

I think the approach might be to have some form of scheduler live in the DOM, that doesn't get unmounted so it can have its own Nanomorph instance. It can then do fine grained scheduling for its child elements, exposing all sorts of events².

Instrumenting Nanomorph through locks

As mentioned above, we would hand animation handlers a lock which they can free at the end of an animation. This is so we can make sure animations complete and don't conflict. E.g. imagine this scenario:

  • element is rendered for the first time, triggers the "enter" animation
  • element is marked to be removed from the DOM, triggers the "leave" animation

In this case we'd want the "enter" animation to complete, and only then mark the element to be removed from the DOM. Now, we can't immediately unmount the element - we need the animation to complete first. So we need a way to prevent the element from being removed.

Maintaining 60fps

ideally we'd be able to schedule the animations to the best of our abilities. E.g. not smush them all together in the same frame - but perhaps even better: schedule them when there's free time available. Something interesting to experiment be would be to find a midway: try and run the animation with window.requestIdleCallback() but run it anyway if it takes longer than 10ms or something. If every component would employ something like this, then things should run relatively smoothly (is my guess). Worth experimenting with at least haha.

Instrumentation

So far we've only talked about how to allow transitions to work with nanomorph. Sometimes we'll need to instrument full page animations³. In react-f1 there's a distinction between individual element renders, and full page instrumentation. Another good example of this would be jam3.com.

The way f1 / react-f1 does it is by declaring a finite state machine, and using a pathfinding algorithm to navigate between the nodes. If you do several layers of this, you can create pretty wild animations. An example of a pathfinding algorithm is jkstra for Dijkstra / A* pathfinding.

Unanswered questions

A tricky thing with Nanomorph is that we can't have multiple on-load handlers on any given node so we're slightly constrained in that regard. But there's a few more things I think we should try to figure out:

  1. is it possible to have a "pure" scheduler - e.g. one where we don't need to mount any wrapper components on the DOM?
  2. What does a application level scheduler look like, and how does it interact with choo's events?
  3. How can we integrate elements like infinite lists and modals with animations and application level schedulers?

Wrapping up

I hope this somewhat explains the constraints my thoughts on the matter, and helps start a discussion on how to best approach animations. Thanks heaps for making it this far, and keen to hear your thoughts!

See Also

edit: meant nanomorph, not morphdom haha

@YerkoPalma
Copy link
Member

YerkoPalma commented Apr 28, 2017

Quick brainstorming:

I think animations are awesome, so good to read this. Also think that route transitions should be treated completly different to component transitions, I see two groups there.
Speaking about route transitions, I like the old vue-router phases definition, I can imagin some 'entering' and 'leaving' events in choo router, since nanorouter is event-emitter like, it should be really natural to introduce those events.

I'm not clear why is needed a finite state machine. I guess that a priority queue would be enough to define animations order, a global one for route transitions, and one per each component. Also guessing here haha.

See also

@hugozap
Copy link

hugozap commented Apr 28, 2017

Morphdom takes care pretty well of rendering things on a screen. If you want to add an element, it'll take care of it with little trouble. However, it can't pause element mutation, which is crucial for animations

In the example you start the animation by adding a CSS class, this way the animation state lives inside the css execution engine which makes it difficult to synch with re renders. This won't happen with style properties that are set from the component state. Re render won't be a problem because each time the component gets rendered the interpolated property value will be set so pausing/resuming will be possible.

Similar to what react-motion does, the animation layer can handle timing and interpolation and pass the values that will be updated by nanomorph.

(Not sure if this makes any sense for the choo architecture)

@YerkoPalma
Copy link
Member

In the example you start the animation by adding a CSS class, this way the animation state lives inside the css execution engine which makes it difficult to synch with re renders.

@hugozap There are the transitionend and the animationend events to interact with the css animations. In fact, I think it is a MUST to handle those.

@kadmil
Copy link

kadmil commented Apr 28, 2017

Hey, just two things to mention:

  1. Maybe snabbdom (https://github.com/snabbdom/snabbdom) could help with ideas on how to handle animations for lifecycle hooks? It's a vdom-thing behind cycle, and vue works on it's fork.
    Code here: https://github.com/snabbdom/snabbdom/blob/master/src/modules/style.ts

  2. For CSS-animations, we have dom events to listen to (https://css-tricks.com/controlling-css-animations-transitions-javascript/#article-header-id-2). Events look like animationstart, animationiteration, animationend, and transitionend , with more on this here https://www.sitepoint.com/css3-animation-javascript-event-handlers/

@hugozap
Copy link

hugozap commented Apr 28, 2017

@YerkoPalma Does this means that additional DOM mutations have to be disabled while the animation ends to avoid issues with re render? This is a big constraint (with css based animations).

@yoshuawuyts
Copy link
Member Author

Thanks so much for all the links so far! - Feel like I haven't considered a bunch of things (e.g. aborting animations and going back) - so reading through all the links you've linked so far and figuring out how to do these things haha. Also had no idea about those DOM APIs - it'll take a lil while to work through it though I think haha. Will keep iterating on the main post to include the stuff y'all have been linking!

@YerkoPalma
Copy link
Member

@hugozap More than disabled, I think paused is the word, and in some case, some animations could cancel/disable other DOM mutations (i.e. route transitions should cancel component mutation IMO)

@hugozap
Copy link

hugozap commented Apr 28, 2017

@YerkoPalma For me the issue with CSS based animations is that things get complicated (or just not possible) for complex escenarios (staggering, composition, speed changes) , this in part is due to the animation information (e.g timing, easing) being hardcoded in the CSS declaration and not as part of the app state. I'm in favor of treating animation parameters like any other UI state data, so the same data flow ( state -> view ) can be maintained. With a non-css based approach to animate something just make sure to update the state as you wish and the animation layer will interpolate values that can be passed to modify the style attribute.

@YerkoPalma
Copy link
Member

YerkoPalma commented Apr 28, 2017

In fact you can manipulate css animations from javascript, so you could include them in the app state, but, IMO, things can get a litle hacky from here.

Links

@yoshuawuyts
Copy link
Member Author

PS if anyone's experimenting with any of this it'd be super cool if you posted back into this thread. Got a feeling that we can get this done soonish if we share notes :D

@yoshuawuyts
Copy link
Member Author

Oh by the way, updates!

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

No branches or pull requests

4 participants