Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active July 5, 2019 15:45
Show Gist options
  • Save mbostock/444757cc9f0fde320a5f469cd36860f4 to your computer and use it in GitHub Desktop.
Save mbostock/444757cc9f0fde320a5f469cd36860f4 to your computer and use it in GitHub Desktop.
Circle Dragging III
license: gpl-3.0
redirect: https://observablehq.com/@d3/circle-dragging-ii

This is a variant of dragging Canvas circles that uses better hit-testing. Rather than choose the top circle that contains the active pointer, this custom drag subject chooses the circle closest to the active pointer. Even better, this allows increasing the search radius for the closest circle, such that the pointer doesn’t need to be inside a circle. This is equivalent to using an SVG Voronoi overlay, and is similar to Tovi Grossman’s bubble cursor.

This implementation uses a linear scan. For force-directed layouts, see simulation.find. See also quadtree.find, though note that a quadtree will only be faster than a linear scan if you can amortize the cost of building the quadtree, such as by using quadtree.remove and quadtree.add to update the circle that is being dragged.

<!DOCTYPE html>
<meta charset="utf-8">
<canvas width="960" height="500"></canvas>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var canvas = d3.select("canvas"),
context = canvas.node().getContext("2d"),
width = canvas.property("width"),
height = canvas.property("height"),
radius = 32;
var circles = d3.range(20).map(function(i) {
return {
index: i,
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
render();
canvas.call(d3.drag()
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
.on("start.render drag.render end.render", render));
function render() {
context.clearRect(0, 0, width, height);
for (var i = 0, n = circles.length, circle; i < n; ++i) {
circle = circles[i];
context.beginPath();
context.moveTo(circle.x + radius, circle.y);
context.arc(circle.x, circle.y, radius, 0, 2 * Math.PI);
context.fillStyle = color(circle.index);
context.fill();
if (circle.active) {
context.lineWidth = 2;
context.stroke();
}
}
}
function dragsubject() {
var i = 0,
n = circles.length,
dx,
dy,
d2,
s2 = radius * radius * 4, // Double the radius.
circle,
subject;
for (i = 0; i < n; ++i) {
circle = circles[i];
dx = d3.event.x - circle.x;
dy = d3.event.y - circle.y;
d2 = dx * dx + dy * dy;
if (d2 < s2) subject = circle, s2 = d2;
}
return subject;
}
function dragstarted() {
circles.splice(circles.indexOf(d3.event.subject), 1);
circles.push(d3.event.subject);
d3.event.subject.active = true;
}
function dragged() {
d3.event.subject.x = d3.event.x;
d3.event.subject.y = d3.event.y;
}
function dragended() {
d3.event.subject.active = false;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment