-
Notifications
You must be signed in to change notification settings - Fork 42
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
Need an ease interface. #13
Comments
It might be worth thinking about d3-interpolate in this context as well. The easeBind function would probably go away if we went with the above proposal, and it would make d3-ease function differently from d3-interpolate’s optional parameters (such as interpolateCubehelix). I was quite happy with that design so it’s just a small bummer that we have the function ambiguity here. |
Use cases with hypothetical examples, some of which are probably impossible to disambiguate.
transition.ease(d3.easeCubicIn);
transition.ease(d3.easePolyIn);
transition.ease(d3.easePolyIn, 2);
transition.ease(d3.easePolyIn(2)); // Or this maybe?
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; });
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; });
transition.ease(function(d, i) { return d3.easePolyIn(d.exponent); }); // Maybe this?
transition.ease(function(d, i) { return d3.easeBind(d3.easePolyIn, d.exponent); }); // Or this maybe?
var te = d3.easeCubicIn(t);
var te = d3.easePolyIn(t);
var te = d3.easePolyIn(t, 2);
transition.ease({ease: Math.sqrt});
transition.ease(function() { return {ease: Math.sqrt}; }); |
Approach A. ease.ease(t[, arguments…]) d3.easeLinearIn = {
ease: function(t) {
return +t;
}
};
d3.easePolyIn = {
ease: function(t, e) {
if (e == null) e = 3;
return Math.pow(t, e);
}
}; Examples: transition.ease(d3.easeCubicIn); // 1
transition.ease(d3.easePolyIn); // 2
transition.ease(d3.easePolyIn, 2); // 3
transition.ease(d3.easeBind(d3.easePolyIn, 2)); // 3, equivalent
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; }); // 5
transition.ease(function(d, i) { return d3.easeBind(d3.easePolyIn, d.exponent); }); // 6
var te = d3.easeCubicIn.ease(t); // 7
var te = d3.easePolyIn.ease(t); // 8
var te = d3.easePolyIn.ease(t, 2); // 9
transition.ease({ease: Math.sqrt}); // 10
transition.ease(function() { return {ease: Math.sqrt}; }); // 11 Pros: works well for 1-5; 10-11 seem reasonable. Cons: requires implicit easeBind for 3; requires explicit easeBind for 6; requires ease.ease for 7-9. I worry that d3.easeBind is slower than using a closure since it requires derived variables to be recomputed each time the easing function is evaluated (as opposed to d3.interpolateBind, where the interpolator is first constructed, and then called repeatedly). |
Approach B. ease.ease([arguments…])(t) d3.easeLinearIn = {
ease: function() {
return function(t) {
return +t;
};
}
};
d3.easePolyIn = {
ease: function(e) {
if (e == null) e = 3;
return function(t) {
return Math.pow(t, e);
};
}
}; Examples: transition.ease(d3.easeCubicIn); // 1
transition.ease(d3.easePolyIn); // 2
transition.ease(d3.easePolyIn, 2); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; }); // 5
transition.ease(function(d, i) { return d3.easeBind(d3.easePolyIn, d.exponent); }); // 6
var te = d3.easeCubicIn.ease()(t); // 7
var te = d3.easePolyIn.ease()(t); // 8
var te = d3.easePolyIn.ease(2)(t); // 9
transition.ease({ease: function() { return Math.sqrt; }}); // 10
transition.ease(function() { return {ease: function() { return Math.sqrt; }}; }); // 11 Pros: 1-5 are fine. Cons: 6 requires the use of d3.easeBind, although the bound function is executed only once to create the easing function. 7-9 are awkward. 10-11 are super awkward. Somewhat inefficient for non-parameterizable and default easing methods unless you implement caching, as in: function linearIn(t) {
return +t;
}
d3.easeLinearIn = {
ease: function() {
return linearIn;
}
}; Note that this optimization can be implemented as a wrapper, though. The implementation of easeBind might look like this: d3.easeBind = function(ease) {
var args = [].slice.call(arguments, 1);
return {
ease: function() {
return ease.ease.apply(ease, args);
}
};
}; |
Approach C. ease(t[, arguments…]), ease.ease = ease. (Approach A + convenience for direct usage.) d3.easeLinearIn = function(t) {
return +t;
};
d3.easePolyIn = function(t, e) {
if (e == null) e = 3;
return Math.pow(t, e);
};
d3.easeLinearIn.ease = d3.easeLinearIn;
d3.easePolyIn.ease = d3.easePolyIn; Examples: transition.ease(d3.easeCubicIn); // 1
transition.ease(d3.easePolyIn); // 2
transition.ease(d3.easePolyIn, 2); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; }); // 5
transition.ease(function(d, i) { return d3.easeBind(d3.easePolyIn, d.exponent); }); // 6
var te = d3.easeCubicIn(t); // 7
var te = d3.easePolyIn(t); // 8
var te = d3.easePolyIn(t, 2); // 9
transition.ease({ease: Math.sqrt}); // 10
transition.ease(function() { return {ease: Math.sqrt}; }); // 11 Pros: Usage is clean (and backwards-compatible!). Cons: Requires implicit easeBind for 3; requires explicit easeBind for 6. The definition is a little awkward and potentially misleading. It looks like the ease function is being used directly, but ease.ease is used instead. |
Approach D. ease([arguments…]).ease(t) d3.easeLinearIn = function() {
return {
ease: function(t) {
return +t;
}
};
};
d3.easePolyIn = function(e) {
if (e == null) e = 3;
return {
ease: function(t) {
return Math.pow(t, e);
}
};
}; Examples: transition.ease(d3.easeCubicIn()); // 1
transition.ease(d3.easePolyIn()); // 2
transition.ease(d3.easePolyIn(2)); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn() : d3.easeLinearIn(); }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn() : d3.easeLinearIn(); }); // 5
transition.ease(function(d, i) { return d3.easePolyIn(d.exponent); }); // 6
var te = d3.easeCubicIn().ease(t); // 7
var te = d3.easePolyIn().ease(t); // 8
var te = d3.easePolyIn(2).ease(t); // 9
transition.ease({ease: Math.sqrt}); // 10
transition.ease(function() { return {ease: Math.sqrt}; }); // 11 Pros: No need for d3.easeBind; a clean interface. Symmetry between non-parameterizable and parameterizable easing methods. Cons: Even non-parameterizable and default easing methods require parens, and if you forget them, bound data is passed to the easing factory which could result in surprising behavior and inefficiency. Somewhat inefficient for non-parameterizable and default easing methods unless you implement caching, as in: var linearIn = {
ease: function(t) {
return +t;
}
};
d3.easeLinearIn = function() {
return linearIn;
}; Note that this optimization can be implemented as a wrapper, though. |
Approach E. ease.ease(t) and ease([arguments…]).ease(t) d3.easeLinearIn = {
ease: function(t) {
return +t;
}
};
d3.easePolyIn = function(e) {
if (e == null) e = 3;
return {
ease: function(t) {
return Math.pow(t, e);
}
};
}; Examples: transition.ease(d3.easeCubicIn); // 1
transition.ease(d3.easePolyIn()); // 2
transition.ease(d3.easePolyIn(2)); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn() : d3.easeLinearIn; }); // 5
transition.ease(function(d, i) { return d3.easePolyIn(d.exponent); }); // 6
var te = d3.easeCubicIn.ease(t); // 7
var te = d3.easePolyIn().ease(t); // 8
var te = d3.easePolyIn(2).ease(t); // 9
transition.ease({ease: Math.sqrt}); // 10
transition.ease(function() { return {ease: Math.sqrt}; }); // 11 Pros: No need for d3.easeBind. Cons: Asymmetry between non-parameterizable and parameterizable easing methods. If you forget to evaluate a parameterizable easing function (even with default parameters), bound data is passed to the easing factory which could result in surprising behavior and inefficiency. |
Approach F. ease.ease(t) and ease.of([arguments…]).ease(t) d3.easeLinearIn = {
ease: function(t) {
return +t;
}
};
d3.easePolyIn = {
of: function(e) {
if (e == null) e = 3;
return {
ease: function(t) {
return Math.pow(t, e);
}
};
}
}; Examples: transition.ease(d3.easeCubicIn); // 1
transition.ease(d3.easePolyIn); // 2
transition.ease(d3.easePolyIn, 2); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; }); // 5
transition.ease(function(d, i) { return d3.easePolyIn.of(d.exponent); }); // 6
var te = d3.easeCubicIn.ease(t); // 7
var te = d3.easePolyIn.of().ease(t); // 8
var te = d3.easePolyIn.of(2).ease(t); // 9
transition.ease({ease: Math.sqrt}); // 10
transition.ease(function() { return {ease: Math.sqrt}; }); // 11 Pros: No need for d3.easeBind. Symmetry between non-parameterizable and parameterizable easing methods, at least for transition.ease. Cons: Now transition.ease has to test for two interfaces: an easing function, and an easing function factory. The latter is only useful in cases 2-3 to provide symmetry. The name “of” is extremely generic. |
Approach G. ease.ease(t) and ease.argument(value).ease(t) d3.easeLinearIn = {
ease: function(t) {
return +t;
}
};
function PolyIn(exponent) {
this._exponent = exponent;
}
PolyIn.prototype = {
exponent: function(e) {
return new PolyIn(e);
},
ease: function(t) {
return Math.pow(t, this._exponent);
}
};
d3.easePolyIn = new PolyIn(1); Alternative implementation with closures: d3.easePolyIn = (function polyIn(e) {
return {
exponent: polyIn,
ease: function(t) {
return Math.pow(t, e);
}
};
})(1); Examples: transition.ease(d3.easeCubicIn); // 1
transition.ease(d3.easePolyIn); // 2
transition.ease(d3.easePolyIn.exponent(2)); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; }); // 5
transition.ease(function(d, i) { return d3.easePolyIn.exponent(d.exponent); }); // 6
var te = d3.easeCubicIn.ease(t); // 7
var te = d3.easePolyIn.ease(t); // 8
var te = d3.easePolyIn.exponent(2).ease(t); // 9
transition.ease({ease: Math.sqrt}); // 10
transition.ease(function() { return {ease: Math.sqrt}; }); // 11 Pros: Clean interface, arguments are explicitly named and self-describing; symmetry between non-parameterizable and default parameterizable easing methods; explicit differentiation for non-default parameterized easing methods. Cons: None? Well, it requires specifying whether ease.argument returns a new easing function or mutates the behavior of ease. Most D3 setter methods modify in-place, but obviously mutating the behavior of the global d3.easePolyIn would be bad. Possibly the first call creates a new instance, and then subsequent calls modify the instance in-place, but that sounds kind of icky, too. This pattern also extends nicely to optional parameters for interpolation, such as interpolate.gamma. |
Rather than expose bare easing functions, this module now exports instances of an easing interface: object.ease(t). For example: var te = d3.easeCubicIn(t); // BEFORE var te = d3.easeCubicIn.ease(t); // AFTER The primary motivation of this change is to allow D3 transitions to differ- entiate between an easing type and a function that returns an easing type, as when the easing type should be determined separately for each selected element. Also, it means you can use a class to implement a configurable easing type, if you want, rather than a closure. This commit also changes how optional parameters are specified for configurable easing types. Rather than binding optional arguments to the easing function, create a new instance using method chaining. For example, to create a cubic easing instance using generic polynomial easing: var cubicIn = d3.easeBind(d3.easePolyIn, 3); // BEFORE var cubicIn = d3.easePolyIn.exponent(3); // AFTER Equivalently, to call the function directly: var te = d3.easePolyIn(t, 3); // BEFORE var te = d3.easePolyIn.exponent(3).ease(t); // AFTER By giving the parameters explicit names, usage is now self-documenting! Also, we can eliminate d3.easeBind and easing functions are slightly faster.
Fixed in 0.6. |
Rather than have interpolators take optional arguments, and then require d3-scale to bind those optional arguments, configurable interpolators (of which cubehelix is the only one, so far) now expose named configuration methods. var i = d3.interpolateCubehelix("red", "blue", 3); // BEFORE var i = d3.interpolateCubehelix.gamma(3)("red", "blue"); // AFTER This makes the usage of optional parameters self-documenting. Related d3/d3-ease#13.
Now that d3.interpolateBind has been removed (see d3/d3-ease#13), there’s no need to support automatic binding of optional interpolator parameters.
I like the new named parameters. I don’t like the new easing interface. It bothers me that I can’t just pass a function to transition.ease, and that easing requires an interface whereas interpolators and scales do not. And I don’t think the answer is to require an interface for interpolators and scales! (Imagine the hassle!) The only reason this interface is required is for transition.ease to accept both a function that returns an easing method and an easing method directly. Wouldn’t it be a lot simpler to avoid that ambiguity by having a separate method, say transition.easeEach, if you wanted to specify per-element easing functions? That name isn’t ideal, but it seems less bad than requiring an easing interface. The function vs. constant overloading works with the other methods in D3 because the constant values aren’t functions. Once the “constant” is itself a function, it seems reasonable to make the distinction explicit rather than inferred. Plus, there are already cases where functions are required for per-element evaluation, such as transition.tween. |
I think there’s a way to do with without requiring you to call ease.ease if you want to use an easing function directly: the built-in easing methods could be functions that you can call directly while also exposing an ease.ease method that points back to the function. So: d3.easeLinear = function(t) { return +t; };
d3.easeLinear.ease = d3.easeLinear; // Have it both ways! When setting transition.ease, it could check with the specified ease has an ease.ease method, and use it like an object; otherwise it would assume that the ease is a function that should be evaluated for each node and return the corresponding ease instance. If you want to pass a custom easing function and use that for all nodes, we could have a d3.ease method to wrap your function: d3.ease = function(ease) {
return {ease: ease};
}; Then you could say, for example: transition.ease(d3.ease(function(t) { return Math.sqrt(t); }));
transition.ease(d3.ease(Math.sqrt)); // Equivalently. Sure, that’s a little more work than specifying a bare function, but I don’t think custom easing functions will be very common, and it seems fine to require a few more characters to make it happen, as compared to: transition.ease(function(t) { return Math.sqrt(t); });
transition.ease(Math.sqrt); // Equivalently. Also, I do think it could be useful to have an interpolator interface, too. For example, if you want to reuse an interpolator for your style tweens, you currently have to do this, which is awkward: var redblue = d3.interpolateRgb("red", "blue");
transition.styleTween("color", function() { return redblue; }); You can also say this, but it’s less efficient because it’s constructing a separate interpolator (with the same start and end values) for each node: transition.styleTween("color", d3.interpolateRgb.bind(null, "red", "blue")); I suppose we could optimize d3.interpolateRgb to reuse interpolators, but that’d be a fair amount of work. If there were an interpolator interface, you could say: transition.styleTween("color", d3.interpolateRgb("red", "blue")); If you wanted to write a custom interpolator, you could use (a new) d3.interpolator wrapper that works like the proposed d3.ease above: transition.styleTween("color", d3.interpolator(function(t) { return "rgb(" + Math.round(t * 255) + ",0,0)"; })); And of course this means that d3.interpolateRgb and the like would return functions with interpolator.interpolate methods that point back to the function. Like this: var i = d3.interpolateRgb("red", "blue");
i.interpolate = i; // Have it both ways! I also have a related concern about whether D3’s extensive use of closures makes it harder for JavaScript runtimes to optimize; I see the “Not optimized: optimized too many times” error in some of my initial testing of 4.0. It’s possible that by only supporting the ease.ease and interpolator.interpolate interface (and using private state rather than captured variables) could be easier for runtimes to optimize. That would necessitate using the interfaces, rather than the closures, even when using easing or interpolation directly. Which means you’d be saying this: transition.ease(d3.easeCubic); // 1
transition.ease(d3.easePoly); // 2
transition.ease(d3.easePoly.exponent(2)); // 3
transition.ease(function(d, i) { return i & 1 ? d3.easeCubic : d3.easeLinear; }); // 4
transition.ease(function(d, i) { return i & 1 ? d3.easePoly : d3.easeLinear; }); // 5
transition.ease(function(d, i) { return d3.easePoly.exponent(d.exponent); }); // 6
var te = d3.easeCubic.ease(t); // 7
var te = d3.easePoly.ease(t); // 8
var te = d3.easePoly.exponent(2).ease(t); // 9
transition.ease(d3.ease(Math.sqrt)); // 10
transition.ease(function() { return d3.ease(Math.sqrt); }); // 11 But… that seems pretty reasonable. |
Also, tweak to approach G: d3.easeLinear = d3.ease(function(t) {
return +t;
}); |
Punting on this for now. |
An interface is needed to disambiguate transition.ease(ease) from transition.ease(function), where the latter is a function that returns an easing function, thereby allowing different easing functions to be used for different nodes in a transition. This is the same issue that lead to the creation of the symbol type interface in d3-shape, which is useful for applying a categorical symbol encoding to a scatterplot.
Admittedly, I don’t anticipate a lot of demand for customizing the easing function per node, but if we didn’t support it, the inconsistency is likely to lead to occasional confusion.
It’d still be nice if you could have optional parameters, though. Not sure how to do that…
Maybe if there’s more than one argument, it automatically gets converted to this?
Well actually that wouldn’t work, because if you didn’t specify the optional argument, then the parameterizable easing function factory would get invoked separately for each node.
Alternatively, the non-parameterizable easings could still be functions that return easing instances, so these would be equivalent…
But that means you’d need to do some manual easing, you’d need to change this:
To this:
Which is a little verbose. (And slower, unless you stash the result of d3.easeCubicInOut() outside of your animation loop.) But then the transition.ease API is probably going to see wider use than the d3-ease API, and it’s not like the proposed API is bad. You can approximate the old API like so:
The text was updated successfully, but these errors were encountered: