Created by Christopher Manning
This is an experiment using a random walk to draw shapes. Adjusting theta results in a very organic or procedural drawing. The random walk stays in the geometry by using a point in polygon test.
xxxxxxxxxx
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
width = Math.max(window.innerWidth, 960)
height = Math.max(window.innerHeight, 500)
config = {"theta": 1, "density" : 10, "scale": 25, "careful": true, "smooth": true};
gui = new dat.GUI({width: 130});
var examples = gui.addFolder('Examples');
examples.open()
config.Sketch = function(){
config["theta"] = 1
config["density"] = 10
config["scale"] = 25
config["careful"] = true
config["smooth"] = true
draw()
}
examples.add(config, "Sketch")
config.Geom = function(){
config["theta"] = 90
config["density"] = 10
config["scale"] = 25
config["careful"] = true
config["smooth"] = true
draw()
}
examples.add(config, "Geom")
config.Squares = function(){
config["theta"] = 90
config["density"] = 30
config["scale"] = 5
config["careful"] = true
config["smooth"] = false
draw()
}
examples.add(config, "Squares")
config.Triangles = function(){
config["theta"] = 60
config["density"] = 1
config["scale"] = 25
config["careful"] = false
config["smooth"] = false
draw()
}
examples.add(config, "Triangles")
config.Random = function(){
gui.__folders.Settings.__controllers.forEach(function(c){
if(typeof(c.__select) != 'undefined') {
c.setValue(c.__select[Math.floor(Math.random()*(c.__select.length-1))].value)
} else {
if(typeof c.initialValue == "boolean") {
c.setValue(Math.round(Math.random()) == true)
} else if(typeof c.initialValue == "number") {
c.setValue(Math.floor(Math.random() * c.__max) + c.__min)
}
}
})
draw()
}
examples.add(config, "Random")
var settings = gui.addFolder('Settings');
thetaChanger = settings.add(config, "theta", 1, 180).step(1).listen()
thetaChanger.onChange(function(value) {
draw()
});
scaleChanger = settings.add(config, "scale", 1, 25).step(1).listen()
scaleChanger.onChange(function(value) {
draw()
});
densityChanger = settings.add(config, "density", 1, 50).listen()
densityChanger.onChange(function(value) {
draw()
});
carefulChanger = settings.add(config, "careful").listen()
carefulChanger.onChange(function(value) {
draw()
});
smoothChanger = settings.add(config, "smooth").listen()
smoothChanger.onChange(function(value) {
draw()
});
config.redraw = function(){
draw()
}
settings.add(config, "redraw")
var zoom = d3.behavior.zoom()
.scale(config["theta"])
.scaleExtent([1, 180])
.on("zoom", function(d,i) {
config["theta"] = Math.floor(d3.event.scale)
draw()
});
var projection = d3.geo.albersUsa()
.scale(1000)
.translate([(width-100) / 2, (height-25) / 2]);
var path = d3.geo.path()
.projection(projection);
canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height)
.call(zoom)
context = canvas.node().getContext("2d")
color = d3.scale.category20()
line = d3.svg.line()
.interpolate(config["interpolation"])
.tension(config["tension"])
.x(function(d, i) { return d.xo })
.y(function(d, i) { return d.yo })
function pointInPolygon(point, polygon) {
for (var n = polygon.length, i = 0, j = n - 1, x = point[0], y = point[1], inside = false; i < n; j = i++) {
var xi = polygon[i][0], yi = polygon[i][1],
xj = polygon[j][0], yj = polygon[j][1];
if ((yi > y ^ yj > y) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) inside = !inside;
}
return inside;
}
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
d3.json('us.json', function(error, us) {
us_json = us
us_json.objects.states.geometries = us_json.objects.states.geometries.filter(function(d) { return d.id < 60 })
states = topojson.feature(us_json, us_json.objects.states).features,
neighbors = topojson.neighbors(us_json.objects.states.geometries);
draw()
})
function draw(){
context.clearRect(0, 0, width, height)
context.lineJoin = 'round'
for (var i0 = 0; i0 < states.length; i0++) {
d = states[i0]
context.beginPath()
context.strokeStyle = states[i0].color ? states[i0].color : states[i0].color = color(states[i0].colorIndex = d3.max(neighbors[i0], function(n) { return states[n].colorIndex; }) + 1 | 0)
for(var i1=0; i1<d.geometry.coordinates.length; i1++) {
if(d.geometry.type=="MultiPolygon") {
p = d.geometry.coordinates[i1][0]
}else{
p = d.geometry.coordinates[i1]
}
d2 = {type: "Feature", properties: {}, geometry: {type: "Polygon", coordinates: [p]}}
centroid = path.centroid(d2)
area = path.area(d2)
// skip small lands unless they're AK or HI
if(area<100 && d.id != 2 && d.id != 15) continue
last = {xo: centroid[0], yo: centroid[1], pip: true}
context.moveTo(last.xo, last.yo)
strokes = area*(config["density"]*.01)
for(var i2=0;i2<(strokes < 4 ? 4 : strokes);i2++){
xo = last.xo
yo = last.yo
if(last.pip) {
// random angle that's a multiple of theta
r = randInt(1, 360/config["theta"])
angle = r * config["theta"]
theta = angle * (Math.PI/180)
} else {
// if we're outside of the polygon, turn towards the centroid
dx = centroid[0] - xo
dy = centroid[1] - yo
theta = Math.atan2(dy, dx)
thetaDeg = theta * (180/Math.PI)
// random angle towards the centroid clamped to theta
randAngle = randInt(thetaDeg - 90, thetaDeg + 90)
angle = Math.floor(randAngle/config["theta"])*config["theta"]
theta = Math.PI * (angle/180)
}
scale = Math.sqrt(area) < config["scale"] ? Math.sqrt(area) : config["scale"]
xo += Math.cos(theta)*scale
yo += Math.sin(theta)*scale
pip = pointInPolygon(projection.invert([xo, yo]), d2.geometry.coordinates[0])
last = {xo: xo, yo: yo, pip: pip}
if(pip || i2==0 || !(pip || config["careful"])) context.lineTo(last.xo, last.yo)
if(!config["smooth"]) context.moveTo(last.xo, last.yo)
}
}
context.stroke()
}
}
</script>
https://d3js.org/d3.v3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js
https://d3js.org/topojson.v1.min.js