Last active
September 28, 2015 15:58
-
-
Save cool-Blue/d7cc880054fcee402ace to your computer and use it in GitHub Desktop.
d3 force layout inertial drag
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html xmlns="http://www.w3.org/1999/xhtml"> | |
<head> | |
<title>SO30387784</title> | |
<style> | |
div { | |
display: inline-block; | |
} | |
div#panel { | |
display: block; | |
white-space: normal; | |
background-color: #ccc; | |
padding: 0 3px 0 3px; | |
} | |
svg { | |
display: block; | |
overflow: visible; | |
} | |
</style> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css"> | |
</head> | |
<body> | |
<div id="panel"> | |
<div id="wrapAlpha">alpha: | |
<div id="alpha"></div> | |
</div><div id="fdg"> | |
</div> | |
</div> | |
<div id="viz"></div> <!--<script src="jquery-1.11.1.js"></script>--> | |
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | |
<!--<script src="d3 CB.js"></script>--> | |
<!--<script src="elapsedTime/elapsedTime/elapsed time 2.0.js"></script>--> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3/master/d3%20CB.js"></script> | |
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script> | |
<script src="script.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment