Coder Social home page Coder Social logo

Need an ease interface. about d3-ease HOT 14 CLOSED

d3 avatar d3 commented on April 28, 2024
Need an ease interface.

from d3-ease.

Comments (14)

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

Use cases with hypothetical examples, some of which are probably impossible to disambiguate.

  1. Specify a non-parameterizable easing function for all selected nodes.
transition.ease(d3.easeCubicIn);
  1. Specify a default parameterizable easing function for all selected nodes.
transition.ease(d3.easePolyIn);
  1. Specify a custom parameterizable easing function for all selected nodes.
transition.ease(d3.easePolyIn, 2);
transition.ease(d3.easePolyIn(2)); // Or this maybe?
  1. Specify a non-parameterizable easing function for certain selected nodes.
transition.ease(function(d, i) { return i & 1 ? d3.easeCubicIn : d3.easeLinearIn; });
  1. Specify a default parameterizable easing function for certain selected nodes.
transition.ease(function(d, i) { return i & 1 ? d3.easePolyIn : d3.easeLinearIn; });
  1. Specify a custom parameterizable easing function for certain selected nodes.
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?
  1. Invoke a non-parameterizable easing function directly.
var te = d3.easeCubicIn(t);
  1. Invoke a default parameterizable easing function directly.
var te = d3.easePolyIn(t);
  1. Invoke a custom parameterizable easing function directly.
var te = d3.easePolyIn(t, 2);
  1. Specify a custom ease implementation to all selected nodes:
transition.ease({ease: Math.sqrt});
  1. Specify a custom ease implementation to some selected nodes:
transition.ease(function() { return {ease: Math.sqrt}; });

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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).

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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);
    }
  };
};

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

Fixed in 0.6.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

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.

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

Also, tweak to approach G:

d3.easeLinear = d3.ease(function(t) {
  return +t;
});

from d3-ease.

mbostock avatar mbostock commented on April 28, 2024

Punting on this for now.

from d3-ease.

Related Issues (16)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.