|
<!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> |
|
|