Graph database Analysis of the Steam Network
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

717 lines
22 KiB

;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize packages:
sigma.utils.pkg('sigma.renderers');
/**
* This function is the constructor of the canvas sigma's renderer.
*
* @param {sigma.classes.graph} graph The graph to render.
* @param {sigma.classes.camera} camera The camera.
* @param {configurable} settings The sigma instance settings
* function.
* @param {object} object The options object.
* @return {sigma.renderers.canvas} The renderer instance.
*/
sigma.renderers.webgl = function(graph, camera, settings, options) {
if (typeof options !== 'object')
throw 'sigma.renderers.webgl: Wrong arguments.';
if (!(options.container instanceof HTMLElement))
throw 'Container not found.';
var k,
i,
l,
a,
fn,
_self = this;
sigma.classes.dispatcher.extend(this);
// Conrad related attributes:
this.jobs = {};
Object.defineProperty(this, 'conradId', {
value: sigma.utils.id()
});
// Initialize main attributes:
this.graph = graph;
this.camera = camera;
this.contexts = {};
this.domElements = {};
this.options = options;
this.container = this.options.container;
this.settings = (
typeof options.settings === 'object' &&
options.settings
) ?
settings.embedObjects(options.settings) :
settings;
// Find the prefix:
this.options.prefix = this.camera.readPrefix;
// Initialize programs hash
Object.defineProperty(this, 'nodePrograms', {
value: {}
});
Object.defineProperty(this, 'edgePrograms', {
value: {}
});
Object.defineProperty(this, 'nodeFloatArrays', {
value: {}
});
Object.defineProperty(this, 'edgeFloatArrays', {
value: {}
});
Object.defineProperty(this, 'edgeIndicesArrays', {
value: {}
});
// Initialize the DOM elements:
if (this.settings(options, 'batchEdgesDrawing')) {
this.initDOM('canvas', 'edges', true);
this.initDOM('canvas', 'nodes', true);
} else {
this.initDOM('canvas', 'scene', true);
this.contexts.nodes = this.contexts.scene;
this.contexts.edges = this.contexts.scene;
}
this.initDOM('canvas', 'labels');
this.initDOM('canvas', 'mouse');
this.contexts.hover = this.contexts.mouse;
// Initialize captors:
this.captors = [];
a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
for (i = 0, l = a.length; i < l; i++) {
fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
this.captors.push(
new fn(
this.domElements.mouse,
this.camera,
this.settings
)
);
}
// Deal with sigma events:
sigma.misc.bindEvents.call(this, this.camera.prefix);
sigma.misc.drawHovers.call(this, this.camera.prefix);
this.resize();
};
/**
* This method will generate the nodes and edges float arrays. This step is
* separated from the "render" method, because to keep WebGL efficient, since
* all the camera and middlewares are modelised as matrices and they do not
* require the float arrays to be regenerated.
*
* Basically, when the user moves the camera or applies some specific linear
* transformations, this process step will be skipped, and the "render"
* method will efficiently refresh the rendering.
*
* And when the user modifies the graph colors or positions (applying a new
* layout or filtering the colors, for instance), this "process" step will be
* required to regenerate the float arrays.
*
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.process = function() {
var a,
i,
l,
k,
type,
renderer,
graph = this.graph,
options = sigma.utils.extend(options, this.options),
defaultEdgeType = this.settings(options, 'defaultEdgeType'),
defaultNodeType = this.settings(options, 'defaultNodeType');
// Empty float arrays:
for (k in this.nodeFloatArrays)
delete this.nodeFloatArrays[k];
for (k in this.edgeFloatArrays)
delete this.edgeFloatArrays[k];
for (k in this.edgeIndicesArrays)
delete this.edgeIndicesArrays[k];
// Sort edges and nodes per types:
for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
type = a[i].type || defaultEdgeType;
k = (type && sigma.webgl.edges[type]) ? type : 'def';
if (!this.edgeFloatArrays[k])
this.edgeFloatArrays[k] = {
edges: []
};
this.edgeFloatArrays[k].edges.push(a[i]);
}
for (a = graph.nodes(), i = 0, l = a.length; i < l; i++) {
type = a[i].type || defaultNodeType;
k = (type && sigma.webgl.nodes[type]) ? type : 'def';
if (!this.nodeFloatArrays[k])
this.nodeFloatArrays[k] = {
nodes: []
};
this.nodeFloatArrays[k].nodes.push(a[i]);
}
// Push edges:
for (k in this.edgeFloatArrays) {
renderer = sigma.webgl.edges[k];
a = this.edgeFloatArrays[k].edges;
// Creating the necessary arrays
this.edgeFloatArrays[k].array = new Float32Array(
a.length * renderer.POINTS * renderer.ATTRIBUTES
);
for (i = 0, l = a.length; i < l; i++) {
// Just check that the edge and both its extremities are visible:
if (
!a[i].hidden &&
!graph.nodes(a[i].source).hidden &&
!graph.nodes(a[i].target).hidden
)
renderer.addEdge(
a[i],
graph.nodes(a[i].source),
graph.nodes(a[i].target),
this.edgeFloatArrays[k].array,
i * renderer.POINTS * renderer.ATTRIBUTES,
options.prefix,
this.settings
);
}
if (typeof renderer.computeIndices === 'function')
this.edgeIndicesArrays[k] = renderer.computeIndices(
this.edgeFloatArrays[k].array
);
}
// Push nodes:
for (k in this.nodeFloatArrays) {
renderer = sigma.webgl.nodes[k];
a = this.nodeFloatArrays[k].nodes;
// Creating the necessary arrays
this.nodeFloatArrays[k].array = new Float32Array(
a.length * renderer.POINTS * renderer.ATTRIBUTES
);
for (i = 0, l = a.length; i < l; i++) {
if (!this.nodeFloatArrays[k].array)
this.nodeFloatArrays[k].array = new Float32Array(
a.length * renderer.POINTS * renderer.ATTRIBUTES
);
// Just check that the edge and both its extremities are visible:
if (
!a[i].hidden
)
renderer.addNode(
a[i],
this.nodeFloatArrays[k].array,
i * renderer.POINTS * renderer.ATTRIBUTES,
options.prefix,
this.settings
);
}
}
return this;
};
/**
* This method renders the graph. It basically calls each program (and
* generate them if they do not exist yet) to render nodes and edges, batched
* per renderer.
*
* As in the canvas renderer, it is possible to display edges, nodes and / or
* labels in batches, to make the whole thing way more scalable.
*
* @param {?object} params Eventually an object of options.
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.render = function(params) {
var a,
i,
l,
k,
o,
program,
renderer,
self = this,
graph = this.graph,
nodesGl = this.contexts.nodes,
edgesGl = this.contexts.edges,
matrix = this.camera.getMatrix(),
options = sigma.utils.extend(params, this.options),
drawLabels = this.settings(options, 'drawLabels'),
drawEdges = this.settings(options, 'drawEdges'),
drawNodes = this.settings(options, 'drawNodes');
// Call the resize function:
this.resize(false);
// Check the 'hideEdgesOnMove' setting:
if (this.settings(options, 'hideEdgesOnMove'))
if (this.camera.isAnimated || this.camera.isMoving)
drawEdges = false;
// Clear canvases:
this.clear();
// Translate matrix to [width/2, height/2]:
matrix = sigma.utils.matrices.multiply(
matrix,
sigma.utils.matrices.translation(this.width / 2, this.height / 2)
);
// Kill running jobs:
for (k in this.jobs)
if (conrad.hasJob(k))
conrad.killJob(k);
if (drawEdges) {
if (this.settings(options, 'batchEdgesDrawing'))
(function() {
var a,
k,
i,
id,
job,
arr,
end,
start,
indices,
renderer,
batchSize,
currentProgram;
id = 'edges_' + this.conradId;
batchSize = this.settings(options, 'webglEdgesBatchSize');
a = Object.keys(this.edgeFloatArrays);
if (!a.length)
return;
i = 0;
renderer = sigma.webgl.edges[a[i]];
arr = this.edgeFloatArrays[a[i]].array;
indices = this.edgeIndicesArrays[a[i]];
start = 0;
end = Math.min(
start + batchSize * renderer.POINTS,
arr.length / renderer.ATTRIBUTES
);
job = function() {
// Check program:
if (!this.edgePrograms[a[i]])
this.edgePrograms[a[i]] = renderer.initProgram(edgesGl);
if (start < end) {
edgesGl.useProgram(this.edgePrograms[a[i]]);
renderer.render(
edgesGl,
this.edgePrograms[a[i]],
arr,
{
settings: this.settings,
matrix: matrix,
width: this.width,
height: this.height,
ratio: this.camera.ratio,
scalingRatio: this.settings(
options,
'webglOversamplingRatio'
),
start: start,
count: end - start,
indicesData: indices
}
);
}
// Catch job's end:
if (
end >= arr.length / renderer.ATTRIBUTES &&
i === a.length - 1
) {
delete this.jobs[id];
return false;
}
if (end >= arr.length / renderer.ATTRIBUTES) {
i++;
arr = this.edgeFloatArrays[a[i]].array;
renderer = sigma.webgl.edges[a[i]];
start = 0;
end = Math.min(
start + batchSize * renderer.POINTS,
arr.length / renderer.ATTRIBUTES
);
} else {
start = end;
end = Math.min(
start + batchSize * renderer.POINTS,
arr.length / renderer.ATTRIBUTES
);
}
return true;
};
this.jobs[id] = job;
conrad.addJob(id, job.bind(this));
}).call(this);
else {
for (k in this.edgeFloatArrays) {
renderer = sigma.webgl.edges[k];
// Check program:
if (!this.edgePrograms[k])
this.edgePrograms[k] = renderer.initProgram(edgesGl);
// Render
if (this.edgeFloatArrays[k]) {
edgesGl.useProgram(this.edgePrograms[k]);
renderer.render(
edgesGl,
this.edgePrograms[k],
this.edgeFloatArrays[k].array,
{
settings: this.settings,
matrix: matrix,
width: this.width,
height: this.height,
ratio: this.camera.ratio,
scalingRatio: this.settings(options, 'webglOversamplingRatio'),
indicesData: this.edgeIndicesArrays[k]
}
);
}
}
}
}
if (drawNodes) {
// Enable blending:
nodesGl.blendFunc(nodesGl.SRC_ALPHA, nodesGl.ONE_MINUS_SRC_ALPHA);
nodesGl.enable(nodesGl.BLEND);
for (k in this.nodeFloatArrays) {
renderer = sigma.webgl.nodes[k];
// Check program:
if (!this.nodePrograms[k])
this.nodePrograms[k] = renderer.initProgram(nodesGl);
// Render
if (this.nodeFloatArrays[k]) {
nodesGl.useProgram(this.nodePrograms[k]);
renderer.render(
nodesGl,
this.nodePrograms[k],
this.nodeFloatArrays[k].array,
{
settings: this.settings,
matrix: matrix,
width: this.width,
height: this.height,
ratio: this.camera.ratio,
scalingRatio: this.settings(options, 'webglOversamplingRatio')
}
);
}
}
}
if (drawLabels) {
a = this.camera.quadtree.area(
this.camera.getRectangle(this.width, this.height)
);
// Apply camera view to these nodes:
this.camera.applyView(
undefined,
undefined,
{
nodes: a,
edges: [],
width: this.width,
height: this.height
}
);
o = function(key) {
return self.settings({
prefix: self.camera.prefix
}, key);
};
for (i = 0, l = a.length; i < l; i++)
if (!a[i].hidden)
(
sigma.canvas.labels[
a[i].type ||
this.settings(options, 'defaultNodeType')
] || sigma.canvas.labels.def
)(a[i], this.contexts.labels, o);
}
this.dispatchEvent('render');
return this;
};
/**
* This method creates a DOM element of the specified type, switches its
* position to "absolute", references it to the domElements attribute, and
* finally appends it to the container.
*
* @param {string} tag The label tag.
* @param {string} id The id of the element (to store it in
* "domElements").
* @param {?boolean} webgl Will init the WebGL context if true.
*/
sigma.renderers.webgl.prototype.initDOM = function(tag, id, webgl) {
var gl,
dom = document.createElement(tag),
self = this;
dom.style.position = 'absolute';
dom.setAttribute('class', 'sigma-' + id);
this.domElements[id] = dom;
this.container.appendChild(dom);
if (tag.toLowerCase() === 'canvas') {
this.contexts[id] = dom.getContext(webgl ? 'experimental-webgl' : '2d', {
preserveDrawingBuffer: true
});
// Adding webgl context loss listeners
if (webgl) {
dom.addEventListener('webglcontextlost', function(e) {
e.preventDefault();
}, false);
dom.addEventListener('webglcontextrestored', function(e) {
self.render();
}, false);
}
}
};
/**
* This method resizes each DOM elements in the container and stores the new
* dimensions. Then, it renders the graph.
*
* @param {?number} width The new width of the container.
* @param {?number} height The new height of the container.
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.resize = function(w, h) {
var k,
oldWidth = this.width,
oldHeight = this.height,
pixelRatio = sigma.utils.getPixelRatio();
if (w !== undefined && h !== undefined) {
this.width = w;
this.height = h;
} else {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
w = this.width;
h = this.height;
}
if (oldWidth !== this.width || oldHeight !== this.height) {
for (k in this.domElements) {
this.domElements[k].style.width = w + 'px';
this.domElements[k].style.height = h + 'px';
if (this.domElements[k].tagName.toLowerCase() === 'canvas') {
// If simple 2D canvas:
if (this.contexts[k] && this.contexts[k].scale) {
this.domElements[k].setAttribute('width', (w * pixelRatio) + 'px');
this.domElements[k].setAttribute('height', (h * pixelRatio) + 'px');
if (pixelRatio !== 1)
this.contexts[k].scale(pixelRatio, pixelRatio);
} else {
this.domElements[k].setAttribute(
'width',
(w * this.settings('webglOversamplingRatio')) + 'px'
);
this.domElements[k].setAttribute(
'height',
(h * this.settings('webglOversamplingRatio')) + 'px'
);
}
}
}
}
// Scale:
for (k in this.contexts)
if (this.contexts[k] && this.contexts[k].viewport)
this.contexts[k].viewport(
0,
0,
this.width * this.settings('webglOversamplingRatio'),
this.height * this.settings('webglOversamplingRatio')
);
return this;
};
/**
* This method clears each canvas.
*
* @return {sigma.renderers.webgl} Returns the instance itself.
*/
sigma.renderers.webgl.prototype.clear = function() {
this.contexts.labels.clearRect(0, 0, this.width, this.height);
this.contexts.nodes.clear(this.contexts.nodes.COLOR_BUFFER_BIT);
this.contexts.edges.clear(this.contexts.edges.COLOR_BUFFER_BIT);
return this;
};
/**
* This method kills contexts and other attributes.
*/
sigma.renderers.webgl.prototype.kill = function() {
var k,
captor;
// Kill captors:
while ((captor = this.captors.pop()))
captor.kill();
delete this.captors;
// Kill contexts:
for (k in this.domElements) {
this.domElements[k].parentNode.removeChild(this.domElements[k]);
delete this.domElements[k];
delete this.contexts[k];
}
delete this.domElements;
delete this.contexts;
};
/**
* The object "sigma.webgl.nodes" contains the different WebGL node
* renderers. The default one draw nodes as discs. Here are the attributes
* any node renderer must have:
*
* {number} POINTS The number of points required to draw a node.
* {number} ATTRIBUTES The number of attributes needed to draw one point.
* {function} addNode A function that adds a node to the data stack that
* will be given to the buffer. Here is the arguments:
* > {object} node
* > {number} index The node index in the
* nodes array.
* > {Float32Array} data The stack.
* > {object} options Some options.
* {function} render The function that will effectively render the nodes
* into the buffer.
* > {WebGLRenderingContext} gl
* > {WebGLProgram} program
* > {Float32Array} data The stack to give to the
* buffer.
* > {object} params An object containing some
* options, like width,
* height, the camera ratio.
* {function} initProgram The function that will initiate the program, with
* the relevant shaders and parameters. It must return
* the newly created program.
*
* Check sigma.webgl.nodes.def or sigma.webgl.nodes.fast to see how it
* works more precisely.
*/
sigma.utils.pkg('sigma.webgl.nodes');
/**
* The object "sigma.webgl.edges" contains the different WebGL edge
* renderers. The default one draw edges as direct lines. Here are the
* attributes any edge renderer must have:
*
* {number} POINTS The number of points required to draw an edge.
* {number} ATTRIBUTES The number of attributes needed to draw one point.
* {function} addEdge A function that adds an edge to the data stack that
* will be given to the buffer. Here is the arguments:
* > {object} edge
* > {object} source
* > {object} target
* > {Float32Array} data The stack.
* > {object} options Some options.
* {function} render The function that will effectively render the edges
* into the buffer.
* > {WebGLRenderingContext} gl
* > {WebGLProgram} program
* > {Float32Array} data The stack to give to the
* buffer.
* > {object} params An object containing some
* options, like width,
* height, the camera ratio.
* {function} initProgram The function that will initiate the program, with
* the relevant shaders and parameters. It must return
* the newly created program.
*
* Check sigma.webgl.edges.def or sigma.webgl.edges.fast to see how it
* works more precisely.
*/
sigma.utils.pkg('sigma.webgl.edges');
/**
* The object "sigma.canvas.labels" contains the different
* label renderers for the WebGL renderer. Since displaying texts in WebGL is
* definitely painful and since there a way less labels to display than nodes
* or edges, the default renderer simply renders them in a canvas.
*
* A labels renderer is a simple function, taking as arguments the related
* node, the renderer and a settings function.
*/
sigma.utils.pkg('sigma.canvas.labels');
}).call(this);