Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active February 14, 2020 13:04
Show Gist options
  • Save Kcnarf/c9592e99fd1b57b9608e7eade6e77a83 to your computer and use it in GitHub Desktop.
Save Kcnarf/c9592e99fd1b57b9608e7eade6e77a83 to your computer and use it in GitHub Desktop.
Voronoï playground - Do you see the path ? (II)
license: gpl-3.0

No path is visible, only cells !

The idea of this block comes when I look at Voronoï tesselation where (sometimes) pathes emerge, due to the smooth chaining of cells's borders. This is put to its extreme in this block.

This block is an enhancement of a previous one, where intersections and close path's portions are now handled.

Acknowledgments to:

<html>
<head>
<meta charset="utf-8">
<title>Voronoï playground : Do you see the path ? (II)</title>
<meta content="Voronoï playground. Path emerging from Voronoï cells. Works with intersections and close path's portions" name="description">
<style>
#options-absoluter {
width: 100%;
position: absolute;
bottom: 0px;
}
#options-relativer {
position: relative;
}
#options {
display: flex;
justify-content: space-around;
align-items: center;
text-align: center;
color: lightgrey;
background-color: white;
}
.options-separator {
height: 10px;
border: 1px solid lightgrey;
}
input {
color: lightgrey;
width: 400px;
text-align: end;
}
#drawing-area {
filter: url("#saturate-around-path");
}
#drawn-path {
fill: none;
stroke-linecap: round;
stroke: white;
stroke-width: 20px;
}
.cell {
fill: none;
stroke: cyan;
}
#predef-pathes-option path {
cursor: pointer;
fill: transparent;
stroke: lightgrey;
stroke-width: 50px;
}
#predef-pathes-option text {
cursor: pointer;
fill:lightgrey;
}
</style>
</head>
<body>
<svg>
<defs>
<filter id="saturate-around-path">
<feGaussianBlur result="blured" in="SourceAlpha" stdDeviation="5"/>
<feColorMatrix result="remove-pixels-to-far-from-path" in="blured" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 2 -0.5"/>
<feGaussianBlur result="blured2" in="remove-pixels-to-far-from-path" stdDeviation="10"/>
<feComposite result="composed" in="SourceGraphic" in2="blured2" operator="in"/>
</filter>
</defs>
</svg>
<div id="options-absoluter">
<div id="options-relativer">
<div id="options">
<div id="drag-opton" class="option">
Click and drag<br/>to make your own path.
</div>
<div class="options-separator"></div>
<div id="predef-pathes-option">
Or choose among predefined pathes<br/>
</div>
<div class="options-separator"></div>
<div id="input-option" class="option">
<input type="text" id="letters" value="Or paste/write a (valid) SVG.path.d, such as M100,100v300h760v-300z">
</div>
</div>
</div>
</div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var width = 960,
halfWidth = width/2,
height = 500,
halfHeight = height/2,
_2PI = 2*Math.PI,
halfPI = Math.PI/2,
LOW_SPEED = 2000,
HIGH_SPEED = 1000;
var sampling = 10, //lower values for smaller cells, at cost of performance
squaredSampling = Math.pow(sampling, 2),
maxSpread = 20, //max distance of voronoi seeds from path
squaredMaxSpread = Math.pow(maxSpread, 2);
var normalDistrib = d3.randomNormal(0,0.8); //spread voronoi seeds not far away
var pathLiner = d3.line()
.curve(d3.curveLinear);
var cellLiner = d3.line()
.curve(d3.curveLinearClosed); //good looking result with d3.curveBasisClosed, and artistic result with d3.curveStep
var strokeOpacityScale = d3.scaleLinear()
.domain([0, 2*maxSpread])
.range([1, 0]);
var predefPathes, pathData, voronoiSeedConstraints, voronoiSeeds;
initData();
var drawnPath, voronoiLayer;
initLayout();
var xAccessor = function(d) { return d.x; };
var yAccessor = function(d) { return d.y; };
var voronoi = d3.voronoi()
.x(xAccessor)
.y(yAccessor)
.extent([[0, 0], [width, height]]);
var input = d3.select("input")
.on("click", function() { this.value = "M100,100v300h760v-300z"; inputed(); })
.on("input", function() { inputed() });
//first render
updateVoronoi(LOW_SPEED);
///////////
// Init //
///////////
function initData() {
pathData = [];
voronoiSeedConstraints = [];
voronoiSeeds = [];
}
function initLayout() {
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.call(d3.drag()
.container(function(d) { return this; })
.subject(function(d) { var p = [d3.event.x, d3.event.y]; return [p, p]; })
.on("start", dragStarted)
.on("drag", dragging));
var drawingArea = svg.append("g")
.attr("id", "drawing-area");
drawnPath = drawingArea.append("path")
.attr("id", "drawn-path")
.attr("d", infinityPath());
voronoiLayer = drawingArea.append("g")
.attr("id", "voronoi-layer");
var predefPathes = getPredefPathes();
var predefPathesSvg = d3.select("#predef-pathes-option").append("svg")
.attr("width", ((predefPathes.length+2)*30)) //+2 for 'space' and '?'
.attr("height", 20);
//begin: draw predef pathes
predefPathesSvg.selectAll(".predef-path")
.data(predefPathes)
.enter()
.append("path")
.classed("predef-path", true)
.attr("d", function(d){ return d; })
.attr("transform", function(d,i){ return "translate("+[10+i*30,5]+")scale(0.025)"; })
.on("click", predefPathClicked)
predefPathesSvg.append("text")
.text("?")
.attr("x", 10+(predefPathes.length+1)*30) //+1 for space between pathes and '?'
.attr("y", 15)
.on("click", makeRandomPath);
//end: draw predef pathes
}
///////////////////////
// User interaction //
///////////////////////
function dragStarted() {
input.node().value = "Or paste/write a (valid) SVG.path.d, such as M100,100v300h760v-300z";
pathData = [];
voronoiSeedConstraints = [];
voronoiSeeds = [];
voronoiLayer.selectAll(".cell").remove();
}
function dragging() {
var d = d3.event.subject,
x0 = d3.event.x,
y0 = d3.event.y;
d3.event.on("drag", function() {
var mx = maxSpread,
x1 = d3.event.x,
y1 = d3.event.y,
dx = x1 - x0,
dy = y1 - y0;
if (x1<mx || x1>(width-mx) || y1<mx || y1>(height-mx)) { return; }
if (dx * dx + dy * dy > squaredSampling) {
pathData.push([x0 = x1, y0 = y1]);
drawnPath.attr("d", pathLiner(pathData));
updateVoronoi(HIGH_SPEED);
}
});
}
function predefPathClicked(d,i) {
input.node().value = "Or paste/write a (valid) SVG.path.d, such as M100,100v300h760v-300z";
initData();
voronoiLayer.selectAll(".cell").remove();
drawnPath.attr("d", d);
updateVoronoi(LOW_SPEED);
}
function makeRandomPath() {
var mx = maxSpread,
randomWidth = width - 2*mx,
randomHeight = height - 2*mx,
path = "";
var segmentcount, cps, cp, prevCp, prevPrevCp, v0, v1, squaredDist, angle, cpFound;
input.node().value = "Or paste/write a (valid) SVG.path.d, such as M100,100v300h760v-300z";
initData();
voronoiLayer.selectAll(".cell").remove();
segmentCount = Math.floor(1+2*Math.random());
d3.range(segmentCount).forEach(function(){
cps = []; v0 = []; v1 = [];
d3.range(8/segmentCount).forEach(function(i){
cpFound = false;
while (!cpFound) {
cp = [mx+randomWidth*Math.random(), mx+randomHeight*Math.random()];
prevCp = cps[i-1];
prevPrevCp = cps[i-2];
if (prevCp===undefined) {
cpFound = true;
} else {
v0[0] = cp[0] - prevCp[0];
v0[1] = cp[1] - prevCp[1];
squaredDist = Math.pow(v0[0], 2) + Math.pow(v0[1], 2);
if (squaredDist > squaredMaxSpread){ //cp far enought form prevCp
if (prevPrevCp===undefined) {
cpFound = true;
} else {
v1[0] = prevPrevCp[0] - prevCp[0];
v1[1] = prevPrevCp[1] - prevCp[1];
angle = (Math.atan2(v1[1], v1[0]) - Math.atan2(v0[1],v0[0]));
if (Math.abs(angle) > _2PI/18){ //not a back and forth, angle<20°
cpFound = true;
}
}
}
}
}
cps.push(cp);
})
path += d3.line().curve(d3.curveBasis)(cps);
})
drawnPath.attr("d", path);
updateVoronoi(LOW_SPEED);
}
function inputed() {
initData();
voronoiLayer.selectAll(".cell").remove();
drawnPath.attr("d", input.node().value);
updateVoronoi(LOW_SPEED);
}
/////////////////////
// Main algorithm //
/////////////////////
function updateVoronoi(speed) {
updateVoronoiConstraints();
redrawVoronoi(speed);
}
function updateVoronoiConstraints () {
var pathNode = drawnPath.node(),
pathLength = drawnPath.node().getTotalLength(),
length = voronoiSeedConstraints.length*sampling;
var point0, point1, midX, midY, spread, pathDist,
dx, dy, tangentAngle, spreadCoef, strokeColor;
point1 = pathNode.getPointAtLength(length);
//begin: compute path-based constraints for Voronoï seeds
for (length+=sampling; length<=pathLength; length+=sampling) {
point0 = point1;
point1 = pathNode.getPointAtLength(length);
midX = (point0.x+point1.x)/2;
midY = (point0.y+point1.y)/2;
spread = maxSpread;
//begin: limit spread of too closed path's portions
voronoiSeedConstraints.forEach (function(sc) {
if (length-sc.length > maxSpread) { //skip too adjacent path's portions
dx = midX - sc.x;
dy = midY - sc.y;
dist = Math.sqrt(Math.pow(dx,2)+Math.pow(dy,2))/2;
if (dist < sc.spread) {
sc.spread = dist;
spread = Math.min(dist, spread);
}
}
})
//end: limit spread of too closed path's portions
dx = point1.x-point0.x;
dy = point1.y-point0.y;
tangentAngle = Math.atan2(dy, dx);
spreadCoef = Math.min(2, Math.abs(normalDistrib()));
strokeColor = d3.hsl((length/2)%360, 1, 0.45);
tangentAngle += Math.PI/2;
voronoiSeedConstraints.push({
length: length,
x: midX,
y: midY,
angle: tangentAngle,
cos: Math.cos(tangentAngle),
sin: Math.sin(tangentAngle),
strokeColor: strokeColor,
spreadCoef: spreadCoef,
spread: spread
})
}
//end: compute path-based constraints for Voronoï seeds
}
function redrawVoronoi(speed) {
var spread, strokeOpacity, dx, dy;
speed = speed || LOW_SPEED;
//begin: compute Voronoï seeds, more or less close to the path
voronoiSeeds = [];
voronoiSeedConstraints.forEach (function(sc) {
spread = sc.spread*sc.spreadCoef;
strokeOpacity = strokeOpacityScale(spread);
dx = spread*sc.cos;
dy = spread*sc.sin;
voronoiSeeds.push({
x: sc.x+dx,
y: sc.y+dy,
strokeOpacity : strokeOpacity,
strokeColor: sc.strokeColor
});
//add a seed which symetric wrt. the path
voronoiSeeds.push({
x: sc.x-dx,
y: sc.y-dy,
strokeOpacity : strokeOpacity,
strokeColor: sc.strokeColor
});
})
//end: compute Voronoï seeds, more or less close to the path
//begin: draw Voronoï cells
limitedCells = voronoi(voronoiSeeds).polygons();
var drawnCells = voronoiLayer.selectAll(".cell")
.data(limitedCells);
drawnCells = drawnCells.enter()
.append("path")
.classed("cell", true)
.style("stroke-opacity", function(d) { return d? d.data.strokeOpacity : 0; })
.style("stroke", function(d) { return d? d.data.strokeColor : "white"; })
.attr("transform", function(d) { return "translate("+[50*(Math.random()-0.5), 50*(Math.random()-0.5)]+")"; })
.merge(drawnCells);
drawnCells.attr("d", cellLiner).transition()
.duration(speed)
.ease(d3.easeCircleOut)
.attr("transform", "translate(0,0)");
//end: draw Voronoï cells
}
////////////////////
// Block utility //
////////////////////
function getPredefPathes() {
var rect = "M100,100v300h760v-300z";
var circle = "M330,250a120,120 0 1,0 240,0a120,120 0 1,0 -240,0";
var plus = "M480,100v300M100,250h760";
var cross = "M130,130L830,370M130,370L830,130";
var star = starPath();
var infinity = infinityPath();
var heart = heartPath();
return [rect, circle, plus, cross, star, infinity, heart];
}
function infinityPath() {
return d3.line().curve(d3.curveBasisClosed)([
[150,0],
[810,500],
[810,0],
[150,500]
])+"z";
}
function starPath() {
var size = 350,
twoFifthAngle = _2PI/5*2;
var angle, cps;
cps = d3.range(5).map(function(i){
angle = -halfPI+i*twoFifthAngle;
return [size*Math.cos(angle)+halfWidth, size*Math.sin(angle)+halfHeight];
})
return d3.line().curve(d3.curveBasisClosed)(cps)+"z";
}
function heartPath() {
var pointNumber = 60,
angleIncrement = 2*Math.PI/pointNumber;
data = [];
var i, t, x, y;
for (i = 0; i < pointNumber; i++) {
t = i*angleIncrement;
x = 16 * Math.pow(Math.sin(t),3);
y = 13 * Math.cos(t) - 5* Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t)
data[i] = [450+13*x,200-13*y];
}
return d3.line()(data)+"z";
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment