简体   繁体   中英

How to chain d3 transitions?

I have a set of animations that happen when a user moves a slider bar. Each increment of the slider bar creates a transition. However, whenever the user moves the slider bar very quickly (ie they increment the slider faster than the transition can complete), there is a race condition on the transitions, old ones get interrupted, and the "flow" of the animation is weird. I would like to have a sequence of transitions and have them always occur in the sequence that they were called . Ie The next one doesn't start until the last one is finished. jsfiddle (hold down the "a" key vs press it several times slowly to see the difference)

var svg = d3.select("svg"),
    margin = {top: 40, right: 40, bottom: 40, left: 40},
    width = svg.attr("width") - margin.left - margin.right,
    height = svg.attr("height") - margin.top - margin.bottom,

g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var y = d3.scalePoint()
    .domain(d3.range(50))
    .range([0, height]);

g.selectAll("circle")
  .data(y.domain())
  .enter().append("circle")
  .attr("r", 25)
  .attr("cx", 50)
  .attr("cy", y);

var radius = 25;
function animate() {
    radius = (radius+5)%50;
    g.selectAll("circle")
    .transition()
    .duration(500)
    .attr("r", radius);
}
document.addEventListener('keydown', e => e.code === "KeyA" ? animate() : 0);

You can use .on("end", callback) to listen to the end of an animation. As you have 50 objects being animated, and will get that event for each one of them, you need a counter to know when the last one of those has finished.

To make sure all keypresses result in the animation, but only after the previous one has finished, you cannot just call animate() on the key event. Instead keep track of how many of those calls have to be performed, and increment that when a key event is fired. Only call animate() when that counter was zero.

Note that this queuing of animate() calls may also give an unnatural behaviour: the animation may keep on going until long after the last key event. To improve the user experience, you could lower the duration parameter, so the animation speeds up when still lots of key events need their corresponding call of animate() processed.

Here is how your code could be adapted to do all that:

var svg = d3.select("svg"),
    margin = {top: 40, right: 40, bottom: 40, left: 40},
    width = svg.attr("width") - margin.left - margin.right,
    height = svg.attr("height") - margin.top - margin.bottom,

g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// Use a variable for the number of items:    
var size = 50;
var y = d3.scalePoint()
    .domain(d3.range(size)) // <--- use that variable
    .range([0, height]);

g.selectAll("circle")
  .data(y.domain())
  .enter().append("circle")
  .attr("r", 25)
  .attr("cx", 50)
  .attr("cy", y);

var radius = 25;
var count = 0; // <--- the number of queued calls of animate
function animate() {
    let i = size; // <-- the number of items that will be animating
    console.log("anim");
    radius = (radius+5)%50;
    g.selectAll("circle")
    .transition()
    .duration(500 / count) // <--- reduce the duration when count is high
    .attr("r", radius)
    .on("end", () => {
        i--;
        // when all objects stopped animating and more animate() calls needed:
        if (i === 0 && --count > 0) animate(); // on to the next...
    });
}
document.addEventListener('keydown', e =>
    // only call animate when no animation is currently ongoing, else increment
    e.code === "KeyA" ? count++ || animate() : 0
);

If using the newest D3 version, which is v5 (by the way, your code works with v5 the way it is, no change needed) you can use the approach in the accepted answer , but with transition.end() instead of transition.on("end",...) .

Unlike transition.on("end",...) , transition.end() (emphasis mine):

Returns a promise that resolves when every selected element finishes transitioning. If any element's transition is cancelled or interrupted, the promise rejects.

That way, you can remove the i variable inside animate() , saving a couple of lines. It becomes:

function animate() {
  radius = (radius + 5) % 50;
  g.selectAll("circle")
    .transition()
    .duration(500 / count)
    .attr("r", radius)
    .end()
    .then(() => {
      if (--count > 0) animate()
    });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM