Reputation: 53
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
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]
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