Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active May 10, 2017 09:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veltman/985bdca56d122f9525e0abab2ca29c34 to your computer and use it in GitHub Desktop.
Save veltman/985bdca56d122f9525e0abab2ca29c34 to your computer and use it in GitHub Desktop.
Scribble fill

Trying out the scribble fill method from this old Apple patent by John B. Turner with a bit of spline embellishment at the corners. Seems to work pretty well for more convex shapes, but results can get weird otherwise (the Olympic Peninsula seems especially uncooperative).

Some possible tweaks:

  • Drop any orphan scribbles with only two or three vertices
  • Add some random control points to make the scribble segments a little swoopier
  • Add a hand-drawn effect with SVG turbulence/displacement filters
  • Select intersections based on a more simplified outline of the shape
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
path {
fill: #ccc;
stroke: #444;
stroke-width: 1px;
}
line {
stroke-width: 1px;
stroke: black;
}
circle {
stroke: black;
stroke-width: 1px;
fill: rgba(255, 0, 255, 0.25);
}
.scribble {
fill: none;
}
</style>
</head>
<body>
<svg width="960" height="500"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script>
let svg = d3.select("svg"),
border = svg.append("path"),
line = d3.line(),
closedLine = d3.line().curve(d3.curveLinearClosed);
d3.json("us.topo.json", function(err, topo){
const states = topojson.feature(topo, topo.objects.states).features.map(d => d.geometry.coordinates[0]);
scribbleStates(d3.shuffle(states));
});
function scribbleStates(states) {
let points = states.shift(),
scribbleAngle = Math.PI * (1 / 16 + Math.random() * 3 / 8) * (Math.random() < 0.5 ? -1 : 1),
midpoint = getMidpoint(points),
rotator = rotateAround(midpoint, scribbleAngle),
rotated = points.map(rotator),
gridlines = getGridlines(getBounds(rotated)),
intersections = getIntersections(gridlines, rotated);
svg.selectAll(".scribble")
.remove();
border.style("opacity", 1)
.datum(points)
.attr("d", closedLine);
rotateShape()
.then(drawGridlines)
.then(drawScribbles)
.then(clearGuides)
.then(unrotate)
.then(wobble)
.then(function(){
scribbleStates([...states, points]);
});
function rotateShape() {
return new Promise(function(resolve) {
border.datum(rotated)
.transition()
.delay(500)
.duration(1000)
.attr("d", closedLine)
.on("end", resolve);
});
}
function drawGridlines() {
svg.selectAll("line")
.data(gridlines)
.enter()
.append("line")
.attr("x1", d => d[0][0])
.attr("x2", d => d[1][0])
.attr("y1", d => d[0][1])
.attr("y2", d => d[1][1]);
svg.selectAll("circle")
.data(d3.merge(intersections))
.enter()
.append("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 5);
}
function drawScribbles() {
let scribbles = getScribbles(intersections, rotated);
svg.selectAll(".scribble")
.data(scribbles)
.enter()
.append("path")
.attr("class", "scribble")
.attr("d", line);
return new Promise(resolve => d3.timeout(resolve, 500));
}
function clearGuides() {
return new Promise(function(resolve){
svg.selectAll("line, circle")
.transition()
.duration(500)
.style("opacity", 0)
.remove()
.on("end", resolve);
});
}
function unrotate() {
let unrotator = rotateAround(midpoint, -scribbleAngle),
paths = svg.selectAll("path");
paths.each(function(d){
d3.select(this).datum(d.map(unrotator));
});
return new Promise(function(resolve){
paths.transition()
.duration(1000)
.attr("d", (d, i) => i ? line(d) : closedLine(d))
.transition()
.filter((d, i) => !i)
.style("opacity", 0.1)
.on("end", resolve);
});
}
function wobble() {
let scribbles = svg.selectAll(".scribble")
.attr("d", d3.line().curve(d3.curveCardinal.tension(0.25)))
.style("stroke", d3.interpolateRainbow(Math.random()))
.style("stroke-width", 3);
return new Promise(function(resolve){
scribbles.transition()
.duration(1000)
.on("end", resolve);
});
}
}
function getScribbles(rows, ring) {
let top = 0,
bottom = 1,
i = j = 0,
p1 = rows[top][i],
p2 = rows[bottom][j],
scribbles = [];
checkSegment();
return scribbles;
function checkSegment() {
if (isInFront() && isContained()) {
addSegment();
} else {
nextBottom();
}
}
function addSegment() {
let found = scribbles.find(scribble => distanceBetween(scribble[scribble.length - 1], p1) < 1e-6);
if (found) {
found.push(p2);
} else {
scribbles.push([p1, p2]);
}
scribbles.sort((a, b) => scribbleLength(b) - scribbleLength(a));
nextTop();
}
function isInFront() {
return (top % 2 ? -1 : 1) * (p2[0] - p1[0]) > 0
}
function isContained() {
if (p1[2] === p2[2] || !d3.polygonContains(ring, pointBetween(p1, p2, 0.5))) {
return false;
}
return ring.every(function(a, segmentIndex){
const b = ring[segmentIndex + 1] || ring[0];
return segmentIndex === p1[2] || segmentIndex === p2[2] || !segmentsIntersect([a, b], [p1, p2]);
});
}
function nextRow() {
if (bottom + 1 < rows.length) {
p1 = rows[++top][i = 0];
p2 = rows[++bottom][j = 0];
checkSegment();
}
}
function nextTop() {
if (i + 1 >= rows[top].length) {
nextRow();
} else {
p1 = rows[top][++i];
checkSegment();
}
}
function nextBottom() {
if (j + 1 >= rows[bottom].length) {
nextTop();
} else {
p2 = rows[bottom][++j];
checkSegment();
}
}
function scribbleLength(points) {
return points.reduce(function(length, point, i){
return i ? length + distanceBetween(point, points[i - 1]) : 0;
}, 0);
}
}
function getGridlines(bounds) {
let lineFrequency = 7 + Math.random() * 3,
lineVariation = lineFrequency * 3 / 8;
i = bounds[0][1],
gridY = [],
space = lineFrequency - lineVariation / 2 + Math.random() * lineVariation;
while (i + space < bounds[1][1]) {
i += space;
gridY.push(i);
space = lineFrequency - lineVariation + Math.random() * lineVariation * 2;
}
return gridY.map(y => [[bounds[0][0] - 5, y], [bounds[1][0] + 5, y]]);
}
function getIntersections(gridlines, ring){
return gridlines.map(function(gridline, i){
const y = gridline[0][1],
row = [],
direction = i % 2 ? - 1 : 1;
ring.forEach(function(p1, j){
const p2 = ring[j + 1] || ring[0],
m = (p2[1] - p1[1]) / (p2[0] - p1[0]),
b = p2[1] - m * p2[0],
x = (y - b) / m;
if ((p1[1] <= y && p2[1] > y) || (p1[1] >= y && p2[1] < y)) {
row.push([x, y, j]);
}
});
row.sort((a, b) => direction * (a[0] - b[0]));
return row;
});
}
function rotateAround(center, angle) {
const cos = Math.cos(angle),
sin = Math.sin(angle);
return function(p) {
return [
center[0] + (p[0] - center[0]) * cos - (p[1] - center[1]) * sin,
center[1] + (p[0] - center[0]) * sin + (p[1] - center[1]) * cos
];
};
}
function getBounds(ring) {
let x0 = y0 = Infinity,
x1 = y1 = -Infinity;
ring.forEach(function(point){
if (point[0] < x0) x0 = point[0];
if (point[0] > x1) x1 = point[0];
if (point[1] < y0) y0 = point[1];
if (point[1] > y1) y1 = point[1];
});
return [
[x0, y0],
[x1, y1]
];
}
function getMidpoint(ring) {
const bounds = getBounds(ring);
return [
(bounds[1][0] + bounds[0][0]) / 2,
(bounds[1][1] + bounds[0][1]) / 2
];
}
function segmentsIntersect(a, b) {
if (orientation(a[0], a[1], b[0]) === orientation(a[0], a[1], b[1])) {
return false;
}
return orientation(b[0], b[1], a[0]) !== orientation(b[0], b[1], a[1]);
}
function orientation(p, q, r) {
const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
return val > 0 ? 1 : val < 0 ? -1 : 0;
}
function pointBetween(a, b, pct) {
const point = [
a[0] + (b[0] - a[0]) * pct,
a[1] + (b[1] - a[1]) * pct
];
return point;
}
function distanceBetween(a, b) {
const dx = a[0] - b[0],
dy = a[1] - b[1];
return Math.sqrt(dx * dx + dy * dy);
}
</script>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment