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

General discussion #1

Open
bdowning opened this issue Apr 20, 2016 · 15 comments
Open

General discussion #1

bdowning opened this issue Apr 20, 2016 · 15 comments

Comments

@bdowning
Copy link
Owner

No description provided.

@bdowning
Copy link
Owner Author

@jeremyckahn wrote in jeremyckahn/rekapi#46:

shifty-core looks super cool. I don't have a problem with the name, but I wonder if calling it something that indicates the functional nature of the project might be better — perhaps shifty-fp, or functional-shifty? I'm getting the idea for the former from lodash-fp. The "core" part suggests a relationship to the shifty.core.js file, which is not accurate. Just my two cents.

I'm a little unclear what you intend to do with shifty-core with regards to Rekapi and Shifty. Would shifty-core become a common dependency of the two projects? I'm fine with adding a small dependency to Rekapi, but I would prefer to keep Shifty dependency-less (though I am not married to that). I am now watching that project, so feel free to initiate a conversation and tag me there and we can discuss how that project fits in further.

I'm looking forward to seeing how it develops!

The idea basically came from looking at what Rekapi uses Shifty for, which is basically just the interpolation engine. Other than reusing some simple Shifty methods on the Actors it doesn't appear that anything from it is required other than what gets called from Tweenable.interpolate. Specifically, all of the stuff for playing tweens (scheduling, timeoutHandler, persistent state handling, etc.) is not used. Unfortunately it looks like some of the efficiency hooks for the extensions like the token module depend on actually using a real, persistent Tweenable; some work is done on tweenCreated which would only be run once on a real Tweenable but is repeated every time with Tweenable.interpolate because internally it constructs a new psuedo-Tweenable, uses it, then throws it away.

I was having trouble figuring out how to modify the event-based hook system that the token module uses to be able to precalculate and save more of the work in a way amenable to being stored in a Rekapi.KeyframeProperty to improve performance in Rekapi. In the process of thinking about this I decided it made more sense to me to have just the interpolation functionality as a core, and then put an interface like the current Shifty on top of it to manage single tween playbacks and give it a more user-friendly UI, or put Rekapi on top for a more full-featured scene-based animation system. shifty-core is my attempt to see what that might look like.

The intent is that Rekapi could, at keyframe creation time, preprocess the keyframe value (saving the produced decoder function), and then create and save an interpolator. Then at playback time all that needs to happen (in KeyframeProperty.getValueAt) is something like:

  const state = nextProperty.interpolate(this.state, nextProperty.state,
                                         interpolatedPosition);
  this.value = nextProperty.decode(state);

Notably, at playback time you did not have to parse either the current or next property's value (i.e. to tokenize and pick apart CSS), you directly call a function to interpolate the values, and you directly call a function to turn the tweened state back into the format needed for the Rekapi state (i.e. regenerate the CSS with the new values popped in).

Because you're directly calling an interpolation and decode function, these can be made to be very efficient. In fact, you can compile them!

Let's say you're tweening from {transform: 'translateX(10px) rotate(10deg)'} to {transform: translateX(25px) rotate(40deg)}. Your token preprocessor will turn these into something like:

fromState = {
  transform__0: 10,
  transform__1: 10,
}
toState = {
  transform__0: 25,
  transform__1: 40,
}

It will also produce a decoder function to reverse the transformation for any state shaped like the above. Let's say you're easing with {transform: [ easeInOutSine, linear ]}. The token preprocessor would have turned this into:

toEasing = {
  transform__0: easeInOutSine,
  transform__1: linear,
}

Then you feed toState and toEasing into createInterpolator, and get an interpolation function specific to the toState shape.

These functions could be implemented as:

interpolator = (new Function('easing0', 'easing1', `
  return function (fromState, toState, position, outputState) {
    outputState = outputState || { };
    outputState.transform__0 =
      fromState.transform__0
        + (toState.transform__0 - fromState.transform__0)
        * easing0(position);
    outputState.transform__1 =
      fromState.transform__1
        + (toState.transform__1 - fromState.transform__1)
        * easing1(position);
    return outputState;
  };
`))(easeInOutSine, linear);

decoder = (new Function(`
  return function (outputState) {
    return {
      transform:
        'translateX(' + outputState.transform__0.toString() +
        'px) rotate(' + outputState.transform__1.toString() +
        'deg)'
    };
  }
`)();

It looks wordy and ugly, but the resulting functions would be incredibly fast. Obviously you'll need to cache these so that you don't regenerate the same function over and over again, but that should be relatively easy. And again, all of this work can happen at keyframe creation time, not runtime.

I do want to get to the point of having a compiler like this, but first I'm just trying to get basic functionality and to prove out the API.

Basically, in summary: Extract the "interesting" parts out of Shifty (interpolation, preprocessing algorithms), ruthlessly minimize, and turn the efficiency up to 11.

@bdowning
Copy link
Owner Author

I'm not wild about the shifty-fp name because, as is hopefully apparent above, this is not intended to be a functional programming version of Shifty. Instead, it's the "core functionality" of Shifty, which just happens to be implemented in a functional-programming way.

@jeremyckahn
Copy link

These are some amazing ideas. Shifty is admittedly not the fastest tweening engine out there, but that's because I prioritized extensibility and flexibility over pure performance. Yours is a much more performance-driven approach, which can make a huge difference on some projects like the ones you are working on. I'm all for this.

The idea of compiling optimized interpolation functions is very interesting. I haven't seen this technique before and I wonder how it might stack up to GSAP, the current king of the hill in terms of performance for animation libraries. I estimate that this would be pretty close to GSAP's level.

Once this gets built out, I'd be totally open to replacing "classic" Shifty for this one as the interpolation engine for Rekapi. Doing so would probably necessitate a 2.0 release. I could also roll my ES6-ification work into that release, making for a pretty big advancement for Rekapi. Please let me know if you want to go that route, and I'll work with you to make it happen.

About the name: Yeah, shifty-fp isn't great. Not long ago, I started on a 2.0 version of Rekapi, which was a total rewrite, but I later decided to spin it off into a separate project called alt-rekapi. It's very incomplete, and I'm not working on it at the moment so as to focus on other projects. But, like this project, it's a reimagining of what Rekapi could be if built with modern tools and practices. It uses Redux and Immutable.js, so our heads are in the same place. Perhaps this could fit into the picture somehow? Since it's conceptually similar to this project, what do you think of "alt-shifty?"

@bdowning
Copy link
Owner Author

Yeah, I hate names.

Thesaurus has "drift" as a synonym of "shift", and amazingly there's not "drifty" npm package yet, but there is a "drifty" company (https://github.com/driftyco) so that's no good. Besides, drift racing is kind of ridiculous. :D

Shift -> Mutate. Googling "mutaty" gets https://en.wiktionary.org/wiki/mutati. Calling a mostly-functional package anything like "mutate" is kind of ironic. ;)

Between -> Betwixt. Already an npm package.

"reshifty" sounds awkward.

I kind of like "retween". Actually the more I think about it the more I like it. Rekapi, Redux, Retween. I think I will go steal the npm package name if nothing else.

@bdowning
Copy link
Owner Author

Well, I'm sold enough to rename the repository. ;-)

@jeremyckahn
Copy link

Retween is great! I love it.

@hexpunk
Copy link

hexpunk commented Apr 20, 2016

Any particular reason why the easing formulas are exported as one big object instead of just exporting each function individually? My best guess would be compatibility with some other library, but it's jsut a guess.

Also, any particular reason why the exported functions in files where only one thing is exported aren't being export default-ed?

Not criticizing, just interested in your coding style.

@bdowning
Copy link
Owner Author

bdowning commented Apr 20, 2016

Hi @jv-PintoBobcat,

For 1), just because it was easy for the moment. They really should be separate so that a module builder can only take the ones that are being used.

For 2), mostly because I started with everything in index.js and haven't cleaned it up yet. I'm pretty much following Redux's coding style, so I will probably change it around to use default exports and then re-exporting with names in index.js like it does.

It's only 24 hours old after all! ;)

@hexpunk
Copy link

hexpunk commented Apr 20, 2016

Fair enough. Code on, my good man.

@bdowning
Copy link
Owner Author

bdowning commented Apr 21, 2016

@jeremyckahn, I did some looking at the speed test you linked above. It looks to me like the primary reason that GSAP is faster is not necessarily raw tweening performance, but rather that it is batching everything up into one requestAnimationFrame call. Since Shifty Tweenables (as far as I know) don't know anything about each other, it's not doing this, and that really hurts performance.

@bdowning
Copy link
Owner Author

I made a quick modification to Shifty to use a global requestAnimationFrame and it's actually just as fast if not a bit faster than GSAP. The reason Shifty looks worse is because you're firing off each new dot in a setTimeout, and they start moving immediately; with 3000 dots multiples are bound to start at the same time, and so they "clump" in rings like you see. GSAP is using a delay parameter which enables more granularity (i.e. the dot can start moving "in-between" frames), so even if its frame rate slows down the starfield still looks correct.

Adding a similar delay parameter to Shifty should make it behave just the same as GSAP.

@bdowning
Copy link
Owner Author

(Aforementioned quick and dirty Shifty hack is here.)

@jeremyckahn
Copy link

Incredible. Thanks for sharing your research! I definitely want to incorporate performance tweaks like this since they are so significant (and simple!). I'm a little backed up with various things right now, so I haven't had time to dig into your various changes and respond to all threads. It's high on my list of things to do, though – just letting you know that I haven't lost track of anything!

@bdowning
Copy link
Owner Author

Turns out GSAP is still a bit faster. I'm pretty sure it has something to do with how they're batching and/or manipulating the DOM. At 3000 dots they get within 1-2 FPS of each other, but at 500 dots you can see in the profiler/timeline that GSAP has more idle time.

@jeremyckahn
Copy link

Batching seems like a smart idea. Even if there are multiple tweenable instances, there's no reason that they shouldn't all be batched. I have opened a ticket to track this: jeremyckahn/shifty#87

Shifty actually does have delay functionality. I'm pretty sure I implemented that some time after I forked the GSAP performance test — I will have update my fork to use that to see what the performance difference is when I have some time. Thanks for the reminder!

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

3 participants