Interpolating along consecutive paths with D3.js

2019-05-23 12:04发布

I'm adapting Mike Bostock's point along path interpolation model to accept an array of n individual paths and interpolate along each consecutively. Being relatively new to D3 the code below shows as far as I've got, which is to run the point interpolation for both paths concurrently. Now I'm a bit stuck over how to restructure this to make the process consecutive (with just one moving object). Really I need to be able to stop between paths to listen for a mouseclick, but I can figure out that code once the structure is there. Most grateful for assistance.

Here's the jsfiddle.

Code for posterity:

  <!DOCTYPE html>
  <meta charset="utf-8">
  <body>
  <style>

  path {
    fill: none;
    stroke: #000;
    stroke-width: 3px;
  }

  circle {
    stroke: #fff;
    stroke-width: 3px;
  }

  </style>
  <script type="text/javascript" src="http://d3js.org/d3.v3.js"></script><script>

var pathdata = [
   [[240, 100],
    [290, 200],
    [340, 50]],
   [[340, 50],
    [90, 150],
    [140, 50],
    [190, 200]]
];

  var svg = d3.select("body").append("svg")
      .attr("width", 960)
      .attr("height", 500);

  var paths = svg.selectAll("path")
      .data(pathdata)
      .enter()
      .append("path")
      .attr("d", d3.svg.line())
      .attr("id",function(d, i) { return "path" + i });

  // plot path vertices
  svg.selectAll(".point")
      .data([].concat.apply([], pathdata))
      .enter().append("circle")
      .attr("r", 5)
      .attr("fill", "red")
      .attr("transform", function(d) { return "translate(" + d + ")"; });

  // interpolate along path0
  var circle = svg.append("circle")
      .attr("r", 10)
      .attr("fill", "steelblue")
      .attr("transform", "translate(" + pathdata[0][1] + ")")
      .transition()
      .duration(4000)
      .attrTween("transform", translateAlong(d3.select("#path0")[0][0]));

  // interpolate along path1
  var circle = svg.append("circle")
      .attr("r", 10)
      .attr("fill", "steelblue")
      .attr("transform", "translate(" + pathdata[1][1] + ")")
      .transition()
      .duration(4000)
      .attrTween("transform", translateAlong(d3.select("#path1")[0][0]));

  function translateAlong(path) {
    console.log(path);
    var l = path.getTotalLength();
    return function(d, i, a) {
      return function(t) {
        var p = path.getPointAtLength(t * l);
        return "translate(" + p.x + "," + p.y + ")";
      };
    };
  }

  </script>

  </body>
  </html>

I'm also wondering if it might be better to format the input data along one of the following lines?

// 3rd field for path id
var points_alt1 = [
  [240, 100, 0],
  [290, 200, 0],
  [340, 50,  0],
  [340, 50,  1],
  [90, 150,  1],
  [140, 50,  1],
  [190, 200, 1]
]    

or..

// 3rd field for interpolation end-points
var points_alt2 = [
  [240, 100, 0],
  [290, 200, 0],
  [340, 50,  1],
  [340, 50,  0],
  [90, 150,  0],
  [140, 50,  0],
  [190, 200, 1]
]

1条回答
虎瘦雄心在
2楼-- · 2019-05-23 12:47

Create a function that takes as params a d3 selection of paths and an integer index of the path (within the selection) along which you want to animate. This function finds the appropriate path within that selection, starts up a transition of a circle along it, and subscribes to the 'end' event of the transition, at which point it triggers the next animation.

Here's the working fiddle

function animateSinglePath(selection, indexOfAnimated) {
    indexOfAnimated = indexOfAnimated || 0;

    // Create circle if doesn't already exist
    // (achived by binding to single element array)
    circle = svg.selectAll('.animated-circle').data([null])
    circle.enter()
        .append("circle")
        .attr("class", "animated-circle")
        .attr("r", 10)
        .attr("fill", "steelblue")    

    selection.each(function(d, i) {
        // find the path we want to animate
        if(i == indexOfAnimated) {
            // In this context, the "this" object is the DOM
            // element of the path whose index, i equals the
            // desired indexOfAnimated
            path = d3.select(this)
            // For style, make it dashed
            path.attr('stroke-dasharray', '5, 5')

            // Move circle to start pos and begin animation
            console.log('Start animation', i)
            circle
                .attr("transform", "translate(" + d[0] + ")")
                .transition()
                .duration(2000)
                .attrTween("transform", translateAlong(path.node()))
                .each('end', function() {
                    console.log('End animation', i);

                    // For style, revert stroke to non-dashed
                    path.attr('stroke-dasharray', '')

                    // trigger the next animation by calling this
                    // function again
                    animateSinglePath(selection, indexOfAnimated + 1);
                });
        }
    });
}

P.S

I wouldn't restructure the data as you proposed, because you need to have distinct SVG <path> elements – one for each "chapter" in the sequence. Having a distinct array for each of path, as you do now, is what enables you to create these <path>s via data() binding.

Depending on what you're trying to achieve, you may even want to further nest each path array, wrapping it with an object {}, to hold meta data about the path:

var pathData = [
  {
    name: "Crossing the Niemen",
    points: [ [240, 100], [290, 200], [340, 50] ]
  },
  {
    name: "March on Vilnius",
    points: [ [340, 50], [90, 150], [140, 50], [190, 200] ]
  },
  {
    name: "March on Moscow",
    points: [ [190, 200], [70, 180], [30, 30], [350, 160] ]
  }
];
查看更多
登录 后发表回答