(function (d3) { //debug panel///////////////////////////////////////////////////////////////////////////// var panel = d3.select("#panel"), logEvents = outputs.OutputDiv("#panel", {"background-color": "#ccc", margin: 0}, "#wrapAlpha"); logEvents.message(function (d) { var width = 12, pad = Array(width+1).join(" "); return ((d3.event ? d3.event.type : d)+pad).slice(0,width); }); logEvents.update("waiting...") var alpha = d3.select("#alpha"), cog = d3.select("#wrapAlpha") .style({ "background-color": "#ccc", margin : 0, padding : '3px 3px 3px 3px', display : 'inline-block', }) .insert("i", "#fdg") .classed("fa fa-cog fa-spin", true) .datum({instID: null}), elapsedTime = outputs.ElapsedTime("#panel", {margin: 0, "background-color": "#ccc"}) .message(function (id) { return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap()) }); elapsedTime.consoleOn = false; var forceState = outputs.OutputDiv("#panel", {margin: 0, "background-color": "#ccc"}) .message(function () { return ' gravity: ' + d3.format(".3f")(g.force.gravity()) + '\tcharge: ' + d3.format(".1f")(g.force.charge()) + '\tfriction: ' + d3.format(".3f")(g.force.friction()) + "\t velocity:\t" +( !isNaN(g.v().dx) ? (d3.format(">8.3f")(g.v().dx) + "\t" + d3.format(">8.3f")(g.v().dy)) : "") }); alpha.log = function(e, force) { elapsedTime.mark().timestamp(); alpha.text(d3.format(" >8.4f")(e.alpha)); forceState.update(force) }; ////////////////////////////////////////////////////////////////////////////////////////// var width = $(window).width(), height = $(window).height()*0.9; var circles = [ { x: width / 2 + 100, y: height / 2, radius: 50 }, { x: width / 2 - 100, y: height / 2, radius: 100 }, // { // x: width / 2, // y: height / 2 + 100, // radius: 100 // }, //{ // x: width / 2 + 100, // y: height / 2, // radius: 100 //}, //{ // x: width / 2 - 100, // y: height / 2, // radius: 100 //}, //{ // x: width / 2, // y: height / 2 + 100, // radius: 100 //}, ], collide = Collide(circles, 0), nodeFill = "#006E3C"; //panel.attr("width", 50 + "%"); var force = d3.layout.force() .gravity(0) .charge(0) .friction(1) .size([width, height]) .nodes(circles) .linkDistance(250) .linkStrength(1) .on("tick", tick) .on("start", function () { elapsedTime.start() }) .start(); circles.forEach(function(d) { // add velocity interface d.v = { get x() {return d.x - d.px}, get y() {return d.y - d.py}, get v() {return [this.x, this.y];}, set v(vel) {d.px = d.x - vel[0]; d.py = d.y - vel[1]}, get m() {return Math.sqrt(this.x*this.x + this.y*this.y)} }; // add a quantised version of all numeric properties d.q = {}; Object.keys(d).forEach(function(p){ if(!isNaN(d[p])) Object.defineProperty(d.q, p, { get: function () {return Math.round(d[p])} }); }) // set random initial velocities d.v.v = [Math.random()*30, Math.random()*30] }); var svg = d3.select("#viz") .append("svg") .attr("width", width) .attr("height", height) .style({"background-color": "black", opacity: 0.6}); var nodes = svg.selectAll(".node"); nodes = nodes.data(circles); nodes.exit().remove(); var enterNode = nodes.enter().append("g") .attr("class", "node") .call(force.drag); //Add circle to group enterNode.append("circle") .attr("r", function (d) { return d.radius; }) .style("fill", "#006E3C") .style("opacity", 0.9); //Drag behaviour/////////////////////////////////////////////////////////////////// // hook drag behavior on force //VELOCITY // maintain velocity state in case a force tick occurs immediately before dragend // the tick wipes out previous position var dragVelocity = (function () { var dx, dy, dd; function f(d) { dd = d = d || dd; if (d3.event) { dx = d3.event.dx; dy = d3.event.dy; return { dx: dx, dy: dy } } else { if (d) return { dx: d.v.x, dy: d.v.y }; else return { dx: "...vx", dy: "...vy" }; } }; f.correct = function (d) { dd = d = d || dd; if (true && dx && d.x === d.px && d.y === d.py) { //tick occurred and set px/y to x/y, re-establish velocity state d.px = d.x - dx; d.py = d.y - dy; } else { //not used! //velocity state is ok but previous and current are reversed during drag //correct the velocity direction x = d.x; d.x = d.px; d.px = x; y = d.y; d.y = d.py; d.py = y; } } f.reset = function() { dx = dy = 0} return f; })() //DRAGSTART HOOK///////////////////////// var stdDragStart = force.drag().on("dragstart.force"); force.drag().on("dragstart.force", myDragStart); function myDragStart(d) { var that = this, node = d3.select(this); logEvents.update(); nonStickyMouse(); dragVelocity.reset(); stdDragStart.call(this, d) function nonStickyMouse() { if (!d.___hooked) { //node is not hooked //hook mouseover///////////////////////// //remove sticky node on mouseover behavior and save listeners d.___mouseover_force = node.on("mouseover.force"); node.on("mouseover.force", myMouseOver); d.___mouseout_force = node.on("mouseout.force"); d.___hooked = true; //standard mouseout will clear d.fixed d.___mouseout_force.call(that, d); } //disable mouseout///////////////////////// node.on("mouseout.force", null); } function myMouseOver(d) { logEvents.update(); } } var f = d3.format("6,.0f"); //DRAG HOOK///////////////////////// var stdDrag = force.drag().on("drag.force"); force.drag().on("drag.force", myDrag); function myDrag(d) { var v, p; //maintain back-up velocity state v = dragVelocity(); p = { x: d3.event.x, y: d3.event.y }; stdDrag.call(this, d) } //DRAGEND HOOK///////////////////////// var stdDragEnd = force.drag().on("dragend.force"); force.drag().on("dragend.force", myDragEnd); function myDragEnd(d) { var that = this, node = d3.select(this); //var x = d.x, y = d.y; //stop dead //d.px = d.x; d.py = d.y; //correct the final velocity vector at drag end dragVelocity.correct(d) logEvents.update(); //hook mouseout///////////////////////// //re-establish standard behavior on mouseout node.on("mouseout.force", function mouseout(d) { myForceMouseOut.call(this, d, elapsedTime.t() + "\tmouseout") }); stdDragEnd.call(that, d); function myForceMouseOut(d, timeSet) { var timerID = window.setTimeout((function () { var that = this, node = d3.select(this); return function unhookMouseover() { if (node.datum().___hooked) { //un-hook mouseover and mouseout//////////// node.on("mouseout.force", d.___mouseout_force); node.on("mouseover.force", d.___mouseover_force); node.datum().___hooked = false; } } }).call(this), 1000); return timerID; } } //DYNAMICS///////////////////////// function tick(e) { if(alpha && alpha.log) alpha.log.call(this, e); nodes.attr("transform", function (d) { var r = d.radius; if (d.x - r <= 0 && d.q.px >= d.q.x) boundary(d, "x", [0, width]); if (d.x + r >= width && d.q.px <= d.q.x) boundary(d, "x", [width, 0]); if (d.y - r <= 0 && d.q.py >= d.q.y) boundary(d, "y", [0, height]); if (d.y + r >= height && d.q.py <= d.q.y) boundary(d, "y",[height, 0]); collide(e.alpha, r)(d); return "translate(" + d.x + "," + d.y + ")"; function boundary(p, y, b) { var k; if(p.q[y] === p.q["p"+y]) { // node velocity is zero p[y] += ((b[0] < b[1]) ? 1 : -1); }else { p["p" + y] = 2* p[y] - p["p" + y]; } } }); nodes.selectAll("circle").style("fill", function (d, i) { return ((d.___hooked && !d.fixed) ? "red" : nodeFill) }) nodes.each(function(d, i) { if(d.___hooked && !d.fixed) console.log(i + "\thooked but not fixed")}) force.start(); } function Collide(nodes, padding) { // Resolve collisions between nodes. var maxRadius = d3.max(nodes, function(d) {return d.radius}); return function collide(alpha) { var quadtree = d3.geom.quadtree(nodes); return function(d) { var r = d.radius + maxRadius + padding, nx1 = d.x - r, nx2 = d.x + r, ny1 = d.y - r, ny2 = d.y + r; quadtree.visit(function(quad, x1, y1, x2, y2) { var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1); if (quad.point && (quad.point !== d) && possible) { var x = d.x - quad.point.x, y = d.y - quad.point.y, l = Math.sqrt(x * x + y * y), r = d.radius + quad.point.radius + padding, m = Math.pow(quad.point.radius, 3), mq = Math.pow(d.radius, 3), mT = m + mq; if (l < r) { //move the nodes away from each other along the radial (normal) vector //taking relative mass into consideration, the sign is already established //in calculating x and y and the nodes are modelled as spheres for calculating mass l = (r - l) / l * alpha; d.x += (x *= l) * m/mT; d.y += (y *= l) * m/mT; quad.point.x -= x * mq/mT; quad.point.y -= y * mq/mT; } } return !possible; }); }; } } var g = {}; g.force = force; g.v = dragVelocity; })(d3);