如何设置一个特定的持续时间,以便在一个时间点沿路径进行插值?

时间:2016-04-26 04:21:04

标签: d3.js transition interpolation

我正在尝试找出沿着路径插入圆圈的最佳方式,就像迈克博斯托克在这个例子中所做的那样:http://bl.ocks.org/mbostock/1705868。但是,我不想像他那样设置一个过渡值,而是希望能够为每个点对点插值设置一个唯一的持续时间;例如,在x毫秒内将圆从节点[0]转换到节点[1],在y毫秒内从节点[1]转换到节点[2]等等。有没有办法在不将路径分割成a一堆较小的独立路径并连续沿着它们过渡?限制因素似乎是path.getTotalLength() - 有没有办法获得路径子集的长度?

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

1 个答案:

答案 0 :(得分:2)

事实上有一种方式,但它太丑陋了(因为它需要初始暴力计算),解决方案涉及以下内容:

首先,您需要一个具有节点之间转换时间的数组,在我的示例中为times,例如,第一个元素3000对应于从{{1}获取的以ms为单位的时间} [480,200]

  • 计算转换时间的总和(整体转换持续时间所需)
  • 计算以毫秒为单位的线性时间以到达构成该路径的每个点,当两点之间的路径不是线时,这实际上是棘手的。一个曲线,在我的例子中,我通过蛮力计算那些时间使它变得丑陋,如果有一个方法计算到达路径本身的某个点所需的路径长度,那就太棒了,不幸的是这样的方法没有据我所知,存在
  • 最后,一旦你知道线性时间,就必须计算正确的时间,就好像它遵循[580,400]数组中的数字列表,例如

假设到达第一个点的线性时间是50ms,我们当前的时间是times,我们必须将这个值在[0ms,50ms]之间映射到范围内的某个位置[0ms,3000ms],由公式t < 50ms

给出

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