Skip to content

Instantly share code, notes, and snippets.

@MrHen
Last active March 27, 2016 00:17
Show Gist options
  • Save MrHen/fd24a8ef0ec4e15da8bd to your computer and use it in GitHub Desktop.
Save MrHen/fd24a8ef0ec4e15da8bd to your computer and use it in GitHub Desktop.
Doodle
height: 550
scrolling: true
license: MIT

D3 recreation of a doodle seen on reddit/imgur

<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="doodle-container"></div>
<button class="control" onclick="resetPoints()">Redraw</button>
<table id="control-container">
<tr>
<td>% Clockwise</td>
<td><input type="range" min="0" max="100" step="1" value="50" id="config-ccw"></td>
</tr>
<tr>
<td>Min delta</td>
<td><input type="range" min="0" max="100" step="1" value="50" id="config-min"></td>
</tr>
<tr>
<td>Max iterations</td>
<td><input type="range" min="0" max="100" step="1" value="20" id="config-max"></td>
</tr>
<tr>
<td>Color polygons</td>
<td><input type="checkbox" id="config-color-polygons"></td>
</tr>
<tr>
<td>Show polygons</td>
<td><input type="checkbox" id="config-show-polygons"></td>
</tr>
</table>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js"></script>
<script src="script.js"></script>
</body>
</html>
var config = {
height: 500,
width: 500,
density: 12000,
min: 9,
max: 240,
colorShapes: false,
ccw: 0.5,
showShapes: false
}
var xScale = d3.scale.linear().domain([-0.5, 0.5]).range([0, config.height]);
var yScale = d3.scale.linear().domain([-0.5, 0.5]).range([0, config.width]);
var offsetScale = d3.scale.linear().domain([0, 1]).range([6, 14]);
var line = d3.svg.line();
var doodle = d3.select("#doodle-container")
.append("svg")
.attr("class", "doodle")
.attr("height", config.height)
.attr("width", config.width);
var shapeSvg = doodle.append("g");
var voronoi = d3.geom.voronoi()
.clipExtent([
[0, 0],
[config.width, config.height]
]);
var vertices = buildVertices(config);
var extras = buildExtras(vertices, config);
drawPolygons(vertices, config);
drawExtras(extras, config);
var ccwScale = d3.scale.linear().domain([0, 100]).range([1, 0]);
d3.select("#config-ccw")
.attr("value", ccwScale.invert(config.ccw))
.on("input", function() {
config.ccw = ccwScale(+this.value);
extras = buildExtras(vertices, config);
drawExtras(extras, config);
});
var minScale = d3.scale.linear().domain([0, 100]).range([1, 50]);
d3.select("#config-min")
.attr("value", minScale.invert(config.min))
.on("input", function() {
config.min = minScale(+this.value);
extras = buildExtras(vertices, config);
drawExtras(extras, config);
});
var maxScale = d3.scale.linear().domain([0, 100]).range([100, 600]);
d3.select("#config-max")
.attr("value", maxScale.invert(config.max))
.on("input", function() {
config.max = maxScale(+this.value);
extras = buildExtras(vertices, config);
drawExtras(extras, config);
});
d3.select("#config-color-polygons")
.on("click", function() {
config.colorShapes = this.checked;
drawPolygons(vertices, config);
});
d3.select("#config-show-polygons")
.on("click", function() {
config.showShapes = this.checked;
drawPolygons(vertices, config);
});
function resetPoints() {
vertices = buildVertices(config);
extras = buildExtras(vertices, config);
drawPolygons(vertices, config);
drawExtras(extras, config);
}
function buildVertices(config) {
var vertices = d3.range(config.height * config.width / config.density).map(function(d) {
return [Math.random() - 0.5, Math.random() - 0.5];
});
var scaled = vertices.map(translatedPoint);
var polygons = voronoi(scaled);
return polygons;
}
function buildExtras(polygons, config) {
var extras = [];
for (var p = 0; p < polygons.length; p++) {
extras = extras.concat(fillShape(polygons[p], config.offset));
}
return extras;
}
function drawPolygons(polygons, config) {
var shapes = shapeSvg.selectAll(".shape")
.data(polygons);
shapes.exit().remove();
shapes.enter()
.append("path")
.attr("class", "shape")
.attr("fill", "transparent");
shapes.attr("stroke", config.showShapes ? "red" : "black")
.classed({
"shape": true,
"shape-a": function(d, i) {
return config.colorShapes && i % 5 === 0;
},
"shape-b": function(d, i) {
return config.colorShapes && i % 5 === 1;
},
"shape-c": function(d, i) {
return config.colorShapes && i % 5 === 2;
},
"shape-d": function(d, i) {
return config.colorShapes && i % 5 === 3;
},
"shape-e": function(d, i) {
return config.colorShapes && i % 5 === 4;
},
})
.attr("d", polygon);
}
function drawExtras(extras, config) {
var extrasSvg = doodle.selectAll(".extra")
.data(extras);
extrasSvg.exit().remove();
extrasSvg.enter()
.append("path")
.attr("class", "extra");
extrasSvg.attr("d", line);
}
function fillShape(shape) {
var lines = [];
var next = shape.slice();
var ccw = Math.random() < config.ccw;
for (var i = 0; i < config.max;) {
if (next.length < 3) {
break;
}
var a = i % next.length;
var b = ccw ? (i + 1) : (i - 1 + next.length);
b %= next.length;
var c = ccw ? (i + 2) : (i - 2 + next.length);
c %= next.length;
var extra = bump(next[b], next[c]);
if (!extra) {
next.splice(b, 1);
continue;
}
next[b] = extra;
lines.push([next[b], next[a]]);
i++;
}
return lines;
}
function bump(near, far) {
var oldDist = distance(near, far);
if (oldDist < config.min) {
return null;
}
var delta = [0, 0];
var offset = offsetScale(Math.random());
if (near[1] === far[1]) {
delta[0] = offset;
} else if (near[0] === far[0]) {
delta[1] = offset;
} else {
var m = slope(near, far);
var r = Math.sqrt((1 + Math.pow(m, 2)));
delta = [offset / r, offset * m / r];
}
var endX = near[0] + delta[0];
var endY = near[1] + delta[1];
// There is probably a better way to do this check...
var newDist = distance([endX, endY], far);
if (oldDist - newDist < 0) {
delta[0] *= -1;
delta[1] *= -1;
}
var endX = near[0] + delta[0];
var endY = near[1] + delta[1];
return [endX, endY];
}
function slope(a, b) {
return (b[1] - a[1]) / (b[0] - a[0]);
}
function distance(a, b) {
return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2));
}
function translatedPoint(d) {
return [xScale(d[0]), yScale(d[1])];
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: grey;
}
#control-container {
margin: 1em auto;
}
#control-container tr td:first-child{
text-align: right;
}
#doodle-container {
margin: 1em;
}
svg {
background: white;
border: solid 2px black;
display: block;
margin: 0 auto;
}
button.control {
display: block;
margin: 1em auto;
border: solid 1px grey;
border-radius: 5px;
background-color: white;
font: inherit;
font-size: 80%;
padding: 0.75em;
}
.shape.shape-a {
fill: rgb(102, 194, 165)
}
.shape.shape-b {
fill: rgb(252, 141, 98)
}
.shape.shape-c {
fill: rgb(141, 160, 203)
}
.shape.shape-d {
fill: rgb(231, 138, 195)
}
.shape.shape-e {
fill: rgb(166, 216, 84)
}
path.extra {
stroke: black;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment