This is an example illustrating the dynamics of repulsion, attraction, and orientation zones used in flocking algorithms. This example is based off of the equation given by Couzin.
The basic principle is that each agent has three regions in a growing radius around it: a zone of repulsion, a zone of orientation, and a zone of attraction respectively.
Dragging/Zooming behavior forked from mbostock's block: Drag & Zoom II
xxxxxxxxxx
<meta charset="utf-8">
<style> /* set the CSS */
.point {
fill: darkslategrey;
}
.line {
stroke: grey;
stroke-width: 1.5;
opacity: .5;
}
.repulsed {
fill: red;
stroke: red;
}
.attracted {
fill: blue;
stroke: blue;
}
.oriented {
fill: LawnGreen;
stroke: LawnGreen;
}
.ao {
fill: ForestGreen;
stroke: ForestGreen;
}
.moved {
stroke: black;
fill: black;
}
.R_r {
fill: lightsalmon;
}
.R_o {
fill: Chartreuse;
}
.R_a {
fill: lightskyblue;
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// Global vars
var R_r = 50;
var R_o = 100;
var R_a = 150;
// Helper vars
var POINT_SIZE = 2.5;
var LINE_LENGTH = 30;
// transition function
var t = d3.transition()
.duration(350)
.ease(d3.easeLinear);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
transform = d3.zoomIdentity;
var points = d3.range(20).map(phyllotaxis(100));
var g = svg.append("g");
function show_Rr(d) {
if (d.show) {
return R_r;
}
else return 0;
}
function show_radius(d, r) {
if (d.show) return r;
else return 0;
}
function onclick(d) {
d.show = !d.show;
console.info(d);
d3.select(".R_r.r--" + d.idx).transition(t)
.attr("r", (d) => { return show_radius(d, R_r); });
d3.select(".R_o.r--" + d.idx).transition(t)
.attr("r", (d) => { return show_radius(d, R_o); });
d3.select(".R_a.r--" + d.idx).transition(t)
.attr("r", (d) => { return show_radius(d, R_a); });
}
// draw points
g.selectAll(".point")
.data(points)
.enter().append("circle")
.attr("id", (d) => "pt-"+d.idx)
.attr("class", "point")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", POINT_SIZE)
.on("click", onclick)
.call(d3.drag()
.on("drag", dragged));
// draw zones
g.selectAll(".R_a")
.data(points)
.enter().append("circle")
.attr("class", (d) => {return "r R_a r--" + d.idx;})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", (d) => { return show_radius(d, R_a); })
.attr("opacity", 0.10);
g.selectAll(".R_o")
.data(points)
.enter().append("circle")
.attr("class", (d) => {return "r R_o r--" + d.idx;})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", (d) => { return show_radius(d, R_o); })
.attr("opacity", 0.15);
g.selectAll(".R_r")
.data(points)
.enter().append("circle")
.attr("class", (d) => {return "r R_r r--" + d.idx;})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", (d) => { return show_radius(d, R_r); })
.attr("opacity", 0.35);
// draw lines
g.selectAll(".line")
.data(points)
.enter().append("line")
.attr('id', (d) => {return 'ln-' + d.idx})
.attr('class', 'line')
.attr('x1', (d) => d.x)
.attr('y1', (d) => d.y)
.attr('x2', (d) => d.x + LINE_LENGTH * Math.cos(toRadians(d.angle)))
.attr('y2', (d) => d.y + LINE_LENGTH * Math.sin(toRadians(d.angle)))
// overlay points on top
g.selectAll(".point").raise();
var zoom = d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed);
svg.call(zoom);
// NOTE: don't need world extents right now, but will be useful for flocking...
checkCollisions();
function zoomed() {
transform = d3.event.transform;
g.attr("transform", d3.event.transform);
}
function dragged(d) {
dx = d3.event.x;
dy = d3.event.y;
// update point
d3.select(this)
.attr("cx", d.x = dx)
.attr("cy", d.y = dy);
// update collision state
checkCollisions();
// update line
d3.select("#ln-"+d.idx)
.attr('x1', dx)
.attr('y1', dy)
.attr('x2', dx + LINE_LENGTH * Math.cos(toRadians(d.angle)))
.attr('y2', dy + LINE_LENGTH * Math.sin(toRadians(d.angle)));
// update radius
d3.select(".R_r.r--" + d.idx)
.attr("cx", d.x = dx).attr("cy", d.y = dy)
.attr("r", (d) => { return show_radius(d, R_r); });
d3.select(".R_o.r--" + d.idx)
.attr("cx", d.x = dx).attr("cy", d.y = dy)
.attr("r", (d) => { return show_radius(d, R_o); })
d3.select(".R_a.r--" + d.idx)
.attr("cx", d.x = dx).attr("cy", d.y = dy)
.attr("r", (d) => { return show_radius(d, R_a); })
}
function checkCollisions() {
d3.selectAll(".point").each((pt1) => {
var pts = pointsInRadius(pt1, R_r);
var point = d3.select("#pt-"+pt1.idx);
// nr > 0
if (pts.length > 0) {
var new_angle = r_angle(pt1, pts);
pt1.angle = new_angle;
if (!point.classed('repulsed')) {
point.attr('class', 'point repulsed');
g.select("#ln-"+pt1.idx).attr('class', 'line repulsed')
.attr('x2', pt1.x + LINE_LENGTH * Math.cos(toRadians(pt1.angle)))
.attr('y2', pt1.y + LINE_LENGTH * Math.sin(toRadians(pt1.angle)));
}
g.select("#ln-"+pt1.idx)
.attr('x2', pt1.x + LINE_LENGTH * Math.cos(toRadians(pt1.angle)))
.attr('y2', pt1.y + LINE_LENGTH * Math.sin(toRadians(pt1.angle)));
}
// nr == 0
else {
var o_pts = betweenRadii(pt1, R_r, R_o),
a_pts = betweenRadii(pt1, R_o, R_a);
if (o_pts.length > 0 || a_pts.length > 0) {
if (a_pts.length > 0 && o_pts.length === 0) {
point.attr('class', 'point attracted');
pt1.angle = a_angle(pt1, a_pts);
g.select("#ln-"+pt1.idx).attr('class', 'line attracted')
.attr('x2', pt1.x + LINE_LENGTH * Math.cos(toRadians(pt1.angle)))
.attr('y2', pt1.y + LINE_LENGTH * Math.sin(toRadians(pt1.angle)));
}
else if (o_pts.length > 0 && a_pts.length === 0) {
point.attr('class', 'point oriented');
pt1.angle = o_angle(pt1, o_pts);
g.select("#ln-"+pt1.idx).attr('class', 'line oriented')
.attr('x2', pt1.x + LINE_LENGTH * Math.cos(toRadians(pt1.angle)))
.attr('y2', pt1.y + LINE_LENGTH * Math.sin(toRadians(pt1.angle)));
}
else {
point.attr('class', 'point ao');
var dir_a = a_angle(pt1, a_pts);
var dir_o = o_angle(pt1, o_pts);
pt1.angle = (dir_a + dir_o)/2;
g.select("#ln-"+pt1.idx).attr('class', 'line ao')
.attr('x2', pt1.x + LINE_LENGTH * Math.cos(toRadians(pt1.angle)))
.attr('y2', pt1.y + LINE_LENGTH * Math.sin(toRadians(pt1.angle)));
}
}
else if (point.classed('repulsed') || point.classed('attracted')) {
point.attr('class', 'point moved');
g.select("#ln-"+pt1.idx).attr('class', 'line moved');
}
}
});
}
function r_angle(d, pts) {
var sum = 0;
for (var p of pts) {
sum += Math.atan2(p.y-d.y, p.x-d.x)
}
var ave = sum / pts.length;
return toDegrees(ave) + 180;
}
function a_angle(d, pts) {
var sum = 0;
for (var p of pts) {
sum += Math.atan2(p.y-d.y, p.x-d.x)
}
var ave = sum / pts.length;
return toDegrees(ave);
}
function o_angle(d, pts) {
var sum = 0;
for (var p of pts) {
sum += p.angle;
}
var ave = sum / pts.length;
return ave;
}
function pointsInRadius(pt1, r) {
var result = [];
d3.selectAll(".point").each((d) => {
if (pt1.idx != d.idx && withinRadius(pt1, d, r)) {
result.push(d);
}
});
return result;
}
function betweenRadii(pt1, r_inner, r_outer) {
var result = [];
d3.selectAll(".point").each((d) => {
if (pt1.idx != d.idx && withinRadius(pt1, d, r_outer) && !withinRadius(pt1, d, r_inner)) {
result.push(d);
}
});
return result;
}
function withinRadius(pt1, pt2, r) {
var dx = pt1.x-pt2.x;
var dy = pt1.y-pt2.y;
return dx*dx + dy*dy <= r*r;
}
function toDegrees (angle) {
return (angle * (180 / Math.PI)) % 360;
}
function toRadians (angle) {
return angle * (Math.PI / 180);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return {
idx: i,
x: width / 2 + r * Math.cos(a),
y: height / 2 + r * Math.sin(a),
angle: toDegrees(a),
show: (() => {return i === 0})() //only for the center one
};
};
}
</script>
https://d3js.org/d3.v4.min.js