This is an attempt at finding a way to make cleaner transitions between polygons. mbostock's block: Shape Tweening works excellently for morphing a shape into a circle, but not for morphing into smoething like a polygon. I was looking for a way to do something much like his other block: Superformula Tweening, but with the ability to pass coordinates for the new shape instead of control points for the superformula to make sense of.
The three biggest challenges were:
Finding the starting points for each path was simply done by looping through the incomming shape's points to find the closest point to the start of the outgoing shape's path. Then it's just a matter of changing the order of the incomming shape's points.
For matching the number of points, I created a linear scale using the length of the outgoing shape and plotted the necessary points along the incomming shape's path proportionally to where they were along the outgoing shape's path. This required using a hidden path element '#hiddenShape' in order to be able to compare the two before rendering the new shape. Most of this is done within the converPath() function.
And matching the orientation of the shape's points was done by using d3.polygonArea(). This tells you the orientation by being either positive or negative, so some logic at the end of convertPath() decides if it should return the array as-is, or reversed.
forked from alexmacy's block: Custom Shape Tweening
xxxxxxxxxx
<meta charset="utf-8">
<head>
<style>
polygon {
fill: #ccc;
stroke: #000;
}
.small:hover {
fill-opacity: .5;
}
</style>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="california.js"></script>
<script src="shapes.js"></script>
</head>
<body></body>
<script>
var width = 960,
selectHeight = 100,
selectShapeWidth = width/(shapes.length + 1),
height = 500 - selectHeight;
var selectSvg = d3.select("body").append("svg")
.style("width", width)
.style("height", selectHeight);
var selectContainer = selectSvg.append("g")
.attr("transform", "translate(50,0)");
selectContainer.selectAll("g")
.data(shapes)
.enter().append("g")
.attr("transform", function(d, i) {return "translate(" + i * selectShapeWidth + ", " + selectHeight/2 + ")"})
.append("path")
.attr("class", "small")
.attr("d", d3.line())
.attr("transform", "translate(0,0)scale(.2)")
.on("click", function(d, i) {loop(i)});
selectContainer.append("g")
.attr("transform", "translate(" + (width - selectShapeWidth) + ", " + selectHeight/2 + ")")
.datum(californiaShape)
.append("path")
.attr("class", "small")
.attr("d", d3.line())
.attr("transform", "translate(0,0)scale(.2)")
.on("click", function(d, i) {
shape.transition().duration(1000)
.attr("points", californiaShape)
.transition().delay(1000)
.on("end", function() {loop(0)})
});
var mainSvg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(0," + selectHeight + ")");
var shape = mainSvg.append("polygon")
.attr("id","state")
.attr("transform", "translate(" + width/2 + ", " + height/2 + ")");
var hiddenShape = mainSvg.append("path")
.attr("id", "hiddenShape")
.style("visibility", "hidden");
shape.transition().duration(1000)
.attr("points", californiaShape)
.transition().delay(1000)
.on("end", function() {loop(0)});
function loop(i) {
shape
.transition().duration(2000)
.attr("points", morph(californiaShape, shapes[i]))
.transition().delay(1500).duration(2000)
.attr("points", californiaShape)
.transition().delay(500)
.on("end", function() {
(i < shapes.length - 1) ? loop(++i) : loop(0);
});
}
function morph(oldShape, newShape) {
//start with finding the closest point of the new shape to the starting point of the original shape, rotating the array to make the starting points of the two shapes as close as possible.
var closestDist = calcDistance(newShape[0], oldShape[0]),
closestPoint,
tempArray = [];
for (i in newShape) {
tempArray.push(newShape[i]);
var thisDist = calcDistance(newShape[i], oldShape[0]);
if (closestDist > thisDist) {
closestDist = thisDist;
closestPoint = i;
}
}
newShape = tempArray.splice(closestPoint).concat(tempArray);
var distances = getDistances(oldShape),
totalLength = d3.max(getDistances(newShape)),
coordsScale = d3.scaleLinear().range([0,1]).domain([0,d3.max(distances)])
hiddenShape.datum(newShape).attr("d", d3.line());
for (i in oldShape) {
var newPoint = document
.getElementById("hiddenShape")
.getPointAtLength(coordsScale(distances[i]) * totalLength);
newShape[i] = [newPoint.x, newPoint.y];
}
//check the rotational direction by calculating the polygon's area. reverse the array of points if needed.
return d3.polygonArea(newShape) < 0 ? newShape : newShape.reverse();
//get distances along the perimeter for plotting the points proportionally
function getDistances(coordsArray) {
var distances = [0];
for (i=1; i<coordsArray.length; i++) {
distances[i] = distances[i-1] + calcDistance(coordsArray[i-1], coordsArray[i]);
}
return distances;
}
//convenience function for calculating distance between two points
function calcDistance(coord1, coord2) {
var distX = coord2[0] - coord1[0];
var distY = coord2[1] - coord1[1];
return Math.sqrt(distX * distX + distY * distY);
}
}
</script>
https://d3js.org/d3.v4.min.js