;(function(undefined) {
|
|
'use strict';
|
|
|
|
if (typeof sigma === 'undefined')
|
|
throw 'sigma is not declared';
|
|
|
|
if (typeof conrad === 'undefined')
|
|
throw 'conrad 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.canvas = function(graph, camera, settings, options) {
|
|
if (typeof options !== 'object')
|
|
throw 'sigma.renderers.canvas: Wrong arguments.';
|
|
|
|
if (!(options.container instanceof HTMLElement))
|
|
throw 'Container not found.';
|
|
|
|
var k,
|
|
i,
|
|
l,
|
|
a,
|
|
fn,
|
|
self = this;
|
|
|
|
sigma.classes.dispatcher.extend(this);
|
|
|
|
// Initialize main attributes:
|
|
Object.defineProperty(this, 'conradId', {
|
|
value: sigma.utils.id()
|
|
});
|
|
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;
|
|
|
|
// Node indexes:
|
|
this.nodesOnScreen = [];
|
|
this.edgesOnScreen = [];
|
|
|
|
// Conrad related attributes:
|
|
this.jobs = {};
|
|
|
|
// Find the prefix:
|
|
this.options.prefix = 'renderer' + this.conradId + ':';
|
|
|
|
// Initialize the DOM elements:
|
|
if (
|
|
!this.settings('batchEdgesDrawing')
|
|
) {
|
|
this.initDOM('canvas', 'scene');
|
|
this.contexts.edges = this.contexts.scene;
|
|
this.contexts.nodes = this.contexts.scene;
|
|
this.contexts.labels = this.contexts.scene;
|
|
} else {
|
|
this.initDOM('canvas', 'edges');
|
|
this.initDOM('canvas', 'scene');
|
|
this.contexts.nodes = this.contexts.scene;
|
|
this.contexts.labels = this.contexts.scene;
|
|
}
|
|
|
|
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.options.prefix);
|
|
sigma.misc.drawHovers.call(this, this.options.prefix);
|
|
|
|
this.resize(false);
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This method renders the graph on the canvases.
|
|
*
|
|
* @param {?object} options Eventually an object of options.
|
|
* @return {sigma.renderers.canvas} Returns the instance itself.
|
|
*/
|
|
sigma.renderers.canvas.prototype.render = function(options) {
|
|
options = options || {};
|
|
|
|
var a,
|
|
i,
|
|
k,
|
|
l,
|
|
o,
|
|
id,
|
|
end,
|
|
job,
|
|
start,
|
|
edges,
|
|
renderers,
|
|
rendererType,
|
|
batchSize,
|
|
tempGCO,
|
|
index = {},
|
|
graph = this.graph,
|
|
nodes = this.graph.nodes,
|
|
prefix = this.options.prefix || '',
|
|
drawEdges = this.settings(options, 'drawEdges'),
|
|
drawNodes = this.settings(options, 'drawNodes'),
|
|
drawLabels = this.settings(options, 'drawLabels'),
|
|
drawEdgeLabels = this.settings(options, 'drawEdgeLabels'),
|
|
embedSettings = this.settings.embedObjects(options, {
|
|
prefix: this.options.prefix
|
|
});
|
|
|
|
// 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;
|
|
|
|
// Apply the camera's view:
|
|
this.camera.applyView(
|
|
undefined,
|
|
this.options.prefix,
|
|
{
|
|
width: this.width,
|
|
height: this.height
|
|
}
|
|
);
|
|
|
|
// Clear canvases:
|
|
this.clear();
|
|
|
|
// Kill running jobs:
|
|
for (k in this.jobs)
|
|
if (conrad.hasJob(k))
|
|
conrad.killJob(k);
|
|
|
|
// Find which nodes are on screen:
|
|
this.edgesOnScreen = [];
|
|
this.nodesOnScreen = this.camera.quadtree.area(
|
|
this.camera.getRectangle(this.width, this.height)
|
|
);
|
|
|
|
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
|
|
index[a[i].id] = a[i];
|
|
|
|
// Draw edges:
|
|
// - If settings('batchEdgesDrawing') is true, the edges are displayed per
|
|
// batches. If not, they are drawn in one frame.
|
|
if (drawEdges) {
|
|
// First, let's identify which edges to draw. To do this, we just keep
|
|
// every edges that have at least one extremity displayed according to
|
|
// the quadtree and the "hidden" attribute. We also do not keep hidden
|
|
// edges.
|
|
for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
|
|
o = a[i];
|
|
if (
|
|
(index[o.source] || index[o.target]) &&
|
|
(!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden)
|
|
)
|
|
this.edgesOnScreen.push(o);
|
|
}
|
|
|
|
// If the "batchEdgesDrawing" settings is true, edges are batched:
|
|
if (this.settings(options, 'batchEdgesDrawing')) {
|
|
id = 'edges_' + this.conradId;
|
|
batchSize = embedSettings('canvasEdgesBatchSize');
|
|
|
|
edges = this.edgesOnScreen;
|
|
l = edges.length;
|
|
|
|
start = 0;
|
|
end = Math.min(edges.length, start + batchSize);
|
|
|
|
job = function() {
|
|
tempGCO = this.contexts.edges.globalCompositeOperation;
|
|
this.contexts.edges.globalCompositeOperation = 'destination-over';
|
|
|
|
renderers = sigma.canvas.edges;
|
|
for (i = start; i < end; i++) {
|
|
o = edges[i];
|
|
(renderers[
|
|
o.type || this.settings(options, 'defaultEdgeType')
|
|
] || renderers.def)(
|
|
o,
|
|
graph.nodes(o.source),
|
|
graph.nodes(o.target),
|
|
this.contexts.edges,
|
|
embedSettings
|
|
);
|
|
}
|
|
|
|
// Draw edge labels:
|
|
if (drawEdgeLabels) {
|
|
renderers = sigma.canvas.edges.labels;
|
|
for (i = start; i < end; i++) {
|
|
o = edges[i];
|
|
if (!o.hidden)
|
|
(renderers[
|
|
o.type || this.settings(options, 'defaultEdgeType')
|
|
] || renderers.def)(
|
|
o,
|
|
graph.nodes(o.source),
|
|
graph.nodes(o.target),
|
|
this.contexts.labels,
|
|
embedSettings
|
|
);
|
|
}
|
|
}
|
|
|
|
// Restore original globalCompositeOperation:
|
|
this.contexts.edges.globalCompositeOperation = tempGCO;
|
|
|
|
// Catch job's end:
|
|
if (end === edges.length) {
|
|
delete this.jobs[id];
|
|
return false;
|
|
}
|
|
|
|
start = end + 1;
|
|
end = Math.min(edges.length, start + batchSize);
|
|
return true;
|
|
};
|
|
|
|
this.jobs[id] = job;
|
|
conrad.addJob(id, job.bind(this));
|
|
|
|
// If not, they are drawn in one frame:
|
|
} else {
|
|
renderers = sigma.canvas.edges;
|
|
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
|
|
o = a[i];
|
|
(renderers[
|
|
o.type || this.settings(options, 'defaultEdgeType')
|
|
] || renderers.def)(
|
|
o,
|
|
graph.nodes(o.source),
|
|
graph.nodes(o.target),
|
|
this.contexts.edges,
|
|
embedSettings
|
|
);
|
|
}
|
|
|
|
// Draw edge labels:
|
|
// - No batching
|
|
if (drawEdgeLabels) {
|
|
renderers = sigma.canvas.edges.labels;
|
|
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++)
|
|
if (!a[i].hidden)
|
|
(renderers[
|
|
a[i].type || this.settings(options, 'defaultEdgeType')
|
|
] || renderers.def)(
|
|
a[i],
|
|
graph.nodes(a[i].source),
|
|
graph.nodes(a[i].target),
|
|
this.contexts.labels,
|
|
embedSettings
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw nodes:
|
|
// - No batching
|
|
if (drawNodes) {
|
|
renderers = sigma.canvas.nodes;
|
|
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
|
|
if (!a[i].hidden)
|
|
(renderers[
|
|
a[i].type || this.settings(options, 'defaultNodeType')
|
|
] || renderers.def)(
|
|
a[i],
|
|
this.contexts.nodes,
|
|
embedSettings
|
|
);
|
|
}
|
|
|
|
// Draw labels:
|
|
// - No batching
|
|
if (drawLabels) {
|
|
renderers = sigma.canvas.labels;
|
|
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
|
|
if (!a[i].hidden)
|
|
(renderers[
|
|
a[i].type || this.settings(options, 'defaultNodeType')
|
|
] || renderers.def)(
|
|
a[i],
|
|
this.contexts.labels,
|
|
embedSettings
|
|
);
|
|
}
|
|
|
|
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").
|
|
*/
|
|
sigma.renderers.canvas.prototype.initDOM = function(tag, id) {
|
|
var dom = document.createElement(tag);
|
|
|
|
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('2d');
|
|
};
|
|
|
|
/**
|
|
* 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.canvas} Returns the instance itself.
|
|
*/
|
|
sigma.renderers.canvas.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') {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* This method clears each canvas.
|
|
*
|
|
* @return {sigma.renderers.canvas} Returns the instance itself.
|
|
*/
|
|
sigma.renderers.canvas.prototype.clear = function() {
|
|
for (var k in this.contexts) {
|
|
this.contexts[k].clearRect(0, 0, this.width, this.height);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* This method kills contexts and other attributes.
|
|
*/
|
|
sigma.renderers.canvas.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 labels, nodes and edges renderers are stored in the three following
|
|
* objects. When an element is drawn, its type will be checked and if a
|
|
* renderer with the same name exists, it will be used. If not found, the
|
|
* default renderer will be used instead.
|
|
*
|
|
* They are stored in different files, in the "./canvas" folder.
|
|
*/
|
|
sigma.utils.pkg('sigma.canvas.nodes');
|
|
sigma.utils.pkg('sigma.canvas.edges');
|
|
sigma.utils.pkg('sigma.canvas.labels');
|
|
}).call(this);
|