d3 / d3-ease

Easing functions for smooth animation.
https://d3js.org/d3-ease
BSD 3-Clause "New" or "Revised" License
606 stars 42 forks source link

Consider using a more generic easing library #1

Closed bcherny closed 9 years ago

bcherny commented 9 years ago

Shameless self promotion - https://github.com/bcherny/penner

mbostock commented 9 years ago

How is yours more generic? This library has no dependencies.

mbostock commented 9 years ago

Also, there are a few differences here:

The names have been changed. (I chose names that are were consistent with JavaScript, such as “sin” for Math.sin instead of “sine”, and “exp” for Math.exp instead of “expo”.) I also introduced a generic “poly” easing function rather than exposing “quart” and “quint”. This is a matter of preference, of course, but this API also needs to be backwards-compatible with D3 3.x.

The easing functions have been simplified to remove the b (bias), c (change), d (domain) parameters that Penner used: only the t (time) parameter is used. D3’s transitions compute the normalized time t in [0,1] so there is no reason for the extra parameters. That makes the implementation here slightly simpler and faster.

If you want those sort of parameters without a transition, you could use a linear scale:

function easedInterpolate(ease) {
  return function(a, b) {
    var interpolate = d3.interpolate(a, b);
    return function(t) {
      return interpolate(ease(t));
    };
  };
}

var scale = d3.scale.linear()
    .domain([0, d])
    .range([b, b + c])
    .interpolate(easedInterpolate(d3.ease("cubic-in-out")));

The d3-scale module has not yet been implemented, but it will also be available for use independent of the rest of D3 as part of the 4.0 restructuring.

Or, with a little math:

function modifiedEase(ease, b, c, d) {
  return function(t) {
    return b + ease(t / d) * c;
  };
}

The ease function takes a string rather than exposing symbols: you say ease("cubic-in-out") rather than d3.ease.cubicInOut. This is mainly for brevity in the context of transition.ease. You can say:

transition.ease("cubic-in-out");

Rather than:

transition.ease(d3.ease.cubicInOut);

Lastly, this API allows you to tweak the behavior of easing functions through parameters that are captured with closures, rather than needing to pass the parameters in every time you call the easing function. This approach makes the generic application of easing functions easier—the consumer of the easing function doesn’t need to know whether additional parameters are needed. Even better, because the ease function takes a string, the caller doesn’t need to know which easing functions are parameterizable. They can just change ease("elastic") to ease("elastic", 1.3) to set the parameter. For example, you can say:

transition.ease("cubic-in");
transition.ease("elastic-in", 1.3);

Rather than:

transition.ease(d3.ease.cubicIn); // Note: no parens, since not configurable.
transition.ease(d3.ease.elasticIn(1.3));

I hope this explanation clarified some of the API decisions here and why I can’t just drop in a replacement implementation of Penner’s functions. I appreciate your desire to avoid yet another implementation. If it’s any consolation, my implementation predates yours by three years—it’s just taken me this long to formally decouple it from D3! :grin:

bcherny commented 9 years ago

If you want those sort of parameters without a transition, you could use a linear scale:

That's a really cool way to deal with the other params!

Even better, because the ease function takes a string, the caller doesn’t need to know which easing functions are parameterizable.

What do you think of using partial application here?

let myTransition = transition.ease("elastic-in")
let myTransition2 = myTransition(1.3)
let myTransition3 = myTransition2(1.4)

If it’s any consolation, my implementation predates yours by three years—it’s just taken me this long to formally decouple it from D3!

That is a bit of a consolation :)

mbostock commented 9 years ago

What do you think of using partial application here?

The difficulty is disambiguating between an easing function and a function that returns an easing function (in Java speak, an “easing function factory” or perhaps an “easing factory”). In the d3-ease API, the ease function is the easing factory, and the returned function is the easing function. So:

var e = ease("elastic-in");
e(.2); // -0.0019531250000000004

Or, with parameters:

var e = ease("elastic-in", 1.3);
e(.2); // -0.0047631825331910455

I suppose you could have multiple levels of factories, but that gets messy…

var e = ease("elastic-in")(); // With no parameters?
var e = ease("elastic-in")(1.3);
var e = ease("cubic-in-out")(); // Necessary even if this easing mode isn’t parameterizable?
bcherny commented 9 years ago

ah, that makes sense - it's not clear whether the arg should configure the easing function or invoke it. thanks for the explanation!