Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active March 11, 2017 01:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enjalot/e44aa613c3e621988f55dc7fb15d3eff to your computer and use it in GitHub Desktop.
Save enjalot/e44aa613c3e621988f55dc7fb15d3eff to your computer and use it in GitHub Desktop.
Poisson-Disc Paint
license: gpl-3.0
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body: {
position:fixed;
top:0; bottom: 0;
left: 0; right: 0;
}
svg {
width: 100%;
height: 100%;
position: absolute;
top:0;
left: 0;
pointer-events: all;
}
canvas {
width: 100%;
height: 100%;
position:absolute;
top:0;
left: 0;
pointer-events: none;
}
circle.data {
stroke: #222;
stroke-opacity: 0.4;
}
g#mouse circle {
stroke: #111;
fill: none;
}
</style>
<body onload="load()">
<script src="//d3js.org/d3.v4.min.js"></script>
<svg>
<defs>
<filter id="dropshadow" x="0" y="0" width="200%" height="200%">
<feOffset result="offOut" in="SourceAlpha" dx="1.25" dy="1.25" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1.25" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
</defs>
</svg>
<canvas></canvas>
<script>
function load() {
var bbox = d3.select("body").node().getBoundingClientRect();
var width = bbox.width - 10 || 960,
height = bbox.height -10 || 500;
if(width < 960) width = 960;
if(height < 500) height = 500;
var grid;
var n = 10000;
var r = 8;
var paintFactor = 0.02;
var sample = poissonDiscSampler(width, height, r);
var mouseR = 50;
var mousePos = {x: width/2, y: height/2}
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var circleg = svg.append("g")
.attr("transform", "translate(13,13)")
// .attr("filter", "url(#dropshadow)")
var mouseg = svg.append("g")
.attr("id", "mouse")
.style("pointer-events", "none")
.append("circle")
var colorScale = d3.scaleSequential(d3.interpolateMagma)
colorScale.domain(colorScale.domain().reverse())
var voronoi = d3.voronoi();
var samples = []
var s;
for(var i = 0; i < n; i++) {
s = sample(r);
if(!s) continue;
samples.push({
id: i,
x: s[0],
y: s[1],
v: 0
})
}
var mousedown = false;
const quadtree = d3.quadtree()
.x(d => d.x)
.y(d => d.y)
.addAll(samples);
// console.log(quadtree.data())
renderCircles();
renderMouse();
function renderCircles() {
var circles = circleg.selectAll("circle.data")
// .data(samples)
.data(samples, function(d) { return d.id })
// circles.exit().remove();
var circlesE = circles.enter().append("circle").classed("data", true)
.attr("cx", function(d) { return d.x })
.attr("cy", function(d) { return d.y })
.attr("r", function(d) { return 3 })
circles = circlesE.merge(circles)
circles.attr("fill", function(d) {
return colorScale(d.v)
})
}
function renderMouse() {
mouseg.attr("cx", mousePos.x)
.attr("cy", mousePos.y)
.attr("r", mouseR)
}
svg.on("mousemove", function() {
var mp = d3.mouse(svg.node())
mousePos.x = mp[0]
mousePos.y = mp[1]
renderMouse();
})
function exit() {
console.log("exit")
mousedown = false;
}
var lastK = 1;
var zoom = d3.zoom()
.on("zoom", function() {
var pos = d3.mouse(svg.node());
if(lastK !== d3.event.transform.k) {
mousedown = false;
} else {
var selected = fixedRadiusSearch(mouseR, pos);
selected.forEach(function(d) {
d.data.v += paintFactor
})
// var ids = selected.map(function(d) { return d.data.id})
// renderCircles();
svg.selectAll("circle.data")
.filter(function(d) { return d.selected })
.attr("fill", function(d) {
return colorScale(d.v)
})
}
mouseR = 50 * d3.event.transform.k;
lastK = d3.event.transform.k;
mousePos.x = pos[0]
mousePos.y = pos[1]
renderMouse();
})
.on("start", function() {
mousedown = true;
})
.on("end", function() {
mousedown = false;
})
svg.call(zoom)
var body = d3.select("body")
.on("click", function(){
console.log(JSON.stringify(samples))
})
// d3.timer(function(elapsed) {
// if(mousedown) {
// }
// });
// Based on https://www.jasondavies.com/poisson-disc/
function poissonDiscSampler(width, height, radius) {
var k = 30; // maximum number of samples before rejection
var radius2 = radius * radius;
var R = 3 * radius2
var cellSize = radius * Math.SQRT1_2
var gridWidth = Math.ceil(width / cellSize)
var gridHeight = Math.ceil(height / cellSize)
console.log("grid", gridWidth, gridHeight)
grid = new Array(gridWidth * gridHeight)
var queue = []
var queueSize = 0
var sampleSize = 0
return function(newRadius) {
radius2 = newRadius * newRadius;
R = 3 * radius2
cellSize = radius * Math.SQRT1_2;
if (!sampleSize) return sample(Math.random()*width,Math.random()*height);
// Pick a random existing sample and remove it from the queue.
while (queueSize) {
var i = Math.random() * queueSize | 0,
s = queue[i];
// Make a new candidate between [radius, 2 * radius] from the existing sample.
for (var j = 0; j < k; ++j) {
var a = 2 * Math.PI * Math.random(),
rad = Math.sqrt(Math.random() * R + radius2),
x = s[0] + rad * Math.cos(a) * Math.random(),
y = s[1] + rad * Math.sin(a) * Math.random();
// Reject candidates that are outside the allowed extent,
// or closer than 2 * radius to any existing sample.
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
// points.push([x,y])
return sample(x, y);
}
}
queue[i] = queue[--queueSize];
queue.length = queueSize;
}
};
function far(x, y) {
var i = x / cellSize | 0,
j = y / cellSize | 0,
i0 = Math.max(i - 2, 0),
j0 = Math.max(j - 2, 0),
i1 = Math.min(i + 3, gridWidth),
j1 = Math.min(j + 3, gridHeight);
for (j = j0; j < j1; ++j) {
var o = j * gridWidth;
for (i = i0; i < i1; ++i) {
if (s = grid[o + i]) {
var s,
dx = s[0] - x,
dy = s[1] - y;
if (dx * dx + dy * dy < radius2) return false;
}
}
}
return true;
}
function sample(x, y) {
var s = [x, y];
queue.push(s);
grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = s;
++sampleSize;
++queueSize;
return s;
}
}
// from http://blockbuilder.org/armollica/2d3fa2c5d61c794bc3fab82166906813
// Find all points in `quadtree` that are within radius `r` of `point`
function fixedRadiusSearch(r, point) {
var cx = point[0], cy = point[1];
var selected = [];
quadtree.visit(function(node, x0, y0, x1, y1) {
if (!node.length) {
node.scanned = true;
node.data.selected = pointInCircle([node.data.x, node.data.y], [cx, cy, r]);
if(node.data.selected) selected.push(node)
// console.log("node", node, distance([node.data.x, node.data.y], point))
}
return rectangleOutsideCircle([[x0, y0],[x1, y1]], [cx, cy, r]);
});
return selected;
}
function rectangleOutsideCircle(rect, circle) {
var x0 = rect[0][0], y0 = rect[0][1],
x1 = rect[1][0], y1 = rect[1][1];
// Rectangle is inside circle if...
// ...circle center is inside rectangle
var isInside = pointInRectangle(circle, [[x0, y0], [x1, y1]]) ? true :
// ...any of the rectangle's corners is inside the circle
pointInCircle([x0, y0], circle) ? true :
pointInCircle([x0, y1], circle) ? true :
pointInCircle([x1, y1], circle) ? true :
pointInCircle([x1, y0], circle) ? true :
// ...any of the rectangle's sides intersects the circle
lineIntersectsCircle([[x0, y0], [x1, y0]], circle) ? true :
lineIntersectsCircle([[x1, y0], [x1, y1]], circle) ? true :
lineIntersectsCircle([[x1, y1], [x0, y1]], circle) ? true :
lineIntersectsCircle([[x0, y1], [x0, y0]], circle) ? true :
false; // ...otherwise it's outside
return !isInside;
}
function pointInRectangle(point, rect) {
var x = point[0], y = point[1],
x0 = rect[0][0], y0 = rect[0][1],
x1 = rect[1][0], y1 = rect[1][1];
return (x0 <= x) && (x <= x1) && (y0 <= y) && (y <= y1);
}
function pointInCircle(point, circle) {
var x = point[0], y = point[1],
cx = circle[0], cy = circle[1], r = circle[2];
return distance([cx, cy], [x, y]) <= r;
}
// Euclidean distance between two points
function distance(p0, p1) {
var x0 = p0[0], y0 = p0[1],
x1 = p1[0], y1 = p1[1];
return Math.sqrt(Math.pow(x1-x0, 2) + Math.pow(y1-y0, 2));
}
function clamp(d, min, max) {
return d < min ? min : d > max ? max : d;
}
function lineIntersectsCircle(line, circle) {
// Taken mostly from http://stackoverflow.com/a/1088058
var x0 = line[0][0], y0 = line[0][1],
x1 = line[1][0], y1 = line[1][1],
cx = circle[0], cy = circle[1], r = circle[2];
var lineDistance = distance([x0, y0], [x1, y1]);
// [dx, dy] = direction vector of line
var dx = (x1-x0)/lineDistance,
dy = (y1-y0)/lineDistance;
// Parametric equation for a line:
// x = dx*t + x0
// y = dy*t + y0
// where 0 <= t <= 1
// Compute parameter t for closest point to circle center
// (clamp t to be between line segment end points)
var t = clamp(dx*(cx-x0) + dy*(cy-y0), 0, lineDistance);
// Compute coordinates of point on line closest to circle center
var px = t*dx + x0,
py = t*dy + y0;
// Get distance of this point from center
var projectionDistance = distance([cx, cy], [px, py]);
return projectionDistance <= r;
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment