// === 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 }; }; }