D3 recreation of a doodle seen on reddit/imgur
Last active
March 27, 2016 00:17
-
-
Save MrHen/fd24a8ef0ec4e15da8bd to your computer and use it in GitHub Desktop.
Doodle
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
height: 550 | |
scrolling: true | |
license: MIT |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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