|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> |
|
<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: DarkTurquoise; |
|
stroke: DarkTurquoise; |
|
} |
|
|
|
.moved { |
|
stroke: black; |
|
fill: black; |
|
} |
|
|
|
.R_r { |
|
fill: lightsalmon; |
|
} |
|
|
|
.R_o { |
|
fill: Chartreuse; |
|
} |
|
|
|
.R_a { |
|
fill: lightskyblue; |
|
} |
|
|
|
.num { |
|
float: left; |
|
} |
|
|
|
.counter { |
|
width: 50px; |
|
float: right; |
|
margin-right: 40px; |
|
} |
|
</style> |
|
<div class="row"> |
|
<div class="col-sm-10"> |
|
<svg id="svg" width="800" height="500"></svg> |
|
</div> |
|
<div class="col-sm-2"> |
|
<div class="row"> |
|
<b>Radii:</b> |
|
</div> |
|
<div class="row"> |
|
<div class="num">Repulsion: </div> |
|
<input class="counter" type="number" onchange="updateR_r(this.value)" value=50></input> |
|
</div> |
|
<div class="row"> |
|
<div class="num">Orientation: </div> |
|
<input class="counter" type="number" onchange="updateR_o(this.value)" value=100></input> |
|
</div> |
|
<div class="row"> |
|
<div class="num">Attraction: </div> |
|
<input class="counter" type="number" onchange="updateR_a(this.value)" value=150></input> |
|
</div> |
|
<hr> |
|
|
|
<div class="row"> |
|
Controls: |
|
<br> |
|
<button onclick="play()"><span class="glyphicon glyphicon-play"></button> |
|
<button onclick="pause()"><span class="glyphicon glyphicon-pause"></button> |
|
<button onclick="next()"><span class="glyphicon glyphicon-step-forward"></button> |
|
</div> |
|
<hr> |
|
<div class="row"> |
|
<button class="btn btn-danger" onclick="reset()">Reset</button> |
|
</div> |
|
|
|
</div> |
|
</div> |
|
|
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script> |
|
// === VARIABLES === |
|
// Global vars |
|
var R_r = 50; |
|
var R_o = 100; |
|
var R_a = 150; |
|
var NUM_AGENTS = 30; |
|
|
|
// Helper vars |
|
var POINT_SIZE = 2.5; |
|
var LINE_LENGTH = 30; |
|
var TRAVEL_LENGTH = 15; |
|
var DT = 350; |
|
var INIT_SPARSITY = 100; |
|
var timer; |
|
|
|
|
|
// === INIT === |
|
var svg = d3.select("svg"), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"), |
|
transform = d3.zoomIdentity; |
|
|
|
var points = d3.range(NUM_AGENTS).map(phyllotaxis(INIT_SPARSITY)); |
|
|
|
var g = svg.append("g"); |
|
|
|
|
|
|
|
|
|
// === D3 FUNCTIONS === |
|
var t = d3.transition() |
|
.duration(DT-50) |
|
.ease(d3.easeLinear); |
|
var zoom = d3.zoom() |
|
.scaleExtent([1 / 2.5, 8]) |
|
.on("zoom", zoomed); |
|
function zoomed() { |
|
transform = d3.event.transform; |
|
g.attr("transform", d3.event.transform); |
|
} |
|
|
|
|
|
// === CONTROLS === |
|
var play = function() { |
|
timer = d3.interval(() => { |
|
next(); |
|
}, DT); |
|
} |
|
var pause = function() { |
|
timer.stop(); |
|
} |
|
function updateR_r(value) { |
|
R_r = value; |
|
d3.selectAll(".R_r").transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_r); }); |
|
} |
|
function updateR_o(value) { |
|
R_o = value; |
|
d3.selectAll(".R_o").transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_o); }); |
|
} |
|
function updateR_a(value) { |
|
R_a = value; |
|
d3.selectAll(".R_a").transition(t) |
|
.attr("r", (d) => { return show_radius(d, R_a); }); |
|
} |
|
function reset() { |
|
svg.selectAll(".point").remove(); |
|
svg.selectAll(".line").remove(); |
|
svg.selectAll(".r").remove(); |
|
points = d3.range(NUM_AGENTS).map(phyllotaxis(INIT_SPARSITY)); |
|
start(); |
|
} |
|
|
|
|
|
// === HELPER FUNCTIONS === |
|
function show_radius(d, r) { |
|
if (d.show) return r; |
|
else return 0; |
|
} |
|
|
|
|
|
// === EVENT HANDLERS === |
|
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); }); |
|
|
|
if (d.show) { |
|
g.append("line") |
|
.datum(d) |
|
.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))) |
|
} |
|
else { |
|
g.select('#ln-'+d.idx).remove(); |
|
} |
|
} |
|
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); }) |
|
} |
|
|
|
|
|
// === START === |
|
function start() { |
|
// SETUP 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); |
|
// overlay points on top |
|
g.selectAll(".point").raise(); |
|
|
|
svg.call(zoom); |
|
checkCollisions(); |
|
} |
|
// invoke immediately |
|
start(); |
|
|
|
|
|
|
|
// === UPDATE FUNCTIONS === |
|
function next() { |
|
// move points |
|
g.selectAll(".point").transition(t) |
|
.attr("cx",(d) => { d.x = d.next.x; return d.x;}) |
|
.attr("cy",(d) => { d.y = d.next.y; return d.y;}); |
|
// move radii |
|
g.selectAll(".r").transition(t) |
|
.attr("cx",(d) => { return d.x;}) |
|
.attr("cy",(d) => { return d.y;}); |
|
// get next positions |
|
updateZones(); |
|
// update styles |
|
g.selectAll(".point").transition(t) |
|
.attr("class", (d) => {return "point " + d.next.style;}); |
|
// update lines |
|
g.selectAll(".line").transition(t) |
|
.attr('x1', (d) => d.x) |
|
.attr('y1', (d) => d.y) |
|
.attr('x2', (d) => d.next.x) |
|
.attr('y2', (d) => d.next.y) |
|
.attr('class', (d) => {return "line " + d.next.style;}); |
|
} |
|
function updateZones() { |
|
d3.selectAll(".point").each((pt1) => { |
|
var r_pts = pointsInRadius(pt1, R_r); |
|
var ptclass = ""; |
|
// nr > 0 |
|
if (r_pts.length > 0) { |
|
var new_angle = r_angle(pt1, r_pts); |
|
pt1.angle = new_angle; |
|
ptclass = 'repulsed'; |
|
} |
|
// 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) { |
|
ptclass = "attracted"; |
|
pt1.angle = a_angle(pt1, a_pts); |
|
} |
|
else if (o_pts.length > 0 && a_pts.length === 0) { |
|
ptclass = "oriented" |
|
pt1.angle = o_angle(pt1, o_pts); |
|
} |
|
else { |
|
ptclass = "ao" |
|
var dir_a = a_angle(pt1, a_pts); |
|
var dir_o = o_angle(pt1, o_pts); |
|
pt1.angle = (dir_a + dir_o)/2; |
|
} |
|
} |
|
} |
|
var dx = pt1.x + TRAVEL_LENGTH * Math.cos(toRadians(pt1.angle)); |
|
var dy = pt1.y + TRAVEL_LENGTH * Math.sin(toRadians(pt1.angle)); |
|
pt1.next = {"x":dx, "y":dy, "style": ptclass}; |
|
}); |
|
} |
|
function checkCollisions() { |
|
d3.selectAll(".point").each((pt1) => { |
|
var pts = pointsInRadius(pt1, R_r); |
|
var point = d3.select("#pt-"+pt1.idx); |
|
var dx, dy; |
|
// 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', dx) |
|
.attr('y2', dy); |
|
} |
|
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'); |
|
} |
|
} |
|
dx = pt1.x + LINE_LENGTH * Math.cos(toRadians(pt1.angle)); |
|
dy = pt1.y + LINE_LENGTH * Math.sin(toRadians(pt1.angle)); |
|
pt1.next = {"x":dx, "y":dy} |
|
}); |
|
} |
|
|
|
|
|
// === MATH FUNCTIONS === |
|
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)+180, |
|
show: (() => {return i === 0})() //only for the center one |
|
}; |
|
}; |
|
} |
|
|
|
</script> |