m.brocks
m.brocks

Reputation: 53

How to set a specific duration to interpolate along a path one point at time?

I'm trying to figure out the best way to interpolate a circle along a path as Mike Bostock does in this example: http://bl.ocks.org/mbostock/1705868. However, instead of setting one transition value as he does, I'd like to be able to set a unique duration for each point-to-point interpolation; e.g., transition the circle from node[0] to node [1] over x milliseconds, transition from node [1] to node [2] over y milliseconds, etc. Is there a way to do this without splitting the path up into a bunch of smaller separate paths and transitioning along them consecutively? The limiting factor seems to be path.getTotalLength() - is there a way to get the length of only the subset of a path?

transition();

function transition() {
   circle.transition()
   .duration(10000)
   .attrTween("transform", translateAlong(path.node()))
   .each("end", transition);
}

// Returns an attrTween for translating along the specified path element.
function translateAlong(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 + ")";
   };
};
}

Upvotes: 0

Views: 564

Answers (1)

Mauricio Poppe
Mauricio Poppe

Reputation: 4876

There's in a fact a way but it's way too ugly (because it needs an initial brute force computation), the solution involves the following:

First of all you need an array with the transition times between nodes, in my example is times, for example the first element 3000 corresponds to the time in ms to get from [480,200] to [580,400]

  • compute the sum of the transition times (needed for the duration of the overall transition)
  • compute the linear time in ms to reach each one of the points that made this path, this is actually tricky when the path between two points is not a line e.g. a curve, in my example I compute those times by brute force which makes it ugly, it'd be awesome if there was a method that computed the path length needed to get to some point lying on the path itself, unfortunately such a method doesn't exist as far as I know
  • Finally once you know the linear times you have to compute the correct time as if it followed the list of the numbers in the times array e.g.

Let's say that the linear time to get to the first point is 50ms and we're currently on the time t < 50ms, we have to map this value which is between [0ms, 50ms] to somewhere in the range [0ms, 3000ms] which is given by the formula 3000 * (t ms - 0ms) / (50ms - 0ms)

var points = [
  [480, 200],
  [580, 400],
  [680, 100],
  [780, 300],
  [180, 300],
  [280, 100],
  [380, 400]
];

var times = [3000, 100, 5000, 100, 3000, 100, 1000]
var totalTime = times.reduce(function (a, b) {return a + b}, 0)

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

var path = svg.append("path")
    .data([points])
    .attr("d", d3.svg.line()
    .tension(0) // Catmull–Rom
    .interpolate("cardinal-closed"));

svg.selectAll(".point")
    .data(points)
  .enter().append("circle")
    .attr("r", 4)
    .attr("transform", function(d) { return "translate(" + d + ")"; });

var circle = svg.append("circle")
    .attr("r", 13)
    .attr("transform", "translate(" + points[0] + ")");

function transition() {
  circle.transition()
      .duration(totalTime)
      .ease('linear')
      .attrTween("transform", translateAlong(path.node()))
      .each("end", transition);
}

// initial computation, linear time needed to reach a point
var timeToReachPoint = []
var pathLength = path.node().getTotalLength();
var pointIndex = 0
for (var t = 0; pointIndex < points.length && t <= 1; t += 0.0001) {
  var data = points[pointIndex]
  var point = path.node().getPointAtLength(t * pathLength)
  // if the distance to the point[i] is approximately less than 1 unit
  // make `t` the linear time needed to get to that point
  if (Math.sqrt(Math.pow(data[0] - point.x, 2) + Math.pow(data[1] - point.y, 2)) < 1) {
    timeToReachPoint.push(t);
    pointIndex += 1
  }
}
timeToReachPoint.push(1)

function translateAlong(path) {
  return function(d, i, a) {
    return function(t) {
      // TODO: optimize
      var timeElapsed = t * totalTime     
      var acc = 0
      for (var it = 0; acc + times[it] < timeElapsed; it += 1) {
        acc += times[it]
      }
      var previousTime = timeToReachPoint[it]
      var diffWithNext = timeToReachPoint[it + 1] - timeToReachPoint[it]
      // range mapping
      var placeInDiff = diffWithNext * ((timeElapsed - acc) / times[it])     
      var p = path.getPointAtLength((previousTime + placeInDiff) * pathLength)
      return "translate(" + p.x + "," + p.y + ")"
    }
  }
}

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

circle {
  fill: steelblue;
  stroke: #fff;
  stroke-width: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 2

Related Questions