-
Notifications
You must be signed in to change notification settings - Fork 8
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
Comments
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 |
Ah! You still can't dispatch actions in
Oh, no haven't thought of that. Well to be honest, I don't see the difference in those two ways. |
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)
}
There's no functional difference, just syntactic. To me doing 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. |
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 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? |
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?
There is no custom middleware for local actions, but if you do this: 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 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.
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. |
Thanks for taking the time! Your above approach won't work unfortunately but I haven't give you enough context.
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
Here ya go: https://gist.github.com/queckezz/91abb31cd43e4887ecf4 |
@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. |
Awesome! I'll try your suggestions. Looks like it's feasible with |
@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. |
🍺 This looks pretty good. Couple of points though:
{state.ripples.map(ripple => <Ripple key={ripple.id} {...ripple} onEnd={local(removeRipple, ripple)} />)}
<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 |
Ah, ya you're totally right about the css things. Didn't think about that at all. I'll fix that up.
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.
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.
Ya, this is the big one. So, I don't intend that people should actually write an 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:
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. |
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 |
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. |
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. |
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
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 ( Also there are quite some good ideas from @ccorcos involving animations over at elmish |
@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? |
@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 |
@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 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. |
I see. But its so cool being able to pause and play animations in your time-traveling debugger :) |
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:
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 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. |
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']) |
@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 EDIT: The reason I think the |
@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]
// ...
}
} |
That's similar to how the old Deku worked with
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 :) |
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. |
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 This means that, in theory, vdux should now be peformant enough to do complex animations using local state. |
Oh nice. I'm going to dig into this tonight and see how it works :) |
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
: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.
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.How do I remove the ripple effects from the array again when there finished animating? there is no way to dispatch an action again.
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.
The text was updated successfully, but these errors were encountered: