|
// === VARIABLES === |
|
// Agent Parameters |
|
var R_r = [d3.select("#zr-1").attr("value"), d3.select("#zr-2").attr("value")]; |
|
var R_o = [d3.select("#zo-1").attr("value"), d3.select("#zo-2").attr("value")]; |
|
var R_a = [d3.select("#za-1").attr("value"), d3.select("#za-2").attr("value")]; |
|
var K = [d3.select("#k-1").attr("value"), d3.select("#k-2").attr("value")]; |
|
var TRAVEL_LENGTH = [d3.select("#speed-1").attr("value"), |
|
d3.select("#speed-2").attr("value")]; |
|
var TURN_RATE = [d3.select("#theta-1").attr("value"), |
|
d3.select("#theta-2").attr("value")]; |
|
var METRICS = [getMetric(d3.select("#metric-1").property("checked")), |
|
getMetric(d3.select("#metric-2").property("checked"))]; |
|
|
|
// Start Conditions |
|
var NUM_AGENTS = d3.select("#agents").attr("value"); |
|
var INIT_SPARSITY = d3.select("#sparsity").attr("value"); |
|
var GROUP_DISTRIBUTION = 1; |
|
|
|
// Helper vars |
|
var POINT_SIZE_1 = 20; |
|
var POINT_SIZE_2 = 30; |
|
var DT = 100; |
|
var timer; |
|
var nodes; |
|
var simulation; |
|
|
|
|
|
// === 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 points = generatePoints(NUM_AGENTS, INIT_SPARSITY); |
|
|
|
var g = svg.append("g"); |
|
|
|
|
|
|
|
// === D3 FUNCTIONS === |
|
var t = d3.transition() |
|
.duration(DT) |
|
.ease(d3.easeLinear); |
|
var zoom = d3.zoom() |
|
.scaleExtent([1 / 4, 8]) |
|
.on("zoom", zoomed); |
|
function zoomed() { |
|
transform = d3.event.transform; |
|
g.attr("transform", d3.event.transform); |
|
} |
|
simulation = d3.forceSimulation() |
|
|
|
|
|
|
|
function start() { |
|
nodes = g.append("g") |
|
.attr("class", "nodes") |
|
.selectAll(".point") |
|
.data(points) |
|
.enter().append("g") |
|
.attr("class", "point") |
|
.attr("transform", (d) => {return getTransform(d.dir2,d.x,d.y);}) |
|
.on("click", onclick) |
|
.call(d3.drag() |
|
.on("start", dragstarted) |
|
.on("drag", dragged) |
|
.on("end", dragended)) |
|
.append("path") |
|
.attr("d", (d) => {return getPointSymbolType(d)()} ) |
|
|
|
simulation |
|
.nodes(points) |
|
.on("tick", ticked); |
|
|
|
simulation.force("center"); |
|
|
|
svg.call(zoom); |
|
} |
|
start(); |
|
|
|
function dragstarted(d) { |
|
if (!d3.event.active) simulation.restart(); |
|
d.fx = d.x; |
|
d.fy = d.y; |
|
} |
|
|
|
function dragged(d) { |
|
d.x = d3.event.x; |
|
d.y = d3.event.y; |
|
var pt = d3.select(this) |
|
.attr("transform", (d) => {return getTransform(d.dir2,d.x,d.y);}) |
|
} |
|
|
|
function dragended(d) { |
|
if (!d3.event.active) simulation.alphaTarget(0); |
|
d.fx = null; |
|
d.fy = null; |
|
} |
|
|
|
function ticked() { |
|
nodes.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }); |
|
} |
|
|
|
function onclick(d) { |
|
console.info(d); |
|
} |
|
|
|
|
|
// // === START === |
|
// function start() { |
|
// // SETUP POINTS |
|
// g.selectAll(".point").data(points).enter() |
|
// .append("g") |
|
// .attr("id", (d) => "pt-"+d.idx) |
|
// .attr("class", "point") |
|
// .attr("transform", (d) => {return getTransform(d.dir2,d.x,d.y);}) |
|
// .on("click", onclick) |
|
// .call(d3.drag().on("drag", dragged)) |
|
// .append("path") |
|
// .attr("d", (d) => {return getPointSymbolType(d)()} ) |
|
|
|
// // overlay points on top |
|
// g.selectAll(".point").raise(); |
|
|
|
// svg.call(zoom); |
|
// // color points |
|
// updateZones(); |
|
// g.selectAll(".point").attr("class", (d) => {return "point " + d.next.style;}); |
|
// } |
|
// // invoke immediately |
|
// start(); |
|
|
|
|
|
|
|
// // === HELPER FUNCTIONS === |
|
function show_radius(d, r) { |
|
if (d.show) return r; |
|
else return 0; |
|
} |
|
function randomGroup(i) { |
|
var x = Math.random(); |
|
var group = x > GROUP_DISTRIBUTION ? 1 : 0; |
|
return group; |
|
} |
|
function getType(group) { |
|
return METRICS[group]; |
|
} |
|
function getMetric(checked) { |
|
if (checked) return "dist"; |
|
else return "knn"; |
|
} |
|
|
|
function generatePoints(num, radius) { |
|
var result = [] |
|
for (var i = 0; i < num; ++i) { |
|
var theta = Math.PI * (3 - Math.sqrt(5)); |
|
var r = radius * Math.sqrt(i), a = theta * i; |
|
var group = randomGroup(i); |
|
var dir = toDegrees(a)+getRandomNum(0,360); |
|
var x = width / 2 + r * Math.cos(a); |
|
var y = height / 2 + r * Math.sin(a); |
|
var p = { |
|
idx: i, |
|
x: x, |
|
y: y, |
|
dir: dir, |
|
dir2: angleToVector(a), |
|
group: group, |
|
next: {"dir": dir, "ddir": getUnitDir(x,y), "theta": 0}, |
|
metric: getType(group), |
|
show: false |
|
}; |
|
result.push(p) |
|
} |
|
return result; |
|
} |
|
function getTransform(dir, x, y) { |
|
var origin = [1, 0]; |
|
var dot = numeric.dot(origin, dir) |
|
var angle = Math.acos(dot) |
|
var nd = toDegrees(angle) - 90; |
|
var result = "" |
|
result += "rotate(" + nd + "," + x +"," + y +") " |
|
result += "translate(" + x + "," + y + ") " |
|
return result; |
|
} |
|
function getPointSymbolType(d) { |
|
if (d.group === 0) return d3.symbol().size(POINT_SIZE_1).type(d3.symbolTriangle); |
|
else return d3.symbol().size(POINT_SIZE_2).type(d3.symbolCircle); |
|
} |
|
|
|
|
|
// // === UPDATE FUNCTIONS === |
|
|
|
|
|
// function next() { |
|
// // get next positions |
|
// updateZones(); |
|
// // move points |
|
// g.selectAll(".point").interrupt().transition(t) |
|
// .attr("transform",(d) => {d.dir2 = d.next.dir; d.x = d.next.x; d.y = d.next.y; return getTransform(d.dir2, d.x, d.y, d)} ) |
|
// .attr("class", (d) => {return "point " + d.next.style;}); |
|
// // move radii |
|
// g.selectAll(".r").interrupt().transition(t) |
|
// .attr("cx",(d) => { return d.x;}) |
|
// .attr("cy",(d) => { return d.y;}); |
|
// } |
|
|
|
// function updateZones() { |
|
// g.selectAll(".point").each((pt1) => { |
|
// var r_pts = pointsInRadius(pt1, R_r[pt1.group]), |
|
// o_pts = betweenRadii(pt1, R_r[pt1.group], R_o[pt1.group]), |
|
// a_pts; |
|
// if (pt1.metric === "dist") { |
|
// a_pts = betweenRadii(pt1, R_o[pt1.group], R_a[pt1.group]) |
|
// } |
|
// else { |
|
// a_pts = knn(pt1, R_o[pt1.group], K[pt1.group]); |
|
// } |
|
// var neighbors = []; |
|
// Array.prototype.push.apply(neighbors, r_pts); |
|
// Array.prototype.push.apply(neighbors, o_pts); |
|
// Array.prototype.push.apply(neighbors, a_pts); |
|
// var ptclass = ""; |
|
// // nr > 0 |
|
// if (r_pts.length > 0) { |
|
// pt1.next.ddir = r_angle(pt1, r_pts); |
|
// ptclass = 'repulsed'; |
|
// } |
|
// // nr == 0 |
|
// else { |
|
// if (o_pts.length > 0 || a_pts.length > 0) { |
|
// if (a_pts.length > 0 && o_pts.length === 0) { |
|
// ptclass = "attracted"; |
|
// pt1.next.ddir = a_angle(pt1, a_pts); |
|
// } |
|
// else if (o_pts.length > 0 && a_pts.length === 0) { |
|
// ptclass = "oriented" |
|
// pt1.next.ddir = o_angle(pt1, o_pts); |
|
// } |
|
// else if (o_pts.length > 0 && a_pts.length > 0) { |
|
// ptclass = "ao" |
|
// var dir_a = a_angle(pt1, a_pts); |
|
// var dir_o = o_angle(pt1, o_pts); |
|
// var nextdir = numeric.add(dir_a, dir_o); |
|
// numeric.diveq(nextdir, 2); |
|
// pt1.next.ddir = nextdir; |
|
// } |
|
// } |
|
// // n == 0 |
|
// else { |
|
// pt1.next.ddir = pt1.dir2; |
|
// console.log("no friends", pt1.idx); |
|
// } |
|
// } |
|
// var dda = getNextAngle2(pt1); |
|
// if(isNaN(dda[0]) || isNaN(dda[1])) { |
|
// console.log("zero vector", dda) |
|
// dda = pt1.dir2; |
|
// } |
|
// var dx = pt1.x + TRAVEL_LENGTH[pt1.group] * dda[0]; |
|
// var dy = pt1.y + TRAVEL_LENGTH[pt1.group] * dda[1]; |
|
// pt1.next = {"dir": dda, "x":dx, "y":dy, "theta": pt1.next.theta, "style": ptclass}; |
|
// pt1.neighbors = neighbors; |
|
// }); |
|
// } |
|
|
|
|
|
// // === EVENT HANDLERS === |
|
// function onclick(d) { |
|
// d.show = !d.show; |
|
// console.info(d); |
|
|
|
// if (d.show) { |
|
// // Attraction |
|
// if (d.metric === "dist") { |
|
// g.append("circle").datum(d).lower() |
|
// .attr("class", (d) => {return "r R_a r--" + d.idx + " R_a-" + d.group;}) |
|
// .attr("cx", function(d) { return d.x; }) |
|
// .attr("cy", function(d) { return d.y; }) |
|
// .attr("r", 0) |
|
// .attr("opacity", 0.10) |
|
// .transition(t) |
|
// .attr("r", (d) => { return show_radius(d, R_a[d.group]); }); |
|
// } |
|
|
|
// // Orientation |
|
// g.append("circle").datum(d).lower() |
|
// .attr("class", (d) => {return "r R_o r--" + d.idx + " R_o-" + d.group;}) |
|
// .attr("cx", function(d) { return d.x; }) |
|
// .attr("cy", function(d) { return d.y; }) |
|
// .attr("r", 0) |
|
// .attr("opacity", 0.15) |
|
// .transition(t) |
|
// .attr("r", (d) => { return show_radius(d, R_o[d.group]); }); |
|
// // Repulsion |
|
// g.append("circle").datum(d).lower() |
|
// .attr("class", (d) => {return "r R_r r--" + d.idx + " R_r-" + d.group;}) |
|
// .attr("cx", function(d) { return d.x; }) |
|
// .attr("cy", function(d) { return d.y; }) |
|
// .attr("r", 0) |
|
// .attr("opacity", 0.35) |
|
// .transition(t) |
|
// .attr("r", (d) => { return show_radius(d, R_r[d.group]); }); |
|
// } |
|
// else { |
|
// d3.select(".R_r.r--"+d.idx).transition(t).attr("r", 0).remove(); |
|
// d3.select(".R_o.r--"+d.idx).transition(t).attr("r", 0).remove(); |
|
// d3.select(".R_a.r--"+d.idx).transition(t).attr("r", 0).remove(); |
|
// } |
|
// } |
|
// function dragged(d) { |
|
// dx = d3.event.x; |
|
// dy = d3.event.y; |
|
// // update point |
|
// var pt = d3.select("#pt-"+d.idx) |
|
// .attr("transform", (d) => {return getTransform(d.dir2,dx,dy);}) |
|
// // update collision state |
|
// updateZones(); |
|
// g.selectAll(".point").attr("class", (d) => {return "point " + d.next.style;}); |
|
// // update radius |
|
// d3.select(".R_r.r--" + d.idx) |
|
// .attr("cx", d.x = dx).attr("cy", d.y = dy); |
|
// d3.select(".R_o.r--" + d.idx) |
|
// .attr("cx", d.x = dx).attr("cy", d.y = dy); |
|
// d3.select(".R_a.r--" + d.idx) |
|
// .attr("cx", d.x = dx).attr("cy", d.y = dy); |
|
// } |
|
|
|
|
|
// // === MATH FUNCTIONS === |
|
// function dist(pt1, pt2) { |
|
// return Math.pow((pt1.x-pt2.x),2) + Math.pow((pt1.y-pt2.y),2); |
|
// } |
|
|
|
// function getNextAngle(pt) { |
|
// var θτ = TURN_RATE[pt.group]; |
|
// var dθ = (pt.dir - pt.next.ddir); |
|
// var newDir = pt.next.ddir; |
|
|
|
// if (Math.abs(dθ) > θτ) { |
|
// newDir = pt.dir + Math.sign(dθ)*θτ; |
|
// } |
|
// return newDir % 360; |
|
// } |
|
|
|
// function getNextAngle2(pt) { |
|
// var θτ = TURN_RATE[pt.group]; |
|
// var dθ = numeric.sub(pt.dir2, pt.next.ddir) |
|
// var newDir = pt.next.ddir; |
|
// var dot = numeric.dot(pt.dir2, pt.next.ddir); |
|
// var acos = toDegrees(Math.acos(dot)); |
|
// pt.next.theta = acos |
|
// if (Math.abs(acos) > θτ) { |
|
// var theta = Math.sign(acos)*θτ; |
|
// pt.next.theta = theta |
|
// var rotMat = [[Math.cos(theta), -Math.sin(theta)], |
|
// [Math.sin(theta), Math.cos(theta)]] |
|
// newDir = numeric.dot(rotMat, pt.dir2) |
|
// } |
|
// return newDir; |
|
// } |
|
|
|
// function getLaplacian() { |
|
// var L = []; |
|
// g.selectAll(".point").each((p1,i,nodes) => { |
|
// L[p1.idx] = Array(nodes.length).fill(0); |
|
// for (var p2 of p1.neighbors) { |
|
// L[p1.idx][p2.idx] = -1; |
|
// } |
|
// L[p1.idx][p1.idx] = -1 * L[p1.idx].reduce( ( acc, cur ) => acc + cur, 0 ); |
|
// }); |
|
// console.info("Graph Laplacian:", L.join('\n')); |
|
// return L; |
|
// } |
|
|
|
function getEigenvalues(L) { |
|
var eigs = numeric.eig(L); |
|
return eigs['lambda'].x.map((x) => +x.toFixed(3)).sort(); |
|
} |
|
|
|
function getFiedler(eigs) { |
|
return eigs.find((v) => {return v > 0;}) |
|
} |
|
|
|
function getNumComponents(eigs) { |
|
return eigs.filter((v) => {return v === 0;}).length; |
|
} |
|
|
|
function r_angle(d, pts) { |
|
var sum = [0, 0]; |
|
for (var p of pts) { |
|
r = [p.x-d.x, p.y-d.y]; |
|
r = normalize(r); |
|
numeric.addeq(sum,r); |
|
} |
|
var ave = normalize(sum); |
|
ave = numeric.neg(ave); |
|
return ave; |
|
} |
|
|
|
function a_angle(d, pts) { |
|
var sum = [0, 0]; |
|
for (var p of pts) { |
|
r = [p.x-d.x, p.y-d.y]; |
|
r = normalize(r); |
|
numeric.addeq(sum,r); |
|
} |
|
var ave = normalize(sum); |
|
return ave; |
|
} |
|
|
|
function o_angle(d, pts) { |
|
var sum = [0, 0]; |
|
for (var p of pts) { |
|
numeric.addeq(sum, p.dir2); |
|
} |
|
var ave = normalize(sum); |
|
return ave; |
|
} |
|
|
|
function knn(pt1,r, k) { |
|
var neighbors = []; |
|
var compare = (a,b) => {return a.dist - b.dist;} |
|
g.selectAll(".point").each((d) => { |
|
if (pt1.idx != d.idx && !withinRadius(pt1, d, r)) { |
|
var item = {"dist": dist(pt1,d), "point": d} |
|
neighbors.push(item); |
|
} |
|
}); |
|
neighbors.sort(compare); |
|
var result = neighbors.map((n) => n.point); |
|
return result.slice(0,k); |
|
} |
|
|
|
|
|
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 getRandomNum(min, max) { |
|
return Math.random() * (max - min) + min; |
|
} |
|
|
|
function normalize(v) { |
|
var mag = numeric.norm2(v); |
|
return numeric.div(v,mag); |
|
} |
|
|
|
function getUnitDir(x,y) { |
|
var v = [x,y]; |
|
var mag = numeric.norm2(v); |
|
return numeric.div(v,mag); |
|
} |
|
|
|
function angleToVector(a) { |
|
var x = Math.cos(a); |
|
var y = Math.sin(a); |
|
var v = [x, y]; |
|
return normalize(v); |
|
} |
|
|
|
function phyllotaxis(radius) { |
|
var theta = Math.PI * (3 - Math.sqrt(5)); |
|
return function(i) { |
|
var r = radius * Math.sqrt(i), a = theta * i; |
|
var group = randomGroup(i); |
|
var dir = toDegrees(a)+getRandomNum(0,360); |
|
var x = width / 2 + r * Math.cos(a); |
|
var y = height / 2 + r * Math.sin(a); |
|
return { |
|
idx: i, |
|
x: x, |
|
y: y, |
|
dir: dir, |
|
dir2: angleToVector(a), |
|
group: group, |
|
next: {"dir": dir, "ddir": getUnitDir(x,y), "theta": 0}, |
|
metric: getType(group), |
|
show: false |
|
}; |
|
}; |
|
} |