Skip to content

Instantly share code, notes, and snippets.

@cool-Blue
Last active April 3, 2017 01:18
Show Gist options
  • Save cool-Blue/7274144b8a07127298cb to your computer and use it in GitHub Desktop.
Save cool-Blue/7274144b8a07127298cb to your computer and use it in GitHub Desktop.
Force directed graph with layered gravity and physically modeled collisions - webGL
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
# Sensitive or high-churn files:
.idea/dataSources/
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

Force Directed Graph with Custom Gravity in webGL

Features

  • Rendered in a webGL canvas using pixi.js
    This is a first-cut attempt at integrating D3 with webGL. The trial architecture uses dummy nodes that are name-spaced out of the DOM render cycle and attributes used to store some information that is passed to the webGL renderer. Most of the communications to the renderer however are via the data bound to the dummy nodes. The dummy nodes are created using canonical, d3 patterns and the the renderer is included in this using selection.call() statements on the update, enter and exit selections. The renderer exposes a member for the nodes that has init, update, enter, exit and merge methods. The init method is required to pass the maximum node radius to the renderer to make the sprite sheet for the nodes.

  • Metrics display and inputs
    The metrics panel across the top of the svg element gives a live display of the layout state. The inputs on the left allow for the number of nodes, the maximum recovery velocity for escaped nodes and the windup factor for clearing overlaps to be adjusted live. The current alpha value for the layout is displayed along with instantaneous and averaged tick time and the average frame rate of the layout. Changing the number of nodes re-starts the layout.

  • Custom gravity (layers)
    The nodes have a cy member which is its target y value, the gravity function, which is called from the force.on("tick"...) callback has behaviour to move the nodes toward their cy position on each tick.

  • Collisions between nodes Based on this example but enhanced to accurately model relative "mass" the rebound velocities are solved for using the equations for conservation of momentum and conservation of energy. The "efficiency" of the collisions - as defined by the ratio of the actual final velocities divided by the ideal, perfectly elastic system - is set internally.

  • Boundary constraints and collisions
    This behaviour is also included in the gravity function and uses basic geometry to reflect the incident velocity in the plane of the boundary. It does this by using the Node velocity API to manipulate the d.px and d.py values of the nodes. It is possible, however, for nodes to penetrate the boundaries due to limitations in the temporal resolution of the layout.

  • Recovering escaped nodes
    If a node escapes the boundaries of the bounding box, the velocity is still reflected in the plane of the penetrated boundary. If the escaped node has no velocity (p.q.x/y == p.q.px/py) then the node is steered back toward the boundary, with constant speed, or a random speed (between 0 and recover speed) if it has come to rest. After it is fully recovered, the velocity of the node will be naturally determined by the distance it was last moved by the recovery behaviour.

  • Node velocity API
    Behaviour is added to the nodes to allow easy setting and getting of node velocity vector. This is accomplished by manipulating the previous value of the node position vector.

  • Node momentum indicators
    Momentum vector tracers are toggled by a button at the bottom of the graph. The vector represents the effective momentum of the nodes which takes into account the frustration factor and the anxiety factor and the magnification selected by the user.

  • Quantisation of screen position
    It is possible for nodes with different values to be stationary due to quantisation of d.x and d.px by the rendering process. This means that decisions in the code based on position may not reflect the rendered state properly. This is fixed by adding a getter to each data point that returns a rounded version of d.x/y and d.px/y: d.q.x/y and d.q.px/y. The quantised versions are used for the decision making in the boundary collisions and recovery behaviour.

  • Guaranteed un-mixing of nodes
    It's possible for nodes to be blocked from reaching they're cy positions by other, more massive nodes in adjacent layers, in order to overcome this, a frustration factor is applied to the isolated nodes which has the effect of increasing they're effective mass with each tick that they are outside they're designated band. The increase in mass factor per tick is the windup which is internally set to 0.01. As soon as the nodes make it to they're layer they're anxiety is switched back to 1.

  • Clearing node overlaps
    Smaller nodes can be squashed in the matrix of bigger nodes, because they have mass is insignificant, the matrix of bigger nodes is not moved and the smallest nodes remain overlapped. In order to clear this issue, the overlaps are monitored and the effective mass of the smaller node in the overlapping pair is increased by fear factor, every collision where the overlap increases. The increase per collision is set by the windUp control in the metrics panel. Nodes with a fear greater than one are highlighted with a lighter stroke color. If the overlap decreases, the fear factor is also decreased by the wind-up value.

  • Extended cooling time
    The cooling time for the force layout is determined by the evolution of its alpha value. This is set to 0.1 when the layout is started and is normally reduced by 1% each tick until it reaches 0.005, at which point the layout will stop updating. It is possible to manipulate the cooling rate so that it cools at x% by dividing the current alpha value by the standard factor of 0.99 and multiplying it by (1 - x%) at the end of the tick callback:

  force.alpha(a/0.99*(1 - x))

d3 features used

  1. d3.layout.force
  2. Ordinal Scales
  3. d3.format
  4. d3.range
  5. d3.geom.quadtree
<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/button/style.css">
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.css">
<style>
body {
background: #2B303B;
margin:0;
padding:0;
}
#application {
margin: 0 auto;
position: relative;
width: 960px;
height: 500px;
}
#inputs {
display: inline-block;
margin: 0;
border: none;
/*padding: 0 0 0 1em;*/
box-sizing: border-box;
background-color: #2B303B;
}
#metrics {
display: inline-block;
margin: 0 auto;
background-color: #2B303B;
}
label, input {
text-align: left;
width: 3.5em;
color: orange;
/*padding-left: 1em;*/
background-color: #2B303B;
outline: none;
border: none;
}
circle {
stroke: black;
}
#viz {
display: block;
position: relative;
margin: 0 auto;
}
canvas {
display: block;
border: 1px dashed black;
/*margin: 0 100px 0 100px;*/
}
text {
text-anchor: middle;
}
.g-button {
color: #804700;
background: black;
border-color: orange;
}
.g-button.g-active {
color: orange;
background: #333333;
border-color: orange;
}
#tool-tip {
-webkit-transition: opacity 1s;
-moz-transition: opacity 1s;
-o-transition: opacity 1s;
transition: opacity 1s;
pointer-events: none;
padding: 3px;
border-radius: 3px;
white-space: pre;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<!--<script src="d3 CB.js"></script>-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/3.0.7/pixi.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinycolor/1.1.2/tinycolor.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/filters/shadow.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/plot-transform.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/button/2.0.0/button.js"></script>
<script>
d3.ns.prefix.webGL = "CB:webGL/dummy/nodes";
var iFrame = {"id": "application", width: 960, height: 500},
app = d3.select("body").append("div").attr(iFrame),
inputs = app.append("div")
.attr("id", "metrics").append("div").attr({id: "inputs"}),
nodeCount = inputs.append("label")
.attr("for", "nodeCount")
.text("nodes: ")
.append("input")
.attr({id: "nodeCount", class: "numIn", type: "number", min: "100", max: "5,000", step: "100", inputmode: "numeric"}),
reEntrySpeed = inputs.append("label")
.attr("for", "sideConstraint")
.text("rec. speed: ")
.append("input")
.attr({id: "sideConstraint", class: "numIn", type: "number", min: "0", max: "100", step: "1", inputmode: "numeric"}),
windUp = inputs.append("label")
.attr("for", "windUp")
.text("windUp: ")
.append("input")
.attr({id: "windUp", class: "numIn", type: "number", min: "0", max: "5", step: "0.5", inputmode: "numeric"}),
elapsedTime = outputs.ElapsedTime("#metrics", {
border: 0, margin: 0, "box-sizing": "border-box",
padding: "0 0 0 6px", background: "#2B303B", "color": "orange"
})
.message(function(value) {
var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap)
return 'alpha:' + d3.format(" >7,.3f")(value)
+ '\tframe rate:' + d3.format(" >4,.1f")(1 / aveLap) + " fps"
}),
hist = d3.ui.FpsMeter("#metrics", {display: "inline-block"}, {
height: 10, width: 100,
values: function(d){return 1/d},
domain: [0, 60]
}),
// set up webGL
detatchedContainer = document.createElement("webGL:scene"),
scene = d3.select(detatchedContainer).attr("id", "scene"),
container = Object.defineProperty(
app.append("div").attr("id", "viz")
.each(function(){
return
}), 'scene', {get: function(){ return scene; }}),
butt_vectors = {
label: "show effective momentum vectors",
onclick: function() {
this.blur();
},
value: false
},
pMagnification = 10,
pX1 = {
label: "X1",
group: "mag",
onclick: function() {
pMagnification = 1;
butt_vectors.value = true;
this.blur();
},
value: false
},
pX2 = {
label: "X2",
group: "mag",
onclick: function() {
pMagnification = 2;
butt_vectors.value = true;
this.blur();
},
value: false
},
pX10 = {
label: "X10",
group: "mag",
onclick: function() {
pMagnification = 10;
butt_vectors.value = true;
this.blur();
},
value: true
},
pX50 = {
label: "X50",
group: "mag",
onclick: function() {
pMagnification = 50;
butt_vectors.value = true;
this.blur();
},
value: false
},
_controls = [butt_vectors, pX1, pX2, pX10, pX50],
buttons = Object.defineProperties(
app.append("div")
.attr("id", "controls")
.style({padding: "10px auto 10px auto", "text-align": "center"})
.call(d3.ui.buttons.toggle, _controls),
{
"showVectors": {
get: function() {
return butt_vectors.value
}
},
"height": {
get: function(){
return this.node().getBoundingClientRect().height;
}
}
}),
width = iFrame.width,
height = iFrame.height - metrics.getBoundingClientRect().height - buttons.height - 3,
r0 = 5.5,
rMax = 0,
n = 2000, // total number of nodes
m = 10; // number of distinct layers
nodeCount
.property("value", n)
.on("change", function() {
viz = update(force, this.value);
this.blur();
});
reEntrySpeed
.property("value", 2)
.value = function() { return this.property("value")};
windUp
.property("value", 1)
.value = function() { return +this.property("value")};
// elapsedTime.selection.style({
// width: (width - parseFloat(window.getComputedStyle(d3.select("#inputs").node()).getPropertyValue("width"))) + "px"
// });
scene.attr("width", width)
.attr("height", height)
.append("webGL:g");
var color = d3.scale.category10()
.domain(d3.range(m)),
xMargin = 0.1,
x = d3.scale.linear()
.domain([0, width])
.range([width*xMargin, width*(1 - xMargin)]),
y = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([height, 0], 1),
w = d3.scale.ordinal()
.domain(d3.range(m))
.rangeBands([height, 0]),
wRange = w.range(),
renderer = Renderer(container);
scene.selectAll("wells").data(wRange).enter().append("webGL:rect")
.attr({width: width, height: w.rangeBand(), y: function(d) {return d}});
var force = d3.layout.force()
.size([width, height])
.gravity(0)
.charge(-1)
.friction(0.5)
.on("tick", tick)
.on("start", function() {
elapsedTime.start(100);
})
.on("end", function() {
window.requestAnimationFrame(tick)
}),
toolTip = container.append("div")
.attr("id", "tool-tip")
.style({
display: "none",
position: "absolute",
"background-color": "#ccc",
color: "black",
opacity: 0
})
.call((function() {
var target;
return function(selection) {
Object.defineProperties(selection, {
target: {
set: function(_) {
target = _;
},
get: function() {return target;}
},
update: {
value: function() {
if(!target) return selection.style({display: "none"});
var d = target;
selection.style({
display: "block",
opacity: 0.8,
top: (d.q.y - rMax * 2) + "px",
left: (d.q.x + rMax * 2) + "px"
})
.text([
["index:", d.index].join("\t"),
["r:", d.radius.toPrecision(3)].join("\t"),
["overlap:", (d.overlap || 0).toPrecision(3)].join("\t"),
["posn:", [d.q.x, d.q.y]].join("\t"),
["vel:", [d.v.x.toPrecision(3), d.v.y.toPrecision(3)]].join("\t"),
["anxiety", d.anxiety()].join("\t"),
["frustration", d.frustration()].join("\t"),
d.fixed,
].join("\n"));
}}
})
}
})()),
viz = update(force, n);
buttons.on("click", viz.momenta);
function tick(e) {
var a = e.alpha || 0.05;
elapsedTime.mark(a);
if(elapsedTime.aveLap.history.length)
hist(elapsedTime.aveLap.history);
for (var i = 0; i < 2; i++) {
viz.circle
.each(viz.Collide(a));
}
viz.circle
.each(gravity(a));
renderer.draw();
toolTip.update();
// if(e.alpha) force.stop();
// else window.requestAnimationFrame(force.tick);
force.alpha(a / 0.99 * 0.999)
}
// Move nodes toward cluster focus.
function gravity(alpha) {
var moreThan;
return function g(d) {
//reflect off the edges of the container
// check for boundary collisions and reverse velocity if necessary
if((moreThan = d.x >= x(width - d.radius)) || d.x <= x(0) + d.radius) {
// if the object is outside the boundaries
// manage the sign of its x velocity component to ensure it is moving back into the bounds
if(~~d.v.x) d.v.x *= moreThan && d.v.x > 0 || !moreThan && d.v.x < 0 ? -1 : 1;
// if vx is too small, then steer it back in
else d.sx = (~~Math.abs(d.v.y) || Math.random() * reEntrySpeed.value()) * (moreThan ? -1 : 1)
}
if((moreThan = d.y >= (height - d.radius)) || d.y <= d.radius) {
if(~~d.v.y) d.v.y *= moreThan && d.v.y > 0 || !moreThan && d.v.y < 0 ? -1 : 1;
else d.sy = (~~Math.abs(d.v.x) || Math.random() * reEntrySpeed.value()) * (moreThan ? -1 : 1)
}
//find the layers
d.y += d.fixed ? 0 : (d.cy - d.y) /** d.frustration()*/ * alpha;
};
}
// collision detection
// physically accurate: conservation of energy and momentum
function Collide(data, eff) {
var maxRadius = d3.max(data, function(d) {
return d.radius
}),
mFixed = Math.pow(maxRadius, 3) * 1000;
return function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function Q(d) {
var cell = d.radius + maxRadius,
nx1 = d.x - cell,
nx2 = d.x + cell,
ny1 = d.y - cell,
ny2 = d.y + cell;
function v(quad, x1, y1, x2, y2) {
var possible = !(x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1);
var q = quad.point;
if(q && (q !== d) && possible) {
var x = d.x - q.x,
y = d.y - q.y,
l = Math.sqrt(x * x + y * y),
r = (d.radius + q.radius) * 1.2,
margin = l - r;
if(margin < 0) {
var dfixed = d.fixed & 2,
qfixed = q.fixed & 2,
m = dfixed ? mFixed : d.m,
mq = qfixed ? mFixed : q.m,
quadIsBigger = mq > m,
df = d.frustration(), da = d.anxiety(margin, quadIsBigger),
qf = q.frustration(), qa = q.anxiety(margin, !quadIsBigger),
meff = m * df * da,
mqeff = mq * qf * qa,
mT = meff + mqeff,
mr = meff / mT /*/ df*/,
mqr = mqeff / mT/* / qf*/,
vels = (true && (d.s > 3 || q.s > 3 || dfixed || qfixed))
? bounce(d, q, x, y, meff, mqeff)
: {d: d.v, q: q.v},
_l = (margin) / l * (1 + alpha);
// if(dfixed) {
// mr = 0;
// mqr = 1
// } else if(qfixed) {
// mqr = 0;
// mr = 1
// }
d.x -= (x *= _l) * mqr;
d.y -= (y *= _l) * mqr;
q.x += x * mr;
q.y += y * mr;
/*
if(dfixed || qfixed /!*|| (d.s > 5 || q.s > 5)*!/) {
var fmt = " >8,.3f";
console.log("hit: " + f(fmt,[r, l, -margin, -_l]).join("\t") + "\td:\t"
+ d.index + "\t" + dfixed + "\t"
+ f(fmt, [d.radius, d.s, vels.d.x, vels.d.y, meff, x*mqr, y*mqr]).join("\t") + "\tq:\t"
+ q.index + "\t" + qfixed + "\t"
+ f(fmt, [q.radius, q.s, vels.q.x, vels.q.y, mqeff, x*mr, y*mr]).join("\t"));
}
*/
d.overlap = q.overlap = -margin;
d.v = vels.d;
q.v = vels.q;
}
}
return !possible;
}
quadtree.visit(v);
};
function bounce(d, q, x, y, m, mq) {
// Note: confirmed that lookup tables for sin and cos are an order of magnitude slower
var dvx = d.v.x, dvy = d.v.y,
collision_angle = Math.atan2(y, x),
magnitude_d = Math.sqrt(dvx * dvx + dvy * dvy),
magnitude_q = Math.sqrt(q.v.x * q.v.x + q.v.y * q.v.y),
direction_d = Math.atan2(dvy, dvx),
direction_q = Math.atan2(q.v.y, q.v.x),
new_vx_d = magnitude_d * Math.cos(direction_d - collision_angle) * eff,
new_vy_d = magnitude_d * Math.sin(direction_d - collision_angle) * eff,
new_vx_q = magnitude_q * Math.cos(direction_q - collision_angle) * eff,
new_vy_q = magnitude_q * Math.sin(direction_q - collision_angle) * eff,
final_vx_d = ((m - mq) * new_vx_d + (mq + mq) * new_vx_q) / (m + mq),
final_vx_q = ((m + m) * new_vx_d + (mq - m) * new_vx_q) / (m + mq),
final_vy_d = new_vy_d,
final_vy_q = new_vy_q,
cos_collision_angle = Math.cos(collision_angle),
sin_collision_angle = Math.sin(collision_angle),
cos_collision_angle_plus_90 = Math.cos(collision_angle + Math.PI / 2),
sin_collision_angle_plus_90 = Math.sin(collision_angle + Math.PI / 2);
return {
d: {
x: cos_collision_angle * final_vx_d + cos_collision_angle_plus_90 * final_vy_d,
y: sin_collision_angle * final_vx_d + sin_collision_angle_plus_90 * final_vy_d
},
q: {
x: cos_collision_angle * final_vx_q + cos_collision_angle_plus_90 * final_vy_q,
y: sin_collision_angle * final_vx_q + sin_collision_angle_plus_90 * final_vy_q
}
}
}
}
}
function initNodes(force, n) {
force.nodes(d3.range(n).map(function(i) {
var layer = Math.floor(Math.random() * m),
// v = (layer + 1) / m * -Math.log(Math.random());
v = -Math.log(Math.random()),
radius = Math.sqrt(v) * r0;
rMax = radius > rMax ? radius : rMax;
return {
radius: radius,
m: Math.pow(radius, 3),
color: layer,
cy: y(layer),
get v() {
var d = this;
return {x: d.x - d.px || 0, y: d.y - d.py || 0}
},
set v(v) {
var d = this;
d.px = d.x - v.x;
d.py = d.y - v.y;
},
set sx(s) {
this.v = {x: s, y: this.v.y}
},
set sy(s) {
this.v = {y: s, x: this.v.x}
},
get s() {
var v = this.v;
return Math.sqrt(v.x * v.x + v.y * v.y)
},
frustration: (function() {
//if they can't get home, they get angry, but, as soon as they're home, they're fine
var anger = 1, windUp = 0.1;
return function() {
// adjust frustration level based on context and windup rate
var d = this, anxious = (Math.abs(d.cy - d.y) > w.rangeBand()
/ 2);
return anger = anxious ? anger + windUp : 1;
}
})(),
anxiety: (function() {
// get agitated if overlaps keep increasing
var fear = 1, prevOverlap;
return function(overlap, runt) {
if(typeof overlap == "undefined") return fear;
// adjust anxiety level based on context and windup rate
var afraid = -overlap > prevOverlap;
prevOverlap = -overlap;
return fear = afraid && runt ?
fear + windUp.value() : 1
/*fear - windUp.value() < 1 ?
fear -1 :
-windUp.value()*/;
}
})(),
index: i
};
}));
// var collide = Collide(force.nodes(), padding);
force.start();
// add a quantiser object that returns a quantised version of all numerical properties
force.nodes().forEach(function(d) {
d.q = {};
Object.keys(d).forEach(function(p) {
if(!isNaN(d[p])) Object.defineProperty(d.q, p, {
get: function() {return Math.round(d[p])}
});
})
});
return Collide(force.nodes(), .8);
}
function update(force, n) {
var c = initNodes(force, n);
function momenta() {
var update = scene.selectAll("line")
.data(buttons.showVectors ? force.nodes() : [])
.call(renderer.momenta.update);
update.enter().append("webGL:line")
.attr({"stroke": "red",
"stroke-width": 6,
"stroke-linecap": "round",
opacity: 0.4
})
.call(renderer.momenta.enter);
update.exit()
.call(renderer.momenta.exit)
.remove();
update
.attr("x1", function(d) {
return d.px;
})
.attr("y1", function(d) {
return d.py;
})
.attr("x2", function(d) {
return d.x;
})
.attr("y2", function(d) {
return d.y;
})
.call(renderer.momenta.merge);
return update;
}
momenta();
return {
Collide: c,
circle: (function() {
renderer.bubbles.init(rMax);
var update = scene.selectAll("circle")
.data(force.nodes())
.call(renderer.bubbles.update);
update.enter().append("webGL:circle")
.call(renderer.bubbles.enter);
update.exit()
.call(renderer.bubbles.exit)
.remove();
update
.attr("r", function(d) {
return d.radius;
})
.call(renderer.bubbles.merge);
// .call(force.drag)
return update;
})(),
momenta: momenta
};
}
function f(_fmt, x) {
return Array.isArray(x) ? x.map(f.bind(null, _fmt)) : d3.format(_fmt)(x);
}
function Renderer(container) {
var view = container.append("canvas"),
pfScene = container.scene,
renderer = new PIXI.
autoDetectRenderer(pfScene.attr("width"), pfScene.attr("height"), {view: view.node()}),
dropShadow = new PIXI.filters.DropShadowFilter(),
stage = new PIXI.Container(),
objects = {};
renderer.backgroundColor = +("0x" + tinycolor("rgba(255,255,255,0)").toHex());
dropShadow.color = "0x" + tinycolor("steelblue").toHex();
dropShadow.angle = Math.PI / 4;
dropShadow.blur = 4;
dropShadow.distance = r0;
stage.filters = [dropShadow];
var bubbles = (function() {
var spriteSheet, rMax,
circles = new PIXI.Container();
circles.___update = updatePosition;
stage.addChild(circles);
function updatePosition() {
circles.children.forEach(function(c) {
if(c.data.fixed & 2) {
// update the data to reflect the moved to position
c.data.x = c.position.x;
c.data.y = c.position.y;
} else {
c.position.x = c.data.x;
c.position.y = c.data.y;
}
})
}
return {
init: function(_rMax) {
rMax = _rMax
spriteSheet = filters.makeSpriteSheet((rMax).toFixed(), color.range())
},
exit: function exit(selection) {
// EXIT
var s = 0;
selection.each(function(d, i, j) {
circles.removeChildAt(i - s++);
});
},
enter: function enter(selection) {
// ENTER
selection.each(
function(d) {
var circle = new PIXI.Sprite(spriteSheet(d.color));
circle.anchor.set(0.5);
circle.interactive = true;
circle.bringToFront = bringToFront;
circle
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut)
// events for drag start
.on('mousedown', onDragStart)
.on('touchstart', onDragStart)
// events for drag end
.on('mouseup', onDragEnd)
.on('mouseupoutside', onDragEnd)
.on('touchend', onDragEnd)
.on('touchendoutside', onDragEnd)
// events for drag move
.on('mousemove', onDragMove)
.on('touchmove', onDragMove);
circles.addChild(circle)
}
);
function onMouseOver(event) {
if(!event.data.originalEvent.shiftKey) return;
var d = this.data;
toolTip.target = d;
d.fixed |= 4;
// d.x = this.position.x -= d.v.x;
// d.y = this.position.y -= d.v.y;
this.bringToFront();
}
function onMouseOut(event) {
toolTip.target = null;
this.data.fixed &= ~4;
this.alpha = 1;
}
function onDragStart(event) {
// store a reference to the data
// the reason for this is because of multitouch
// we want to track the movement of this particular touch
this.eventData = event.data;
// this.alpha = 0.5;
this.data.fixed |= 2;
}
function onDragEnd() {
this.alpha = 1;
this.data.fixed &= ~6;
// set the interaction data to null
this.eventData = null;
}
function onDragMove(event) {
if(this.data.fixed & 2) {
var newPosition = this.eventData.getLocalPosition(this.parent);
this.data.px = this.position.x = newPosition.x;
this.data.py = this.position.y = newPosition.y;
}
}
},
update: function(){},
merge: function b(selection) {
// UPDATE+ENTER
selection.each(function(d, i, j) {
var circle = circles.children[i];
circle.texture = spriteSheet(d.color);
circle.scale.set(d.radius / rMax);
circle.data = d;
});
}
}
})();
var momenta = (function() {
var lines = new PIXI.Graphics(),
maxM;
lines.___update = updatePosition;
stage.addChild(lines);
function updatePosition() {
function u(d){
function qS(s){
var minS = 0.1;
return Math.abs(s) > minS ? s : 0;
}
var a;
return qS(d.s) ? [d.v.x / d.s, d.v.y / d.s] :
[Math.cos(a = Math.random() * 2 * Math.PI), Math.sin(a)];
}
if(!lines.graphicsData.length) return;
lines.graphicsData.forEach(function(g) {
var d = g.__datum__;
// if (d.anxiety() /** d.frustration()*/ == 1) return;
var effMass = d.m / maxM * d.frustration() * d.anxiety(), uv = u(d);
g.shape.points = [
d.x, d.y,
d.x - uv[0] * effMass * pMagnification,
d.y - uv[1] * effMass * pMagnification
];
g.points = g.shape.points.concat(g.shape.points.slice(2));
});
lines.dirty = lines.clearDirty = true;
}
return {
exit: function exit(selection) {
// EXIT
var s = 0;
selection.each(function(d, i, j) {
lines.graphicsData.splice(i - s++, 1);
});
lines.dirty = lines.clearDirty = true;
},
enter: function(selection){
selection.each(function(d) {
var line = d3.select(this),
attr = line.attr.bind(line);
lines.lineStyle(attr("stroke-width"),
+("0x" + tinycolor(attr("stroke")).toHex()) || 0,
+attr("opacity") || 1);
lines.moveTo(d.x, d.y);
lines.lineTo(d.x - d.v.x / d.s, d.y - d.v.y / d.s);
})
// updatePosition();
},
update: function(selection){
},
merge: function b(selection) {
// UPDATE+ENTER
maxM = d3.max(selection,function(g) {
return d3.max(g, function(n) {
return d3.select(n).datum().m
})
});
selection.each(function(d, i, j) {
lines.graphicsData[i].__datum__ = d;
});
updatePosition();
}
}
})();
function bringToFront() {
var container = this.parent;
container.swapChildren(this, container.children[container.children.length - 1]);
}
return {
draw: function draw(){
stage.children.forEach(function updateStage(c){
if (!c.___update) return;
c.___update();
});
renderer.render(stage)
},
bubbles: bubbles,
momenta: momenta,
stage: stage
};
}
function myName(args) {
return /function\s+(\w*)\(/.exec(args.callee)[1];
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment