Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 19, 2017 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kcnarf/2df494f34292f24964785a25d10e69c4 to your computer and use it in GitHub Desktop.
Save Kcnarf/2df494f34292f24964785a25d10e69c4 to your computer and use it in GitHub Desktop.
Voronoï playground: Voronoï map (study 2)
license: gpl-3.0
border: no

This block is a Work In Progress, a continuation of a previous block, and tries to implements a Voronoï map (i.e., one-level treemap) based on the algorithm presented in Computing Voronoi Treemaps - Faster, Simpler, and Resolution-independent, and augmented with heuristics found on a Java implementation done by Arlind Nocaj, one of the co-authors of the scientific paper. Because this algorithm requires the computation of weighted Voronoï, this block uses the d3-weighted-voronoi plugin.

Without going into details (cf. the scientific paper), the overall algorithm doesn't differs from the one used in the previous block. Hence, at each iteration :

  • the weighted voronoi diagram is computed based on each weighted sites
  • then, each site is re-position at the center of its influence area
  • then, another weighted voronoi diagram is computed based on each weighted relocated sites
  • then, each site is re-weighted in order to fit its expected influence area

The algorithm stops when each site is no longer moved or re-weighted (more exactly, when each site is moved or is re-weighted below a certain treshold), or if a certain number of iterations is reached.

Differences from the algorithm used in the previous block are :

  • rework the limitation of weights when re-positioning and when re-weighting sites; both handled in function handleOverweigthed, which prevent any site to be overweighted (and hence prevent any site to produce an empty cell)
  • heuristics to mitigate near-zero weight (if active, min allowed weight = (max weight / 1000)); near-zero weights are difficult to handle because re-positioning systematically overweighted near-zero sites, making stabilization difficult to reach
  • heuristics to mitigate flickering (if active, if area error < 5% of total area, the more the flickering count, the less sites are re-positioned and re-weigted)

Notes :

  • only one level, no nested hierarchy
  • compared to the previous block, the algorithm used in this block performs better : in term of representativity of initial weights (reaching error below 1% is common, compared to 15%-20% in the previous block), and in term of number of iterations needed to reach the stable state
  • experience shows that stabilization is not reached when a lightweighted site is trapped between 2 heavyweighted sites and the edges of the encosing polygon; this is because (i) heavyweighted sites have reached their targeted area and don't need to be re-positioned or re-weighted by their own, (ii) lightweighted site has much larger area (than the one targeted) but can no longer lowers its weight (it has reached the minimim weight), and (iii) the others sites haven't enought energy to grow and reach their targeted area (which would push heavyweighedt sites on the lightweighted one)

User interactions :

  • you can choose to draw the Weighted Voronoï Diagram (default) or the weights (visualized as circles).
  • you can hide/show sites
  • you can choose among different rendering (greyscale, radial rainbow, or conical rainbow (default, having hard-&-fun time to implement it because canvas don't provides conical gradient)).

Acknowledgments to :

<html lang="en">
<head>
<meta charset="utf-8" />
<title>Voronoï playground: Voronoï map (study 2)</title>
<meta name="description" content="Voronoi map in D3.js (study 2)">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://raw.githack.com/Kcnarf/d3-weighted-voronoi/master/build/d3-weighted-voronoi.js"></script>
<style>
#layouter {
text-align: center;
position: relative;
}
#wip {
display: none;
position: absolute;
top: 200px;
left: 330px;
font-size: 40px;
text-align: center;
}
.control {
position: absolute;
}
.control.top{
top: 5px;
}
.control.bottom {
bottom: 5px;
}
.control.left{
left: 5px;
}
.control.right {
right: 5px;
}
.control.right div{
text-align: right;
}
.control.left div{
text-align: left;
}
.control .separator {
height: 5px;
}
canvas {
margin: 1px;
border-radius: 1000px;
box-shadow: 2px 2px 6px grey;
}
canvas#background-image, canvas#alpha {
display: none;
}
</style>
</head>
<body>
<div id="layouter">
<canvas id="background-image"></canvas>
<canvas id="alpha"></canvas>
<canvas id="colored"></canvas>
<div id="control0" class="control top left">
<div>
<input id="cellsOrWeights" type="radio" name="cellsOrWeights" value="cells" checked onchange="cellsOrWeightsUpdated('cells')"> Weighted Voronoï
</div>
<div>
<input id="cellsOrWeights" type="radio" name="cellsOrWeights" value="circles" onchange="cellsOrWeightsUpdated('weights')"> Weights
</div>
<div>
<input id="cellsOrWeights" type="radio" name="cellsOrWeights" value="circles" onchange="cellsOrWeightsUpdated('cellsAndWeights')"> Both
</div>
</div>
<div id="control1" class="control bottom left">
<div>
<input id="showSites" type="checkbox" name="showSites" onchange="siteVisibilityUpdated()"> Show sites
</div>
</div>
<div id="control2" class="control bottom right">
<div>
Grey <input id="bgImgGrey" type="radio" name="bgImg" onchange="bgImgUpdated('grey')">
</div>
<div>
Radial rainbow <input id="bgImgRadialRainbow" type="radio" name="bgImg" onchange="bgImgUpdated('radialRainbow')">
</div>
<div>
Canonical rainbow <input id="bgImgCanonicalRainbow" type="radio" name="bgImg" checked onchange="bgImgUpdated('canonicalRainbow')">
</div>
</div>
<div id="wip">
Work in progress ...
</div>
</div>
<script>
var _PI = Math.PI,
_2PI = 2*Math.PI,
sqrt = Math.sqrt,
sqr = function(d) { return Math.pow(d,2); }
epsilon = 1;
//begin: layout conf.
var totalWidth = 550,
totalHeight = 500,
controlsHeight = 0,
canvasbw = 1, // canvas border width
canvasbs = 8, // canvas box-shadow
radius = (totalHeight-controlsHeight-canvasbs-2*canvasbw)/2,
width = 2*radius,
height = 2*radius,
halfRadius = radius/2
halfWidth = halfRadius,
halfHeight = halfRadius,
quarterRadius = radius/4;
quarterWidth = quarterRadius,
quarterHeight = quarterRadius;
//end: layout conf.
//begin: drawing conf.
var drawSites = false,
bgType = "canonicalRainbow",
drawCellsOrWeights = "cells",
bgImgCanvas, alphaCanvas, coloredCanvas,
bgImgContext, alphaContext, coloredContext,
radialGradient;
//end: drawing conf.
//begin: init layout
initLayout()
//end: init layout
//begin: voronoi treemap conf.
var clippingPolygon = computeClippingPolygon(),
weightedVoronoi = d3.weightedVoronoi().clip(clippingPolygon),
siteCount = 20,
baseWeight = 2000,
outlierCount = siteCount/10,
outlierWeight = 3*baseWeight,
convergenceTreshold = 0.01, // 0.01 means 1% error
totalArea = Math.abs(d3.polygonArea(clippingPolygon)),
areaErrorTreshold = convergenceTreshold*totalArea,
areaErrorHistory = [], // used to detect flickering
areaErrorHistoryLength = 10,
maxIterationCount = 200,
shouldBreakOnMaxIteration = true;
var totalWeight, avgWeight;
//end: voronoi treemap conf.
//begin: algorithm conf.
var shouldComputeVoronoiAfterReposition = true,
handleOverweightedVariant = 1,
shouldRotateHandleOverweighted = false, // mixing severall heuristics seems to performs better (limit flickering), but sometimes freezes (infinite loop in handleOverweighted1)
shouldMinimizeWeight = false, // when activated, not flickering, but stabilization at higher iterations
shouldHandleNearZeroWeights = true,
nearZeroWeightRatio = 0.01, // 0.01 means min allowed weight = 1% of max weight
adaptPlacementsVariant = 1,
adaptWeightsVariant = 1;
var handleOverweight;
//end: algorithm conf.
function computeClippingPolygon() {
var circlingPolygon = [];
for (a=0; a<_2PI; a+=_2PI/60) {
circlingPolygon.push(
[radius + (radius-1)*Math.cos(a), radius + (radius-1)*Math.sin(a)]
)
}
return circlingPolygon;
};
//begin: user interaction handlers
function siteVisibilityUpdated() {
drawSites = d3.select("#showSites").node().checked;
};
function bgImgUpdated(newType) {
bgType = newType;
resetBackgroundImage() ;
};
function cellsOrWeightsUpdated(type) {
drawCellsOrWeights = type;
};
//end: user interaction handlers
function adapt(polygons, iterationCount) {
var converged, adaptedTreemapPoints;
if (shouldRotateHandleOverweighted) {
rotateHandleOverweighted();
}
adaptPlacements(polygons);
if (shouldComputeVoronoiAfterReposition) {
adaptedTreemapPoints = polygons.map(function(p) { return p.site.originalObject; });
polygons = weightedVoronoi(adaptedTreemapPoints);
if (polygons.length<siteCount) {
console.log("at least 1 site has no area, which is not supposed to arise");
debugger;
}
}
adaptWeights(polygons);
adaptedTreemapPoints = polygons.map(function(p) { return p.site.originalObject; });
polygons = weightedVoronoi(adaptedTreemapPoints);
if (polygons.length<siteCount) {
console.log("at least 1 site has no area, which is not supposed to arise");
debugger;
}
redraw(adaptedTreemapPoints, polygons);
converged = overallConvergence(polygons);
if (shouldBreakOnMaxIteration && iterationCount===maxIterationCount) {
console.log("Max iteration reached")
setTimeout(reset, 1750);
} else {
if (converged) {
console.log("Stopped at iteration "+iterationCount);
finalize(adaptedTreemapPoints, polygons, 20);
} else {
setTimeout(function(){
adapt(polygons, iterationCount+1);
}, 50);
}
}
};
function finalize(treemapPoints, polygons, countDown) {
redraw(treemapPoints, polygons);
if (countDown === 0) {
setTimeout(reset, 1750);
} else {
setTimeout(function(){
finalize(treemapPoints, polygons, countDown-1);
}, 50);
}
}
function adaptPlacements0(polygons) {
var newTreemapPoints = [];
var polygon, treemapPoint, centroid;
for(var i=0; i<siteCount; i++) {
polygon = polygons[i];
treemapPoint = polygon.site.originalObject;
centroid = d3.polygonCentroid(polygon);
treemapPoint.x = centroid[0];
treemapPoint.y = centroid[1];
newTreemapPoints.push(treemapPoint);
}
handleOverweighted(newTreemapPoints);
};
// flickering mitigation
function adaptPlacements1(polygons) {
var newTreemapPoints = [];
var polygon, treemapPoint, centroid, flickeringInfluence;
flickeringInfluence = 0.5*flickeringMitigationRatio(polygons);
for(var i=0; i<siteCount; i++) {
polygon = polygons[i];
treemapPoint = polygon.site.originalObject;
centroid = d3.polygonCentroid(polygon);
dx = centroid[0] - treemapPoint.x;
dy = centroid[1] - treemapPoint.y;
//begin: handle excessive change;
dx *= (1-flickeringInfluence);
dy *= (1-flickeringInfluence);
//end: handle excessive change;
treemapPoint.x += dx;
treemapPoint.y += dy;
newTreemapPoints.push(treemapPoint);
}
handleOverweighted(newTreemapPoints);
};
function adaptWeights0(polygons) {
var newTreemapPoints = [];
var polygon, treemapPoint, currentArea, adaptRatio;
for(var i=0; i<siteCount; i++) {
polygon = polygons[i];
treemapPoint = polygon.site.originalObject;
currentArea = d3.polygonArea(polygon);
adaptRatio = treemapPoint.targetedArea/currentArea;
//begin: handle excessive change;
adaptRatio = Math.max(adaptRatio, 0.9);
adaptRatio = Math.min(adaptRatio, 1.1);
//end: handle excessive change;
adaptedWeight = treemapPoint.weight*adaptRatio;
adaptedWeight = Math.max(adaptedWeight, epsilon);
treemapPoint.weight = adaptedWeight;
newTreemapPoints.push(treemapPoint);
}
handleOverweighted(newTreemapPoints);
};
// flickering mitigation
function adaptWeights1(polygons) {
var newTreemapPoints = [];
var polygon, treemapPoint, currentArea, adaptRatio, flickeringInfluence;
flickeringInfluence = 0.1*flickeringMitigationRatio(polygons);
for(var i=0; i<siteCount; i++) {
polygon = polygons[i];
treemapPoint = polygon.site.originalObject;
currentArea = d3.polygonArea(polygon);
adaptRatio = treemapPoint.targetedArea/currentArea;
//begin: handle excessive change;
adaptRatio = Math.max(adaptRatio, 0.9+flickeringInfluence);
adaptRatio = Math.min(adaptRatio, 1.1-flickeringInfluence);
//end: handle excessive change;
adaptedWeight = treemapPoint.weight*adaptRatio;
adaptedWeight = Math.max(adaptedWeight, epsilon);
treemapPoint.weight = adaptedWeight;
newTreemapPoints.push(treemapPoint);
}
handleOverweighted(newTreemapPoints);
};
// heuristics: lower heavy weights
function handleOverweighted0(treemapPoints) {
var fixCount = 0;
var fixApplied, tpi, tpj, weightest, lightest, sqrD, adaptedWeight;
do {
fixApplied = false;
for(var i=0; i<siteCount; i++) {
tpi = treemapPoints[i];
for(var j=i+1; j<siteCount; j++) {
tpj = treemapPoints[j];
if (tpi.weight > tpj.weight) {
weightest = tpi;
lightest = tpj;
} else {
weightest = tpj;
lightest = tpi;
}
sqrD = squaredDistance(tpi, tpj);
if (sqrD < weightest.weight-lightest.weight) {
// adaptedWeight = sqrD - epsilon; // as in ArlindNocaj/Voronoi-Treemap-Library
// adaptedWeight = sqrD + lightest.weight - epsilon; // works, but below loc performs better (less flickering)
adaptedWeight = sqrD + lightest.weight/2;
adaptedWeight = Math.max(adaptedWeight, epsilon);
weightest.weight = adaptedWeight;
fixApplied = true;
fixCount++;
break;
}
}
if (fixApplied) { break; }
}
} while (fixApplied)
if (fixCount>0) {
if (shouldMinimizeWeight) {
minimizeWeight(treemapPoints);
}
console.log("# fix: "+fixCount);
}
}
// heuristics: increase light weights
function handleOverweighted1(treemapPoints) {
var fixCount = 0;
var fixApplied, tpi, tpj, weightest, lightest, sqrD, overweight;
do {
fixApplied = false;
for(var i=0; i<siteCount; i++) {
tpi = treemapPoints[i];
for(var j=i+1; j<siteCount; j++) {
tpj = treemapPoints[j];
if (tpi.weight > tpj.weight) {
weightest = tpi;
lightest = tpj;
} else {
weightest = tpj;
lightest = tpi;
}
sqrD = squaredDistance(tpi, tpj);
if (sqrD < weightest.weight-lightest.weight) {
overweight = weightest.weight - lightest.weight - sqrD
lightest.weight += overweight + epsilon;
fixApplied = true;
fixCount++;
break;
}
}
if (fixApplied) { break; }
}
} while (fixApplied)
if (fixCount>0) {
if (shouldMinimizeWeight) {
minimizeWeight(treemapPoints);
}
console.log("# fix: "+fixCount);
}
}
function minimizeWeight(treemapPoints) {
var minWeight = treemapPoints[0].weight;
for (var i=1; i<siteCount; i++) {
minWeight = Math.min(minWeight, treemapPoints[i].weight);
}
minWeight -= epsilon;
for (var i=0; i<siteCount; i++) {
treemapPoints[i].weight -= minWeight;
}
}
function squaredDistance(s0, s1) {
return sqr(s1.x - s0.x) + sqr(s1.y - s0.y);
};
function distance(s0, s1) {
return sqrt(squaredDistance(s0, s1));
};
function computeAreaError(polygons) {
//convergence based on summation of all sites current areas
var areaErrorSum = 0;
var polygon, treemapPoint, currentArea;
for(var i=0; i<siteCount; i++) {
polygon = polygons[i];
treemapPoint = polygon.site.originalObject;
currentArea = d3.polygonArea(polygon);
areaErrorSum += Math.abs(treemapPoint.targetedArea-currentArea);;
}
return areaErrorSum;
};
function overallConvergence(polygons) {
//convergence based on summation of all sites current areas
var areaError = computeAreaError(polygons);
areaErrorHistory.unshift(areaError);
if (areaErrorHistory.length>areaErrorHistoryLength) {
areaErrorHistory.pop();
}
console.log("error %: "+Math.round(areaError*100*1000/totalArea)/1000);
return areaError < areaErrorTreshold;
};
// should be computed once and used both in adaptPlacements and adaptweights
// should count flikering iteratively (memorize flickering position of old frame, detect flickering wrt. previous frame, not re-detect flickering on old frames)
function flickeringMitigationRatio(polygons) {
var flickeringCount = 0,
totalCount = 0,
initialIndexWeight = 3,
indexWeightDecrement = 1,
indexWeight = initialIndexWeight;
var error0, error1, direction, flickeringMitigationRatio;
if (areaErrorHistory.length < areaErrorHistoryLength) { return 0; }
if (computeAreaError(polygons) > totalArea/10) { return 0; }
error0 = areaErrorHistory[0];
error1 = areaErrorHistory[1];
direction = (error0 - error1) > 0;
for(var i=2; i<areaErrorHistory.length-2; i++) {
error0 = error1;
error1 = areaErrorHistory[i];
if (((error0-error1)>0) != direction) {
flickeringCount += indexWeight;
direction = !direction;
}
totalCount += indexWeight;
indexWeight -= indexWeightDecrement;
if (indexWeight<1) {
indexWeight = 1;
}
}
flickeringMitigationRatio = flickeringCount/totalCount;
if (flickeringMitigationRatio>0) {
console.log("flickering mitigation ratio: "+Math.floor(flickeringMitigationRatio*1000)/1000);
}
return flickeringMitigationRatio;
}
function setAdaptPlacements() {
switch (adaptPlacementsVariant) {
case 0:
adaptPlacements = adaptPlacements0;
break;
case 1:
adaptPlacements = adaptPlacements1;
break;
default:
console.log("Variant of 'adaptPlacements' is unknown")
}
};
function setAdaptWeights() {
switch (adaptWeightsVariant) {
case 0:
adaptWeights = adaptWeights0;
break;
case 1:
adaptWeights = adaptWeights1;
break;
default:
console.log("Variant of 'adaptWeights' is unknown")
}
};
function setHandleOverweighted() {
switch (handleOverweightedVariant) {
case 0:
handleOverweighted = handleOverweighted0;
break;
case 1:
handleOverweighted = handleOverweighted1;
break;
default:
console.log("Variant of 'handleOverweighted' is unknown")
}
};
function rotateHandleOverweighted() {
handleOverweightedVariant = (handleOverweightedVariant+1)%2;
setHandleOverweighted();
};
function reset() {
var basePoints = [];
var weight, treemapPoints, polygons;
//begin: create points
for (i=0; i<siteCount; i++) {
weight = (0+1*sqr(Math.random()))*baseWeight;
// weight = (i+1)*baseWeight; // +1: weights of 0 are not handled
// weight = i+1; // +1: weights of 0 are not handled
basePoints.push({
index: i,
weight: weight
});
}
for (i=0; i<outlierCount; i++) {
basePoints[i].weight = outlierWeight;
}
//end: create points
if (shouldHandleNearZeroWeights) {
handleNearZeorWeights(basePoints);
}
// create treemap-related points
// (with targetedArea, and initial placement)
// choose among several inital placement policies: random/pie/sortedPie
treemapPoints = createTreemapPoints(basePoints, 'random');
polygons = weightedVoronoi(treemapPoints);
areaErrorHistory = [];
alphaContext.clearRect(0, 0, width, height);
redraw(treemapPoints, polygons);
setTimeout(function(){
adapt(polygons, 0);
}, 1500);
};
function handleNearZeorWeights(basePoints) {
var maxWeight = basePoints.reduce(function(max, bp){
return Math.max(max, bp.weight);
}, -Infinity);
var minAllowedWeight = maxWeight*nearZeroWeightRatio,
nearZeroCount = 0;
basePoints.forEach(function(bp) {
if (bp.weight<minAllowedWeight) {
bp.weight = minAllowedWeight;
nearZeroCount++;
}
})
if (nearZeroCount>0) {
console.log("# near-zero weights: "+nearZeroCount);
}
}
function createTreemapPoints(basePoints, initialPlacementPolicy) {
var totalWeight = basePoints.reduce(function(acc, bp){ return acc+=bp.weight; }, 0);
var avgWeight = totalWeight/siteCount;
avgArea = totalArea/siteCount,
defaultWeight = avgArea/2;
if (initialPlacementPolicy === 'sortedPie') {
// sortedPie ensures :
// - a gradient from light weights to heavy weights
// - i.e., light weights will not be confine between heavy weights
// - i.e., light weights will move/re-weight more easily
var sortedBasePoints = basePoints.sort(function(bp0, bp1){
return bp0.weight < bp1.weight;
});
return createTreemapPoints(sortedBasePoints, 'pie');
}
else if (initialPlacementPolicy === 'pie') {
var deltaRad = _2PI/siteCount;
var rad;
return basePoints.map(function(bp, i) {
rad = deltaRad*i;
return {
index: bp.index,
targetedArea: totalArea*bp.weight/totalWeight,
data: bp,
x: radius+halfRadius*Math.cos(rad)+Math.random(),
y: radius+halfRadius*Math.sin(rad)+Math.random(),
weight: defaultWeight
}
})
} else {
var xExtent = d3.extent(clippingPolygon.map(function(p){return p[0];})),
yExtent = d3.extent(clippingPolygon.map(function(p){return p[1];})),
dx = xExtent[1]-xExtent[0],
dy = yExtent[1]-yExtent[0];
var x,y;
return basePoints.map(function(bp) {
//use (x,y) instead of (r,a) for a better uniform placement of sites (ie. less centered)
x = xExtent[0]+dx*Math.random();
y = yExtent[0]+dy*Math.random();
while (!d3.polygonContains(clippingPolygon, [x, y])) {
x = xExtent[0]+dx*Math.random();
y = yExtent[0]+dy*Math.random();
}
return {
index: bp.index,
targetedArea: totalArea*bp.weight/totalWeight,
data: bp,
x: x,
y: y,
weight: defaultWeight
}
})
}
}
setAdaptPlacements();
setAdaptWeights();
setHandleOverweighted();
reset();
/********************************/
/* Drawing functions */
/* Playing with canvas :-) */
/* */
/* Experiment to draw */
/* with a uniform color, */
/* or with a radial gradient, */
/* or over a background image */
/********************************/
function initLayout() {
d3.select("#layouter").style("width", totalWidth+"px").style("height", totalHeight+"px");
d3.selectAll("canvas").attr("width", width).attr("height", height);
bgImgCanvas = document.querySelector("canvas#background-image");
bgImgContext = bgImgCanvas.getContext("2d");
alphaCanvas = document.querySelector("canvas#alpha");
alphaContext = alphaCanvas.getContext("2d");
coloredCanvas = document.querySelector("canvas#colored");
coloredContext = coloredCanvas.getContext("2d");
//begin: set a radial rainbow
radialGradient = coloredContext.createRadialGradient(radius, radius, 0, radius, radius, radius);
var gradientStopNumber = 10,
stopDelta = 0.9/gradientStopNumber;
hueDelta = 360/gradientStopNumber,
stop = 0.1,
hue = 0;
while (hue<360) {
radialGradient.addColorStop(stop, d3.hsl(Math.abs(hue+160), 1, 0.45));
stop += stopDelta;
hue += hueDelta;
}
//end: set a radial rainbow
//begin: set the background image
resetBackgroundImage();
//end: set the initial background image
}
function resetBackgroundImage() {
if (bgType==="canonicalRainbow") {
//begin: make conical rainbow gradient
var imageData = bgImgContext.getImageData(0, 0, 2*radius, 2*radius);
var i = -radius,
j = -radius,
pixel = 0,
radToDeg = 180/Math.PI;
var aRad, aDeg, rgb;
while (i<radius) {
j = -radius;
while (j<radius) {
aRad = Math.atan2(j, i);
aDeg = aRad*radToDeg;
rgb = d3.hsl(aDeg, 1, 0.45).rgb();
imageData.data[pixel++] = rgb.r;
imageData.data[pixel++] = rgb.g;
imageData.data[pixel++] = rgb.b;
imageData.data[pixel++] = 255;
j++;
}
i++;
}
bgImgContext.putImageData(imageData, 0, 0);
//end: make conical rainbow gradient
} else if (bgType==="radialRainbow") {
bgImgContext.fillStyle = radialGradient;
bgImgContext.fillRect(0, 0, width, height);
} else {
bgImgContext.fillStyle = "grey";
bgImgContext.fillRect(0, 0, width, height);
}
}
function redraw(points, polygons) {
// At each iteration:
// 1- update the 'alpha' canvas
// 1.1- fade 'alpha' canvas to simulate passing time
// 1.2- add the new tessellation/weights to the 'alpha' canvas
// 2- blend 'background-image' and 'alpha' => produces colorfull rendering
alphaContext.lineWidth= 2;
fade();
alphaContext.beginPath();
//begin: add the new tessellation/weights (to the 'grey-scale' canvas)
if (drawCellsOrWeights==="cellsAndWeights") {
alphaContext.globalAlpha = 0.5;
polygons.forEach(function(polygon){
addCell(polygon);
});
alphaContext.globalAlpha = 0.2;
points.forEach(function(point){
addWeight(point);
});
} else if (drawCellsOrWeights==="cells") {
alphaContext.globalAlpha = 0.5;
polygons.forEach(function(polygon){
addCell(polygon);
});
} else {
alphaContext.globalAlpha = 0.2;
points.forEach(function(point){
addWeight(point);
});
}
//begin: add the new tessellation/weights (to the 'grey-scale' canvas)
if (drawSites) {
//begin: add sites (to 'grey-scale' canvas)
alphaContext.globalAlpha = 1;
points.forEach(function(point){
addSite(point);
});
//begin: add sites (to 'grey-scale' canvas)
}
alphaContext.stroke();
//begin: use 'background-image' to color pixels of the 'grey-scale' canvas
coloredContext.clearRect(0, 0, width, height);
coloredContext.globalCompositeOperation = "source-over";
coloredContext.drawImage(bgImgCanvas, 0, 0);
coloredContext.globalCompositeOperation = "destination-in";
coloredContext.drawImage(alphaCanvas, 0, 0);
//begin: use 'background-image' to color pixels of the 'grey-scale' canvas
}
function addCell(polygon) {
alphaContext.moveTo(polygon[0][0], polygon[0][1]);
polygon.slice(1).forEach(function(vertex){
alphaContext.lineTo(vertex[0], vertex[1]);
});
alphaContext.lineTo(polygon[0][0], polygon[0][1]);
}
function addWeight(point) {
var radius = sqrt(point.weight);
alphaContext.moveTo(point.x+radius, point.y);
alphaContext.arc(point.x, point.y, radius, 0, _2PI);
}
function addSite(point) {
alphaContext.moveTo(point.x, point.y);
alphaContext.arc(point.x, point.y, 1, 0, _2PI);
// alphaContext.fillText(Math.round(point.weight), point.x, point.y);
}
function fade() {
var imageData = alphaContext.getImageData(0, 0, width, height);
for (var i = 3, l = imageData.data.length; i < l; i += 4) {
imageData.data[i] = Math.max(0, imageData.data[i] - 10);
}
alphaContext.putImageData(imageData, 0, 0);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment