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