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

How to implement (performant) animations #11

Open
queckezz opened this issue Dec 31, 2015 · 27 comments
Open

How to implement (performant) animations #11

queckezz opened this issue Dec 31, 2015 · 27 comments

Comments

@queckezz
Copy link
Member

I'm trying to recreate the material design ripple effect with canvas and vdux. The actual code for the effects works just fine and now I need to wire it with vdux. So basically I create an initial array for all effects currently happening in the canvas via virtex-local:

const initialState = () => ({
  ripples: []
})

Adding a new ripple effect is pretty easy, do some math based on the canvas element and the coordinates of the click and finally dispatch an action.

const render = () => {
  let canvas

  return (
    <button class={buttonStyle} onClick={e => {
      const { x, y, scaleRatio } = computeScaleRatio(e)
      return local(addRipple)({
        x,
        y,
        scaleRatio
      })
    }}>
      Click to Ripple

      <canvas
        class={canvasStyle}
        style={{ width: percent(100), height: percent(100) }}
        hook={ el => canvas = el}}
      >
      </canvas>
    </button>
  )
}

With the corresponding reducer you will now have state.ripples available. So now comes the tricky part. I need to start the actual canvas render loop.

const render = ({ state }) => {
  state.ripples.length != 0 && startAnimation(state.ripple)
}

How do I remove the ripple effects from the array again when there finished animating? there is no way to dispatch an action again.

const startAnimation = (ripples) => {
  // start animation
  // at some point remove ripples from the array
  setTimeout() => local(removeRipple(ripple[i])), 750)
}

Maybe virtex-local is not the right tool for that? I thought it would be extremely convenient, if we already have a state managment system available, to also use it for animations.

@ashaffer
Copy link
Collaborator

Ah, so, I use redux-effects for all of my effectful things, timeouts included. I didn't realize I had sort of forced that pattern on people here. If you use redux-effects-timeout though, you can do:

return setTimeout(() => local(removeRipple(ripple[i[)), 750)

Or you can just make your own timeout middleware that works however you want. For now that's what i'd recommend doing.

EDIT: Also, do you know that you can just wrap your entire event handler with local? E.g. local(e => ...). Or do you prefer executing it immediately like that? I'm still not sure I have the most natural API there.

@queckezz
Copy link
Member Author

Ah! You still can't dispatch actions in startAnimation() since it wasn't fired by an event handler (where you can return an action), right?

Also, do you know that you can just wrap your entire event handler with local? E.g. local(e => ...). Or do you prefer executing it immediately like that? I'm still not sure I have the most natural API there.

Oh, no haven't thought of that. Well to be honest, I don't see the difference in those two ways.

@ashaffer
Copy link
Collaborator

Ah! You still can't dispatch actions in startAnimation() since it wasn't fired by an event handler (where you can return an action), right?

So what you do is return a timeout action, which then gets executed by your middleware stack, and then dispatches the result of your timeout callback. The timeout middleware just looks essentially like this:

function mw ({dispatch}) {
  return next => action => action.type === 'TIMEOUT' 
    ? setTimeout(() => dispatch(action.fn()), action.timeout) 
    : next(action)
}

Oh, no haven't thought of that. Well to be honest, I don't see the difference in those two ways.

There's no functional difference, just syntactic. To me doing local(...)(...) inline is kinda annoying to write but if you're ok with it it works just fine.

EDIT: Also, I wonder if there is a good way to virtualize canvas manipulation, so that you don't have to do raw DOM stuff here. Do you have any thoughts on this? It'd be great to be able to do all these animations in a totally pure way.

@queckezz
Copy link
Member Author

Yes that makes sense. I was wondering tho, how do I dispatch an action in other functions than event handlers. Sorry if I haven't stated the problem clearly. You can return actions in event listeners but you can't in functions called at let's say the beginning of render like startAnimation()

const render = ({state}) => {
  state.ripples.length != 0 && startAnimation(state.ripples)
}

const startAnimation = ripples => {
 // can't return actions here, is there a way to do dispatch(action()) ?
}

Also there is no middleware for local actions?

@ashaffer
Copy link
Collaborator

Yes that makes sense. I was wondering tho, how do I dispatch an action in other functions than event handlers. Sorry if I haven't stated the problem clearly. You can return actions in event listeners but you can't in functions called at the beginning of render like startAnimation()

I'd suggest you call that in the hook that gets canvas maybe. So like:

import {timeout} from 'redux-effects-timeout'

function render ({local}) {
  return <canvas hook={e => timeout(local(startAnimation), 100)} />
}

Something like that maybe?

Also there is no middleware for local actions?

There is no custom middleware for local actions, but if you do this: return timeout(local(startAnimation)) or anything else that passes a local wrapped function someplace, it will work in the middleware system just fine.

EDIT: Except I just realized the return value of attribute hooks is not dispatched. You can address that by either using another lifecycle hook (e.g. beforeUpdate/afterUpdate), or we can try to figure out an abstraction that makes sense here with respect to attribute hooks. A simple solution might just be passing dispatch into the attribute hooks, since they are already given access to the DOM nodes, imperative dispatching might not be so bad there.

EDIT2: This kind of depends on what you need to actually do in your event handlers. I see that you're saving the value of canvas inside your render closure. What do you intend to do with it there?

EDIT3: I think this is an instance of a pretty general issue. Would you mind posting a complete code example of how your animation works. I'd like to try to come up with a general solution for this type of problem, but I don't do much stuff with canvas so i'm not sure what the best way to model the problem is.

EDIT4: Sorry I also realized I didn't actually directly answer your question.

I was wondering tho, how do I dispatch an action in other functions than event handlers.

You don't, ideally. Every action should be returned by some function, either a lifecycle hook, user-generated event, or some synthetic thing originating in middleware (e.g. timeout, websocket). In this way, you never actually imperatively cause anything, you just create descriptions of what you want done to be executed in a well-defined way by middleware. This pattern is how you make interacting with the real-world pure and functional.

The trouble you're having here is that there is a mismatch between what you want to do and the lifecycle hooks, etc. that i've made available to you. We just need to sort out what the right abstractions necessary for doing what you want to do are.

@queckezz
Copy link
Member Author

queckezz commented Jan 1, 2016

Thanks for taking the time! Your above approach won't work unfortunately but I haven't give you enough context.

A simple solution might just be passing dispatch into the attribute hooks, since they are already given access to the DOM nodes, imperative dispatching might not be so bad there.

Yea I created a gist with my situation + full code better explained. I hope we can figure out if it's really needed to expose dispatch() or I'm just misunderstanding the architecture.

EDIT3: I think this is an instance of a pretty general issue. Would you mind posting a complete code example of how your animation works. I'd like to try to come up with a general solution for this type of problem, but I don't do much stuff with canvas so i'm not sure what the best way to model the problem is.

Here ya go: https://gist.github.com/queckezz/91abb31cd43e4887ecf4

@ashaffer
Copy link
Collaborator

ashaffer commented Jan 1, 2016

@queckezz Just a dummy comment to trigger a notification for you on the gist. Made a post there :). So annoying that gist comments don't trigger notifications.

@queckezz
Copy link
Member Author

queckezz commented Jan 2, 2016

Awesome! I'll try your suggestions. Looks like it's feasible with beforeUpdate

@ashaffer
Copy link
Collaborator

@queckezz Hey man, I just added an animation example that tries to recreate your button thing. I think its pretty close. Let me know if there is some material difference that I missed. The example is pretty rough right now, I want to do some thinking about good patterns around animation for vdux so that it's as pure as possible, at least from the component's perspective.

Also added hot reloading support fyi, its pretty great.

@queckezz
Copy link
Member Author

Also added hot reloading support fyi, its pretty great.

🍺

This looks pretty good. Couple of points though:

  • Animating the left top properties of a dom element is reaaaally slow. As you can see oncsstriggers.com it triggers layout, paint and composite which is really expensive atm. A better alternative would be transform which triggers only composite (no geometry changes): translate3d(x, y, 0).3d` also triggers hardware acceleration for even smoother performance.

Changing left alters the geometry of the element. That means that it may affect the position or size of other elements on the page, both of which require the browser to perform layout operations.
Once those layout operations have completed any damaged pixels will need to be painted and the page must then be composited together

  • Animating the width for the initial value change of the property is also expensive because it triggers all 3 browser operations. Better alternative would also be transform: scale3d(r, r, 0).
  • maxSize is something that needs to be computed and can't be a static value for the material design ripple effect. Based on where you press you need to find out the furtest point to which the ripple needs to expand.
  • The reason I picked canvas over raw dom elements is because for each ripple effect (and removing a ripple effect) it causes a dom mutation. Spamming a button for example may cause some performance issues on mobile platforms. What do you think?
{state.ripples.map(ripple => <Ripple key={ripple.id} {...ripple} onEnd={local(removeRipple, ripple)} />)}
  • node.style[key] = val is still a side effect that needs to be handled externally or do you think this is a minor thing? React is doing it declaratively, where a callback is passed to a component each time a frame happens (see react-imation) for example. I think that would be quite a nice approach.
<Timeline
  playOnMount={true}
  min={0}
  max={100}
  loop={true}>
{({time, playing, togglePlay, setTime}) => {
 // use time with tween() or smtn.
}</Timeline>

Im not at home atm so I can't create a pull request. Feel free to apply the changes if needed or I'll push something later.

I'm reallly happy about that example though. It finally helped me to crasp how you handle requestAnimationFrame. Really elegant with the event delegation too.

@ashaffer
Copy link
Collaborator

Ah, ya you're totally right about the css things. Didn't think about that at all. I'll fix that up.

maxSize is something that needs to be computed and can't be a static value for the material design ripple effect. Based on where you press you need to find out the furtest point to which the ripple needs to expand.

Oh, that's true. I didn't think too hard about the correctness of the details. I'll fix that up. That should be easy I think.

The reason I picked canvas over raw dom elements is because for each ripple effect (and removing a ripple effect) it causes a dom mutation. Spamming a button for example may cause some performance issues on mobile platforms. What do you think?

Canvas is definitely preferable, but when I looked into it, material-ui seemed to be getting by with divs. And while I would like to add support for canvas stuff, it seems like that'd be somewhat of a project to do right. Definitely intend to sort that out at some point though, but I think we can get by (at least for this) with DOM mutations for the time being.

node.style[key] = val is still a side effect that needs to be handled externally or do you think this is a minor thing? React is doing it declaratively, where a callback is passed to a component each time a frame happens (see react-imation) for example. I think that would be quite a nice approach.

Ya, this is the big one. So, I don't intend that people should actually write an animate.js like in the example. What i'm thinking is that something like that should exist as a middleware that can be triggered by a special animate prop, or possibly an onTick prop?

I really like how timeline and also react-motion accept children functions rather than nodes, but at the moment vdux, by design, doesn't support partial subtree re-rendering. So we could add that in, but I think it sort of breaks the paradigm a bit.

Fundamentally I think there are two options:

  • Support partial subtree re-rendering as a first class operation. This would add a fair amount of complexity to the core, and also somewhat break the pattern that f(state) = ui, in that your children can change out from under you without your knowledge. Probably marking paths as dirty or something would be the way of achieving this, and then we'd also have to expose some sort of sync API to synchronize the DOM even though state in redux hasn't changed.
  • Treat animation more like an edge case, with a special prop or something that is handled in a special way by a middleware that does raw DOM mutations on requestAnimationFrame or something. Then maybe wrap this in a component to make it a bit nicer to work with. This is the approach I took in the example, just without a middleware to make it more official.

The former is obviously more powerful and gives you a very natural API to work with, but the latter probably works just fine for 90% of UI animations, and keeps the paradigm more consistent.

@ashaffer
Copy link
Collaborator

I should add, you can of course always use state updates to do something like what react-motion or react-imation is doing. E.g.

function afterMount ({props}) {
  const {ease, start, end}
  let t = 0
  return raf(function interpolate () {
    const value = ease(t++, start, end)
    if (value !== end) {
      return [updateValue(value), raf(interpolate)]
    }
  })
}

function render ({children, state}) {
  return children[0](state.value)
}

function reducer (state, action) {
  if (action.type === 'update value') {
    return {...state, value: action.payload}
  }
}

function updateValue (value) {
  return {type: 'update value', payload: value}
}

export default {
  render,
  reducer,
  afterMount
}

Which is actually what react-motion seems to be doing. The problem with this approach is that it may be slow in javascript to update state that frequently - although in practice this may actually be totally fine. But it does mean that the performance characteristics of your animation depend on where it lives in your tree, which is kind of a weird thing to have to think about.

EDIT: Upon taking a closer look, this is exactly what react-imation is doing. Actually they are doing it slightly more elegantly, by taking time as the sole piece of state. It could be implemented in vdux easily, I just worry a bit about state-based animations for performance reasons, but maybe it's really nothing to worry about.

@ashaffer
Copy link
Collaborator

Man, the time prop is a really nice way of implementing animation. It would be amazing if we could find some way to make it performant.

function render () {
  return (
    <Tick>
      {state.ripples.map(tick => <Ripple tick={tick} {...ripple} />)}
    </Tick>
  )
}

Is so elegant, because that way each ripple just has to render correctly for that single point in time. The ripple then just becomes:

function render ({props}) {
  return <div style={circle(tick, props)}</div>
}

function circle (t, {x, y}) {
  const size = getSize(t)

  return {
    left: x - (size / 2),
    top: y - (size / 2),
    width: size,
    height: size
  }
}

Which is really really easy to reason about. I would love to be able to implement 100% of animations like this.

@ashaffer
Copy link
Collaborator

Alright per @joshrtay's offline recommendation, I think the simplest thing to do is mostly keep animation out of vdux proper. The ripple effect, for instance, can be handled at the top level, e.g.:

document.body.addEventListener('click', function (e) {
  let node = e.currentTarget
  do {
    if (node.classList.contains('md-ripple')) {
      // exec ripple effect
    }
  } while (node = node.parent)

Another, slightly more vdux-integrated approach could be a middleware with animation effects triggered by actions, e.g.:

function render ({path}) {
  return <button id={path} onClick={[handleClick, animate('#' + path, 'ripple')]}>Do a thing</button>
}

// Where animate is

function animate (selector, name, params) {
  return {
    type: 'animate',
    payload: {
      selector, 
      name,
      params
    }
  }
}

// And then somewhere in your middleware stack is...

function animator (animations) {
  return api => next => action => 
    action.type === 'animate'
      ? animations[action.payload.name](action.payload.selector)
      : next(action)
}

And all associated state/etc is maintained externally. This obviously has some limitations, particularly around enter/leave animations. But I think those types can be solved using something analogous to React's CssTransitionGroup component.

@queckezz
Copy link
Member Author

I think the simplest thing to do is mostly keep animation out of vdux proper

Yea for simple animations this seems the way to go. As soon as you want additive animations like in IOS9 you need to have something more flexible.

I agree with all your points above. Having something like a <Tick> component can not only be useful for animations but also for other things like canvas.

I just worry a bit about state-based animations for performance reasons, but maybe it's really nothing to worry about.

Same and it's actually quite a bottleneck. Best pratices suggest that you have about 10 - 12ms (for 60fps) between each frame for the animation including potential garbage collection and stuff that can happen. I tested it with a basic middleware stack (vdux, redux-effects, redux-effects-timeout, redux-logger) and it took without the actual animation computation about 10ms for an action. Maybe we can implement a way to opt-out of the middleware stack?

Also there are quite some good ideas from @ccorcos involving animations over at elmish

@queckezz queckezz changed the title dispatch outside of an event listener [Discussion] How to implement (performant) animations Jan 18, 2016
@ashaffer
Copy link
Collaborator

@queckezz Ya I suspected that would be a problem. Would you mind trying without redux-logger though? I suspect that may actually be slowing things down a great deal. Logs are actually pretty slow.

As for the middleware stack - my guess is that's not actually your bottleneck. I would suspect it's the state update in redux-ephemeral, and then just the diffing/re-rendering. The middleware stack should actually be pretty fast, since it's just doing a string compare on the action type. Have you profiled it?

@ccorcos
Copy link

ccorcos commented Jan 24, 2016

@queckezz, in terms of performance, I've actually been hung up on this for about a month now. The performance is ok, but from a complexity perspective, its not. Using pure stateless components, (1) you have to compute the layout after every tick and (2) React has to diff the entire layout on every tick. React overcomes this issue by actually lazily evaluating the ui components using React.createElement(Component, props) and referentially diffing props. But this performance isn't optimal either because when something changes at the bottom of the tree, that entire arm of the tree all the way down to the base gets recomputed. The optimal can be achieved with streams which proactively dispatch events when they change. Cycle.js does a good job with this. But in Cycle.js, the state is littered throughout. I'm still thinking about this, but I'd love to discuss and learn more about how this is being addressed in vdux

@ashaffer
Copy link
Collaborator

@ccorcos Hey, at the moment my thinking is to see how far we can get with animations being considered an external thing, possibly managed by something like redux-saga. E.g.

function render ({path}) {
  return <div id={path} onClick={e => triggerAnimation('ripple', {id: path, x: e.clientX, y: e.clientY})}></div>
}

Where you have triggerAnimation return an action that triggers a saga that performs the animation using raw DOM manipulation. But that is really just my current thinking, it's certainly possible that it has significant drawbacks and isn't a good solution.

The app i'm building is unfortunately not very animation heavy so it's not something i'm actively working on as much, but if you have specific problem cases you'd like to discuss we can try to hash out good solutions.

@ccorcos
Copy link

ccorcos commented Jan 26, 2016

I see. But its so cool being able to pause and play animations in your time-traveling debugger :)

@ashaffer
Copy link
Collaborator

You're right, that is very cool. Hmm..you're certainly free to maintain your animations in state, it should perform ok I think. If not, there are a few options:

  • Add the ability to configure redux-ephemeral which vdux uses internally to rely on ImmutableJs or some other HAMT implementation.
  • Optimize vdux's internals even more. This is probably just a cat and mouse game, since at some point a UI tree will be deep enough to have issues always.
  • Add some sort of support for partial subtree re-rendering. This would represent an architectural shift and i'm not sure it's something I want to do, but if we could think of a good abstraction for it that made sense I could see adding it.

The last thing is the only true permanent solution to the problem. I think maybe something like analogous to observ-struct, but designed to operate on immutable structures. Perhaps a component could export an observe structure that would allow it to re-render in isolation on changes to a particular path in the state tree. I'm not sure how to make this consistent with the other ideas though. Maybe if only the observed path changes it may do a partial re-render, and in all other cases it does a full re-render?

E.g. maybe a component does

function observe ({path}) {
  // local state is scoped under 'ui.' in the redux state atom
  return 'ui.' + path
}

return {
  render,
  observe
}

And then vdux internally, when it sees this, does something like:

subscribe(component.observe(model), rerender(component))

Where subscribe emits change events for all changed paths in state, and re-renders subscribed components.

EDIT: I posted a more robust version of this idea over here where perhaps we can get more eyes on it.

@anthonyshort
Copy link

Add some sort of support for partial subtree re-rendering.

One way I've been thinking about doing this is just dispatching an action that marks the path as dirty. Then when you re-render from the top you can optionally pass in an array/object of dirty paths and they will be guaranteed to be re-rendered even if a whole tree arm is skipped higher up. If no paths are passed in, you just re-render the whole thing.

Something rough:

render(state, ['0.0.1.4'])

@ashaffer
Copy link
Collaborator

@anthonyshort Ya, I was just thinking the exact same thing. I think that's the way to do it. Although I was thinking actually you'd just compute their dirtiness from the observe listeners and re-render the dirty ones if everything other than the local state subtree was the same, so that you don't have to actually maintain a dirtiness map.

EDIT: The reason I think the render(state, changedPaths(nextState, prevState)) based on observe may be better is that it also allows you to implement an arbitrary context, which components may subscribe to by observing the paths they care about.

@ashaffer
Copy link
Collaborator

@anthonyshort I made a quick pass at attempting to implement this in vdux/virtex. But very quickly I ran into a bit of trouble. If you want to re-render a subtree and presumably don't want to mutate the existing tree, then you'll need to clone all the nodes above the subtree. This means you're still doing, asymptotically, the same number of operations as a full diff, which seems like it kind of defeats the purpose.

The only way around this I can think of is to maintain a list of updated partial subtrees, and then pass that along as well to every render call, e.g. something like:

function render (...) {
  let tree
  const partials = {}

  return (nextTree, paths) => {
    if (paths) {
      paths.reduce((partials, path) => {
        partials[path] = renderSubtree(tree, path)
        return partials
      }, partials)
    } else {
      render(nextTree)
    }
  }

  function render (nextTree, path = '') {
    if (partials[path]) nextTree = partials[path]
    // ...
  }
}

@anthonyshort
Copy link

That's similar to how the old Deku worked with setState.

you'll need to clone all the nodes above the subtree

Not sure I understand that part though. In Deku now, we store the previous render on the vnode. So even if we didn't re-render a sub-tree, we'd just walk down it to find more sub-trees to try and re-render. But I haven't tried implementing it, so I'm just going to assume I'm missing something big :)

@ashaffer
Copy link
Collaborator

Ya the problem is that when you walk down the sub-tree and find one you want to re-render, you need to store a new render on the vnode at some point in the sub-tree. So, let's say you have:

A -> B -> C

And C has a state update. You start at A, and its props are the same, so you just copy the reference to the old cached vnode onto the new tree. But you know that it contains a dirty path, so you keep descending. When you get to C you have to re-render it because you it's dirty, so you get back a brand new vnode. But now you're inside of the cached vnode tree from A, so you have to either mutate the cached vnode and destroy history, or clone A and B.

It's possible that in practice this isn't so bad, but for deeply nested components, especially those in large lists, e.g.:

A -> B -> C -> D(500) -> E(500) -> F

You're going to be doing a fair amount of cloning on each little state update at the leaves.

@ashaffer
Copy link
Collaborator

Alright guys, some significant updates in this regard. I've added support for partial subtree re-rendering for state changes, and redux-ephemeral now uses an HAMT internally, so the performance of executing and rendering a state update should now be decoupled from the number of components on the page. I also optimized the way inline styles are applied to elements, so it doesn't just set the style property as a string.

This means that, in theory, vdux should now be peformant enough to do complex animations using local state.

@anthonyshort
Copy link

Oh nice. I'm going to dig into this tonight and see how it works :)

@queckezz queckezz changed the title [Discussion] How to implement (performant) animations How to implement (performant) animations Feb 15, 2016
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