This block experiments a new way to transition from one Voronoï tesselation to another.
Usually, this is achieved by smoothly changing the locations of sites from their originals locations to their finals locations, while recomputing the Voronoï tessellation during displacements. Here, I use weighted Voronoï diagram (the 2D additive weighted power diagram variation) to do the job. It gives a totally new feeling: the final tesselation seems to emerge from the initial one. Chery on the cake, this technic allows to transition between tessellations of distinct number of sites!
This block is a continuation of 3, 2 and 1.
This is still a Work In Progress, as the code is not yet packaged/self-contained, and contains weird hacks.
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Voronoï playground : interactive Voronoï transitioning thanks to weighted Voronoï</title>
<meta name="description" content="Transitioning from one Voronoï tessellation to another thanks to Weighted Voronoï in D3.js">
<script src="//d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script language="javascript" type="text/javascript" src="ConvexHull.js"></script>
<script language="javascript" type="text/javascript" src="PowerDiagram.js"></script>
<script language="javascript" type="text/javascript" src="d3-polygon-clip.js"></script>
<style>
#wip {
display: none;
position: absolute;
top: 200px;
left: 330px;
font-size: 40px;
text-align: center;
}
.control {
position: absolute;
top: 5px;
}
.control#control-0 {
left: 5px;
text-align: center;
}
.control#control-1 {
left: 5px;
top: 25px;
}
.control#control-2 {
right: 5px;
top: 25px;
}
.control span {
width: 100px;
}
.control input[type="range"] {
width: 665px;
}
#drawing-area.dragging {
cursor: move;
}
#site-container {
clip-path: url("#clipper");
}
.seed {
fill: steelblue;
}
.seed.group-green {
fill: lightgreen;
}
.seed.hide {
display: none;
}
.cell {
fill-opacity: 0.1;
fill: lightsteelBlue;
stroke: lightsteelBlue;
}
.cell.group-green {
fill: lightgreen;
stroke: lightgreen;
}
</style>
</head>
<body>
<div id="control-0" class="control">
<span>Voronoï of blue sites</span>
<input id="weight" type="range" name="points" min="-15000" max="15000" value="0" oninput="weightUpdated()">
<span>Voronoï of green sites</span>
</div>
<div id="control-1" class="control">
<span>Show blue sites</span>
<input id="weight" type="checkbox" name="showSites" onchange="blueSiteVisibilityUpdated()">
</div>
<div id="control-2" class="control">
<input id="weight" type="checkbox" name="showSites" onchange="greenSiteVisibilityUpdated()">
<span>Show green sites</span>
</div>
<svg>
<defs>
<clipPath id="clipper">
<rect x="0" y="0" width="960" height="500" />
</clipPath>
</defs>
<g id="drawing-area">
<g id="cell-container"></g>
<g id="site-container"></g>
</g>
</svg>
<div id="wip">
Work in progress ...
</div>
</body>
<script>
var WITH_TRANSITION = true;
var WITHOUT_TRANSITION = false;
var duration = 250;
//begin: layout conf.
var svgWidth = 960,
svgHeight = 500,
margin = {top: 50, right: 10, bottom: 10, left: 10},
width = svgWidth - margin.left - margin.right,
height = svgHeight - margin.top - margin.bottom,
halfWidth = width/2,
halfHeight = height/2,
quarterWidth = width/4,
quarterHeight = height/4;
//end: layout conf.
//begin: voronoi stuff definitions
var siteCount = 200,
halfSiteCount = siteCount/2;
var blueSites = [],
greenSites = [];
var baseWeight = 10000;
for (i=0; i<halfSiteCount; i++) {
blueSites.push({index: i, group: "blue", x: width*Math.random(), y: height*Math.random(), weight: baseWeight});
greenSites.push({index: i+siteCount, group: "green", x: width*Math.random(), y: height*Math.random(), weight: baseWeight});
}
var sites = blueSites.concat(greenSites);
var clippingPolygon = [[0,0], [0,height], [width,height], [width,0]];
var cells = sites.map(function(s){ return []; }); // stores, for each site, each cell's verteces
//end: voronoi stuff definitions
//begin: utilities
function uc(w) { return (w<=0)? 0 : Math.sqrt(w); } //scale for unit-circles
var cellLiner = d3.line()
.x(function(d){ return d[0]; })
.y(function(d){ return d[1]; });
//end: utilities
//begin: reusable d3-selections
var svg = d3.select("svg"),
clipper = d3.select("#clipper>rect"),
drawingArea = d3.select("#drawing-area"),
cellContainer = d3.select("#cell-container"),
siteContainer = d3.select("#site-container");
//end: reusable d3-selections
//begin: user interaction handlers
function weightUpdated() {
var deltaWeight,
newBlueWeigth,
newGreenWeight;
deltaWeight = +d3.select("#control-0 input").node().value;
newBlueWeigth = baseWeight - deltaWeight;
newGreenWeight = baseWeight + deltaWeight;
blueSites.forEach(function(s){ s.weight = newBlueWeigth });
greenSites.forEach(function(s){ s.weight = newGreenWeight });
computeAllCells();
redrawAllCells(WITHOUT_TRANSITION);
}
function blueSiteVisibilityUpdated() {
visibility = d3.select("#control-1 input").node().checked? 1:0;
redrawGroup("blue", visibility , WITH_TRANSITION);
}
function greenSiteVisibilityUpdated() {
visibility = d3.select("#control-2 input").node().checked? 1:0;
redrawGroup("green", visibility, WITH_TRANSITION);
}
//end: user interaction handlers
computeAllCells();
initLayout();
redrawAllCells(WITHOUT_TRANSITION);
/***************/
/* Computation */
/***************/
function computeAllCells() {
//begin: map sites to the expected format of PowerDiagram
var formatedSites = sites.map(function(s) {
return new Vertex(s.x, s.y, null, s.weight, s, false);
})
var boundingSites = getBoundingSites();
//end: map sites to the expected format of PowerDiagram
cells = computePowerDiagramIntegrated(formatedSites, boundingSites, clippingPolygon);
}
function getBoundingSites() {
var siteBasedBounding = false; // when activated (as done in original code), resulting clipping is weird: losange-shaped clipping
var boundingSites = [],
xExtent, yExtent,
minX, maxX, minY, maxY,
x0, x1, y0, y1;
if (siteBasedBounding) {
xExtent = d3.extent(sites.map(function(s) {return s.x;}));
yExtent = d3.extent(sites.map(function(s) {return s.y;}));
} else {
xExtent = [0,width];
yExtent = [0,height];
}
minX = xExtent[0];
maxX = xExtent[1];
minY = yExtent[0];
maxY = yExtent[1];
x0 = minX - maxX;
x1 = 2 * maxX;
y0 = minY - maxY;
y1 = 2 * maxY;
var result = [];
result[0] = [x0, y0];
result[1] = [x1, y0];
result[2] = [x1, y1];
result[3] = [x0, y1];
for (var i = 0; i < result.length; i++){
boundingSites.push( new Vertex(result[i][0], result[i][1], null, epsilon, new Vertex(result[i][0], result[i][1], null, epsilon, null, true), true));
}
return boundingSites;
}
/***********/
/* Drawing */
/***********/
//redraw group = show/hide sites of particular group
function redrawGroup(color, finalOpacity, withTransition) {
siteContainer.selectAll(".seed").filter(function(d){ return d.group === color; })
.transition()
.duration(withTransition? duration : 0)
.attr("opacity", finalOpacity);
}
function redrawAllCells(withTransition) {
var cellSelection = cellContainer.selectAll(".cell")
.data(cells, function(c){ return c.site.originalObject.index; });
cellSelection.enter()
.append("path")
.attr("class", function(d){ return "group-"+d.site.originalObject.group; })
.classed("cell", true)
.attr("id", function(d,i){ return "cell-"+d.site.originalObject.index; })
.merge(cellSelection)
.transition()
.duration(withTransition? duration : 0)
.attr("d", function(d){ return cellLiner(d)+"z"; });
cellSelection.exit().remove();
}
function initLayout () {
svg.attr("width", svgWidth)
.attr("height", svgHeight);
clipper.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
drawingArea.attr("width", width)
.attr("height", height)
.attr("transform", "translate("+[margin.left, margin.top]+")");
//begin: draw sites
var drawnSites = siteContainer.selectAll(".site")
.data(sites)
.enter()
.append("g")
.attr("id", function(d){ return "site-"+d.index})
.classed("site", true);
drawnSites.append("circle")
.attr("id", function(d,i){ return "seed-"+i; })
.attr("class", function(d){ return "group-"+d.group; })
.classed("seed", true)
.attr("r", 2)
.attr("opacity", 0)
.attr("transform", function(d){ return "translate("+[d.x,d.y]+")"; });;
//end: draw sites
//begin: draw cells
cellContainer.selectAll(".cell")
.data(cells)
.enter()
.append("path")
.attr("class", function(d){ return "group-"+d.site.originalObject.group; })
.classed("cell", true)
.attr("id", function(d,i){ return "cell-"+d.site.originalObject.index; });
//end: draw cells
}
</script>
</html>
https://d3js.org/d3.v4.min.js